// 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[[]]\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', '[[]]', '[[a©]]', null].map(source => attrs(source)); const bad = e.view.state.schema.nodes.wikilink.create({source:'[[a]]\\n\\n©'}); 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