@texuli/deck 0.0.0-stage → 0.4.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 +182 -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,30 @@
1
+ /**
2
+ * The frontmatter: a closed key set with scalar values only (no YAML parser,
3
+ * no lists, maps, anchors, tags or multi-line values). Values are typed by
4
+ * key; the deck model validates them afterwards, and its issues map back to
5
+ * the key's line.
6
+ */
7
+ import { type PathSegment } from '../schema/index.js';
8
+ import type { IssueSink } from './issues.js';
9
+ import type { Line } from './scan.js';
10
+ type Kind = 'string' | 'boolean' | 'int';
11
+ interface KeySpec {
12
+ kind: Kind;
13
+ /** Where the value goes in the deck. */
14
+ path: readonly PathSegment[];
15
+ }
16
+ /** The closed frontmatter keys, in canonical (written) order. */
17
+ export declare const FRONTMATTER_KEYS: Readonly<Record<string, KeySpec>>;
18
+ export interface Frontmatter {
19
+ /** Key → typed value, in the order read. */
20
+ values: Map<string, string | number | boolean>;
21
+ /** Key → line. */
22
+ lines: Map<string, number>;
23
+ }
24
+ /** Read the frontmatter lines. Unknown keys and non-scalar values are errors. */
25
+ export declare function readFrontmatter(lines: readonly Line[], sink: IssueSink): Frontmatter;
26
+ /** The JSON paths a frontmatter key's value lands on (for the source map). */
27
+ export declare function frontmatterPath(key: string): string;
28
+ /** A frontmatter scalar written canonically: bare when unambiguous, else double-quoted. */
29
+ export declare function writeScalar(value: string | number | boolean): string;
30
+ export {};
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The frontmatter: a closed key set with scalar values only (no YAML parser,
3
+ * no lists, maps, anchors, tags or multi-line values). Values are typed by
4
+ * key; the deck model validates them afterwards, and its issues map back to
5
+ * the key's line.
6
+ */
7
+ import { toJsonPath } from "../schema/index.js";
8
+ import { trimBlankEnd } from "./text.js";
9
+ /** The closed frontmatter keys, in canonical (written) order. */
10
+ export const FRONTMATTER_KEYS = Object.freeze({
11
+ title: { kind: 'string', path: ['meta', 'title'] },
12
+ description: { kind: 'string', path: ['meta', 'description'] },
13
+ author: { kind: 'string', path: ['meta', 'author'] },
14
+ date: { kind: 'string', path: ['meta', 'date'] },
15
+ locale: { kind: 'string', path: ['meta', 'locale'] },
16
+ dir: { kind: 'string', path: ['meta', 'dir'] },
17
+ theme: { kind: 'string', path: ['style', 'theme'] },
18
+ themeMode: { kind: 'string', path: ['style', 'theme', 'mode'] },
19
+ themeSeed: { kind: 'int', path: ['style', 'theme', 'seed'] },
20
+ fontPair: { kind: 'string', path: ['style', 'fontPair'] },
21
+ aspect: { kind: 'string', path: ['style', 'aspect'] },
22
+ density: { kind: 'string', path: ['style', 'density'] },
23
+ radius: { kind: 'string', path: ['style', 'radius'] },
24
+ motion: { kind: 'string', path: ['style', 'motion'] },
25
+ transition: { kind: 'string', path: ['style', 'transition'] },
26
+ background: { kind: 'string', path: ['style', 'background'] },
27
+ logo: { kind: 'string', path: ['style', 'logo'] },
28
+ footer: { kind: 'string', path: ['style', 'footer'] },
29
+ slideNumbers: { kind: 'boolean', path: ['style', 'slideNumbers'] },
30
+ progress: { kind: 'string', path: ['style', 'progress'] },
31
+ codeTheme: { kind: 'string', path: ['style', 'codeTheme'] },
32
+ math: { kind: 'boolean', path: ['style', 'math'] },
33
+ start: { kind: 'string', path: ['navigation', 'start'] },
34
+ loop: { kind: 'boolean', path: ['navigation', 'loop'] },
35
+ autoSlide: { kind: 'int', path: ['navigation', 'autoSlideMs'] },
36
+ outlineTitle: { kind: 'string', path: ['outline', 'title'] },
37
+ });
38
+ /** Kebab-case spellings accepted on input (the writer uses camelCase). */
39
+ const ALIASES = Object.freeze({
40
+ 'font-pair': 'fontPair',
41
+ 'slide-numbers': 'slideNumbers',
42
+ 'code-theme': 'codeTheme',
43
+ 'auto-slide': 'autoSlide',
44
+ 'theme-mode': 'themeMode',
45
+ 'theme-seed': 'themeSeed',
46
+ 'outline-title': 'outlineTitle',
47
+ });
48
+ /** Runs on `trimBlankEnd(text)`: no trailing `[ \t]*` competes with the value. */
49
+ const KEY = /^([A-Za-z][A-Za-z0-9-]*)[ \t]*:(?:[ \t]+(.*))?$/;
50
+ function scalar(raw) {
51
+ if (raw.startsWith('"')) {
52
+ if (!raw.endsWith('"') || raw.length < 2)
53
+ return { error: 'The quoted value never closes.' };
54
+ const inner = raw.slice(1, -1);
55
+ let out = '';
56
+ for (let i = 0; i < inner.length; i++) {
57
+ const c = inner[i];
58
+ if (c === '\\') {
59
+ const next = inner[i + 1];
60
+ if (next === '"' || next === '\\') {
61
+ out += next;
62
+ i++;
63
+ continue;
64
+ }
65
+ return { error: 'Only \\" and \\\\ escapes are read in a quoted value.' };
66
+ }
67
+ if (c === '"')
68
+ return { error: 'Escape a " inside a quoted value as \\".' };
69
+ out += c;
70
+ }
71
+ return { value: out };
72
+ }
73
+ if (raw.startsWith("'")) {
74
+ if (!raw.endsWith("'") || raw.length < 2)
75
+ return { error: 'The quoted value never closes.' };
76
+ const inner = raw.slice(1, -1);
77
+ if (/(?<!')'(?!')/.test(inner.replaceAll("''", ''))) {
78
+ return { error: "Escape a ' inside a single-quoted value as ''." };
79
+ }
80
+ return { value: inner.replaceAll("''", "'") };
81
+ }
82
+ if (/^[[{|>&*!%@`]/.test(raw)) {
83
+ return { error: 'Only scalar values: no lists, maps, blocks, anchors or tags.' };
84
+ }
85
+ return { value: raw };
86
+ }
87
+ /** Read the frontmatter lines. Unknown keys and non-scalar values are errors. */
88
+ export function readFrontmatter(lines, sink) {
89
+ const values = new Map();
90
+ const lineOf = new Map();
91
+ for (const line of lines) {
92
+ const text = line.text;
93
+ if (text.trim() === '' || text.trimStart().startsWith('#'))
94
+ continue;
95
+ const match = KEY.exec(trimBlankEnd(text));
96
+ if (!match) {
97
+ sink.report('md-frontmatter-value', 'A frontmatter line is `key: value`.', { line: line.n, column: 1 }, [], 'Write one scalar per line, e.g. `title: Quarterly review`.');
98
+ continue;
99
+ }
100
+ const written = match[1] ?? '';
101
+ const key = Object.hasOwn(ALIASES, written) ? (ALIASES[written] ?? written) : written;
102
+ const spec = Object.hasOwn(FRONTMATTER_KEYS, key) ? FRONTMATTER_KEYS[key] : undefined;
103
+ const column = text.indexOf(':') + 2;
104
+ if (!spec) {
105
+ sink.report('md-frontmatter-key', `Unknown frontmatter key "${written.slice(0, 40)}".`, { line: line.n, column: 1 }, [], `Use one of: ${Object.keys(FRONTMATTER_KEYS).join(', ')}.`);
106
+ continue;
107
+ }
108
+ if (values.has(key)) {
109
+ sink.report('md-frontmatter-key', `"${key}" is set twice.`, { line: line.n, column: 1 });
110
+ continue;
111
+ }
112
+ const raw = match[2] ?? '';
113
+ if (raw === '') {
114
+ sink.report('md-frontmatter-value', `"${key}" needs a value.`, { line: line.n, column });
115
+ continue;
116
+ }
117
+ const read = scalar(raw);
118
+ if ('error' in read) {
119
+ sink.report('md-frontmatter-value', read.error, { line: line.n, column });
120
+ continue;
121
+ }
122
+ let value = read.value;
123
+ if (spec.kind === 'boolean') {
124
+ if (read.value !== 'true' && read.value !== 'false') {
125
+ sink.report('md-frontmatter-value', `"${key}" is true or false.`, { line: line.n, column });
126
+ continue;
127
+ }
128
+ value = read.value === 'true';
129
+ }
130
+ else if (spec.kind === 'int') {
131
+ if (!/^-?\d{1,10}$/.test(read.value)) {
132
+ sink.report('md-frontmatter-value', `"${key}" is a whole number.`, {
133
+ line: line.n,
134
+ column,
135
+ });
136
+ continue;
137
+ }
138
+ value = Number(read.value);
139
+ }
140
+ values.set(key, value);
141
+ lineOf.set(key, line.n);
142
+ }
143
+ return { values, lines: lineOf };
144
+ }
145
+ /** The JSON paths a frontmatter key's value lands on (for the source map). */
146
+ export function frontmatterPath(key) {
147
+ const spec = FRONTMATTER_KEYS[key];
148
+ return spec ? toJsonPath(spec.path) : '$';
149
+ }
150
+ /** A frontmatter scalar written canonically: bare when unambiguous, else double-quoted. */
151
+ export function writeScalar(value) {
152
+ if (typeof value !== 'string')
153
+ return String(value);
154
+ if (value === '' ||
155
+ value !== value.trim() ||
156
+ /^[[{|>&*!%@`"'#]/.test(value) ||
157
+ value === 'true' ||
158
+ value === 'false' ||
159
+ /^-?\d+$/.test(value) ||
160
+ value.includes(': ') ||
161
+ value.includes(' #')) {
162
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
163
+ }
164
+ return value;
165
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @texuli/deck/markdown: the deck Markdown dialect (ADR-009). reveal.js is an
3
+ * inspiration, not a compatibility target, and there is no HTML:
4
+ *
5
+ * - `parseDeckMarkdown(text, options)` reads closed-key scalar frontmatter,
6
+ * `---` slides and `--` vertical stacks, `Note:`, `<!-- .slide: -->` and
7
+ * `<!-- .element: -->` attributes from a closed set (reveal.js aliases
8
+ * mapped), fragments, code fences with `[1-2|4-6]` steps, `$$` math, the
9
+ * 20 layouts through `layout="<id>"` and `:::` regions, and
10
+ * `::: component <id>` fences with a bounded JSON body. Raw HTML, iframes,
11
+ * `class`, `style` and `on*` are refused with a diagnostic. It returns the
12
+ * same validated `Deck` as `parseDeck`, every issue with a source line and
13
+ * column, and the source map.
14
+ * - `deckToMarkdown(deck)` writes canonical Markdown that reads back as the
15
+ * same deck (JSON fences where Markdown would not round-trip).
16
+ * - `deckToText` and `deckOutline` are the one-way text twins.
17
+ *
18
+ * Inline `$…$` math stays inside text blocks as written; block `$$` math
19
+ * becomes a `{ type: "math", tex }` block (see `@texuli/deck/math`).
20
+ */
21
+ export { type Attr, attributeWritable, readAttributes, writeAttribute } from './attributes.js';
22
+ export { mapEffect } from './attrs-closed.js';
23
+ export { FRONTMATTER_KEYS } from './frontmatter.js';
24
+ export { DECK_MARKDOWN_CODES, type DeckMarkdownCode, type DeckMarkdownCodeInfo } from './issues.js';
25
+ export { type JsonReadOptions, type JsonReadResult, readJson } from './json.js';
26
+ export { DECK_MARKDOWN_LIMITS } from './limits.js';
27
+ export { type DeckDraft, type DeckMarkdownResult, deriveSlideId, type ParseDeckMarkdownOptions, parseDeckMarkdown, readDeckDraft, } from './parse.js';
28
+ export { type DeckOutlineSlide, type DeckOutlineText, type DeckTextOptions, deckOutline, deckToText, formatDeckOutline, slideTitle, } from './twin.js';
29
+ export { type DeckMarkdownWriteResult, deckToMarkdown, writeDeckMarkdown } from './write.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @texuli/deck/markdown: the deck Markdown dialect (ADR-009). reveal.js is an
3
+ * inspiration, not a compatibility target, and there is no HTML:
4
+ *
5
+ * - `parseDeckMarkdown(text, options)` reads closed-key scalar frontmatter,
6
+ * `---` slides and `--` vertical stacks, `Note:`, `<!-- .slide: -->` and
7
+ * `<!-- .element: -->` attributes from a closed set (reveal.js aliases
8
+ * mapped), fragments, code fences with `[1-2|4-6]` steps, `$$` math, the
9
+ * 20 layouts through `layout="<id>"` and `:::` regions, and
10
+ * `::: component <id>` fences with a bounded JSON body. Raw HTML, iframes,
11
+ * `class`, `style` and `on*` are refused with a diagnostic. It returns the
12
+ * same validated `Deck` as `parseDeck`, every issue with a source line and
13
+ * column, and the source map.
14
+ * - `deckToMarkdown(deck)` writes canonical Markdown that reads back as the
15
+ * same deck (JSON fences where Markdown would not round-trip).
16
+ * - `deckToText` and `deckOutline` are the one-way text twins.
17
+ *
18
+ * Inline `$…$` math stays inside text blocks as written; block `$$` math
19
+ * becomes a `{ type: "math", tex }` block (see `@texuli/deck/math`).
20
+ */
21
+ export { attributeWritable, readAttributes, writeAttribute } from "./attributes.js";
22
+ export { mapEffect } from "./attrs-closed.js";
23
+ export { FRONTMATTER_KEYS } from "./frontmatter.js";
24
+ export { DECK_MARKDOWN_CODES } from "./issues.js";
25
+ export { readJson } from "./json.js";
26
+ export { DECK_MARKDOWN_LIMITS } from "./limits.js";
27
+ export { deriveSlideId, parseDeckMarkdown, readDeckDraft, } from "./parse.js";
28
+ export { deckOutline, deckToText, formatDeckOutline, slideTitle, } from "./twin.js";
29
+ export { deckToMarkdown, writeDeckMarkdown } from "./write.js";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The diagnostics of the deck Markdown dialect: stable `md-*` codes, each
3
+ * with its severity and meaning, reported as the engine's `DeckDiagnostic`
4
+ * with a `source` line and column (1-based; columns count UTF-16 units).
5
+ */
6
+ import { type DeckDiagnostic, type DiagnosticSeverity, type DiagnosticSource, type PathSegment } from '../schema/index.js';
7
+ export interface DeckMarkdownCodeInfo {
8
+ readonly severity: DiagnosticSeverity;
9
+ readonly meaning: string;
10
+ }
11
+ /** Every code the Markdown reader and writer report. */
12
+ export declare const DECK_MARKDOWN_CODES: Readonly<{
13
+ readonly 'md-too-large': DeckMarkdownCodeInfo;
14
+ readonly 'md-invisible-char': DeckMarkdownCodeInfo;
15
+ readonly 'md-frontmatter-key': DeckMarkdownCodeInfo;
16
+ readonly 'md-frontmatter-value': DeckMarkdownCodeInfo;
17
+ readonly 'md-frontmatter-unclosed': DeckMarkdownCodeInfo;
18
+ readonly 'md-title-missing': DeckMarkdownCodeInfo;
19
+ readonly 'md-html-refused': DeckMarkdownCodeInfo;
20
+ readonly 'md-iframe-refused': DeckMarkdownCodeInfo;
21
+ readonly 'md-css-refused': DeckMarkdownCodeInfo;
22
+ readonly 'md-class-refused': DeckMarkdownCodeInfo;
23
+ readonly 'md-style-refused': DeckMarkdownCodeInfo;
24
+ readonly 'md-handler-refused': DeckMarkdownCodeInfo;
25
+ readonly 'md-attr-unknown': DeckMarkdownCodeInfo;
26
+ readonly 'md-attr-unsupported': DeckMarkdownCodeInfo;
27
+ readonly 'md-attr-value': DeckMarkdownCodeInfo;
28
+ readonly 'md-attr-duplicate': DeckMarkdownCodeInfo;
29
+ readonly 'md-attr-conflict': DeckMarkdownCodeInfo;
30
+ readonly 'md-comment-unclosed': DeckMarkdownCodeInfo;
31
+ readonly 'md-element-orphan': DeckMarkdownCodeInfo;
32
+ readonly 'md-effect-approximated': DeckMarkdownCodeInfo;
33
+ readonly 'md-fragment-approximated': DeckMarkdownCodeInfo;
34
+ readonly 'md-fragment-not-steppable': DeckMarkdownCodeInfo;
35
+ readonly 'md-layout-unknown': DeckMarkdownCodeInfo;
36
+ readonly 'md-layout-extra': DeckMarkdownCodeInfo;
37
+ readonly 'md-layout-missing': DeckMarkdownCodeInfo;
38
+ readonly 'md-region-unknown': DeckMarkdownCodeInfo;
39
+ readonly 'md-region-unclosed': DeckMarkdownCodeInfo;
40
+ readonly 'md-directive-depth': DeckMarkdownCodeInfo;
41
+ readonly 'md-json': DeckMarkdownCodeInfo;
42
+ readonly 'md-json-limit': DeckMarkdownCodeInfo;
43
+ readonly 'md-tier-ignored': DeckMarkdownCodeInfo;
44
+ readonly 'md-code-unclosed': DeckMarkdownCodeInfo;
45
+ readonly 'md-code-steps': DeckMarkdownCodeInfo;
46
+ readonly 'md-code-offset-unsupported': DeckMarkdownCodeInfo;
47
+ readonly 'md-code-info': DeckMarkdownCodeInfo;
48
+ readonly 'md-math-unclosed': DeckMarkdownCodeInfo;
49
+ readonly 'md-tex-unsupported': DeckMarkdownCodeInfo;
50
+ readonly 'md-tex-limit': DeckMarkdownCodeInfo;
51
+ readonly 'md-url-refused': DeckMarkdownCodeInfo;
52
+ readonly 'md-inline-image': DeckMarkdownCodeInfo;
53
+ readonly 'md-heading-inline': DeckMarkdownCodeInfo;
54
+ readonly 'md-too-many-slides': DeckMarkdownCodeInfo;
55
+ readonly 'md-stack-start': DeckMarkdownCodeInfo;
56
+ readonly 'md-unrepresentable': DeckMarkdownCodeInfo;
57
+ }>;
58
+ export type DeckMarkdownCode = keyof typeof DECK_MARKDOWN_CODES;
59
+ /** Collects issues with the dialect's cap; errors are counted past the cap. */
60
+ export declare class IssueSink {
61
+ readonly issues: DeckDiagnostic[];
62
+ errors: number;
63
+ truncated: boolean;
64
+ readonly max: number;
65
+ constructor(max?: number);
66
+ add(issue: DeckDiagnostic): void;
67
+ report(code: DeckMarkdownCode, message: string, source: DiagnosticSource, path?: readonly PathSegment[], fix?: string): void;
68
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The diagnostics of the deck Markdown dialect: stable `md-*` codes, each
3
+ * with its severity and meaning, reported as the engine's `DeckDiagnostic`
4
+ * with a `source` line and column (1-based; columns count UTF-16 units).
5
+ */
6
+ import { createDiagnostic, } from "../schema/index.js";
7
+ import { DECK_MARKDOWN_LIMITS } from "./limits.js";
8
+ function code(severity, meaning) {
9
+ return Object.freeze({ severity, meaning });
10
+ }
11
+ /** Every code the Markdown reader and writer report. */
12
+ export const DECK_MARKDOWN_CODES = Object.freeze({
13
+ 'md-too-large': code('error', 'The document is over the byte, line or line-length bound.'),
14
+ 'md-invisible-char': code('error', 'A control, bidi or invisible formatting character (removed before parsing).'),
15
+ 'md-frontmatter-key': code('error', 'The frontmatter key is not in the closed key set.'),
16
+ 'md-frontmatter-value': code('error', 'The frontmatter value is not a scalar of the right type (no lists, maps or YAML tags).'),
17
+ 'md-frontmatter-unclosed': code('error', 'The frontmatter has no closing `---`.'),
18
+ 'md-title-missing': code('warning', 'No `title` in the frontmatter; one was derived.'),
19
+ 'md-html-refused': code('error', 'Raw HTML is refused; it renders nothing (inline tags stay text).'),
20
+ 'md-iframe-refused': code('error', 'iframes (tags and backgrounds) are refused.'),
21
+ 'md-css-refused': code('error', 'A CSS value (gradient, url()) is refused; use a preset.'),
22
+ 'md-class-refused': code('error', 'A `class` that maps to nothing is refused.'),
23
+ 'md-style-refused': code('error', 'A `style` attribute is refused.'),
24
+ 'md-handler-refused': code('error', 'An `on*` event handler attribute is refused.'),
25
+ 'md-attr-unknown': code('error', 'The attribute is not in the closed attribute set.'),
26
+ 'md-attr-unsupported': code('warning', 'A reveal.js attribute with no deck equivalent; dropped.'),
27
+ 'md-attr-value': code('error', 'The attribute value is invalid, or the comment is malformed.'),
28
+ 'md-attr-duplicate': code('error', 'The attribute is set twice for the same target.'),
29
+ 'md-attr-conflict': code('error', 'Two attributes contradict each other.'),
30
+ 'md-comment-unclosed': code('error', 'An HTML comment never closes before the end of the slide.'),
31
+ 'md-element-orphan': code('error', 'An `.element` comment has no element before it.'),
32
+ 'md-effect-approximated': code('warning', 'A reveal.js fragment effect with no exact equivalent was mapped to the nearest one.'),
33
+ 'md-fragment-approximated': code('warning', 'Per-item fragments that differ were folded into one list-level step.'),
34
+ 'md-fragment-not-steppable': code('warning', 'This layout does not reveal that element; the fragment was dropped.'),
35
+ 'md-layout-unknown': code('error', 'The `layout` is not one of the 20 layouts or a slide kind.'),
36
+ 'md-layout-extra': code('error', 'Content the layout has no field for.'),
37
+ 'md-layout-missing': code('error', 'A field the layout needs is missing from the slide.'),
38
+ 'md-region-unknown': code('error', 'The `:::` directive is unknown, or not allowed here.'),
39
+ 'md-region-unclosed': code('error', 'A `:::` directive never closes before the end of the slide.'),
40
+ 'md-directive-depth': code('error', 'Directives nest deeper than a region holding a fence.'),
41
+ 'md-json': code('error', 'A `:::` JSON body is not one valid JSON object.'),
42
+ 'md-json-limit': code('error', 'A `:::` JSON body is over a size, depth or node bound.'),
43
+ 'md-tier-ignored': code('warning', 'The document cannot choose a trust tier; the host does.'),
44
+ 'md-code-unclosed': code('error', 'A code fence never closes before the end of the slide.'),
45
+ 'md-code-steps': code('error', 'A `[1-2|4-6]` highlight step list is malformed or too long.'),
46
+ 'md-code-offset-unsupported': code('warning', 'A `[n: …]` line offset is not supported; dropped.'),
47
+ 'md-code-info': code('warning', 'Unread text after a code fence language; dropped.'),
48
+ 'md-math-unclosed': code('error', 'A `$$` block never closes before the end of the slide.'),
49
+ 'md-tex-unsupported': code('error', 'The formula uses TeX outside the supported subset.'),
50
+ 'md-tex-limit': code('error', 'The formula is over a size, nesting or node bound.'),
51
+ 'md-url-refused': code('error', 'A link or media URL fails the deck URL syntax.'),
52
+ 'md-inline-image': code('warning', 'An image inside text renders as its alt text only.'),
53
+ 'md-heading-inline': code('warning', 'Headings are plain text; inline Markdown shows literally.'),
54
+ 'md-too-many-slides': code('error', 'More than 200 slides; the rest were not read.'),
55
+ 'md-stack-start': code('error', 'A `--` vertical slide with no horizontal slide before it.'),
56
+ 'md-unrepresentable': code('warning', 'A deck field the Markdown dialect cannot express.'),
57
+ });
58
+ /** Collects issues with the dialect's cap; errors are counted past the cap. */
59
+ export class IssueSink {
60
+ issues = [];
61
+ errors = 0;
62
+ truncated = false;
63
+ max;
64
+ constructor(max = DECK_MARKDOWN_LIMITS.issues) {
65
+ this.max = max;
66
+ }
67
+ add(issue) {
68
+ if (issue.severity === 'error')
69
+ this.errors++;
70
+ if (this.issues.length >= this.max) {
71
+ this.truncated = true;
72
+ return;
73
+ }
74
+ this.issues.push(issue);
75
+ }
76
+ report(code, message, source, path = [], fix) {
77
+ this.add(createDiagnostic(code, message, path, {
78
+ severity: DECK_MARKDOWN_CODES[code].severity,
79
+ source,
80
+ ...(fix === undefined ? {} : { fix }),
81
+ }));
82
+ }
83
+ }
@@ -0,0 +1,21 @@
1
+ export interface JsonReadOptions {
2
+ /** Values this read may still produce (a document-wide budget). */
3
+ nodes: number;
4
+ maxDepth?: number;
5
+ maxLength?: number;
6
+ }
7
+ export type JsonReadResult = {
8
+ ok: true;
9
+ value: unknown;
10
+ /** `toJsonPath(path)` (relative to the body) → UTF-16 offset of the value. */
11
+ offsets: Map<string, number>;
12
+ nodes: number;
13
+ } | {
14
+ ok: false;
15
+ message: string;
16
+ offset: number;
17
+ limit: boolean;
18
+ nodes: number;
19
+ };
20
+ /** Read one JSON value from a fence body. Never throws. */
21
+ export declare function readJson(text: string, options: JsonReadOptions): JsonReadResult;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The owned bounded JSON reader for `:::` fence bodies. Strict RFC 8259
3
+ * JSON with explicit bounds (size, depth, nodes, string length), no
4
+ * prototype keys, no duplicate keys and finite numbers only. It records the
5
+ * source offset of every value by its path, so model issues inside a
6
+ * component's props point at the exact line and column.
7
+ */
8
+ import { toJsonPath } from "../schema/index.js";
9
+ import { DECK_MARKDOWN_LIMITS } from "./limits.js";
10
+ const UNSAFE = new Set(['__proto__', 'constructor', 'prototype']);
11
+ class JsonError extends Error {
12
+ offset;
13
+ limit;
14
+ constructor(message, offset, limit = false) {
15
+ super(message);
16
+ this.offset = offset;
17
+ this.limit = limit;
18
+ }
19
+ }
20
+ class Reader {
21
+ text;
22
+ maxDepth;
23
+ budget;
24
+ offsets = new Map();
25
+ i = 0;
26
+ nodes = 0;
27
+ constructor(text, budget, maxDepth) {
28
+ this.text = text;
29
+ this.budget = budget;
30
+ this.maxDepth = maxDepth;
31
+ }
32
+ fail(message, limit = false) {
33
+ throw new JsonError(message, this.i, limit);
34
+ }
35
+ space() {
36
+ while (this.i < this.text.length) {
37
+ const c = this.text.charCodeAt(this.i);
38
+ if (c === 0x20 || c === 0x0a || c === 0x09 || c === 0x0d)
39
+ this.i++;
40
+ else
41
+ break;
42
+ }
43
+ }
44
+ value(path, depth) {
45
+ this.space();
46
+ this.nodes++;
47
+ if (this.nodes > this.budget) {
48
+ this.fail(`JSON bodies hold at most ${DECK_MARKDOWN_LIMITS.jsonNodes} values per document.`, true);
49
+ }
50
+ if (depth > this.maxDepth)
51
+ this.fail(`JSON nests at most ${this.maxDepth} levels.`, true);
52
+ this.offsets.set(toJsonPath(path), this.i);
53
+ const c = this.text[this.i];
54
+ if (c === '{')
55
+ return this.object(path, depth);
56
+ if (c === '[')
57
+ return this.array(path, depth);
58
+ if (c === '"')
59
+ return this.string();
60
+ if (c === 't' && this.text.startsWith('true', this.i)) {
61
+ this.i += 4;
62
+ return true;
63
+ }
64
+ if (c === 'f' && this.text.startsWith('false', this.i)) {
65
+ this.i += 5;
66
+ return false;
67
+ }
68
+ if (c === 'n' && this.text.startsWith('null', this.i)) {
69
+ this.i += 4;
70
+ return null;
71
+ }
72
+ if (c === '-' || (c !== undefined && c >= '0' && c <= '9'))
73
+ return this.number();
74
+ return this.fail(c === undefined ? 'The JSON ends early.' : `Unexpected \`${c}\` in JSON.`);
75
+ }
76
+ object(path, depth) {
77
+ this.i++;
78
+ const out = {};
79
+ this.space();
80
+ if (this.text[this.i] === '}') {
81
+ this.i++;
82
+ return out;
83
+ }
84
+ for (;;) {
85
+ this.space();
86
+ if (this.text[this.i] !== '"')
87
+ this.fail('A JSON key must be a "string".');
88
+ const keyAt = this.i;
89
+ const key = this.string();
90
+ if (UNSAFE.has(key)) {
91
+ this.i = keyAt;
92
+ this.fail(`The key "${key}" is refused.`);
93
+ }
94
+ if (Object.hasOwn(out, key)) {
95
+ this.i = keyAt;
96
+ this.fail(`The key "${key.slice(0, 64)}" appears twice.`);
97
+ }
98
+ this.space();
99
+ if (this.text[this.i] !== ':')
100
+ this.fail('Expected `:` after a JSON key.');
101
+ this.i++;
102
+ out[key] = this.value([...path, key], depth + 1);
103
+ this.space();
104
+ const c = this.text[this.i];
105
+ if (c === ',') {
106
+ this.i++;
107
+ continue;
108
+ }
109
+ if (c === '}') {
110
+ this.i++;
111
+ return out;
112
+ }
113
+ this.fail('Expected `,` or `}` in a JSON object.');
114
+ }
115
+ }
116
+ array(path, depth) {
117
+ this.i++;
118
+ const out = [];
119
+ this.space();
120
+ if (this.text[this.i] === ']') {
121
+ this.i++;
122
+ return out;
123
+ }
124
+ for (;;) {
125
+ out.push(this.value([...path, out.length], depth + 1));
126
+ this.space();
127
+ const c = this.text[this.i];
128
+ if (c === ',') {
129
+ this.i++;
130
+ continue;
131
+ }
132
+ if (c === ']') {
133
+ this.i++;
134
+ return out;
135
+ }
136
+ this.fail('Expected `,` or `]` in a JSON array.');
137
+ }
138
+ }
139
+ string() {
140
+ const start = this.i;
141
+ this.i++;
142
+ let out = '';
143
+ let run = this.i;
144
+ while (this.i < this.text.length) {
145
+ const c = this.text.charCodeAt(this.i);
146
+ if (c === 0x22) {
147
+ out += this.text.slice(run, this.i);
148
+ this.i++;
149
+ return out;
150
+ }
151
+ if (c < 0x20)
152
+ this.fail('Control characters must be escaped in JSON strings.');
153
+ if (c === 0x5c) {
154
+ out += this.text.slice(run, this.i);
155
+ const e = this.text[this.i + 1];
156
+ const simple = {
157
+ '"': '"',
158
+ '\\': '\\',
159
+ '/': '/',
160
+ b: '\b',
161
+ f: '\f',
162
+ n: '\n',
163
+ r: '\r',
164
+ t: '\t',
165
+ };
166
+ if (e !== undefined && Object.hasOwn(simple, e)) {
167
+ out += simple[e];
168
+ this.i += 2;
169
+ }
170
+ else if (e === 'u') {
171
+ const hex = this.text.slice(this.i + 2, this.i + 6);
172
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
173
+ this.fail('A \\u escape needs four hex digits.');
174
+ out += String.fromCharCode(Number.parseInt(hex, 16));
175
+ this.i += 6;
176
+ }
177
+ else
178
+ this.fail('Invalid escape in a JSON string.');
179
+ run = this.i;
180
+ continue;
181
+ }
182
+ this.i++;
183
+ }
184
+ this.i = start;
185
+ return this.fail('A JSON string never closes.');
186
+ }
187
+ number() {
188
+ const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.i, this.i + 400));
189
+ if (!match)
190
+ this.fail('Invalid JSON number.');
191
+ const value = Number(match[0]);
192
+ if (!Number.isFinite(value))
193
+ this.fail('JSON numbers must be finite.');
194
+ this.i += match[0].length;
195
+ return value;
196
+ }
197
+ }
198
+ /** Read one JSON value from a fence body. Never throws. */
199
+ export function readJson(text, options) {
200
+ const maxLength = options.maxLength ?? DECK_MARKDOWN_LIMITS.jsonBody;
201
+ if (text.length > maxLength) {
202
+ return {
203
+ ok: false,
204
+ message: `A JSON body is at most ${maxLength} characters.`,
205
+ offset: maxLength,
206
+ limit: true,
207
+ nodes: 0,
208
+ };
209
+ }
210
+ const reader = new Reader(text, options.nodes, options.maxDepth ?? DECK_MARKDOWN_LIMITS.jsonDepth);
211
+ try {
212
+ const value = reader.value([], 0);
213
+ reader.space();
214
+ if (reader.i < text.length)
215
+ reader.fail('Unexpected text after the JSON value.');
216
+ return { ok: true, value, offsets: reader.offsets, nodes: reader.nodes };
217
+ }
218
+ catch (error) {
219
+ if (error instanceof JsonError) {
220
+ return {
221
+ ok: false,
222
+ message: error.message,
223
+ offset: error.offset,
224
+ limit: error.limit,
225
+ nodes: reader.nodes,
226
+ };
227
+ }
228
+ return {
229
+ ok: false,
230
+ message: 'The JSON could not be read.',
231
+ offset: 0,
232
+ limit: false,
233
+ nodes: reader.nodes,
234
+ };
235
+ }
236
+ }