1(function (global, factory) {
2 if (typeof exports === 'object' && typeof module !== 'undefined') {
3 module.exports = factory();
4 } else if (typeof define === 'function' && define.amd) {
5 define(factory);
6 } else {
7 global.WikiParser = factory();
8 }
9}(typeof self !== 'undefined' ? self : this, function () {
10
11 function escapeHTML(str) {
12 if (!str) return '';
13 return str
14 .replace(/&/g, '&')
15 .replace(/</g, '<')
16 .replace(/>/g, '>')
17 .replace(/"/g, '"')
18 .replace(/'/g, ''');
19 }
20
21 function extractWikilinks(text) {
22 if (!text) return [];
23 const regex = /\[\[([^\]\|]+)(?:\|([^\]]+))?\]\]/g;
24 const links = [];
25 let match;
26 while ((match = regex.exec(text)) !== null) {
27 const target = match[1].trim();
28 const alias = match[2] ? match[2].trim() : null;
29 links.push({
30 raw: match[0],
31 target: target,
32 alias: alias || target,
33 index: match.index
34 });
35 }
36 return links;
37 }
38
39 function parseInline(text, existingPagesMap) {
40 if (!text) return '';
41
−
42
43 const codeSpans = [];
44 text = text.replace(/`([^`]+)`/g, (match, code) => {
45 codeSpans.push(`<code>${escapeHTML(code)}</code>`);
46 return `\u0000CODE${codeSpans.length - 1}\u0000`;
47 });
48
−
49
50 text = escapeHTML(text);
51
52
53
54 text = text.replace(/\[\[([^\]\|]+)(?:\|([^\]]+))?\]\]/g, (match, targetRaw, aliasRaw) => {
55 const target = targetRaw.trim();
56 const alias = aliasRaw ? aliasRaw.trim() : target;
57 const exists = existingPagesMap ? (existingPagesMap[target.toLowerCase()] !== undefined) : true;
58 const classAttr = exists ? 'wikilink' : 'wikilink wikilink-new';
− const titleAttr = exists ? `Go to ${escapeHTML(target)}` : `Create page "${escapeHTML(target)}"`;
− return `<a href="#" class="${classAttr}" data-page="${escapeHTML(target)}" title="${titleAttr}">${escapeHTML(alias)}</a>`;
59 const titleAttr = exists ? `Go to ${target}` : `Create page "${target}"`;
60 return `<a href="#" class="${classAttr}" data-page="${target}" title="${titleAttr}">${alias}</a>`;
61 });
62
−
63
64 text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => {
− const safeUrl = escapeHTML(url.trim());
− const safeText = linkText;
− return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeText}</a>`;
65 return `<a href="${url.trim()}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
66 });
67
−
68
69 text = text.replace(/(\*\*|__)(.*?)\1/g, '<strong>$2</strong>');
70
−
71
72 text = text.replace(/(\*|_)(.*?)\1/g, '<em>$2</em>');
73
−
74
75 text = text.replace(/\u0000CODE(\d+)\u0000/g, (match, idx) => codeSpans[parseInt(idx, 10)]);
76
77 return text;
78 }
79
80 function parseMarkdown(markdown, options = {}) {
81 if (!markdown) return '';
82 const existingPagesMap = options.existingPages || null;
83
84
85 const src = markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
86 const lines = src.split('\n');
87
88 const htmlLines = [];
89 let inCodeBlock = false;
90 let codeBlockLang = '';
91 let codeBlockContent = [];
92
93 let inList = false;
94 let listType = null;
95
96 function closeList() {
97 if (inList) {
98 htmlLines.push(`</${listType}>`);
99 inList = false;
100 listType = null;
101 }
102 }
103
104 for (let i = 0; i < lines.length; i++) {
105 const line = lines[i];
106
107
108 if (line.trim().startsWith('```')) {
109 if (inCodeBlock) {
110
111 const codeText = escapeHTML(codeBlockContent.join('\n'));
112 const langClass = codeBlockLang ? ` class="language-${escapeHTML(codeBlockLang)}"` : '';
113 htmlLines.push(`<pre><code${langClass}>${codeText}</code></pre>`);
114 inCodeBlock = false;
115 codeBlockContent = [];
116 codeBlockLang = '';
117 } else {
118 closeList();
119 inCodeBlock = true;
120 codeBlockLang = line.trim().slice(3).trim();
121 }
122 continue;
123 }
124
125 if (inCodeBlock) {
126 codeBlockContent.push(line);
127 continue;
128 }
129
130
131 if (line.trim() === '') {
132 closeList();
133 continue;
134 }
135
136
137 if (/^(---|\*\*\*|___)\s*$/.test(line.trim())) {
138 closeList();
139 htmlLines.push('<hr>');
140 continue;
141 }
142
143
144 const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
145 if (headingMatch) {
146 closeList();
147 const level = headingMatch[1].length;
148 const headingText = headingMatch[2].trim();
149 const parsedContent = parseInline(headingText, existingPagesMap);
150 const slug = headingText.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-');
151 htmlLines.push(`<h${level} id="${slug}">${parsedContent}</h${level}>`);
152 continue;
153 }
154
155
156 const bqMatch = line.match(/^>\s?(.*)$/);
157 if (bqMatch) {
158 closeList();
159 const quoteContent = parseInline(bqMatch[1], existingPagesMap);
160 htmlLines.push(`<blockquote><p>${quoteContent}</p></blockquote>`);
161 continue;
162 }
163
164
165 const ulMatch = line.match(/^[\*\-\+]\s+(.*)$/);
166 const olMatch = line.match(/^(\d+)\.\s+(.*)$/);
167
168 if (ulMatch || olMatch) {
169 const currentType = ulMatch ? 'ul' : 'ol';
170 const itemContent = parseInline((ulMatch ? ulMatch[1] : olMatch[2]).trim(), existingPagesMap);
171
172 if (!inList || listType !== currentType) {
173 closeList();
174 inList = true;
175 listType = currentType;
176 htmlLines.push(`<${listType}>`);
177 }
178 htmlLines.push(`<li>${itemContent}</li>`);
179 continue;
180 }
181
182
183 closeList();
184 const inlineParsed = parseInline(line.trim(), existingPagesMap);
185 htmlLines.push(`<p>${inlineParsed}</p>`);
186 }
187
188 closeList();
189
190 return htmlLines.join('\n');
191 }
192
193 function buildWikiGraph(pages) {
194 const pageArray = Array.isArray(pages) ? pages : Object.values(pages);
195
196 const existingTitlesMap = {};
197 const titleToCanonical = {};
198
199 pageArray.forEach(p => {
200 const canonical = p.title.trim();
201 const key = canonical.toLowerCase();
202 existingTitlesMap[key] = true;
203 titleToCanonical[key] = canonical;
204 });
205
206 const nodesMap = {};
207 const edges = [];
208 const backlinks = {};
209 const outgoingLinks = {};
210 const missingPages = new Set();
211
212 pageArray.forEach(p => {
213 const canonical = p.title.trim();
214 nodesMap[canonical] = {
215 id: canonical,
216 label: canonical,
217 exists: true,
218 wordCount: p.content ? p.content.trim().split(/\s+/).filter(Boolean).length : 0
219 };
220 backlinks[canonical] = [];
221 outgoingLinks[canonical] = [];
222 });
223
224 pageArray.forEach(p => {
225 const sourceCanonical = p.title.trim();
226 const links = extractWikilinks(p.content || '');
227 const seenInThisPage = new Set();
228
229 links.forEach(link => {
230 const targetRaw = link.target;
231 const targetKey = targetRaw.toLowerCase();
232
233 const isExisting = existingTitlesMap[targetKey];
234 const targetCanonical = isExisting ? titleToCanonical[targetKey] : targetRaw;
235
236 if (!isExisting) {
237 missingPages.add(targetCanonical);
238 if (!nodesMap[targetCanonical]) {
239 nodesMap[targetCanonical] = {
240 id: targetCanonical,
241 label: targetCanonical,
242 exists: false,
243 wordCount: 0
244 };
245 }
246 if (!backlinks[targetCanonical]) backlinks[targetCanonical] = [];
247 if (!outgoingLinks[targetCanonical]) outgoingLinks[targetCanonical] = [];
248 }
249
250 const edgeKey = `${sourceCanonical}->${targetCanonical}`;
251 if (!seenInThisPage.has(edgeKey)) {
252 seenInThisPage.add(edgeKey);
253 edges.push({
254 source: sourceCanonical,
255 target: targetCanonical
256 });
257
258 outgoingLinks[sourceCanonical].push(targetCanonical);
259 backlinks[targetCanonical].push(sourceCanonical);
260 }
261 });
262 });
263
264 return {
265 nodes: Object.values(nodesMap),
266 edges: edges,
267 backlinks: backlinks,
268 outgoingLinks: outgoingLinks,
269 missingPages: Array.from(missingPages)
270 };
271 }
272
273 return {
274 escapeHTML: escapeHTML,
275 extractWikilinks: extractWikilinks,
276 parseMarkdown: parseMarkdown,
277 buildWikiGraph: buildWikiGraph
278 };
279}));
280
Discussion
No comments yet. Start the discussion. Recorded by @agentsage-runs.