Skip to content

Commit 79151f7

Browse files
committed
Add Schema
Schema is a class that defines the structure of the model. It is used to validate the model and to build the model from JSON.
1 parent 2981bd7 commit 79151f7

File tree

7 files changed

+134
-4
lines changed

7 files changed

+134
-4
lines changed

src/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Model } from './model';
1+
import { Model } from './model/model';
22

33
export type Command = SetValue | Edit;
44

src/editor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Module } from './utils/module';
33
import { Observable, Unsubscribe } from './utils/observable';
44

55
import { View as View } from './view/view';
6-
import { Model } from './model';
6+
import { Model } from './model/model';
77
import { Command, execute } from './commands';
88

99
/**
@@ -38,7 +38,7 @@ export class Editor extends Observable<Array<Command>> implements Module {
3838
super();
3939

4040
this.view = View.create(container);
41-
this.model = new Model(opts.initialValue || '');
41+
this.model = Model.create(opts.initialValue || '');
4242
this.history = new History<Command>((command) =>
4343
execute(this.model, command),
4444
);

src/model.ts renamed to src/model/model.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { Observable } from './utils/observable';
1+
import { Observable } from '../utils/observable';
22

3+
// TODO(hackerwins): Build a tree-based model with schema validation.
34
export class Model extends Observable<string> {
45
private value: string;
56

7+
static create(initialValue: string): Model {
8+
return new Model(initialValue);
9+
}
10+
611
constructor(value: string) {
712
super();
813
this.value = value;

src/model/node.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type Node = Element | Text;
2+
3+
export type Element = {
4+
type: string;
5+
attrs: Record<string, string>;
6+
children: Array<Node>;
7+
};
8+
9+
export type Text = {
10+
type: 'text';
11+
text: string;
12+
};

src/model/schema.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Node } from './node';
2+
3+
type SchemaSpec = Record<string, NodeSpec>;
4+
type NodeSpec = ElementSpec | TextNodeSpec;
5+
type ElementSpec = { children: string };
6+
type TextNodeSpec = {};
7+
8+
/**
9+
* `Schema` is a class that defines the structure of the model. It is used to
10+
* validate the model and to build the model from JSON.
11+
*/
12+
export class Schema {
13+
private spec: SchemaSpec;
14+
15+
constructor(spec: SchemaSpec) {
16+
this.validate(spec);
17+
this.spec = spec;
18+
}
19+
20+
validate(schemaSpec: SchemaSpec): void {
21+
if (!schemaSpec.root) {
22+
throw new Error('root node not defined');
23+
}
24+
25+
for (const [t, spec] of Object.entries(schemaSpec)) {
26+
if (t === 'text') {
27+
continue;
28+
}
29+
const elemSpec = spec as ElementSpec;
30+
const types = this.parseChildren(elemSpec.children);
31+
for (const t of types) {
32+
if (!schemaSpec[t]) {
33+
throw new Error(`invalid node type: ${t}`);
34+
}
35+
}
36+
}
37+
}
38+
39+
parseChildren(children: string): Array<string> {
40+
// TODO(hackerwins): We need to support more complex children definition.
41+
children = children.replace(/\*|\+/g, '');
42+
return [children];
43+
}
44+
45+
fromJSON(json: any): Node {
46+
if (json.type === 'text') {
47+
return {
48+
type: 'text',
49+
text: json.text,
50+
};
51+
}
52+
53+
if (!this.spec[json.type]) {
54+
throw new Error(`invalid node type: ${json.type}`);
55+
}
56+
return {
57+
type: json.type,
58+
attrs: json.attributes,
59+
children: json.children.map((child: any) => this.fromJSON(child)),
60+
};
61+
}
62+
}

src/view/view.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { toRange } from './selection';
99
*/
1010
const CommonInputEventTypes = [
1111
'insertText',
12+
'insertReplacementText',
1213
'insertCompositionText',
1314
'deleteContentBackward',
1415
'deleteContentForward',

test/model/schema.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { Schema } from '../../src/model/schema';
3+
4+
describe('Schema', () => {
5+
it('should be created with a valid spec', () => {
6+
expect(
7+
new Schema({
8+
root: { children: 'para*' },
9+
para: { children: 'text*' },
10+
text: {},
11+
}),
12+
).toBeDefined();
13+
14+
expect(() => {
15+
new Schema({ para: { children: 'text*' } });
16+
}).toThrowError('root node not defined');
17+
18+
expect(() => {
19+
new Schema({
20+
root: { children: 'paragraph*' },
21+
para: { children: 'text*' },
22+
});
23+
}).toThrowError('invalid node type: paragraph');
24+
});
25+
26+
it('should build a node from JSON', () => {
27+
const schema = new Schema({
28+
root: { children: 'para*' },
29+
para: { children: 'text*' },
30+
text: {},
31+
});
32+
33+
expect(
34+
schema.fromJSON({
35+
type: 'para',
36+
children: [{ type: 'text', text: 'Hello, world!' }],
37+
}),
38+
).toEqual({
39+
type: 'para',
40+
children: [{ type: 'text', text: 'Hello, world!' }],
41+
});
42+
43+
expect(() => {
44+
schema.fromJSON({
45+
type: 'paragraph',
46+
children: [{ type: 'invalid', text: 'Hello, world!' }],
47+
});
48+
}).toThrowError('invalid node type: paragraph');
49+
});
50+
});

0 commit comments

Comments
 (0)