diff --git a/src/lib.js b/src/lib.js index 812d09a..55f11f8 100644 --- a/src/lib.js +++ b/src/lib.js @@ -267,18 +267,35 @@ export function yDocToProsemirrorJSON ( } if (d.attributes) { - text.marks = Object.keys(d.attributes).map((type) => { - const attrs = d.attributes[type] - const mark = { - type - } + let marks = [] + text.marks = Object.keys(d.attributes).forEach((type) => { + let attrs = d.attributes[type] + if (Array.isArray(attrs)) { + // multiple marks of same type + attrs.forEach(singleAttrs => { + const mark = { + type + } - if (Object.keys(attrs)) { - mark.attrs = attrs - } + if (Object.keys(singleAttrs)) { + mark.attrs = singleAttrs + } + + marks.push(mark) + }) + } else { + const mark = { + type + } - return mark + if (Object.keys(attrs)) { + mark.attrs = attrs + } + + marks.push(mark) + } }) + text.marks = marks } return text }) diff --git a/src/plugins/sync-plugin.js b/src/plugins/sync-plugin.js index 7051bbd..a1000bf 100644 --- a/src/plugins/sync-plugin.js +++ b/src/plugins/sync-plugin.js @@ -473,7 +473,15 @@ const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot, const delta = deltas[i] const marks = [] for (const markName in delta.attributes) { - marks.push(schema.mark(markName, delta.attributes[markName])) + if (Array.isArray(delta.attributes[markName])) { + // multiple marks of same type + delta.attributes[markName].forEach(attrs => { + marks.push(schema.mark(markName, attrs)) + }) + } else { + // single mark + marks.push(schema.mark(markName, delta.attributes[markName])) + } } nodes.push(schema.text(delta.insert, marks)) } @@ -545,6 +553,14 @@ const equalAttrs = (pattrs, yattrs) => { return eq } +const containsEqualMark = (pattrs, yattrs) => { + if (Array.isArray(yattrs)) { + return !!yattrs.find(el => equalAttrs(pattrs, el)) + } else { + return equalAttrs(pattrs, yattrs) + } +} + /** * @typedef {Array|PModel.Node>} NormalizedPNodeContent */ @@ -572,13 +588,25 @@ const normalizePNodeContent = pnode => { return res } +const countYTextMarks = (yattrs) => { + let count = 0 + object.forEach(yattrs, (val) => { + if (Array.isArray(val)) { + count += val.length + } else { + count++ + } + }) + return count +} + /** * @param {Y.XmlText} ytext * @param {Array} ptexts */ const equalYTextPText = (ytext, ptexts) => { const delta = ytext.toDelta() - return delta.length === ptexts.length && delta.every((d, i) => d.insert === /** @type {any} */ (ptexts[i]).text && object.keys(d.attributes || {}).length === ptexts[i].marks.length && ptexts[i].marks.every(mark => equalAttrs(d.attributes[mark.type.name] || {}, mark.attrs))) + return delta.length === ptexts.length && delta.every((d, i) => d.insert === /** @type {any} */ (ptexts[i]).text && countYTextMarks(d.attributes || {}) === ptexts[i].marks.length && ptexts[i].marks.every(mark => containsEqualMark(d.attributes[mark.type.name] || {}, mark.attrs))) } /** @@ -682,7 +710,16 @@ const marksToAttributes = marks => { const pattrs = {} marks.forEach(mark => { if (mark.type.name !== 'ychange') { - pattrs[mark.type.name] = mark.attrs + if (pattrs[mark.type.name] && Array.isArray(pattrs[mark.type.name])) { + // already has multiple marks of same type + pattrs[mark.type.name].push(mark.attrs) + } else if (pattrs[mark.type.name]) { + // already has mark of same type, change to array + pattrs[mark.type.name] = [pattrs[mark.type.name], mark.attrs] + } else { + // first mark of this type + pattrs[mark.type.name] = mark.attrs + } } }) return pattrs diff --git a/test/complexSchema.js b/test/complexSchema.js index 013190d..c8cda86 100644 --- a/test/complexSchema.js +++ b/test/complexSchema.js @@ -157,6 +157,7 @@ export const nodes = { const emDOM = ['em', 0] const strongDOM = ['strong', 0] const codeDOM = ['code', 0] +const commentDOM = ['span', 0] // :: Object [Specs](#model.MarkSpec) for the marks in the schema. export const marks = { @@ -223,6 +224,16 @@ export const marks = { return codeDOM } }, + comment: { + attrs: { + id: { default: null } + }, + exclude: '', // allow multiple "comments" marks to overlap + parseDOM: [{ tag: 'span' }], + toDOM () { + return commentDOM + } + }, ychange: { attrs: { user: { default: null }, diff --git a/test/y-prosemirror.test.js b/test/y-prosemirror.test.js index a26fbf6..92971e5 100644 --- a/test/y-prosemirror.test.js +++ b/test/y-prosemirror.test.js @@ -26,6 +26,36 @@ export const testDocTransformation = tc => { t.compare(stateJSON, backandforth) } +/** + * @param {t.TestCase} tc + */ +export const testDuplicateMarks = tc => { + const ydoc = new Y.Doc() + const type = ydoc.getXmlFragment('prosemirror') + const view = createNewComplexProsemirrorView(ydoc) + t.assert(type.toString() === '', 'should only sync after first change') + + view.dispatch( + view.state.tr.setNodeMarkup(0, undefined, { + checked: true + }) + ) + + const marks = [complexSchema.mark('comment', { id: 0 }), complexSchema.mark('comment', { id: 1 })] + view.dispatch(view.state.tr.insert(view.state.doc.content.size - 1, /** @type {any} */ complexSchema.text('hello world', marks))) + const stateJSON = view.state.doc.toJSON() + + // test if transforming back and forth from Yjs doc works + const backandforth = yDocToProsemirrorJSON(prosemirrorJSONToYDoc(/** @type {any} */ (complexSchema), stateJSON)) + + // TODO: I think the duplicate marks work, but I think this fails because + // there is a yChange on stateJSON.content[1] (and not on backandforth) + t.compare(stateJSON, backandforth) + + // TODO: create a toString test, this currently fails because YXmlText breaks + // t.compareStrings(type.toString(), '') +} + /** * @param {t.TestCase} tc */