@texuli/blocks 0.0.0-stage → 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/code/CodeBlockView.svelte +135 -0
- package/dist/code/CodeBlockView.svelte.d.ts +5 -0
- package/dist/code/check.d.ts +15 -0
- package/dist/code/check.js +14 -0
- package/dist/code/code.css +201 -0
- package/dist/code/code.css.d.ts +1 -0
- package/dist/code/engine-entry.d.ts +10 -0
- package/dist/code/engine-entry.js +10 -0
- package/dist/code/engine.d.ts +18 -0
- package/dist/code/engine.js +18 -0
- package/dist/code/family-engine.d.ts +23 -0
- package/dist/code/family-engine.js +4 -0
- package/dist/code/family.d.ts +7 -0
- package/dist/code/family.js +5 -0
- package/dist/code/grammars.d.ts +12 -0
- package/dist/code/grammars.js +83 -0
- package/dist/code/highlight.d.ts +15 -0
- package/dist/code/highlight.js +53 -0
- package/dist/code/index.d.ts +13 -0
- package/dist/code/index.js +12 -0
- package/dist/code/labels.d.ts +27 -0
- package/dist/code/labels.js +24 -0
- package/dist/code/languages/bash.d.ts +3 -0
- package/dist/code/languages/bash.js +48 -0
- package/dist/code/languages/css.d.ts +10 -0
- package/dist/code/languages/css.js +67 -0
- package/dist/code/languages/html.d.ts +6 -0
- package/dist/code/languages/html.js +35 -0
- package/dist/code/languages/javascript.d.ts +3 -0
- package/dist/code/languages/javascript.js +113 -0
- package/dist/code/languages/json.d.ts +3 -0
- package/dist/code/languages/json.js +24 -0
- package/dist/code/languages/python.d.ts +3 -0
- package/dist/code/languages/python.js +35 -0
- package/dist/code/languages/shared.d.ts +22 -0
- package/dist/code/languages/shared.js +37 -0
- package/dist/code/languages/sql.d.ts +3 -0
- package/dist/code/languages/sql.js +32 -0
- package/dist/code/languages/yaml.d.ts +7 -0
- package/dist/code/languages/yaml.js +36 -0
- package/dist/code/renderers.d.ts +5 -0
- package/dist/code/renderers.js +5 -0
- package/dist/code/token-styles.d.ts +25 -0
- package/dist/code/token-styles.js +51 -0
- package/dist/code/tokenize-code.d.ts +17 -0
- package/dist/code/tokenize-code.js +34 -0
- package/dist/code/tokenizer.d.ts +76 -0
- package/dist/code/tokenizer.js +171 -0
- package/dist/core/catalog/entries.d.ts +10 -0
- package/dist/core/catalog/entries.js +589 -0
- package/dist/core/catalog/index.d.ts +4 -0
- package/dist/core/catalog/index.js +2 -0
- package/dist/core/catalog/types.d.ts +36 -0
- package/dist/core/catalog/types.js +1 -0
- package/dist/core/catalog/validate.d.ts +15 -0
- package/dist/core/catalog/validate.js +46 -0
- package/dist/core/formula/evaluate.d.ts +14 -0
- package/dist/core/formula/evaluate.js +181 -0
- package/dist/core/formula/index.d.ts +8 -0
- package/dist/core/formula/index.js +6 -0
- package/dist/core/formula/parse.d.ts +19 -0
- package/dist/core/formula/parse.js +452 -0
- package/dist/core/formula/types.d.ts +76 -0
- package/dist/core/formula/types.js +7 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +13 -0
- package/dist/core/limits.d.ts +27 -0
- package/dist/core/limits.js +26 -0
- package/dist/core/safety.d.ts +30 -0
- package/dist/core/safety.js +110 -0
- package/dist/core/schema/blocks.d.ts +138 -0
- package/dist/core/schema/blocks.js +181 -0
- package/dist/core/schema/code-family.d.ts +4 -0
- package/dist/core/schema/code-family.js +75 -0
- package/dist/core/schema/line-spec.d.ts +7 -0
- package/dist/core/schema/line-spec.js +19 -0
- package/dist/core/schema/media-family.d.ts +4 -0
- package/dist/core/schema/media-family.js +260 -0
- package/dist/core/schema/parse-family.d.ts +14 -0
- package/dist/core/schema/parse-family.js +37 -0
- package/dist/core/schema/primitives.d.ts +73 -0
- package/dist/core/schema/primitives.js +140 -0
- package/dist/core/schema/text-family.d.ts +4 -0
- package/dist/core/schema/text-family.js +121 -0
- package/dist/core/schema/vocabulary.d.ts +20 -0
- package/dist/core/schema/vocabulary.js +33 -0
- package/dist/core/text.d.ts +31 -0
- package/dist/core/text.js +139 -0
- package/dist/core/untrusted.d.ts +20 -0
- package/dist/core/untrusted.js +377 -0
- package/dist/css-modules.d.ts +1 -0
- package/dist/css.d.ts +1 -0
- package/dist/data/ChartBlockView.svelte +77 -0
- package/dist/data/ChartBlockView.svelte.d.ts +10 -0
- package/dist/data/DataBlockView.svelte +109 -0
- package/dist/data/DataBlockView.svelte.d.ts +5 -0
- package/dist/data/check.d.ts +10 -0
- package/dist/data/check.js +9 -0
- package/dist/data/engine.d.ts +18 -0
- package/dist/data/engine.js +10 -0
- package/dist/data/family.d.ts +6 -0
- package/dist/data/family.js +4 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +8 -0
- package/dist/data/labels.d.ts +8 -0
- package/dist/data/labels.js +8 -0
- package/dist/data/renderers.d.ts +2 -0
- package/dist/data/renderers.js +2 -0
- package/dist/data/schema.d.ts +84 -0
- package/dist/data/schema.js +385 -0
- package/dist/diagram/BoardDiagram.svelte +48 -0
- package/dist/diagram/BoardDiagram.svelte.d.ts +8 -0
- package/dist/diagram/DiagramFamily.svelte +46 -0
- package/dist/diagram/DiagramFamily.svelte.d.ts +10 -0
- package/dist/diagram/FlowDiagram.svelte +108 -0
- package/dist/diagram/FlowDiagram.svelte.d.ts +8 -0
- package/dist/diagram/TerminalDiagram.svelte +38 -0
- package/dist/diagram/TerminalDiagram.svelte.d.ts +8 -0
- package/dist/diagram/TreeDiagram.svelte +149 -0
- package/dist/diagram/TreeDiagram.svelte.d.ts +8 -0
- package/dist/diagram/diagram.css +287 -0
- package/dist/diagram/diagram.css.d.ts +1 -0
- package/dist/diagram/engine.d.ts +16 -0
- package/dist/diagram/engine.js +14 -0
- package/dist/diagram/family.d.ts +7 -0
- package/dist/diagram/family.js +5 -0
- package/dist/diagram/index.d.ts +10 -0
- package/dist/diagram/index.js +9 -0
- package/dist/diagram/layout.d.ts +26 -0
- package/dist/diagram/layout.js +165 -0
- package/dist/diagram/schema.d.ts +74 -0
- package/dist/diagram/schema.js +358 -0
- package/dist/diagram/tones.d.ts +6 -0
- package/dist/diagram/tones.js +16 -0
- package/dist/embed/EmbedApp.svelte +138 -0
- package/dist/embed/EmbedApp.svelte.d.ts +17 -0
- package/dist/embed/EmbedBlockView.svelte +32 -0
- package/dist/embed/EmbedBlockView.svelte.d.ts +5 -0
- package/dist/embed/LinkPreview.svelte +48 -0
- package/dist/embed/LinkPreview.svelte.d.ts +10 -0
- package/dist/embed/engine.d.ts +30 -0
- package/dist/embed/engine.js +17 -0
- package/dist/embed/family.d.ts +7 -0
- package/dist/embed/family.js +5 -0
- package/dist/embed/index.d.ts +5 -0
- package/dist/embed/index.js +3 -0
- package/dist/embed/renderers.d.ts +3 -0
- package/dist/embed/renderers.js +3 -0
- package/dist/embed/sandbox.d.ts +86 -0
- package/dist/embed/sandbox.js +114 -0
- package/dist/embed/schema.d.ts +30 -0
- package/dist/embed/schema.js +217 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/interactive/CalculatorBlock.svelte +61 -0
- package/dist/interactive/CalculatorBlock.svelte.d.ts +10 -0
- package/dist/interactive/ChecklistBlock.svelte +44 -0
- package/dist/interactive/ChecklistBlock.svelte.d.ts +10 -0
- package/dist/interactive/InteractiveBlockView.svelte +31 -0
- package/dist/interactive/InteractiveBlockView.svelte.d.ts +5 -0
- package/dist/interactive/PollBlock.svelte +111 -0
- package/dist/interactive/PollBlock.svelte.d.ts +10 -0
- package/dist/interactive/QuizBlock.svelte +75 -0
- package/dist/interactive/QuizBlock.svelte.d.ts +10 -0
- package/dist/interactive/check.d.ts +10 -0
- package/dist/interactive/check.js +9 -0
- package/dist/interactive/engine.d.ts +19 -0
- package/dist/interactive/engine.js +12 -0
- package/dist/interactive/family.d.ts +6 -0
- package/dist/interactive/family.js +4 -0
- package/dist/interactive/index.d.ts +13 -0
- package/dist/interactive/index.js +13 -0
- package/dist/interactive/interactive.css +97 -0
- package/dist/interactive/interactive.css.d.ts +1 -0
- package/dist/interactive/labels.d.ts +16 -0
- package/dist/interactive/labels.js +16 -0
- package/dist/interactive/renderers.d.ts +9 -0
- package/dist/interactive/renderers.js +9 -0
- package/dist/interactive/schema.d.ts +76 -0
- package/dist/interactive/schema.js +324 -0
- package/dist/map/MapBlockView.svelte +465 -0
- package/dist/map/MapBlockView.svelte.d.ts +5 -0
- package/dist/map/check.d.ts +11 -0
- package/dist/map/check.js +6 -0
- package/dist/map/engine.d.ts +19 -0
- package/dist/map/engine.js +8 -0
- package/dist/map/family.d.ts +7 -0
- package/dist/map/family.js +5 -0
- package/dist/map/index.d.ts +11 -0
- package/dist/map/index.js +10 -0
- package/dist/map/labels.d.ts +27 -0
- package/dist/map/labels.js +21 -0
- package/dist/map/renderers.d.ts +2 -0
- package/dist/map/renderers.js +2 -0
- package/dist/map/schema.d.ts +41 -0
- package/dist/map/schema.js +176 -0
- package/dist/map/view-model.d.ts +55 -0
- package/dist/map/view-model.js +168 -0
- package/dist/media/MediaBlockView.svelte +258 -0
- package/dist/media/MediaBlockView.svelte.d.ts +5 -0
- package/dist/media/check.d.ts +14 -0
- package/dist/media/check.js +13 -0
- package/dist/media/engine.d.ts +18 -0
- package/dist/media/engine.js +10 -0
- package/dist/media/family.d.ts +7 -0
- package/dist/media/family.js +5 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.js +10 -0
- package/dist/media/labels.d.ts +7 -0
- package/dist/media/labels.js +12 -0
- package/dist/media/renderers.d.ts +5 -0
- package/dist/media/renderers.js +5 -0
- package/dist/media/resolve.d.ts +6 -0
- package/dist/media/resolve.js +15 -0
- package/dist/structure/NestedBlocks.svelte +65 -0
- package/dist/structure/NestedBlocks.svelte.d.ts +11 -0
- package/dist/structure/SafeBody.svelte +13 -0
- package/dist/structure/SafeBody.svelte.d.ts +7 -0
- package/dist/structure/StructureFamily.svelte +157 -0
- package/dist/structure/StructureFamily.svelte.d.ts +5 -0
- package/dist/structure/check.d.ts +14 -0
- package/dist/structure/check.js +27 -0
- package/dist/structure/engine.d.ts +18 -0
- package/dist/structure/engine.js +18 -0
- package/dist/structure/family.d.ts +7 -0
- package/dist/structure/family.js +5 -0
- package/dist/structure/index.d.ts +11 -0
- package/dist/structure/index.js +10 -0
- package/dist/structure/labels.d.ts +11 -0
- package/dist/structure/labels.js +11 -0
- package/dist/structure/renderers.d.ts +5 -0
- package/dist/structure/renderers.js +5 -0
- package/dist/structure/schema.d.ts +71 -0
- package/dist/structure/schema.js +327 -0
- package/dist/structure/structure.css +182 -0
- package/dist/structure/structure.css.d.ts +1 -0
- package/dist/text/RichText.svelte +169 -0
- package/dist/text/RichText.svelte.d.ts +13 -0
- package/dist/text/TextBlockView.svelte +177 -0
- package/dist/text/TextBlockView.svelte.d.ts +5 -0
- package/dist/text/check.d.ts +14 -0
- package/dist/text/check.js +13 -0
- package/dist/text/engine.d.ts +17 -0
- package/dist/text/engine.js +10 -0
- package/dist/text/family.d.ts +7 -0
- package/dist/text/family.js +5 -0
- package/dist/text/index.d.ts +12 -0
- package/dist/text/index.js +11 -0
- package/dist/text/labels.d.ts +22 -0
- package/dist/text/labels.js +29 -0
- package/dist/text/markdown.d.ts +80 -0
- package/dist/text/markdown.js +361 -0
- package/dist/text/renderers.d.ts +6 -0
- package/dist/text/renderers.js +6 -0
- package/package.json +167 -4
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { classifyMediaSource } from "../safety.js";
|
|
2
|
+
import { plainObject } from "./parse-family.js";
|
|
3
|
+
import { BLOCK_CONTENT_LIMITS, fail, ok, parsePlainText, } from "./primitives.js";
|
|
4
|
+
function parseMediaDimension(value, path) {
|
|
5
|
+
if (typeof value !== 'number' ||
|
|
6
|
+
!Number.isInteger(value) ||
|
|
7
|
+
value < 1 ||
|
|
8
|
+
value > BLOCK_CONTENT_LIMITS.mediaDimension) {
|
|
9
|
+
return fail(path, `dimension must be an integer from 1 to ${BLOCK_CONTENT_LIMITS.mediaDimension}`);
|
|
10
|
+
}
|
|
11
|
+
return ok(value);
|
|
12
|
+
}
|
|
13
|
+
function parseImageSource(value, path) {
|
|
14
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
15
|
+
return fail(path, 'src must be a non-empty string');
|
|
16
|
+
}
|
|
17
|
+
const result = classifyMediaSource(value, 'image');
|
|
18
|
+
if (!result.ok)
|
|
19
|
+
return fail(path, result.message);
|
|
20
|
+
return ok(value);
|
|
21
|
+
}
|
|
22
|
+
function parseVideoSource(value, path) {
|
|
23
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
24
|
+
return fail(path, 'src must be a non-empty string');
|
|
25
|
+
}
|
|
26
|
+
const result = classifyMediaSource(value, 'video');
|
|
27
|
+
if (!result.ok)
|
|
28
|
+
return fail(path, result.message);
|
|
29
|
+
return ok(value);
|
|
30
|
+
}
|
|
31
|
+
function parseFit(value, path) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return ok(undefined);
|
|
34
|
+
if (value !== 'contain' && value !== 'cover') {
|
|
35
|
+
return fail(path, 'fit must be "contain" or "cover"');
|
|
36
|
+
}
|
|
37
|
+
return ok(value);
|
|
38
|
+
}
|
|
39
|
+
function parseOptionalBoolean(value, path) {
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return ok(undefined);
|
|
42
|
+
if (typeof value !== 'boolean')
|
|
43
|
+
return fail(path, 'must be a boolean');
|
|
44
|
+
return ok(value);
|
|
45
|
+
}
|
|
46
|
+
function parseAltRules(record, path) {
|
|
47
|
+
const decorative = parseOptionalBoolean(record.decorative, [...path, 'decorative']);
|
|
48
|
+
if (!decorative.success)
|
|
49
|
+
return decorative;
|
|
50
|
+
let alt;
|
|
51
|
+
if (record.alt !== undefined) {
|
|
52
|
+
const parsed = parsePlainText(record.alt, [...path, 'alt'], 280);
|
|
53
|
+
if (!parsed.success)
|
|
54
|
+
return parsed;
|
|
55
|
+
alt = parsed.data;
|
|
56
|
+
}
|
|
57
|
+
if (decorative.data === true && alt !== undefined) {
|
|
58
|
+
return fail([...path, 'alt'], 'A decorative image has no alt text; drop alt or decorative');
|
|
59
|
+
}
|
|
60
|
+
if (decorative.data !== true && alt === undefined) {
|
|
61
|
+
return fail([...path, 'alt'], 'Describe the image in alt, or mark it decorative: true');
|
|
62
|
+
}
|
|
63
|
+
return ok({
|
|
64
|
+
...(alt !== undefined ? { alt } : {}),
|
|
65
|
+
...(decorative.data !== undefined ? { decorative: decorative.data } : {}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function parseImageLikeFields(record, path) {
|
|
69
|
+
const src = parseImageSource(record.src, [...path, 'src']);
|
|
70
|
+
if (!src.success)
|
|
71
|
+
return src;
|
|
72
|
+
const width = parseMediaDimension(record.width, [...path, 'width']);
|
|
73
|
+
if (!width.success)
|
|
74
|
+
return width;
|
|
75
|
+
const height = parseMediaDimension(record.height, [...path, 'height']);
|
|
76
|
+
if (!height.success)
|
|
77
|
+
return height;
|
|
78
|
+
const altRules = parseAltRules(record, path);
|
|
79
|
+
if (!altRules.success)
|
|
80
|
+
return altRules;
|
|
81
|
+
const fit = parseFit(record.fit, [...path, 'fit']);
|
|
82
|
+
if (!fit.success)
|
|
83
|
+
return fit;
|
|
84
|
+
let credit;
|
|
85
|
+
if (record.credit !== undefined) {
|
|
86
|
+
const parsed = parsePlainText(record.credit, [...path, 'credit'], 120);
|
|
87
|
+
if (!parsed.success)
|
|
88
|
+
return parsed;
|
|
89
|
+
credit = parsed.data;
|
|
90
|
+
}
|
|
91
|
+
return ok({
|
|
92
|
+
src: src.data,
|
|
93
|
+
width: width.data,
|
|
94
|
+
height: height.data,
|
|
95
|
+
...altRules.data,
|
|
96
|
+
...(fit.data !== undefined ? { fit: fit.data } : {}),
|
|
97
|
+
...(credit !== undefined ? { credit } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function parseImage(record, path, base) {
|
|
101
|
+
const fields = parseImageLikeFields(record, path);
|
|
102
|
+
if (!fields.success)
|
|
103
|
+
return fields;
|
|
104
|
+
let caption;
|
|
105
|
+
if (record.caption !== undefined) {
|
|
106
|
+
const parsed = parsePlainText(record.caption, [...path, 'caption'], 280);
|
|
107
|
+
if (!parsed.success)
|
|
108
|
+
return parsed;
|
|
109
|
+
caption = parsed.data;
|
|
110
|
+
}
|
|
111
|
+
return ok({
|
|
112
|
+
...base,
|
|
113
|
+
type: 'image',
|
|
114
|
+
...fields.data,
|
|
115
|
+
...(caption !== undefined ? { caption } : {}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function parseFigure(record, path, base) {
|
|
119
|
+
const fields = parseImageLikeFields(record, path);
|
|
120
|
+
if (!fields.success)
|
|
121
|
+
return fields;
|
|
122
|
+
const caption = parsePlainText(record.caption, [...path, 'caption'], 280);
|
|
123
|
+
if (!caption.success)
|
|
124
|
+
return caption;
|
|
125
|
+
return ok({
|
|
126
|
+
...base,
|
|
127
|
+
type: 'figure',
|
|
128
|
+
...fields.data,
|
|
129
|
+
caption: caption.data,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function parseVideo(record, path, base) {
|
|
133
|
+
const src = parseVideoSource(record.src, [...path, 'src']);
|
|
134
|
+
if (!src.success)
|
|
135
|
+
return src;
|
|
136
|
+
const label = parsePlainText(record.label, [...path, 'label'], 160);
|
|
137
|
+
if (!label.success)
|
|
138
|
+
return label;
|
|
139
|
+
const width = parseMediaDimension(record.width, [...path, 'width']);
|
|
140
|
+
if (!width.success)
|
|
141
|
+
return width;
|
|
142
|
+
const height = parseMediaDimension(record.height, [...path, 'height']);
|
|
143
|
+
if (!height.success)
|
|
144
|
+
return height;
|
|
145
|
+
let poster;
|
|
146
|
+
if (record.poster !== undefined) {
|
|
147
|
+
const parsed = parseImageSource(record.poster, [...path, 'poster']);
|
|
148
|
+
if (!parsed.success)
|
|
149
|
+
return parsed;
|
|
150
|
+
poster = parsed.data;
|
|
151
|
+
}
|
|
152
|
+
let caption;
|
|
153
|
+
if (record.caption !== undefined) {
|
|
154
|
+
const parsed = parsePlainText(record.caption, [...path, 'caption'], 280);
|
|
155
|
+
if (!parsed.success)
|
|
156
|
+
return parsed;
|
|
157
|
+
caption = parsed.data;
|
|
158
|
+
}
|
|
159
|
+
const autoplay = parseOptionalBoolean(record.autoplay, [...path, 'autoplay']);
|
|
160
|
+
if (!autoplay.success)
|
|
161
|
+
return autoplay;
|
|
162
|
+
const loop = parseOptionalBoolean(record.loop, [...path, 'loop']);
|
|
163
|
+
if (!loop.success)
|
|
164
|
+
return loop;
|
|
165
|
+
const muted = parseOptionalBoolean(record.muted, [...path, 'muted']);
|
|
166
|
+
if (!muted.success)
|
|
167
|
+
return muted;
|
|
168
|
+
const controls = parseOptionalBoolean(record.controls, [...path, 'controls']);
|
|
169
|
+
if (!controls.success)
|
|
170
|
+
return controls;
|
|
171
|
+
if (autoplay.data === true && muted.data !== true) {
|
|
172
|
+
return fail([...path, 'muted'], 'An autoplaying video must be muted: true');
|
|
173
|
+
}
|
|
174
|
+
return ok({
|
|
175
|
+
...base,
|
|
176
|
+
type: 'video',
|
|
177
|
+
src: src.data,
|
|
178
|
+
label: label.data,
|
|
179
|
+
width: width.data,
|
|
180
|
+
height: height.data,
|
|
181
|
+
...(poster !== undefined ? { poster } : {}),
|
|
182
|
+
...(caption !== undefined ? { caption } : {}),
|
|
183
|
+
...(autoplay.data !== undefined ? { autoplay: autoplay.data } : {}),
|
|
184
|
+
...(loop.data !== undefined ? { loop: loop.data } : {}),
|
|
185
|
+
...(muted.data !== undefined ? { muted: muted.data } : {}),
|
|
186
|
+
...(controls.data !== undefined ? { controls: controls.data } : {}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function parseGalleryItem(value, path) {
|
|
190
|
+
const object = plainObject(value, path);
|
|
191
|
+
if (!object.success)
|
|
192
|
+
return object;
|
|
193
|
+
const fields = parseImageLikeFields(object.data, path);
|
|
194
|
+
if (!fields.success)
|
|
195
|
+
return fields;
|
|
196
|
+
let caption;
|
|
197
|
+
if (object.data.caption !== undefined) {
|
|
198
|
+
const parsed = parsePlainText(object.data.caption, [...path, 'caption'], 280);
|
|
199
|
+
if (!parsed.success)
|
|
200
|
+
return parsed;
|
|
201
|
+
caption = parsed.data;
|
|
202
|
+
}
|
|
203
|
+
// Gallery items do not carry credit (block-level only on figure/image).
|
|
204
|
+
const { credit: _credit, ...item } = fields.data;
|
|
205
|
+
return ok({
|
|
206
|
+
...item,
|
|
207
|
+
...(caption !== undefined ? { caption } : {}),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function parseGallery(record, path, base) {
|
|
211
|
+
if (!Array.isArray(record.items) || record.items.length === 0) {
|
|
212
|
+
return fail([...path, 'items'], 'gallery needs at least one item');
|
|
213
|
+
}
|
|
214
|
+
if (record.items.length > BLOCK_CONTENT_LIMITS.galleryItems) {
|
|
215
|
+
return fail([...path, 'items'], `gallery may contain at most ${BLOCK_CONTENT_LIMITS.galleryItems} items`);
|
|
216
|
+
}
|
|
217
|
+
const items = [];
|
|
218
|
+
for (let i = 0; i < record.items.length; i += 1) {
|
|
219
|
+
const item = parseGalleryItem(record.items[i], [...path, 'items', i]);
|
|
220
|
+
if (!item.success)
|
|
221
|
+
return item;
|
|
222
|
+
items.push(item.data);
|
|
223
|
+
}
|
|
224
|
+
let title;
|
|
225
|
+
if (record.title !== undefined) {
|
|
226
|
+
const parsed = parsePlainText(record.title, [...path, 'title'], 120);
|
|
227
|
+
if (!parsed.success)
|
|
228
|
+
return parsed;
|
|
229
|
+
title = parsed.data;
|
|
230
|
+
}
|
|
231
|
+
let columns;
|
|
232
|
+
if (record.columns !== undefined) {
|
|
233
|
+
if (record.columns !== 2 && record.columns !== 3 && record.columns !== 4) {
|
|
234
|
+
return fail([...path, 'columns'], 'columns must be 2, 3 or 4');
|
|
235
|
+
}
|
|
236
|
+
columns = record.columns;
|
|
237
|
+
}
|
|
238
|
+
return ok({
|
|
239
|
+
...base,
|
|
240
|
+
type: 'gallery',
|
|
241
|
+
items,
|
|
242
|
+
...(title !== undefined ? { title } : {}),
|
|
243
|
+
...(columns !== undefined ? { columns } : {}),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** Media family: image, figure, video and gallery. */
|
|
247
|
+
export function parseMediaFamilyBlock(type, record, path, base) {
|
|
248
|
+
switch (type) {
|
|
249
|
+
case 'image':
|
|
250
|
+
return parseImage(record, path, base);
|
|
251
|
+
case 'figure':
|
|
252
|
+
return parseFigure(record, path, base);
|
|
253
|
+
case 'video':
|
|
254
|
+
return parseVideo(record, path, base);
|
|
255
|
+
case 'gallery':
|
|
256
|
+
return parseGallery(record, path, base);
|
|
257
|
+
default:
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared entry for family-scoped block parsing (no family parser imported here).
|
|
3
|
+
*/
|
|
4
|
+
import { type BlockBase, type ParseResult } from './primitives.js';
|
|
5
|
+
export declare function plainObject(value: unknown, path: (string | number)[]): ParseResult<Record<string, unknown>>;
|
|
6
|
+
/** One family's parser: the block for `type`, or `null` when the type is not the family's. */
|
|
7
|
+
export type FamilyParser = (type: string, record: Record<string, unknown>, path: (string | number)[], base: BlockBase) => ParseResult<unknown> | null;
|
|
8
|
+
/**
|
|
9
|
+
* Parses one untrusted block with only the given family parsers: the plain
|
|
10
|
+
* object and the shared base fields first, then the family's own rules. A
|
|
11
|
+
* family renderer uses this instead of \`safeParseBlock\`, so it bundles its
|
|
12
|
+
* own parser and not every family's. A type no parser claims is rejected.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseFamilyBlock<T>(value: unknown, parsers: readonly FamilyParser[], family: string): ParseResult<T>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared entry for family-scoped block parsing (no family parser imported here).
|
|
3
|
+
*/
|
|
4
|
+
import { fail, ok, parseBlockBase } from "./primitives.js";
|
|
5
|
+
export function plainObject(value, path) {
|
|
6
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
7
|
+
return fail(path, 'Expected a plain block object');
|
|
8
|
+
}
|
|
9
|
+
const prototype = Object.getPrototypeOf(value);
|
|
10
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
11
|
+
return fail(path, 'Block objects must be plain JSON data');
|
|
12
|
+
}
|
|
13
|
+
return ok(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parses one untrusted block with only the given family parsers: the plain
|
|
17
|
+
* object and the shared base fields first, then the family's own rules. A
|
|
18
|
+
* family renderer uses this instead of \`safeParseBlock\`, so it bundles its
|
|
19
|
+
* own parser and not every family's. A type no parser claims is rejected.
|
|
20
|
+
*/
|
|
21
|
+
export function parseFamilyBlock(value, parsers, family) {
|
|
22
|
+
const object = plainObject(value, []);
|
|
23
|
+
if (!object.success)
|
|
24
|
+
return object;
|
|
25
|
+
const record = object.data;
|
|
26
|
+
if (typeof record.type !== 'string')
|
|
27
|
+
return fail(['type'], 'block type is required');
|
|
28
|
+
const base = parseBlockBase(record, []);
|
|
29
|
+
if (!base.success)
|
|
30
|
+
return base;
|
|
31
|
+
for (const parse of parsers) {
|
|
32
|
+
const parsed = parse(record.type, record, [], base.data);
|
|
33
|
+
if (parsed)
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
return fail(['type'], `"${record.type}" is not a ${family} block`);
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type BlockReveal, type BlockTone } from './vocabulary.js';
|
|
2
|
+
/**
|
|
3
|
+
* Content limits per block. They follow layout limits so a block always fits a
|
|
4
|
+
* slide, a page section or a doc. Owned validators — no third-party schema lib.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BLOCK_CONTENT_LIMITS: Readonly<{
|
|
7
|
+
markdown: 4000;
|
|
8
|
+
shortMarkdown: 600;
|
|
9
|
+
listItems: 12;
|
|
10
|
+
statItems: 6;
|
|
11
|
+
tableColumns: 8;
|
|
12
|
+
tableRows: 50;
|
|
13
|
+
timelineEvents: 12;
|
|
14
|
+
steps: 8;
|
|
15
|
+
comparisonColumns: 4;
|
|
16
|
+
comparisonRows: 8;
|
|
17
|
+
cards: 12;
|
|
18
|
+
people: 24;
|
|
19
|
+
ctaActions: 3;
|
|
20
|
+
columns: 4;
|
|
21
|
+
blocksPerColumn: 12;
|
|
22
|
+
containerDepth: 3;
|
|
23
|
+
tabPanels: 6;
|
|
24
|
+
pollOptions: 8;
|
|
25
|
+
formFields: 20;
|
|
26
|
+
formTextLength: 4000;
|
|
27
|
+
code: 20000;
|
|
28
|
+
codeLines: 400;
|
|
29
|
+
/** Highlight steps per code block, characters per line spec, caption length. */
|
|
30
|
+
codeHighlightSteps: 16;
|
|
31
|
+
codeHighlightSpec: 120;
|
|
32
|
+
codeCaption: 200;
|
|
33
|
+
mediaDimension: 4096;
|
|
34
|
+
galleryItems: 12;
|
|
35
|
+
href: 2048;
|
|
36
|
+
heading: 200;
|
|
37
|
+
plain: 500;
|
|
38
|
+
}>;
|
|
39
|
+
/** Blueprint-style JSONPath, e.g. `$[0]["text"]`. */
|
|
40
|
+
export declare function toJsonPath(path: readonly (string | number)[]): string;
|
|
41
|
+
export interface FieldIssue {
|
|
42
|
+
path: (string | number)[];
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
export type ParseOk<T> = {
|
|
46
|
+
success: true;
|
|
47
|
+
data: T;
|
|
48
|
+
issues: [];
|
|
49
|
+
};
|
|
50
|
+
export type ParseFail = {
|
|
51
|
+
success: false;
|
|
52
|
+
data: null;
|
|
53
|
+
issues: FieldIssue[];
|
|
54
|
+
};
|
|
55
|
+
export type ParseResult<T> = ParseOk<T> | ParseFail;
|
|
56
|
+
export declare function ok<T>(data: T): ParseOk<T>;
|
|
57
|
+
export declare function fail(path: (string | number)[], message: string): ParseFail;
|
|
58
|
+
export declare function failMany(issues: FieldIssue[]): ParseFail;
|
|
59
|
+
export declare function parseBlockId(value: unknown, path: (string | number)[]): ParseResult<string | undefined>;
|
|
60
|
+
export declare function parseStep(value: unknown, path: (string | number)[]): ParseResult<number | undefined>;
|
|
61
|
+
export declare function parseTone(value: unknown, path: (string | number)[]): ParseResult<BlockTone | undefined>;
|
|
62
|
+
export declare function parseReveal(value: unknown, path: (string | number)[]): ParseResult<BlockReveal | undefined>;
|
|
63
|
+
/** One line of plain text: no line breaks, control or bidi override characters. */
|
|
64
|
+
export declare function parsePlainText(value: unknown, path: (string | number)[], max: number): ParseResult<string>;
|
|
65
|
+
/** Multiline markdown-ish body: newlines allowed; tabs only when allowTabs. */
|
|
66
|
+
export declare function parseMultilineText(value: unknown, path: (string | number)[], max: number, allowTabs?: boolean): ParseResult<string>;
|
|
67
|
+
export interface BlockBase {
|
|
68
|
+
id?: string;
|
|
69
|
+
step?: number;
|
|
70
|
+
reveal?: BlockReveal;
|
|
71
|
+
tone?: BlockTone;
|
|
72
|
+
}
|
|
73
|
+
export declare function parseBlockBase(record: Record<string, unknown>, path: (string | number)[]): ParseResult<BlockBase>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { hasUnsafeText } from "../safety.js";
|
|
2
|
+
import { isBlockReveal, isBlockTone } from "./vocabulary.js";
|
|
3
|
+
/**
|
|
4
|
+
* Content limits per block. They follow layout limits so a block always fits a
|
|
5
|
+
* slide, a page section or a doc. Owned validators — no third-party schema lib.
|
|
6
|
+
*/
|
|
7
|
+
export const BLOCK_CONTENT_LIMITS = Object.freeze({
|
|
8
|
+
markdown: 4_000,
|
|
9
|
+
shortMarkdown: 600,
|
|
10
|
+
listItems: 12,
|
|
11
|
+
statItems: 6,
|
|
12
|
+
tableColumns: 8,
|
|
13
|
+
tableRows: 50,
|
|
14
|
+
timelineEvents: 12,
|
|
15
|
+
steps: 8,
|
|
16
|
+
comparisonColumns: 4,
|
|
17
|
+
comparisonRows: 8,
|
|
18
|
+
cards: 12,
|
|
19
|
+
people: 24,
|
|
20
|
+
ctaActions: 3,
|
|
21
|
+
columns: 4,
|
|
22
|
+
blocksPerColumn: 12,
|
|
23
|
+
containerDepth: 3,
|
|
24
|
+
tabPanels: 6,
|
|
25
|
+
pollOptions: 8,
|
|
26
|
+
formFields: 20,
|
|
27
|
+
formTextLength: 4_000,
|
|
28
|
+
code: 20_000,
|
|
29
|
+
codeLines: 400,
|
|
30
|
+
/** Highlight steps per code block, characters per line spec, caption length. */
|
|
31
|
+
codeHighlightSteps: 16,
|
|
32
|
+
codeHighlightSpec: 120,
|
|
33
|
+
codeCaption: 200,
|
|
34
|
+
mediaDimension: 4_096,
|
|
35
|
+
galleryItems: 12,
|
|
36
|
+
href: 2_048,
|
|
37
|
+
heading: 200,
|
|
38
|
+
plain: 500,
|
|
39
|
+
});
|
|
40
|
+
/** Blueprint-style JSONPath, e.g. `$[0]["text"]`. */
|
|
41
|
+
export function toJsonPath(path) {
|
|
42
|
+
let result = '$';
|
|
43
|
+
for (const segment of path) {
|
|
44
|
+
result += typeof segment === 'number' ? `[${segment}]` : `[${JSON.stringify(segment)}]`;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export function ok(data) {
|
|
49
|
+
return { success: true, data, issues: [] };
|
|
50
|
+
}
|
|
51
|
+
export function fail(path, message) {
|
|
52
|
+
return { success: false, data: null, issues: [{ path, message }] };
|
|
53
|
+
}
|
|
54
|
+
export function failMany(issues) {
|
|
55
|
+
return { success: false, data: null, issues };
|
|
56
|
+
}
|
|
57
|
+
/** A stable block id: deep links and interactive state keys use it. */
|
|
58
|
+
const BLOCK_ID = /^[a-z][a-z0-9-]{0,47}$/;
|
|
59
|
+
export function parseBlockId(value, path) {
|
|
60
|
+
if (value === undefined)
|
|
61
|
+
return ok(undefined);
|
|
62
|
+
if (typeof value !== 'string' || !BLOCK_ID.test(value)) {
|
|
63
|
+
return fail(path, 'Use 1–48 lowercase letters, digits or hyphens, starting with a letter');
|
|
64
|
+
}
|
|
65
|
+
return ok(value);
|
|
66
|
+
}
|
|
67
|
+
export function parseStep(value, path) {
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
return ok(undefined);
|
|
70
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 32) {
|
|
71
|
+
return fail(path, 'step must be an integer from 0 to 32');
|
|
72
|
+
}
|
|
73
|
+
return ok(value);
|
|
74
|
+
}
|
|
75
|
+
export function parseTone(value, path) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
return ok(undefined);
|
|
78
|
+
if (!isBlockTone(value))
|
|
79
|
+
return fail(path, 'tone must be a block tone token');
|
|
80
|
+
return ok(value);
|
|
81
|
+
}
|
|
82
|
+
export function parseReveal(value, path) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return ok(undefined);
|
|
85
|
+
if (!isBlockReveal(value))
|
|
86
|
+
return fail(path, 'reveal must be a known animation token');
|
|
87
|
+
return ok(value);
|
|
88
|
+
}
|
|
89
|
+
/** One line of plain text: no line breaks, control or bidi override characters. */
|
|
90
|
+
export function parsePlainText(value, path, max) {
|
|
91
|
+
if (typeof value !== 'string')
|
|
92
|
+
return fail(path, 'Expected a string');
|
|
93
|
+
if (value.length === 0)
|
|
94
|
+
return fail(path, 'Text must not be empty');
|
|
95
|
+
if ([...value].length > max)
|
|
96
|
+
return fail(path, `Text must be at most ${max} characters`);
|
|
97
|
+
if (hasUnsafeText(value, false, false)) {
|
|
98
|
+
return fail(path, 'Text contains control or bidi override characters');
|
|
99
|
+
}
|
|
100
|
+
if (/[\r\n]/.test(value))
|
|
101
|
+
return fail(path, 'Plain text must be a single line');
|
|
102
|
+
return ok(value);
|
|
103
|
+
}
|
|
104
|
+
/** Multiline markdown-ish body: newlines allowed; tabs only when allowTabs. */
|
|
105
|
+
export function parseMultilineText(value, path, max, allowTabs = false) {
|
|
106
|
+
if (typeof value !== 'string')
|
|
107
|
+
return fail(path, 'Expected a string');
|
|
108
|
+
if (value.length === 0)
|
|
109
|
+
return fail(path, 'Text must not be empty');
|
|
110
|
+
if ([...value].length > max)
|
|
111
|
+
return fail(path, `Text must be at most ${max} characters`);
|
|
112
|
+
if (hasUnsafeText(value, true, allowTabs)) {
|
|
113
|
+
return fail(path, 'Text contains control or bidi override characters');
|
|
114
|
+
}
|
|
115
|
+
return ok(value);
|
|
116
|
+
}
|
|
117
|
+
export function parseBlockBase(record, path) {
|
|
118
|
+
const id = parseBlockId(record.id, [...path, 'id']);
|
|
119
|
+
if (!id.success)
|
|
120
|
+
return id;
|
|
121
|
+
const step = parseStep(record.step, [...path, 'step']);
|
|
122
|
+
if (!step.success)
|
|
123
|
+
return step;
|
|
124
|
+
const reveal = parseReveal(record.reveal, [...path, 'reveal']);
|
|
125
|
+
if (!reveal.success)
|
|
126
|
+
return reveal;
|
|
127
|
+
const tone = parseTone(record.tone, [...path, 'tone']);
|
|
128
|
+
if (!tone.success)
|
|
129
|
+
return tone;
|
|
130
|
+
const base = {};
|
|
131
|
+
if (id.data !== undefined)
|
|
132
|
+
base.id = id.data;
|
|
133
|
+
if (step.data !== undefined)
|
|
134
|
+
base.step = step.data;
|
|
135
|
+
if (reveal.data !== undefined)
|
|
136
|
+
base.reveal = reveal.data;
|
|
137
|
+
if (tone.data !== undefined)
|
|
138
|
+
base.tone = tone.data;
|
|
139
|
+
return ok(base);
|
|
140
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CalloutBlock, HeadingBlock, ListBlock, QuoteBlock, TextBlock } from './blocks.js';
|
|
2
|
+
import { type BlockBase, type ParseResult } from './primitives.js';
|
|
3
|
+
/** Text family: heading, text, list, quote and callout. */
|
|
4
|
+
export declare function parseTextFamilyBlock(type: string, record: Record<string, unknown>, path: (string | number)[], base: BlockBase): ParseResult<HeadingBlock | TextBlock | ListBlock | QuoteBlock | CalloutBlock> | null;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { plainObject } from "./parse-family.js";
|
|
2
|
+
import { BLOCK_CONTENT_LIMITS, fail, ok, parseMultilineText, parsePlainText, } from "./primitives.js";
|
|
3
|
+
function parseHeading(record, path, base) {
|
|
4
|
+
const level = record.level;
|
|
5
|
+
if (level !== 1 && level !== 2 && level !== 3 && level !== 4) {
|
|
6
|
+
return fail([...path, 'level'], 'heading level must be 1–4');
|
|
7
|
+
}
|
|
8
|
+
const text = parsePlainText(record.text, [...path, 'text'], BLOCK_CONTENT_LIMITS.heading);
|
|
9
|
+
if (!text.success)
|
|
10
|
+
return text;
|
|
11
|
+
let eyebrow;
|
|
12
|
+
if (record.eyebrow !== undefined) {
|
|
13
|
+
const parsed = parsePlainText(record.eyebrow, [...path, 'eyebrow'], BLOCK_CONTENT_LIMITS.plain);
|
|
14
|
+
if (!parsed.success)
|
|
15
|
+
return parsed;
|
|
16
|
+
eyebrow = parsed.data;
|
|
17
|
+
}
|
|
18
|
+
let subtitle;
|
|
19
|
+
if (record.subtitle !== undefined) {
|
|
20
|
+
const parsed = parsePlainText(record.subtitle, [...path, 'subtitle'], BLOCK_CONTENT_LIMITS.plain);
|
|
21
|
+
if (!parsed.success)
|
|
22
|
+
return parsed;
|
|
23
|
+
subtitle = parsed.data;
|
|
24
|
+
}
|
|
25
|
+
return ok({
|
|
26
|
+
...base,
|
|
27
|
+
type: 'heading',
|
|
28
|
+
level,
|
|
29
|
+
text: text.data,
|
|
30
|
+
...(eyebrow !== undefined ? { eyebrow } : {}),
|
|
31
|
+
...(subtitle !== undefined ? { subtitle } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function parseTextBlock(record, path, base) {
|
|
35
|
+
const markdown = parseMultilineText(record.markdown, [...path, 'markdown'], BLOCK_CONTENT_LIMITS.markdown);
|
|
36
|
+
if (!markdown.success)
|
|
37
|
+
return markdown;
|
|
38
|
+
return ok({ ...base, type: 'text', markdown: markdown.data });
|
|
39
|
+
}
|
|
40
|
+
function parseList(record, path, base) {
|
|
41
|
+
if (!Array.isArray(record.items) || record.items.length === 0) {
|
|
42
|
+
return fail([...path, 'items'], 'list needs at least one item');
|
|
43
|
+
}
|
|
44
|
+
if (record.items.length > BLOCK_CONTENT_LIMITS.listItems) {
|
|
45
|
+
return fail([...path, 'items'], `list may contain at most ${BLOCK_CONTENT_LIMITS.listItems} items`);
|
|
46
|
+
}
|
|
47
|
+
const items = [];
|
|
48
|
+
for (let i = 0; i < record.items.length; i += 1) {
|
|
49
|
+
const itemPath = [...path, 'items', i];
|
|
50
|
+
const item = plainObject(record.items[i], itemPath);
|
|
51
|
+
if (!item.success)
|
|
52
|
+
return item;
|
|
53
|
+
const text = parseMultilineText(item.data.text, [...itemPath, 'text'], BLOCK_CONTENT_LIMITS.shortMarkdown);
|
|
54
|
+
if (!text.success)
|
|
55
|
+
return text;
|
|
56
|
+
items.push({ text: text.data });
|
|
57
|
+
}
|
|
58
|
+
const ordered = record.ordered === true ? true : record.ordered === false ? false : undefined;
|
|
59
|
+
if (record.ordered !== undefined && ordered === undefined) {
|
|
60
|
+
return fail([...path, 'ordered'], 'ordered must be a boolean');
|
|
61
|
+
}
|
|
62
|
+
return ok({
|
|
63
|
+
...base,
|
|
64
|
+
type: 'list',
|
|
65
|
+
items,
|
|
66
|
+
...(ordered !== undefined ? { ordered } : {}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function parseQuote(record, path, base) {
|
|
70
|
+
const quote = parseMultilineText(record.quote, [...path, 'quote'], BLOCK_CONTENT_LIMITS.shortMarkdown);
|
|
71
|
+
if (!quote.success)
|
|
72
|
+
return quote;
|
|
73
|
+
let cite;
|
|
74
|
+
if (record.cite !== undefined) {
|
|
75
|
+
const parsed = parsePlainText(record.cite, [...path, 'cite'], BLOCK_CONTENT_LIMITS.plain);
|
|
76
|
+
if (!parsed.success)
|
|
77
|
+
return parsed;
|
|
78
|
+
cite = parsed.data;
|
|
79
|
+
}
|
|
80
|
+
return ok({
|
|
81
|
+
...base,
|
|
82
|
+
type: 'quote',
|
|
83
|
+
quote: quote.data,
|
|
84
|
+
...(cite !== undefined ? { cite } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function parseCallout(record, path, base) {
|
|
88
|
+
const body = parseMultilineText(record.body, [...path, 'body'], BLOCK_CONTENT_LIMITS.shortMarkdown);
|
|
89
|
+
if (!body.success)
|
|
90
|
+
return body;
|
|
91
|
+
let title;
|
|
92
|
+
if (record.title !== undefined) {
|
|
93
|
+
const parsed = parsePlainText(record.title, [...path, 'title'], BLOCK_CONTENT_LIMITS.plain);
|
|
94
|
+
if (!parsed.success)
|
|
95
|
+
return parsed;
|
|
96
|
+
title = parsed.data;
|
|
97
|
+
}
|
|
98
|
+
return ok({
|
|
99
|
+
...base,
|
|
100
|
+
type: 'callout',
|
|
101
|
+
body: body.data,
|
|
102
|
+
...(title !== undefined ? { title } : {}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Text family: heading, text, list, quote and callout. */
|
|
106
|
+
export function parseTextFamilyBlock(type, record, path, base) {
|
|
107
|
+
switch (type) {
|
|
108
|
+
case 'heading':
|
|
109
|
+
return parseHeading(record, path, base);
|
|
110
|
+
case 'text':
|
|
111
|
+
return parseTextBlock(record, path, base);
|
|
112
|
+
case 'list':
|
|
113
|
+
return parseList(record, path, base);
|
|
114
|
+
case 'quote':
|
|
115
|
+
return parseQuote(record, path, base);
|
|
116
|
+
case 'callout':
|
|
117
|
+
return parseCallout(record, path, base);
|
|
118
|
+
default:
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colors in block data are tones, never CSS, hex values or style strings.
|
|
3
|
+
* Renderers map each tone to theme tokens so contrast holds in every theme.
|
|
4
|
+
*/
|
|
5
|
+
export declare const BLOCK_TONES: readonly ["neutral", "accent", "info", "success", "warning", "danger"];
|
|
6
|
+
export type BlockTone = (typeof BLOCK_TONES)[number];
|
|
7
|
+
/**
|
|
8
|
+
* Render families. Each one is a lazy UI subpath so a document downloads only
|
|
9
|
+
* the families its blocks use.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BLOCK_FAMILIES: readonly ["text", "media", "data", "structure", "code", "diagram", "interactive", "map", "embed"];
|
|
12
|
+
export type BlockFamily = (typeof BLOCK_FAMILIES)[number];
|
|
13
|
+
/** True when `value` is one of the block tones. */
|
|
14
|
+
export declare function isBlockTone(value: unknown): value is BlockTone;
|
|
15
|
+
/** True when `value` is one of the render families. */
|
|
16
|
+
export declare function isBlockFamily(value: unknown): value is BlockFamily;
|
|
17
|
+
/** Fragment order inside a slide; pages and docs ignore it. */
|
|
18
|
+
export declare const BLOCK_REVEALS: readonly ["fade", "fade-up", "grow", "highlight", "strike"];
|
|
19
|
+
export type BlockReveal = (typeof BLOCK_REVEALS)[number];
|
|
20
|
+
export declare function isBlockReveal(value: unknown): value is BlockReveal;
|