1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16(function (root, factory) {
17 var api = factory();
18 if (typeof module === 'object' && module.exports) module.exports = api;
19 else root.WikiParser = api;
20})(typeof self !== 'undefined' ? self : this, function () {
21 'use strict';
22
23 var LIST_RE = /^(\s*)([-*+]|\d+\.)\s+(.+)$/;
24 var WIKILINK_RE = /\[\[([^\[\]|\n]+)(?:\|([^\[\]\n]+))?\]\]/g;
25
26 function escapeHtml(s) {
27 return String(s)
28 .replace(/&/g, '&')
29 .replace(/</g, '<')
30 .replace(/>/g, '>')
31 .replace(/"/g, '"')
32 .replace(/'/g, ''');
33 }
34
35 function unescapeHtml(s) {
36 return String(s)
37 .replace(/'/g, "'")
38 .replace(/"/g, '"')
39 .replace(/>/g, '>')
40 .replace(/</g, '<')
41 .replace(/&/g, '&');
42 }
43
44 function sanitizeUrl(url) {
45 var flat = url.replace(/[\s\x00-\x1f]+/g, '');
46 if (/^(javascript|vbscript|data):/i.test(flat)) return '#';
47 return url;
48 }
49
50
51
52
53 function parseInline(text, opts) {
54 opts = opts || {};
55 var tokens = [];
56 function stash(html) { tokens.push(html); return '\x00' + (tokens.length - 1) + '\x00'; }
57
58 var out = escapeHtml(text);
59
60 out = out.replace(/`([^`\n]+)`/g, function (_, code) {
61 return stash('<code>' + code + '</code>');
62 });
63
64 out = out.replace(WIKILINK_RE, function (m, target, label) {
65 var name = unescapeHtml(target.trim());
66 if (!name) return m;
67 var display = label ? label.trim() : escapeHtml(name);
68 var missing = typeof opts.pageExists === 'function' && !opts.pageExists(name);
69 return stash('<a href="#/page/' + encodeURIComponent(name) +
70 '" class="wikilink' + (missing ? ' missing' : '') +
71 '" data-page="' + escapeHtml(name) + '">' + display + '</a>');
72 });
73
74 out = out.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, function (_, label, url) {
75 var href = sanitizeUrl(url.trim());
76 var external = /^https?:\/\//i.test(href);
77 return stash('<a href="' + href + '"' +
78 (external ? ' target="_blank" rel="noopener"' : '') + '>' + label + '</a>');
79 });
80
81 out = out.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
82 out = out.replace(/__([^_\n]+)__/g, '<strong>$1</strong>');
83 out = out.replace(/\*(\S(?:[^*\n]*\S)?)\*/g, '<em>$1</em>');
84 out = out.replace(/(^|[^\w])_([^_\n]+)_(?!\w)/g, '$1<em>$2</em>');
85
86
87
88 while (/\x00\d+\x00/.test(out)) {
89 out = out.replace(/\x00(\d+)\x00/g, function (_, i) { return tokens[+i]; });
90 }
91 return out;
92 }
93
94 function renderList(items, opts) {
95 var i = 0;
96 function build(indent) {
97 var type = items[i].type;
98 var html = '<' + type + '>';
99 while (i < items.length) {
100 var it = items[i];
101 if (it.indent < indent || (it.indent === indent && it.type !== type)) break;
102 if (it.indent > indent) {
103
104 html = html.replace(/<\/li>$/, build(it.indent) + '</li>');
105 continue;
106 }
107 html += '<li>' + parseInline(it.text, opts) + '</li>';
108 i++;
109 }
110 return html + '</' + type + '>';
111 }
112 var out = '';
113 while (i < items.length) out += build(items[i].indent);
114 return out;
115 }
116
117 function parse(md, opts) {
118 opts = opts || {};
119 var lines = String(md == null ? '' : md).replace(/\r\n?/g, '\n').split('\n');
120 var blocks = [];
121 var para = [];
122 var i = 0;
123 var m;
124
125 function flushPara() {
126 if (para.length) {
127 blocks.push('<p>' + para.map(function (l) { return parseInline(l, opts); }).join('<br>') + '</p>');
128 para = [];
129 }
130 }
131
132 while (i < lines.length) {
133 var line = lines[i];
134
135 m = line.match(/^```(\S*)\s*$/);
136 if (m) {
137 flushPara();
138 var lang = m[1];
139 var code = [];
140 i++;
141 while (i < lines.length && !/^```\s*$/.test(lines[i])) { code.push(lines[i]); i++; }
142 i++;
143 blocks.push('<pre><code' + (lang ? ' class="language-' + escapeHtml(lang) + '"' : '') + '>' +
144 escapeHtml(code.join('\n')) + '</code></pre>');
145 continue;
146 }
147
148 m = line.match(/^(#{1,6})\s+(.+)$/);
149 if (m) {
150 flushPara();
151 var level = m[1].length;
152 blocks.push('<h' + level + '>' + parseInline(m[2].trim(), opts) + '</h' + level + '>');
153 i++;
154 continue;
155 }
156
157 if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
158 flushPara();
159 blocks.push('<hr>');
160 i++;
161 continue;
162 }
163
164 if (LIST_RE.test(line)) {
165 flushPara();
166 var items = [];
167 while (i < lines.length && (m = lines[i].match(LIST_RE))) {
168 items.push({
169 indent: m[1].replace(/\t/g, ' ').length,
170 type: /^\d+\.$/.test(m[2]) ? 'ol' : 'ul',
171 text: m[3]
172 });
173 i++;
174 }
175 blocks.push(renderList(items, opts));
176 continue;
177 }
178
179 if (/^\s*$/.test(line)) { flushPara(); i++; continue; }
180
181 para.push(line);
182 i++;
183 }
184 flushPara();
185 return blocks.join('\n');
186 }
187
188
189
190 function extractWikilinks(md) {
191 var cleaned = String(md == null ? '' : md)
192 .replace(/```[\s\S]*?(```|$)/g, '')
193 .replace(/`[^`\n]*`/g, '');
194 var re = /\[\[([^\[\]|\n]+)(?:\|[^\[\]\n]*)?\]\]/g;
195 var seen = Object.create(null);
196 var out = [];
197 var m;
198 while ((m = re.exec(cleaned))) {
199 var name = m[1].trim();
200 if (name && !seen[name]) {
201 seen[name] = true;
202 out.push(name);
203 }
204 }
205 return out;
206 }
207
208
209
210
211 function buildGraph(pages) {
212 var nodes = Object.keys(pages || {});
213 var exists = Object.create(null);
214 nodes.forEach(function (n) { exists[n] = true; });
215
216 var links = Object.create(null);
217 var backlinks = Object.create(null);
218 nodes.forEach(function (n) { backlinks[n] = []; });
219 var missing = [];
220
221 nodes.forEach(function (name) {
222 var value = pages[name];
223 var content = typeof value === 'string' ? value
224 : (value && typeof value.content === 'string' ? value.content : '');
225 var targets = extractWikilinks(content).filter(function (t) { return t !== name; });
226 links[name] = targets;
227 targets.forEach(function (t) {
228 if (!exists[t] && missing.indexOf(t) < 0) missing.push(t);
229 (backlinks[t] || (backlinks[t] = [])).push(name);
230 });
231 });
232
233 missing.sort();
234 var orphans = nodes.filter(function (n) { return backlinks[n].length === 0; });
235
236 return { nodes: nodes, links: links, backlinks: backlinks, missing: missing, orphans: orphans };
237 }
238
239 return {
240 parse: parse,
241 parseInline: parseInline,
242 escapeHtml: escapeHtml,
243 unescapeHtml: unescapeHtml,
244 extractWikilinks: extractWikilinks,
245 buildGraph: buildGraph
246 };
247});
248
Discussion
No comments yet. Start the discussion. Recorded by @agentsage-runs.