83 lines
4.6 KiB
JavaScript
83 lines
4.6 KiB
JavaScript
// C2-11a. Unchanged files alone cannot detect escaped wikilinks: the editor
|
|
// keeps raw source. The browser checks the structured node and a dirty block.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { loadEditor } from './helpers/editor-bundle.mjs';
|
|
import { WIKILINK_CASES } from './helpers/wikilink-cases.mjs';
|
|
import { fixture } from './fixture.mjs';
|
|
import { browser } from './browser.mjs';
|
|
|
|
const editor = loadEditor();
|
|
for (const { name, text } of WIKILINK_CASES) test(`wikilink round-trip: ${name}`, () => {
|
|
assert.equal(editor.roundTrip(text), text);
|
|
assert.equal(editor.roundTrip(text.trimEnd()), text.trimEnd(), 'no trailing newline');
|
|
});
|
|
|
|
const page = async t => {
|
|
const f = await fixture(); t.after(() => f.close());
|
|
const b = await browser(); t.after(() => b.close());
|
|
await b.navigate(f.base);
|
|
return b;
|
|
};
|
|
const mount = text => `(() => {
|
|
window.__wikiEditor?.destroy();
|
|
const host = document.createElement('div'); document.body.append(host);
|
|
window.__wikiEditor = window.MosaicEditor.mount(host, {text: ${JSON.stringify(text)}});
|
|
return true;
|
|
})()`;
|
|
const atoms = `(() => {
|
|
const result = [];
|
|
window.__wikiEditor.view.state.doc.descendants(node => {
|
|
if (node.type.name === 'wikilink') result.push({source: node.attrs.source, atom: node.isAtom, inline: node.isInline});
|
|
}); return result;
|
|
})()`;
|
|
|
|
test('wikilinks are inline atoms and survive a neighbouring edit in every required context', { timeout: 120000 }, async t => {
|
|
const b = await page(t);
|
|
for (const { name, text, sources } of WIKILINK_CASES) {
|
|
await b.evaluate(mount(text));
|
|
assert.deepEqual(await b.evaluate(atoms), sources.map(source => ({ source, atom: true, inline: true })), name);
|
|
assert.equal(await b.evaluate('document.querySelectorAll(".md-wikilink a, .md-wikilink img, .md-wikilink iframe").length'), 0, 'no navigation or resource elements');
|
|
const changed = await b.evaluate(`(() => {
|
|
const e = window.__wikiEditor; let end;
|
|
e.view.state.doc.descendants((node, pos) => { if (node.isTextblock && node.type.name !== 'raw_block') end = pos + node.nodeSize - 1; });
|
|
e.view.dispatch(e.view.state.tr.insertText(' changed', end));
|
|
const output = e.text(); e.setText(output); return output;
|
|
})()`);
|
|
assert.ok(changed.includes(' changed'), name);
|
|
for (const source of sources) assert.ok(changed.includes(source), `${name}: spelling survives dirty-block serialisation`);
|
|
assert.deepEqual(await b.evaluate(atoms), sources.map(source => ({ source, atom: true, inline: true })), `${name}: survives reparse`);
|
|
if (text.startsWith('---')) assert.ok(changed.startsWith(text.slice(0, text.indexOf('\n\n') + 2)), 'front matter is untouched');
|
|
if (name === 'reference-definition collision')
|
|
assert.ok(changed.endsWith('\n\n[a]: u\n'), 'the separate reference-definition block keeps its exact bytes');
|
|
}
|
|
});
|
|
|
|
test('code, escaped openers, malformed syntax and HTML-like targets stay outside wikilink atoms', { timeout: 120000 }, async t => {
|
|
const b = await page(t);
|
|
const text = '\\[[escaped]] `[[inline-code]]`\n\n```\n[[fenced-code]]\n```\n\n[[open\n\n[[]]\n\n[[Q&A]]\n\n[[a`]]`\n\n[[<img src=x onerror=alert(1)>]]\n';
|
|
assert.equal(editor.roundTrip(text), text);
|
|
await b.evaluate(mount(text));
|
|
assert.deepEqual(await b.evaluate(atoms), []);
|
|
assert.equal(await b.evaluate('document.querySelector(".ProseMirror").querySelectorAll("img, iframe, script").length'), 0);
|
|
});
|
|
|
|
test('wikilink DOM paste accepts only bounded single-line inert syntax; malformed attrs serialise as escaped text', { timeout: 120000 }, async t => {
|
|
const b = await page(t); await b.evaluate(mount('[[a]]\n'));
|
|
const result = await b.evaluate(`(() => {
|
|
const e = window.__wikiEditor, spec = e.view.state.schema.nodes.wikilink.spec;
|
|
const attrs = source => spec.parseDOM[0].getAttrs({getAttribute: () => source});
|
|
const invalid = ['not a link', '[[a]]\\n\\n<script>x</script>', '[[<img src=x>]]', '[[a©]]', null].map(source => attrs(source));
|
|
const bad = e.view.state.schema.nodes.wikilink.create({source:'[[a]]\\n\\n<img src=x onerror=alert(1)>©'});
|
|
e.view.dispatch(e.view.state.tr.replaceWith(1, e.view.state.doc.content.size - 1, bad));
|
|
return {valid:attrs('[[a#h|label]]'), invalid, output:e.text()};
|
|
})()`);
|
|
assert.deepEqual(result.valid, { source: '[[a#h|label]]' });
|
|
assert.deepEqual(result.invalid, [false, false, false, false, false]);
|
|
assert.ok(!result.output.includes('\n\n<img'));
|
|
assert.doesNotMatch(result.output, /(?<!\\)<img/);
|
|
assert.match(result.output, /\\<img/);
|
|
assert.doesNotMatch(result.output, /(?<!\\)©/);
|
|
assert.match(result.output, /\\©/);
|
|
});
|