1
2
3(function () {
4 'use strict';
5 var IS_NODE = typeof window === 'undefined';
6 var P = IS_NODE ? require('./parser.js') : window.WikiParser;
7 var parse = P.parse;
8
9 var tests = [];
10 function test(name, fn) { tests.push({ name: name, fn: fn }); }
11 function assert(cond, msg) { if (!cond) throw new Error(msg || 'assertion failed'); }
12 function assertEq(actual, expected, msg) {
13 if (actual !== expected) {
14 throw new Error((msg || 'values differ') +
15 '\n expected: ' + JSON.stringify(expected) +
16 '\n actual: ' + JSON.stringify(actual));
17 }
18 }
19 function assertDeep(actual, expected, msg) {
20 assertEq(JSON.stringify(actual), JSON.stringify(expected), msg);
21 }
22
23
24
25 test('headings h1 through h6', function () {
26 for (var n = 1; n <= 6; n++) {
27 assertEq(parse('#'.repeat(n) + ' Title'), '<h' + n + '>Title</h' + n + '>', 'level ' + n);
28 }
29 });
30
31 test('seven hashes is not a heading', function () {
32 assertEq(parse('####### nope'), '<p>####### nope</p>');
33 });
34
35 test('inline formatting works inside headings', function () {
36 assertEq(parse('### A **b**'), '<h3>A <strong>b</strong></h3>');
37 });
38
39 test('blank line separates paragraphs', function () {
40 assertEq(parse('a\n\nb'), '<p>a</p>\n<p>b</p>');
41 });
42
43 test('single newline becomes a line break', function () {
44 assertEq(parse('line one\nline two'), '<p>line one<br>line two</p>');
45 });
46
47 test('horizontal rule from three dashes', function () {
48 assertEq(parse('---'), '<hr>');
49 });
50
51 test('fenced code block with language', function () {
52 assertEq(parse('```js\nvar a = 1 < 2;\n```'),
53 '<pre><code class="language-js">var a = 1 < 2;</code></pre>');
54 });
55
56 test('code block content is not parsed as markdown', function () {
57 assertEq(parse('```\n# not a heading\n```'), '<pre><code># not a heading</code></pre>');
58 });
59
60 test('unterminated code block runs to end of input', function () {
61 assertEq(parse('```\nabc'), '<pre><code>abc</code></pre>');
62 });
63
64 test('code block preserves blank lines', function () {
65 assertEq(parse('```\na\n\nb\n```'), '<pre><code>a\n\nb</code></pre>');
66 });
67
68
69
70 test('bold with asterisks', function () {
71 assertEq(parse('**bold** text'), '<p><strong>bold</strong> text</p>');
72 });
73
74 test('bold with underscores', function () {
75 assertEq(parse('__bold__ text'), '<p><strong>bold</strong> text</p>');
76 });
77
78 test('italic with asterisks', function () {
79 assertEq(parse('*it* text'), '<p><em>it</em> text</p>');
80 });
81
82 test('italic with underscores', function () {
83 assertEq(parse('so _it_ goes'), '<p>so <em>it</em> goes</p>');
84 });
85
86 test('underscores inside words are left alone', function () {
87 assertEq(parse('snake_case_name stays'), '<p>snake_case_name stays</p>');
88 });
89
90 test('spaced asterisks are not italics', function () {
91 assertEq(parse('2 * 3 * 4'), '<p>2 * 3 * 4</p>');
92 });
93
94 test('triple asterisks give bold italic', function () {
95 assertEq(parse('***both***'), '<p><em><strong>both</strong></em></p>');
96 });
97
98 test('inline code protects markdown inside it', function () {
99 assertEq(parse('a `**x**` b'), '<p>a <code>**x**</code> b</p>');
100 });
101
102 test('inline code escapes HTML', function () {
103 assertEq(parse('`<div>`'), '<p><code><div></code></p>');
104 });
105
106 test('raw HTML in paragraphs is escaped', function () {
107 assertEq(parse('<script>alert(1)</script>'),
108 '<p><script>alert(1)</script></p>');
109 });
110
111 test('external links open in a new tab', function () {
112 assertEq(parse('[Anthropic](https://www.anthropic.com)'),
113 '<p><a href="https://www.anthropic.com" target="_blank" rel="noopener">Anthropic</a></p>');
114 });
115
116 test('internal links get no target attribute', function () {
117 assertEq(parse('[t](#/page/X)'), '<p><a href="#/page/X">t</a></p>');
118 });
119
120 test('javascript: URLs are neutralized', function () {
121 assertEq(parse('[x](javascript:doEvil)'), '<p><a href="#">x</a></p>');
122 });
123
124 test('escapeHtml escapes all special characters', function () {
125 assertEq(P.escapeHtml('<a href="x">&\'y'), '<a href="x">&'y');
126 });
127
128
129
130 test('basic wikilink', function () {
131 assertEq(parse('[[Home]]'),
132 '<p><a href="#/page/Home" class="wikilink" data-page="Home">Home</a></p>');
133 });
134
135 test('wikilink to missing page gets missing class', function () {
136 assertEq(parse('[[Nowhere]]', { pageExists: function () { return false; } }),
137 '<p><a href="#/page/Nowhere" class="wikilink missing" data-page="Nowhere">Nowhere</a></p>');
138 });
139
140 test('wikilink to existing page has no missing class', function () {
141 assertEq(parse('[[Home]]', { pageExists: function () { return true; } }),
142 '<p><a href="#/page/Home" class="wikilink" data-page="Home">Home</a></p>');
143 });
144
145 test('wikilink with label', function () {
146 assertEq(parse('[[Target|My label]]'),
147 '<p><a href="#/page/Target" class="wikilink" data-page="Target">My label</a></p>');
148 });
149
150 test('wikilink with spaces is URL-encoded', function () {
151 assertEq(parse('[[Foo Bar]]'),
152 '<p><a href="#/page/Foo%20Bar" class="wikilink" data-page="Foo Bar">Foo Bar</a></p>');
153 });
154
155 test('wikilink target is trimmed', function () {
156 assertEq(parse('[[ Foo ]]'),
157 '<p><a href="#/page/Foo" class="wikilink" data-page="Foo">Foo</a></p>');
158 });
159
160 test('wikilink with ampersand escapes and encodes correctly', function () {
161 assertEq(parse('[[A & B]]'),
162 '<p><a href="#/page/A%20%26%20B" class="wikilink" data-page="A & B">A & B</a></p>');
163 });
164
165 test('wikilink inside inline code is not linked', function () {
166 assertEq(parse('`[[NotALink]]`'), '<p><code>[[NotALink]]</code></p>');
167 });
168
169 test('wikilink inside fenced code is not linked', function () {
170 assertEq(parse('```\n[[X]]\n```'), '<pre><code>[[X]]</code></pre>');
171 });
172
173
174
175 test('unordered list', function () {
176 assertEq(parse('- a\n- b'), '<ul><li>a</li><li>b</li></ul>');
177 });
178
179 test('ordered list', function () {
180 assertEq(parse('1. a\n2. b'), '<ol><li>a</li><li>b</li></ol>');
181 });
182
183 test('nested list', function () {
184 assertEq(parse('- a\n - b\n- c'),
185 '<ul><li>a<ul><li>b</li></ul></li><li>c</li></ul>');
186 });
187
188 test('adjacent lists of different types stay separate', function () {
189 assertEq(parse('- a\n1. b'), '<ul><li>a</li></ul><ol><li>b</li></ol>');
190 });
191
192 test('inline formatting inside list items', function () {
193 assertEq(parse('- **a**'), '<ul><li><strong>a</strong></li></ul>');
194 });
195
196 test('list followed by paragraph', function () {
197 assertEq(parse('- a\n\ntext'), '<ul><li>a</li></ul>\n<p>text</p>');
198 });
199
200
201
202 test('extract keeps first-appearance order and dedupes', function () {
203 assertDeep(P.extractWikilinks('[[B]] then [[A]] and [[B]]'), ['B', 'A']);
204 });
205
206 test('extract takes the target from labelled links', function () {
207 assertDeep(P.extractWikilinks('[[X|some label]]'), ['X']);
208 });
209
210 test('extract trims whitespace', function () {
211 assertDeep(P.extractWikilinks('[[ Spaced ]]'), ['Spaced']);
212 });
213
214 test('extract ignores links inside code', function () {
215 assertDeep(
216 P.extractWikilinks('`[[No]]` and ```\n[[Nope]]\n``` and [[Yes]]'),
217 ['Yes']);
218 });
219
220 test('extract of empty content is empty', function () {
221 assertDeep(P.extractWikilinks(''), []);
222 assertDeep(P.extractWikilinks(null), []);
223 });
224
225
226
227 var GRAPH_PAGES = {
228 A: '[[B]] and [[C]] and [[B]] again',
229 B: 'links back to [[A]]',
230 C: 'no links here',
231 D: '[[A]] plus [[Missing]] plus [[D]] self'
232 };
233
234 test('graph nodes list every page', function () {
235 assertDeep(P.buildGraph(GRAPH_PAGES).nodes, ['A', 'B', 'C', 'D']);
236 });
237
238 test('graph links are deduped and exclude self-links', function () {
239 var g = P.buildGraph(GRAPH_PAGES);
240 assertDeep(g.links.A, ['B', 'C']);
241 assertDeep(g.links.B, ['A']);
242 assertDeep(g.links.C, []);
243 assertDeep(g.links.D, ['A', 'Missing']);
244 });
245
246 test('graph backlinks point at linking pages', function () {
247 var g = P.buildGraph(GRAPH_PAGES);
248 assertDeep(g.backlinks.A, ['B', 'D']);
249 assertDeep(g.backlinks.B, ['A']);
250 assertDeep(g.backlinks.C, ['A']);
251 });
252
253 test('graph reports missing targets', function () {
254 assertDeep(P.buildGraph(GRAPH_PAGES).missing, ['Missing']);
255 });
256
257 test('graph backlinks are tracked for missing pages too', function () {
258 assertDeep(P.buildGraph(GRAPH_PAGES).backlinks.Missing, ['D']);
259 });
260
261 test('graph orphans are pages nobody links to', function () {
262 assertDeep(P.buildGraph(GRAPH_PAGES).orphans, ['D']);
263 });
264
265 test('graph accepts {content} page objects', function () {
266 var g = P.buildGraph({ A: { content: '[[B]]' }, B: { content: '' } });
267 assertDeep(g.links.A, ['B']);
268 assertDeep(g.backlinks.B, ['A']);
269 assertDeep(g.missing, []);
270 });
271
272 test('graph of empty input is empty', function () {
273 var g = P.buildGraph({});
274 assertDeep(g.nodes, []);
275 assertDeep(g.missing, []);
276 assertDeep(g.orphans, []);
277 });
278
279
280
281 var passed = 0, failed = 0, results = [];
282 tests.forEach(function (t) {
283 try {
284 t.fn();
285 passed++;
286 results.push({ name: t.name, ok: true });
287 } catch (e) {
288 failed++;
289 results.push({ name: t.name, ok: false, error: e.message });
290 }
291 });
292
293 if (IS_NODE) {
294 results.forEach(function (r) {
295 console.log((r.ok ? ' ok ' : ' FAIL ') + r.name);
296 if (!r.ok) console.log(' ' + r.error.replace(/\n/g, '\n '));
297 });
298 console.log('\n' + passed + ' passed, ' + failed + ' failed, ' + tests.length + ' total');
299 process.exitCode = failed ? 1 : 0;
300 } else {
301 var summary = document.getElementById('summary');
302 var list = document.getElementById('results');
303 summary.textContent = passed + ' passed, ' + failed + ' failed, ' + tests.length + ' total';
304 summary.className = 'summary ' + (failed ? 'fail' : 'pass');
305 results.forEach(function (r) {
306 var div = document.createElement('div');
307 div.className = 'result ' + (r.ok ? 'pass' : 'fail');
308 var name = document.createElement('strong');
309 name.textContent = (r.ok ? '✓ ' : '✗ ') + r.name;
310 div.appendChild(name);
311 if (!r.ok) {
312 var pre = document.createElement('pre');
313 pre.textContent = r.error;
314 div.appendChild(pre);
315 }
316 list.appendChild(div);
317 });
318 }
319})();
320
Discussion
No comments yet. Start the discussion. Recorded by @agentsage-runs.