@glyphjs/ir 0.1.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 +41 -0
- package/dist/index.cjs +453 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +441 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { GlyphIR, Diagnostic, GlyphPatch, IRMigration } from '@glyphjs/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a content-addressed block ID.
|
|
5
|
+
*
|
|
6
|
+
* Returns `"b-"` + first 12 hex chars of SHA-256(documentId + blockType + SHA-256(content)).
|
|
7
|
+
*/
|
|
8
|
+
declare function generateBlockId(documentId: string, blockType: string, content: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Generates a document ID from the given options.
|
|
11
|
+
*
|
|
12
|
+
* Priority:
|
|
13
|
+
* 1. If `glyphId` is present, return it directly.
|
|
14
|
+
* 2. If `filePath` is present, normalize to forward slashes and return.
|
|
15
|
+
* 3. If `content` is present, return `"doc-"` + first 16 hex chars of SHA-256(content).
|
|
16
|
+
*/
|
|
17
|
+
declare function generateDocumentId(options: {
|
|
18
|
+
glyphId?: string;
|
|
19
|
+
filePath?: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
}): string;
|
|
22
|
+
/**
|
|
23
|
+
* Resolves duplicate block IDs by appending `-1`, `-2`, etc. to collisions.
|
|
24
|
+
*
|
|
25
|
+
* The first occurrence of an ID is left unchanged; subsequent duplicates
|
|
26
|
+
* receive numeric suffixes.
|
|
27
|
+
*/
|
|
28
|
+
declare function resolveBlockIdCollisions(ids: string[]): string[];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validates a GlyphIR document for structural integrity.
|
|
32
|
+
*
|
|
33
|
+
* Checks:
|
|
34
|
+
* - `version` is a non-empty string
|
|
35
|
+
* - `id` is a non-empty string
|
|
36
|
+
* - all block IDs are unique
|
|
37
|
+
* - all reference `sourceBlockId` / `targetBlockId` exist in blocks
|
|
38
|
+
* - blocks have required fields (`id`, `type`, `data`, `position`)
|
|
39
|
+
* - layout mode is valid
|
|
40
|
+
*
|
|
41
|
+
* Returns an array of `Diagnostic` objects describing any issues found.
|
|
42
|
+
*/
|
|
43
|
+
declare function validateIR(ir: GlyphIR): Diagnostic[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes a patch that transforms `before` into `after`.
|
|
47
|
+
*
|
|
48
|
+
* Compares blocks (added, removed, updated, moved), references (added, removed),
|
|
49
|
+
* metadata changes, and layout changes.
|
|
50
|
+
*
|
|
51
|
+
* Invariant: `applyPatch(before, diffIR(before, after))` deep-equals `after`.
|
|
52
|
+
*/
|
|
53
|
+
declare function diffIR(before: GlyphIR, after: GlyphIR): GlyphPatch;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Applies a patch to an IR document, returning a new IR (immutable).
|
|
57
|
+
*
|
|
58
|
+
* Invariant: `applyPatch(ir, [])` returns `ir` unchanged (identity).
|
|
59
|
+
*/
|
|
60
|
+
declare function applyPatch(ir: GlyphIR, patch: GlyphPatch): GlyphIR;
|
|
61
|
+
/**
|
|
62
|
+
* Composes two patches into a single patch.
|
|
63
|
+
*
|
|
64
|
+
* Simplistic but correct: concatenates the operations of `a` followed by `b`.
|
|
65
|
+
*
|
|
66
|
+
* Invariant: `composePatch` is associative —
|
|
67
|
+
* `compose(compose(a, b), c)` equals `compose(a, compose(b, c))`.
|
|
68
|
+
*/
|
|
69
|
+
declare function composePatch(a: GlyphPatch, b: GlyphPatch): GlyphPatch;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Registers a migration that transforms IR from one version to another.
|
|
73
|
+
*
|
|
74
|
+
* Migrations are pure functions registered in `@glyphjs/ir`.
|
|
75
|
+
* They are applied in sequence by `migrateIR`.
|
|
76
|
+
*/
|
|
77
|
+
declare function registerMigration(migration: IRMigration): void;
|
|
78
|
+
/**
|
|
79
|
+
* Migrates an IR document from its current version to the target version
|
|
80
|
+
* by applying registered migrations in sequence.
|
|
81
|
+
*
|
|
82
|
+
* Throws an error if:
|
|
83
|
+
* - The IR version is already at or ahead of the target version
|
|
84
|
+
* and no migration path exists.
|
|
85
|
+
* - No migration is registered for a required intermediate version.
|
|
86
|
+
* - The target version is older than the current version (downgrade).
|
|
87
|
+
*/
|
|
88
|
+
declare function migrateIR(ir: GlyphIR, targetVersion: string): GlyphIR;
|
|
89
|
+
/**
|
|
90
|
+
* Clears all registered migrations. Useful for testing.
|
|
91
|
+
*/
|
|
92
|
+
declare function clearMigrations(): void;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a valid empty GlyphIR document with default values.
|
|
96
|
+
*
|
|
97
|
+
* Returns:
|
|
98
|
+
* ```json
|
|
99
|
+
* {
|
|
100
|
+
* "version": "1.0.0",
|
|
101
|
+
* "id": id,
|
|
102
|
+
* "metadata": {},
|
|
103
|
+
* "blocks": [],
|
|
104
|
+
* "references": [],
|
|
105
|
+
* "layout": { "mode": "document", "spacing": "normal" }
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
declare function createEmptyIR(id: string): GlyphIR;
|
|
110
|
+
|
|
111
|
+
export { applyPatch, clearMigrations, composePatch, createEmptyIR, diffIR, generateBlockId, generateDocumentId, migrateIR, registerMigration, resolveBlockIdCollisions, validateIR };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { GlyphIR, Diagnostic, GlyphPatch, IRMigration } from '@glyphjs/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a content-addressed block ID.
|
|
5
|
+
*
|
|
6
|
+
* Returns `"b-"` + first 12 hex chars of SHA-256(documentId + blockType + SHA-256(content)).
|
|
7
|
+
*/
|
|
8
|
+
declare function generateBlockId(documentId: string, blockType: string, content: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Generates a document ID from the given options.
|
|
11
|
+
*
|
|
12
|
+
* Priority:
|
|
13
|
+
* 1. If `glyphId` is present, return it directly.
|
|
14
|
+
* 2. If `filePath` is present, normalize to forward slashes and return.
|
|
15
|
+
* 3. If `content` is present, return `"doc-"` + first 16 hex chars of SHA-256(content).
|
|
16
|
+
*/
|
|
17
|
+
declare function generateDocumentId(options: {
|
|
18
|
+
glyphId?: string;
|
|
19
|
+
filePath?: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
}): string;
|
|
22
|
+
/**
|
|
23
|
+
* Resolves duplicate block IDs by appending `-1`, `-2`, etc. to collisions.
|
|
24
|
+
*
|
|
25
|
+
* The first occurrence of an ID is left unchanged; subsequent duplicates
|
|
26
|
+
* receive numeric suffixes.
|
|
27
|
+
*/
|
|
28
|
+
declare function resolveBlockIdCollisions(ids: string[]): string[];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validates a GlyphIR document for structural integrity.
|
|
32
|
+
*
|
|
33
|
+
* Checks:
|
|
34
|
+
* - `version` is a non-empty string
|
|
35
|
+
* - `id` is a non-empty string
|
|
36
|
+
* - all block IDs are unique
|
|
37
|
+
* - all reference `sourceBlockId` / `targetBlockId` exist in blocks
|
|
38
|
+
* - blocks have required fields (`id`, `type`, `data`, `position`)
|
|
39
|
+
* - layout mode is valid
|
|
40
|
+
*
|
|
41
|
+
* Returns an array of `Diagnostic` objects describing any issues found.
|
|
42
|
+
*/
|
|
43
|
+
declare function validateIR(ir: GlyphIR): Diagnostic[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes a patch that transforms `before` into `after`.
|
|
47
|
+
*
|
|
48
|
+
* Compares blocks (added, removed, updated, moved), references (added, removed),
|
|
49
|
+
* metadata changes, and layout changes.
|
|
50
|
+
*
|
|
51
|
+
* Invariant: `applyPatch(before, diffIR(before, after))` deep-equals `after`.
|
|
52
|
+
*/
|
|
53
|
+
declare function diffIR(before: GlyphIR, after: GlyphIR): GlyphPatch;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Applies a patch to an IR document, returning a new IR (immutable).
|
|
57
|
+
*
|
|
58
|
+
* Invariant: `applyPatch(ir, [])` returns `ir` unchanged (identity).
|
|
59
|
+
*/
|
|
60
|
+
declare function applyPatch(ir: GlyphIR, patch: GlyphPatch): GlyphIR;
|
|
61
|
+
/**
|
|
62
|
+
* Composes two patches into a single patch.
|
|
63
|
+
*
|
|
64
|
+
* Simplistic but correct: concatenates the operations of `a` followed by `b`.
|
|
65
|
+
*
|
|
66
|
+
* Invariant: `composePatch` is associative —
|
|
67
|
+
* `compose(compose(a, b), c)` equals `compose(a, compose(b, c))`.
|
|
68
|
+
*/
|
|
69
|
+
declare function composePatch(a: GlyphPatch, b: GlyphPatch): GlyphPatch;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Registers a migration that transforms IR from one version to another.
|
|
73
|
+
*
|
|
74
|
+
* Migrations are pure functions registered in `@glyphjs/ir`.
|
|
75
|
+
* They are applied in sequence by `migrateIR`.
|
|
76
|
+
*/
|
|
77
|
+
declare function registerMigration(migration: IRMigration): void;
|
|
78
|
+
/**
|
|
79
|
+
* Migrates an IR document from its current version to the target version
|
|
80
|
+
* by applying registered migrations in sequence.
|
|
81
|
+
*
|
|
82
|
+
* Throws an error if:
|
|
83
|
+
* - The IR version is already at or ahead of the target version
|
|
84
|
+
* and no migration path exists.
|
|
85
|
+
* - No migration is registered for a required intermediate version.
|
|
86
|
+
* - The target version is older than the current version (downgrade).
|
|
87
|
+
*/
|
|
88
|
+
declare function migrateIR(ir: GlyphIR, targetVersion: string): GlyphIR;
|
|
89
|
+
/**
|
|
90
|
+
* Clears all registered migrations. Useful for testing.
|
|
91
|
+
*/
|
|
92
|
+
declare function clearMigrations(): void;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a valid empty GlyphIR document with default values.
|
|
96
|
+
*
|
|
97
|
+
* Returns:
|
|
98
|
+
* ```json
|
|
99
|
+
* {
|
|
100
|
+
* "version": "1.0.0",
|
|
101
|
+
* "id": id,
|
|
102
|
+
* "metadata": {},
|
|
103
|
+
* "blocks": [],
|
|
104
|
+
* "references": [],
|
|
105
|
+
* "layout": { "mode": "document", "spacing": "normal" }
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
declare function createEmptyIR(id: string): GlyphIR;
|
|
110
|
+
|
|
111
|
+
export { applyPatch, clearMigrations, composePatch, createEmptyIR, diffIR, generateBlockId, generateDocumentId, migrateIR, registerMigration, resolveBlockIdCollisions, validateIR };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/ids.ts
|
|
4
|
+
function sha256Hex(input) {
|
|
5
|
+
return createHash("sha256").update(input).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
function generateBlockId(documentId, blockType, content) {
|
|
8
|
+
const contentFingerprint = sha256Hex(content);
|
|
9
|
+
const hash = sha256Hex(documentId + blockType + contentFingerprint);
|
|
10
|
+
return `b-${hash.slice(0, 12)}`;
|
|
11
|
+
}
|
|
12
|
+
function generateDocumentId(options) {
|
|
13
|
+
if (options.glyphId) {
|
|
14
|
+
return options.glyphId;
|
|
15
|
+
}
|
|
16
|
+
if (options.filePath) {
|
|
17
|
+
return options.filePath.replace(/\\/g, "/");
|
|
18
|
+
}
|
|
19
|
+
if (options.content) {
|
|
20
|
+
const hash = sha256Hex(options.content);
|
|
21
|
+
return `doc-${hash.slice(0, 16)}`;
|
|
22
|
+
}
|
|
23
|
+
return `doc-${sha256Hex("")}`.slice(0, 20);
|
|
24
|
+
}
|
|
25
|
+
function resolveBlockIdCollisions(ids) {
|
|
26
|
+
const seen = /* @__PURE__ */ new Map();
|
|
27
|
+
const result = [];
|
|
28
|
+
for (const id of ids) {
|
|
29
|
+
const count = seen.get(id);
|
|
30
|
+
if (count === void 0) {
|
|
31
|
+
seen.set(id, 1);
|
|
32
|
+
result.push(id);
|
|
33
|
+
} else {
|
|
34
|
+
seen.set(id, count + 1);
|
|
35
|
+
result.push(`${id}-${String(count)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/validate.ts
|
|
42
|
+
var VALID_LAYOUT_MODES = /* @__PURE__ */ new Set(["document", "dashboard", "presentation"]);
|
|
43
|
+
function validateIR(ir) {
|
|
44
|
+
const diagnostics = [];
|
|
45
|
+
if (!ir.version || typeof ir.version !== "string") {
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
severity: "error",
|
|
48
|
+
code: "IR_MISSING_VERSION",
|
|
49
|
+
message: 'IR document must have a non-empty "version" string.',
|
|
50
|
+
source: "compiler"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!ir.id || typeof ir.id !== "string") {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
severity: "error",
|
|
56
|
+
code: "IR_MISSING_ID",
|
|
57
|
+
message: 'IR document must have a non-empty "id" string.',
|
|
58
|
+
source: "compiler"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const blockIds = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const block of ir.blocks) {
|
|
63
|
+
if (!block.id || typeof block.id !== "string") {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
severity: "error",
|
|
66
|
+
code: "BLOCK_MISSING_ID",
|
|
67
|
+
message: 'Block is missing a non-empty "id" field.',
|
|
68
|
+
source: "compiler"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!block.type || typeof block.type !== "string") {
|
|
72
|
+
diagnostics.push({
|
|
73
|
+
severity: "error",
|
|
74
|
+
code: "BLOCK_MISSING_TYPE",
|
|
75
|
+
message: `Block "${String(block.id)}" is missing a non-empty "type" field.`,
|
|
76
|
+
source: "compiler"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (block.data === void 0 || block.data === null) {
|
|
80
|
+
diagnostics.push({
|
|
81
|
+
severity: "error",
|
|
82
|
+
code: "BLOCK_MISSING_DATA",
|
|
83
|
+
message: `Block "${String(block.id)}" is missing a "data" field.`,
|
|
84
|
+
source: "compiler"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!block.position) {
|
|
88
|
+
diagnostics.push({
|
|
89
|
+
severity: "error",
|
|
90
|
+
code: "BLOCK_MISSING_POSITION",
|
|
91
|
+
message: `Block "${String(block.id)}" is missing a "position" field.`,
|
|
92
|
+
source: "compiler"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (block.id) {
|
|
96
|
+
if (blockIds.has(block.id)) {
|
|
97
|
+
diagnostics.push({
|
|
98
|
+
severity: "error",
|
|
99
|
+
code: "BLOCK_DUPLICATE_ID",
|
|
100
|
+
message: `Duplicate block ID: "${block.id}".`,
|
|
101
|
+
source: "compiler"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
blockIds.add(block.id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const ref of ir.references) {
|
|
108
|
+
if (!blockIds.has(ref.sourceBlockId)) {
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
severity: "error",
|
|
111
|
+
code: "REF_MISSING_SOURCE",
|
|
112
|
+
message: `Reference "${ref.id}" has sourceBlockId "${ref.sourceBlockId}" which does not exist in blocks.`,
|
|
113
|
+
source: "compiler"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!blockIds.has(ref.targetBlockId)) {
|
|
117
|
+
diagnostics.push({
|
|
118
|
+
severity: "error",
|
|
119
|
+
code: "REF_MISSING_TARGET",
|
|
120
|
+
message: `Reference "${ref.id}" has targetBlockId "${ref.targetBlockId}" which does not exist in blocks.`,
|
|
121
|
+
source: "compiler"
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (ir.layout && !VALID_LAYOUT_MODES.has(ir.layout.mode)) {
|
|
126
|
+
diagnostics.push({
|
|
127
|
+
severity: "error",
|
|
128
|
+
code: "LAYOUT_INVALID_MODE",
|
|
129
|
+
message: `Invalid layout mode: "${ir.layout.mode}". Expected one of: ${[...VALID_LAYOUT_MODES].join(", ")}.`,
|
|
130
|
+
source: "compiler"
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return diagnostics;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/diff.ts
|
|
137
|
+
function deepEqual(a, b) {
|
|
138
|
+
if (a === b) return true;
|
|
139
|
+
if (a === null || b === null) return false;
|
|
140
|
+
if (typeof a !== typeof b) return false;
|
|
141
|
+
if (Array.isArray(a)) {
|
|
142
|
+
if (!Array.isArray(b)) return false;
|
|
143
|
+
if (a.length !== b.length) return false;
|
|
144
|
+
return a.every((val, idx) => deepEqual(val, b[idx]));
|
|
145
|
+
}
|
|
146
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
147
|
+
const objA = a;
|
|
148
|
+
const objB = b;
|
|
149
|
+
const keysA = Object.keys(objA);
|
|
150
|
+
const keysB = Object.keys(objB);
|
|
151
|
+
if (keysA.length !== keysB.length) return false;
|
|
152
|
+
return keysA.every((key) => deepEqual(objA[key], objB[key]));
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
function blockContentEqual(a, b) {
|
|
157
|
+
return a.type === b.type && deepEqual(a.data, b.data) && deepEqual(a.position, b.position) && deepEqual(a.children, b.children) && deepEqual(a.diagnostics, b.diagnostics) && deepEqual(a.metadata, b.metadata);
|
|
158
|
+
}
|
|
159
|
+
function referenceEqual(a, b) {
|
|
160
|
+
return deepEqual(a, b);
|
|
161
|
+
}
|
|
162
|
+
function getBlockAt(blocks, index) {
|
|
163
|
+
return blocks[index];
|
|
164
|
+
}
|
|
165
|
+
function diffIR(before, after) {
|
|
166
|
+
const ops = [];
|
|
167
|
+
if (!deepEqual(before.metadata, after.metadata)) {
|
|
168
|
+
ops.push({
|
|
169
|
+
op: "updateMetadata",
|
|
170
|
+
metadata: after.metadata
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (!deepEqual(before.layout, after.layout)) {
|
|
174
|
+
ops.push({
|
|
175
|
+
op: "updateLayout",
|
|
176
|
+
layout: after.layout
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const beforeBlockMap = /* @__PURE__ */ new Map();
|
|
180
|
+
for (const block of before.blocks) {
|
|
181
|
+
beforeBlockMap.set(block.id, block);
|
|
182
|
+
}
|
|
183
|
+
const afterBlockMap = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const block of after.blocks) {
|
|
185
|
+
afterBlockMap.set(block.id, block);
|
|
186
|
+
}
|
|
187
|
+
for (const block of before.blocks) {
|
|
188
|
+
if (!afterBlockMap.has(block.id)) {
|
|
189
|
+
ops.push({ op: "removeBlock", blockId: block.id });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (let i = 0; i < after.blocks.length; i++) {
|
|
193
|
+
const afterBlock = getBlockAt(after.blocks, i);
|
|
194
|
+
if (!afterBlock) continue;
|
|
195
|
+
const beforeBlock = beforeBlockMap.get(afterBlock.id);
|
|
196
|
+
const prevBlock = i > 0 ? getBlockAt(after.blocks, i - 1) : void 0;
|
|
197
|
+
const afterBlockId = prevBlock?.id;
|
|
198
|
+
if (!beforeBlock) {
|
|
199
|
+
ops.push({
|
|
200
|
+
op: "addBlock",
|
|
201
|
+
block: afterBlock,
|
|
202
|
+
afterBlockId
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
if (!blockContentEqual(beforeBlock, afterBlock)) {
|
|
206
|
+
const data = {};
|
|
207
|
+
if (afterBlock.type !== beforeBlock.type) {
|
|
208
|
+
data.type = afterBlock.type;
|
|
209
|
+
}
|
|
210
|
+
if (!deepEqual(afterBlock.data, beforeBlock.data)) {
|
|
211
|
+
data.data = afterBlock.data;
|
|
212
|
+
}
|
|
213
|
+
if (!deepEqual(afterBlock.position, beforeBlock.position)) {
|
|
214
|
+
data.position = afterBlock.position;
|
|
215
|
+
}
|
|
216
|
+
if (!deepEqual(afterBlock.children, beforeBlock.children)) {
|
|
217
|
+
data.children = afterBlock.children;
|
|
218
|
+
}
|
|
219
|
+
if (!deepEqual(afterBlock.diagnostics, beforeBlock.diagnostics)) {
|
|
220
|
+
data.diagnostics = afterBlock.diagnostics;
|
|
221
|
+
}
|
|
222
|
+
if (!deepEqual(afterBlock.metadata, beforeBlock.metadata)) {
|
|
223
|
+
data.metadata = afterBlock.metadata;
|
|
224
|
+
}
|
|
225
|
+
ops.push({
|
|
226
|
+
op: "updateBlock",
|
|
227
|
+
blockId: afterBlock.id,
|
|
228
|
+
data
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const beforeIndex = before.blocks.findIndex(
|
|
232
|
+
(b) => b.id === afterBlock.id
|
|
233
|
+
);
|
|
234
|
+
const prevBeforeBlock = beforeIndex > 0 ? getBlockAt(before.blocks, beforeIndex - 1) : void 0;
|
|
235
|
+
const expectedAfterBlockId = prevBeforeBlock?.id;
|
|
236
|
+
if (afterBlockId !== expectedAfterBlockId) {
|
|
237
|
+
ops.push({
|
|
238
|
+
op: "moveBlock",
|
|
239
|
+
blockId: afterBlock.id,
|
|
240
|
+
afterBlockId
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const beforeRefMap = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const ref of before.references) {
|
|
247
|
+
beforeRefMap.set(ref.id, ref);
|
|
248
|
+
}
|
|
249
|
+
const afterRefMap = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const ref of after.references) {
|
|
251
|
+
afterRefMap.set(ref.id, ref);
|
|
252
|
+
}
|
|
253
|
+
for (const ref of before.references) {
|
|
254
|
+
if (!afterRefMap.has(ref.id)) {
|
|
255
|
+
ops.push({ op: "removeReference", referenceId: ref.id });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const ref of after.references) {
|
|
259
|
+
const beforeRef = beforeRefMap.get(ref.id);
|
|
260
|
+
if (!beforeRef) {
|
|
261
|
+
ops.push({ op: "addReference", reference: ref });
|
|
262
|
+
} else if (!referenceEqual(beforeRef, ref)) {
|
|
263
|
+
ops.push({ op: "removeReference", referenceId: ref.id });
|
|
264
|
+
ops.push({ op: "addReference", reference: ref });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return ops;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/patch.ts
|
|
271
|
+
function deepClone(value) {
|
|
272
|
+
return JSON.parse(JSON.stringify(value));
|
|
273
|
+
}
|
|
274
|
+
function applyOperation(ir, op) {
|
|
275
|
+
switch (op.op) {
|
|
276
|
+
case "addBlock": {
|
|
277
|
+
const blocks = [...ir.blocks];
|
|
278
|
+
if (op.afterBlockId) {
|
|
279
|
+
const idx = blocks.findIndex((b) => b.id === op.afterBlockId);
|
|
280
|
+
if (idx !== -1) {
|
|
281
|
+
blocks.splice(idx + 1, 0, deepClone(op.block));
|
|
282
|
+
} else {
|
|
283
|
+
blocks.push(deepClone(op.block));
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
blocks.unshift(deepClone(op.block));
|
|
287
|
+
}
|
|
288
|
+
return { ...ir, blocks };
|
|
289
|
+
}
|
|
290
|
+
case "removeBlock": {
|
|
291
|
+
const blocks = ir.blocks.filter((b) => b.id !== op.blockId);
|
|
292
|
+
return { ...ir, blocks };
|
|
293
|
+
}
|
|
294
|
+
case "updateBlock": {
|
|
295
|
+
const blocks = ir.blocks.map((b) => {
|
|
296
|
+
if (b.id !== op.blockId) return b;
|
|
297
|
+
const updated = { ...b };
|
|
298
|
+
if (op.data.type !== void 0) updated.type = op.data.type;
|
|
299
|
+
if (op.data.data !== void 0) updated.data = deepClone(op.data.data);
|
|
300
|
+
if (op.data.position !== void 0)
|
|
301
|
+
updated.position = deepClone(op.data.position);
|
|
302
|
+
if (op.data.children !== void 0)
|
|
303
|
+
updated.children = deepClone(op.data.children);
|
|
304
|
+
if (op.data.diagnostics !== void 0)
|
|
305
|
+
updated.diagnostics = deepClone(op.data.diagnostics);
|
|
306
|
+
if (op.data.metadata !== void 0)
|
|
307
|
+
updated.metadata = deepClone(op.data.metadata);
|
|
308
|
+
return updated;
|
|
309
|
+
});
|
|
310
|
+
return { ...ir, blocks };
|
|
311
|
+
}
|
|
312
|
+
case "moveBlock": {
|
|
313
|
+
const blockToMove = ir.blocks.find((b) => b.id === op.blockId);
|
|
314
|
+
if (!blockToMove) return ir;
|
|
315
|
+
const blocks = ir.blocks.filter((b) => b.id !== op.blockId);
|
|
316
|
+
if (op.afterBlockId) {
|
|
317
|
+
const idx = blocks.findIndex((b) => b.id === op.afterBlockId);
|
|
318
|
+
if (idx !== -1) {
|
|
319
|
+
blocks.splice(idx + 1, 0, blockToMove);
|
|
320
|
+
} else {
|
|
321
|
+
blocks.push(blockToMove);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
blocks.unshift(blockToMove);
|
|
325
|
+
}
|
|
326
|
+
return { ...ir, blocks };
|
|
327
|
+
}
|
|
328
|
+
case "addReference": {
|
|
329
|
+
return {
|
|
330
|
+
...ir,
|
|
331
|
+
references: [...ir.references, deepClone(op.reference)]
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
case "removeReference": {
|
|
335
|
+
return {
|
|
336
|
+
...ir,
|
|
337
|
+
references: ir.references.filter((r) => r.id !== op.referenceId)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
case "updateMetadata": {
|
|
341
|
+
return { ...ir, metadata: deepClone(op.metadata) };
|
|
342
|
+
}
|
|
343
|
+
case "updateLayout": {
|
|
344
|
+
const newLayout = {
|
|
345
|
+
mode: ir.layout.mode,
|
|
346
|
+
...deepClone(op.layout)
|
|
347
|
+
};
|
|
348
|
+
return { ...ir, layout: newLayout };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function applyPatch(ir, patch) {
|
|
353
|
+
if (patch.length === 0) return ir;
|
|
354
|
+
let result = deepClone(ir);
|
|
355
|
+
for (const op of patch) {
|
|
356
|
+
result = applyOperation(result, op);
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
function composePatch(a, b) {
|
|
361
|
+
return [...a, ...b];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/migrate.ts
|
|
365
|
+
var migrations = [];
|
|
366
|
+
function registerMigration(migration) {
|
|
367
|
+
migrations.push(migration);
|
|
368
|
+
}
|
|
369
|
+
function compareVersions(a, b) {
|
|
370
|
+
const partsA = a.split(".").map(Number);
|
|
371
|
+
const partsB = b.split(".").map(Number);
|
|
372
|
+
const len = Math.max(partsA.length, partsB.length);
|
|
373
|
+
for (let i = 0; i < len; i++) {
|
|
374
|
+
const numA = partsA[i] ?? 0;
|
|
375
|
+
const numB = partsB[i] ?? 0;
|
|
376
|
+
if (numA < numB) return -1;
|
|
377
|
+
if (numA > numB) return 1;
|
|
378
|
+
}
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
function migrateIR(ir, targetVersion) {
|
|
382
|
+
let current = { ...ir };
|
|
383
|
+
if (current.version === targetVersion) {
|
|
384
|
+
return current;
|
|
385
|
+
}
|
|
386
|
+
if (compareVersions(current.version, targetVersion) > 0) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Cannot downgrade IR from version "${current.version}" to "${targetVersion}". Only forward migrations are supported.`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const migrationMap = /* @__PURE__ */ new Map();
|
|
392
|
+
for (const m of migrations) {
|
|
393
|
+
migrationMap.set(m.from, m);
|
|
394
|
+
}
|
|
395
|
+
let iterations = 0;
|
|
396
|
+
const maxIterations = migrations.length + 1;
|
|
397
|
+
while (current.version !== targetVersion && iterations < maxIterations) {
|
|
398
|
+
const migration = migrationMap.get(current.version);
|
|
399
|
+
if (!migration) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`No migration registered from version "${current.version}" toward target "${targetVersion}".`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (compareVersions(migration.to, targetVersion) > 0) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Migration from "${migration.from}" to "${migration.to}" would overshoot target version "${targetVersion}".`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
current = migration.migrate(current);
|
|
410
|
+
current = { ...current, version: migration.to };
|
|
411
|
+
iterations++;
|
|
412
|
+
}
|
|
413
|
+
if (current.version !== targetVersion) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Could not reach target version "${targetVersion}" from "${ir.version}". Stopped at "${current.version}".`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return current;
|
|
419
|
+
}
|
|
420
|
+
function clearMigrations() {
|
|
421
|
+
migrations.length = 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/factory.ts
|
|
425
|
+
function createEmptyIR(id) {
|
|
426
|
+
return {
|
|
427
|
+
version: "1.0.0",
|
|
428
|
+
id,
|
|
429
|
+
metadata: {},
|
|
430
|
+
blocks: [],
|
|
431
|
+
references: [],
|
|
432
|
+
layout: {
|
|
433
|
+
mode: "document",
|
|
434
|
+
spacing: "normal"
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export { applyPatch, clearMigrations, composePatch, createEmptyIR, diffIR, generateBlockId, generateDocumentId, migrateIR, registerMigration, resolveBlockIdCollisions, validateIR };
|
|
440
|
+
//# sourceMappingURL=index.js.map
|
|
441
|
+
//# sourceMappingURL=index.js.map
|