@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,32 @@
1
+ /**
2
+ * Bounds of the deck Markdown dialect (ADR-009). Hostile documents stop at a
3
+ * bound with a diagnostic; nothing past a bound is scanned twice.
4
+ */
5
+ export declare const DECK_MARKDOWN_LIMITS: Readonly<{
6
+ /** UTF-8 bytes of the whole document (the untrusted loader's 1 MiB). */
7
+ bytes: 1048576;
8
+ /** Lines of the whole document. */
9
+ lines: 100000;
10
+ /** UTF-16 units of one line. */
11
+ lineLength: 20000;
12
+ /** Lines of the frontmatter block. */
13
+ frontmatterLines: 64;
14
+ /** Slides (the model's cap). */
15
+ slides: 200;
16
+ /** Attributes in one comment or brace list. */
17
+ attributes: 32;
18
+ /** UTF-16 units of one attribute value. */
19
+ attributeValue: 512;
20
+ /** Directive nesting: a region may hold a component fence, nothing deeper. */
21
+ directiveDepth: 2;
22
+ /** UTF-16 units of one JSON fence body. */
23
+ jsonBody: 65536;
24
+ /** Nesting of one JSON fence body. */
25
+ jsonDepth: 32;
26
+ /** JSON values across one document. */
27
+ jsonNodes: 20000;
28
+ /** Highlight steps of one code fence (`[1-2|4-6]`). */
29
+ codeSteps: 16;
30
+ /** Diagnostics kept (the rest set `truncated`). */
31
+ issues: 200;
32
+ }>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Bounds of the deck Markdown dialect (ADR-009). Hostile documents stop at a
3
+ * bound with a diagnostic; nothing past a bound is scanned twice.
4
+ */
5
+ export const DECK_MARKDOWN_LIMITS = Object.freeze({
6
+ /** UTF-8 bytes of the whole document (the untrusted loader's 1 MiB). */
7
+ bytes: 1_048_576,
8
+ /** Lines of the whole document. */
9
+ lines: 100_000,
10
+ /** UTF-16 units of one line. */
11
+ lineLength: 20_000,
12
+ /** Lines of the frontmatter block. */
13
+ frontmatterLines: 64,
14
+ /** Slides (the model's cap). */
15
+ slides: 200,
16
+ /** Attributes in one comment or brace list. */
17
+ attributes: 32,
18
+ /** UTF-16 units of one attribute value. */
19
+ attributeValue: 512,
20
+ /** Directive nesting: a region may hold a component fence, nothing deeper. */
21
+ directiveDepth: 2,
22
+ /** UTF-16 units of one JSON fence body. */
23
+ jsonBody: 65_536,
24
+ /** Nesting of one JSON fence body. */
25
+ jsonDepth: 32,
26
+ /** JSON values across one document. */
27
+ jsonNodes: 20_000,
28
+ /** Highlight steps of one code fence (`[1-2|4-6]`). */
29
+ codeSteps: 16,
30
+ /** Diagnostics kept (the rest set `truncated`). */
31
+ issues: 200,
32
+ });
@@ -0,0 +1,31 @@
1
+ import { type DeckParseResult, type ParseDeckOptions } from '../model/parse.js';
2
+ import { type DiagnosticSource } from '../schema/index.js';
3
+ import { IssueSink } from './issues.js';
4
+ type Bag = Record<string, unknown>;
5
+ export interface ParseDeckMarkdownOptions extends ParseDeckOptions {
6
+ }
7
+ export interface DeckMarkdownResult extends DeckParseResult {
8
+ /** The deck object read from the Markdown, before model validation (for editors). */
9
+ draft?: Bag;
10
+ /** JSON path (`$.slides[2].props.title`) → where it was written. */
11
+ sourceMap: Map<string, DiagnosticSource>;
12
+ }
13
+ /** The draft deck and everything known about where it came from. */
14
+ export interface DeckDraft {
15
+ draft: Bag | null;
16
+ sink: IssueSink;
17
+ map: Map<string, DiagnosticSource>;
18
+ }
19
+ /** Derive a slide id from its title, unique among the ids before it. */
20
+ export declare function deriveSlideId(slide: Bag, index: number, used: ReadonlySet<string>): string;
21
+ /** The id of a vertical stack's section when the author names none. */
22
+ export declare function stackSectionId(firstSlideId: string): string;
23
+ /** Read the Markdown into a draft deck (not validated) with its issues and source map. */
24
+ export declare function readDeckDraft(text: string): DeckDraft;
25
+ /**
26
+ * Parse a deck Markdown document into a validated v2 deck. Every dialect and
27
+ * model issue is reported (capped at 200) with a source line and column;
28
+ * `ok` is true only when there is no error. Never throws.
29
+ */
30
+ export declare function parseDeckMarkdown(text: string, options?: ParseDeckMarkdownOptions): DeckMarkdownResult;
31
+ export {};
@@ -0,0 +1,283 @@
1
+ /**
2
+ * `parseDeckMarkdown`: a deck Markdown document → the same validated v2
3
+ * `Deck` that JSON produces. The dialect's own diagnostics carry a source
4
+ * line and column; the model's diagnostics (JSON paths) are mapped back to
5
+ * the Markdown through a source map built while reading.
6
+ */
7
+ import { DECK_SOLID_TOKENS, isDeckBackgroundPreset } from "../model/backgrounds/schema.js";
8
+ import { DECK_FORMAT_VERSION } from "../model/deck.js";
9
+ import { parseDeck } from "../model/parse.js";
10
+ import { toJsonPath, } from "../schema/index.js";
11
+ import { readSlide } from "./blocks.js";
12
+ import { frontmatterPath, readFrontmatter } from "./frontmatter.js";
13
+ import { IssueSink } from "./issues.js";
14
+ import { DECK_MARKDOWN_LIMITS } from "./limits.js";
15
+ import { normalizeLines, scanDocument } from "./scan.js";
16
+ import { buildSlide, idCandidate } from "./slides.js";
17
+ import { slugify } from "./text.js";
18
+ const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
19
+ function expandHex(value) {
20
+ return value.length === 7 ? value : `#${[...value.slice(1)].map((c) => c + c).join('')}`;
21
+ }
22
+ /** Derive a slide id from its title, unique among the ids before it. */
23
+ export function deriveSlideId(slide, index, used) {
24
+ const stem = slugify(idCandidate(slide)) || `slide-${index + 1}`;
25
+ if (!used.has(stem))
26
+ return stem;
27
+ for (let n = 2;; n++) {
28
+ const candidate = `${stem}-${n}`;
29
+ if (!used.has(candidate))
30
+ return candidate;
31
+ }
32
+ }
33
+ /** The id of a vertical stack's section when the author names none. */
34
+ export function stackSectionId(firstSlideId) {
35
+ return `stack-${firstSlideId}`.slice(0, 64).replace(/-+$/, '');
36
+ }
37
+ /** Read the Markdown into a draft deck (not validated) with its issues and source map. */
38
+ export function readDeckDraft(text) {
39
+ const sink = new IssueSink();
40
+ const map = new Map();
41
+ const lines = normalizeLines(String(text), sink);
42
+ if (lines === null)
43
+ return { draft: null, sink, map };
44
+ const scanned = scanDocument(lines, sink);
45
+ const front = readFrontmatter(scanned.frontmatter ?? [], sink);
46
+ // Slides: empty ones (only blank lines) are dropped.
47
+ const ctx = { sink, map, jsonBudget: { nodes: DECK_MARKDOWN_LIMITS.jsonNodes } };
48
+ const built = [];
49
+ for (const raw of scanned.slides) {
50
+ if (raw.lines.every((line) => line.text.trim() === ''))
51
+ continue;
52
+ if (built.length >= DECK_MARKDOWN_LIMITS.slides) {
53
+ sink.report('md-too-many-slides', `A deck has at most ${DECK_MARKDOWN_LIMITS.slides} slides; the rest were not read.`, { line: raw.line, column: 1 }, ['slides'], 'Split the deck.');
54
+ break;
55
+ }
56
+ const source = readSlide(raw.lines, sink);
57
+ const first = raw.lines.find((line) => line.text.trim() !== '');
58
+ const src = { line: first?.n ?? raw.line, column: 1 };
59
+ let vertical = raw.vertical;
60
+ if (vertical && built.length === 0) {
61
+ sink.report('md-stack-start', 'A `--` vertical slide needs a horizontal slide before it.', src);
62
+ vertical = false;
63
+ }
64
+ built.push({ ...buildSlide(source, built.length, src, ctx), vertical });
65
+ }
66
+ // Ids
67
+ const used = new Set();
68
+ const slides = built.map((entry, index) => {
69
+ const own = entry.slide.id;
70
+ const id = typeof own === 'string' ? own : (entry.attrs.id ?? deriveSlideId(entry.slide, index, used));
71
+ used.add(id);
72
+ const attr = entry.attrs.at.get('id');
73
+ map.set(toJsonPath(['slides', index, 'id']), attr ? { line: attr.line, column: attr.column } : entry.src);
74
+ return { id, ...entry.slide };
75
+ });
76
+ // Outline: vertical stacks and named sections, in document order.
77
+ const sections = [];
78
+ let named = null;
79
+ for (let i = 0; i < built.length;) {
80
+ let end = i + 1;
81
+ while (end < built.length && built[end]?.vertical)
82
+ end++;
83
+ const entry = built[i];
84
+ const ids = slides.slice(i, end).map((slide) => slide.id);
85
+ if (end - i > 1) {
86
+ named = null;
87
+ const section = {
88
+ id: entry.attrs.section ?? stackSectionId(ids[0] ?? ''),
89
+ slideIds: ids,
90
+ stack: true,
91
+ };
92
+ if (entry.attrs.sectionTitle !== undefined)
93
+ section.title = entry.attrs.sectionTitle;
94
+ map.set(toJsonPath(['outline', 'sections', sections.length]), entry.src);
95
+ sections.push(section);
96
+ }
97
+ else {
98
+ if (entry.attrs.sectionEnd)
99
+ named = null;
100
+ if (entry.attrs.section !== undefined) {
101
+ const section = { id: entry.attrs.section, slideIds: ids };
102
+ if (entry.attrs.sectionTitle !== undefined)
103
+ section.title = entry.attrs.sectionTitle;
104
+ const attr = entry.attrs.at.get('section');
105
+ map.set(toJsonPath(['outline', 'sections', sections.length]), attr ? { line: attr.line, column: attr.column } : entry.src);
106
+ sections.push(section);
107
+ named = section;
108
+ }
109
+ else if (named)
110
+ named.slideIds.push(...ids);
111
+ else if (entry.attrs.sectionTitle !== undefined) {
112
+ sink.report('md-attr-conflict', '`section-title` needs `section`.', entry.src, [
113
+ 'slides',
114
+ i,
115
+ ]);
116
+ }
117
+ }
118
+ i = end;
119
+ }
120
+ // Frontmatter → meta, style, navigation, outline title
121
+ const value = (key) => front.values.get(key);
122
+ const deck = { formatVersion: DECK_FORMAT_VERSION };
123
+ const meta = {};
124
+ const style = {};
125
+ const navigation = {};
126
+ for (const key of front.values.keys()) {
127
+ const line = front.lines.get(key) ?? 1;
128
+ map.set(frontmatterPath(key), { line, column: 1 });
129
+ }
130
+ for (const key of ['title', 'description', 'author', 'date', 'locale', 'dir']) {
131
+ const v = value(key);
132
+ if (v !== undefined)
133
+ meta[key] = v;
134
+ }
135
+ if (meta.title === undefined) {
136
+ const first = slides[0] ? idCandidate(slides[0]) : '';
137
+ meta.title = first.trim() || 'Untitled deck';
138
+ sink.report('md-title-missing', 'The frontmatter has no `title`; one was derived.', { line: 1, column: 1 }, ['meta', 'title'], 'Add `title: …` to the frontmatter.');
139
+ }
140
+ const theme = value('theme');
141
+ const themeMode = value('themeMode');
142
+ const themeSeed = value('themeSeed');
143
+ if (themeSeed !== undefined) {
144
+ style.theme =
145
+ themeMode === undefined
146
+ ? { kind: 'seed', seed: themeSeed }
147
+ : { kind: 'seed', seed: themeSeed, mode: themeMode };
148
+ if (theme !== undefined) {
149
+ sink.report('md-frontmatter-key', '`theme` and `themeSeed` exclude each other.', {
150
+ line: front.lines.get('theme') ?? 1,
151
+ column: 1,
152
+ });
153
+ }
154
+ }
155
+ else if (themeMode !== undefined) {
156
+ if (theme === undefined) {
157
+ sink.report('md-frontmatter-key', '`themeMode` needs `theme` (or `themeSeed`).', {
158
+ line: front.lines.get('themeMode') ?? 1,
159
+ column: 1,
160
+ });
161
+ }
162
+ else
163
+ style.theme = { kind: 'catalog', id: theme, mode: themeMode };
164
+ }
165
+ else if (theme !== undefined)
166
+ style.theme = theme;
167
+ for (const key of [
168
+ 'fontPair',
169
+ 'aspect',
170
+ 'density',
171
+ 'radius',
172
+ 'motion',
173
+ 'transition',
174
+ 'logo',
175
+ 'footer',
176
+ 'slideNumbers',
177
+ 'progress',
178
+ 'codeTheme',
179
+ 'math',
180
+ ]) {
181
+ const v = value(key);
182
+ if (v !== undefined)
183
+ style[key] = v;
184
+ }
185
+ const background = value('background');
186
+ if (typeof background === 'string') {
187
+ if (isDeckBackgroundPreset(background))
188
+ style.background = background;
189
+ else if (HEX.test(background))
190
+ style.background = { kind: 'solid', color: expandHex(background) };
191
+ else if (DECK_SOLID_TOKENS.includes(background)) {
192
+ style.background = { kind: 'solid', token: background };
193
+ }
194
+ else
195
+ style.background = background;
196
+ }
197
+ const start = value('start');
198
+ if (start !== undefined)
199
+ navigation.start = start;
200
+ const loop = value('loop');
201
+ if (loop !== undefined)
202
+ navigation.loop = loop;
203
+ const autoSlide = value('autoSlide');
204
+ if (autoSlide !== undefined)
205
+ navigation.autoSlideMs = autoSlide;
206
+ const outlineTitle = value('outlineTitle');
207
+ deck.meta = meta;
208
+ if (Object.keys(style).length > 0)
209
+ deck.style = style;
210
+ if (Object.keys(navigation).length > 0)
211
+ deck.navigation = navigation;
212
+ if (sections.length > 0) {
213
+ deck.outline = outlineTitle === undefined ? { sections } : { title: outlineTitle, sections };
214
+ }
215
+ else if (outlineTitle !== undefined) {
216
+ sink.report('md-frontmatter-value', '`outlineTitle` needs at least one section (`section=` or a `--` stack).', { line: front.lines.get('outlineTitle') ?? 1, column: 1 });
217
+ }
218
+ deck.slides = slides;
219
+ return { draft: deck, sink, map };
220
+ }
221
+ /** The source of the longest prefix of `path` that was recorded. */
222
+ function locate(map, path) {
223
+ for (let n = path.length; n >= 0; n--) {
224
+ const found = map.get(toJsonPath(path.slice(0, n)));
225
+ if (found)
226
+ return found;
227
+ }
228
+ return undefined;
229
+ }
230
+ /**
231
+ * Parse a deck Markdown document into a validated v2 deck. Every dialect and
232
+ * model issue is reported (capped at 200) with a source line and column;
233
+ * `ok` is true only when there is no error. Never throws.
234
+ */
235
+ export function parseDeckMarkdown(text, options = {}) {
236
+ let read;
237
+ try {
238
+ read = readDeckDraft(text);
239
+ }
240
+ catch {
241
+ const sink = new IssueSink();
242
+ sink.report('md-too-large', 'The document could not be read.', { line: 1, column: 1 });
243
+ return { ok: false, issues: sink.issues, steps: [], truncated: false, sourceMap: new Map() };
244
+ }
245
+ const { draft, sink, map } = read;
246
+ if (draft === null) {
247
+ return { ok: false, issues: sink.issues, steps: [], truncated: sink.truncated, sourceMap: map };
248
+ }
249
+ const model = parseDeck(draft, options);
250
+ const issues = [...sink.issues];
251
+ const max = options.maxIssues !== undefined && options.maxIssues > 0
252
+ ? options.maxIssues
253
+ : DECK_MARKDOWN_LIMITS.issues;
254
+ let truncated = sink.truncated || model.truncated;
255
+ for (const issue of model.issues) {
256
+ if (issues.length >= max) {
257
+ truncated = true;
258
+ break;
259
+ }
260
+ if (issue.source === undefined) {
261
+ const source = locate(map, issue.path);
262
+ if (source)
263
+ issue.source = { ...source };
264
+ }
265
+ issues.push(issue);
266
+ }
267
+ if (issues.length > max) {
268
+ issues.length = max;
269
+ truncated = true;
270
+ }
271
+ const ok = model.ok && sink.errors === 0;
272
+ const result = {
273
+ ok,
274
+ issues,
275
+ steps: model.steps,
276
+ truncated,
277
+ draft,
278
+ sourceMap: map,
279
+ };
280
+ if (ok && model.deck)
281
+ result.deck = model.deck;
282
+ return result;
283
+ }
@@ -0,0 +1,26 @@
1
+ import type { IssueSink } from './issues.js';
2
+ export interface Line {
3
+ text: string;
4
+ /** 1-based line number in the source. */
5
+ n: number;
6
+ }
7
+ export interface RawSlide {
8
+ lines: Line[];
9
+ /** Came after a `--` line: part of the previous horizontal slide's stack. */
10
+ vertical: boolean;
11
+ /** The line where the slide starts (its first line, or its separator + 1). */
12
+ line: number;
13
+ }
14
+ export interface Scanned {
15
+ frontmatter: Line[] | null;
16
+ /** Line of the opening `---` of the frontmatter. */
17
+ frontmatterLine: number;
18
+ slides: RawSlide[];
19
+ }
20
+ /**
21
+ * Line endings normalized (CR LF, CR, U+2028 and U+2029 become line feeds),
22
+ * size bounds applied, hidden characters reported and removed.
23
+ */
24
+ export declare function normalizeLines(text: string, sink: IssueSink): Line[] | null;
25
+ /** Split the frontmatter and the slides. */
26
+ export declare function scanDocument(lines: Line[], sink: IssueSink): Scanned;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The line scanner: input bounds, line endings and hidden characters, the
3
+ * frontmatter block, and the split into slides at `---` (horizontal) and
4
+ * `--` (vertical) lines outside code and math fences. One pass over the
5
+ * lines (a second, fence-bounded pass only after an unclosed fence).
6
+ */
7
+ import { utf8Length } from "../model/plain.js";
8
+ import { DECK_MARKDOWN_LIMITS } from "./limits.js";
9
+ import { isHiddenCode } from "./text.js";
10
+ /**
11
+ * Line endings normalized (CR LF, CR, U+2028 and U+2029 become line feeds),
12
+ * size bounds applied, hidden characters reported and removed.
13
+ */
14
+ export function normalizeLines(text, sink) {
15
+ if (utf8Length(text) > DECK_MARKDOWN_LIMITS.bytes) {
16
+ sink.report('md-too-large', `The document is over ${DECK_MARKDOWN_LIMITS.bytes} bytes; nothing was read.`, { line: 1, column: 1 }, [], 'Split the deck, or move large data out of the Markdown.');
17
+ return null;
18
+ }
19
+ let source = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
20
+ // U+2028 and U+2029 are line terminators to JavaScript regexes (`.` and `$`
21
+ // stop at them), so they end a line here too: no line holds one.
22
+ source = source.replace(/\r\n?|[\u2028\u2029]/g, '\n');
23
+ const raw = source.split('\n');
24
+ if (raw.length > DECK_MARKDOWN_LIMITS.lines) {
25
+ sink.report('md-too-large', `The document is over ${DECK_MARKDOWN_LIMITS.lines} lines; the rest was not read.`, { line: DECK_MARKDOWN_LIMITS.lines + 1, column: 1 });
26
+ raw.length = DECK_MARKDOWN_LIMITS.lines;
27
+ }
28
+ const lines = [];
29
+ for (let index = 0; index < raw.length; index++) {
30
+ let line = raw[index] ?? '';
31
+ const n = index + 1;
32
+ if (line.length > DECK_MARKDOWN_LIMITS.lineLength) {
33
+ sink.report('md-too-large', `A line is at most ${DECK_MARKDOWN_LIMITS.lineLength} characters; the rest was cut.`, { line: n, column: DECK_MARKDOWN_LIMITS.lineLength + 1 });
34
+ line = line.slice(0, DECK_MARKDOWN_LIMITS.lineLength);
35
+ }
36
+ let clean = '';
37
+ let start = 0;
38
+ let reported = false;
39
+ for (let i = 0; i < line.length; i++) {
40
+ const code = line.charCodeAt(i);
41
+ if (code === 0x09 || !isHiddenCode(code))
42
+ continue;
43
+ if (!reported) {
44
+ sink.report('md-invisible-char', `Hidden character U+${code.toString(16).toUpperCase().padStart(4, '0')} (control, bidi or invisible formatting).`, { line: n, column: i + 1 }, [], 'Delete the character; it can hide or reorder text (Trojan Source).');
45
+ reported = true;
46
+ }
47
+ clean += line.slice(start, i);
48
+ start = i + 1;
49
+ }
50
+ lines.push({ text: start === 0 ? line : clean + line.slice(start), n });
51
+ }
52
+ return lines;
53
+ }
54
+ const H_SEP = /^-{3,}[ \t]*$/;
55
+ const V_SEP = /^--[ \t]*$/;
56
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
57
+ const MATH_FENCE = /^ {0,3}\$\$[ \t]*$/;
58
+ function closesFence(text, char, length) {
59
+ const match = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(text);
60
+ return match?.[1] !== undefined && match[1][0] === char && match[1].length >= length;
61
+ }
62
+ /** Split the frontmatter and the slides. */
63
+ export function scanDocument(lines, sink) {
64
+ let start = 0;
65
+ let frontmatter = null;
66
+ if (lines.length > 0 && /^---[ \t]*$/.test(lines[0]?.text ?? '')) {
67
+ let close = -1;
68
+ const end = Math.min(lines.length, DECK_MARKDOWN_LIMITS.frontmatterLines + 2);
69
+ for (let i = 1; i < end; i++) {
70
+ if (/^---[ \t]*$/.test(lines[i]?.text ?? '')) {
71
+ close = i;
72
+ break;
73
+ }
74
+ }
75
+ if (close === -1) {
76
+ sink.report('md-frontmatter-unclosed', `The frontmatter has no closing \`---\` within ${DECK_MARKDOWN_LIMITS.frontmatterLines} lines.`, { line: 1, column: 1 }, [], 'Close the frontmatter with a `---` line.');
77
+ }
78
+ else {
79
+ frontmatter = lines.slice(1, close);
80
+ start = close + 1;
81
+ }
82
+ }
83
+ const slides = splitSlides(lines, start, false);
84
+ return {
85
+ frontmatter,
86
+ frontmatterLine: 1,
87
+ slides: slides ?? splitSlides(lines, start, true) ?? [],
88
+ };
89
+ }
90
+ /**
91
+ * Split at separators. Returns null when a fence is still open at the end of
92
+ * the document; the caller then splits again with `bounded`, where every
93
+ * fence ends at the next separator line (so one bad fence cannot swallow the
94
+ * rest of the deck).
95
+ */
96
+ function splitSlides(lines, from, bounded) {
97
+ const slides = [];
98
+ let current = { lines: [], vertical: false, line: lines[from]?.n ?? 1 };
99
+ let fence = null;
100
+ let math = false;
101
+ for (let i = from; i < lines.length; i++) {
102
+ const line = lines[i];
103
+ const text = line.text;
104
+ if (fence !== null || math) {
105
+ const separator = H_SEP.test(text) || V_SEP.test(text);
106
+ if (bounded && separator) {
107
+ fence = null;
108
+ math = false;
109
+ }
110
+ else {
111
+ if (fence !== null && closesFence(text, fence.char, fence.length))
112
+ fence = null;
113
+ else if (math && MATH_FENCE.test(text))
114
+ math = false;
115
+ current.lines.push(line);
116
+ continue;
117
+ }
118
+ }
119
+ if (H_SEP.test(text) || V_SEP.test(text)) {
120
+ slides.push(current);
121
+ current = { lines: [], vertical: V_SEP.test(text), line: line.n + 1 };
122
+ continue;
123
+ }
124
+ const open = FENCE_OPEN.exec(text);
125
+ if (open?.[1] !== undefined &&
126
+ !(open[1][0] === '`' && text.slice(open[0].length).includes('`'))) {
127
+ fence = { char: open[1][0] ?? '`', length: open[1].length };
128
+ }
129
+ else if (MATH_FENCE.test(text)) {
130
+ math = true;
131
+ }
132
+ current.lines.push(line);
133
+ }
134
+ if ((fence !== null || math) && !bounded)
135
+ return null;
136
+ slides.push(current);
137
+ return slides;
138
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Slide builder: the nodes of one slide → a v2 slide object (not yet
3
+ * validated), with fragments numbered the reveal.js way and a source map
4
+ * from JSON paths to Markdown positions. Free slides get blocks; the 20
5
+ * catalog layouts map headings, paragraphs, lists, images, tables and `:::`
6
+ * regions onto their props; `component`, `custom` and `::: props` / `::: slide`
7
+ * fences carry bounded JSON.
8
+ */
9
+ import { type SlideAttrRead } from './attrs-closed.js';
10
+ import type { SlideSource, Src } from './blocks.js';
11
+ import type { IssueSink } from './issues.js';
12
+ type Bag = Record<string, unknown>;
13
+ /** Shared state across the slides of one document. */
14
+ export interface BuildContext {
15
+ sink: IssueSink;
16
+ /** JSON path → source position. */
17
+ map: Map<string, Src>;
18
+ /** JSON values still allowed in fence bodies. */
19
+ jsonBudget: {
20
+ nodes: number;
21
+ };
22
+ }
23
+ export interface BuiltSlide {
24
+ slide: Bag;
25
+ attrs: SlideAttrRead;
26
+ src: Src;
27
+ }
28
+ /** Build one slide. `index` is its position in `slides`. */
29
+ export declare function buildSlide(source: SlideSource, index: number, src: Src, ctx: BuildContext): BuiltSlide;
30
+ /** The text an id is derived from: the slide title, the props title, or a free slide's first heading. */
31
+ export declare function idCandidate(slide: Bag): string;
32
+ export {};