@texuli/blocks 0.0.0-stage → 0.1.0-next.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 +77 -2
- package/dist/code/CodeBlockView.svelte +135 -0
- package/dist/code/CodeBlockView.svelte.d.ts +5 -0
- package/dist/code/check.d.ts +15 -0
- package/dist/code/check.js +14 -0
- package/dist/code/code.css +201 -0
- package/dist/code/code.css.d.ts +1 -0
- package/dist/code/engine-entry.d.ts +10 -0
- package/dist/code/engine-entry.js +10 -0
- package/dist/code/engine.d.ts +18 -0
- package/dist/code/engine.js +18 -0
- package/dist/code/family-engine.d.ts +23 -0
- package/dist/code/family-engine.js +4 -0
- package/dist/code/family.d.ts +7 -0
- package/dist/code/family.js +5 -0
- package/dist/code/grammars.d.ts +12 -0
- package/dist/code/grammars.js +83 -0
- package/dist/code/highlight.d.ts +15 -0
- package/dist/code/highlight.js +53 -0
- package/dist/code/index.d.ts +13 -0
- package/dist/code/index.js +12 -0
- package/dist/code/labels.d.ts +27 -0
- package/dist/code/labels.js +24 -0
- package/dist/code/languages/bash.d.ts +3 -0
- package/dist/code/languages/bash.js +48 -0
- package/dist/code/languages/css.d.ts +10 -0
- package/dist/code/languages/css.js +67 -0
- package/dist/code/languages/html.d.ts +6 -0
- package/dist/code/languages/html.js +35 -0
- package/dist/code/languages/javascript.d.ts +3 -0
- package/dist/code/languages/javascript.js +113 -0
- package/dist/code/languages/json.d.ts +3 -0
- package/dist/code/languages/json.js +24 -0
- package/dist/code/languages/python.d.ts +3 -0
- package/dist/code/languages/python.js +35 -0
- package/dist/code/languages/shared.d.ts +22 -0
- package/dist/code/languages/shared.js +37 -0
- package/dist/code/languages/sql.d.ts +3 -0
- package/dist/code/languages/sql.js +32 -0
- package/dist/code/languages/yaml.d.ts +7 -0
- package/dist/code/languages/yaml.js +36 -0
- package/dist/code/renderers.d.ts +5 -0
- package/dist/code/renderers.js +5 -0
- package/dist/code/token-styles.d.ts +25 -0
- package/dist/code/token-styles.js +51 -0
- package/dist/code/tokenize-code.d.ts +17 -0
- package/dist/code/tokenize-code.js +34 -0
- package/dist/code/tokenizer.d.ts +76 -0
- package/dist/code/tokenizer.js +171 -0
- package/dist/core/catalog/entries.d.ts +10 -0
- package/dist/core/catalog/entries.js +589 -0
- package/dist/core/catalog/index.d.ts +4 -0
- package/dist/core/catalog/index.js +2 -0
- package/dist/core/catalog/types.d.ts +36 -0
- package/dist/core/catalog/types.js +1 -0
- package/dist/core/catalog/validate.d.ts +15 -0
- package/dist/core/catalog/validate.js +46 -0
- package/dist/core/formula/evaluate.d.ts +14 -0
- package/dist/core/formula/evaluate.js +181 -0
- package/dist/core/formula/index.d.ts +8 -0
- package/dist/core/formula/index.js +6 -0
- package/dist/core/formula/parse.d.ts +19 -0
- package/dist/core/formula/parse.js +452 -0
- package/dist/core/formula/types.d.ts +76 -0
- package/dist/core/formula/types.js +7 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +13 -0
- package/dist/core/limits.d.ts +27 -0
- package/dist/core/limits.js +26 -0
- package/dist/core/safety.d.ts +30 -0
- package/dist/core/safety.js +110 -0
- package/dist/core/schema/blocks.d.ts +138 -0
- package/dist/core/schema/blocks.js +181 -0
- package/dist/core/schema/code-family.d.ts +4 -0
- package/dist/core/schema/code-family.js +75 -0
- package/dist/core/schema/line-spec.d.ts +7 -0
- package/dist/core/schema/line-spec.js +19 -0
- package/dist/core/schema/media-family.d.ts +4 -0
- package/dist/core/schema/media-family.js +260 -0
- package/dist/core/schema/parse-family.d.ts +14 -0
- package/dist/core/schema/parse-family.js +37 -0
- package/dist/core/schema/primitives.d.ts +73 -0
- package/dist/core/schema/primitives.js +140 -0
- package/dist/core/schema/text-family.d.ts +4 -0
- package/dist/core/schema/text-family.js +121 -0
- package/dist/core/schema/vocabulary.d.ts +20 -0
- package/dist/core/schema/vocabulary.js +33 -0
- package/dist/core/text.d.ts +31 -0
- package/dist/core/text.js +139 -0
- package/dist/core/untrusted.d.ts +20 -0
- package/dist/core/untrusted.js +377 -0
- package/dist/css-modules.d.ts +1 -0
- package/dist/css.d.ts +1 -0
- package/dist/data/ChartBlockView.svelte +77 -0
- package/dist/data/ChartBlockView.svelte.d.ts +10 -0
- package/dist/data/DataBlockView.svelte +109 -0
- package/dist/data/DataBlockView.svelte.d.ts +5 -0
- package/dist/data/check.d.ts +10 -0
- package/dist/data/check.js +9 -0
- package/dist/data/engine.d.ts +18 -0
- package/dist/data/engine.js +10 -0
- package/dist/data/family.d.ts +6 -0
- package/dist/data/family.js +4 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +8 -0
- package/dist/data/labels.d.ts +8 -0
- package/dist/data/labels.js +8 -0
- package/dist/data/renderers.d.ts +2 -0
- package/dist/data/renderers.js +2 -0
- package/dist/data/schema.d.ts +84 -0
- package/dist/data/schema.js +385 -0
- package/dist/diagram/BoardDiagram.svelte +48 -0
- package/dist/diagram/BoardDiagram.svelte.d.ts +8 -0
- package/dist/diagram/DiagramFamily.svelte +46 -0
- package/dist/diagram/DiagramFamily.svelte.d.ts +10 -0
- package/dist/diagram/FlowDiagram.svelte +108 -0
- package/dist/diagram/FlowDiagram.svelte.d.ts +8 -0
- package/dist/diagram/TerminalDiagram.svelte +38 -0
- package/dist/diagram/TerminalDiagram.svelte.d.ts +8 -0
- package/dist/diagram/TreeDiagram.svelte +149 -0
- package/dist/diagram/TreeDiagram.svelte.d.ts +8 -0
- package/dist/diagram/diagram.css +287 -0
- package/dist/diagram/diagram.css.d.ts +1 -0
- package/dist/diagram/engine.d.ts +16 -0
- package/dist/diagram/engine.js +14 -0
- package/dist/diagram/family.d.ts +7 -0
- package/dist/diagram/family.js +5 -0
- package/dist/diagram/index.d.ts +10 -0
- package/dist/diagram/index.js +9 -0
- package/dist/diagram/layout.d.ts +26 -0
- package/dist/diagram/layout.js +165 -0
- package/dist/diagram/schema.d.ts +74 -0
- package/dist/diagram/schema.js +358 -0
- package/dist/diagram/tones.d.ts +6 -0
- package/dist/diagram/tones.js +16 -0
- package/dist/embed/EmbedApp.svelte +138 -0
- package/dist/embed/EmbedApp.svelte.d.ts +17 -0
- package/dist/embed/EmbedBlockView.svelte +32 -0
- package/dist/embed/EmbedBlockView.svelte.d.ts +5 -0
- package/dist/embed/LinkPreview.svelte +48 -0
- package/dist/embed/LinkPreview.svelte.d.ts +10 -0
- package/dist/embed/engine.d.ts +30 -0
- package/dist/embed/engine.js +17 -0
- package/dist/embed/family.d.ts +7 -0
- package/dist/embed/family.js +5 -0
- package/dist/embed/index.d.ts +5 -0
- package/dist/embed/index.js +3 -0
- package/dist/embed/renderers.d.ts +3 -0
- package/dist/embed/renderers.js +3 -0
- package/dist/embed/sandbox.d.ts +86 -0
- package/dist/embed/sandbox.js +114 -0
- package/dist/embed/schema.d.ts +30 -0
- package/dist/embed/schema.js +217 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/interactive/CalculatorBlock.svelte +61 -0
- package/dist/interactive/CalculatorBlock.svelte.d.ts +10 -0
- package/dist/interactive/ChecklistBlock.svelte +44 -0
- package/dist/interactive/ChecklistBlock.svelte.d.ts +10 -0
- package/dist/interactive/InteractiveBlockView.svelte +31 -0
- package/dist/interactive/InteractiveBlockView.svelte.d.ts +5 -0
- package/dist/interactive/PollBlock.svelte +111 -0
- package/dist/interactive/PollBlock.svelte.d.ts +10 -0
- package/dist/interactive/QuizBlock.svelte +75 -0
- package/dist/interactive/QuizBlock.svelte.d.ts +10 -0
- package/dist/interactive/check.d.ts +10 -0
- package/dist/interactive/check.js +9 -0
- package/dist/interactive/engine.d.ts +19 -0
- package/dist/interactive/engine.js +12 -0
- package/dist/interactive/family.d.ts +6 -0
- package/dist/interactive/family.js +4 -0
- package/dist/interactive/index.d.ts +13 -0
- package/dist/interactive/index.js +13 -0
- package/dist/interactive/interactive.css +97 -0
- package/dist/interactive/interactive.css.d.ts +1 -0
- package/dist/interactive/labels.d.ts +16 -0
- package/dist/interactive/labels.js +16 -0
- package/dist/interactive/renderers.d.ts +9 -0
- package/dist/interactive/renderers.js +9 -0
- package/dist/interactive/schema.d.ts +76 -0
- package/dist/interactive/schema.js +324 -0
- package/dist/map/MapBlockView.svelte +465 -0
- package/dist/map/MapBlockView.svelte.d.ts +5 -0
- package/dist/map/check.d.ts +11 -0
- package/dist/map/check.js +6 -0
- package/dist/map/engine.d.ts +19 -0
- package/dist/map/engine.js +8 -0
- package/dist/map/family.d.ts +7 -0
- package/dist/map/family.js +5 -0
- package/dist/map/index.d.ts +11 -0
- package/dist/map/index.js +10 -0
- package/dist/map/labels.d.ts +27 -0
- package/dist/map/labels.js +21 -0
- package/dist/map/renderers.d.ts +2 -0
- package/dist/map/renderers.js +2 -0
- package/dist/map/schema.d.ts +41 -0
- package/dist/map/schema.js +176 -0
- package/dist/map/view-model.d.ts +55 -0
- package/dist/map/view-model.js +168 -0
- package/dist/media/MediaBlockView.svelte +258 -0
- package/dist/media/MediaBlockView.svelte.d.ts +5 -0
- package/dist/media/check.d.ts +14 -0
- package/dist/media/check.js +13 -0
- package/dist/media/engine.d.ts +18 -0
- package/dist/media/engine.js +10 -0
- package/dist/media/family.d.ts +7 -0
- package/dist/media/family.js +5 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.js +10 -0
- package/dist/media/labels.d.ts +7 -0
- package/dist/media/labels.js +12 -0
- package/dist/media/renderers.d.ts +5 -0
- package/dist/media/renderers.js +5 -0
- package/dist/media/resolve.d.ts +6 -0
- package/dist/media/resolve.js +15 -0
- package/dist/structure/NestedBlocks.svelte +65 -0
- package/dist/structure/NestedBlocks.svelte.d.ts +11 -0
- package/dist/structure/SafeBody.svelte +13 -0
- package/dist/structure/SafeBody.svelte.d.ts +7 -0
- package/dist/structure/StructureFamily.svelte +157 -0
- package/dist/structure/StructureFamily.svelte.d.ts +5 -0
- package/dist/structure/check.d.ts +14 -0
- package/dist/structure/check.js +27 -0
- package/dist/structure/engine.d.ts +18 -0
- package/dist/structure/engine.js +18 -0
- package/dist/structure/family.d.ts +7 -0
- package/dist/structure/family.js +5 -0
- package/dist/structure/index.d.ts +11 -0
- package/dist/structure/index.js +10 -0
- package/dist/structure/labels.d.ts +11 -0
- package/dist/structure/labels.js +11 -0
- package/dist/structure/renderers.d.ts +5 -0
- package/dist/structure/renderers.js +5 -0
- package/dist/structure/schema.d.ts +71 -0
- package/dist/structure/schema.js +327 -0
- package/dist/structure/structure.css +182 -0
- package/dist/structure/structure.css.d.ts +1 -0
- package/dist/text/RichText.svelte +169 -0
- package/dist/text/RichText.svelte.d.ts +13 -0
- package/dist/text/TextBlockView.svelte +177 -0
- package/dist/text/TextBlockView.svelte.d.ts +5 -0
- package/dist/text/check.d.ts +14 -0
- package/dist/text/check.js +13 -0
- package/dist/text/engine.d.ts +17 -0
- package/dist/text/engine.js +10 -0
- package/dist/text/family.d.ts +7 -0
- package/dist/text/family.js +5 -0
- package/dist/text/index.d.ts +12 -0
- package/dist/text/index.js +11 -0
- package/dist/text/labels.d.ts +22 -0
- package/dist/text/labels.js +29 -0
- package/dist/text/markdown.d.ts +80 -0
- package/dist/text/markdown.js +361 -0
- package/dist/text/renderers.d.ts +6 -0
- package/dist/text/renderers.js +6 -0
- package/package.json +167 -4
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const FLOW_NODE_WIDTH = 160;
|
|
2
|
+
const FLOW_NODE_HEIGHT = 64;
|
|
3
|
+
const FLOW_GAP = 48;
|
|
4
|
+
const TREE_NODE_WIDTH = 176;
|
|
5
|
+
const TREE_NODE_HEIGHT = 56;
|
|
6
|
+
const TREE_X_GAP = 48;
|
|
7
|
+
const TREE_Y_GAP = 40;
|
|
8
|
+
const BOARD_COLUMN_WIDTH = 224;
|
|
9
|
+
const BOARD_CARD_WIDTH = 200;
|
|
10
|
+
const BOARD_CARD_HEIGHT = 64;
|
|
11
|
+
const BOARD_ROW_GAP = 16;
|
|
12
|
+
export function layoutFlow(diagram) {
|
|
13
|
+
const positions = diagram.nodes.map((node, index) => ({
|
|
14
|
+
id: node.id,
|
|
15
|
+
x: index * (FLOW_NODE_WIDTH + FLOW_GAP),
|
|
16
|
+
y: 0,
|
|
17
|
+
width: FLOW_NODE_WIDTH,
|
|
18
|
+
height: FLOW_NODE_HEIGHT,
|
|
19
|
+
label: node.label,
|
|
20
|
+
...(node.tone === undefined ? {} : { tone: node.tone }),
|
|
21
|
+
}));
|
|
22
|
+
const edges = diagram.nodes.slice(1).map((node, index) => ({
|
|
23
|
+
from: diagram.nodes[index]?.id ?? '',
|
|
24
|
+
to: node.id,
|
|
25
|
+
kind: 'flow',
|
|
26
|
+
}));
|
|
27
|
+
if (diagram.loopBack && diagram.nodes.length > 1) {
|
|
28
|
+
edges.push({
|
|
29
|
+
from: diagram.nodes.at(-1)?.id ?? '',
|
|
30
|
+
to: diagram.nodes[0]?.id ?? '',
|
|
31
|
+
kind: 'loop-back',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
width: diagram.nodes.length * FLOW_NODE_WIDTH + (diagram.nodes.length - 1) * FLOW_GAP,
|
|
36
|
+
height: FLOW_NODE_HEIGHT + (diagram.loopBack ? 40 : 0),
|
|
37
|
+
positions,
|
|
38
|
+
edges,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function layoutTree(diagram) {
|
|
42
|
+
const byId = new Map(diagram.nodes.map((node) => [node.id, node]));
|
|
43
|
+
const children = new Map();
|
|
44
|
+
for (const node of diagram.nodes) {
|
|
45
|
+
const list = children.get(node.parentId) ?? [];
|
|
46
|
+
list.push(node.id);
|
|
47
|
+
children.set(node.parentId, list);
|
|
48
|
+
}
|
|
49
|
+
const roots = children.get(null) ?? [];
|
|
50
|
+
const positions = new Map();
|
|
51
|
+
const edges = [];
|
|
52
|
+
if (diagram.style === 'file-tree') {
|
|
53
|
+
let row = 0;
|
|
54
|
+
const walk = (id, depth) => {
|
|
55
|
+
const node = byId.get(id);
|
|
56
|
+
if (!node)
|
|
57
|
+
return;
|
|
58
|
+
positions.set(id, {
|
|
59
|
+
id,
|
|
60
|
+
x: depth * (TREE_NODE_WIDTH / 2),
|
|
61
|
+
y: row * (TREE_NODE_HEIGHT + TREE_Y_GAP),
|
|
62
|
+
width: TREE_NODE_WIDTH,
|
|
63
|
+
height: TREE_NODE_HEIGHT,
|
|
64
|
+
label: node.label,
|
|
65
|
+
...(node.tone === undefined ? {} : { tone: node.tone }),
|
|
66
|
+
});
|
|
67
|
+
row += 1;
|
|
68
|
+
for (const childId of children.get(id) ?? []) {
|
|
69
|
+
edges.push({ from: id, to: childId, kind: 'parent' });
|
|
70
|
+
walk(childId, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
for (const root of roots)
|
|
74
|
+
walk(root, 0);
|
|
75
|
+
const width = Math.max(TREE_NODE_WIDTH, ...[...positions.values()].map((position) => position.x + position.width));
|
|
76
|
+
const height = Math.max(TREE_NODE_HEIGHT, positions.size * (TREE_NODE_HEIGHT + TREE_Y_GAP) - TREE_Y_GAP);
|
|
77
|
+
return { width, height, positions: [...positions.values()], edges };
|
|
78
|
+
}
|
|
79
|
+
let leaf = 0;
|
|
80
|
+
let maxDepth = 0;
|
|
81
|
+
const place = (id, depth) => {
|
|
82
|
+
const node = byId.get(id);
|
|
83
|
+
if (!node)
|
|
84
|
+
return 0;
|
|
85
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
86
|
+
const childIds = children.get(id) ?? [];
|
|
87
|
+
let x;
|
|
88
|
+
if (childIds.length === 0) {
|
|
89
|
+
x = leaf * (TREE_NODE_WIDTH + TREE_X_GAP);
|
|
90
|
+
leaf += 1;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const childXs = childIds.map((childId) => {
|
|
94
|
+
edges.push({ from: id, to: childId, kind: 'parent' });
|
|
95
|
+
return place(childId, depth + 1);
|
|
96
|
+
});
|
|
97
|
+
x = ((childXs[0] ?? 0) + (childXs.at(-1) ?? 0)) / 2;
|
|
98
|
+
}
|
|
99
|
+
positions.set(id, {
|
|
100
|
+
id,
|
|
101
|
+
x,
|
|
102
|
+
y: depth * (TREE_NODE_HEIGHT + TREE_Y_GAP),
|
|
103
|
+
width: TREE_NODE_WIDTH,
|
|
104
|
+
height: TREE_NODE_HEIGHT,
|
|
105
|
+
label: node.label,
|
|
106
|
+
...(node.tone === undefined ? {} : { tone: node.tone }),
|
|
107
|
+
});
|
|
108
|
+
return x;
|
|
109
|
+
};
|
|
110
|
+
for (const root of roots)
|
|
111
|
+
place(root, 0);
|
|
112
|
+
const values = [...positions.values()];
|
|
113
|
+
// Each leaf receives a stable top-left slot; parents center between their
|
|
114
|
+
// first and last child slots.
|
|
115
|
+
const width = Math.max(TREE_NODE_WIDTH, leaf * (TREE_NODE_WIDTH + TREE_X_GAP) - TREE_X_GAP);
|
|
116
|
+
const height = (maxDepth + 1) * (TREE_NODE_HEIGHT + TREE_Y_GAP) - TREE_Y_GAP;
|
|
117
|
+
return { width, height, positions: values, edges };
|
|
118
|
+
}
|
|
119
|
+
export function layoutBoard(diagram) {
|
|
120
|
+
const positions = [];
|
|
121
|
+
const edges = [];
|
|
122
|
+
diagram.columns.forEach((column, columnIndex) => {
|
|
123
|
+
const x = columnIndex * BOARD_COLUMN_WIDTH;
|
|
124
|
+
positions.push({
|
|
125
|
+
id: column.id,
|
|
126
|
+
x,
|
|
127
|
+
y: 0,
|
|
128
|
+
width: BOARD_CARD_WIDTH,
|
|
129
|
+
height: BOARD_CARD_HEIGHT,
|
|
130
|
+
label: column.title,
|
|
131
|
+
});
|
|
132
|
+
column.cards.forEach((card, cardIndex) => {
|
|
133
|
+
positions.push({
|
|
134
|
+
id: card.id,
|
|
135
|
+
x,
|
|
136
|
+
y: (cardIndex + 1) * (BOARD_CARD_HEIGHT + BOARD_ROW_GAP),
|
|
137
|
+
width: BOARD_CARD_WIDTH,
|
|
138
|
+
height: BOARD_CARD_HEIGHT,
|
|
139
|
+
label: card.title,
|
|
140
|
+
...(card.tone === undefined ? {} : { tone: card.tone }),
|
|
141
|
+
});
|
|
142
|
+
edges.push({ from: column.id, to: card.id, kind: 'parent' });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
const maxCards = Math.max(0, ...diagram.columns.map((column) => column.cards.length));
|
|
146
|
+
return {
|
|
147
|
+
width: diagram.columns.length * BOARD_COLUMN_WIDTH - (BOARD_COLUMN_WIDTH - BOARD_CARD_WIDTH),
|
|
148
|
+
height: (maxCards + 1) * (BOARD_CARD_HEIGHT + BOARD_ROW_GAP) - BOARD_ROW_GAP,
|
|
149
|
+
positions,
|
|
150
|
+
edges,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function layoutTerminal(diagram) {
|
|
154
|
+
const lineHeight = 28;
|
|
155
|
+
const positions = diagram.lines.map((line, index) => ({
|
|
156
|
+
id: `line-${index + 1}`,
|
|
157
|
+
x: 0,
|
|
158
|
+
y: index * lineHeight,
|
|
159
|
+
width: 720,
|
|
160
|
+
height: lineHeight,
|
|
161
|
+
label: line.text,
|
|
162
|
+
kind: line.kind,
|
|
163
|
+
}));
|
|
164
|
+
return { width: 720, height: diagram.lines.length * lineHeight, positions, edges: [] };
|
|
165
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded diagram models (flow, tree/org, board, terminal). Untrusted input
|
|
3
|
+
* is parsed without throwing; diagnostics use blueprint-style JSON paths.
|
|
4
|
+
* No third-party schema library.
|
|
5
|
+
*/
|
|
6
|
+
import type { BlockBase } from '../core/schema/primitives.js';
|
|
7
|
+
import type { BlockTone } from '../core/schema/vocabulary.js';
|
|
8
|
+
export declare const DIAGRAM_LIMITS: Readonly<{
|
|
9
|
+
flowNodes: 12;
|
|
10
|
+
treeNodes: 64;
|
|
11
|
+
treeDepth: 6;
|
|
12
|
+
boardColumns: 6;
|
|
13
|
+
boardCardsPerColumn: 8;
|
|
14
|
+
terminalLines: 40;
|
|
15
|
+
}>;
|
|
16
|
+
export type FlowNode = {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
tone?: BlockTone;
|
|
20
|
+
};
|
|
21
|
+
export type FlowDiagram = BlockBase & {
|
|
22
|
+
type: 'flow';
|
|
23
|
+
nodes: FlowNode[];
|
|
24
|
+
loopBack?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type TreeNode = {
|
|
27
|
+
id: string;
|
|
28
|
+
parentId: string | null;
|
|
29
|
+
label: string;
|
|
30
|
+
tone?: BlockTone;
|
|
31
|
+
};
|
|
32
|
+
export type TreeDiagram = BlockBase & {
|
|
33
|
+
type: 'tree';
|
|
34
|
+
style: 'file-tree' | 'org';
|
|
35
|
+
nodes: TreeNode[];
|
|
36
|
+
};
|
|
37
|
+
export type BoardCard = {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
tone?: BlockTone;
|
|
41
|
+
};
|
|
42
|
+
export type BoardColumn = {
|
|
43
|
+
id: string;
|
|
44
|
+
title: string;
|
|
45
|
+
cards: BoardCard[];
|
|
46
|
+
};
|
|
47
|
+
export type BoardDiagram = BlockBase & {
|
|
48
|
+
type: 'board';
|
|
49
|
+
columns: BoardColumn[];
|
|
50
|
+
};
|
|
51
|
+
export type TerminalLine = {
|
|
52
|
+
kind: 'prompt' | 'output' | 'comment';
|
|
53
|
+
text: string;
|
|
54
|
+
};
|
|
55
|
+
export type TerminalDiagram = BlockBase & {
|
|
56
|
+
type: 'terminal';
|
|
57
|
+
lines: TerminalLine[];
|
|
58
|
+
};
|
|
59
|
+
export type Diagram = FlowDiagram | TreeDiagram | BoardDiagram | TerminalDiagram;
|
|
60
|
+
export type DiagramType = Diagram['type'];
|
|
61
|
+
export declare const DIAGRAM_BLOCK_TYPES: readonly ["flow", "tree", "board", "terminal"];
|
|
62
|
+
export type DiagramParseResult = {
|
|
63
|
+
success: true;
|
|
64
|
+
data: Diagram;
|
|
65
|
+
} | {
|
|
66
|
+
success: false;
|
|
67
|
+
issues: {
|
|
68
|
+
jsonPath: string;
|
|
69
|
+
message: string;
|
|
70
|
+
}[];
|
|
71
|
+
};
|
|
72
|
+
export declare function diagramJsonPath(path: readonly (string | number)[]): string;
|
|
73
|
+
/** Parse an untrusted diagram without throwing. */
|
|
74
|
+
export declare function parseDiagram(input: unknown): DiagramParseResult;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded diagram models (flow, tree/org, board, terminal). Untrusted input
|
|
3
|
+
* is parsed without throwing; diagnostics use blueprint-style JSON paths.
|
|
4
|
+
* No third-party schema library.
|
|
5
|
+
*/
|
|
6
|
+
import { BLOCK_LIMITS } from "../core/limits.js";
|
|
7
|
+
import { fail, failMany, ok, parseBlockBase, parsePlainText, parseTone, toJsonPath, } from "../core/schema/primitives.js";
|
|
8
|
+
export const DIAGRAM_LIMITS = Object.freeze({
|
|
9
|
+
flowNodes: BLOCK_LIMITS.flowNodes,
|
|
10
|
+
treeNodes: BLOCK_LIMITS.treeNodes,
|
|
11
|
+
treeDepth: BLOCK_LIMITS.treeDepth,
|
|
12
|
+
boardColumns: BLOCK_LIMITS.boardColumns,
|
|
13
|
+
boardCardsPerColumn: BLOCK_LIMITS.boardCardsPerColumn,
|
|
14
|
+
terminalLines: BLOCK_LIMITS.terminalLines,
|
|
15
|
+
});
|
|
16
|
+
const DIAGRAM_ID = /^[a-z][a-z0-9-]{0,47}$/;
|
|
17
|
+
export const DIAGRAM_BLOCK_TYPES = [
|
|
18
|
+
'flow',
|
|
19
|
+
'tree',
|
|
20
|
+
'board',
|
|
21
|
+
'terminal',
|
|
22
|
+
];
|
|
23
|
+
export function diagramJsonPath(path) {
|
|
24
|
+
return toJsonPath(path);
|
|
25
|
+
}
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function parseDiagramId(value, path) {
|
|
30
|
+
if (typeof value !== 'string' || !DIAGRAM_ID.test(value)) {
|
|
31
|
+
return fail(path, 'Use 1–48 lowercase letters, digits or hyphens, starting with a letter');
|
|
32
|
+
}
|
|
33
|
+
return ok(value);
|
|
34
|
+
}
|
|
35
|
+
function parseFlow(record, path) {
|
|
36
|
+
const base = parseBlockBase(record, path);
|
|
37
|
+
if (!base.success)
|
|
38
|
+
return base;
|
|
39
|
+
if (!Array.isArray(record.nodes))
|
|
40
|
+
return fail([...path, 'nodes'], 'nodes must be an array');
|
|
41
|
+
if (record.nodes.length < 1 || record.nodes.length > DIAGRAM_LIMITS.flowNodes) {
|
|
42
|
+
return fail([...path, 'nodes'], `nodes must have 1–${DIAGRAM_LIMITS.flowNodes} items`);
|
|
43
|
+
}
|
|
44
|
+
if (record.loopBack !== undefined && typeof record.loopBack !== 'boolean') {
|
|
45
|
+
return fail([...path, 'loopBack'], 'loopBack must be a boolean');
|
|
46
|
+
}
|
|
47
|
+
const nodes = [];
|
|
48
|
+
const ids = new Set();
|
|
49
|
+
const issues = [];
|
|
50
|
+
record.nodes.forEach((raw, index) => {
|
|
51
|
+
if (!isPlainObject(raw)) {
|
|
52
|
+
issues.push({ path: [...path, 'nodes', index], message: 'Expected a node object' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const id = parseDiagramId(raw.id, [...path, 'nodes', index, 'id']);
|
|
56
|
+
if (!id.success) {
|
|
57
|
+
issues.push(...id.issues);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (ids.has(id.data)) {
|
|
61
|
+
issues.push({
|
|
62
|
+
path: [...path, 'nodes', index, 'id'],
|
|
63
|
+
message: `Duplicate node id: ${id.data}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
ids.add(id.data);
|
|
67
|
+
const label = parsePlainText(raw.label, [...path, 'nodes', index, 'label'], 120);
|
|
68
|
+
if (!label.success) {
|
|
69
|
+
issues.push(...label.issues);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const tone = parseTone(raw.tone, [...path, 'nodes', index, 'tone']);
|
|
73
|
+
if (!tone.success) {
|
|
74
|
+
issues.push(...tone.issues);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const node = { id: id.data, label: label.data };
|
|
78
|
+
if (tone.data !== undefined)
|
|
79
|
+
node.tone = tone.data;
|
|
80
|
+
nodes.push(node);
|
|
81
|
+
});
|
|
82
|
+
if (issues.length > 0)
|
|
83
|
+
return failMany(issues);
|
|
84
|
+
const data = { ...base.data, type: 'flow', nodes };
|
|
85
|
+
if (record.loopBack === true)
|
|
86
|
+
data.loopBack = true;
|
|
87
|
+
return ok(data);
|
|
88
|
+
}
|
|
89
|
+
function parseTree(record, path) {
|
|
90
|
+
const base = parseBlockBase(record, path);
|
|
91
|
+
if (!base.success)
|
|
92
|
+
return base;
|
|
93
|
+
if (record.style !== 'file-tree' && record.style !== 'org') {
|
|
94
|
+
return fail([...path, 'style'], 'style must be "file-tree" or "org"');
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(record.nodes))
|
|
97
|
+
return fail([...path, 'nodes'], 'nodes must be an array');
|
|
98
|
+
if (record.nodes.length < 1 || record.nodes.length > DIAGRAM_LIMITS.treeNodes) {
|
|
99
|
+
return fail([...path, 'nodes'], `nodes must have 1–${DIAGRAM_LIMITS.treeNodes} items`);
|
|
100
|
+
}
|
|
101
|
+
const nodes = [];
|
|
102
|
+
const byId = new Map();
|
|
103
|
+
const issues = [];
|
|
104
|
+
record.nodes.forEach((raw, index) => {
|
|
105
|
+
if (!isPlainObject(raw)) {
|
|
106
|
+
issues.push({ path: [...path, 'nodes', index], message: 'Expected a node object' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const id = parseDiagramId(raw.id, [...path, 'nodes', index, 'id']);
|
|
110
|
+
if (!id.success) {
|
|
111
|
+
issues.push(...id.issues);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (byId.has(id.data)) {
|
|
115
|
+
issues.push({
|
|
116
|
+
path: [...path, 'nodes', index, 'id'],
|
|
117
|
+
message: `Duplicate node id: ${id.data}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
let parentId;
|
|
121
|
+
if (raw.parentId === null)
|
|
122
|
+
parentId = null;
|
|
123
|
+
else {
|
|
124
|
+
const parent = parseDiagramId(raw.parentId, [...path, 'nodes', index, 'parentId']);
|
|
125
|
+
if (!parent.success) {
|
|
126
|
+
issues.push(...parent.issues);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
parentId = parent.data;
|
|
130
|
+
}
|
|
131
|
+
const label = parsePlainText(raw.label, [...path, 'nodes', index, 'label'], 120);
|
|
132
|
+
if (!label.success) {
|
|
133
|
+
issues.push(...label.issues);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const tone = parseTone(raw.tone, [...path, 'nodes', index, 'tone']);
|
|
137
|
+
if (!tone.success) {
|
|
138
|
+
issues.push(...tone.issues);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const node = { id: id.data, parentId, label: label.data };
|
|
142
|
+
if (tone.data !== undefined)
|
|
143
|
+
node.tone = tone.data;
|
|
144
|
+
nodes.push(node);
|
|
145
|
+
byId.set(node.id, node);
|
|
146
|
+
});
|
|
147
|
+
if (issues.length > 0)
|
|
148
|
+
return failMany(issues);
|
|
149
|
+
const roots = nodes.filter((n) => n.parentId === null);
|
|
150
|
+
if (roots.length === 0) {
|
|
151
|
+
return fail([...path, 'nodes'], 'A tree needs at least one root node');
|
|
152
|
+
}
|
|
153
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
154
|
+
const node = nodes[index];
|
|
155
|
+
if (!node)
|
|
156
|
+
continue;
|
|
157
|
+
if (node.parentId !== null && !byId.has(node.parentId)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
path: [...path, 'nodes', index, 'parentId'],
|
|
160
|
+
message: `Unknown parent id: ${node.parentId}`,
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let depth = 1;
|
|
165
|
+
let parentId = node.parentId;
|
|
166
|
+
const visited = new Set([node.id]);
|
|
167
|
+
while (parentId !== null && byId.has(parentId)) {
|
|
168
|
+
if (visited.has(parentId)) {
|
|
169
|
+
issues.push({
|
|
170
|
+
path: [...path, 'nodes', index, 'parentId'],
|
|
171
|
+
message: 'Tree contains a parent cycle',
|
|
172
|
+
});
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
visited.add(parentId);
|
|
176
|
+
depth += 1;
|
|
177
|
+
parentId = byId.get(parentId)?.parentId ?? null;
|
|
178
|
+
}
|
|
179
|
+
if (depth > DIAGRAM_LIMITS.treeDepth) {
|
|
180
|
+
issues.push({
|
|
181
|
+
path: [...path, 'nodes', index],
|
|
182
|
+
message: `Tree depth exceeds ${DIAGRAM_LIMITS.treeDepth}`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (issues.length > 0)
|
|
187
|
+
return failMany(issues);
|
|
188
|
+
return ok({ ...base.data, type: 'tree', style: record.style, nodes });
|
|
189
|
+
}
|
|
190
|
+
function parseBoard(record, path) {
|
|
191
|
+
const base = parseBlockBase(record, path);
|
|
192
|
+
if (!base.success)
|
|
193
|
+
return base;
|
|
194
|
+
if (!Array.isArray(record.columns))
|
|
195
|
+
return fail([...path, 'columns'], 'columns must be an array');
|
|
196
|
+
if (record.columns.length < 1 || record.columns.length > DIAGRAM_LIMITS.boardColumns) {
|
|
197
|
+
return fail([...path, 'columns'], `columns must have 1–${DIAGRAM_LIMITS.boardColumns} items`);
|
|
198
|
+
}
|
|
199
|
+
const columns = [];
|
|
200
|
+
const ids = new Set();
|
|
201
|
+
const issues = [];
|
|
202
|
+
record.columns.forEach((rawCol, columnIndex) => {
|
|
203
|
+
if (!isPlainObject(rawCol)) {
|
|
204
|
+
issues.push({ path: [...path, 'columns', columnIndex], message: 'Expected a column object' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const id = parseDiagramId(rawCol.id, [...path, 'columns', columnIndex, 'id']);
|
|
208
|
+
if (!id.success) {
|
|
209
|
+
issues.push(...id.issues);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (ids.has(id.data)) {
|
|
213
|
+
issues.push({
|
|
214
|
+
path: [...path, 'columns', columnIndex, 'id'],
|
|
215
|
+
message: `Duplicate diagram id: ${id.data}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
ids.add(id.data);
|
|
219
|
+
const title = parsePlainText(rawCol.title, [...path, 'columns', columnIndex, 'title'], 120);
|
|
220
|
+
if (!title.success) {
|
|
221
|
+
issues.push(...title.issues);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(rawCol.cards)) {
|
|
225
|
+
issues.push({
|
|
226
|
+
path: [...path, 'columns', columnIndex, 'cards'],
|
|
227
|
+
message: 'cards must be an array',
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (rawCol.cards.length > DIAGRAM_LIMITS.boardCardsPerColumn) {
|
|
232
|
+
issues.push({
|
|
233
|
+
path: [...path, 'columns', columnIndex, 'cards'],
|
|
234
|
+
message: `cards must have at most ${DIAGRAM_LIMITS.boardCardsPerColumn} items`,
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const cards = [];
|
|
239
|
+
rawCol.cards.forEach((rawCard, cardIndex) => {
|
|
240
|
+
if (!isPlainObject(rawCard)) {
|
|
241
|
+
issues.push({
|
|
242
|
+
path: [...path, 'columns', columnIndex, 'cards', cardIndex],
|
|
243
|
+
message: 'Expected a card object',
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const cardId = parseDiagramId(rawCard.id, [
|
|
248
|
+
...path,
|
|
249
|
+
'columns',
|
|
250
|
+
columnIndex,
|
|
251
|
+
'cards',
|
|
252
|
+
cardIndex,
|
|
253
|
+
'id',
|
|
254
|
+
]);
|
|
255
|
+
if (!cardId.success) {
|
|
256
|
+
issues.push(...cardId.issues);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (ids.has(cardId.data)) {
|
|
260
|
+
issues.push({
|
|
261
|
+
path: [...path, 'columns', columnIndex, 'cards', cardIndex, 'id'],
|
|
262
|
+
message: `Duplicate diagram id: ${cardId.data}`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
ids.add(cardId.data);
|
|
266
|
+
const cardTitle = parsePlainText(rawCard.title, [...path, 'columns', columnIndex, 'cards', cardIndex, 'title'], 120);
|
|
267
|
+
if (!cardTitle.success) {
|
|
268
|
+
issues.push(...cardTitle.issues);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const tone = parseTone(rawCard.tone, [
|
|
272
|
+
...path,
|
|
273
|
+
'columns',
|
|
274
|
+
columnIndex,
|
|
275
|
+
'cards',
|
|
276
|
+
cardIndex,
|
|
277
|
+
'tone',
|
|
278
|
+
]);
|
|
279
|
+
if (!tone.success) {
|
|
280
|
+
issues.push(...tone.issues);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const card = { id: cardId.data, title: cardTitle.data };
|
|
284
|
+
if (tone.data !== undefined)
|
|
285
|
+
card.tone = tone.data;
|
|
286
|
+
cards.push(card);
|
|
287
|
+
});
|
|
288
|
+
columns.push({ id: id.data, title: title.data, cards });
|
|
289
|
+
});
|
|
290
|
+
if (issues.length > 0)
|
|
291
|
+
return failMany(issues);
|
|
292
|
+
return ok({ ...base.data, type: 'board', columns });
|
|
293
|
+
}
|
|
294
|
+
function parseTerminal(record, path) {
|
|
295
|
+
const base = parseBlockBase(record, path);
|
|
296
|
+
if (!base.success)
|
|
297
|
+
return base;
|
|
298
|
+
if (!Array.isArray(record.lines))
|
|
299
|
+
return fail([...path, 'lines'], 'lines must be an array');
|
|
300
|
+
if (record.lines.length < 1 || record.lines.length > DIAGRAM_LIMITS.terminalLines) {
|
|
301
|
+
return fail([...path, 'lines'], `lines must have 1–${DIAGRAM_LIMITS.terminalLines} items`);
|
|
302
|
+
}
|
|
303
|
+
const lines = [];
|
|
304
|
+
const issues = [];
|
|
305
|
+
record.lines.forEach((raw, index) => {
|
|
306
|
+
if (!isPlainObject(raw)) {
|
|
307
|
+
issues.push({ path: [...path, 'lines', index], message: 'Expected a line object' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (raw.kind !== 'prompt' && raw.kind !== 'output' && raw.kind !== 'comment') {
|
|
311
|
+
issues.push({
|
|
312
|
+
path: [...path, 'lines', index, 'kind'],
|
|
313
|
+
message: 'kind must be prompt, output or comment',
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const text = parsePlainText(raw.text, [...path, 'lines', index, 'text'], 240);
|
|
318
|
+
if (!text.success) {
|
|
319
|
+
issues.push(...text.issues);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
lines.push({ kind: raw.kind, text: text.data });
|
|
323
|
+
});
|
|
324
|
+
if (issues.length > 0)
|
|
325
|
+
return failMany(issues);
|
|
326
|
+
return ok({ ...base.data, type: 'terminal', lines });
|
|
327
|
+
}
|
|
328
|
+
/** Parse an untrusted diagram without throwing. */
|
|
329
|
+
export function parseDiagram(input) {
|
|
330
|
+
if (!isPlainObject(input)) {
|
|
331
|
+
return { success: false, issues: [{ jsonPath: '$', message: 'Expected a diagram object' }] };
|
|
332
|
+
}
|
|
333
|
+
const type = input.type;
|
|
334
|
+
let result;
|
|
335
|
+
if (type === 'flow')
|
|
336
|
+
result = parseFlow(input, []);
|
|
337
|
+
else if (type === 'tree')
|
|
338
|
+
result = parseTree(input, []);
|
|
339
|
+
else if (type === 'board')
|
|
340
|
+
result = parseBoard(input, []);
|
|
341
|
+
else if (type === 'terminal')
|
|
342
|
+
result = parseTerminal(input, []);
|
|
343
|
+
else {
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
issues: [{ jsonPath: '$["type"]', message: 'type must be flow, tree, board or terminal' }],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (result.success)
|
|
350
|
+
return { success: true, data: result.data };
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
issues: result.issues.map((issue) => ({
|
|
354
|
+
jsonPath: diagramJsonPath(issue.path),
|
|
355
|
+
message: issue.message,
|
|
356
|
+
})),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BlockTone } from '../core/schema/vocabulary.js';
|
|
2
|
+
/** Diagram pad around SVG/HTML layout boxes (px). */
|
|
3
|
+
export declare const DIAGRAM_PAD = 16;
|
|
4
|
+
/** Map block tones to accent border classes (token colors). */
|
|
5
|
+
export declare const TONE_BORDER: Readonly<Record<BlockTone, string>>;
|
|
6
|
+
export declare function toneBorder(tone: string | undefined): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Diagram pad around SVG/HTML layout boxes (px). */
|
|
2
|
+
export const DIAGRAM_PAD = 16;
|
|
3
|
+
/** Map block tones to accent border classes (token colors). */
|
|
4
|
+
export const TONE_BORDER = Object.freeze({
|
|
5
|
+
neutral: 'tx-diagram-tone--neutral',
|
|
6
|
+
accent: 'tx-diagram-tone--accent',
|
|
7
|
+
info: 'tx-diagram-tone--info',
|
|
8
|
+
success: 'tx-diagram-tone--success',
|
|
9
|
+
warning: 'tx-diagram-tone--warning',
|
|
10
|
+
danger: 'tx-diagram-tone--danger',
|
|
11
|
+
});
|
|
12
|
+
export function toneBorder(tone) {
|
|
13
|
+
if (tone && tone in TONE_BORDER)
|
|
14
|
+
return TONE_BORDER[tone];
|
|
15
|
+
return TONE_BORDER.neutral;
|
|
16
|
+
}
|