@texuli/deck 0.0.0-stage → 0.5.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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/dist/authoring/builder.d.ts +351 -0
  4. package/dist/authoring/builder.js +64 -0
  5. package/dist/authoring/index.d.ts +6 -0
  6. package/dist/authoring/index.js +6 -0
  7. package/dist/authoring/lint.d.ts +36 -0
  8. package/dist/authoring/lint.js +325 -0
  9. package/dist/authoring/outlines.d.ts +34 -0
  10. package/dist/authoring/outlines.js +255 -0
  11. package/dist/authoring/patch.d.ts +23 -0
  12. package/dist/authoring/patch.js +166 -0
  13. package/dist/authoring/select-layouts.d.ts +19 -0
  14. package/dist/authoring/select-layouts.js +90 -0
  15. package/dist/engine/auto-slide.d.ts +32 -0
  16. package/dist/engine/auto-slide.js +67 -0
  17. package/dist/engine/canvas.d.ts +56 -0
  18. package/dist/engine/canvas.js +48 -0
  19. package/dist/engine/controller.d.ts +126 -0
  20. package/dist/engine/controller.js +248 -0
  21. package/dist/engine/controls.d.ts +128 -0
  22. package/dist/engine/controls.js +460 -0
  23. package/dist/engine/hash.d.ts +42 -0
  24. package/dist/engine/hash.js +86 -0
  25. package/dist/engine/index.d.ts +88 -0
  26. package/dist/engine/index.js +88 -0
  27. package/dist/engine/messages.d.ts +138 -0
  28. package/dist/engine/messages.js +140 -0
  29. package/dist/engine/motion.d.ts +64 -0
  30. package/dist/engine/motion.js +112 -0
  31. package/dist/engine/navigation.d.ts +87 -0
  32. package/dist/engine/navigation.js +156 -0
  33. package/dist/engine/presenter.d.ts +58 -0
  34. package/dist/engine/presenter.js +157 -0
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +9 -0
  37. package/dist/markdown/attributes.d.ts +24 -0
  38. package/dist/markdown/attributes.js +94 -0
  39. package/dist/markdown/attrs-closed.d.ts +52 -0
  40. package/dist/markdown/attrs-closed.js +520 -0
  41. package/dist/markdown/blocks.d.ts +112 -0
  42. package/dist/markdown/blocks.js +766 -0
  43. package/dist/markdown/frontmatter.d.ts +30 -0
  44. package/dist/markdown/frontmatter.js +165 -0
  45. package/dist/markdown/index.d.ts +29 -0
  46. package/dist/markdown/index.js +29 -0
  47. package/dist/markdown/issues.d.ts +68 -0
  48. package/dist/markdown/issues.js +83 -0
  49. package/dist/markdown/json.d.ts +21 -0
  50. package/dist/markdown/json.js +236 -0
  51. package/dist/markdown/limits.d.ts +32 -0
  52. package/dist/markdown/limits.js +32 -0
  53. package/dist/markdown/parse.d.ts +31 -0
  54. package/dist/markdown/parse.js +283 -0
  55. package/dist/markdown/scan.d.ts +26 -0
  56. package/dist/markdown/scan.js +138 -0
  57. package/dist/markdown/slides.d.ts +32 -0
  58. package/dist/markdown/slides.js +854 -0
  59. package/dist/markdown/text.d.ts +76 -0
  60. package/dist/markdown/text.js +447 -0
  61. package/dist/markdown/twin.d.ts +41 -0
  62. package/dist/markdown/twin.js +374 -0
  63. package/dist/markdown/write.d.ts +17 -0
  64. package/dist/markdown/write.js +688 -0
  65. package/dist/math/index.d.ts +14 -0
  66. package/dist/math/index.js +14 -0
  67. package/dist/math/parse.d.ts +9 -0
  68. package/dist/math/parse.js +648 -0
  69. package/dist/math/symbols.d.ts +44 -0
  70. package/dist/math/symbols.js +333 -0
  71. package/dist/math/types.d.ts +59 -0
  72. package/dist/math/types.js +69 -0
  73. package/dist/model/backgrounds/contrast.d.ts +77 -0
  74. package/dist/model/backgrounds/contrast.js +186 -0
  75. package/dist/model/backgrounds/index.d.ts +7 -0
  76. package/dist/model/backgrounds/index.js +7 -0
  77. package/dist/model/backgrounds/resolve.d.ts +86 -0
  78. package/dist/model/backgrounds/resolve.js +215 -0
  79. package/dist/model/backgrounds/schema.d.ts +178 -0
  80. package/dist/model/backgrounds/schema.js +203 -0
  81. package/dist/model/codes.d.ts +73 -0
  82. package/dist/model/codes.js +67 -0
  83. package/dist/model/deck.d.ts +392 -0
  84. package/dist/model/deck.js +338 -0
  85. package/dist/model/index.d.ts +19 -0
  86. package/dist/model/index.js +19 -0
  87. package/dist/model/layouts/catalog.d.ts +433 -0
  88. package/dist/model/layouts/catalog.js +495 -0
  89. package/dist/model/layouts/index.d.ts +5 -0
  90. package/dist/model/layouts/index.js +5 -0
  91. package/dist/model/layouts/props.d.ts +302 -0
  92. package/dist/model/layouts/props.js +272 -0
  93. package/dist/model/loader.d.ts +37 -0
  94. package/dist/model/loader.js +317 -0
  95. package/dist/model/migrate.d.ts +34 -0
  96. package/dist/model/migrate.js +453 -0
  97. package/dist/model/motion/index.d.ts +6 -0
  98. package/dist/model/motion/index.js +6 -0
  99. package/dist/model/motion/transition.d.ts +96 -0
  100. package/dist/model/motion/transition.js +127 -0
  101. package/dist/model/parse.d.ts +57 -0
  102. package/dist/model/parse.js +307 -0
  103. package/dist/model/plain.d.ts +40 -0
  104. package/dist/model/plain.js +275 -0
  105. package/dist/model/shared.d.ts +63 -0
  106. package/dist/model/shared.js +86 -0
  107. package/dist/model/steps/index.d.ts +7 -0
  108. package/dist/model/steps/index.js +7 -0
  109. package/dist/model/steps/phase.d.ts +34 -0
  110. package/dist/model/steps/phase.js +50 -0
  111. package/dist/model/steps/slide-steps.d.ts +129 -0
  112. package/dist/model/steps/slide-steps.js +256 -0
  113. package/dist/model/style/index.d.ts +8 -0
  114. package/dist/model/style/index.js +8 -0
  115. package/dist/model/style/resolve.d.ts +87 -0
  116. package/dist/model/style/resolve.js +174 -0
  117. package/dist/model/style/schema.d.ts +85 -0
  118. package/dist/model/style/schema.js +53 -0
  119. package/dist/model/style/theme.d.ts +69 -0
  120. package/dist/model/style/theme.js +84 -0
  121. package/dist/schema/builders.d.ts +146 -0
  122. package/dist/schema/builders.js +285 -0
  123. package/dist/schema/diagnostics.d.ts +78 -0
  124. package/dist/schema/diagnostics.js +81 -0
  125. package/dist/schema/index.d.ts +15 -0
  126. package/dist/schema/index.js +14 -0
  127. package/dist/schema/internal.d.ts +40 -0
  128. package/dist/schema/internal.js +209 -0
  129. package/dist/schema/json-schema.d.ts +12 -0
  130. package/dist/schema/json-schema.js +213 -0
  131. package/dist/schema/types.d.ts +166 -0
  132. package/dist/schema/types.js +1 -0
  133. package/dist/schema/validate.d.ts +51 -0
  134. package/dist/schema/validate.js +765 -0
  135. package/dist/templates/index.d.ts +2 -0
  136. package/dist/templates/index.js +2 -0
  137. package/dist/templates/templates.d.ts +11 -0
  138. package/dist/templates/templates.js +47 -0
  139. package/package.json +60 -4
@@ -0,0 +1,37 @@
1
+ import { type DeckSource } from './migrate.js';
2
+ import { type DeckParseResult, type ParseDeckOptions } from './parse.js';
3
+ export interface UntrustedDeckLimits {
4
+ /** UTF-8 bytes of the JSON (text, or the copy of parsed data). Default 1 MiB. */
5
+ maxBytes: number;
6
+ /** Object and array nesting. Default 32. */
7
+ maxDepth: number;
8
+ /** Objects and arrays under the slides, all slides together. Default 20,000. */
9
+ maxNodes: number;
10
+ /** Default 200. */
11
+ maxSlides: number;
12
+ }
13
+ export declare const DEFAULT_UNTRUSTED_DECK_LIMITS: Readonly<UntrustedDeckLimits>;
14
+ export interface UntrustedDeckOptions extends Partial<UntrustedDeckLimits>, ParseDeckOptions {
15
+ }
16
+ export interface UntrustedDeckResult extends DeckParseResult {
17
+ /** The detected input shape; absent when the input never got that far. */
18
+ from?: DeckSource;
19
+ }
20
+ /** Nesting depth of JSON text, without parsing it; stops early past `limit`. */
21
+ export declare function jsonTextDepth(text: string, limit: number): number;
22
+ /**
23
+ * The offset of the first JSON syntax error in `text` (engines word their
24
+ * `JSON.parse` errors differently, so the position comes from this scanner).
25
+ * Call it only on text with bounded depth.
26
+ */
27
+ export declare function jsonErrorOffset(text: string): number;
28
+ /** 1-based line and column (UTF-16 units) of `offset` in `text`. */
29
+ export declare function lineColumn(text: string, offset: number): {
30
+ line: number;
31
+ column: number;
32
+ };
33
+ /**
34
+ * Load a deck from JSON text or parsed data that may be hostile. On success
35
+ * `deck` is a fresh validated v2 copy; earlier shapes are migrated first.
36
+ */
37
+ export declare function loadUntrustedDeck(input: unknown, options?: UntrustedDeckOptions): UntrustedDeckResult;
@@ -0,0 +1,317 @@
1
+ /**
2
+ * `loadUntrustedDeck`: load a deck from somewhere it could have been
3
+ * tampered with (a share link, a pasted file, an embed message, agent
4
+ * output). Ported from Protoyard's `untrusted.ts`; cheap checks run before
5
+ * any deep work, so hostile input costs bounded time:
6
+ *
7
+ * 1. JSON text: UTF-8 size and nesting depth scanned on the text, then
8
+ * `JSON.parse`; a syntax error reports its line and column.
9
+ * 2. A plain-data copy: no accessors, cycles, class instances, functions or
10
+ * prototype keys; hidden characters (controls, zero-width, bidi) are
11
+ * stripped with a `hidden-characters` warning.
12
+ * 3. Counts: slides and nodes under the slides.
13
+ * 4. `migrateDeck` when the input is an earlier shape, then `parseDeck`.
14
+ *
15
+ * Nothing here runs code, fetches or renders. Never throws.
16
+ */
17
+ import { createDiagnostic, MAX_DIAGNOSTICS, } from "../schema/index.js";
18
+ import { DECK_MODEL_LIMITS } from "./deck.js";
19
+ import { detectDeckShape, migrateDeck } from "./migrate.js";
20
+ import { parseDeck } from "./parse.js";
21
+ import { copyPlainData, utf8Length } from "./plain.js";
22
+ export const DEFAULT_UNTRUSTED_DECK_LIMITS = Object.freeze({
23
+ maxBytes: 1024 * 1024,
24
+ maxDepth: 32,
25
+ maxNodes: 20_000,
26
+ maxSlides: DECK_MODEL_LIMITS.slides,
27
+ });
28
+ function fail(issues, truncated = false) {
29
+ return { ok: false, issues, steps: [], truncated };
30
+ }
31
+ function single(code, message, fix, path = []) {
32
+ return fail([createDiagnostic(code, message, path, { fix })]);
33
+ }
34
+ /** Nesting depth of JSON text, without parsing it; stops early past `limit`. */
35
+ export function jsonTextDepth(text, limit) {
36
+ let depth = 0;
37
+ let max = 0;
38
+ let inString = false;
39
+ for (let i = 0; i < text.length; i++) {
40
+ const c = text.charCodeAt(i);
41
+ if (inString) {
42
+ if (c === 0x5c)
43
+ i++;
44
+ else if (c === 0x22)
45
+ inString = false;
46
+ }
47
+ else if (c === 0x22)
48
+ inString = true;
49
+ else if (c === 0x7b || c === 0x5b) {
50
+ depth++;
51
+ if (depth > max) {
52
+ max = depth;
53
+ if (max > limit)
54
+ return max;
55
+ }
56
+ }
57
+ else if (c === 0x7d || c === 0x5d)
58
+ depth--;
59
+ }
60
+ return max;
61
+ }
62
+ /**
63
+ * The offset of the first JSON syntax error in `text` (engines word their
64
+ * `JSON.parse` errors differently, so the position comes from this scanner).
65
+ * Call it only on text with bounded depth.
66
+ */
67
+ export function jsonErrorOffset(text) {
68
+ let i = 0;
69
+ const n = text.length;
70
+ const space = () => {
71
+ while (i < n) {
72
+ const c = text[i];
73
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r')
74
+ i++;
75
+ else
76
+ break;
77
+ }
78
+ };
79
+ const fail = (at) => {
80
+ throw at;
81
+ };
82
+ const string = () => {
83
+ i++;
84
+ while (i < n) {
85
+ const c = text.charCodeAt(i);
86
+ if (c === 0x22) {
87
+ i++;
88
+ return;
89
+ }
90
+ if (c < 0x20)
91
+ fail(i);
92
+ if (c === 0x5c) {
93
+ const next = text[i + 1];
94
+ if (next === 'u') {
95
+ if (!/^[0-9a-fA-F]{4}$/.test(text.slice(i + 2, i + 6)))
96
+ fail(i);
97
+ i += 6;
98
+ }
99
+ else if (next !== undefined && '"\\/bfnrt'.includes(next))
100
+ i += 2;
101
+ else
102
+ fail(i);
103
+ }
104
+ else
105
+ i++;
106
+ }
107
+ fail(n);
108
+ };
109
+ const number = () => {
110
+ const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(text.slice(i, i + 400));
111
+ if (!match || match[0].length === 0)
112
+ fail(i);
113
+ i += match[0].length;
114
+ };
115
+ const value = () => {
116
+ space();
117
+ const c = text[i];
118
+ if (c === '{') {
119
+ i++;
120
+ space();
121
+ if (text[i] === '}') {
122
+ i++;
123
+ return;
124
+ }
125
+ for (;;) {
126
+ space();
127
+ if (text[i] !== '"')
128
+ fail(i);
129
+ string();
130
+ space();
131
+ if (text[i] !== ':')
132
+ fail(i);
133
+ i++;
134
+ value();
135
+ space();
136
+ if (text[i] === ',')
137
+ i++;
138
+ else if (text[i] === '}') {
139
+ i++;
140
+ return;
141
+ }
142
+ else
143
+ fail(i);
144
+ }
145
+ }
146
+ if (c === '[') {
147
+ i++;
148
+ space();
149
+ if (text[i] === ']') {
150
+ i++;
151
+ return;
152
+ }
153
+ for (;;) {
154
+ value();
155
+ space();
156
+ if (text[i] === ',')
157
+ i++;
158
+ else if (text[i] === ']') {
159
+ i++;
160
+ return;
161
+ }
162
+ else
163
+ fail(i);
164
+ }
165
+ }
166
+ if (c === '"') {
167
+ string();
168
+ return;
169
+ }
170
+ if (c === '-' || (c !== undefined && c >= '0' && c <= '9')) {
171
+ number();
172
+ return;
173
+ }
174
+ for (const word of ['true', 'false', 'null']) {
175
+ if (text.startsWith(word, i)) {
176
+ i += word.length;
177
+ return;
178
+ }
179
+ }
180
+ fail(i);
181
+ };
182
+ try {
183
+ value();
184
+ space();
185
+ return i < n ? i : -1;
186
+ }
187
+ catch (at) {
188
+ return typeof at === 'number' ? at : 0;
189
+ }
190
+ }
191
+ /** 1-based line and column (UTF-16 units) of `offset` in `text`. */
192
+ export function lineColumn(text, offset) {
193
+ let line = 1;
194
+ let start = 0;
195
+ const end = Math.min(Math.max(0, offset), text.length);
196
+ for (let i = 0; i < end; i++) {
197
+ if (text.charCodeAt(i) === 0x0a) {
198
+ line++;
199
+ start = i + 1;
200
+ }
201
+ }
202
+ return { line, column: end - start + 1 };
203
+ }
204
+ /** Objects and arrays under the slides (or a tree's children), counted iteratively. */
205
+ function countNodes(root, stopAt) {
206
+ const stack = [root.slides, root.children];
207
+ if (root.props !== null && typeof root.props === 'object') {
208
+ stack.push(root.props.slides);
209
+ }
210
+ let count = 0;
211
+ while (stack.length > 0 && count <= stopAt) {
212
+ const node = stack.pop();
213
+ if (node === null || typeof node !== 'object')
214
+ continue;
215
+ count++;
216
+ if (Array.isArray(node))
217
+ for (const child of node)
218
+ stack.push(child);
219
+ else
220
+ for (const key of Object.keys(node))
221
+ stack.push(node[key]);
222
+ }
223
+ return count;
224
+ }
225
+ function slideCount(root) {
226
+ const props = root.props;
227
+ const lists = [
228
+ root.slides,
229
+ root.children,
230
+ props !== null && typeof props === 'object'
231
+ ? props.slides
232
+ : undefined,
233
+ ];
234
+ return Math.max(0, ...lists.map((list) => (Array.isArray(list) ? list.length : 0)));
235
+ }
236
+ /**
237
+ * Load a deck from JSON text or parsed data that may be hostile. On success
238
+ * `deck` is a fresh validated v2 copy; earlier shapes are migrated first.
239
+ */
240
+ export function loadUntrustedDeck(input, options = {}) {
241
+ try {
242
+ return load(input, options);
243
+ }
244
+ catch {
245
+ return single('invalid-input', 'The deck could not be read.', 'Pass JSON text or plain JSON data.');
246
+ }
247
+ }
248
+ function load(input, options) {
249
+ const limits = { ...DEFAULT_UNTRUSTED_DECK_LIMITS };
250
+ for (const key of ['maxBytes', 'maxDepth', 'maxNodes', 'maxSlides']) {
251
+ const value = options[key];
252
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
253
+ limits[key] = value;
254
+ }
255
+ const maxIssues = options.maxIssues !== undefined && options.maxIssues > 0 ? options.maxIssues : MAX_DIAGNOSTICS;
256
+ let data = input;
257
+ if (typeof input === 'string') {
258
+ const bytes = utf8Length(input);
259
+ if (bytes > limits.maxBytes) {
260
+ return single('too-large', `The deck is ${bytes} bytes; the limit is ${limits.maxBytes}.`, 'Shorten notes or media, or split the deck.');
261
+ }
262
+ if (jsonTextDepth(input, limits.maxDepth) > limits.maxDepth) {
263
+ return single('too-deep', `The deck nests deeper than ${limits.maxDepth} levels.`, 'Flatten nested blocks or props.');
264
+ }
265
+ try {
266
+ data = JSON.parse(input);
267
+ }
268
+ catch {
269
+ const offset = jsonErrorOffset(input);
270
+ const at = lineColumn(input, offset < 0 ? input.length : offset);
271
+ return fail([
272
+ createDiagnostic('invalid-json', `The deck is not valid JSON (line ${at.line}, column ${at.column}).`, [], { fix: 'Fix the JSON syntax at that position.', source: at }),
273
+ ]);
274
+ }
275
+ }
276
+ const copy = copyPlainData(data, {
277
+ maxDepth: limits.maxDepth,
278
+ maxBytes: limits.maxBytes,
279
+ strip: true,
280
+ maxIssues,
281
+ });
282
+ if (copy.failed)
283
+ return fail(copy.issues, copy.truncated);
284
+ const root = copy.value;
285
+ if (root === null || typeof root !== 'object' || Array.isArray(root)) {
286
+ return single('invalid-input', 'A deck is a JSON object.', 'Pass {"formatVersion": 2, "meta": {"title": "…"}, "slides": […]}.');
287
+ }
288
+ const record = root;
289
+ const slides = slideCount(record);
290
+ if (slides > limits.maxSlides) {
291
+ return single('too-many', `The deck has ${slides} slides; the limit is ${limits.maxSlides}.`, 'Split it into several decks.', ['slides']);
292
+ }
293
+ if (countNodes(record, limits.maxNodes) > limits.maxNodes) {
294
+ return single('too-many', `The slides hold more than ${limits.maxNodes} objects and arrays.`, 'Use fewer or simpler blocks.', ['slides']);
295
+ }
296
+ const issues = [...copy.issues];
297
+ const from = detectDeckShape(record);
298
+ let deck = record;
299
+ if (from !== 'v2') {
300
+ const migration = migrateDeck(record);
301
+ deck = migration.deck;
302
+ issues.push(...migration.issues.slice(0, Math.max(0, maxIssues - issues.length)));
303
+ }
304
+ const parsed = parseDeck(deck, { ...options, maxIssues: Math.max(1, maxIssues - issues.length) });
305
+ const all = [...issues, ...parsed.issues].slice(0, maxIssues);
306
+ const errors = all.some((issue) => issue.severity === 'error');
307
+ const result = {
308
+ ok: parsed.ok && !errors,
309
+ issues: all,
310
+ steps: parsed.steps,
311
+ truncated: parsed.truncated || copy.truncated || issues.length + parsed.issues.length > maxIssues,
312
+ from,
313
+ };
314
+ if (result.ok && parsed.deck !== undefined)
315
+ result.deck = parsed.deck;
316
+ return result;
317
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `migrateDeck`: convert an earlier deck to model v2 (model-v2 §3.10). The
3
+ * input shape is detected, not read from a version number:
4
+ *
5
+ * - **v2** (`formatVersion` present): returned unchanged.
6
+ * - **Texuli v1** (`meta` + `slides[].blocks`, optional `masters` and
7
+ * `settings`): every slide becomes a lossless free slide with its master;
8
+ * `settings` moves to `style` and `navigation`, `startIndex` to a start id.
9
+ * - **Texuli grammar tree** (`layout-deck` with `deck-slide` or legacy
10
+ * `layout-card` children, or a `slides[]` grammar): block nodes become
11
+ * blocks, `charts-*` nodes chart blocks, other catalog nodes component items.
12
+ * - **Protoyard** (`style` + `slides` with `layout`, no `meta`): a title is
13
+ * derived, `paletteSeed` becomes a seed theme, ChartSpec becomes a chart
14
+ * block body, unknown background slugs become `solid-color`.
15
+ *
16
+ * The result is not validated (run `parseDeck`); every conversion that is a
17
+ * judgement call is reported as a `migrated` warning. Idempotent, pure, and
18
+ * it never throws. Lifting a master to a catalog layout is a lint
19
+ * suggestion, never done here.
20
+ */
21
+ import { type DeckDiagnostic } from '../schema/index.js';
22
+ export declare const DECK_SOURCES: readonly ["v2", "texuli-v1", "texuli-grammar", "protoyard", "unknown"];
23
+ export type DeckSource = (typeof DECK_SOURCES)[number];
24
+ export interface DeckMigration {
25
+ /** The v2 document (unvalidated: pass it to `parseDeck`); the input itself for `v2` and `unknown`. */
26
+ deck: unknown;
27
+ from: DeckSource;
28
+ /** `migrated` warnings, plus `invalid-deck` when the shape is unknown. */
29
+ issues: DeckDiagnostic[];
30
+ }
31
+ /** Which earlier shape `input` has (see the module comment). */
32
+ export declare function detectDeckShape(input: unknown): DeckSource;
33
+ /** Convert `input` to model v2; see the module comment. Never throws. */
34
+ export declare function migrateDeck(input: unknown): DeckMigration;