@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.
Files changed (257) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -2
  3. package/dist/code/CodeBlockView.svelte +135 -0
  4. package/dist/code/CodeBlockView.svelte.d.ts +5 -0
  5. package/dist/code/check.d.ts +15 -0
  6. package/dist/code/check.js +14 -0
  7. package/dist/code/code.css +201 -0
  8. package/dist/code/code.css.d.ts +1 -0
  9. package/dist/code/engine-entry.d.ts +10 -0
  10. package/dist/code/engine-entry.js +10 -0
  11. package/dist/code/engine.d.ts +18 -0
  12. package/dist/code/engine.js +18 -0
  13. package/dist/code/family-engine.d.ts +23 -0
  14. package/dist/code/family-engine.js +4 -0
  15. package/dist/code/family.d.ts +7 -0
  16. package/dist/code/family.js +5 -0
  17. package/dist/code/grammars.d.ts +12 -0
  18. package/dist/code/grammars.js +83 -0
  19. package/dist/code/highlight.d.ts +15 -0
  20. package/dist/code/highlight.js +53 -0
  21. package/dist/code/index.d.ts +13 -0
  22. package/dist/code/index.js +12 -0
  23. package/dist/code/labels.d.ts +27 -0
  24. package/dist/code/labels.js +24 -0
  25. package/dist/code/languages/bash.d.ts +3 -0
  26. package/dist/code/languages/bash.js +48 -0
  27. package/dist/code/languages/css.d.ts +10 -0
  28. package/dist/code/languages/css.js +67 -0
  29. package/dist/code/languages/html.d.ts +6 -0
  30. package/dist/code/languages/html.js +35 -0
  31. package/dist/code/languages/javascript.d.ts +3 -0
  32. package/dist/code/languages/javascript.js +113 -0
  33. package/dist/code/languages/json.d.ts +3 -0
  34. package/dist/code/languages/json.js +24 -0
  35. package/dist/code/languages/python.d.ts +3 -0
  36. package/dist/code/languages/python.js +35 -0
  37. package/dist/code/languages/shared.d.ts +22 -0
  38. package/dist/code/languages/shared.js +37 -0
  39. package/dist/code/languages/sql.d.ts +3 -0
  40. package/dist/code/languages/sql.js +32 -0
  41. package/dist/code/languages/yaml.d.ts +7 -0
  42. package/dist/code/languages/yaml.js +36 -0
  43. package/dist/code/renderers.d.ts +5 -0
  44. package/dist/code/renderers.js +5 -0
  45. package/dist/code/token-styles.d.ts +25 -0
  46. package/dist/code/token-styles.js +51 -0
  47. package/dist/code/tokenize-code.d.ts +17 -0
  48. package/dist/code/tokenize-code.js +34 -0
  49. package/dist/code/tokenizer.d.ts +76 -0
  50. package/dist/code/tokenizer.js +171 -0
  51. package/dist/core/catalog/entries.d.ts +10 -0
  52. package/dist/core/catalog/entries.js +589 -0
  53. package/dist/core/catalog/index.d.ts +4 -0
  54. package/dist/core/catalog/index.js +2 -0
  55. package/dist/core/catalog/types.d.ts +36 -0
  56. package/dist/core/catalog/types.js +1 -0
  57. package/dist/core/catalog/validate.d.ts +15 -0
  58. package/dist/core/catalog/validate.js +46 -0
  59. package/dist/core/formula/evaluate.d.ts +14 -0
  60. package/dist/core/formula/evaluate.js +181 -0
  61. package/dist/core/formula/index.d.ts +8 -0
  62. package/dist/core/formula/index.js +6 -0
  63. package/dist/core/formula/parse.d.ts +19 -0
  64. package/dist/core/formula/parse.js +452 -0
  65. package/dist/core/formula/types.d.ts +76 -0
  66. package/dist/core/formula/types.js +7 -0
  67. package/dist/core/index.d.ts +14 -0
  68. package/dist/core/index.js +13 -0
  69. package/dist/core/limits.d.ts +27 -0
  70. package/dist/core/limits.js +26 -0
  71. package/dist/core/safety.d.ts +30 -0
  72. package/dist/core/safety.js +110 -0
  73. package/dist/core/schema/blocks.d.ts +138 -0
  74. package/dist/core/schema/blocks.js +181 -0
  75. package/dist/core/schema/code-family.d.ts +4 -0
  76. package/dist/core/schema/code-family.js +75 -0
  77. package/dist/core/schema/line-spec.d.ts +7 -0
  78. package/dist/core/schema/line-spec.js +19 -0
  79. package/dist/core/schema/media-family.d.ts +4 -0
  80. package/dist/core/schema/media-family.js +260 -0
  81. package/dist/core/schema/parse-family.d.ts +14 -0
  82. package/dist/core/schema/parse-family.js +37 -0
  83. package/dist/core/schema/primitives.d.ts +73 -0
  84. package/dist/core/schema/primitives.js +140 -0
  85. package/dist/core/schema/text-family.d.ts +4 -0
  86. package/dist/core/schema/text-family.js +121 -0
  87. package/dist/core/schema/vocabulary.d.ts +20 -0
  88. package/dist/core/schema/vocabulary.js +33 -0
  89. package/dist/core/text.d.ts +31 -0
  90. package/dist/core/text.js +139 -0
  91. package/dist/core/untrusted.d.ts +20 -0
  92. package/dist/core/untrusted.js +377 -0
  93. package/dist/css-modules.d.ts +1 -0
  94. package/dist/css.d.ts +1 -0
  95. package/dist/data/ChartBlockView.svelte +77 -0
  96. package/dist/data/ChartBlockView.svelte.d.ts +10 -0
  97. package/dist/data/DataBlockView.svelte +109 -0
  98. package/dist/data/DataBlockView.svelte.d.ts +5 -0
  99. package/dist/data/check.d.ts +10 -0
  100. package/dist/data/check.js +9 -0
  101. package/dist/data/engine.d.ts +18 -0
  102. package/dist/data/engine.js +10 -0
  103. package/dist/data/family.d.ts +6 -0
  104. package/dist/data/family.js +4 -0
  105. package/dist/data/index.d.ts +9 -0
  106. package/dist/data/index.js +8 -0
  107. package/dist/data/labels.d.ts +8 -0
  108. package/dist/data/labels.js +8 -0
  109. package/dist/data/renderers.d.ts +2 -0
  110. package/dist/data/renderers.js +2 -0
  111. package/dist/data/schema.d.ts +84 -0
  112. package/dist/data/schema.js +385 -0
  113. package/dist/diagram/BoardDiagram.svelte +48 -0
  114. package/dist/diagram/BoardDiagram.svelte.d.ts +8 -0
  115. package/dist/diagram/DiagramFamily.svelte +46 -0
  116. package/dist/diagram/DiagramFamily.svelte.d.ts +10 -0
  117. package/dist/diagram/FlowDiagram.svelte +108 -0
  118. package/dist/diagram/FlowDiagram.svelte.d.ts +8 -0
  119. package/dist/diagram/TerminalDiagram.svelte +38 -0
  120. package/dist/diagram/TerminalDiagram.svelte.d.ts +8 -0
  121. package/dist/diagram/TreeDiagram.svelte +149 -0
  122. package/dist/diagram/TreeDiagram.svelte.d.ts +8 -0
  123. package/dist/diagram/diagram.css +287 -0
  124. package/dist/diagram/diagram.css.d.ts +1 -0
  125. package/dist/diagram/engine.d.ts +16 -0
  126. package/dist/diagram/engine.js +14 -0
  127. package/dist/diagram/family.d.ts +7 -0
  128. package/dist/diagram/family.js +5 -0
  129. package/dist/diagram/index.d.ts +10 -0
  130. package/dist/diagram/index.js +9 -0
  131. package/dist/diagram/layout.d.ts +26 -0
  132. package/dist/diagram/layout.js +165 -0
  133. package/dist/diagram/schema.d.ts +74 -0
  134. package/dist/diagram/schema.js +358 -0
  135. package/dist/diagram/tones.d.ts +6 -0
  136. package/dist/diagram/tones.js +16 -0
  137. package/dist/embed/EmbedApp.svelte +138 -0
  138. package/dist/embed/EmbedApp.svelte.d.ts +17 -0
  139. package/dist/embed/EmbedBlockView.svelte +32 -0
  140. package/dist/embed/EmbedBlockView.svelte.d.ts +5 -0
  141. package/dist/embed/LinkPreview.svelte +48 -0
  142. package/dist/embed/LinkPreview.svelte.d.ts +10 -0
  143. package/dist/embed/engine.d.ts +30 -0
  144. package/dist/embed/engine.js +17 -0
  145. package/dist/embed/family.d.ts +7 -0
  146. package/dist/embed/family.js +5 -0
  147. package/dist/embed/index.d.ts +5 -0
  148. package/dist/embed/index.js +3 -0
  149. package/dist/embed/renderers.d.ts +3 -0
  150. package/dist/embed/renderers.js +3 -0
  151. package/dist/embed/sandbox.d.ts +86 -0
  152. package/dist/embed/sandbox.js +114 -0
  153. package/dist/embed/schema.d.ts +30 -0
  154. package/dist/embed/schema.js +217 -0
  155. package/dist/index.d.ts +16 -0
  156. package/dist/index.js +15 -0
  157. package/dist/interactive/CalculatorBlock.svelte +61 -0
  158. package/dist/interactive/CalculatorBlock.svelte.d.ts +10 -0
  159. package/dist/interactive/ChecklistBlock.svelte +44 -0
  160. package/dist/interactive/ChecklistBlock.svelte.d.ts +10 -0
  161. package/dist/interactive/InteractiveBlockView.svelte +31 -0
  162. package/dist/interactive/InteractiveBlockView.svelte.d.ts +5 -0
  163. package/dist/interactive/PollBlock.svelte +111 -0
  164. package/dist/interactive/PollBlock.svelte.d.ts +10 -0
  165. package/dist/interactive/QuizBlock.svelte +75 -0
  166. package/dist/interactive/QuizBlock.svelte.d.ts +10 -0
  167. package/dist/interactive/check.d.ts +10 -0
  168. package/dist/interactive/check.js +9 -0
  169. package/dist/interactive/engine.d.ts +19 -0
  170. package/dist/interactive/engine.js +12 -0
  171. package/dist/interactive/family.d.ts +6 -0
  172. package/dist/interactive/family.js +4 -0
  173. package/dist/interactive/index.d.ts +13 -0
  174. package/dist/interactive/index.js +13 -0
  175. package/dist/interactive/interactive.css +97 -0
  176. package/dist/interactive/interactive.css.d.ts +1 -0
  177. package/dist/interactive/labels.d.ts +16 -0
  178. package/dist/interactive/labels.js +16 -0
  179. package/dist/interactive/renderers.d.ts +9 -0
  180. package/dist/interactive/renderers.js +9 -0
  181. package/dist/interactive/schema.d.ts +76 -0
  182. package/dist/interactive/schema.js +324 -0
  183. package/dist/map/MapBlockView.svelte +465 -0
  184. package/dist/map/MapBlockView.svelte.d.ts +5 -0
  185. package/dist/map/check.d.ts +11 -0
  186. package/dist/map/check.js +6 -0
  187. package/dist/map/engine.d.ts +19 -0
  188. package/dist/map/engine.js +8 -0
  189. package/dist/map/family.d.ts +7 -0
  190. package/dist/map/family.js +5 -0
  191. package/dist/map/index.d.ts +11 -0
  192. package/dist/map/index.js +10 -0
  193. package/dist/map/labels.d.ts +27 -0
  194. package/dist/map/labels.js +21 -0
  195. package/dist/map/renderers.d.ts +2 -0
  196. package/dist/map/renderers.js +2 -0
  197. package/dist/map/schema.d.ts +41 -0
  198. package/dist/map/schema.js +176 -0
  199. package/dist/map/view-model.d.ts +55 -0
  200. package/dist/map/view-model.js +168 -0
  201. package/dist/media/MediaBlockView.svelte +258 -0
  202. package/dist/media/MediaBlockView.svelte.d.ts +5 -0
  203. package/dist/media/check.d.ts +14 -0
  204. package/dist/media/check.js +13 -0
  205. package/dist/media/engine.d.ts +18 -0
  206. package/dist/media/engine.js +10 -0
  207. package/dist/media/family.d.ts +7 -0
  208. package/dist/media/family.js +5 -0
  209. package/dist/media/index.d.ts +11 -0
  210. package/dist/media/index.js +10 -0
  211. package/dist/media/labels.d.ts +7 -0
  212. package/dist/media/labels.js +12 -0
  213. package/dist/media/renderers.d.ts +5 -0
  214. package/dist/media/renderers.js +5 -0
  215. package/dist/media/resolve.d.ts +6 -0
  216. package/dist/media/resolve.js +15 -0
  217. package/dist/structure/NestedBlocks.svelte +65 -0
  218. package/dist/structure/NestedBlocks.svelte.d.ts +11 -0
  219. package/dist/structure/SafeBody.svelte +13 -0
  220. package/dist/structure/SafeBody.svelte.d.ts +7 -0
  221. package/dist/structure/StructureFamily.svelte +157 -0
  222. package/dist/structure/StructureFamily.svelte.d.ts +5 -0
  223. package/dist/structure/check.d.ts +14 -0
  224. package/dist/structure/check.js +27 -0
  225. package/dist/structure/engine.d.ts +18 -0
  226. package/dist/structure/engine.js +18 -0
  227. package/dist/structure/family.d.ts +7 -0
  228. package/dist/structure/family.js +5 -0
  229. package/dist/structure/index.d.ts +11 -0
  230. package/dist/structure/index.js +10 -0
  231. package/dist/structure/labels.d.ts +11 -0
  232. package/dist/structure/labels.js +11 -0
  233. package/dist/structure/renderers.d.ts +5 -0
  234. package/dist/structure/renderers.js +5 -0
  235. package/dist/structure/schema.d.ts +71 -0
  236. package/dist/structure/schema.js +327 -0
  237. package/dist/structure/structure.css +182 -0
  238. package/dist/structure/structure.css.d.ts +1 -0
  239. package/dist/text/RichText.svelte +169 -0
  240. package/dist/text/RichText.svelte.d.ts +13 -0
  241. package/dist/text/TextBlockView.svelte +177 -0
  242. package/dist/text/TextBlockView.svelte.d.ts +5 -0
  243. package/dist/text/check.d.ts +14 -0
  244. package/dist/text/check.js +13 -0
  245. package/dist/text/engine.d.ts +17 -0
  246. package/dist/text/engine.js +10 -0
  247. package/dist/text/family.d.ts +7 -0
  248. package/dist/text/family.js +5 -0
  249. package/dist/text/index.d.ts +12 -0
  250. package/dist/text/index.js +11 -0
  251. package/dist/text/labels.d.ts +22 -0
  252. package/dist/text/labels.js +29 -0
  253. package/dist/text/markdown.d.ts +80 -0
  254. package/dist/text/markdown.js +361 -0
  255. package/dist/text/renderers.d.ts +6 -0
  256. package/dist/text/renderers.js +6 -0
  257. package/package.json +167 -4
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Data-family block shapes and parsers (table, stat, chart, kpi).
3
+ * Wired into the core `safeParseBlock` switch via `parseDataBlock`.
4
+ */
5
+ import { BLOCK_CONTENT_LIMITS, fail, ok, parseMultilineText, parsePlainText, } from "../core/schema/primitives.js";
6
+ import { isBlockTone } from "../core/schema/vocabulary.js";
7
+ export const DATA_BLOCK_TYPES = [
8
+ 'table',
9
+ 'stat',
10
+ 'kpi',
11
+ 'chart',
12
+ ];
13
+ export const CHART_KINDS = [
14
+ 'line',
15
+ 'area',
16
+ 'bar',
17
+ 'stacked-bar',
18
+ 'heatmap',
19
+ 'scatter',
20
+ 'bubble',
21
+ ];
22
+ function plainObject(value, path) {
23
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
24
+ return fail(path, 'Expected a plain object');
25
+ }
26
+ const prototype = Object.getPrototypeOf(value);
27
+ if (prototype !== Object.prototype && prototype !== null) {
28
+ return fail(path, 'Objects must be plain JSON data');
29
+ }
30
+ return ok(value);
31
+ }
32
+ function parseTone(value, path) {
33
+ if (value === undefined)
34
+ return ok(undefined);
35
+ if (!isBlockTone(value))
36
+ return fail(path, 'tone must be a known block tone');
37
+ return ok(value);
38
+ }
39
+ function parseColumnId(value, path) {
40
+ if (typeof value !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/.test(value)) {
41
+ return fail(path, 'column id must be a short lowercase token');
42
+ }
43
+ return ok(value);
44
+ }
45
+ export function parseTableBlock(record, path, base) {
46
+ if (!Array.isArray(record.columns) || record.columns.length === 0) {
47
+ return fail([...path, 'columns'], 'table needs at least one column');
48
+ }
49
+ if (record.columns.length > BLOCK_CONTENT_LIMITS.tableColumns) {
50
+ return fail([...path, 'columns'], `table may have at most ${BLOCK_CONTENT_LIMITS.tableColumns} columns`);
51
+ }
52
+ if (!Array.isArray(record.rows)) {
53
+ return fail([...path, 'rows'], 'table rows must be an array');
54
+ }
55
+ if (record.rows.length > BLOCK_CONTENT_LIMITS.tableRows) {
56
+ return fail([...path, 'rows'], `table may have at most ${BLOCK_CONTENT_LIMITS.tableRows} rows`);
57
+ }
58
+ const columns = [];
59
+ const ids = new Set();
60
+ for (let i = 0; i < record.columns.length; i += 1) {
61
+ const colPath = [...path, 'columns', i];
62
+ const col = plainObject(record.columns[i], colPath);
63
+ if (!col.success)
64
+ return col;
65
+ const id = parseColumnId(col.data.id, [...colPath, 'id']);
66
+ if (!id.success)
67
+ return id;
68
+ if (ids.has(id.data))
69
+ return fail([...colPath, 'id'], `duplicate column id "${id.data}"`);
70
+ ids.add(id.data);
71
+ const label = parsePlainText(col.data.label, [...colPath, 'label'], BLOCK_CONTENT_LIMITS.plain);
72
+ if (!label.success)
73
+ return label;
74
+ columns.push({ id: id.data, label: label.data });
75
+ }
76
+ const rows = [];
77
+ for (let r = 0; r < record.rows.length; r += 1) {
78
+ const rowPath = [...path, 'rows', r];
79
+ const rowObj = plainObject(record.rows[r], rowPath);
80
+ if (!rowObj.success)
81
+ return rowObj;
82
+ const row = {};
83
+ for (const column of columns) {
84
+ const raw = rowObj.data[column.id];
85
+ if (raw === undefined || raw === null) {
86
+ row[column.id] = null;
87
+ continue;
88
+ }
89
+ if (typeof raw === 'number') {
90
+ if (!Number.isFinite(raw)) {
91
+ return fail([...rowPath, column.id], 'numeric cells must be finite');
92
+ }
93
+ row[column.id] = raw;
94
+ continue;
95
+ }
96
+ if (typeof raw === 'string') {
97
+ const text = parsePlainText(raw, [...rowPath, column.id], BLOCK_CONTENT_LIMITS.plain);
98
+ if (!text.success)
99
+ return text;
100
+ row[column.id] = text.data;
101
+ continue;
102
+ }
103
+ return fail([...rowPath, column.id], 'cell must be string, number or null');
104
+ }
105
+ rows.push(row);
106
+ }
107
+ let caption;
108
+ if (record.caption !== undefined) {
109
+ const parsed = parsePlainText(record.caption, [...path, 'caption'], BLOCK_CONTENT_LIMITS.plain);
110
+ if (!parsed.success)
111
+ return parsed;
112
+ caption = parsed.data;
113
+ }
114
+ return ok({
115
+ ...base,
116
+ type: 'table',
117
+ columns,
118
+ rows,
119
+ ...(caption !== undefined ? { caption } : {}),
120
+ });
121
+ }
122
+ function parseStatItems(value, path, max) {
123
+ if (!Array.isArray(value) || value.length === 0) {
124
+ return fail(path, 'needs at least one item');
125
+ }
126
+ if (value.length > max) {
127
+ return fail(path, `at most ${max} items`);
128
+ }
129
+ const items = [];
130
+ for (let i = 0; i < value.length; i += 1) {
131
+ const itemPath = [...path, i];
132
+ const item = plainObject(value[i], itemPath);
133
+ if (!item.success)
134
+ return item;
135
+ const label = parsePlainText(item.data.label, [...itemPath, 'label'], BLOCK_CONTENT_LIMITS.plain);
136
+ if (!label.success)
137
+ return label;
138
+ const val = parsePlainText(item.data.value, [...itemPath, 'value'], BLOCK_CONTENT_LIMITS.plain);
139
+ if (!val.success)
140
+ return val;
141
+ let hint;
142
+ if (item.data.hint !== undefined) {
143
+ const parsed = parsePlainText(item.data.hint, [...itemPath, 'hint'], BLOCK_CONTENT_LIMITS.plain);
144
+ if (!parsed.success)
145
+ return parsed;
146
+ hint = parsed.data;
147
+ }
148
+ let delta;
149
+ if (item.data.delta !== undefined) {
150
+ const parsed = parsePlainText(item.data.delta, [...itemPath, 'delta'], BLOCK_CONTENT_LIMITS.plain);
151
+ if (!parsed.success)
152
+ return parsed;
153
+ delta = parsed.data;
154
+ }
155
+ const tone = parseTone(item.data.tone, [...itemPath, 'tone']);
156
+ if (!tone.success)
157
+ return tone;
158
+ items.push({
159
+ label: label.data,
160
+ value: val.data,
161
+ ...(hint !== undefined ? { hint } : {}),
162
+ ...(delta !== undefined ? { delta } : {}),
163
+ ...(tone.data !== undefined ? { tone: tone.data } : {}),
164
+ });
165
+ }
166
+ return ok(items);
167
+ }
168
+ export function parseStatBlock(record, path, base) {
169
+ const items = parseStatItems(record.items, [...path, 'items'], BLOCK_CONTENT_LIMITS.statItems);
170
+ if (!items.success)
171
+ return items;
172
+ return ok({ ...base, type: 'stat', items: items.data });
173
+ }
174
+ export function parseKpiBlock(record, path, base) {
175
+ const items = parseStatItems(record.items, [...path, 'items'], BLOCK_CONTENT_LIMITS.statItems);
176
+ if (!items.success)
177
+ return items;
178
+ return ok({ ...base, type: 'kpi', items: items.data });
179
+ }
180
+ function parseChartSeries(value, path) {
181
+ if (!Array.isArray(value) || value.length === 0) {
182
+ return fail(path, 'chart series needs at least one series');
183
+ }
184
+ if (value.length > 8)
185
+ return fail(path, 'chart may have at most 8 series');
186
+ const series = [];
187
+ const ids = new Set();
188
+ for (let i = 0; i < value.length; i += 1) {
189
+ const sPath = [...path, i];
190
+ const raw = plainObject(value[i], sPath);
191
+ if (!raw.success)
192
+ return raw;
193
+ const id = parseColumnId(raw.data.id, [...sPath, 'id']);
194
+ if (!id.success)
195
+ return id;
196
+ if (ids.has(id.data))
197
+ return fail([...sPath, 'id'], `duplicate series id "${id.data}"`);
198
+ ids.add(id.data);
199
+ const label = parsePlainText(raw.data.label, [...sPath, 'label'], BLOCK_CONTENT_LIMITS.plain);
200
+ if (!label.success)
201
+ return label;
202
+ if (!Array.isArray(raw.data.points) || raw.data.points.length === 0) {
203
+ return fail([...sPath, 'points'], 'series needs points');
204
+ }
205
+ if (raw.data.points.length > 64) {
206
+ return fail([...sPath, 'points'], 'series may have at most 64 points');
207
+ }
208
+ const points = [];
209
+ for (let p = 0; p < raw.data.points.length; p += 1) {
210
+ const pPath = [...sPath, 'points', p];
211
+ const pt = plainObject(raw.data.points[p], pPath);
212
+ if (!pt.success)
213
+ return pt;
214
+ const x = pt.data.x;
215
+ if (typeof x !== 'string' && typeof x !== 'number') {
216
+ return fail([...pPath, 'x'], 'x must be string or number');
217
+ }
218
+ if (typeof x === 'string') {
219
+ const text = parsePlainText(x, [...pPath, 'x'], BLOCK_CONTENT_LIMITS.plain);
220
+ if (!text.success)
221
+ return text;
222
+ }
223
+ else if (!Number.isFinite(x)) {
224
+ return fail([...pPath, 'x'], 'numeric x must be finite');
225
+ }
226
+ const y = pt.data.y;
227
+ if (y !== null && (typeof y !== 'number' || !Number.isFinite(y))) {
228
+ return fail([...pPath, 'y'], 'y must be a finite number or null');
229
+ }
230
+ points.push({ x: x, y: y });
231
+ }
232
+ let color;
233
+ if (raw.data.color !== undefined) {
234
+ if (typeof raw.data.color !== 'string' || !/^var\(--[a-z0-9-]+\)$/.test(raw.data.color)) {
235
+ return fail([...sPath, 'color'], 'color must be a CSS var(--token) reference');
236
+ }
237
+ color = raw.data.color;
238
+ }
239
+ series.push({
240
+ id: id.data,
241
+ label: label.data,
242
+ points,
243
+ ...(color !== undefined ? { color } : {}),
244
+ });
245
+ }
246
+ return ok(series);
247
+ }
248
+ export function parseChartBlock(record, path, base) {
249
+ if (typeof record.kind !== 'string' ||
250
+ !CHART_KINDS.includes(record.kind)) {
251
+ return fail([...path, 'kind'], `kind must be one of ${CHART_KINDS.join(', ')}`);
252
+ }
253
+ const kind = record.kind;
254
+ const title = parsePlainText(record.title, [...path, 'title'], BLOCK_CONTENT_LIMITS.heading);
255
+ if (!title.success)
256
+ return title;
257
+ const description = parseMultilineText(record.description, [...path, 'description'], BLOCK_CONTENT_LIMITS.shortMarkdown);
258
+ if (!description.success)
259
+ return description;
260
+ if (kind === 'heatmap') {
261
+ if (!Array.isArray(record.cells) || record.cells.length === 0) {
262
+ return fail([...path, 'cells'], 'heatmap needs cells');
263
+ }
264
+ if (record.cells.length > BLOCK_CONTENT_LIMITS.tableColumns * BLOCK_CONTENT_LIMITS.tableRows) {
265
+ return fail([...path, 'cells'], 'too many heatmap cells');
266
+ }
267
+ const cells = [];
268
+ for (let i = 0; i < record.cells.length; i += 1) {
269
+ const cPath = [...path, 'cells', i];
270
+ const cell = plainObject(record.cells[i], cPath);
271
+ if (!cell.success)
272
+ return cell;
273
+ const x = parsePlainText(cell.data.x, [...cPath, 'x'], BLOCK_CONTENT_LIMITS.plain);
274
+ if (!x.success)
275
+ return x;
276
+ const y = parsePlainText(cell.data.y, [...cPath, 'y'], BLOCK_CONTENT_LIMITS.plain);
277
+ if (!y.success)
278
+ return y;
279
+ if (typeof cell.data.value !== 'number' || !Number.isFinite(cell.data.value)) {
280
+ return fail([...cPath, 'value'], 'value must be a finite number');
281
+ }
282
+ cells.push({ x: x.data, y: y.data, value: cell.data.value });
283
+ }
284
+ return ok({
285
+ ...base,
286
+ type: 'chart',
287
+ kind,
288
+ title: title.data,
289
+ description: description.data,
290
+ cells,
291
+ });
292
+ }
293
+ if (kind === 'scatter' || kind === 'bubble') {
294
+ if (!Array.isArray(record.points) || record.points.length === 0) {
295
+ return fail([...path, 'points'], 'scatter/bubble needs points');
296
+ }
297
+ if (record.points.length > 64)
298
+ return fail([...path, 'points'], 'at most 64 points');
299
+ const points = [];
300
+ const ids = new Set();
301
+ for (let i = 0; i < record.points.length; i += 1) {
302
+ const pPath = [...path, 'points', i];
303
+ const pt = plainObject(record.points[i], pPath);
304
+ if (!pt.success)
305
+ return pt;
306
+ const id = parseColumnId(pt.data.id, [...pPath, 'id']);
307
+ if (!id.success)
308
+ return id;
309
+ if (ids.has(id.data))
310
+ return fail([...pPath, 'id'], 'duplicate point id');
311
+ ids.add(id.data);
312
+ const label = parsePlainText(pt.data.label, [...pPath, 'label'], BLOCK_CONTENT_LIMITS.plain);
313
+ if (!label.success)
314
+ return label;
315
+ if (typeof pt.data.x !== 'number' || !Number.isFinite(pt.data.x)) {
316
+ return fail([...pPath, 'x'], 'x must be finite');
317
+ }
318
+ if (typeof pt.data.y !== 'number' || !Number.isFinite(pt.data.y)) {
319
+ return fail([...pPath, 'y'], 'y must be finite');
320
+ }
321
+ let r;
322
+ if (kind === 'bubble') {
323
+ if (typeof pt.data.r !== 'number' || !Number.isFinite(pt.data.r) || pt.data.r < 0) {
324
+ return fail([...pPath, 'r'], 'bubble r must be a non-negative finite number');
325
+ }
326
+ r = pt.data.r;
327
+ }
328
+ else if (pt.data.r !== undefined) {
329
+ if (typeof pt.data.r !== 'number' || !Number.isFinite(pt.data.r) || pt.data.r < 0) {
330
+ return fail([...pPath, 'r'], 'r must be a non-negative finite number when set');
331
+ }
332
+ r = pt.data.r;
333
+ }
334
+ let seriesId;
335
+ if (pt.data.seriesId !== undefined) {
336
+ const sid = parseColumnId(pt.data.seriesId, [...pPath, 'seriesId']);
337
+ if (!sid.success)
338
+ return sid;
339
+ seriesId = sid.data;
340
+ }
341
+ points.push({
342
+ id: id.data,
343
+ label: label.data,
344
+ x: pt.data.x,
345
+ y: pt.data.y,
346
+ ...(r !== undefined ? { r } : {}),
347
+ ...(seriesId !== undefined ? { seriesId } : {}),
348
+ });
349
+ }
350
+ return ok({
351
+ ...base,
352
+ type: 'chart',
353
+ kind,
354
+ title: title.data,
355
+ description: description.data,
356
+ points,
357
+ });
358
+ }
359
+ const series = parseChartSeries(record.series, [...path, 'series']);
360
+ if (!series.success)
361
+ return series;
362
+ return ok({
363
+ ...base,
364
+ type: 'chart',
365
+ kind,
366
+ title: title.data,
367
+ description: description.data,
368
+ series: series.data,
369
+ });
370
+ }
371
+ /** Dispatch for the four data block types; returns null when type is not data. */
372
+ export function parseDataBlock(type, record, path, base) {
373
+ switch (type) {
374
+ case 'table':
375
+ return parseTableBlock(record, path, base);
376
+ case 'stat':
377
+ return parseStatBlock(record, path, base);
378
+ case 'kpi':
379
+ return parseKpiBlock(record, path, base);
380
+ case 'chart':
381
+ return parseChartBlock(record, path, base);
382
+ default:
383
+ return null;
384
+ }
385
+ }
@@ -0,0 +1,48 @@
1
+ <script lang="ts">
2
+ import type { BoardDiagram } from './schema.js';
3
+ import { toneBorder } from './tones.js';
4
+ import './diagram.css';
5
+
6
+ /** Board: columns of cards as labelled lists (structure is the text alternative). */
7
+ let { block }: { block: BoardDiagram } = $props();
8
+
9
+ const cards = $derived(block.columns.reduce((sum, column) => sum + column.cards.length, 0));
10
+ const summary = $derived(
11
+ `Board with ${block.columns.length} ${block.columns.length === 1 ? 'column' : 'columns'} and ${cards} ${
12
+ cards === 1 ? 'card' : 'cards'
13
+ }`
14
+ );
15
+ </script>
16
+
17
+ <figure class="tx-diagram" data-tx-diagram="board">
18
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -- wide board scrolls; keyboard users must reach it. -->
19
+ <div class="tx-diagram__scroll" role="region" aria-label={summary} tabindex="0">
20
+ <div class="tx-diagram-board">
21
+ {#each block.columns as column (column.id)}
22
+ <section
23
+ class="tx-diagram-column"
24
+ aria-label="{column.title}, {column.cards.length} {column.cards.length === 1
25
+ ? 'card'
26
+ : 'cards'}"
27
+ data-column={column.id}
28
+ >
29
+ <p class="tx-diagram-column__title" aria-hidden="true">
30
+ <span>{column.title}</span>
31
+ <span class="tx-diagram-column__count">{column.cards.length}</span>
32
+ </p>
33
+ {#if column.cards.length > 0}
34
+ <ul class="tx-diagram-cards">
35
+ {#each column.cards as card (card.id)}
36
+ <li class={`tx-diagram-card ${toneBorder(card.tone)}`} data-card={card.id}>
37
+ {card.title}
38
+ </li>
39
+ {/each}
40
+ </ul>
41
+ {:else}
42
+ <p class="tx-diagram-empty">No cards</p>
43
+ {/if}
44
+ </section>
45
+ {/each}
46
+ </div>
47
+ </div>
48
+ </figure>
@@ -0,0 +1,8 @@
1
+ /// <reference path="../css-modules.d.ts" />
2
+ import type { BoardDiagram } from './schema.js';
3
+ type $$ComponentProps = {
4
+ block: BoardDiagram;
5
+ };
6
+ declare const BoardDiagram: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type BoardDiagram = ReturnType<typeof BoardDiagram>;
8
+ export default BoardDiagram;
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { FieldIssue } from '../core/schema/primitives.js';
3
+ import BoardDiagram from './BoardDiagram.svelte';
4
+ import FlowDiagram from './FlowDiagram.svelte';
5
+ import { parseDiagram } from './schema.js';
6
+ import TerminalDiagram from './TerminalDiagram.svelte';
7
+ import TreeDiagram from './TreeDiagram.svelte';
8
+ import './diagram.css';
9
+
10
+ interface Props {
11
+ /** Untrusted block data; validated before anything renders. */
12
+ block: unknown;
13
+ oninvalid?: (issues: FieldIssue[]) => void;
14
+ }
15
+
16
+ let { block, oninvalid }: Props = $props();
17
+
18
+ const parsed = $derived(parseDiagram(block));
19
+
20
+ $effect(() => {
21
+ if (!parsed.success) {
22
+ oninvalid?.(
23
+ parsed.issues.map((issue) => ({
24
+ path: [],
25
+ message: `${issue.jsonPath}: ${issue.message}`,
26
+ }))
27
+ );
28
+ }
29
+ });
30
+ </script>
31
+
32
+ {#if !parsed.success}
33
+ <p class="tx-diagram-fallback" role="note" data-tx-diagram-invalid>
34
+ This diagram could not be shown. The data failed validation.
35
+ </p>
36
+ {:else if parsed.data.type === 'flow'}
37
+ <FlowDiagram block={parsed.data} />
38
+ {:else if parsed.data.type === 'tree'}
39
+ <TreeDiagram block={parsed.data} />
40
+ {:else if parsed.data.type === 'board'}
41
+ <BoardDiagram block={parsed.data} />
42
+ {:else if parsed.data.type === 'terminal'}
43
+ <TerminalDiagram block={parsed.data} />
44
+ {:else}
45
+ <p class="tx-diagram-fallback" role="note">This block renderer is not available yet.</p>
46
+ {/if}
@@ -0,0 +1,10 @@
1
+ /// <reference path="../css-modules.d.ts" />
2
+ import type { FieldIssue } from '../core/schema/primitives.js';
3
+ interface Props {
4
+ /** Untrusted block data; validated before anything renders. */
5
+ block: unknown;
6
+ oninvalid?: (issues: FieldIssue[]) => void;
7
+ }
8
+ declare const DiagramFamily: import("svelte").Component<Props, {}, "">;
9
+ type DiagramFamily = ReturnType<typeof DiagramFamily>;
10
+ export default DiagramFamily;
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import { layoutFlow } from './layout.js';
3
+ import type { FlowDiagram } from './schema.js';
4
+ import { DIAGRAM_PAD, toneBorder } from './tones.js';
5
+ import './diagram.css';
6
+
7
+ /**
8
+ * Flow steps left to right from layoutFlow, joined by arrows, optional loop-back.
9
+ * Ordered list is the text alternative; drawing is aria-hidden.
10
+ */
11
+ let { block }: { block: FlowDiagram } = $props();
12
+
13
+ const uid = $props.id();
14
+ const arrow = `tx-flow-arrow-${uid}`;
15
+ const layout = $derived(layoutFlow(block));
16
+ const byId = $derived(new Map(layout.positions.map((p) => [p.id, p])));
17
+ const stageW = $derived(layout.width + DIAGRAM_PAD * 2);
18
+ const stageH = $derived(layout.height + DIAGRAM_PAD * 2);
19
+ const summary = $derived(
20
+ `Flow with ${block.nodes.length} ${block.nodes.length === 1 ? 'step' : 'steps'}${
21
+ block.loopBack && block.nodes.length > 1 ? ', repeating from the last step to the first' : ''
22
+ }`
23
+ );
24
+
25
+ function edgePath(from: string, to: string, kind: string): string {
26
+ const a = byId.get(from);
27
+ const b = byId.get(to);
28
+ if (!a || !b) return '';
29
+ if (kind === 'loop-back') {
30
+ const y = a.y + a.height + 24;
31
+ return `M ${a.x + a.width / 2} ${a.y + a.height} V ${y} H ${b.x + b.width / 2} V ${b.y + b.height + 6}`;
32
+ }
33
+ const y = a.y + a.height / 2;
34
+ return `M ${a.x + a.width} ${y} H ${b.x - 6}`;
35
+ }
36
+
37
+ function nodeStyle(node: { x: number; y: number; width: number; height: number }): string {
38
+ return [
39
+ `--tx-dx:${node.x + DIAGRAM_PAD}px`,
40
+ `--tx-dy:${node.y + DIAGRAM_PAD}px`,
41
+ `--tx-dw:${node.width}px`,
42
+ `--tx-dh:${node.height}px`,
43
+ ].join(';');
44
+ }
45
+ </script>
46
+
47
+ <figure class="tx-diagram" data-tx-diagram="flow">
48
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -- wide flow scrolls; keyboard users must reach it (WCAG 2.1.1). -->
49
+ <div class="tx-diagram__scroll" role="region" aria-label={summary} tabindex="0">
50
+ <ol class="tx-diagram-sr">
51
+ {#each block.nodes as node, index (node.id)}
52
+ <li>Step {index + 1} of {block.nodes.length}: {node.label}</li>
53
+ {/each}
54
+ {#if block.loopBack && block.nodes.length > 1}
55
+ <li>Then back to step 1: {block.nodes[0]?.label}</li>
56
+ {/if}
57
+ </ol>
58
+ <div
59
+ class="tx-diagram__stage"
60
+ aria-hidden="true"
61
+ style={`--tx-diagram-w:${stageW}px;--tx-diagram-h:${stageH}px`}
62
+ >
63
+ <svg
64
+ class="tx-diagram__svg"
65
+ width={stageW}
66
+ height={stageH}
67
+ viewBox={`${-DIAGRAM_PAD} ${-DIAGRAM_PAD} ${stageW} ${stageH}`}
68
+ focusable="false"
69
+ >
70
+ <defs>
71
+ <marker
72
+ id={arrow}
73
+ viewBox="0 0 10 10"
74
+ refX="8"
75
+ refY="5"
76
+ markerWidth="7"
77
+ markerHeight="7"
78
+ orient="auto-start-reverse"
79
+ >
80
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
81
+ </marker>
82
+ </defs>
83
+ {#each layout.edges as edge (`${edge.from}-${edge.to}-${edge.kind}`)}
84
+ <path
85
+ d={edgePath(edge.from, edge.to, edge.kind)}
86
+ fill="none"
87
+ stroke="currentColor"
88
+ stroke-width="1.5"
89
+ stroke-dasharray={edge.kind === 'loop-back' ? '4 4' : undefined}
90
+ marker-end={`url(#${arrow})`}
91
+ data-edge={edge.kind}
92
+ />
93
+ {/each}
94
+ </svg>
95
+ {#each layout.positions as node, index (node.id)}
96
+ <div
97
+ class={`tx-diagram-node ${toneBorder(node.tone)}`}
98
+ style={nodeStyle(node)}
99
+ title={node.label}
100
+ data-node={node.id}
101
+ >
102
+ <span class="tx-diagram-node__index">{index + 1}</span>
103
+ <span class="tx-diagram-node__label">{node.label}</span>
104
+ </div>
105
+ {/each}
106
+ </div>
107
+ </div>
108
+ </figure>
@@ -0,0 +1,8 @@
1
+ /// <reference path="../css-modules.d.ts" />
2
+ import type { FlowDiagram } from './schema.js';
3
+ type $$ComponentProps = {
4
+ block: FlowDiagram;
5
+ };
6
+ declare const FlowDiagram: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type FlowDiagram = ReturnType<typeof FlowDiagram>;
8
+ export default FlowDiagram;
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import type { TerminalDiagram } from './schema.js';
3
+ import './diagram.css';
4
+
5
+ /**
6
+ * Terminal session as text: prompts, output and comments in order.
7
+ * `$` is decoration; assistive tech hears "Command:".
8
+ */
9
+ let { block }: { block: TerminalDiagram } = $props();
10
+
11
+ const commands = $derived(block.lines.filter((line) => line.kind === 'prompt').length);
12
+ </script>
13
+
14
+ <figure class="tx-diagram" data-tx-diagram="terminal">
15
+ <div class="tx-diagram-terminal">
16
+ <div class="tx-diagram-terminal__chrome" aria-hidden="true">
17
+ <span class="tx-diagram-terminal__dot"></span>
18
+ <span class="tx-diagram-terminal__dot"></span>
19
+ <span class="tx-diagram-terminal__dot"></span>
20
+ </div>
21
+ <ol
22
+ class="tx-diagram-terminal__lines"
23
+ aria-label="Terminal session, {commands} {commands === 1 ? 'command' : 'commands'}"
24
+ >
25
+ {#each block.lines as line, index (index)}
26
+ <li class="tx-diagram-terminal__line" data-kind={line.kind}>
27
+ {#if line.kind === 'prompt'}
28
+ <span class="tx-diagram-terminal__prompt-mark" aria-hidden="true">{'$ '}</span>
29
+ <span class="tx-diagram-sr">Command: </span>
30
+ <span class="tx-diagram-terminal__prompt-text">{line.text}</span>
31
+ {:else}
32
+ {line.text}
33
+ {/if}
34
+ </li>
35
+ {/each}
36
+ </ol>
37
+ </div>
38
+ </figure>
@@ -0,0 +1,8 @@
1
+ /// <reference path="../css-modules.d.ts" />
2
+ import type { TerminalDiagram } from './schema.js';
3
+ type $$ComponentProps = {
4
+ block: TerminalDiagram;
5
+ };
6
+ declare const TerminalDiagram: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type TerminalDiagram = ReturnType<typeof TerminalDiagram>;
8
+ export default TerminalDiagram;