// check.mjs: the accepted-shape check over a document's text, and the // sanitiser the stage runs before binding (codex 3). The messages are // core/src/check.rs's, so a case here has a twin in its tests. import test from 'node:test' import assert from 'node:assert/strict' import * as Y from 'yjs' import { yXmlFragmentToProseMirrorRootNode } from 'y-prosemirror' import { applyOps, view } from './ops.mjs' import { schema, readText } from './schema.mjs' import { checkDoc, checkFragment, sanitiseFragment, MARK_ATTRIBUTES } from './check.mjs' import { blockFragment } from './stage.mjs' import { poisonDoc } from './poison.mjs' function deck () { const doc = new Y.Doc({ guid: 'check-test' }) doc.clientID = 1 applyOps(doc, [ { op: 'add_slide', id: 'sld', layout: 'flow', template: 'title-body' }, { op: 'set_notes', id: 'sld', markdown: 'Say **hello**' }, { op: 'add_block', slide: 'sld', id: 'blk', type: 'text', markdown: '**b** *i* `c` ==h== [l](https://x)' }, { op: 'add_block', slide: 'sld', id: 'tbl', type: 'table', props: { rows: 1, cols: 2 } } ]) return doc } /** The one XmlText inside the block's first paragraph. */ function firstText (fragment) { return fragment.get(0).get(0) } test('what the ops write passes, and so does an empty document', () => { assert.equal(checkDoc(deck()), null) assert.equal(checkDoc(new Y.Doc({ guid: 'empty' })), null) }) test('the shapes y-prosemirror and older documents write pass', () => { const doc = deck() const t = firstText(blockFragment(doc, 'sld', 'blk')) t.applyDelta([{ insert: 'x', attributes: { b: {}, href: 'https://old' } }]) assert.equal(checkDoc(doc), null) }) test('an unknown formatting attribute is reported with the core\'s message', () => { const doc = deck() const t = firstText(blockFragment(doc, 'sld', 'blk')) t.applyDelta([{ insert: 'lost', attributes: { unknown_mark: true } }]) // The projection still reads the words: that is exactly why the gate // cannot be the projection. assert.equal(view(doc).slides[0].blocks[0].text[0][0].t, 'lost') assert.equal(checkDoc(doc), 'slide "sld" block "blk" text: paragraph 0 carries the formatting attribute "unknown_mark", which is not one of b, i, c, h, href') }) test('an element inside a paragraph, a non-paragraph child and bare text are reported', () => { const doc = deck() const slide = doc.getArray('slides').get(0) slide.get('notes').get(0).insert(1, [new Y.XmlElement('strong')]) assert.equal(checkDoc(doc), 'slide "sld" notes: paragraph 0 holds a element, and a paragraph holds only text') const doc2 = deck() const tbl = doc2.getArray('slides').get(0).get('blocks').get(1) tbl.get('cells').get(1).insert(0, [new Y.XmlElement('heading')]) assert.equal(checkDoc(doc2), 'slide "sld" block "tbl" cell 1: child 0 is a element, and only is allowed') const doc3 = deck() doc3.getArray('slides').get(0).get('notes').insert(1, [new Y.XmlText('loose')]) assert.equal(checkDoc(doc3), 'slide "sld" notes: child 1 is text outside a paragraph, and only is allowed') const frag = new Y.Doc({ guid: 'f' }).getXmlFragment('t') frag.insert(0, [new Y.XmlFragment()]) assert.equal(checkFragment(frag, 'x'), 'x: child 0 is a nested fragment, and only is allowed') }) test('the unknownmark poison shape is what the check refuses', () => { const doc = poisonDoc('unknownmark') assert.match(checkDoc(doc), /^slide "poisonmrksld" block "poisonmrkblk" text: paragraph 0 carries the formatting attribute "unknown_mark"/) assert.deepEqual(view(doc).slides[0].blocks[0].text, [[{ t: 'keep these words' }]]) }) test('y-prosemirror 1.3.7 deletes the words under an unknown mark; sanitising first keeps them', () => { // Codex 3 reproduced: the library's own conversion, no DOM needed. const lost = deck() const lostFrag = blockFragment(lost, 'sld', 'blk') firstText(lostFrag).applyDelta([{ insert: 'keep ' }, { insert: 'these', attributes: { unknown_mark: true } }]) assert.equal(readText(lostFrag)[0].map(r => r.t).join(''), 'keep theseb i c h l') const node = yXmlFragmentToProseMirrorRootNode(lostFrag, schema) assert.equal(node.textContent, '', 'the library emptied the paragraph') assert.deepEqual(readText(lostFrag), [[]], 'and deleted the XmlText from the document') // The same document, sanitised the way bindView does before binding. const kept = deck() const keptFrag = blockFragment(kept, 'sld', 'blk') firstText(keptFrag).applyDelta([{ insert: 'keep ' }, { insert: 'these', attributes: { unknown_mark: true, i: true } }]) const origins = [] kept.on('afterTransaction', tr => origins.push(tr.origin)) assert.equal(sanitiseFragment(keptFrag), 1) assert.deepEqual(origins, ['sanitise'], 'one transaction, under an origin the undo manager does not track') assert.equal(checkDoc(kept), null) const bound = yXmlFragmentToProseMirrorRootNode(keptFrag, schema) assert.equal(bound.textContent, 'keep theseb i c h l', 'every word survives the bind') // The known mark on the same run is untouched; only the unknown one went. assert.deepEqual(readText(keptFrag)[0].slice(0, 2), [{ t: 'keep ' }, { t: 'these', i: true }]) // Nothing to strip is not a write. assert.equal(sanitiseFragment(keptFrag), 0) assert.deepEqual(origins, ['sanitise']) }) test('the vocabulary is the five marks and nothing else', () => { assert.deepEqual(MARK_ATTRIBUTES, ['b', 'i', 'c', 'h', 'href']) })