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