diff --git a/README.md b/README.md index b499c80..6116e07 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ The "API" for the clients to call to send Automerge operations. ... } ``` -and +and ``` componentDidMount = () => { this.connection.open() @@ -181,7 +181,7 @@ This receives a message from the server regarding new remote changes and applies this.setState({ value: Value.fromJSON(newJson) }) } ``` -This is the failure handler when the Automerge -> Slate conversion fails. +This is the failure handler when the Automerge -> Slate conversion fails. - When sending a change: ``` @@ -205,6 +205,7 @@ This converts the Slate operation to Automerge operations, applies it to the cli 3) If a new client joins, do they have to initialize the entire Automerge document (with the history)? Or can they just start from the latest snapshot? 4) What's a good way to batch changes from a client? To reduce network traffic, it would be nice to batch keystrokes within a second of each other together. 5) How should we send over information (such as cursor location) which we don't want to persist? +6) Currently does not include support for marks (especially with the Slate 0.34 update) ## Original README below diff --git a/package.json b/package.json index 83c3214..49e74e5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "react": "16.0.0", "react-dom": "16.0.0", "react-scripts": "1.0.14", - "slate": "0.33.6", + "slate": "0.34.0", "slate-react": "0.12.4", "slate-edit-list": "0.11.3" }, diff --git a/src/example/App.js b/src/example/App.js index 316f785..44190ca 100644 --- a/src/example/App.js +++ b/src/example/App.js @@ -1,15 +1,15 @@ import { Client } from "./client" import { initialValue } from "../utils/initialSlateValue" import { slateCustomToJson } from "../libs/slateAutomergeBridge" -import { Value } from 'slate' -import Automerge from 'automerge' -import React from 'react' -import './App.css'; +import { Value } from "slate" +import Automerge from "automerge" +import React from "react" +import "./App.css"; const docId = 1; let doc = Automerge.init(); const initialSlateValue = Value.fromJSON(initialValue); -doc = Automerge.change(doc, 'Initialize Slate state', doc => { +doc = Automerge.change(doc, "Initialize Slate state", doc => { doc.note = slateCustomToJson(initialSlateValue.document); }) // const savedAutomergeDoc = Automerge.save(doc); diff --git a/src/example/client.js b/src/example/client.js index 135cdf4..6b7359e 100644 --- a/src/example/client.js +++ b/src/example/client.js @@ -3,13 +3,13 @@ */ import { applyAutomergeOperations, applySlateOperations, automergeJsonToSlate } from "../libs/slateAutomergeBridge" -import { Editor } from 'slate-react' -import { Value } from 'slate' -import Automerge from 'automerge' -import EditList from 'slate-edit-list' +import { Editor } from "slate-react" +import { Value } from "slate" +import Automerge from "automerge" +import EditList from "slate-edit-list" import Immutable from "immutable"; -import React from 'react' -import './client.css'; +import React from "react" +import "./client.css"; const plugin = EditList(); @@ -22,25 +22,25 @@ function renderNode(props) { .contains(node); switch (node.type) { - case 'ul_list': + case "ul_list": return ; - case 'ol_list': + case "ol_list": return
    {children}
; - case 'list_item': + case "list_item": return (
  • {props.children}
  • ); - case 'paragraph': + case "paragraph": return
    {children}
    ; - case 'heading': + case "heading": return

    {children}

    ; default: return
    {children}
    ; @@ -164,7 +164,7 @@ export class Client extends React.Component { if (isOnline) { this.props.connectionHandler(this.props.clientId, true) this.connection.open() - let clock = this.docSet.getDoc(this.props.docId)._state.getIn(['opSet', 'clock']); + let clock = this.docSet.getDoc(this.props.docId)._state.getIn(["opSet", "clock"]); this.props.sendMessage(this.props.clientId, { clock: clock, docId: this.props.docId, @@ -217,7 +217,7 @@ export class Client extends React.Component { */ renderInternalClock = () => { try { - let clockList = this.docSet.getDoc(this.props.docId)._state.getIn(['opSet', 'clock']); + let clockList = this.docSet.getDoc(this.props.docId)._state.getIn(["opSet", "clock"]); let clockComponents = []; clockList.forEach((value, actorId) => { actorId = actorId.substr(0, actorId.indexOf("-")) diff --git a/src/libs/convertAutomergeToSlateOps.js b/src/libs/applyAutomergeOperations.js similarity index 71% rename from src/libs/convertAutomergeToSlateOps.js rename to src/libs/applyAutomergeOperations.js index ad3eeb4..fa48b52 100644 --- a/src/libs/convertAutomergeToSlateOps.js +++ b/src/libs/applyAutomergeOperations.js @@ -23,8 +23,8 @@ export const applyAutomergeOperations = (opSetDiff, change, failureCallback) => } catch (e) { // If an error occurs, release the Slate Value based on the Automerge // document, which is the ground truth. - console.debug("The following warning is fine:") - console.debug(e) + console.info("The following warning is fine:") + console.info(e) if (failureCallback) { failureCallback(); } @@ -35,7 +35,7 @@ export const applyAutomergeOperations = (opSetDiff, change, failureCallback) => * @function convertAutomergeToSlateOps * @desc Converts Automerge operations to Slate operations. * @param {Array} automergeOps - a list of Automerge operations created from Automerge.diff - * @return {Array} List of Slate operations + * @return {Array} Array of Slate operations */ export const convertAutomergeToSlateOps = (automergeOps) => { // To build objects from Automerge operations @@ -53,9 +53,7 @@ export const convertAutomergeToSlateOps = (automergeOps) => { case "remove": temp = automergeOpRemove(op, objIdMap); objIdMap = temp.objIdMap; - if (temp.slateOp) { - slateOps[idx] = [temp.slateOp] - } + slateOps[idx] = temp.slateOps break; case "set": objIdMap = automergeOpSet(op, objIdMap); @@ -64,6 +62,7 @@ export const convertAutomergeToSlateOps = (automergeOps) => { temp = automergeOpInsert(op, objIdMap); objIdMap = temp.objIdMap; deferredOps[idx] = temp.deferredOps; + slateOps[idx] = temp.slateOps; if (temp.deferredOps && !containsDeferredOps) { containsDeferredOps = true; } @@ -90,14 +89,14 @@ export const convertAutomergeToSlateOps = (automergeOps) => { */ const automergeOpCreate = (op, objIdMap) => { switch (op.type) { - case 'map': + case "map": objIdMap[op.obj] = {} break; - case 'list': + case "list": objIdMap[op.obj] = [] break; default: - console.error('`create`, unsupported type: ', op.type) + console.error("`create`, unsupported type: ", op.type) } return objIdMap; } @@ -107,10 +106,11 @@ const automergeOpCreate = (op, objIdMap) => { * @desc Handles the `remove` Automerge operation * @param {Object} op - Automerge operation * @param {Object} objIdMap - Map from the objectId to created object - * @return {Object} The objIdMap and corresponding Slate Operations for this operation + * @return {Object} The objIdMap and array of corresponding Slate Operations for + * this operation */ const automergeOpRemove = (op, objIdMap) => { - let slatePath, slateOp + let nodePath, slateOps let pathString = op.path.slice(1).join("/") const lastObjectPath = op.path[op.path.length - 1]; pathString = pathString.match(/\d+/g) @@ -119,41 +119,38 @@ const automergeOpRemove = (op, objIdMap) => { objIdMap[op.obj].splice(op.index, 1) } else { switch (lastObjectPath) { - case 'characters': + case "text": // Remove a character - if (pathString) { - slatePath = pathString.map(x => { return parseInt(x, 10); }); - } else { - slatePath = [op.index] - } + nodePath = pathString.map(x => { return parseInt(x, 10); }) + nodePath = nodePath.splice(0, nodePath.length - 1) - slateOp = { - type: 'remove_text', - path: slatePath, + slateOps = [{ + type: "remove_text", + path: nodePath, offset: op.index, - text: '*', + text: "*", marks: [] - } + }] break; - case 'nodes': + case "nodes": // Remove a node if (pathString) { - slatePath = pathString.map(x => { return parseInt(x, 10); }); - slatePath = [...slatePath, op.index]; + nodePath = pathString.map(x => { return parseInt(x, 10); }); + nodePath = [...nodePath, op.index]; } else { - slatePath = [op.index] + nodePath = [op.index] } - slateOp = { - type: 'remove_node', - path: slatePath, - } + slateOps = [{ + type: "remove_node", + path: nodePath, + }] break; default: - console.error('`remove`, unsupported node type:', lastObjectPath) + console.error("`remove`, unsupported node type:", lastObjectPath) } } - return { objIdMap: objIdMap, slateOp: slateOp }; + return { objIdMap: objIdMap, slateOps: slateOps }; } @@ -165,7 +162,7 @@ const automergeOpRemove = (op, objIdMap) => { * @return {Object} Map from Object Id to Object */ const automergeOpSet = (op, objIdMap) => { - if (op.hasOwnProperty('link')) { + if (op.hasOwnProperty("link")) { // What's the point of the `link` field? All my experiments // have `link` = true if (op.link) { @@ -175,7 +172,7 @@ const automergeOpSet = (op, objIdMap) => { objIdMap[op.obj][op.key] = objIdMap[op.value] } else { // TODO: Does this ever happen? - console.error('`set`, unable to find objectId: ', op.value) + console.error("`set`, unable to find objectId: ", op.value) } } } else { @@ -189,12 +186,13 @@ const automergeOpSet = (op, objIdMap) => { * @desc Handles the `insert` Automerge operation * @param {Object} op - Automerge operation * @param {Object} objIdMap - Map from the objectId to created object - * @return {Object} Containing the map from Object Id to Object and deferred operation + * @return {Object} Containing the map from Object Id to Object, + * deferred operation, and array of Slate operations. */ const automergeOpInsert = (op, objIdMap) => { if (op.link) { // Check if inserting into a newly created object or one that - // already exists in our Automerge document + // already exists in our Automerge document. if (objIdMap.hasOwnProperty(op.obj)) { objIdMap[op.obj].splice(op.index, 0, objIdMap[op.value]) } else { @@ -202,10 +200,30 @@ const automergeOpInsert = (op, objIdMap) => { } } else { - // TODO: Does this ever happen? - console.log('op.action is `insert`, but link is false') + // If adding in a primitive to a list, then op.link is False. + // This is used when adding in a character to the text field of a Leaf + // node. + if (objIdMap.hasOwnProperty(op.obj)) { + objIdMap[op.obj].splice(op.index, 0, op.value) + } else { + let pathString = op.path.slice(1).join("/") + pathString = pathString.match(/\d+/g) + let nodePath = pathString.map(x => { + return parseInt(x, 10); + }); + nodePath = nodePath.splice(0, nodePath.length - 1) + + const slateOp = { + type: "insert_text", + path: nodePath, + offset: op.index, + text: op.value, + marks: [] + } + return { objIdMap: objIdMap, deferredOps: null, slateOps: [slateOp] } + } } - return { objIdMap: objIdMap, deferredOps: null }; + return { objIdMap: objIdMap } } /** @@ -220,21 +238,21 @@ const automergeOpInsertText = (deferredOps, objIdMap, slateOps) => { // We know all ops in this list have the following conditions true: // - op.action === `insert` // - pathMap.hasOwnProperty(op.obj) - // - typeof pathMap[op.obj] === 'string' || + // - typeof pathMap[op.obj] === "string" || // pathMap[op.obj] instanceof String deferredOps.forEach((op, idx) => { if (op === undefined || op === null) return; const insertInto = op.path.slice(1).join("/") - let pathString, slatePath + let pathString, nodePath let slateOp = [] if (insertInto === "nodes") { - // If inserting into the "root" of the tree, the slatePath is [] - slatePath = [] + // If inserting into the "root" of the tree, the nodePath is [] + nodePath = [] } else { pathString = insertInto.match(/\d+/g) - slatePath = pathString.map(x => { + nodePath = pathString.map(x => { return parseInt(x, 10); }); } @@ -244,8 +262,8 @@ const automergeOpInsertText = (deferredOps, objIdMap, slateOps) => { switch (nodeToAdd.object) { case "character": slateOp.push({ - type: 'insert_text', - path: slatePath, + type: "insert_text", + path: nodePath, offset: op.index, text: objIdMap[op.value].text, marks: objIdMap[op.value].marks @@ -253,10 +271,10 @@ const automergeOpInsertText = (deferredOps, objIdMap, slateOps) => { break; case "block": const newNode = automergeJsonToSlate(nodeToAdd); - slatePath.push(op.index) + nodePath.push(op.index) slateOp.push({ type: "insert_node", - path: slatePath, + path: nodePath, node: newNode, }) break; @@ -278,7 +296,7 @@ const automergeOpInsertText = (deferredOps, objIdMap, slateOps) => { const flattenArray = (array_of_lists) => { let newList = [] array_of_lists.forEach((items) => { - if (items !== null) { + if (items !== null && items !== undefined) { items.forEach((item) => { newList.push(item) }) } }); diff --git a/src/libs/applySlateOperations.js b/src/libs/applySlateOperations.js index e69de29..65bb47c 100644 --- a/src/libs/applySlateOperations.js +++ b/src/libs/applySlateOperations.js @@ -0,0 +1,261 @@ +/** + * This converts a Slate operation to operations that act on an Automerge + * document. This converts the functions in + * https://github.com/ianstormtaylor/slate/blob/master/packages/slate/src/operations/apply.js + * to modify the Automerge JSON instead of the Slate Value. + * + * NOTE: The move operation in Slate is a linking op in Automerge. For now, to + * simplify the conversion from Automerge operationsto Slate, rather than move, + * we delete the node and re-insert a new node. This results in more Automerge + * ops but makes it so that the reverse conversion + * (in convertAutomerge.automergeOpInsertText) does not need to know the path + * to the previous node. If we update Automerge to contain the path to the + * old node, we can use the move node operation. + */ + + +import Automerge from "automerge" +import slateCustomToJson from "./slateCustomToJson" + +const allowedOperations = [ + "insert_text", "remove_text", "insert_node", "split_node", + "remove_node", "merge_node", "set_node", "move_node" +]; + +/** + * @function applySlateOperations + * @desc converts a Slate operation to operations that act on an Automerge document + * @param {Automerge.DocSet} doc - the Automerge document + * @param {number} doc - Automerge document id + * @param {List} slateOperations - a list of Slate Operations + * @param {number} clientId - (optional) Id of the client + */ +export const applySlateOperations = (docSet, docId, slateOperations, clientId) => { + const currentDoc = docSet.getDoc(docId) + if (currentDoc) { + const message = clientId ? `Client ${clientId}` : "Change log" + const docNew = Automerge.change(currentDoc, message, doc => { + // Use the Slate operations to modify the Automerge document. + applySlateOperationsHelper(doc, slateOperations) + }) + docSet.setDoc(docId, docNew) + } +} + +/** + * @function applySlateOperationsHelper + * @desc converts a Slate operation to operations that act on an Automerge document + * @param {Automerge.document} doc - the Automerge document + * @param {List} operations - a list of Slate Operations + */ +const applySlateOperationsHelper = (doc, operations) => { + operations.forEach(op => { + if (allowedOperations.indexOf(op.type) === -1) { + return; + } + const { + path, offset, text, length, mark, + node, position, properties, newPath + } = op; + const index = path[path.length - 1]; + const rest = path.slice(0, -1) + let currentNode = doc.note; + switch (op.type) { + // NOTE: Marks are definitely broken as of Slate 0.34 + // case "add_mark": + // // Untested + // path.forEach(el => { + // currentNode = currentNode.nodes[el]; + // }) + // currentNode.characters.forEach((char, i) => { + // if (i < offset) return; + // if (i >= offset + length) return; + // const hasMark = char.marks.find((charMark) => { + // return charMark.type === mark.type + // }) + // if (!hasMark) { + // char.marks.push(mark) + // } + // }) + // break; + // case "remove_mark": + // // Untested + // path.forEach(el => { + // currentNode = currentNode.nodes[el]; + // }) + // currentNode.characters.forEach((char, i) => { + // if (i < offset) return; + // if (i >= offset + length) return; + // const markIndex = char.marks.findIndex((charMark) => { + // return charMark.type === mark.type + // }) + // if (markIndex) { + // char.marks.deleteAt(markIndex, 1); + // } + // }) + // break; + // case "set_mark": + // // Untested + // path.forEach(el => { + // currentNode = currentNode.nodes[el]; + // }) + // currentNode.characters.forEach((char, i) => { + // if (i < offset) return; + // if (i >= offset + length) return; + // const markIndex = char.marks.findIndex((charMark) => { + // return charMark.type === mark.type + // }) + // if (markIndex) { + // char.marks[markIndex] = mark; + // } + // }) + // break; + case "insert_text": + path.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + // Assumes no marks and only 1 leaf + currentNode.leaves[0].text.insertAt(offset, text); + break; + case "remove_text": + path.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + // Assumes no marks and only 1 leaf + currentNode.leaves[0].text.deleteAt(offset, text.length); + break; + case "split_node": + rest.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + let childOne = currentNode.nodes[index]; + let childTwo = JSON.parse(JSON.stringify(currentNode.nodes[index])); + if (childOne.object === "text") { + childOne.leaves[0].text.splice(position) + childTwo.leaves[0].text.splice(0, position) + } else { + childOne.nodes.splice(position) + childTwo.nodes.splice(0, position) + } + currentNode.nodes.insertAt(index + 1, childTwo); + if (properties) { + if (currentNode.nodes[index + 1].object !== "text") { + let propertiesJSON = slateCustomToJson(properties); + Object.keys(propertiesJSON).forEach(key => { + if (propertiesJSON.key) { + currentNode.nodes[index + 1][key] = propertiesJSON.key; + } + }) + } + } + break; + case "merge_node": + rest.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + let one = currentNode.nodes[index - 1]; + let two = currentNode.nodes[index]; + if (one.object === "text") { + // TOFIX: This is to strip out the objectId and create a new list. + // Not ideal at all but Slate can't do the linking that Automerge can + // and it's alot of work to try to move references in Slate. + // See Note above. + let temp = JSON.parse(JSON.stringify(two.leaves[0].text)) + // one.leaves.push(...temp) + one.leaves[0].text.push(...temp) + } else { + // TOFIX: This is to strip out the objectId and create a new list. + // Not ideal at all but Slate can't do the linking that Automerge can + // and it's alot of work to try to move references in Slate. + // See Note above. + let temp = JSON.parse(JSON.stringify(two.nodes)) + one.nodes.push(...temp) + } + currentNode.nodes.deleteAt(index, 1); + break; + case "insert_node": + rest.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + currentNode.nodes.insertAt(index, slateCustomToJson(node)); + break; + case "remove_node": + rest.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + currentNode.nodes.deleteAt(index, 1); + break; + case "set_node": + path.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + for (let attrname in properties) { + currentNode[attrname] = properties[attrname]; + } + break; + case "move_node": + const newIndex = newPath[newPath.length - 1] + const newParentPath = newPath.slice(0, -1) + const oldParentPath = path.slice(0, -1) + const oldIndex = path[path.length - 1] + + // Remove the old node from it's current parent. + oldParentPath.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + let nodeToMove = currentNode.nodes[oldIndex]; + + // Find the new target... + if ( + oldParentPath.every((x, i) => x === newParentPath[i]) && + oldParentPath.length === newParentPath.length + ) { + // Do nothing + } else if ( + oldParentPath.every((x, i) => x === newParentPath[i]) && + oldIndex < newParentPath[oldParentPath.length] + ) { + // Remove the old node from it's current parent. + currentNode.nodes.deleteAt(oldIndex, 1); + + // Otherwise, if the old path removal resulted in the new path being no longer + // correct, we need to decrement the new path at the old path's last index. + currentNode = doc.note; + newParentPath[oldParentPath.length]-- + newParentPath.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + + // TOFIX: This is to strip out the objectId and create a new list. + // Not ideal at all but Slate can't do the linking that Automerge can + // and it's alot of work to try to move references in Slate. + // See Note above. + nodeToMove = JSON.parse(JSON.stringify(nodeToMove)); + // Insert the new node to its new parent. + currentNode.nodes.insertAt(newIndex, nodeToMove); + } else { + // Remove the old node from it's current parent. + currentNode.nodes.deleteAt(oldIndex, 1); + + // Otherwise, we can just grab the target normally... + currentNode = doc.note; + newParentPath.forEach(el => { + currentNode = currentNode.nodes[el]; + }) + + // TOFIX: This is to strip out the objectId and create a new list. + // Not ideal at all but Slate can't do the linking that Automerge can + // and it's alot of work to try to move references in Slate. + // See Note above. + nodeToMove = JSON.parse(JSON.stringify(nodeToMove)); + // Insert the new node to its new parent. + currentNode.nodes.insertAt(newIndex, nodeToMove); + + } + break; + default: + console.log("In default case") + break; + } + }) +} diff --git a/src/libs/automergeJsonToSlate.js b/src/libs/automergeJsonToSlate.js index 8bbc6a2..b0d60b0 100644 --- a/src/libs/automergeJsonToSlate.js +++ b/src/libs/automergeJsonToSlate.js @@ -1,23 +1,15 @@ /** * This contains a custom fromJSON function for Automerge objects intended to - * initialize as a Slate Value. - * This will not be needed once the PR related to - * https://github.com/ianstormtaylor/slate/issues/1813 is completed. + * initialize as a Slate Value. This currently does not have support for marks. */ -const getLeaves = (characterList) => { - let leaves = []; - let text = "" - characterList.forEach((character) => { - text = text.concat(character.text) - }) - let leaf = { +const createLeaf = (leaf) => { + let newLeaf = { object: "leaf", marks: [], - text: text + text: leaf.text.join("") } - leaves.push(leaf) - return leaves + return newLeaf } const fromJSON = (value) => { @@ -38,7 +30,7 @@ const fromJSON = (value) => { }) if (value.object === "text") { - newJson.leaves = getLeaves(value.characters) + newJson.leaves = value.leaves.map(leaf => createLeaf(leaf)) } return newJson; diff --git a/src/libs/slateAutomergeBridge.js b/src/libs/slateAutomergeBridge.js index e014bf3..2f71845 100644 --- a/src/libs/slateAutomergeBridge.js +++ b/src/libs/slateAutomergeBridge.js @@ -1,8 +1,8 @@ -import { applySlateOperations } from "./slateOpsToAutomerge" -import { applyAutomergeOperations } from "./convertAutomergeToSlateOps" +import { applySlateOperations } from "./applySlateOperations" +import { applyAutomergeOperations } from "./applyAutomergeOperations" import slateCustomToJson from "./slateCustomToJson" -import automergeJsonToSlate from "../libs/automergeJsonToSlate" +import automergeJsonToSlate from "./automergeJsonToSlate" export { applySlateOperations, applyAutomergeOperations, automergeJsonToSlate, slateCustomToJson -} \ No newline at end of file +} diff --git a/src/libs/slateCustomToJson.js b/src/libs/slateCustomToJson.js index b101252..e5f1052 100644 --- a/src/libs/slateCustomToJson.js +++ b/src/libs/slateCustomToJson.js @@ -1,11 +1,16 @@ /** * This contains a custom toJSON function for Slate objects intended to copy - * exactly the Slate value. The code was modified from the toJSON() methods in + * exactly the Slate value for Automerge with the exception of Text nodes. + * The code was modified from the toJSON() methods in * https://github.com/ianstormtaylor/slate/tree/master/packages/slate/src/models - * This should not be needed once the PR related to - * https://github.com/ianstormtaylor/slate/issues/1813 is completed. + * + * Primary differences: + * - For leaf nodes, text is changed from a string to an array of characters. + Should use Automerge.Text nodes if possible + * - Currently does not support marks. */ +import Automerge from "automerge" /** * @function toJSON @@ -29,12 +34,6 @@ const toJSON = (value, options = {}) => { nodes: value.nodes.toArray().map(n => toJSON(n, options)), type: value.type, } - case "character": - return { - object: value.object, - marks: value.marks.toArray().map(m => toJSON(m, options)), - text: value.text, - } case "data": return object.toJSON(); case "document": @@ -58,10 +57,12 @@ const toJSON = (value, options = {}) => { type: value.type, } case "leaf": + // Should convert leaf.text to an Automerge.Text object + const automergeText = value.text.split("") return { object: value.object, marks: value.marks.toArray().map(m => toJSON(m, options)), - text: value.text, + text: automergeText, } case "mark": return { @@ -80,8 +81,7 @@ const toJSON = (value, options = {}) => { focusOffset: value.focusOffset, isBackward: value.isBackward, isFocused: value.isFocused, - marks: - value.marks === null ? null : value.marks.toArray().map(m => toJSON(m, options)), + marks: value.marks === null ? null : value.marks.toArray().map(m => toJSON(m, options)), } case "schema": return { @@ -93,7 +93,7 @@ const toJSON = (value, options = {}) => { case "text": return { object: value.object, - characters: value.characters.toArray().map(c => toJSON(c, options)) + leaves: value.leaves.toArray().map(c => toJSON(c, options)) } case "value": return valueJSON(value, options) diff --git a/src/libs/slateOpsToAutomerge.js b/src/libs/slateOpsToAutomerge.js deleted file mode 100644 index b0fc9f4..0000000 --- a/src/libs/slateOpsToAutomerge.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * This converts a Slate operation to operations that act on an Automerge - * document. This converts the functions in - * https://github.com/ianstormtaylor/slate/blob/master/packages/slate/src/operations/apply.js - * to modify the Automerge JSON instead of the Slate Value. - * - * NOTE: The move operation in Slate is a linking op in Automerge. For now, to - * simplify the conversion from Automerge operationsto Slate, rather than move, - * we delete the node and re-insert a new node. This results in more Automerge - * ops but makes it so that the reverse conversion - * (in convertAutomerge.automergeOpInsertText) does not need to know the path - * to the previous node. If we update Automerge to contain the path to the - * old node, we can use the move node operation. - */ - - -import Automerge from 'automerge' -import slateCustomToJson from "./slateCustomToJson" - -const allowedOperations = [ - "insert_text", "remove_text", "insert_node", "split_node", - "remove_node", "merge_node", "set_node", "move_node" -]; - -/** - * @function applySlateOperations - * @desc converts a Slate operation to operations that act on an Automerge document - * @param {Automerge.DocSet} doc - the Automerge document - * @param {number} doc - Automerge document id - * @param {List} slateOperations - a list of Slate Operations - * @param {number} clientId - (optional) Id of the client - */ -export const applySlateOperations = (docSet, docId, slateOperations, clientId) => { - const currentDoc = docSet.getDoc(docId) - if (currentDoc) { - const message = clientId ? `Client ${clientId}` : "Change log" - const docNew = Automerge.change(currentDoc, message, doc => { - // Use the Slate operations to modify the Automerge document. - applySlateOperationsHelper(doc, slateOperations) - }) - docSet.setDoc(docId, docNew) - } -} - -/** - * @function applySlateOperationsHelper - * @desc converts a Slate operation to operations that act on an Automerge document - * @param {Automerge.document} doc - the Automerge document - * @param {List} operations - a list of Slate Operations - */ -const applySlateOperationsHelper = (doc, operations) => { - operations.forEach(op => { - if (allowedOperations.indexOf(op.type) === -1) { - return; - } - const { - path, offset, text, length, mark, - node, position, properties, newPath - } = op; - const index = path[path.length - 1]; - const rest = path.slice(0, -1) - let currentNode = doc.note; - switch (op.type) { - case "add_mark": - // Untested - path.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - currentNode.characters.forEach((char, i) => { - if (i < offset) return; - if (i >= offset + length) return; - const hasMark = char.marks.find((charMark) => { - return charMark.type === mark.type - }) - if (!hasMark) { - char.marks.push(mark) - } - }) - break; - case "remove_mark": - // Untested - path.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - currentNode.characters.forEach((char, i) => { - if (i < offset) return; - if (i >= offset + length) return; - const markIndex = char.marks.findIndex((charMark) => { - return charMark.type === mark.type - }) - if (markIndex) { - char.marks.deleteAt(markIndex, 1); - } - }) - break; - case "set_mark": - // Untested - path.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - currentNode.characters.forEach((char, i) => { - if (i < offset) return; - if (i >= offset + length) return; - const markIndex = char.marks.findIndex((charMark) => { - return charMark.type === mark.type - }) - if (markIndex) { - char.marks[markIndex] = mark; - } - }) - break; - case "insert_text": - path.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - const characterNode = { - object: "character", - marks: [], - text: text, - } - currentNode.characters.insertAt(offset, characterNode); - break; - case "remove_text": - path.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - currentNode.characters.deleteAt(offset, text.length); - break; - case "split_node": - rest.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - let childOne = currentNode.nodes[index]; - let childTwo = JSON.parse(JSON.stringify(currentNode.nodes[index])); - if (childOne.object === "text") { - childOne.characters.splice(position) - childTwo.characters.splice(0, position) - } else { - childOne.nodes.splice(position) - childTwo.nodes.splice(0, position) - } - currentNode.nodes.insertAt(index + 1, childTwo); - if (properties) { - if (currentNode.nodes[index + 1].object !== "text") { - let propertiesJSON = slateCustomToJson(properties); - Object.keys(propertiesJSON).forEach(key => { - if (propertiesJSON.key) { - currentNode.nodes[index + 1][key] = propertiesJSON.key; - } - }) - } - } - break; - case "merge_node": - rest.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - let one = currentNode.nodes[index - 1]; - let two = currentNode.nodes[index]; - if (one.object === "text") { - // TOFIX: This is to strip out the objectId and create a new list. - // Not ideal at all but Slate can't do the linking that Automerge can - // and it's alot of work to try to move references in Slate. - // See Note above. - let temp = JSON.parse(JSON.stringify(two.characters)) - one.characters.push(...temp) - } else { - // TOFIX: This is to strip out the objectId and create a new list. - // Not ideal at all but Slate can't do the linking that Automerge can - // and it's alot of work to try to move references in Slate. - // See Note above. - let temp = JSON.parse(JSON.stringify(two.nodes)) - one.nodes.push(...temp) - } - currentNode.nodes.deleteAt(index, 1); - break; - case "insert_node": - rest.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - currentNode.nodes.insertAt(index, slateCustomToJson(node)); - break; - case "remove_node": - rest.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - currentNode.nodes.deleteAt(index, 1); - break; - case "set_node": - path.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - for (let attrname in properties) { - currentNode[attrname] = properties[attrname]; - } - break; - case "move_node": - const newIndex = newPath[newPath.length - 1] - const newParentPath = newPath.slice(0, -1) - const oldParentPath = path.slice(0, -1) - const oldIndex = path[path.length - 1] - - // Remove the old node from it's current parent. - oldParentPath.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - let nodeToMove = currentNode.nodes[oldIndex]; - - // Find the new target... - if ( - oldParentPath.every((x, i) => x === newParentPath[i]) && - oldParentPath.length === newParentPath.length - ) { - // Do nothing - } else if ( - oldParentPath.every((x, i) => x === newParentPath[i]) && - oldIndex < newParentPath[oldParentPath.length] - ) { - // Remove the old node from it's current parent. - currentNode.nodes.deleteAt(oldIndex, 1); - - // Otherwise, if the old path removal resulted in the new path being no longer - // correct, we need to decrement the new path at the old path's last index. - currentNode = doc.note; - newParentPath[oldParentPath.length]-- - newParentPath.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - - // TOFIX: This is to strip out the objectId and create a new list. - // Not ideal at all but Slate can't do the linking that Automerge can - // and it's alot of work to try to move references in Slate. - // See Note above. - nodeToMove = JSON.parse(JSON.stringify(nodeToMove)); - // Insert the new node to its new parent. - currentNode.nodes.insertAt(newIndex, nodeToMove); - } else { - // Remove the old node from it's current parent. - currentNode.nodes.deleteAt(oldIndex, 1); - - // Otherwise, we can just grab the target normally... - currentNode = doc.note; - newParentPath.forEach(el => { - currentNode = currentNode.nodes[el]; - }) - - // TOFIX: This is to strip out the objectId and create a new list. - // Not ideal at all but Slate can't do the linking that Automerge can - // and it's alot of work to try to move references in Slate. - // See Note above. - nodeToMove = JSON.parse(JSON.stringify(nodeToMove)); - // Insert the new node to its new parent. - currentNode.nodes.insertAt(newIndex, nodeToMove); - - } - break; - default: - console.log("In default case") - break; - } - }) -} diff --git a/yarn.lock b/yarn.lock index d182e06..6ced5ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -124,8 +124,8 @@ acorn@^4.0.3, acorn@^4.0.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" acorn@^5.0.0, acorn@^5.5.0: - version "5.6.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7" + version "5.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8" address@1.0.3, address@^1.0.1: version "1.0.3" @@ -1426,12 +1426,12 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000852" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000852.tgz#c37a706048f8d81f87946a7c13f39ed636876659" + version "1.0.30000856" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000856.tgz#fbebb99abe15a5654fc7747ebb5315bdfde3358f" caniuse-lite@^1.0.30000697, caniuse-lite@^1.0.30000792: - version "1.0.30000852" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000852.tgz#8b7510cec030cac7842e52beca2bf292af65f935" + version "1.0.30000856" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000856.tgz#ecc16978135a6f219b138991eb62009d25ee8daa" capture-stack-trace@^1.0.0: version "1.0.0" @@ -5730,8 +5730,8 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" psl@^1.1.24: - version "1.1.27" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.27.tgz#2b2c77019db86855170d903532400bf71ee085b6" + version "1.1.28" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.28.tgz#4fb6ceb08a1e2214d4fd4de0ca22dae13740bc7b" public-encrypt@^4.0.0: version "4.0.2" @@ -6462,8 +6462,8 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" slate-base64-serializer@^0.2.29: - version "0.2.32" - resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.32.tgz#71de1644b212351684e3c8694fca9a46699fbcb9" + version "0.2.34" + resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.34.tgz#8a310672bf2f1b00dd469bc5e5247c20bb1ecd40" dependencies: isomorphic-base64 "^1.0.2" @@ -6476,14 +6476,14 @@ slate-edit-list@0.11.3: resolved "https://registry.yarnpkg.com/slate-edit-list/-/slate-edit-list-0.11.3.tgz#d27ff2ff93a83bef49131a6a44b87a9558c9d44c" slate-plain-serializer@^0.5.10: - version "0.5.13" - resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.5.13.tgz#826c9e75517b68f3c08a36805087abd87e27e1e2" + version "0.5.15" + resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.5.15.tgz#a1a306ae2f395ae90bf0f618799d886a4f0ce499" dependencies: slate-dev-logger "^0.1.39" slate-prop-types@^0.4.27: - version "0.4.30" - resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.30.tgz#6bd20bee862bdffc79ff5798656fc9a1e58ab3b3" + version "0.4.32" + resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.32.tgz#3d39a6db4b61a416ea54af6512f5ffa5542095d2" dependencies: slate-dev-logger "^0.1.39" @@ -6507,13 +6507,13 @@ slate-react@0.12.4: slate-plain-serializer "^0.5.10" slate-prop-types "^0.4.27" -slate-schema-violations@^0.1.10: - version "0.1.11" - resolved "https://registry.yarnpkg.com/slate-schema-violations/-/slate-schema-violations-0.1.11.tgz#3a7ccdaa6539f11edb30c1d6d826c4e4c74ef5d9" +slate-schema-violations@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/slate-schema-violations/-/slate-schema-violations-0.1.13.tgz#0b6133ff8e1c0237714249ef6d6adf7b97908985" -slate@0.33.6: - version "0.33.6" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.33.6.tgz#0c7cb193cc5adeecec5c81e2ec0c86ab23dd6755" +slate@0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/slate/-/slate-0.34.0.tgz#967d24460f2caf32e409251d042e25f789420d6b" dependencies: debug "^3.1.0" direction "^0.1.5" @@ -6522,7 +6522,7 @@ slate@0.33.6: is-plain-object "^2.0.4" lodash "^4.17.4" slate-dev-logger "^0.1.39" - slate-schema-violations "^0.1.10" + slate-schema-violations "^0.1.13" type-of "^2.0.1" slice-ansi@1.0.0: