Skip to content

Commit

Permalink
Add next and previous properties to Scripture nodes
Browse files Browse the repository at this point in the history
- add ScriptureDocumentMixin to support creating Scripture documents
  • Loading branch information
ddaspit committed Jan 20, 2025
1 parent 37bdbd6 commit de52bad
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 192 deletions.
6 changes: 6 additions & 0 deletions .changeset/weak-items-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sillsdev/lynx-delta': patch
'@sillsdev/lynx': patch
---

Add next and previous properties to Scripture nodes
5 changes: 5 additions & 0 deletions packages/core/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ export default [
},
},
...library,
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
];
2 changes: 1 addition & 1 deletion packages/core/src/document/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export { ScriptureChapter } from './scripture-chapter';
export { ScriptureCharacterStyle } from './scripture-character-style';
export { ScriptureContainer } from './scripture-container';
export type { ScriptureDocument, ScriptureNode } from './scripture-document';
export { findScriptureNodes, ScriptureNodeType } from './scripture-document';
export { ScriptureChildren, ScriptureDocumentMixin, ScriptureNodeType } from './scripture-document';
export type { ScriptureEditFactory } from './scripture-edit-factory';
export { ScriptureLeaf } from './scripture-leaf';
export { ScriptureMilestone } from './scripture-milestone';
Expand Down
53 changes: 15 additions & 38 deletions packages/core/src/document/scripture-container.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Position } from '../common/position';
import { Range } from '../common/range';
import { ScriptureDocument } from './scripture-document';
import { findScriptureNodes, ScriptureNode, ScriptureNodeType } from './scripture-document';
import { ScriptureChildren, ScriptureDocument, ScriptureNode, ScriptureNodeType } from './scripture-document';

export abstract class ScriptureContainer implements ScriptureNode {
private _parent?: ScriptureNode;
private readonly _children: ScriptureNode[] = [];
parent?: ScriptureNode;
next?: ScriptureNode;
previous?: ScriptureNode;
private readonly _children = new ScriptureChildren(this);
readonly isLeaf = false;

constructor(
Expand All @@ -22,26 +23,18 @@ export abstract class ScriptureContainer implements ScriptureNode {
abstract readonly type: ScriptureNodeType;

get document(): ScriptureDocument | undefined {
return this._parent?.document;
}

get parent(): ScriptureNode | undefined {
return this._parent;
return this.parent?.document;
}

get children(): readonly ScriptureNode[] {
return this._children;
}

updateParent(parent: ScriptureNode | undefined): void {
this._parent = parent;
return this._children.nodes;
}

remove(): void {
if (this._parent == null) {
if (this.parent == null) {
throw new Error('The node does not have a parent.');
}
this._parent.removeChild(this);
this.parent.removeChild(this);
}

getText(): string {
Expand All @@ -54,7 +47,7 @@ export abstract class ScriptureContainer implements ScriptureNode {
findNodes(
filter?: ScriptureNodeType | ((node: ScriptureNode) => boolean) | ScriptureNodeType[],
): IterableIterator<ScriptureNode> {
return findScriptureNodes(this, filter);
return this._children.find(filter);
}

positionAt(offset: number): Position {
Expand All @@ -65,38 +58,22 @@ export abstract class ScriptureContainer implements ScriptureNode {
}

appendChild(child: ScriptureNode): void {
this._children.push(child);
child.updateParent(this);
this._children.append(child);
}

insertChild(index: number, child: ScriptureNode): void {
this._children.splice(index, 0, child);
child.updateParent(this);
this._children.insert(index, child);
}

removeChild(child: ScriptureNode): void {
if (child.parent !== this) {
throw new Error('This node does not contain the specified child.');
}
const index = this._children.indexOf(child);
if (index === -1) {
throw new Error('This node does not contain the specified child.');
}
this._children.splice(index, 1);
child.updateParent(undefined);
this._children.remove(child);
}

spliceChildren(start: number, deleteCount: number, ...items: ScriptureNode[]): void {
const removed = this._children.splice(start, deleteCount, ...items);
for (const child of removed) {
child.updateParent(undefined);
}
for (const child of items) {
child.updateParent(this);
}
this._children.splice(start, deleteCount, ...items);
}

clearChildren(): void {
this._children.length = 0;
this._children.clear();
}
}
157 changes: 157 additions & 0 deletions packages/core/src/document/scripture-document.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { describe, expect, it } from 'vitest';

import { ScriptureChapter } from './scripture-chapter';
import { ScriptureContainer } from './scripture-container';
import { ScriptureDocument, ScriptureNode, ScriptureNodeType } from './scripture-document';

describe('ScriptureDocument', () => {
it('appendChild', () => {
const doc = new TestScriptureDocument('uri', 'format', 1);

doc.appendChild(new ScriptureChapter('1'));

expect(doc.children).toHaveLength(1);
let chapter1 = doc.children[0] as ScriptureChapter;
expect(chapter1.number).toBe('1');
expect(chapter1.next).toBeUndefined();
expect(chapter1.previous).toBeUndefined();

doc.appendChild(new ScriptureChapter('2'));

expect(doc.children).toHaveLength(2);
chapter1 = doc.children[0] as ScriptureChapter;
const chapter2 = doc.children[1] as ScriptureChapter;
expect(chapter1.number).toBe('1');
expect(chapter1.next).toBe(chapter2);
expect(chapter1.previous).toBeUndefined();
expect(chapter2.number).toBe('2');
expect(chapter2.next).toBeUndefined();
expect(chapter2.previous).toBe(chapter1);
});

it('insertChild', () => {
const doc = new TestScriptureDocument('uri', 'format', 1);

doc.insertChild(0, new ScriptureChapter('2'));

expect(doc.children).toHaveLength(1);
let chapter2 = doc.children[0] as ScriptureChapter;
expect(chapter2.number).toBe('2');
expect(chapter2.next).toBeUndefined();
expect(chapter2.previous).toBeUndefined();

doc.insertChild(0, new ScriptureChapter('1'));

expect(doc.children).toHaveLength(2);
let chapter1 = doc.children[0] as ScriptureChapter;
chapter2 = doc.children[1] as ScriptureChapter;
expect(chapter1.number).toBe('1');
expect(chapter1.next).toBe(chapter2);
expect(chapter1.previous).toBeUndefined();
expect(chapter2.number).toBe('2');
expect(chapter2.next).toBeUndefined();
expect(chapter2.previous).toBe(chapter1);

doc.insertChild(2, new ScriptureChapter('3'));

expect(doc.children).toHaveLength(3);
chapter1 = doc.children[0] as ScriptureChapter;
chapter2 = doc.children[1] as ScriptureChapter;
const chapter3 = doc.children[2] as ScriptureChapter;
expect(chapter2.number).toBe('2');
expect(chapter2.next).toBe(chapter3);
expect(chapter2.previous).toBe(chapter1);
expect(chapter3.number).toBe('3');
expect(chapter3.next).toBeUndefined();
expect(chapter3.previous).toBe(chapter2);
});

it('spliceChildren', () => {
const doc = new TestScriptureDocument('uri', 'format', 1);

doc.spliceChildren(0, 0, new ScriptureChapter('1'), new ScriptureChapter('4'), new ScriptureChapter('5'));

expect(doc.children).toHaveLength(3);
let chapter1 = doc.children[0] as ScriptureChapter;
const chapter4 = doc.children[1] as ScriptureChapter;
const chapter5 = doc.children[2] as ScriptureChapter;
expect(chapter1.number).toBe('1');
expect(chapter1.next).toBe(chapter4);
expect(chapter1.previous).toBeUndefined();
expect(chapter4.number).toBe('4');
expect(chapter4.next).toBe(chapter5);
expect(chapter4.previous).toBe(chapter1);
expect(chapter5.number).toBe('5');
expect(chapter5.next).toBeUndefined();
expect(chapter5.previous).toBe(chapter4);

doc.spliceChildren(1, 2, new ScriptureChapter('2'), new ScriptureChapter('3'));
expect(doc.children).toHaveLength(3);
chapter1 = doc.children[0] as ScriptureChapter;
const chapter2 = doc.children[1] as ScriptureChapter;
const chapter3 = doc.children[2] as ScriptureChapter;
expect(chapter1.number).toBe('1');
expect(chapter1.next).toBe(chapter2);
expect(chapter1.previous).toBeUndefined();
expect(chapter2.number).toBe('2');
expect(chapter2.next).toBe(chapter3);
expect(chapter2.previous).toBe(chapter1);
expect(chapter3.number).toBe('3');
expect(chapter3.next).toBeUndefined();
expect(chapter3.previous).toBe(chapter2);
});

it('removeChild', () => {
const doc = new TestScriptureDocument('uri', 'format', 1, [
new ScriptureChapter('1'),
new ScriptureChapter('3'),
new ScriptureChapter('2'),
]);

doc.removeChild(doc.children[1]);

expect(doc.children).toHaveLength(2);
let chapter1 = doc.children[0] as ScriptureChapter;
const chapter2 = doc.children[1] as ScriptureChapter;
expect(chapter1.number).toBe('1');
expect(chapter1.next).toBe(chapter2);
expect(chapter1.previous).toBeUndefined();
expect(chapter2.number).toBe('2');
expect(chapter2.next).toBeUndefined();
expect(chapter2.previous).toBe(chapter1);

doc.removeChild(doc.children[1]);

expect(doc.children).toHaveLength(1);
chapter1 = doc.children[0] as ScriptureChapter;
expect(chapter1.number).toBe('1');
expect(chapter1.next).toBeUndefined();
expect(chapter1.previous).toBeUndefined();
});

it('clearChildren', () => {
const doc = new TestScriptureDocument('uri', 'format', 1, [
new ScriptureChapter('1'),
new ScriptureChapter('2'),
new ScriptureChapter('3'),
]);

doc.clearChildren();

expect(doc.children).toHaveLength(0);
});
});

class TestScriptureDocument extends ScriptureContainer implements ScriptureDocument {
readonly uri: string;
readonly version: number;
readonly format: string;
readonly type = ScriptureNodeType.Document;

constructor(uri: string, format: string, version: number, children?: ScriptureNode[]) {
super(children);
this.uri = uri;
this.format = format;
this.version = version;
}
}
Loading

0 comments on commit de52bad

Please sign in to comment.