@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,217 @@
|
|
|
1
|
+
import { safeHref } from '@texuli/core/url-policy';
|
|
2
|
+
import { hasUnsafeText } from "../core/safety.js";
|
|
3
|
+
import { fail, ok, parseBlockBase, parsePlainText } from "../core/schema/primitives.js";
|
|
4
|
+
export const EMBED_BLOCK_TYPES = ['embed-app', 'link-preview'];
|
|
5
|
+
export const EMBED_APP_MAX_BYTES = 2_400;
|
|
6
|
+
const EMBED_APP_MAX_DEPTH = 12;
|
|
7
|
+
const EMBED_APP_MAX_NODES = 2_000;
|
|
8
|
+
function plainRecord(value, path) {
|
|
9
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
10
|
+
return fail(path, 'Expected a plain object');
|
|
11
|
+
}
|
|
12
|
+
const prototype = Object.getPrototypeOf(value);
|
|
13
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
14
|
+
return fail(path, 'Object data must be plain JSON');
|
|
15
|
+
}
|
|
16
|
+
return ok(value);
|
|
17
|
+
}
|
|
18
|
+
function cloneJson(value, path, depth, ancestors, budget) {
|
|
19
|
+
budget.nodes += 1;
|
|
20
|
+
if (budget.nodes > EMBED_APP_MAX_NODES)
|
|
21
|
+
return fail(path, 'Embedded app has too many values');
|
|
22
|
+
if (depth > EMBED_APP_MAX_DEPTH)
|
|
23
|
+
return fail(path, 'Embedded app data is nested too deeply');
|
|
24
|
+
if (value === null || typeof value === 'boolean')
|
|
25
|
+
return ok(value);
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
return hasUnsafeText(value, true, true)
|
|
28
|
+
? fail(path, 'Embedded app text contains unsafe characters')
|
|
29
|
+
: ok(value);
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'number') {
|
|
32
|
+
return Number.isFinite(value) ? ok(value) : fail(path, 'Embedded app numbers must be finite');
|
|
33
|
+
}
|
|
34
|
+
if (typeof value !== 'object')
|
|
35
|
+
return fail(path, 'Embedded app must contain only JSON values');
|
|
36
|
+
if (ancestors.has(value))
|
|
37
|
+
return fail(path, 'Embedded app data must not contain cycles');
|
|
38
|
+
ancestors.add(value);
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
const output = [];
|
|
41
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
42
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
43
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
44
|
+
ancestors.delete(value);
|
|
45
|
+
return fail([...path, index], 'Embedded app arrays must contain plain values');
|
|
46
|
+
}
|
|
47
|
+
const child = cloneJson(descriptor.value, [...path, index], depth + 1, ancestors, budget);
|
|
48
|
+
if (!child.success) {
|
|
49
|
+
ancestors.delete(value);
|
|
50
|
+
return child;
|
|
51
|
+
}
|
|
52
|
+
output.push(child.data);
|
|
53
|
+
}
|
|
54
|
+
const extraKeys = Reflect.ownKeys(value).filter((key) => {
|
|
55
|
+
if (key === 'length')
|
|
56
|
+
return false;
|
|
57
|
+
if (typeof key !== 'string' || !/^\d+$/.test(key))
|
|
58
|
+
return true;
|
|
59
|
+
const index = Number(key);
|
|
60
|
+
return String(index) !== key || index >= value.length;
|
|
61
|
+
});
|
|
62
|
+
ancestors.delete(value);
|
|
63
|
+
return extraKeys.length === 0
|
|
64
|
+
? ok(output)
|
|
65
|
+
: fail(path, 'Embedded app arrays cannot have extra properties');
|
|
66
|
+
}
|
|
67
|
+
const record = plainRecord(value, path);
|
|
68
|
+
if (!record.success) {
|
|
69
|
+
ancestors.delete(value);
|
|
70
|
+
return record;
|
|
71
|
+
}
|
|
72
|
+
const output = {};
|
|
73
|
+
for (const key of Reflect.ownKeys(record.data)) {
|
|
74
|
+
if (typeof key !== 'string' ||
|
|
75
|
+
key === '__proto__' ||
|
|
76
|
+
key === 'constructor' ||
|
|
77
|
+
key === 'prototype') {
|
|
78
|
+
ancestors.delete(value);
|
|
79
|
+
return fail([...path, String(key)], 'Embedded app object has an unsafe key');
|
|
80
|
+
}
|
|
81
|
+
const descriptor = Object.getOwnPropertyDescriptor(record.data, key);
|
|
82
|
+
if (!descriptor?.enumerable || !('value' in descriptor)) {
|
|
83
|
+
ancestors.delete(value);
|
|
84
|
+
return fail([...path, key], 'Embedded app objects must contain plain data properties');
|
|
85
|
+
}
|
|
86
|
+
const child = cloneJson(descriptor.value, [...path, key], depth + 1, ancestors, budget);
|
|
87
|
+
if (!child.success) {
|
|
88
|
+
ancestors.delete(value);
|
|
89
|
+
return child;
|
|
90
|
+
}
|
|
91
|
+
output[key] = child.data;
|
|
92
|
+
}
|
|
93
|
+
ancestors.delete(value);
|
|
94
|
+
return ok(output);
|
|
95
|
+
}
|
|
96
|
+
function parseEmbedApp(record, base) {
|
|
97
|
+
const title = parsePlainText(record.title, ['title'], 80);
|
|
98
|
+
if (!title.success)
|
|
99
|
+
return title;
|
|
100
|
+
let startLabel;
|
|
101
|
+
if (record.startLabel !== undefined) {
|
|
102
|
+
const result = parsePlainText(record.startLabel, ['startLabel'], 40);
|
|
103
|
+
if (!result.success)
|
|
104
|
+
return result;
|
|
105
|
+
startLabel = result.data;
|
|
106
|
+
}
|
|
107
|
+
if (record.height !== undefined &&
|
|
108
|
+
record.height !== 'sm' &&
|
|
109
|
+
record.height !== 'md' &&
|
|
110
|
+
record.height !== 'lg') {
|
|
111
|
+
return fail(['height'], 'height must be sm, md or lg');
|
|
112
|
+
}
|
|
113
|
+
const app = cloneJson(record.app, ['app'], 1, new WeakSet(), { nodes: 0 });
|
|
114
|
+
if (!app.success)
|
|
115
|
+
return app;
|
|
116
|
+
const appRecord = plainRecord(app.data, ['app']);
|
|
117
|
+
if (!appRecord.success)
|
|
118
|
+
return appRecord;
|
|
119
|
+
if (appRecord.data.kind !== undefined && appRecord.data.kind !== 'app') {
|
|
120
|
+
return fail(['app', 'kind'], 'Only app blueprints can be embedded');
|
|
121
|
+
}
|
|
122
|
+
let serialized;
|
|
123
|
+
try {
|
|
124
|
+
serialized = JSON.stringify(appRecord.data);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return fail(['app'], 'Embedded app could not be serialized as JSON');
|
|
128
|
+
}
|
|
129
|
+
if (new TextEncoder().encode(serialized).byteLength > EMBED_APP_MAX_BYTES) {
|
|
130
|
+
return fail(['app'], `Embedded app JSON must be at most ${EMBED_APP_MAX_BYTES} bytes`);
|
|
131
|
+
}
|
|
132
|
+
if (containsNestedEmbed(appRecord.data)) {
|
|
133
|
+
return fail(['app'], 'An embedded app cannot contain another embed-app block');
|
|
134
|
+
}
|
|
135
|
+
return ok({
|
|
136
|
+
...base,
|
|
137
|
+
type: 'embed-app',
|
|
138
|
+
title: title.data,
|
|
139
|
+
...(startLabel === undefined ? {} : { startLabel }),
|
|
140
|
+
...(record.height === undefined ? {} : { height: record.height }),
|
|
141
|
+
app: appRecord.data,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function containsNestedEmbed(value) {
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return value.some(containsNestedEmbed);
|
|
147
|
+
if (value === null || typeof value !== 'object')
|
|
148
|
+
return false;
|
|
149
|
+
const record = value;
|
|
150
|
+
return record.type === 'embed-app' || Object.values(record).some(containsNestedEmbed);
|
|
151
|
+
}
|
|
152
|
+
function parseLinkPreview(record, base) {
|
|
153
|
+
if (typeof record.href !== 'string' ||
|
|
154
|
+
record.href.length > 2_048 ||
|
|
155
|
+
hasUnsafeText(record.href, false, false)) {
|
|
156
|
+
return fail(['href'], 'href must be a URL of at most 2048 characters');
|
|
157
|
+
}
|
|
158
|
+
const href = safeHref(record.href, 'untrusted');
|
|
159
|
+
if (!href)
|
|
160
|
+
return fail(['href'], 'link previews require a safe HTTPS URL');
|
|
161
|
+
let url;
|
|
162
|
+
try {
|
|
163
|
+
url = new URL(href);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return fail(['href'], 'link previews require an absolute HTTPS URL');
|
|
167
|
+
}
|
|
168
|
+
if (url.protocol !== 'https:' || url.username !== '' || url.password !== '') {
|
|
169
|
+
return fail(['href'], 'link previews require HTTPS without embedded credentials');
|
|
170
|
+
}
|
|
171
|
+
const title = parsePlainText(record.title, ['title'], 120);
|
|
172
|
+
if (!title.success)
|
|
173
|
+
return title;
|
|
174
|
+
let description;
|
|
175
|
+
if (record.description !== undefined) {
|
|
176
|
+
const parsed = parsePlainText(record.description, ['description'], 240);
|
|
177
|
+
if (!parsed.success)
|
|
178
|
+
return parsed;
|
|
179
|
+
description = parsed.data;
|
|
180
|
+
}
|
|
181
|
+
let site;
|
|
182
|
+
if (record.site !== undefined) {
|
|
183
|
+
const parsed = parsePlainText(record.site, ['site'], 80);
|
|
184
|
+
if (!parsed.success)
|
|
185
|
+
return parsed;
|
|
186
|
+
site = parsed.data;
|
|
187
|
+
}
|
|
188
|
+
return ok({
|
|
189
|
+
...base,
|
|
190
|
+
type: 'link-preview',
|
|
191
|
+
href,
|
|
192
|
+
title: title.data,
|
|
193
|
+
...(description === undefined ? {} : { description }),
|
|
194
|
+
...(site === undefined ? {} : { site }),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** Validate a link preview or app frame block before exposing it to a renderer. */
|
|
198
|
+
export function safeParseEmbedBlock(value) {
|
|
199
|
+
const object = plainRecord(value, []);
|
|
200
|
+
if (!object.success)
|
|
201
|
+
return object;
|
|
202
|
+
const record = object.data;
|
|
203
|
+
if (record.type !== 'embed-app' && record.type !== 'link-preview') {
|
|
204
|
+
return fail(['type'], 'Expected embed-app or link-preview');
|
|
205
|
+
}
|
|
206
|
+
const base = parseBlockBase(record, []);
|
|
207
|
+
if (!base.success)
|
|
208
|
+
return base;
|
|
209
|
+
return record.type === 'embed-app'
|
|
210
|
+
? parseEmbedApp(record, base.data)
|
|
211
|
+
: parseLinkPreview(record, base.data);
|
|
212
|
+
}
|
|
213
|
+
/** Renderer-facing validation result, with untrusted values kept inert on failure. */
|
|
214
|
+
export function checkEmbedBlock(value) {
|
|
215
|
+
const result = safeParseEmbedBlock(value);
|
|
216
|
+
return result.success ? { ok: true, block: result.data } : { ok: false, issues: result.issues };
|
|
217
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference path="./css-modules.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* `@texuli/blocks` — content blocks for pages, documents and slides.
|
|
4
|
+
* Core engine (schemas, limits, untrusted guard, formula, catalog) lives in
|
|
5
|
+
* `./core`. UI families land in later PLAN_001 tasks.
|
|
6
|
+
*/
|
|
7
|
+
export { CODE_BLOCK_TYPES, type CodeBlockCheck, type CodeBlockLabelOverrides, type CodeBlockLabels, type CodeBlockViewProps, type CodeFamilyBlock, type CodeFamilyBlockType, checkCodeBlock, isCodeFamilyBlock, loadCodeFamily, } from './code/index.js';
|
|
8
|
+
export * from './core/index.js';
|
|
9
|
+
export { checkDataBlock, DATA_BLOCK_TYPES, type DataBlockCheck, type DataBlockLabelOverrides, type DataBlockLabels, type DataBlockViewProps, type DataFamilyBlock, type DataFamilyBlockType, isDataFamilyBlock, loadDataFamily, } from './data/index.js';
|
|
10
|
+
export { DIAGRAM_BLOCK_TYPES, type Diagram, type DiagramBlockViewProps, type DiagramFamilyBlock, type DiagramType, isDiagramFamilyBlock, isDiagramFamilyType, loadDiagramFamily, parseDiagram, } from './diagram/index.js';
|
|
11
|
+
export { checkEmbedBlock, EMBED_APP_MAX_BYTES, EMBED_BLOCK_TYPES, type EmbedAppBlock, type EmbedBlock, type EmbedBlockCheck, type EmbedBlockLabels, type EmbedBlockViewProps, type EmbedFamilyBlock, type EmbedFamilyBlockType, type EmbedMountCleanup, type EmbedMountResult, type EmbedParseResult, type LinkPreviewBlock, loadEmbedFamily, safeParseEmbedBlock, } from './embed/index.js';
|
|
12
|
+
export { type CalculatorBlock, type ChecklistBlock, checkInteractiveBlock, evaluateCalculator, INTERACTIVE_BLOCK_TYPES, type InteractiveBlock, type InteractiveBlockCheck, type InteractiveBlockLabelOverrides, type InteractiveBlockLabels, type InteractiveBlockViewProps, type InteractiveFamilyBlock, type InteractiveFamilyBlockType, isInteractiveFamilyBlock, loadInteractiveFamily, type PollBlock, parseInteractiveBlock, type QuizBlock, } from './interactive/index.js';
|
|
13
|
+
export { checkMapBlock, isMapBlock, loadMapFamily, MAP_BASEMAPS, MAP_BLOCK_TYPES, MAP_LIMITS, type MapBasemapId, type MapBlock, type MapBlockCheck, type MapBlockLabelOverrides, type MapBlockLabels, type MapBlockViewProps, type MapPoint, parseMapBlock, } from './map/index.js';
|
|
14
|
+
export { checkMediaBlock, isMediaFamilyBlock, loadMediaFamily, MEDIA_BLOCK_TYPES, type MediaBlockCheck, type MediaBlockLabelOverrides, type MediaBlockLabels, type MediaBlockViewProps, type MediaFamilyBlock, type MediaFamilyBlockType, } from './media/index.js';
|
|
15
|
+
export { checkStructureBlock, isStructureFamilyBlock, loadStructureFamily, STRUCTURE_BLOCK_TYPES, type StructureBlockCheck, type StructureBlockLabelOverrides, type StructureBlockLabels, type StructureBlockViewProps, type StructureFamilyBlock, type StructureFamilyBlockType, } from './structure/index.js';
|
|
16
|
+
export { checkTextBlock, isTextFamilyBlock, loadTextFamily, TEXT_BLOCK_TYPES, type TextBlockCheck, type TextBlockLabelOverrides, type TextBlockLabels, type TextBlockViewProps, type TextFamilyBlock, type TextFamilyBlockType, } from './text/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/blocks` — content blocks for pages, documents and slides.
|
|
3
|
+
* Core engine (schemas, limits, untrusted guard, formula, catalog) lives in
|
|
4
|
+
* `./core`. UI families land in later PLAN_001 tasks.
|
|
5
|
+
*/
|
|
6
|
+
export { CODE_BLOCK_TYPES, checkCodeBlock, isCodeFamilyBlock, loadCodeFamily, } from "./code/index.js";
|
|
7
|
+
export * from "./core/index.js";
|
|
8
|
+
export { checkDataBlock, DATA_BLOCK_TYPES, isDataFamilyBlock, loadDataFamily, } from "./data/index.js";
|
|
9
|
+
export { DIAGRAM_BLOCK_TYPES, isDiagramFamilyBlock, isDiagramFamilyType, loadDiagramFamily, parseDiagram, } from "./diagram/index.js";
|
|
10
|
+
export { checkEmbedBlock, EMBED_APP_MAX_BYTES, EMBED_BLOCK_TYPES, loadEmbedFamily, safeParseEmbedBlock, } from "./embed/index.js";
|
|
11
|
+
export { checkInteractiveBlock, evaluateCalculator, INTERACTIVE_BLOCK_TYPES, isInteractiveFamilyBlock, loadInteractiveFamily, parseInteractiveBlock, } from "./interactive/index.js";
|
|
12
|
+
export { checkMapBlock, isMapBlock, loadMapFamily, MAP_BASEMAPS, MAP_BLOCK_TYPES, MAP_LIMITS, parseMapBlock, } from "./map/index.js";
|
|
13
|
+
export { checkMediaBlock, isMediaFamilyBlock, loadMediaFamily, MEDIA_BLOCK_TYPES, } from "./media/index.js";
|
|
14
|
+
export { checkStructureBlock, isStructureFamilyBlock, loadStructureFamily, STRUCTURE_BLOCK_TYPES, } from "./structure/index.js";
|
|
15
|
+
export { checkTextBlock, isTextFamilyBlock, loadTextFamily, TEXT_BLOCK_TYPES, } from "./text/index.js";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
3
|
+
import { type CalculatorBlock, evaluateCalculator } from './schema.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
block,
|
|
7
|
+
labels,
|
|
8
|
+
}: {
|
|
9
|
+
block: CalculatorBlock;
|
|
10
|
+
labels: InteractiveBlockLabels;
|
|
11
|
+
} = $props();
|
|
12
|
+
|
|
13
|
+
let values = $state<Record<string, number | null>>({});
|
|
14
|
+
let seeded = false;
|
|
15
|
+
$effect.pre(() => {
|
|
16
|
+
if (seeded) return;
|
|
17
|
+
values = Object.fromEntries(
|
|
18
|
+
block.inputs.map((input) => [input.name, input.default ?? input.min ?? 0])
|
|
19
|
+
);
|
|
20
|
+
seeded = true;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const evaluation = $derived(evaluateCalculator(block, values));
|
|
24
|
+
|
|
25
|
+
function onInput(name: string, event: Event): void {
|
|
26
|
+
const raw = (event.currentTarget as HTMLInputElement).value;
|
|
27
|
+
const n = Number(raw);
|
|
28
|
+
values = { ...values, [name]: Number.isFinite(n) ? n : null };
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div class="tx-block-interactive" data-tx-block="calculator" data-tone={block.tone}>
|
|
33
|
+
{#if block.title}
|
|
34
|
+
<h3 class="tx-block-interactive__title">{block.title}</h3>
|
|
35
|
+
{/if}
|
|
36
|
+
{#if block.description}
|
|
37
|
+
<p class="tx-block-interactive__prompt">{block.description}</p>
|
|
38
|
+
{/if}
|
|
39
|
+
|
|
40
|
+
<div class="tx-block-interactive__inputs">
|
|
41
|
+
{#each block.inputs as input (input.name)}
|
|
42
|
+
<label class="tx-block-interactive__field">
|
|
43
|
+
<span>{input.label}</span>
|
|
44
|
+
<input
|
|
45
|
+
type="number"
|
|
46
|
+
name={input.name}
|
|
47
|
+
value={values[input.name] ?? ''}
|
|
48
|
+
min={input.min}
|
|
49
|
+
max={input.max}
|
|
50
|
+
step={input.step ?? 'any'}
|
|
51
|
+
oninput={(event) => onInput(input.name, event)}
|
|
52
|
+
/>
|
|
53
|
+
</label>
|
|
54
|
+
{/each}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<p class="tx-block-interactive__result" aria-live="polite" data-tx-calculator-result>
|
|
58
|
+
{block.outputLabel ?? labels.calculatorResult}:
|
|
59
|
+
{evaluation.ok ? evaluation.formatted : evaluation.message}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
3
|
+
import { type CalculatorBlock } from './schema.js';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
block: CalculatorBlock;
|
|
6
|
+
labels: InteractiveBlockLabels;
|
|
7
|
+
};
|
|
8
|
+
declare const CalculatorBlock: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type CalculatorBlock = ReturnType<typeof CalculatorBlock>;
|
|
10
|
+
export default CalculatorBlock;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Checkbox } from 'texuli/choice';
|
|
3
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
4
|
+
import type { ChecklistBlock } from './schema.js';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
block,
|
|
8
|
+
labels,
|
|
9
|
+
}: {
|
|
10
|
+
block: ChecklistBlock;
|
|
11
|
+
labels: InteractiveBlockLabels;
|
|
12
|
+
} = $props();
|
|
13
|
+
|
|
14
|
+
let done = $state<Record<string, boolean>>({});
|
|
15
|
+
let seeded = false;
|
|
16
|
+
$effect.pre(() => {
|
|
17
|
+
if (seeded) return;
|
|
18
|
+
done = Object.fromEntries(block.items.map((item) => [item.id, item.done === true]));
|
|
19
|
+
seeded = true;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const completed = $derived(Object.values(done).filter(Boolean).length);
|
|
23
|
+
const total = $derived(block.items.length);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="tx-block-interactive" data-tx-block="checklist" data-tone={block.tone}>
|
|
27
|
+
{#if block.title}
|
|
28
|
+
<h3 class="tx-block-interactive__title">{block.title}</h3>
|
|
29
|
+
{/if}
|
|
30
|
+
<p class="tx-block-interactive__prompt" aria-live="polite">
|
|
31
|
+
{labels.checklistProgress}: {completed} / {total}
|
|
32
|
+
</p>
|
|
33
|
+
<div class="tx-block-interactive__options" role="group" aria-label={block.title ?? 'Checklist'}>
|
|
34
|
+
{#each block.items as item (item.id)}
|
|
35
|
+
<Checkbox
|
|
36
|
+
checked={done[item.id] === true}
|
|
37
|
+
label={item.label}
|
|
38
|
+
onCheckedChange={(next) => {
|
|
39
|
+
done = { ...done, [item.id]: next };
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
{/each}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
3
|
+
import type { ChecklistBlock } from './schema.js';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
block: ChecklistBlock;
|
|
6
|
+
labels: InteractiveBlockLabels;
|
|
7
|
+
};
|
|
8
|
+
declare const ChecklistBlock: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type ChecklistBlock = ReturnType<typeof ChecklistBlock>;
|
|
10
|
+
export default ChecklistBlock;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import CalculatorBlockView from './CalculatorBlock.svelte';
|
|
3
|
+
import ChecklistBlockView from './ChecklistBlock.svelte';
|
|
4
|
+
import { checkInteractiveBlock } from './check.js';
|
|
5
|
+
import type { InteractiveBlockViewProps } from './family.js';
|
|
6
|
+
import { resolveInteractiveBlockLabels } from './labels.js';
|
|
7
|
+
import PollBlockView from './PollBlock.svelte';
|
|
8
|
+
import QuizBlockView from './QuizBlock.svelte';
|
|
9
|
+
import './interactive.css';
|
|
10
|
+
|
|
11
|
+
let { block, labels, oninvalid }: InteractiveBlockViewProps = $props();
|
|
12
|
+
|
|
13
|
+
const check = $derived(checkInteractiveBlock(block));
|
|
14
|
+
const text = $derived(resolveInteractiveBlockLabels(labels));
|
|
15
|
+
|
|
16
|
+
$effect(() => {
|
|
17
|
+
if (!check.ok) oninvalid?.(check.issues);
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
{#if !check.ok}
|
|
22
|
+
<p class="tx-block tx-block-invalid" role="note" data-tx-block="invalid">{text.invalid}</p>
|
|
23
|
+
{:else if check.block.type === 'poll'}
|
|
24
|
+
<PollBlockView block={check.block} labels={text} />
|
|
25
|
+
{:else if check.block.type === 'quiz'}
|
|
26
|
+
<QuizBlockView block={check.block} labels={text} />
|
|
27
|
+
{:else if check.block.type === 'calculator'}
|
|
28
|
+
<CalculatorBlockView block={check.block} labels={text} />
|
|
29
|
+
{:else if check.block.type === 'checklist'}
|
|
30
|
+
<ChecklistBlockView block={check.block} labels={text} />
|
|
31
|
+
{/if}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import type { InteractiveBlockViewProps } from './family.js';
|
|
3
|
+
declare const InteractiveBlockView: import("svelte").Component<InteractiveBlockViewProps, {}, "">;
|
|
4
|
+
type InteractiveBlockView = ReturnType<typeof InteractiveBlockView>;
|
|
5
|
+
export default InteractiveBlockView;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from 'texuli/button';
|
|
3
|
+
import { Checkbox, RadioGroup } from 'texuli/choice';
|
|
4
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
5
|
+
import type { PollBlock } from './schema.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
block,
|
|
9
|
+
labels,
|
|
10
|
+
}: {
|
|
11
|
+
block: PollBlock;
|
|
12
|
+
labels: InteractiveBlockLabels;
|
|
13
|
+
} = $props();
|
|
14
|
+
|
|
15
|
+
let selected = $state<string[]>([]);
|
|
16
|
+
let radioValue = $state('');
|
|
17
|
+
let submitted = $state(false);
|
|
18
|
+
|
|
19
|
+
const multi = $derived(block.allowMultiple === true);
|
|
20
|
+
const qId = $derived(`${block.id ?? 'poll'}-q`);
|
|
21
|
+
|
|
22
|
+
function toggle(id: string, next: boolean): void {
|
|
23
|
+
if (submitted) return;
|
|
24
|
+
if (next) {
|
|
25
|
+
if (!selected.includes(id)) selected = [...selected, id];
|
|
26
|
+
} else {
|
|
27
|
+
selected = selected.filter((x) => x !== id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function submit(): void {
|
|
32
|
+
if (multi) {
|
|
33
|
+
if (selected.length === 0) return;
|
|
34
|
+
} else {
|
|
35
|
+
if (!radioValue) return;
|
|
36
|
+
selected = [radioValue];
|
|
37
|
+
}
|
|
38
|
+
submitted = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function reset(): void {
|
|
42
|
+
selected = [];
|
|
43
|
+
radioValue = '';
|
|
44
|
+
submitted = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const counts = $derived.by(() => {
|
|
48
|
+
const map = Object.fromEntries(block.options.map((o) => [o.id, 0])) as Record<string, number>;
|
|
49
|
+
if (submitted) {
|
|
50
|
+
for (const id of selected) {
|
|
51
|
+
if (map[id] !== undefined) map[id] += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return map;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const total = $derived(submitted ? Math.max(1, selected.length) : 1);
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div class="tx-block-interactive" data-tx-block="poll" data-tone={block.tone}>
|
|
61
|
+
<p class="tx-block-interactive__prompt" id={qId}>{block.question}</p>
|
|
62
|
+
|
|
63
|
+
{#if multi}
|
|
64
|
+
<div class="tx-block-interactive__options" role="group" aria-labelledby={qId}>
|
|
65
|
+
{#each block.options as option (option.id)}
|
|
66
|
+
<Checkbox
|
|
67
|
+
checked={selected.includes(option.id)}
|
|
68
|
+
disabled={submitted}
|
|
69
|
+
label={option.label}
|
|
70
|
+
onCheckedChange={(next) => toggle(option.id, next)}
|
|
71
|
+
/>
|
|
72
|
+
{/each}
|
|
73
|
+
</div>
|
|
74
|
+
{:else}
|
|
75
|
+
<RadioGroup
|
|
76
|
+
bind:value={radioValue}
|
|
77
|
+
disabled={submitted}
|
|
78
|
+
items={block.options.map((o) => ({ value: o.id, label: o.label }))}
|
|
79
|
+
aria-labelledby={qId}
|
|
80
|
+
/>
|
|
81
|
+
{/if}
|
|
82
|
+
|
|
83
|
+
<div class="tx-block-interactive__actions">
|
|
84
|
+
{#if !submitted}
|
|
85
|
+
<Button
|
|
86
|
+
type="button"
|
|
87
|
+
onclick={submit}
|
|
88
|
+
disabled={multi ? selected.length === 0 : radioValue === ''}
|
|
89
|
+
>
|
|
90
|
+
{labels.pollSubmit}
|
|
91
|
+
</Button>
|
|
92
|
+
{:else}
|
|
93
|
+
<Button type="button" variant="secondary" onclick={reset}>{labels.pollReset}</Button>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{#if submitted}
|
|
98
|
+
<div class="tx-block-interactive__results" aria-live="polite">
|
|
99
|
+
<p class="tx-block-interactive__result">{labels.pollResults}</p>
|
|
100
|
+
{#each block.options as option (option.id)}
|
|
101
|
+
{@const pct = Math.round(((counts[option.id] ?? 0) / total) * 100)}
|
|
102
|
+
<div class="tx-block-interactive__bar">
|
|
103
|
+
<span>{option.label} — {pct}%</span>
|
|
104
|
+
<div class="tx-block-interactive__bar-track">
|
|
105
|
+
<div class="tx-block-interactive__bar-fill" style:inline-size="{pct}%"></div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{/each}
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
111
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
3
|
+
import type { PollBlock } from './schema.js';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
block: PollBlock;
|
|
6
|
+
labels: InteractiveBlockLabels;
|
|
7
|
+
};
|
|
8
|
+
declare const PollBlock: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type PollBlock = ReturnType<typeof PollBlock>;
|
|
10
|
+
export default PollBlock;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from 'texuli/button';
|
|
3
|
+
import { RadioGroup } from 'texuli/choice';
|
|
4
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
5
|
+
import type { QuizBlock } from './schema.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
block,
|
|
9
|
+
labels,
|
|
10
|
+
}: {
|
|
11
|
+
block: QuizBlock;
|
|
12
|
+
labels: InteractiveBlockLabels;
|
|
13
|
+
} = $props();
|
|
14
|
+
|
|
15
|
+
let index = $state(0);
|
|
16
|
+
let choice = $state('');
|
|
17
|
+
let checked = $state(false);
|
|
18
|
+
let score = $state(0);
|
|
19
|
+
let done = $state(false);
|
|
20
|
+
|
|
21
|
+
const question = $derived(block.questions[index]);
|
|
22
|
+
const total = $derived(block.questions.length);
|
|
23
|
+
|
|
24
|
+
function checkAnswer(): void {
|
|
25
|
+
if (!question || !choice || checked) return;
|
|
26
|
+
checked = true;
|
|
27
|
+
if (choice === question.correctId) score += 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function next(): void {
|
|
31
|
+
if (!checked) return;
|
|
32
|
+
if (index + 1 >= total) {
|
|
33
|
+
done = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
index += 1;
|
|
37
|
+
choice = '';
|
|
38
|
+
checked = false;
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div class="tx-block-interactive" data-tx-block="quiz" data-tone={block.tone}>
|
|
43
|
+
{#if block.title}
|
|
44
|
+
<h3 class="tx-block-interactive__title">{block.title}</h3>
|
|
45
|
+
{/if}
|
|
46
|
+
|
|
47
|
+
{#if done}
|
|
48
|
+
<p class="tx-block-interactive__result" aria-live="polite">
|
|
49
|
+
{labels.quizScore}: {score} / {total}
|
|
50
|
+
</p>
|
|
51
|
+
{:else if question}
|
|
52
|
+
<p class="tx-block-interactive__prompt" id="{question.id}-prompt">
|
|
53
|
+
{index + 1}. {question.prompt}
|
|
54
|
+
</p>
|
|
55
|
+
<RadioGroup
|
|
56
|
+
bind:value={choice}
|
|
57
|
+
disabled={checked}
|
|
58
|
+
items={question.options.map((o) => ({ value: o.id, label: o.label }))}
|
|
59
|
+
aria-labelledby="{question.id}-prompt"
|
|
60
|
+
/>
|
|
61
|
+
<div class="tx-block-interactive__actions">
|
|
62
|
+
{#if !checked}
|
|
63
|
+
<Button type="button" onclick={checkAnswer} disabled={!choice}>{labels.quizSubmit}</Button>
|
|
64
|
+
{:else}
|
|
65
|
+
<p class="tx-block-interactive__result" aria-live="polite">
|
|
66
|
+
{choice === question.correctId ? labels.quizCorrect : labels.quizIncorrect}
|
|
67
|
+
{#if question.explanation}
|
|
68
|
+
— {question.explanation}
|
|
69
|
+
{/if}
|
|
70
|
+
</p>
|
|
71
|
+
<Button type="button" onclick={next}>{labels.quizNext}</Button>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
74
|
+
{/if}
|
|
75
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference path="../css-modules.d.ts" />
|
|
2
|
+
import type { InteractiveBlockLabels } from './labels.js';
|
|
3
|
+
import type { QuizBlock } from './schema.js';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
block: QuizBlock;
|
|
6
|
+
labels: InteractiveBlockLabels;
|
|
7
|
+
};
|
|
8
|
+
declare const QuizBlock: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type QuizBlock = ReturnType<typeof QuizBlock>;
|
|
10
|
+
export default QuizBlock;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FieldIssue } from '../core/schema/primitives.js';
|
|
2
|
+
import type { InteractiveFamilyBlock } from './engine.js';
|
|
3
|
+
export type InteractiveBlockCheck = {
|
|
4
|
+
ok: true;
|
|
5
|
+
block: InteractiveFamilyBlock;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
issues: FieldIssue[];
|
|
9
|
+
};
|
|
10
|
+
export declare function checkInteractiveBlock(value: unknown): InteractiveBlockCheck;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { parseFamilyBlock } from "../core/schema/parse-family.js";
|
|
2
|
+
import { parseInteractiveBlock } from "./schema.js";
|
|
3
|
+
export function checkInteractiveBlock(value) {
|
|
4
|
+
// Only this family's parser: the renderer does not bundle every family's rules.
|
|
5
|
+
const parsed = parseFamilyBlock(value, [parseInteractiveBlock], 'interactive');
|
|
6
|
+
if (!parsed.success)
|
|
7
|
+
return { ok: false, issues: parsed.issues };
|
|
8
|
+
return { ok: true, block: parsed.data };
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/blocks/interactive/engine` — Svelte-free interactive parsers + formula.
|
|
3
|
+
* Prefer `@texuli/blocks/formula` when only the calculator AST is needed.
|
|
4
|
+
*/
|
|
5
|
+
import type { Block, FieldIssue } from '../core/index.js';
|
|
6
|
+
import type { InteractiveBlockLabelOverrides } from './labels.js';
|
|
7
|
+
import { type InteractiveBlock, type InteractiveBlockType } from './schema.js';
|
|
8
|
+
export { type ArithmeticOp, type ComparisonOp, type Condition, evaluateCondition, evaluateFormula, FORMULA_LIMITS, type FormatFormulaOptions, type FormulaDiagnostic, type FormulaInputs, type FormulaParseResult, type FormulaUnit, formatFormulaValue, formulaJsonPath, formulaVariables, type NumberFormula, type ParseFormulaOptions, parseFormula, roundTo, } from '../core/formula/index.js';
|
|
9
|
+
export { checkInteractiveBlock, type InteractiveBlockCheck } from './check.js';
|
|
10
|
+
export { DEFAULT_INTERACTIVE_BLOCK_LABELS, type InteractiveBlockLabelOverrides, type InteractiveBlockLabels, resolveInteractiveBlockLabels, } from './labels.js';
|
|
11
|
+
export { type CalculatorBlock, type CalculatorInput, type ChecklistBlock, type ChecklistItem, evaluateCalculator, INTERACTIVE_BLOCK_TYPES, type InteractiveBlock, type InteractiveBlockType, type PollBlock, type PollOption, parseInteractiveBlock, type QuizBlock, type QuizOption, type QuizQuestion, } from './schema.js';
|
|
12
|
+
export type InteractiveFamilyBlock = InteractiveBlock;
|
|
13
|
+
export type InteractiveFamilyBlockType = InteractiveBlockType;
|
|
14
|
+
export declare function isInteractiveFamilyBlock(block: Block): block is InteractiveFamilyBlock;
|
|
15
|
+
export interface InteractiveBlockViewProps {
|
|
16
|
+
block: unknown;
|
|
17
|
+
labels?: InteractiveBlockLabelOverrides;
|
|
18
|
+
oninvalid?: (issues: FieldIssue[]) => void;
|
|
19
|
+
}
|