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: