@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,33 @@
|
|
|
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 const BLOCK_TONES = ['neutral', 'accent', 'info', 'success', 'warning', 'danger'];
|
|
6
|
+
/**
|
|
7
|
+
* Render families. Each one is a lazy UI subpath so a document downloads only
|
|
8
|
+
* the families its blocks use.
|
|
9
|
+
*/
|
|
10
|
+
export const BLOCK_FAMILIES = [
|
|
11
|
+
'text',
|
|
12
|
+
'media',
|
|
13
|
+
'data',
|
|
14
|
+
'structure',
|
|
15
|
+
'code',
|
|
16
|
+
'diagram',
|
|
17
|
+
'interactive',
|
|
18
|
+
'map',
|
|
19
|
+
'embed',
|
|
20
|
+
];
|
|
21
|
+
/** True when `value` is one of the block tones. */
|
|
22
|
+
export function isBlockTone(value) {
|
|
23
|
+
return typeof value === 'string' && BLOCK_TONES.includes(value);
|
|
24
|
+
}
|
|
25
|
+
/** True when `value` is one of the render families. */
|
|
26
|
+
export function isBlockFamily(value) {
|
|
27
|
+
return typeof value === 'string' && BLOCK_FAMILIES.includes(value);
|
|
28
|
+
}
|
|
29
|
+
/** Fragment order inside a slide; pages and docs ignore it. */
|
|
30
|
+
export const BLOCK_REVEALS = ['fade', 'fade-up', 'grow', 'highlight', 'strike'];
|
|
31
|
+
export function isBlockReveal(value) {
|
|
32
|
+
return typeof value === 'string' && BLOCK_REVEALS.includes(value);
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Block } from './schema/blocks.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure text helpers over block trees (no DOM). Used by docs (TOC), decks
|
|
4
|
+
* (notes) and authoring (word counts). Markdown is stripped lightly without a
|
|
5
|
+
* full parser — later tasks may plug a shared markdown texel.
|
|
6
|
+
*/
|
|
7
|
+
export interface WalkedBlock {
|
|
8
|
+
block: Block;
|
|
9
|
+
path: readonly (string | number)[];
|
|
10
|
+
jsonPath: string;
|
|
11
|
+
depth: number;
|
|
12
|
+
}
|
|
13
|
+
/** Depth-first walk of every block, including nested columns. */
|
|
14
|
+
export declare function walkBlocks(blocks: readonly Block[]): WalkedBlock[];
|
|
15
|
+
/** Stable slug for a heading label. */
|
|
16
|
+
export declare function slugifyHeading(label: string): string;
|
|
17
|
+
/** Unique slug among `used`, appending `-2`, `-3`, … when needed. */
|
|
18
|
+
export declare function uniqueSlug(label: string, used: ReadonlySet<string>): string;
|
|
19
|
+
/** Strip a few markdown markers for plain-text summaries (not a full parser). */
|
|
20
|
+
export declare function stripLightMarkdown(source: string): string;
|
|
21
|
+
/** Plain text of one block (no children of containers). */
|
|
22
|
+
export declare function blockText(block: Block): string;
|
|
23
|
+
/** Concatenated plain text of a block list (depth-first). */
|
|
24
|
+
export declare function documentText(blocks: readonly Block[]): string;
|
|
25
|
+
/** Heading TOC entries from a block list. */
|
|
26
|
+
export declare function headingOutline(blocks: readonly Block[]): {
|
|
27
|
+
level: number;
|
|
28
|
+
text: string;
|
|
29
|
+
id: string;
|
|
30
|
+
path: readonly (string | number)[];
|
|
31
|
+
}[];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { forEachBlock, toJsonPath } from "./schema/blocks.js";
|
|
2
|
+
/** Depth-first walk of every block, including nested columns. */
|
|
3
|
+
export function walkBlocks(blocks) {
|
|
4
|
+
const out = [];
|
|
5
|
+
forEachBlock(blocks, (block, path) => {
|
|
6
|
+
out.push({
|
|
7
|
+
block,
|
|
8
|
+
path,
|
|
9
|
+
jsonPath: toJsonPath(path),
|
|
10
|
+
depth: path.filter((segment) => typeof segment === 'string').length,
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
/** Stable slug for a heading label. */
|
|
16
|
+
export function slugifyHeading(label) {
|
|
17
|
+
return (label
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.normalize('NFKD')
|
|
20
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
21
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
22
|
+
.replace(/^-+|-+$/g, '') || 'section');
|
|
23
|
+
}
|
|
24
|
+
/** Unique slug among `used`, appending `-2`, `-3`, … when needed. */
|
|
25
|
+
export function uniqueSlug(label, used) {
|
|
26
|
+
const base = slugifyHeading(label);
|
|
27
|
+
if (!used.has(base))
|
|
28
|
+
return base;
|
|
29
|
+
let n = 2;
|
|
30
|
+
while (used.has(`${base}-${n}`))
|
|
31
|
+
n += 1;
|
|
32
|
+
return `${base}-${n}`;
|
|
33
|
+
}
|
|
34
|
+
function joinParts(parts) {
|
|
35
|
+
return parts
|
|
36
|
+
.map((part) => part.replace(/\s+/g, ' ').trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(' ')
|
|
39
|
+
.trim();
|
|
40
|
+
}
|
|
41
|
+
/** Strip a few markdown markers for plain-text summaries (not a full parser). */
|
|
42
|
+
export function stripLightMarkdown(source) {
|
|
43
|
+
return source
|
|
44
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
45
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
46
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
47
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
48
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
49
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim();
|
|
52
|
+
}
|
|
53
|
+
/** Plain text of one block (no children of containers). */
|
|
54
|
+
export function blockText(block) {
|
|
55
|
+
switch (block.type) {
|
|
56
|
+
case 'heading':
|
|
57
|
+
return joinParts([block.eyebrow ?? '', block.text, block.subtitle ?? '']);
|
|
58
|
+
case 'text':
|
|
59
|
+
return stripLightMarkdown(block.markdown);
|
|
60
|
+
case 'list':
|
|
61
|
+
return block.items.map((item) => stripLightMarkdown(item.text)).join('\n');
|
|
62
|
+
case 'quote':
|
|
63
|
+
return joinParts([stripLightMarkdown(block.quote), block.cite ?? '']);
|
|
64
|
+
case 'callout':
|
|
65
|
+
return joinParts([block.title ?? '', stripLightMarkdown(block.body)]);
|
|
66
|
+
case 'code':
|
|
67
|
+
return block.caption ? `${block.caption.trim()}\n${block.code}` : block.code;
|
|
68
|
+
case 'cta':
|
|
69
|
+
return block.actions.map((action) => action.label).join(' ');
|
|
70
|
+
case 'divider':
|
|
71
|
+
return '';
|
|
72
|
+
case 'columns':
|
|
73
|
+
return '';
|
|
74
|
+
case 'tabs':
|
|
75
|
+
return joinParts([block.label, ...block.panels.map((panel) => panel.label)]);
|
|
76
|
+
case 'accordion':
|
|
77
|
+
return joinParts([
|
|
78
|
+
block.label ?? '',
|
|
79
|
+
...block.items.map((item) => joinParts([item.title, item.body ?? ''])),
|
|
80
|
+
]);
|
|
81
|
+
case 'cards':
|
|
82
|
+
return joinParts([
|
|
83
|
+
block.title ?? '',
|
|
84
|
+
...block.items.map((item) => joinParts([item.eyebrow ?? '', item.title, item.body ?? ''])),
|
|
85
|
+
]);
|
|
86
|
+
case 'grid':
|
|
87
|
+
return joinParts([
|
|
88
|
+
block.title ?? '',
|
|
89
|
+
...block.cells.map((cell) => joinParts([cell.title ?? '', cell.body ?? ''])),
|
|
90
|
+
]);
|
|
91
|
+
case 'image':
|
|
92
|
+
return joinParts([
|
|
93
|
+
block.decorative ? '' : (block.alt ?? ''),
|
|
94
|
+
block.caption ?? '',
|
|
95
|
+
block.credit ?? '',
|
|
96
|
+
]);
|
|
97
|
+
case 'figure':
|
|
98
|
+
return joinParts([
|
|
99
|
+
block.decorative ? '' : (block.alt ?? ''),
|
|
100
|
+
block.caption,
|
|
101
|
+
block.credit ?? '',
|
|
102
|
+
]);
|
|
103
|
+
case 'video':
|
|
104
|
+
return joinParts([block.label, block.caption ?? '']);
|
|
105
|
+
case 'gallery':
|
|
106
|
+
return joinParts([
|
|
107
|
+
block.title ?? '',
|
|
108
|
+
...block.items.map((item) => joinParts([item.decorative ? '' : (item.alt ?? ''), item.caption ?? ''])),
|
|
109
|
+
]);
|
|
110
|
+
default:
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Concatenated plain text of a block list (depth-first). */
|
|
115
|
+
export function documentText(blocks) {
|
|
116
|
+
return walkBlocks(blocks)
|
|
117
|
+
.map((entry) => blockText(entry.block))
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join('\n\n')
|
|
120
|
+
.trim();
|
|
121
|
+
}
|
|
122
|
+
/** Heading TOC entries from a block list. */
|
|
123
|
+
export function headingOutline(blocks) {
|
|
124
|
+
const used = new Set();
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const entry of walkBlocks(blocks)) {
|
|
127
|
+
if (entry.block.type !== 'heading')
|
|
128
|
+
continue;
|
|
129
|
+
const id = entry.block.id ?? uniqueSlug(entry.block.text, used);
|
|
130
|
+
used.add(id);
|
|
131
|
+
out.push({
|
|
132
|
+
level: entry.block.level,
|
|
133
|
+
text: entry.block.text,
|
|
134
|
+
id,
|
|
135
|
+
path: entry.path,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type BlockLimits } from './limits.js';
|
|
2
|
+
import { type Block } from './schema/blocks.js';
|
|
3
|
+
export type UntrustedBlockIssue = {
|
|
4
|
+
code: 'invalid-input' | 'too-large' | 'too-deep' | 'too-many' | 'unsafe-key' | 'invalid-json' | 'invalid-blocks';
|
|
5
|
+
path: (string | number)[];
|
|
6
|
+
jsonPath: string;
|
|
7
|
+
message: string;
|
|
8
|
+
fix: string;
|
|
9
|
+
};
|
|
10
|
+
export type UntrustedBlocksResult = {
|
|
11
|
+
success: true;
|
|
12
|
+
blocks: Block[];
|
|
13
|
+
issues: [];
|
|
14
|
+
} | {
|
|
15
|
+
success: false;
|
|
16
|
+
blocks: null;
|
|
17
|
+
issues: UntrustedBlockIssue[];
|
|
18
|
+
};
|
|
19
|
+
/** Sanitize and validate block documents before a host uses them. */
|
|
20
|
+
export declare function parseUntrustedBlocks(input: unknown, limits?: Partial<BlockLimits>): UntrustedBlocksResult;
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { BLOCK_LIMITS } from "./limits.js";
|
|
2
|
+
import { base64DecodedBytes, isUnsafeTextCharacter, normalizeLineSeparators } from "./safety.js";
|
|
3
|
+
import { safeParseBlocks } from "./schema/blocks.js";
|
|
4
|
+
import { toJsonPath } from "./schema/primitives.js";
|
|
5
|
+
function stripUnsafe(value, allowNewlines, allowTabs) {
|
|
6
|
+
return [...normalizeLineSeparators(value.replace(/\r\n/g, '\n'), allowNewlines)]
|
|
7
|
+
.filter((character) => !isUnsafeTextCharacter(character, allowNewlines, allowTabs))
|
|
8
|
+
.join('');
|
|
9
|
+
}
|
|
10
|
+
function issue(code, path, message, fix) {
|
|
11
|
+
return { code, path, jsonPath: toJsonPath(path), message, fix };
|
|
12
|
+
}
|
|
13
|
+
function fatal(code, message, fix) {
|
|
14
|
+
return issue(code, [], message, fix);
|
|
15
|
+
}
|
|
16
|
+
function utf8Length(value) {
|
|
17
|
+
return new TextEncoder().encode(value).byteLength;
|
|
18
|
+
}
|
|
19
|
+
function cappedLimits(limits) {
|
|
20
|
+
return Object.fromEntries(Object.entries(BLOCK_LIMITS).map(([key, maximum]) => {
|
|
21
|
+
const requested = limits[key];
|
|
22
|
+
const value = typeof requested === 'number' && Number.isFinite(requested)
|
|
23
|
+
? Math.floor(Math.max(0, requested))
|
|
24
|
+
: maximum;
|
|
25
|
+
return [key, Math.min(maximum, value)];
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
const MULTILINE_KEYS = new Set([
|
|
29
|
+
'markdown',
|
|
30
|
+
'quote',
|
|
31
|
+
'body',
|
|
32
|
+
'prompt',
|
|
33
|
+
'explanation',
|
|
34
|
+
'description',
|
|
35
|
+
'code',
|
|
36
|
+
]);
|
|
37
|
+
const TEXT_VALUE_KEYS = new Set([
|
|
38
|
+
'text',
|
|
39
|
+
'markdown',
|
|
40
|
+
'quote',
|
|
41
|
+
'body',
|
|
42
|
+
'label',
|
|
43
|
+
'title',
|
|
44
|
+
'caption',
|
|
45
|
+
'cite',
|
|
46
|
+
'eyebrow',
|
|
47
|
+
'subtitle',
|
|
48
|
+
'name',
|
|
49
|
+
'filename',
|
|
50
|
+
'code',
|
|
51
|
+
]);
|
|
52
|
+
const DATA_URI = /^data:[^,]*;base64,([A-Za-z0-9+/]*={0,2})$/;
|
|
53
|
+
function sanitizeJson(input, limits) {
|
|
54
|
+
const ancestors = new WeakSet();
|
|
55
|
+
let visited = 0;
|
|
56
|
+
const visit = (current, path, depth, textContext = 'plain', listItem = false) => {
|
|
57
|
+
visited += 1;
|
|
58
|
+
if (visited > limits.maxBytes) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
issue: fatal('too-large', 'Block input contains too many JSON values.', `Keep the serialized input at or below ${limits.maxBytes} UTF-8 bytes.`),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (depth > limits.maxDepth) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
issue: issue('too-deep', path, `Block JSON exceeds the maximum nesting depth of ${limits.maxDepth}.`, `Flatten the document so blocks are at most ${limits.maxDepth} JSON levels deep.`),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (typeof current === 'string') {
|
|
71
|
+
if (current.length > limits.maxBytes || utf8Length(current) > limits.maxBytes) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
issue: issue('too-large', path, `A block value exceeds the ${limits.maxBytes}-byte input limit.`, `Keep the input at or below ${limits.maxBytes} UTF-8 bytes.`),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const clean = stripUnsafe(current, textContext === 'markdown' || textContext === 'code', textContext === 'code');
|
|
78
|
+
if (TEXT_VALUE_KEYS.has(String(path.at(-1) ?? '')) &&
|
|
79
|
+
!clean.startsWith('data:') &&
|
|
80
|
+
[...clean].length > limits.maxTextLength) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
issue: issue('too-large', path, `Text exceeds the ${limits.maxTextLength}-character guard limit.`, `Shorten this text to at most ${limits.maxTextLength} characters.`),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, value: clean };
|
|
87
|
+
}
|
|
88
|
+
if (current === null || typeof current === 'boolean' || typeof current === 'number') {
|
|
89
|
+
return { ok: true, value: current };
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(current)) {
|
|
92
|
+
if (ancestors.has(current)) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
issue: fatal('invalid-input', 'Block input must be acyclic JSON data.', 'Pass JSON text or a plain, acyclic JSON value.'),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
ancestors.add(current);
|
|
99
|
+
const output = [];
|
|
100
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
101
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, String(index));
|
|
102
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
issue: issue('invalid-input', [...path, index], 'Block arrays must contain plain JSON values.', 'Remove holes, accessors and non-data properties.'),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const child = visit(descriptor.value, [...path, index], depth + 1, textContext, listItem);
|
|
109
|
+
if (!child.ok)
|
|
110
|
+
return child;
|
|
111
|
+
output.push(child.value);
|
|
112
|
+
}
|
|
113
|
+
ancestors.delete(current);
|
|
114
|
+
return { ok: true, value: output };
|
|
115
|
+
}
|
|
116
|
+
if (typeof current !== 'object') {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
issue: fatal('invalid-input', 'Input must be JSON text or plain JSON data.', 'Provide a block array or its JSON text.'),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const prototype = Object.getPrototypeOf(current);
|
|
123
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
issue: fatal('invalid-input', 'Block objects must be plain JSON data.', 'Remove custom prototypes and pass plain JSON objects.'),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (ancestors.has(current)) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
issue: fatal('invalid-input', 'Block input must be acyclic JSON data.', 'Pass JSON text or a plain, acyclic JSON value.'),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
ancestors.add(current);
|
|
136
|
+
const output = Object.create(null);
|
|
137
|
+
const record = current;
|
|
138
|
+
for (const key of Object.keys(record)) {
|
|
139
|
+
if (key === '__proto__' || key === 'constructor') {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
issue: issue('unsafe-key', [...path, key], `Block input contains the unsafe key "${key}".`, 'Remove prototype-related keys from the input.'),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
146
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
issue: issue('invalid-input', [...path, key], 'Block objects must contain plain JSON values.', 'Remove getters and setters from the input.'),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const safeKey = stripUnsafe(key, false, false);
|
|
153
|
+
if (safeKey === '__proto__' || safeKey === 'constructor') {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
issue: issue('unsafe-key', [...path, safeKey], `Block input contains the unsafe key "${safeKey}" after sanitization.`, 'Remove prototype-related keys from the input.'),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (Object.hasOwn(output, safeKey)) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
issue: issue('invalid-input', [...path, key], 'Sanitization produced duplicate object keys.', 'Rename keys so they remain unique after control characters are removed.'),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const value = descriptor.value;
|
|
166
|
+
const nextContext = key === 'code' && record.type === 'code'
|
|
167
|
+
? 'code'
|
|
168
|
+
: MULTILINE_KEYS.has(key) || (listItem && key === 'text')
|
|
169
|
+
? 'markdown'
|
|
170
|
+
: 'plain';
|
|
171
|
+
const child = visit(value, [...path, safeKey], depth + 1, nextContext, key === 'items' && record.type === 'list');
|
|
172
|
+
if (!child.ok)
|
|
173
|
+
return child;
|
|
174
|
+
output[safeKey] = child.value;
|
|
175
|
+
}
|
|
176
|
+
ancestors.delete(current);
|
|
177
|
+
return { ok: true, value: output };
|
|
178
|
+
};
|
|
179
|
+
return visit(input, [], 0);
|
|
180
|
+
}
|
|
181
|
+
function checkJsonDepth(text, maxDepth) {
|
|
182
|
+
let depth = 0;
|
|
183
|
+
let inString = false;
|
|
184
|
+
let escaped = false;
|
|
185
|
+
for (const character of text) {
|
|
186
|
+
if (inString) {
|
|
187
|
+
if (escaped)
|
|
188
|
+
escaped = false;
|
|
189
|
+
else if (character === '\\')
|
|
190
|
+
escaped = true;
|
|
191
|
+
else if (character === '"')
|
|
192
|
+
inString = false;
|
|
193
|
+
}
|
|
194
|
+
else if (character === '"')
|
|
195
|
+
inString = true;
|
|
196
|
+
else if (character === '{' || character === '[') {
|
|
197
|
+
depth += 1;
|
|
198
|
+
if (depth > maxDepth)
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
else if (character === '}' || character === ']')
|
|
202
|
+
depth -= 1;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
function checkBlockCounts(input, limits) {
|
|
207
|
+
if (!Array.isArray(input))
|
|
208
|
+
return null;
|
|
209
|
+
let count = 0;
|
|
210
|
+
const walk = (blocks, path, containerDepth) => {
|
|
211
|
+
for (const [index, value] of blocks.entries()) {
|
|
212
|
+
const blockPath = [...path, index];
|
|
213
|
+
count += 1;
|
|
214
|
+
if (count > limits.maxBlocks) {
|
|
215
|
+
return issue('too-many', blockPath, `Document contains more than ${limits.maxBlocks} blocks.`, `Reduce the document to at most ${limits.maxBlocks} blocks.`);
|
|
216
|
+
}
|
|
217
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
218
|
+
continue;
|
|
219
|
+
const block = value;
|
|
220
|
+
if (block.type === 'columns' && Array.isArray(block.columns)) {
|
|
221
|
+
if (containerDepth + 1 > limits.maxContainerDepth) {
|
|
222
|
+
return issue('too-deep', blockPath, `Block containers nest more than ${limits.maxContainerDepth} deep.`, `Flatten the containers to at most ${limits.maxContainerDepth} levels.`);
|
|
223
|
+
}
|
|
224
|
+
for (const [columnIndex, column] of block.columns.entries()) {
|
|
225
|
+
if (!Array.isArray(column))
|
|
226
|
+
continue;
|
|
227
|
+
const childIssue = walk(column, [...blockPath, 'columns', columnIndex], containerDepth + 1);
|
|
228
|
+
if (childIssue)
|
|
229
|
+
return childIssue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (block.type === 'tabs' && Array.isArray(block.panels)) {
|
|
233
|
+
if (containerDepth + 1 > limits.maxContainerDepth) {
|
|
234
|
+
return issue('too-deep', blockPath, `Block containers nest more than ${limits.maxContainerDepth} deep.`, `Flatten the containers to at most ${limits.maxContainerDepth} levels.`);
|
|
235
|
+
}
|
|
236
|
+
for (const [panelIndex, panel] of block.panels.entries()) {
|
|
237
|
+
if (!panel || typeof panel !== 'object' || Array.isArray(panel))
|
|
238
|
+
continue;
|
|
239
|
+
const panelBlocks = panel.blocks;
|
|
240
|
+
if (!Array.isArray(panelBlocks))
|
|
241
|
+
continue;
|
|
242
|
+
const childIssue = walk(panelBlocks, [...blockPath, 'panels', panelIndex, 'blocks'], containerDepth + 1);
|
|
243
|
+
if (childIssue)
|
|
244
|
+
return childIssue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (block.type === 'table' && Array.isArray(block.columns) && Array.isArray(block.rows)) {
|
|
248
|
+
const cells = block.columns.length * block.rows.length;
|
|
249
|
+
if (cells > limits.maxTableCells) {
|
|
250
|
+
return issue('too-many', [...blockPath, 'rows'], `Table contains ${cells} cells; the limit is ${limits.maxTableCells}.`, `Reduce table rows and columns to at most ${limits.maxTableCells} cells.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
};
|
|
256
|
+
return walk(input, [], 0);
|
|
257
|
+
}
|
|
258
|
+
function checkDataUris(input, limits) {
|
|
259
|
+
let total = 0;
|
|
260
|
+
const walk = (value, path) => {
|
|
261
|
+
if (typeof value === 'string') {
|
|
262
|
+
const match = DATA_URI.exec(value);
|
|
263
|
+
if (!match)
|
|
264
|
+
return null;
|
|
265
|
+
const bytes = base64DecodedBytes(match[1] ?? '');
|
|
266
|
+
if (bytes > limits.maxDataUriBytes) {
|
|
267
|
+
return issue('too-large', path, `Embedded media is ${bytes} bytes; the limit is ${limits.maxDataUriBytes}.`, `Use a data URI at or below ${limits.maxDataUriBytes} decoded bytes.`);
|
|
268
|
+
}
|
|
269
|
+
total += bytes;
|
|
270
|
+
if (total > limits.maxDocumentDataUriBytes) {
|
|
271
|
+
return issue('too-large', path, `Embedded media totals more than ${limits.maxDocumentDataUriBytes} bytes per document.`, `Reduce the document's embedded media to at most ${limits.maxDocumentDataUriBytes} decoded bytes.`);
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
for (const [index, child] of value.entries()) {
|
|
277
|
+
const found = walk(child, [...path, index]);
|
|
278
|
+
if (found)
|
|
279
|
+
return found;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (value && typeof value === 'object') {
|
|
283
|
+
for (const [key, child] of Object.entries(value)) {
|
|
284
|
+
const found = walk(child, [...path, key]);
|
|
285
|
+
if (found)
|
|
286
|
+
return found;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
};
|
|
291
|
+
return walk(input, []);
|
|
292
|
+
}
|
|
293
|
+
/** Sanitize and validate block documents before a host uses them. */
|
|
294
|
+
export function parseUntrustedBlocks(input, limits = {}) {
|
|
295
|
+
const caps = cappedLimits(limits);
|
|
296
|
+
try {
|
|
297
|
+
let parsed = input;
|
|
298
|
+
if (typeof input === 'string') {
|
|
299
|
+
if (utf8Length(input) > caps.maxBytes) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
blocks: null,
|
|
303
|
+
issues: [
|
|
304
|
+
fatal('too-large', 'Block input exceeds the byte limit.', `Keep the input at or below ${caps.maxBytes} UTF-8 bytes.`),
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (!checkJsonDepth(input, caps.maxDepth)) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
blocks: null,
|
|
312
|
+
issues: [
|
|
313
|
+
fatal('too-deep', 'Block JSON exceeds the maximum nesting depth.', `Flatten the document so it is at most ${caps.maxDepth} JSON levels deep.`),
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
parsed = JSON.parse(input);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return {
|
|
322
|
+
success: false,
|
|
323
|
+
blocks: null,
|
|
324
|
+
issues: [
|
|
325
|
+
fatal('invalid-json', 'Block input is not valid JSON.', 'Fix the JSON syntax and try again.'),
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const safe = sanitizeJson(parsed, caps);
|
|
331
|
+
if (!safe.ok)
|
|
332
|
+
return { success: false, blocks: null, issues: [safe.issue] };
|
|
333
|
+
const serialized = JSON.stringify(safe.value);
|
|
334
|
+
if (serialized === undefined || utf8Length(serialized) > caps.maxBytes) {
|
|
335
|
+
return {
|
|
336
|
+
success: false,
|
|
337
|
+
blocks: null,
|
|
338
|
+
issues: [
|
|
339
|
+
fatal('too-large', 'Block input exceeds the byte limit.', `Keep the input at or below ${caps.maxBytes} UTF-8 bytes.`),
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (!Array.isArray(safe.value)) {
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
blocks: null,
|
|
347
|
+
issues: [
|
|
348
|
+
fatal('invalid-input', 'Block input must be an array.', 'Provide a block list or its JSON text.'),
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const countIssue = checkBlockCounts(safe.value, caps);
|
|
353
|
+
if (countIssue)
|
|
354
|
+
return { success: false, blocks: null, issues: [countIssue] };
|
|
355
|
+
const mediaIssue = checkDataUris(safe.value, caps);
|
|
356
|
+
if (mediaIssue)
|
|
357
|
+
return { success: false, blocks: null, issues: [mediaIssue] };
|
|
358
|
+
const result = safeParseBlocks(safe.value);
|
|
359
|
+
if (!result.success) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
blocks: null,
|
|
363
|
+
issues: result.issues.map((problem) => issue('invalid-blocks', problem.path, problem.message, 'Correct this value to match the block schema, then try again.')),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return { success: true, blocks: result.data, issues: [] };
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
blocks: null,
|
|
372
|
+
issues: [
|
|
373
|
+
fatal('invalid-input', 'Block input could not be safely loaded.', 'Provide plain JSON data that matches the block schema.'),
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.css';
|
package/dist/css.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.css';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
import type { ChartBlock } from '../core/index.js';
|
|
4
|
+
|
|
5
|
+
let { block, mode = 'light' }: { block: ChartBlock; mode?: 'light' | 'dark' } = $props();
|
|
6
|
+
|
|
7
|
+
let Chart = $state<Component<Record<string, unknown>> | null>(null);
|
|
8
|
+
let loadError = $state('');
|
|
9
|
+
|
|
10
|
+
$effect(() => {
|
|
11
|
+
let cancelled = false;
|
|
12
|
+
loadError = '';
|
|
13
|
+
Chart = null;
|
|
14
|
+
const kind = block.kind;
|
|
15
|
+
(async () => {
|
|
16
|
+
try {
|
|
17
|
+
if (kind === 'heatmap') {
|
|
18
|
+
const mod = await import('@texuli/charts/matrix');
|
|
19
|
+
if (!cancelled) Chart = mod.HeatmapChart as unknown as Component<Record<string, unknown>>;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (kind === 'scatter' || kind === 'bubble') {
|
|
23
|
+
const mod = await import('@texuli/charts/matrix');
|
|
24
|
+
if (!cancelled) {
|
|
25
|
+
Chart = (kind === 'bubble' ? mod.BubbleChart : mod.ScatterChart) as unknown as Component<
|
|
26
|
+
Record<string, unknown>
|
|
27
|
+
>;
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const mod = await import('@texuli/charts/cartesian');
|
|
32
|
+
const map: Record<string, unknown> = {
|
|
33
|
+
line: mod.LineChart,
|
|
34
|
+
area: mod.AreaChart,
|
|
35
|
+
bar: mod.BarChart,
|
|
36
|
+
'stacked-bar': mod.StackedBarChart,
|
|
37
|
+
};
|
|
38
|
+
if (!cancelled) Chart = map[kind] as Component<Record<string, unknown>>;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!cancelled) {
|
|
41
|
+
loadError = error instanceof Error ? error.message : 'Chart failed to load';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
return () => {
|
|
46
|
+
cancelled = true;
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
{#if loadError}
|
|
52
|
+
<p class="tx-block-chart-error" role="note">{loadError}</p>
|
|
53
|
+
{:else if Chart}
|
|
54
|
+
{#if block.kind === 'heatmap'}
|
|
55
|
+
<Chart title={block.title} description={block.description} cells={block.cells ?? []} {mode} />
|
|
56
|
+
{:else if block.kind === 'scatter' || block.kind === 'bubble'}
|
|
57
|
+
<Chart title={block.title} description={block.description} points={block.points ?? []} {mode} />
|
|
58
|
+
{:else}
|
|
59
|
+
<Chart
|
|
60
|
+
title={block.title}
|
|
61
|
+
description={block.description}
|
|
62
|
+
series={block.series ?? []}
|
|
63
|
+
{mode}
|
|
64
|
+
/>
|
|
65
|
+
{/if}
|
|
66
|
+
{:else}
|
|
67
|
+
<p class="tx-block-chart-loading" role="status">Loading chart…</p>
|
|
68
|
+
{/if}
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.tx-block-chart-loading,
|
|
72
|
+
.tx-block-chart-error {
|
|
73
|
+
margin: 0;
|
|
74
|
+
color: var(--tx-muted-foreground, GrayText);
|
|
75
|
+
font-size: 0.875rem;
|
|
76
|
+
}
|
|
77
|
+
</style>
|