@rtif-sdk/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/apply.d.ts +31 -0
- package/dist/apply.d.ts.map +1 -0
- package/dist/apply.js +604 -0
- package/dist/apply.js.map +1 -0
- package/dist/error.d.ts +19 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +21 -0
- package/dist/error.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +58 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +6 -0
- package/dist/model.js.map +1 -0
- package/dist/normalize.d.ts +40 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +74 -0
- package/dist/normalize.js.map +1 -0
- package/dist/operations.d.ts +90 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +6 -0
- package/dist/operations.js.map +1 -0
- package/dist/queries.d.ts +68 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +159 -0
- package/dist/queries.js.map +1 -0
- package/dist/resolve.d.ts +58 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +90 -0
- package/dist/resolve.js.map +1 -0
- package/dist/serialization.d.ts +89 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/serialization.js +39 -0
- package/dist/serialization.js.map +1 -0
- package/dist/validate.d.ts +38 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +93 -0
- package/dist/validate.js.map +1 -0
- package/package.json +25 -0
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offset resolution — converts absolute offsets to block-local coordinates.
|
|
3
|
+
* See SPEC.md §2.4 for specification.
|
|
4
|
+
*/
|
|
5
|
+
import { RtifError } from './error.js';
|
|
6
|
+
/**
|
|
7
|
+
* Compute the text length of a block (sum of all span text lengths).
|
|
8
|
+
*
|
|
9
|
+
* @param block - The block to measure
|
|
10
|
+
* @returns Total character count across all spans
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* blockTextLength({ id: 'b1', type: 'text', spans: [{ text: 'hello' }] });
|
|
15
|
+
* // => 5
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function blockTextLength(block) {
|
|
19
|
+
let len = 0;
|
|
20
|
+
for (const span of block.spans) {
|
|
21
|
+
len += span.text.length;
|
|
22
|
+
}
|
|
23
|
+
return len;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compute the total document length (all block text + virtual newlines between blocks).
|
|
27
|
+
*
|
|
28
|
+
* @param doc - The document to measure
|
|
29
|
+
* @returns Total character count including virtual newline separators
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* docLength({ version: 1, blocks: [
|
|
34
|
+
* { id: 'b1', type: 'text', spans: [{ text: 'hello' }] },
|
|
35
|
+
* { id: 'b2', type: 'text', spans: [{ text: 'world' }] },
|
|
36
|
+
* ] });
|
|
37
|
+
* // => 11 (5 + 1 + 5)
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function docLength(doc) {
|
|
41
|
+
let len = 0;
|
|
42
|
+
for (let i = 0; i < doc.blocks.length; i++) {
|
|
43
|
+
if (i > 0)
|
|
44
|
+
len += 1; // virtual \n separator
|
|
45
|
+
len += blockTextLength(doc.blocks[i]);
|
|
46
|
+
}
|
|
47
|
+
return len;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve an absolute document offset to a block index and local offset.
|
|
51
|
+
*
|
|
52
|
+
* Walks blocks, subtracting lengths (including the +1 separator per block
|
|
53
|
+
* boundary), until the offset lands inside a block.
|
|
54
|
+
*
|
|
55
|
+
* @param doc - The document to resolve against
|
|
56
|
+
* @param offset - Absolute character offset from document start
|
|
57
|
+
* @returns Block index and character offset within that block
|
|
58
|
+
* @throws {RtifError} OFFSET_OUT_OF_RANGE if offset is negative or exceeds document length
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const { blockIndex, localOffset } = resolve(doc, 5);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function resolve(doc, offset) {
|
|
66
|
+
if (offset < 0) {
|
|
67
|
+
throw new RtifError('OFFSET_OUT_OF_RANGE', `Offset ${offset} is negative`);
|
|
68
|
+
}
|
|
69
|
+
let remaining = offset;
|
|
70
|
+
for (let i = 0; i < doc.blocks.length; i++) {
|
|
71
|
+
const block = doc.blocks[i];
|
|
72
|
+
const len = blockTextLength(block);
|
|
73
|
+
// If remaining fits within this block (including end-of-block position)
|
|
74
|
+
if (remaining <= len) {
|
|
75
|
+
return { blockIndex: i, localOffset: remaining };
|
|
76
|
+
}
|
|
77
|
+
// Subtract block length + virtual \n separator
|
|
78
|
+
remaining -= len;
|
|
79
|
+
// Account for virtual newline between blocks (not after last block)
|
|
80
|
+
if (i < doc.blocks.length - 1) {
|
|
81
|
+
remaining -= 1; // virtual \n
|
|
82
|
+
if (remaining < 0) {
|
|
83
|
+
// Offset lands exactly on the virtual \n → resolve to end of this block
|
|
84
|
+
return { blockIndex: i, localOffset: len };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new RtifError('OFFSET_OUT_OF_RANGE', `Offset ${offset} exceeds document length ${docLength(doc)}`);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AASvC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,KAAY;IAC1C,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC/B,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAC1B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,SAAS,CAAC,GAAa;IACrC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC;YAAE,GAAG,IAAI,CAAC,CAAC,CAAC,uBAAuB;QAC5C,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,OAAO,CAAC,GAAa,EAAE,MAAc;IACnD,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACf,MAAM,IAAI,SAAS,CACjB,qBAAqB,EACrB,UAAU,MAAM,cAAc,CAC/B,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,GAAG,MAAM,CAAC;IAEvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QAEnC,wEAAwE;QACxE,IAAI,SAAS,IAAI,GAAG,EAAE,CAAC;YACrB,OAAO,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;QACnD,CAAC;QAED,+CAA+C;QAC/C,SAAS,IAAI,GAAG,CAAC;QAEjB,oEAAoE;QACpE,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,SAAS,IAAI,CAAC,CAAC,CAAC,aAAa;YAE7B,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAClB,wEAAwE;gBACxE,OAAO,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC;YAC7C,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,IAAI,SAAS,CACjB,qBAAqB,EACrB,UAAU,MAAM,4BAA4B,SAAS,CAAC,GAAG,CAAC,EAAE,CAC7D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark serialization contracts for format plugins.
|
|
3
|
+
*
|
|
4
|
+
* Provides a registry for mark-specific serializers that format plugins
|
|
5
|
+
* (plaintext, markdown, HTML) can consult when converting RTIF documents
|
|
6
|
+
* to and from external formats.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/** Supported format identifiers for mark serialization. */
|
|
11
|
+
export type SerializationFormat = 'plaintext' | 'markdown' | 'html';
|
|
12
|
+
/**
|
|
13
|
+
* A mark serializer converts a span's text and mark value to a formatted string.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const boldSerializer: MarkSerializer = {
|
|
18
|
+
* serialize: (text) => `**${text}**`,
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export interface MarkSerializer {
|
|
23
|
+
/**
|
|
24
|
+
* Serialize a marked span's text to the target format.
|
|
25
|
+
*
|
|
26
|
+
* @param text - The span's raw text content
|
|
27
|
+
* @param value - The mark value (e.g., `true` for bold, `{ id, displayName }` for mention)
|
|
28
|
+
* @returns The formatted string representation
|
|
29
|
+
*/
|
|
30
|
+
serialize(text: string, value: unknown): string;
|
|
31
|
+
/**
|
|
32
|
+
* Deserialize formatted text back to a mark value.
|
|
33
|
+
* Optional -- not all serializers support deserialization.
|
|
34
|
+
*
|
|
35
|
+
* @param formatted - The formatted string to parse
|
|
36
|
+
* @returns The mark value, or null if the input cannot be parsed
|
|
37
|
+
*/
|
|
38
|
+
deserialize?(formatted: string): unknown | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Registry of mark serializers, keyed by mark type and format.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const registry = createMarkSerializerRegistry();
|
|
46
|
+
* registry.register('mention', 'markdown', mentionMarkdownSerializer);
|
|
47
|
+
* const serializer = registry.get('mention', 'markdown');
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export interface MarkSerializerRegistry {
|
|
51
|
+
/**
|
|
52
|
+
* Register a serializer for a mark type and format.
|
|
53
|
+
*
|
|
54
|
+
* @param markType - The mark type string (e.g., "mention", "link")
|
|
55
|
+
* @param format - The target serialization format
|
|
56
|
+
* @param serializer - The serializer implementation
|
|
57
|
+
*/
|
|
58
|
+
register(markType: string, format: SerializationFormat, serializer: MarkSerializer): void;
|
|
59
|
+
/**
|
|
60
|
+
* Get the serializer for a mark type and format.
|
|
61
|
+
*
|
|
62
|
+
* @param markType - The mark type to look up
|
|
63
|
+
* @param format - The target format
|
|
64
|
+
* @returns The registered serializer, or undefined if none registered
|
|
65
|
+
*/
|
|
66
|
+
get(markType: string, format: SerializationFormat): MarkSerializer | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Check whether a serializer is registered for a mark type and format.
|
|
69
|
+
*
|
|
70
|
+
* @param markType - The mark type to check
|
|
71
|
+
* @param format - The format to check
|
|
72
|
+
* @returns `true` if a serializer is registered
|
|
73
|
+
*/
|
|
74
|
+
has(markType: string, format: SerializationFormat): boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create a new mark serializer registry.
|
|
78
|
+
*
|
|
79
|
+
* @returns An empty registry ready for serializer registration
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const registry = createMarkSerializerRegistry();
|
|
84
|
+
* registry.register('bold', 'markdown', { serialize: (text) => `**${text}**` });
|
|
85
|
+
* registry.register('bold', 'html', { serialize: (text) => `<strong>${text}</strong>` });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function createMarkSerializerRegistry(): MarkSerializerRegistry;
|
|
89
|
+
//# sourceMappingURL=serialization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization.d.ts","sourceRoot":"","sources":["../src/serialization.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;AAEpE;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IAEhD;;;;;;OAMG;IACH,WAAW,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;CACjD;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,EAAE,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAE1F;;;;;;OAMG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,cAAc,GAAG,SAAS,CAAC;IAE/E;;;;;;OAMG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC;CAC7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,IAAI,sBAAsB,CAoBrE"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark serialization contracts for format plugins.
|
|
3
|
+
*
|
|
4
|
+
* Provides a registry for mark-specific serializers that format plugins
|
|
5
|
+
* (plaintext, markdown, HTML) can consult when converting RTIF documents
|
|
6
|
+
* to and from external formats.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Create a new mark serializer registry.
|
|
12
|
+
*
|
|
13
|
+
* @returns An empty registry ready for serializer registration
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const registry = createMarkSerializerRegistry();
|
|
18
|
+
* registry.register('bold', 'markdown', { serialize: (text) => `**${text}**` });
|
|
19
|
+
* registry.register('bold', 'html', { serialize: (text) => `<strong>${text}</strong>` });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function createMarkSerializerRegistry() {
|
|
23
|
+
const serializers = new Map();
|
|
24
|
+
function makeKey(markType, format) {
|
|
25
|
+
return `${markType}:${format}`;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
register(markType, format, serializer) {
|
|
29
|
+
serializers.set(makeKey(markType, format), serializer);
|
|
30
|
+
},
|
|
31
|
+
get(markType, format) {
|
|
32
|
+
return serializers.get(makeKey(markType, format));
|
|
33
|
+
},
|
|
34
|
+
has(markType, format) {
|
|
35
|
+
return serializers.has(makeKey(markType, format));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=serialization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization.js","sourceRoot":"","sources":["../src/serialization.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0EH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,4BAA4B;IAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEtD,SAAS,OAAO,CAAC,QAAgB,EAAE,MAA2B;QAC5D,OAAO,GAAG,QAAQ,IAAI,MAAM,EAAE,CAAC;IACjC,CAAC;IAED,OAAO;QACL,QAAQ,CAAC,QAAgB,EAAE,MAA2B,EAAE,UAA0B;YAChF,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC;QACzD,CAAC;QAED,GAAG,CAAC,QAAgB,EAAE,MAA2B;YAC/C,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACpD,CAAC;QAED,GAAG,CAAC,QAAgB,EAAE,MAA2B;YAC/C,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACpD,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document validation — structural integrity checks.
|
|
3
|
+
* See docs/spec/data-model.md for invariants.
|
|
4
|
+
*/
|
|
5
|
+
import type { Document } from './model.js';
|
|
6
|
+
export interface ValidationError {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ValidationResult {
|
|
11
|
+
readonly valid: boolean;
|
|
12
|
+
readonly errors: readonly ValidationError[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate a document's structural integrity.
|
|
16
|
+
*
|
|
17
|
+
* Checks all six document invariants:
|
|
18
|
+
* 1. `version` is 1
|
|
19
|
+
* 2. At least one block (Invariant 1)
|
|
20
|
+
* 3. Every block has a non-empty `id` and `type`
|
|
21
|
+
* 4. Every block has at least one span (Invariant 2)
|
|
22
|
+
* 5. No empty spans in multi-span blocks (Invariant 3)
|
|
23
|
+
* 6. No adjacent spans with identical marks (Invariant 4)
|
|
24
|
+
* 7. Block IDs are unique (Invariant 5)
|
|
25
|
+
*
|
|
26
|
+
* @param doc - The document to validate
|
|
27
|
+
* @returns Validation result with collected errors
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const result = validate(doc);
|
|
32
|
+
* if (!result.valid) {
|
|
33
|
+
* console.error(result.errors);
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function validate(doc: Document): ValidationResult;
|
|
38
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;CAC7C;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,QAAQ,GAAG,gBAAgB,CA0ExD"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document validation — structural integrity checks.
|
|
3
|
+
* See docs/spec/data-model.md for invariants.
|
|
4
|
+
*/
|
|
5
|
+
import { marksEqual } from './normalize.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validate a document's structural integrity.
|
|
8
|
+
*
|
|
9
|
+
* Checks all six document invariants:
|
|
10
|
+
* 1. `version` is 1
|
|
11
|
+
* 2. At least one block (Invariant 1)
|
|
12
|
+
* 3. Every block has a non-empty `id` and `type`
|
|
13
|
+
* 4. Every block has at least one span (Invariant 2)
|
|
14
|
+
* 5. No empty spans in multi-span blocks (Invariant 3)
|
|
15
|
+
* 6. No adjacent spans with identical marks (Invariant 4)
|
|
16
|
+
* 7. Block IDs are unique (Invariant 5)
|
|
17
|
+
*
|
|
18
|
+
* @param doc - The document to validate
|
|
19
|
+
* @returns Validation result with collected errors
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const result = validate(doc);
|
|
24
|
+
* if (!result.valid) {
|
|
25
|
+
* console.error(result.errors);
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function validate(doc) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
// 1. version must be 1
|
|
32
|
+
if (doc.version !== 1) {
|
|
33
|
+
errors.push({
|
|
34
|
+
path: 'version',
|
|
35
|
+
message: `Expected version 1, got ${String(doc.version)}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// 2. at least one block
|
|
39
|
+
if (doc.blocks.length === 0) {
|
|
40
|
+
errors.push({
|
|
41
|
+
path: 'blocks',
|
|
42
|
+
message: 'Document must have at least one block',
|
|
43
|
+
});
|
|
44
|
+
return { valid: false, errors };
|
|
45
|
+
}
|
|
46
|
+
const seenIds = new Set();
|
|
47
|
+
for (let i = 0; i < doc.blocks.length; i++) {
|
|
48
|
+
const block = doc.blocks[i];
|
|
49
|
+
const bp = `blocks[${i}]`;
|
|
50
|
+
// 3a. non-empty id
|
|
51
|
+
if (!block.id) {
|
|
52
|
+
errors.push({ path: `${bp}.id`, message: 'Block must have a non-empty id' });
|
|
53
|
+
}
|
|
54
|
+
// 3b. non-empty type
|
|
55
|
+
if (!block.type) {
|
|
56
|
+
errors.push({ path: `${bp}.type`, message: 'Block must have a non-empty type' });
|
|
57
|
+
}
|
|
58
|
+
// 7. unique block IDs
|
|
59
|
+
if (block.id) {
|
|
60
|
+
if (seenIds.has(block.id)) {
|
|
61
|
+
errors.push({ path: `${bp}.id`, message: `Duplicate block id '${block.id}'` });
|
|
62
|
+
}
|
|
63
|
+
seenIds.add(block.id);
|
|
64
|
+
}
|
|
65
|
+
// 4. at least one span
|
|
66
|
+
if (block.spans.length === 0) {
|
|
67
|
+
errors.push({ path: `${bp}.spans`, message: 'Block must have at least one span' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// 5. no empty spans in multi-span blocks
|
|
71
|
+
if (block.spans.length > 1) {
|
|
72
|
+
for (let j = 0; j < block.spans.length; j++) {
|
|
73
|
+
if (block.spans[j].text === '') {
|
|
74
|
+
errors.push({
|
|
75
|
+
path: `${bp}.spans[${j}]`,
|
|
76
|
+
message: 'Empty span in multi-span block',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 6. no adjacent spans with identical marks
|
|
82
|
+
for (let j = 0; j < block.spans.length - 1; j++) {
|
|
83
|
+
if (marksEqual(block.spans[j].marks, block.spans[j + 1].marks)) {
|
|
84
|
+
errors.push({
|
|
85
|
+
path: `${bp}.spans[${j}]`,
|
|
86
|
+
message: 'Adjacent spans have identical marks',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { valid: errors.length === 0, errors };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rtif-sdk/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "RTIF core data model, operations, and utilities",
|
|
5
|
+
"author": "coryrobinson42@gmail.com",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"engines": { "node": ">=20.0.0" },
|
|
18
|
+
"keywords": ["rich-text", "editor", "document-model", "text-editing", "rtif", "operations"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc --build tsconfig.build.json",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|