y-prosemirror
Advanced tools
Comparing version 0.0.10 to 0.0.11
@@ -30,3 +30,2 @@ 'use strict'; | ||
while (n !== null && type !== n) { | ||
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize; | ||
if (n.constructor === Y.XmlText) { | ||
@@ -50,27 +49,30 @@ if (n._length >= pos) { | ||
} | ||
} else if (n._first !== null && pos < pNodeSize) { | ||
n = /** @type {Y.ContentType} */ (n._first.content).type; | ||
pos--; | ||
} else { | ||
if (pos === 1 && n._length === 0 && pNodeSize > 1) { | ||
// edge case, should end in this paragraph | ||
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null) | ||
} | ||
pos -= pNodeSize; | ||
if (n._item !== null && n._item.next !== null) { | ||
n = /** @type {Y.ContentType} */ (n._item.next.content).type; | ||
const pNodeSize = /** @type {any} */ (mapping.get(n) || { nodeSize: 0 }).nodeSize; | ||
if (n._first !== null && pos < pNodeSize) { | ||
n = /** @type {Y.ContentType} */ (n._first.content).type; | ||
pos--; | ||
} else { | ||
if (pos === 0) { | ||
// set to end of n.parent | ||
n = n._item === null ? n : n._item.parent; | ||
if (pos === 1 && n._length === 0 && pNodeSize > 1) { | ||
// edge case, should end in this paragraph | ||
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null) | ||
} | ||
do { | ||
n = /** @type {Y.Item} */ (n._item).parent; | ||
pos--; | ||
} while (n !== type && /** @type {Y.Item} */ (n._item).next === null) | ||
// if n is null at this point, we have an unexpected case | ||
if (n !== type) { | ||
// We know that n._item.next is defined because of above loop condition | ||
n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type; | ||
pos -= pNodeSize; | ||
if (n._item !== null && n._item.next !== null) { | ||
n = /** @type {Y.ContentType} */ (n._item.next.content).type; | ||
} else { | ||
if (pos === 0) { | ||
// set to end of n.parent | ||
n = n._item === null ? n : n._item.parent; | ||
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null) | ||
} | ||
do { | ||
n = /** @type {Y.Item} */ (n._item).parent; | ||
pos--; | ||
} while (n !== type && /** @type {Y.Item} */ (n._item).next === null) | ||
// if n is null at this point, we have an unexpected case | ||
if (n !== type) { | ||
// We know that n._item.next is defined because of above loop condition | ||
n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type; | ||
} | ||
} | ||
@@ -114,3 +116,3 @@ } | ||
} else { | ||
pos += mapping.get(t).nodeSize; | ||
pos += /** @type {any} */ (mapping.get(t)).nodeSize; | ||
} | ||
@@ -139,3 +141,3 @@ } | ||
} else { | ||
pos += mapping.get(contentType).nodeSize; | ||
pos += /** @type {any} */ (mapping.get(contentType)).nodeSize; | ||
} | ||
@@ -158,3 +160,4 @@ } | ||
/** | ||
* @typedef {Map<Y.AbstractType, Object>} ProsemirrorMapping | ||
* Either a node if type is YXmlElement or an Array of text nodes if YXmlText | ||
* @typedef {Map<Y.AbstractType, PModel.Node | Array<PModel.Node>>} ProsemirrorMapping | ||
*/ | ||
@@ -176,3 +179,3 @@ | ||
*/ | ||
const ySyncPlugin = (yXmlFragment) => { | ||
const ySyncPlugin = yXmlFragment => { | ||
let changedInitialContent = false; | ||
@@ -334,6 +337,10 @@ const plugin = new prosemirrorState.Plugin({ | ||
this.mux(() => { | ||
const delStruct = (_, struct) => this.mapping.delete(struct); | ||
/** | ||
* @param {any} _ | ||
* @param {Y.AbstractType} type | ||
*/ | ||
const delType = (_, type) => this.mapping.delete(type); | ||
Y.iterateDeletedStructs(transaction, transaction.deleteSet, this.doc.store, struct => struct.constructor === Y.Item && this.mapping.delete(/** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type)); | ||
transaction.changed.forEach(delStruct); | ||
transaction.changedParentTypes.forEach(delStruct); | ||
transaction.changed.forEach(delType); | ||
transaction.changedParentTypes.forEach(delType); | ||
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(/** @type {Y.XmlElement | Y.XmlHook} */ (t), this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null); | ||
@@ -351,3 +358,3 @@ let tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0)); | ||
this.mux(() => { | ||
updateYFragment(this.doc, this.type, doc.content, this.mapping); | ||
updateYFragment(this.doc, this.type, doc, this.mapping); | ||
}); | ||
@@ -370,3 +377,3 @@ } | ||
const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => { | ||
const node = mapping.get(el); | ||
const node = /** @type {PModel.Node} */ (mapping.get(el)); | ||
if (node === undefined) { | ||
@@ -426,3 +433,2 @@ if (el instanceof Y.XmlElement) { | ||
} | ||
let node; | ||
try { | ||
@@ -437,3 +443,5 @@ const attrs = el.getAttributes(_snapshot); | ||
} | ||
node = schema.node(el.nodeName, attrs, children); | ||
const node = schema.node(el.nodeName, attrs, children); | ||
mapping.set(el, node); | ||
return node | ||
} catch (e) { | ||
@@ -444,6 +452,5 @@ // an error occured while creating the node. This is probably a result of a concurrent action. | ||
}); | ||
mapping.delete(el); | ||
return null | ||
} | ||
mapping.set(el, node); | ||
return node | ||
}; | ||
@@ -472,5 +479,2 @@ | ||
} | ||
if (nodes.length > 0) { | ||
mapping.set(text, nodes[0]); // only map to first child, all following children are also considered bound to this type | ||
} | ||
} catch (e) { | ||
@@ -489,31 +493,33 @@ // an error occured while creating the node. This is probably a result of a concurrent action. | ||
* @private | ||
* @param {Object} node prosemirror node | ||
* @param {Array<PModel.Node>} nodes prosemirror node | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {Y.XmlElement | Y.XmlText} | ||
* @return {Y.XmlText} | ||
*/ | ||
const createTypeFromNode = (node, mapping) => { | ||
let type; | ||
if (node.isText) { | ||
type = new Y.XmlText(); | ||
const attrs = {}; | ||
node.marks.forEach(mark => { | ||
if (mark.type.name !== 'ychange') { | ||
attrs[mark.type.name] = mark.attrs; | ||
} | ||
}); | ||
type.insert(0, node.text, attrs); | ||
} else { | ||
type = new Y.XmlElement(node.type.name); | ||
for (let key in node.attrs) { | ||
const val = node.attrs[key]; | ||
if (val !== null && key !== 'ychange') { | ||
type.setAttribute(key, val); | ||
} | ||
const createTypeFromTextNodes = (nodes, mapping) => { | ||
const type = new Y.XmlText(); | ||
const delta = nodes.map(node => ({ | ||
// @ts-ignore | ||
insert: node.text, | ||
attributes: marksToAttributes(node.marks) | ||
})); | ||
type.applyDelta(delta); | ||
mapping.set(type, nodes); | ||
return type | ||
}; | ||
/** | ||
* @private | ||
* @param {PModel.Node} node prosemirror node | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {Y.XmlElement} | ||
*/ | ||
const createTypeFromElementNode = (node, mapping) => { | ||
const type = new Y.XmlElement(node.type.name); | ||
for (let key in node.attrs) { | ||
const val = node.attrs[key]; | ||
if (val !== null && key !== 'ychange') { | ||
type.setAttribute(key, val); | ||
} | ||
const ins = []; | ||
for (let i = 0; i < node.childCount; i++) { | ||
ins.push(createTypeFromNode(node.child(i), mapping)); | ||
} | ||
type.insert(0, ins); | ||
} | ||
type.insert(0, normalizePNodeContent(node).map(n => createTypeFromTextOrElementNode(n, mapping))); | ||
mapping.set(type, node); | ||
@@ -523,2 +529,10 @@ return type | ||
/** | ||
* @private | ||
* @param {PModel.Node|Array<PModel.Node>} node prosemirror text node | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {Y.XmlElement|Y.XmlText} | ||
*/ | ||
const createTypeFromTextOrElementNode = (node, mapping) => node instanceof Array ? createTypeFromTextNodes(node, mapping) : createTypeFromElementNode(node, mapping); | ||
const equalAttrs = (pattrs, yattrs) => { | ||
@@ -536,19 +550,66 @@ const keys = Object.keys(pattrs).filter(key => pattrs[key] !== null); | ||
const equalYTextPText = (ytext, ptext) => { | ||
/** | ||
* @typedef {Array<Array<PModel.Node>|PModel.Node>} NormalizedPNodeContent | ||
*/ | ||
/** | ||
* @param {PModel.Node} pnode | ||
* @return {NormalizedPNodeContent} | ||
*/ | ||
const normalizePNodeContent = pnode => { | ||
const c = pnode.content.content; | ||
const res = []; | ||
for (let i = 0; i < c.length; i++) { | ||
const n = c[i]; | ||
if (n.isText) { | ||
const textNodes = []; | ||
for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) { | ||
textNodes.push(tnode); | ||
} | ||
i--; | ||
res.push(textNodes); | ||
} else { | ||
res.push(n); | ||
} | ||
} | ||
return res | ||
}; | ||
/** | ||
* @param {Y.XmlText} ytext | ||
* @param {Array<PModel.Node>} ptexts | ||
*/ | ||
const equalYTextPText = (ytext, ptexts) => { | ||
const delta = ytext.toDelta(); | ||
if (delta.length === 0) { | ||
return ptext.text === '' | ||
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))) | ||
}; | ||
/** | ||
* @param {Y.XmlElement|Y.XmlText|Y.XmlHook} ytype | ||
* @param {PModel.Node|Array<PModel.Node>} pnode | ||
*/ | ||
const equalYTypePNode = (ytype, pnode) => { | ||
if (ytype instanceof Y.XmlElement && !(pnode instanceof Array) && matchNodeName(ytype, pnode)) { | ||
const normalizedContent = normalizePNodeContent(pnode); | ||
return ytype._length === normalizedContent.length && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i])) | ||
} | ||
const d = delta[0]; | ||
return d.insert === ptext.text && object.keys(d.attributes || {}).length === ptext.marks.length && ptext.marks.every(mark => equalAttrs(d.attributes[mark.type.name], mark.attrs)) | ||
return ytype instanceof Y.XmlText && pnode instanceof Array && equalYTextPText(ytype, pnode) | ||
}; | ||
const equalYTypePNode = (ytype, pnode) => | ||
ytype.constructor === Y.XmlText | ||
? equalYTextPText(ytype, pnode) | ||
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, pnode.child(i)))); | ||
/** | ||
* @param {PModel.Node | Array<PModel.Node> | undefined} mapped | ||
* @param {PModel.Node | Array<PModel.Node>} pcontent | ||
*/ | ||
const mappedIdentity = (mapped, pcontent) => mapped === pcontent || (mapped instanceof Array && pcontent instanceof Array && mapped.length === pcontent.length && mapped.every((a, i) => pcontent[i] === a)); | ||
/** | ||
* @param {Y.XmlElement} ytype | ||
* @param {PModel.Node} pnode | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {{ foundMappedChild: boolean, equalityFactor: number }} | ||
*/ | ||
const computeChildEqualityFactor = (ytype, pnode, mapping) => { | ||
const yChildren = ytype.toArray(); | ||
const pChildCnt = pnode.childCount; | ||
const pChildren = normalizePNodeContent(pnode); | ||
const pChildCnt = pChildren.length; | ||
const yChildCnt = yChildren.length; | ||
@@ -561,4 +622,4 @@ const minCnt = math.min(yChildCnt, pChildCnt); | ||
const leftY = yChildren[left]; | ||
const leftP = pnode.child(left); | ||
if (mapping.get(leftY) === leftP) { | ||
const leftP = pChildren[left]; | ||
if (mappedIdentity(mapping.get(leftY), leftP)) { | ||
foundMappedChild = true;// definite (good) match! | ||
@@ -571,6 +632,6 @@ } else if (!equalYTypePNode(leftY, leftP)) { | ||
const rightY = yChildren[yChildCnt - right - 1]; | ||
const rightP = pnode.child(pChildCnt - right - 1); | ||
if (mapping.get(rightY) !== rightP) { | ||
const rightP = pChildren[pChildCnt - right - 1]; | ||
if (mappedIdentity(mapping.get(rightY), rightP)) { | ||
foundMappedChild = true; | ||
} else if (!equalYTypePNode(rightP, rightP)) { | ||
} else if (!equalYTypePNode(rightY, rightP)) { | ||
break | ||
@@ -585,18 +646,68 @@ } | ||
const ytextTrans = ytext => { | ||
let str = ''; | ||
/** | ||
* @type {Y.Item|null} | ||
*/ | ||
let n = ytext._start; | ||
const nAttrs = {}; | ||
while (n !== null) { | ||
if (!n.deleted) { | ||
if (n.countable && n.content instanceof Y.ContentString) { | ||
str += n.content.str; | ||
} else if (n.content instanceof Y.ContentFormat) { | ||
nAttrs[n.content.key] = null; | ||
} | ||
} | ||
n = n.right; | ||
} | ||
return { | ||
str, | ||
nAttrs | ||
} | ||
}; | ||
/** | ||
* @todo test this more | ||
* | ||
* @param {Y.Text} ytext | ||
* @param {Array<PModel.Node>} ptexts | ||
* @param {ProsemirrorMapping} mapping | ||
*/ | ||
const updateYText = (ytext, ptexts, mapping) => { | ||
mapping.set(ytext, ptexts); | ||
const { nAttrs, str } = ytextTrans(ytext); | ||
const content = ptexts.map(p => ({ insert: /** @type {any} */ (p).text, attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks)) })); | ||
const { insert, remove, index } = diff_js.simpleDiff(str, content.map(c => c.insert).join('')); | ||
ytext.delete(index, remove); | ||
ytext.insert(index, insert); | ||
ytext.applyDelta(content.map(c => ({ retain: c.insert.length, attributes: c.attributes }))); | ||
}; | ||
const marksToAttributes = marks => { | ||
const pattrs = {}; | ||
marks.forEach(mark => { | ||
if (mark.type.name !== 'ychange') { | ||
pattrs[mark.type.name] = mark.attrs; | ||
} | ||
}); | ||
return pattrs | ||
}; | ||
/** | ||
* @private | ||
* @param {Y.Doc} y | ||
* @param {Y.XmlFragment} yDomFragment | ||
* @param {Object} pContent | ||
* @param {PModel.Node} pNode | ||
* @param {ProsemirrorMapping} mapping | ||
*/ | ||
const updateYFragment = (y, yDomFragment, pContent, mapping) => { | ||
if (yDomFragment instanceof Y.XmlElement && yDomFragment.nodeName !== pContent.type.name) { | ||
const updateYFragment = (y, yDomFragment, pNode, mapping) => { | ||
if (yDomFragment instanceof Y.XmlElement && yDomFragment.nodeName !== pNode.type.name) { | ||
throw new Error('node name mismatch!') | ||
} | ||
mapping.set(yDomFragment, pContent); | ||
mapping.set(yDomFragment, pNode); | ||
// update attributes | ||
if (yDomFragment instanceof Y.XmlElement) { | ||
const yDomAttrs = yDomFragment.getAttributes(); | ||
const pAttrs = pContent.attrs; | ||
const pAttrs = pNode.attrs; | ||
for (let key in pAttrs) { | ||
@@ -619,3 +730,4 @@ if (pAttrs[key] !== null) { | ||
// update children | ||
const pChildCnt = pContent.childCount; | ||
const pChildren = normalizePNodeContent(pNode); | ||
const pChildCnt = pChildren.length; | ||
const yChildren = yDomFragment.toArray(); | ||
@@ -629,4 +741,4 @@ const yChildCnt = yChildren.length; | ||
const leftY = yChildren[left]; | ||
const leftP = pContent.child(left); | ||
if (mapping.get(leftY) !== leftP) { | ||
const leftP = pChildren[left]; | ||
if (!mappedIdentity(mapping.get(leftY), leftP)) { | ||
if (equalYTypePNode(leftY, leftP)) { | ||
@@ -641,6 +753,6 @@ // update mapping | ||
// find number of matching elements from right | ||
for (;right + left < minCnt; right++) { | ||
for (;right + left + 1 < minCnt; right++) { | ||
const rightY = yChildren[yChildCnt - right - 1]; | ||
const rightP = pContent.child(pChildCnt - right - 1); | ||
if (mapping.get(rightY) !== rightP) { | ||
const rightP = pChildren[pChildCnt - right - 1]; | ||
if (!mappedIdentity(mapping.get(rightY), rightP)) { | ||
if (equalYTypePNode(rightY, rightP)) { | ||
@@ -658,24 +770,8 @@ // update mapping | ||
const leftY = yChildren[left]; | ||
const leftP = pContent.child(left); | ||
const leftP = pChildren[left]; | ||
const rightY = yChildren[yChildCnt - right - 1]; | ||
const rightP = pContent.child(pChildCnt - right - 1); | ||
if (leftY instanceof Y.XmlText && leftP.isText) { | ||
const rightP = pChildren[pChildCnt - right - 1]; | ||
if (leftY instanceof Y.XmlText && leftP instanceof Array) { | ||
if (!equalYTextPText(leftY, leftP)) { | ||
// try to apply diff. Only if attrs don't match, delete insert | ||
// TODO: use a single ytext to hold all following Prosemirror Text nodes | ||
const pattrs = {}; | ||
leftP.marks.forEach(mark => { | ||
if (mark.type.name !== 'ychange') { | ||
pattrs[mark.type.name] = mark.attrs; | ||
} | ||
}); | ||
const delta = leftY.toDelta(); | ||
if (delta.length === 1 && delta[0].insert && equalAttrs(pattrs, delta[0].attributes || {})) { | ||
const diff = diff_js.simpleDiff(delta[0].insert, leftP.text); | ||
leftY.delete(diff.index, diff.remove); | ||
leftY.insert(diff.index, diff.insert, delta[0].attributes || {}); | ||
} else { | ||
yDomFragment.delete(left, 1); | ||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)]); | ||
} | ||
updateYText(leftY, leftP, mapping); | ||
} | ||
@@ -688,4 +784,4 @@ left += 1; | ||
// decide which which element to update | ||
const equalityLeft = computeChildEqualityFactor(leftY, leftP, mapping); | ||
const equalityRight = computeChildEqualityFactor(rightY, rightP, mapping); | ||
const equalityLeft = computeChildEqualityFactor(/** @type {Y.XmlElement} */ (leftY), /** @type {PModel.Node} */ (leftP), mapping); | ||
const equalityRight = computeChildEqualityFactor(/** @type {Y.XmlElement} */ (rightY), /** @type {PModel.Node} */ (rightP), mapping); | ||
if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) { | ||
@@ -702,10 +798,10 @@ updateRight = false; | ||
if (updateLeft) { | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (leftY), leftP, mapping); | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (leftY), /** @type {PModel.Node} */ (leftP), mapping); | ||
left += 1; | ||
} else if (updateRight) { | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (rightY), rightP, mapping); | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (rightY), /** @type {PModel.Node} */ (rightP), mapping); | ||
right += 1; | ||
} else { | ||
yDomFragment.delete(left, 1); | ||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)]); | ||
yDomFragment.insert(left, [createTypeFromTextOrElementNode(leftP, mapping)]); | ||
left += 1; | ||
@@ -722,3 +818,3 @@ } | ||
for (let i = left; i < pChildCnt - right; i++) { | ||
ins.push(createTypeFromNode(pContent.child(i), mapping)); | ||
ins.push(createTypeFromTextOrElementNode(pChildren[i], mapping)); | ||
} | ||
@@ -928,5 +1024,8 @@ yDomFragment.insert(left, ins); | ||
exports.createTextNodesFromYText = createTextNodesFromYText; | ||
exports.createTypeFromNode = createTypeFromNode; | ||
exports.createTypeFromElementNode = createTypeFromElementNode; | ||
exports.createTypeFromTextNodes = createTypeFromTextNodes; | ||
exports.createTypeFromTextOrElementNode = createTypeFromTextOrElementNode; | ||
exports.getRelativeSelection = getRelativeSelection; | ||
exports.isVisible = isVisible; | ||
exports.normalizePNodeContent = normalizePNodeContent; | ||
exports.redo = redo; | ||
@@ -933,0 +1032,0 @@ exports.undo = undo; |
{ | ||
"name": "y-prosemirror", | ||
"version": "0.0.10", | ||
"version": "0.0.11", | ||
"description": "Prosemirror bindings for Yjs", | ||
@@ -5,0 +5,0 @@ "main": "./dist/y-prosemirror.js", |
@@ -5,2 +5,3 @@ import { ProsemirrorMapping } from './plugins/sync-plugin.js' // eslint-disable-line | ||
import * as error from 'lib0/error.js' | ||
import * as PModel from 'prosemirror-model' // eslint-disable-line | ||
@@ -21,3 +22,2 @@ /** | ||
while (n !== null && type !== n) { | ||
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize | ||
if (n.constructor === Y.XmlText) { | ||
@@ -41,27 +41,30 @@ if (n._length >= pos) { | ||
} | ||
} else if (n._first !== null && pos < pNodeSize) { | ||
n = /** @type {Y.ContentType} */ (n._first.content).type | ||
pos-- | ||
} else { | ||
if (pos === 1 && n._length === 0 && pNodeSize > 1) { | ||
// edge case, should end in this paragraph | ||
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null) | ||
} | ||
pos -= pNodeSize | ||
if (n._item !== null && n._item.next !== null) { | ||
n = /** @type {Y.ContentType} */ (n._item.next.content).type | ||
const pNodeSize = /** @type {any} */ (mapping.get(n) || { nodeSize: 0 }).nodeSize | ||
if (n._first !== null && pos < pNodeSize) { | ||
n = /** @type {Y.ContentType} */ (n._first.content).type | ||
pos-- | ||
} else { | ||
if (pos === 0) { | ||
// set to end of n.parent | ||
n = n._item === null ? n : n._item.parent | ||
if (pos === 1 && n._length === 0 && pNodeSize > 1) { | ||
// edge case, should end in this paragraph | ||
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null) | ||
} | ||
do { | ||
n = /** @type {Y.Item} */ (n._item).parent | ||
pos-- | ||
} while (n !== type && /** @type {Y.Item} */ (n._item).next === null) | ||
// if n is null at this point, we have an unexpected case | ||
if (n !== type) { | ||
// We know that n._item.next is defined because of above loop condition | ||
n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type | ||
pos -= pNodeSize | ||
if (n._item !== null && n._item.next !== null) { | ||
n = /** @type {Y.ContentType} */ (n._item.next.content).type | ||
} else { | ||
if (pos === 0) { | ||
// set to end of n.parent | ||
n = n._item === null ? n : n._item.parent | ||
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null) | ||
} | ||
do { | ||
n = /** @type {Y.Item} */ (n._item).parent | ||
pos-- | ||
} while (n !== type && /** @type {Y.Item} */ (n._item).next === null) | ||
// if n is null at this point, we have an unexpected case | ||
if (n !== type) { | ||
// We know that n._item.next is defined because of above loop condition | ||
n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type | ||
} | ||
} | ||
@@ -105,3 +108,3 @@ } | ||
} else { | ||
pos += mapping.get(t).nodeSize | ||
pos += /** @type {any} */ (mapping.get(t)).nodeSize | ||
} | ||
@@ -130,3 +133,3 @@ } | ||
} else { | ||
pos += mapping.get(contentType).nodeSize | ||
pos += /** @type {any} */ (mapping.get(contentType)).nodeSize | ||
} | ||
@@ -133,0 +136,0 @@ } |
@@ -19,3 +19,4 @@ /** | ||
/** | ||
* @typedef {Map<Y.AbstractType, Object>} ProsemirrorMapping | ||
* Either a node if type is YXmlElement or an Array of text nodes if YXmlText | ||
* @typedef {Map<Y.AbstractType, PModel.Node | Array<PModel.Node>>} ProsemirrorMapping | ||
*/ | ||
@@ -37,3 +38,3 @@ | ||
*/ | ||
export const ySyncPlugin = (yXmlFragment) => { | ||
export const ySyncPlugin = yXmlFragment => { | ||
let changedInitialContent = false | ||
@@ -195,6 +196,10 @@ const plugin = new Plugin({ | ||
this.mux(() => { | ||
const delStruct = (_, struct) => this.mapping.delete(struct) | ||
/** | ||
* @param {any} _ | ||
* @param {Y.AbstractType} type | ||
*/ | ||
const delType = (_, type) => this.mapping.delete(type) | ||
Y.iterateDeletedStructs(transaction, transaction.deleteSet, this.doc.store, struct => struct.constructor === Y.Item && this.mapping.delete(/** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type)) | ||
transaction.changed.forEach(delStruct) | ||
transaction.changedParentTypes.forEach(delStruct) | ||
transaction.changed.forEach(delType) | ||
transaction.changedParentTypes.forEach(delType) | ||
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(/** @type {Y.XmlElement | Y.XmlHook} */ (t), this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null) | ||
@@ -212,3 +217,3 @@ let tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0)) | ||
this.mux(() => { | ||
updateYFragment(this.doc, this.type, doc.content, this.mapping) | ||
updateYFragment(this.doc, this.type, doc, this.mapping) | ||
}) | ||
@@ -231,3 +236,3 @@ } | ||
export const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => { | ||
const node = mapping.get(el) | ||
const node = /** @type {PModel.Node} */ (mapping.get(el)) | ||
if (node === undefined) { | ||
@@ -287,3 +292,2 @@ if (el instanceof Y.XmlElement) { | ||
} | ||
let node | ||
try { | ||
@@ -298,3 +302,5 @@ const attrs = el.getAttributes(_snapshot) | ||
} | ||
node = schema.node(el.nodeName, attrs, children) | ||
const node = schema.node(el.nodeName, attrs, children) | ||
mapping.set(el, node) | ||
return node | ||
} catch (e) { | ||
@@ -305,6 +311,5 @@ // an error occured while creating the node. This is probably a result of a concurrent action. | ||
}) | ||
mapping.delete(el) | ||
return null | ||
} | ||
mapping.set(el, node) | ||
return node | ||
} | ||
@@ -333,5 +338,2 @@ | ||
} | ||
if (nodes.length > 0) { | ||
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type | ||
} | ||
} catch (e) { | ||
@@ -350,31 +352,33 @@ // an error occured while creating the node. This is probably a result of a concurrent action. | ||
* @private | ||
* @param {Object} node prosemirror node | ||
* @param {Array<PModel.Node>} nodes prosemirror node | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {Y.XmlElement | Y.XmlText} | ||
* @return {Y.XmlText} | ||
*/ | ||
export const createTypeFromNode = (node, mapping) => { | ||
let type | ||
if (node.isText) { | ||
type = new Y.XmlText() | ||
const attrs = {} | ||
node.marks.forEach(mark => { | ||
if (mark.type.name !== 'ychange') { | ||
attrs[mark.type.name] = mark.attrs | ||
} | ||
}) | ||
type.insert(0, node.text, attrs) | ||
} else { | ||
type = new Y.XmlElement(node.type.name) | ||
for (let key in node.attrs) { | ||
const val = node.attrs[key] | ||
if (val !== null && key !== 'ychange') { | ||
type.setAttribute(key, val) | ||
} | ||
export const createTypeFromTextNodes = (nodes, mapping) => { | ||
const type = new Y.XmlText() | ||
const delta = nodes.map(node => ({ | ||
// @ts-ignore | ||
insert: node.text, | ||
attributes: marksToAttributes(node.marks) | ||
})) | ||
type.applyDelta(delta) | ||
mapping.set(type, nodes) | ||
return type | ||
} | ||
/** | ||
* @private | ||
* @param {PModel.Node} node prosemirror node | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {Y.XmlElement} | ||
*/ | ||
export const createTypeFromElementNode = (node, mapping) => { | ||
const type = new Y.XmlElement(node.type.name) | ||
for (let key in node.attrs) { | ||
const val = node.attrs[key] | ||
if (val !== null && key !== 'ychange') { | ||
type.setAttribute(key, val) | ||
} | ||
const ins = [] | ||
for (let i = 0; i < node.childCount; i++) { | ||
ins.push(createTypeFromNode(node.child(i), mapping)) | ||
} | ||
type.insert(0, ins) | ||
} | ||
type.insert(0, normalizePNodeContent(node).map(n => createTypeFromTextOrElementNode(n, mapping))) | ||
mapping.set(type, node) | ||
@@ -384,2 +388,10 @@ return type | ||
/** | ||
* @private | ||
* @param {PModel.Node|Array<PModel.Node>} node prosemirror text node | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {Y.XmlElement|Y.XmlText} | ||
*/ | ||
export const createTypeFromTextOrElementNode = (node, mapping) => node instanceof Array ? createTypeFromTextNodes(node, mapping) : createTypeFromElementNode(node, mapping) | ||
const equalAttrs = (pattrs, yattrs) => { | ||
@@ -397,19 +409,66 @@ const keys = Object.keys(pattrs).filter(key => pattrs[key] !== null) | ||
const equalYTextPText = (ytext, ptext) => { | ||
/** | ||
* @typedef {Array<Array<PModel.Node>|PModel.Node>} NormalizedPNodeContent | ||
*/ | ||
/** | ||
* @param {PModel.Node} pnode | ||
* @return {NormalizedPNodeContent} | ||
*/ | ||
export const normalizePNodeContent = pnode => { | ||
const c = pnode.content.content | ||
const res = [] | ||
for (let i = 0; i < c.length; i++) { | ||
const n = c[i] | ||
if (n.isText) { | ||
const textNodes = [] | ||
for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) { | ||
textNodes.push(tnode) | ||
} | ||
i-- | ||
res.push(textNodes) | ||
} else { | ||
res.push(n) | ||
} | ||
} | ||
return res | ||
} | ||
/** | ||
* @param {Y.XmlText} ytext | ||
* @param {Array<PModel.Node>} ptexts | ||
*/ | ||
const equalYTextPText = (ytext, ptexts) => { | ||
const delta = ytext.toDelta() | ||
if (delta.length === 0) { | ||
return ptext.text === '' | ||
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))) | ||
} | ||
/** | ||
* @param {Y.XmlElement|Y.XmlText|Y.XmlHook} ytype | ||
* @param {PModel.Node|Array<PModel.Node>} pnode | ||
*/ | ||
const equalYTypePNode = (ytype, pnode) => { | ||
if (ytype instanceof Y.XmlElement && !(pnode instanceof Array) && matchNodeName(ytype, pnode)) { | ||
const normalizedContent = normalizePNodeContent(pnode) | ||
return ytype._length === normalizedContent.length && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i])) | ||
} | ||
const d = delta[0] | ||
return d.insert === ptext.text && object.keys(d.attributes || {}).length === ptext.marks.length && ptext.marks.every(mark => equalAttrs(d.attributes[mark.type.name], mark.attrs)) | ||
return ytype instanceof Y.XmlText && pnode instanceof Array && equalYTextPText(ytype, pnode) | ||
} | ||
const equalYTypePNode = (ytype, pnode) => | ||
ytype.constructor === Y.XmlText | ||
? equalYTextPText(ytype, pnode) | ||
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, pnode.child(i)))) | ||
/** | ||
* @param {PModel.Node | Array<PModel.Node> | undefined} mapped | ||
* @param {PModel.Node | Array<PModel.Node>} pcontent | ||
*/ | ||
const mappedIdentity = (mapped, pcontent) => mapped === pcontent || (mapped instanceof Array && pcontent instanceof Array && mapped.length === pcontent.length && mapped.every((a, i) => pcontent[i] === a)) | ||
/** | ||
* @param {Y.XmlElement} ytype | ||
* @param {PModel.Node} pnode | ||
* @param {ProsemirrorMapping} mapping | ||
* @return {{ foundMappedChild: boolean, equalityFactor: number }} | ||
*/ | ||
const computeChildEqualityFactor = (ytype, pnode, mapping) => { | ||
const yChildren = ytype.toArray() | ||
const pChildCnt = pnode.childCount | ||
const pChildren = normalizePNodeContent(pnode) | ||
const pChildCnt = pChildren.length | ||
const yChildCnt = yChildren.length | ||
@@ -422,4 +481,4 @@ const minCnt = math.min(yChildCnt, pChildCnt) | ||
const leftY = yChildren[left] | ||
const leftP = pnode.child(left) | ||
if (mapping.get(leftY) === leftP) { | ||
const leftP = pChildren[left] | ||
if (mappedIdentity(mapping.get(leftY), leftP)) { | ||
foundMappedChild = true// definite (good) match! | ||
@@ -432,6 +491,6 @@ } else if (!equalYTypePNode(leftY, leftP)) { | ||
const rightY = yChildren[yChildCnt - right - 1] | ||
const rightP = pnode.child(pChildCnt - right - 1) | ||
if (mapping.get(rightY) !== rightP) { | ||
const rightP = pChildren[pChildCnt - right - 1] | ||
if (mappedIdentity(mapping.get(rightY), rightP)) { | ||
foundMappedChild = true | ||
} else if (!equalYTypePNode(rightP, rightP)) { | ||
} else if (!equalYTypePNode(rightY, rightP)) { | ||
break | ||
@@ -446,18 +505,68 @@ } | ||
const ytextTrans = ytext => { | ||
let str = '' | ||
/** | ||
* @type {Y.Item|null} | ||
*/ | ||
let n = ytext._start | ||
const nAttrs = {} | ||
while (n !== null) { | ||
if (!n.deleted) { | ||
if (n.countable && n.content instanceof Y.ContentString) { | ||
str += n.content.str | ||
} else if (n.content instanceof Y.ContentFormat) { | ||
nAttrs[n.content.key] = null | ||
} | ||
} | ||
n = n.right | ||
} | ||
return { | ||
str, | ||
nAttrs | ||
} | ||
} | ||
/** | ||
* @todo test this more | ||
* | ||
* @param {Y.Text} ytext | ||
* @param {Array<PModel.Node>} ptexts | ||
* @param {ProsemirrorMapping} mapping | ||
*/ | ||
const updateYText = (ytext, ptexts, mapping) => { | ||
mapping.set(ytext, ptexts) | ||
const { nAttrs, str } = ytextTrans(ytext) | ||
const content = ptexts.map(p => ({ insert: /** @type {any} */ (p).text, attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks)) })) | ||
const { insert, remove, index } = simpleDiff(str, content.map(c => c.insert).join('')) | ||
ytext.delete(index, remove) | ||
ytext.insert(index, insert) | ||
ytext.applyDelta(content.map(c => ({ retain: c.insert.length, attributes: c.attributes }))) | ||
} | ||
const marksToAttributes = marks => { | ||
const pattrs = {} | ||
marks.forEach(mark => { | ||
if (mark.type.name !== 'ychange') { | ||
pattrs[mark.type.name] = mark.attrs | ||
} | ||
}) | ||
return pattrs | ||
} | ||
/** | ||
* @private | ||
* @param {Y.Doc} y | ||
* @param {Y.XmlFragment} yDomFragment | ||
* @param {Object} pContent | ||
* @param {PModel.Node} pNode | ||
* @param {ProsemirrorMapping} mapping | ||
*/ | ||
const updateYFragment = (y, yDomFragment, pContent, mapping) => { | ||
if (yDomFragment instanceof Y.XmlElement && yDomFragment.nodeName !== pContent.type.name) { | ||
const updateYFragment = (y, yDomFragment, pNode, mapping) => { | ||
if (yDomFragment instanceof Y.XmlElement && yDomFragment.nodeName !== pNode.type.name) { | ||
throw new Error('node name mismatch!') | ||
} | ||
mapping.set(yDomFragment, pContent) | ||
mapping.set(yDomFragment, pNode) | ||
// update attributes | ||
if (yDomFragment instanceof Y.XmlElement) { | ||
const yDomAttrs = yDomFragment.getAttributes() | ||
const pAttrs = pContent.attrs | ||
const pAttrs = pNode.attrs | ||
for (let key in pAttrs) { | ||
@@ -480,3 +589,4 @@ if (pAttrs[key] !== null) { | ||
// update children | ||
const pChildCnt = pContent.childCount | ||
const pChildren = normalizePNodeContent(pNode) | ||
const pChildCnt = pChildren.length | ||
const yChildren = yDomFragment.toArray() | ||
@@ -490,4 +600,4 @@ const yChildCnt = yChildren.length | ||
const leftY = yChildren[left] | ||
const leftP = pContent.child(left) | ||
if (mapping.get(leftY) !== leftP) { | ||
const leftP = pChildren[left] | ||
if (!mappedIdentity(mapping.get(leftY), leftP)) { | ||
if (equalYTypePNode(leftY, leftP)) { | ||
@@ -502,6 +612,6 @@ // update mapping | ||
// find number of matching elements from right | ||
for (;right + left < minCnt; right++) { | ||
for (;right + left + 1 < minCnt; right++) { | ||
const rightY = yChildren[yChildCnt - right - 1] | ||
const rightP = pContent.child(pChildCnt - right - 1) | ||
if (mapping.get(rightY) !== rightP) { | ||
const rightP = pChildren[pChildCnt - right - 1] | ||
if (!mappedIdentity(mapping.get(rightY), rightP)) { | ||
if (equalYTypePNode(rightY, rightP)) { | ||
@@ -519,24 +629,8 @@ // update mapping | ||
const leftY = yChildren[left] | ||
const leftP = pContent.child(left) | ||
const leftP = pChildren[left] | ||
const rightY = yChildren[yChildCnt - right - 1] | ||
const rightP = pContent.child(pChildCnt - right - 1) | ||
if (leftY instanceof Y.XmlText && leftP.isText) { | ||
const rightP = pChildren[pChildCnt - right - 1] | ||
if (leftY instanceof Y.XmlText && leftP instanceof Array) { | ||
if (!equalYTextPText(leftY, leftP)) { | ||
// try to apply diff. Only if attrs don't match, delete insert | ||
// TODO: use a single ytext to hold all following Prosemirror Text nodes | ||
const pattrs = {} | ||
leftP.marks.forEach(mark => { | ||
if (mark.type.name !== 'ychange') { | ||
pattrs[mark.type.name] = mark.attrs | ||
} | ||
}) | ||
const delta = leftY.toDelta() | ||
if (delta.length === 1 && delta[0].insert && equalAttrs(pattrs, delta[0].attributes || {})) { | ||
const diff = simpleDiff(delta[0].insert, leftP.text) | ||
leftY.delete(diff.index, diff.remove) | ||
leftY.insert(diff.index, diff.insert, delta[0].attributes || {}) | ||
} else { | ||
yDomFragment.delete(left, 1) | ||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)]) | ||
} | ||
updateYText(leftY, leftP, mapping) | ||
} | ||
@@ -549,4 +643,4 @@ left += 1 | ||
// decide which which element to update | ||
const equalityLeft = computeChildEqualityFactor(leftY, leftP, mapping) | ||
const equalityRight = computeChildEqualityFactor(rightY, rightP, mapping) | ||
const equalityLeft = computeChildEqualityFactor(/** @type {Y.XmlElement} */ (leftY), /** @type {PModel.Node} */ (leftP), mapping) | ||
const equalityRight = computeChildEqualityFactor(/** @type {Y.XmlElement} */ (rightY), /** @type {PModel.Node} */ (rightP), mapping) | ||
if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) { | ||
@@ -563,10 +657,10 @@ updateRight = false | ||
if (updateLeft) { | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (leftY), leftP, mapping) | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (leftY), /** @type {PModel.Node} */ (leftP), mapping) | ||
left += 1 | ||
} else if (updateRight) { | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (rightY), rightP, mapping) | ||
updateYFragment(y, /** @type {Y.XmlFragment} */ (rightY), /** @type {PModel.Node} */ (rightP), mapping) | ||
right += 1 | ||
} else { | ||
yDomFragment.delete(left, 1) | ||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)]) | ||
yDomFragment.insert(left, [createTypeFromTextOrElementNode(leftP, mapping)]) | ||
left += 1 | ||
@@ -583,3 +677,3 @@ } | ||
for (let i = left; i < pChildCnt - right; i++) { | ||
ins.push(createTypeFromNode(pContent.child(i), mapping)) | ||
ins.push(createTypeFromTextOrElementNode(pChildren[i], mapping)) | ||
} | ||
@@ -586,0 +680,0 @@ yDomFragment.insert(left, ins) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
2358814
21280