-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
79151f7
commit 4b000cf
Showing
23 changed files
with
999 additions
and
129 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Model } from '../model/model'; | ||
import { Range } from '../model/types'; | ||
import { Operation } from '../model/operations'; | ||
|
||
export type Command = { | ||
ops: Array<Operation>; | ||
}; | ||
|
||
export function insertText(range: Range, text: string): Command { | ||
return { | ||
ops: [ | ||
{ | ||
type: 'edit', | ||
range, | ||
value: [{ type: 'text', text }], | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
export function splitBlock(range: Range): Command { | ||
// TODO(hackerwins): Implement this according to the schema. | ||
return { | ||
ops: [ | ||
{ | ||
type: 'edit', | ||
range, | ||
value: [{ type: 'p' }], | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
/** | ||
* `execute` executes the command on the model and returns inverse command. | ||
*/ | ||
export function execute(model: Model, command: Command): Command { | ||
const ops = []; | ||
|
||
for (const op of command.ops) { | ||
const inverse = model.apply(op); | ||
ops.push(inverse); | ||
} | ||
|
||
return { | ||
ops: ops, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,143 @@ | ||
import { Observable } from '../utils/observable'; | ||
import { Node, Text, Element, Range, NodePos } from './types'; | ||
import { Schema, SchemaSpec } from './schema'; | ||
import { Operation } from './operations'; | ||
import { | ||
insertAfter, | ||
insertBefore, | ||
nodesBetween, | ||
pathOf, | ||
removeNode, | ||
splitText, | ||
toNodePos, | ||
toXML, | ||
} from './nodes'; | ||
import { firstOf, lastOf } from '../utils/array'; | ||
import { isLeftMost } from './nodepos'; | ||
import { isCollapsed } from './range'; | ||
|
||
// TODO(hackerwins): Build a tree-based model with schema validation. | ||
export class Model extends Observable<string> { | ||
private value: string; | ||
export class Model extends Observable<Array<Operation>> { | ||
private schema: Schema; | ||
private root: Element; | ||
|
||
static create(initialValue: string): Model { | ||
return new Model(initialValue); | ||
static create(spec: SchemaSpec, initialValue: string): Model { | ||
const schema = new Schema(spec); | ||
const value = schema.fromXML(initialValue); | ||
return new Model(schema, value); | ||
} | ||
|
||
constructor(value: string) { | ||
constructor(schema: Schema, value: Node) { | ||
super(); | ||
this.value = value; | ||
this.schema = schema; | ||
this.root = value; | ||
} | ||
|
||
setValue(value: string): void { | ||
this.value = value; | ||
this.notify(this.value); | ||
createText(value: string): Text { | ||
return this.schema.create('text', value) as Text; | ||
} | ||
|
||
getValue(from?: number, to?: number): string { | ||
if (from !== undefined && to !== undefined) { | ||
return this.value.slice(from, to); | ||
toXML(): string { | ||
return toXML(this.root); | ||
} | ||
|
||
apply(op: Operation): Operation { | ||
switch (op.type) { | ||
case 'edit': | ||
return this.edit(op.range, op.value); | ||
case 'move': | ||
return this.move(/*op.source, op.target*/); | ||
default: | ||
throw new Error(`invalid operation type: ${op}`); | ||
} | ||
} | ||
|
||
edit(range: Range, values: Array<Node>): Operation { | ||
// NOTE(hackerwins): To keep the node of the position, we need to split the | ||
// text node at the end of the range. | ||
const end = this.nodePosOf(range.e); | ||
const start = isCollapsed(range) ? end : this.nodePosOf(range.s); | ||
|
||
const nodesToRemove: Array<Node> = []; | ||
for (const [node, type] of nodesBetween(this.root, start, end)) { | ||
if (type === 'open') { | ||
continue; | ||
} | ||
|
||
nodesToRemove.push(node); | ||
}; | ||
|
||
for (const node of nodesToRemove) { | ||
removeNode(node); | ||
} | ||
|
||
if (isLeftMost(start)) { | ||
insertBefore(start.node, ...values); | ||
} else { | ||
insertAfter(start.node, ...values); | ||
} | ||
|
||
|
||
const firstNode = firstOf(values)!; | ||
const lastNode = lastOf(values)!; | ||
const startPath = pathOf(firstNode, this.root); | ||
const endPath = pathOf(lastNode, this.root); | ||
|
||
return { | ||
type: 'edit', | ||
range: { s: startPath, e: endPath }, | ||
value: nodesToRemove | ||
}; | ||
} | ||
|
||
/** | ||
* `nodePosOf` returns the node position of the given path. | ||
*/ | ||
nodePosOf(path: Array<number>, withSplitText: boolean = true): NodePos { | ||
const pos = toNodePos(path, this.root); | ||
if (!withSplitText) { | ||
return pos; | ||
} | ||
|
||
const newNode = splitText(pos); | ||
if (!newNode) { | ||
return pos; | ||
} | ||
|
||
return { | ||
node: newNode, | ||
offset: 0, | ||
}; | ||
} | ||
|
||
/** | ||
* `getContentEndRange` returns the range of the end of the content in the | ||
* model. | ||
*/ | ||
getContentEndRange(): Range { | ||
const path = []; | ||
|
||
let node = this.root; | ||
while (node) { | ||
if (node.type === 'text') { | ||
const text = node as Text; | ||
path.push(text.text.length); | ||
break; | ||
} | ||
|
||
const elem = node as Element; | ||
const children = elem.children || []; | ||
if (children.length === 0) { | ||
break; | ||
} | ||
|
||
path.push(children.length - 1); | ||
node = lastOf(children)!; | ||
} | ||
|
||
return this.value; | ||
return { s: path, e: path }; | ||
} | ||
|
||
edit(start: number, end: number, text: string): void { | ||
this.value = this.value.slice(0, start) + text + this.value.slice(end); | ||
this.notify(this.value); | ||
move(/*source: Range, target: Range*/): Operation { | ||
throw new Error('Method not implemented.'); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { NodePos } from "./types"; | ||
import { lengthOf } from "./nodes"; | ||
|
||
/** | ||
* `isLeftMost` checks if the node position is the leftmost position of the node. | ||
*/ | ||
export function isLeftMost(pos: NodePos): boolean { | ||
return pos.offset === 0; | ||
} | ||
|
||
/** | ||
* `isRightMost` checks if the node position is the rightmost position of the node. | ||
*/ | ||
export function isRightMost(pos: NodePos): boolean { | ||
return pos.offset === lengthOf(pos.node); | ||
} | ||
|
||
/** | ||
* `equals` checks if two node positions are equal. | ||
*/ | ||
export function equals(a: NodePos, b: NodePos): boolean { | ||
return a.node === b.node && a.offset === b.offset; | ||
} |
Oops, something went wrong.