-
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.
This commit enhances the editor to support schema management and improved text insertion logic. It also introduces new command structure for text operations. and it adds methods for node handling, utility functions, and a new schema for plain text. Implement comprehensive unit tests to ensure reliability.
- Loading branch information
1 parent
79151f7
commit 9f4613b
Showing
24 changed files
with
1,033 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,160 @@ | ||
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, | ||
lengthOf, | ||
pathOf, | ||
removeNode, | ||
splitText, | ||
toXML, | ||
} from './nodes'; | ||
import { firstOf, lastOf } from '../utils/array'; | ||
import { isLeftMost, toNodePos, nodesBetween } 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 { | ||
let inverse: Operation; | ||
switch (op.type) { | ||
case 'edit': | ||
inverse = this.edit(op.range, op.value); | ||
this.notify([op]); | ||
return inverse; | ||
case 'move': | ||
inverse = this.move(/*op.source, op.target*/); | ||
this.notify([op]); | ||
return inverse; | ||
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); | ||
} | ||
|
||
const hasValues = values.length > 0; | ||
if (hasValues) { | ||
if (isLeftMost(start)) { | ||
insertBefore(start.node, ...values); | ||
} else { | ||
insertAfter(start.node, ...values); | ||
} | ||
} | ||
|
||
return { | ||
type: 'edit', | ||
range: hasValues ? this.rangeOf(values) : { s: range.s, e: range.s }, | ||
value: nodesToRemove, | ||
}; | ||
} | ||
|
||
/** | ||
* `rangeOf` returns the range of the given nodes in the model. | ||
*/ | ||
rangeOf(values: Array<Node>): Range { | ||
// TODO(hackerwins): This is a naive implementation that assumes the first | ||
// and last nodes are always text nodes. We need to handle the case where | ||
// the first and last nodes are elements. | ||
const start = pathOf(firstOf(values)!, this.root); | ||
start.push(0); | ||
|
||
const end = pathOf(lastOf(values)!, this.root); | ||
end.push(lengthOf(lastOf(values)!)); | ||
|
||
return { s: start, e: end }; | ||
} | ||
|
||
/** | ||
* `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.
Oops, something went wrong.