mirror of
https://github.com/cudr/slate-collaborative.git
synced 2026-03-02 03:40:18 +00:00
initial commit
This commit is contained in:
17
packages/bridge/src/apply/annotation.ts
Normal file
17
packages/bridge/src/apply/annotation.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const addAnnotation = (doc: any, op: any) => {
|
||||
return doc
|
||||
}
|
||||
|
||||
export const removeAnnotation = (doc: any, op: any) => {
|
||||
return doc
|
||||
}
|
||||
|
||||
export const setAnnotation = (doc: any, op: any) => {
|
||||
return doc
|
||||
}
|
||||
|
||||
export default {
|
||||
add_annotation: addAnnotation,
|
||||
remove_annotation: removeAnnotation,
|
||||
set_annotation: setAnnotation
|
||||
}
|
||||
36
packages/bridge/src/apply/index.ts
Normal file
36
packages/bridge/src/apply/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Operation, Operations, SyncDoc } from '../model'
|
||||
|
||||
import node from './node'
|
||||
import mark from './mark'
|
||||
import text from './text'
|
||||
import annotation from './annotation'
|
||||
|
||||
const setSelection = doc => doc
|
||||
const setValue = (doc, op) => doc
|
||||
|
||||
const opType: any = {
|
||||
...text,
|
||||
...annotation,
|
||||
...node,
|
||||
...mark,
|
||||
|
||||
set_selection: setSelection,
|
||||
set_value: setValue
|
||||
}
|
||||
|
||||
export const applyOperation = (doc: SyncDoc, op: Operation): SyncDoc => {
|
||||
try {
|
||||
const applyOp = opType[op.type]
|
||||
|
||||
if (!applyOp) throw new TypeError('Invalid operation type!')
|
||||
|
||||
return applyOp(doc, op)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
return doc
|
||||
}
|
||||
}
|
||||
|
||||
export const applySlateOps = (doc: SyncDoc, operations: Operations) =>
|
||||
operations.reduce(applyOperation, doc)
|
||||
72
packages/bridge/src/apply/mark.ts
Normal file
72
packages/bridge/src/apply/mark.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { getTarget } from '../path'
|
||||
import { toSync } from '../utils'
|
||||
import { SyncDoc } from '../model'
|
||||
|
||||
import { AddMarkOperation, RemoveMarkOperation, SetMarkOperation } from 'slate'
|
||||
|
||||
const findIndex = (node, mark) =>
|
||||
node.marks.findIndex(m => m.type === mark.type)
|
||||
|
||||
export const addMark = (doc: SyncDoc, op: AddMarkOperation) => {
|
||||
const node = getTarget(doc, op.path)
|
||||
|
||||
if (node.object !== 'text') {
|
||||
throw new TypeError('cannot set marks on non-text node')
|
||||
}
|
||||
|
||||
if (findIndex(node, op.mark) < 0) node.marks.push(toSync(op.mark.toJS()))
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const removeMark = (doc: SyncDoc, op: RemoveMarkOperation) => {
|
||||
const node = getTarget(doc, op.path)
|
||||
|
||||
if (node.object !== 'text') {
|
||||
throw new TypeError('cannot set marks on non-text node')
|
||||
}
|
||||
|
||||
const index = findIndex(node, op.mark)
|
||||
|
||||
if (index >= 0) node.marks.splice(index, 1)
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const setMark = (doc: SyncDoc, op: SetMarkOperation) => {
|
||||
const node = getTarget(doc, op.path)
|
||||
|
||||
if (node.object !== 'text') {
|
||||
throw new TypeError('cannot set marks on non-text node')
|
||||
}
|
||||
|
||||
const index = findIndex(node, op.properties)
|
||||
|
||||
if (index === -1) {
|
||||
console.warn('did not find old mark with properties', op.properties)
|
||||
|
||||
if (!op.newProperties.type) {
|
||||
throw new TypeError('no old mark, and new mark missing type')
|
||||
}
|
||||
|
||||
node.marks.push({
|
||||
object: 'mark',
|
||||
type: op.newProperties.type,
|
||||
...op.newProperties
|
||||
})
|
||||
} else {
|
||||
node.marks[index] = {
|
||||
object: 'mark',
|
||||
...node.marks[index],
|
||||
...op.newProperties
|
||||
}
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export default {
|
||||
add_mark: addMark,
|
||||
remove_mark: removeMark,
|
||||
set_mark: setMark
|
||||
}
|
||||
107
packages/bridge/src/apply/node.ts
Normal file
107
packages/bridge/src/apply/node.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { SyncDoc } from '../model'
|
||||
|
||||
import {
|
||||
SplitNodeOperation,
|
||||
InsertNodeOperation,
|
||||
MoveNodeOperation,
|
||||
RemoveNodeOperation,
|
||||
MergeNodeOperation,
|
||||
SetNodeOperation
|
||||
} from 'slate'
|
||||
|
||||
import { getTarget, getParent } from '../path'
|
||||
import { toJS, cloneNode, toSync } from '../utils'
|
||||
|
||||
export const insertNode = (doc: SyncDoc, op: InsertNodeOperation): SyncDoc => {
|
||||
const [parent, index] = getParent(doc, op.path)
|
||||
|
||||
if (parent.object === 'text') {
|
||||
throw new TypeError('cannot insert node into text node')
|
||||
}
|
||||
|
||||
parent.nodes.splice(index, 0, toSync(op.node.toJS()))
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const moveNode = (doc: SyncDoc, op: MoveNodeOperation): SyncDoc => {
|
||||
const [from, fromIndex] = getParent(doc, op.path)
|
||||
const [to, toIndex] = getParent(doc, op.newPath)
|
||||
|
||||
if (from.object === 'text' || to.object === 'text') {
|
||||
throw new TypeError('cannot move node as child of a text node')
|
||||
}
|
||||
|
||||
to.nodes.splice(toIndex, 0, ...from.nodes.splice(fromIndex, 1))
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const removeNode = (doc: SyncDoc, op: RemoveNodeOperation): SyncDoc => {
|
||||
const [parent, index] = getParent(doc, op.path)
|
||||
|
||||
if (parent.object === 'text') {
|
||||
throw new TypeError('cannot remove node from text node')
|
||||
}
|
||||
|
||||
parent.nodes.splice(index, 1)
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const splitNode = (doc: SyncDoc, op: SplitNodeOperation): SyncDoc => {
|
||||
const [parent, index]: [any, number] = getParent(doc, op.path)
|
||||
|
||||
const target = parent.nodes[index]
|
||||
const inject = cloneNode(target)
|
||||
|
||||
if (target.object === 'text') {
|
||||
target.text.length > op.position &&
|
||||
target.text.deleteAt(op.position, target.text.length - op.position)
|
||||
op.position && inject.text.deleteAt(0, op.position)
|
||||
} else {
|
||||
target.nodes.splice(op.position, target.nodes.length - op.position)
|
||||
op.position && inject.nodes.splice(0, op.position)
|
||||
}
|
||||
|
||||
parent.nodes.insertAt(index + 1, inject)
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const mergeNode = (doc: SyncDoc, op: MergeNodeOperation) => {
|
||||
const [parent, index]: [any, number] = getParent(doc, op.path)
|
||||
|
||||
const prev = parent.nodes[index - 1]
|
||||
const next = parent.nodes[index]
|
||||
|
||||
if (prev.object === 'text') {
|
||||
prev.text.insertAt(prev.text.length, ...toJS(next.text).split(''))
|
||||
} else {
|
||||
next.nodes.forEach(n => prev.nodes.push(cloneNode(n)))
|
||||
}
|
||||
|
||||
parent.nodes.deleteAt(index, 1)
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const setNode = (doc: SyncDoc, op: SetNodeOperation) => {
|
||||
const node = getTarget(doc, op.path)
|
||||
|
||||
const { type, data }: any = op.newProperties
|
||||
|
||||
if (type) node.type = type
|
||||
if (node.object !== 'text' && data) node.data = data.toJSON()
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export default {
|
||||
insert_node: insertNode,
|
||||
move_node: moveNode,
|
||||
remove_node: removeNode,
|
||||
split_node: splitNode,
|
||||
merge_node: mergeNode,
|
||||
set_node: setNode
|
||||
}
|
||||
24
packages/bridge/src/apply/text.ts
Normal file
24
packages/bridge/src/apply/text.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { SyncDoc } from '../model'
|
||||
import { InsertTextOperation, RemoveTextOperation } from 'slate'
|
||||
import { getTarget } from '../path'
|
||||
|
||||
export const insertText = (doc: SyncDoc, op: InsertTextOperation): SyncDoc => {
|
||||
const node = getTarget(doc, op.path)
|
||||
|
||||
node.text.insertAt(op.offset, op.text)
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export const removeText = (doc: SyncDoc, op: RemoveTextOperation): SyncDoc => {
|
||||
const node = getTarget(doc, op.path)
|
||||
|
||||
node.text.deleteAt(op.offset, op.text.length)
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
export default {
|
||||
insert_text: insertText,
|
||||
remove_text: removeText
|
||||
}
|
||||
9
packages/bridge/src/convert/create.ts
Normal file
9
packages/bridge/src/convert/create.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const createByType = type => (type === 'map' ? {} : type === 'list' ? [] : '')
|
||||
|
||||
const opCreate = ({ obj, type }, [map, ops]) => {
|
||||
map[obj] = createByType(type)
|
||||
|
||||
return [map, ops]
|
||||
}
|
||||
|
||||
export default opCreate
|
||||
34
packages/bridge/src/convert/index.ts
Normal file
34
packages/bridge/src/convert/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import opInsert from './insert'
|
||||
import opRemove from './remove'
|
||||
import opSet from './set'
|
||||
import opCreate from './create'
|
||||
|
||||
const byAction = {
|
||||
create: opCreate,
|
||||
remove: opRemove,
|
||||
set: opSet,
|
||||
insert: opInsert
|
||||
}
|
||||
|
||||
const rootKey = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
const toSlateOp = ops => {
|
||||
const iterate = (acc, op) => {
|
||||
const action = byAction[op.action]
|
||||
|
||||
const result = action ? action(op, acc) : acc
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const [tree, defer] = ops.reduce(iterate, [
|
||||
{
|
||||
[rootKey]: {}
|
||||
},
|
||||
[]
|
||||
])
|
||||
|
||||
return defer.map(op => op(tree))
|
||||
}
|
||||
|
||||
export { toSlateOp }
|
||||
51
packages/bridge/src/convert/insert.ts
Normal file
51
packages/bridge/src/convert/insert.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { toSlatePath, toJS } from '../utils/index'
|
||||
|
||||
const insertTextOp = ({ index, path, value }) => () => ({
|
||||
type: 'insert_text',
|
||||
path: toSlatePath(path),
|
||||
offset: index,
|
||||
text: value,
|
||||
marks: []
|
||||
})
|
||||
|
||||
const insertNodeOp = ({ value, index, path }) => map => ({
|
||||
type: 'insert_node',
|
||||
path: [...toSlatePath(path), index],
|
||||
node: map[value]
|
||||
})
|
||||
|
||||
const insertByType = {
|
||||
text: insertTextOp,
|
||||
list: insertNodeOp
|
||||
}
|
||||
|
||||
const opInsert = (op, [map, ops]) => {
|
||||
try {
|
||||
const { link, obj, path, index, type, value } = op
|
||||
|
||||
if (link && map[obj]) {
|
||||
map[obj].splice(index, 0, map[value] || value)
|
||||
} else if (type === 'text' && !path) {
|
||||
map[obj] = map[obj]
|
||||
? map[obj]
|
||||
.slice(0, index)
|
||||
.concat(value)
|
||||
.concat(map[obj].slice(index))
|
||||
: value
|
||||
} else {
|
||||
const insert = insertByType[type]
|
||||
|
||||
const operation = insert && insert(op, map)
|
||||
|
||||
ops.push(operation)
|
||||
}
|
||||
|
||||
return [map, ops]
|
||||
} catch (e) {
|
||||
console.error(e, op, toJS(map))
|
||||
|
||||
return [map, ops]
|
||||
}
|
||||
}
|
||||
|
||||
export default opInsert
|
||||
49
packages/bridge/src/convert/remove.ts
Normal file
49
packages/bridge/src/convert/remove.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { toSlatePath, toJS } from '../utils/index'
|
||||
|
||||
const removeTextOp = ({ index, path }) => () => ({
|
||||
type: 'remove_text',
|
||||
path: toSlatePath(path).slice(0, path.length),
|
||||
offset: index,
|
||||
text: '*',
|
||||
marks: []
|
||||
})
|
||||
|
||||
const removeNodesOp = ({ index, path }) => () => {
|
||||
const nPath = toSlatePath(path)
|
||||
return {
|
||||
type: 'remove_node',
|
||||
path: nPath.length ? nPath.concat(index) : [index],
|
||||
node: {
|
||||
object: 'text'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeByType = {
|
||||
text: removeTextOp,
|
||||
nodes: removeNodesOp
|
||||
}
|
||||
|
||||
const opRemove = (op, [map, ops]) => {
|
||||
try {
|
||||
const { index, path, obj } = op
|
||||
|
||||
if (map.hasOwnProperty(obj) && op.type !== 'text') {
|
||||
map[obj].splice(index, 1)
|
||||
|
||||
return [map, ops]
|
||||
}
|
||||
|
||||
if (!path) return [map, ops]
|
||||
|
||||
const fn = removeByType[path[path.length - 1]]
|
||||
|
||||
return [map, [...ops, fn(op)]]
|
||||
} catch (e) {
|
||||
console.error(e, op, toJS(map))
|
||||
|
||||
return [map, ops]
|
||||
}
|
||||
}
|
||||
|
||||
export default opRemove
|
||||
16
packages/bridge/src/convert/set.ts
Normal file
16
packages/bridge/src/convert/set.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { toJS } from '../utils/index'
|
||||
|
||||
const opSet = (op, [map, ops]) => {
|
||||
const { link, value, obj, key } = op
|
||||
try {
|
||||
map[obj][key] = link ? map[value] : value
|
||||
|
||||
return [map, ops]
|
||||
} catch (e) {
|
||||
console.error(e, op, toJS(map))
|
||||
|
||||
return [map, ops]
|
||||
}
|
||||
}
|
||||
|
||||
export default opSet
|
||||
3
packages/bridge/src/index.ts
Normal file
3
packages/bridge/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './apply'
|
||||
export * from './convert'
|
||||
export * from './utils'
|
||||
3
packages/bridge/src/model/automerge.ts
Normal file
3
packages/bridge/src/model/automerge.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Doc } from 'automerge'
|
||||
|
||||
export type SyncDoc = Doc<any>
|
||||
2
packages/bridge/src/model/index.ts
Normal file
2
packages/bridge/src/model/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './automerge'
|
||||
export * from './slate'
|
||||
8
packages/bridge/src/model/slate.ts
Normal file
8
packages/bridge/src/model/slate.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Operation, NodeJSON } from 'slate'
|
||||
import { List } from 'immutable'
|
||||
|
||||
export type Operations = List<Operation>
|
||||
export type SyncNode = NodeJSON
|
||||
export type Path = List<number>
|
||||
|
||||
export { Operation }
|
||||
38
packages/bridge/src/path/index.ts
Normal file
38
packages/bridge/src/path/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { SyncDoc, Path } from '../model'
|
||||
import { NodeJSON } from 'slate'
|
||||
|
||||
export const isTree = (node: NodeJSON): any => node.object !== 'text'
|
||||
|
||||
export const getTarget = (doc: SyncDoc, path: Path) => {
|
||||
const iterate = (current: any, idx: number) => {
|
||||
if (!isTree(current) || !current.nodes) {
|
||||
throw new TypeError(
|
||||
`path ${path.toString()} does not match tree ${JSON.stringify(current)}`
|
||||
)
|
||||
}
|
||||
|
||||
return current.nodes[idx]
|
||||
}
|
||||
|
||||
return path.reduce(iterate, doc.document)
|
||||
}
|
||||
|
||||
export const getParentPath = (
|
||||
path: Path,
|
||||
level: number = 1
|
||||
): [number, Path] => {
|
||||
if (level > path.size) {
|
||||
throw new TypeError('requested ancestor is higher than root')
|
||||
}
|
||||
|
||||
return [path.get(path.size - level), path.slice(0, path.size - level) as Path]
|
||||
}
|
||||
|
||||
export const getParent = (
|
||||
doc: SyncDoc,
|
||||
path: Path,
|
||||
level = 1
|
||||
): [NodeJSON, number] => {
|
||||
const [idx, parentPath] = getParentPath(path, level)
|
||||
return [getTarget(doc, parentPath), idx]
|
||||
}
|
||||
9
packages/bridge/src/utils/index.ts
Normal file
9
packages/bridge/src/utils/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import toSync from './toSync'
|
||||
|
||||
export const toJS = node => JSON.parse(JSON.stringify(node))
|
||||
|
||||
export const cloneNode = node => toSync(toJS(node))
|
||||
|
||||
const toSlatePath = path => (path ? path.filter(d => Number.isInteger(d)) : [])
|
||||
|
||||
export { toSync, toSlatePath }
|
||||
33
packages/bridge/src/utils/toSync.ts
Normal file
33
packages/bridge/src/utils/toSync.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as Automerge from 'automerge'
|
||||
|
||||
const toSync = (node: any): any => {
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.hasOwnProperty('text')) {
|
||||
return {
|
||||
...node,
|
||||
text: new Automerge.Text(node.text)
|
||||
}
|
||||
} else if (node.nodes) {
|
||||
return {
|
||||
...node,
|
||||
nodes: node.nodes.map(toSync)
|
||||
}
|
||||
} else if (node.leaves) {
|
||||
return {
|
||||
...node,
|
||||
leaves: node.leaves.map(toSync)
|
||||
}
|
||||
} else if (node.document) {
|
||||
return {
|
||||
...node,
|
||||
document: toSync(node.document)
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
export default toSync
|
||||
Reference in New Issue
Block a user