Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for overlapping marks #52

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
43 changes: 40 additions & 3 deletions src/plugins/sync-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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<Array<PModel.Node>|PModel.Node>} NormalizedPNodeContent
*/
Expand Down Expand Up @@ -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<any>} 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)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR for handling multiple marks!

I was pulling this PR into my project and ran into an issue with containsEqualMark. It looks like you're calling containsEqualMark with yattrs, pattrs instead of pattrs, yattrs.

}

/**
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions test/complexSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 },
Expand Down
30 changes: 30 additions & 0 deletions test/y-prosemirror.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(), '<custom checked="true"></custom><paragraph></paragraph>')
}

/**
* @param {t.TestCase} tc
*/
Expand Down