@bendyline/squisq 1.5.1 → 2.0.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/README.md +63 -10
- package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
- package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
- package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
- package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-4X3JQXNM.js +35 -0
- package/dist/chunk-4X3JQXNM.js.map +1 -0
- package/dist/{chunk-2OIBZYKP.js → chunk-5DKBFXPY.js} +31 -121
- package/dist/chunk-5DKBFXPY.js.map +1 -0
- package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
- package/dist/chunk-5J4HKRJF.js.map +1 -0
- package/dist/chunk-5UYI3RJB.js +2503 -0
- package/dist/chunk-5UYI3RJB.js.map +1 -0
- package/dist/{chunk-TEEEILMP.js → chunk-66QO7XFB.js} +19 -292
- package/dist/chunk-66QO7XFB.js.map +1 -0
- package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
- package/dist/chunk-6PJRB2CF.js.map +1 -0
- package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
- package/dist/chunk-AMIR72JP.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-LH7I6SH7.js → chunk-GF3BVNLU.js} +6360 -5723
- package/dist/chunk-GF3BVNLU.js.map +1 -0
- package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
- package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
- package/dist/chunk-JOQYPRDZ.js +127 -0
- package/dist/chunk-JOQYPRDZ.js.map +1 -0
- package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
- package/dist/chunk-LYNJEOAC.js.map +1 -0
- package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
- package/dist/chunk-PXLVXTLW.js.map +1 -0
- package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
- package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
- package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
- package/dist/chunk-QTSJOY54.js.map +1 -0
- package/dist/chunk-QVANWDPH.js +2358 -0
- package/dist/chunk-QVANWDPH.js.map +1 -0
- package/dist/{chunk-6ATE2PSM.js → chunk-R2GC6RLD.js} +7 -7
- package/dist/chunk-R2GC6RLD.js.map +1 -0
- package/dist/chunk-R2YZTCLO.js +292 -0
- package/dist/chunk-R2YZTCLO.js.map +1 -0
- package/dist/chunk-S7A5D7ZJ.js +174 -0
- package/dist/chunk-S7A5D7ZJ.js.map +1 -0
- package/dist/chunk-T3TI7GSZ.js +884 -0
- package/dist/chunk-T3TI7GSZ.js.map +1 -0
- package/dist/{chunk-RUDYOTA6.js → chunk-TUWMK62B.js} +110 -4
- package/dist/chunk-TUWMK62B.js.map +1 -0
- package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
- package/dist/chunk-VR7UPJ7C.js.map +1 -0
- package/dist/doc/index.d.ts +2334 -5
- package/dist/doc/index.js +134 -34
- package/dist/generate/index.d.ts +7 -2
- package/dist/generate/index.js +1 -1
- package/dist/imageEdit/index.d.ts +9 -6
- package/dist/imageEdit/index.js +3 -3
- package/dist/index.d.ts +12 -10
- package/dist/index.js +253 -81
- package/dist/jsonForm/index.d.ts +7 -3
- package/dist/jsonForm/index.js +9 -4
- package/dist/markdown/index.d.ts +6 -70
- package/dist/markdown/index.js +13 -9
- package/dist/narration/index.d.ts +618 -0
- package/dist/narration/index.js +82 -0
- package/dist/recommend/index.d.ts +15 -1
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +28 -6
- package/dist/schemas/index.js +15 -13
- package/dist/spatial/index.d.ts +0 -1
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
- package/dist/timing/index.js +5 -3
- package/dist/transform/index.d.ts +35 -24
- package/dist/transform/index.js +9 -10
- package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
- package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
- package/dist/versions/index.d.ts +2 -1
- package/dist/versions/index.js +2 -2
- package/package.json +6 -6
- package/src/__tests__/applyNarrationTiming.test.ts +190 -0
- package/src/__tests__/applyRenderStyle.test.ts +3 -3
- package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
- package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
- package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
- package/src/__tests__/asciiDiagramParse.test.ts +233 -0
- package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
- package/src/__tests__/asciiDiagramRender.test.ts +162 -0
- package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
- package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
- package/src/__tests__/asciiTimelineParse.test.ts +205 -0
- package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
- package/src/__tests__/asciiTimelineRender.test.ts +376 -0
- package/src/__tests__/attrTokens.test.ts +5 -0
- package/src/__tests__/contentContainer.test.ts +43 -0
- package/src/__tests__/coverBlock.test.ts +33 -2
- package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
- package/src/__tests__/dataTable.test.ts +25 -15
- package/src/__tests__/diagramBlock.test.ts +3 -3
- package/src/__tests__/diagramText.test.ts +26 -0
- package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
- package/src/__tests__/drawingBlock.test.ts +3 -3
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
- package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
- package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
- package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
- package/src/__tests__/getLayers.test.ts +32 -18
- package/src/__tests__/getLayersFallback.test.ts +8 -8
- package/src/__tests__/imageEditVersions.test.ts +2 -0
- package/src/__tests__/jsonForm.pointer.test.ts +47 -1
- package/src/__tests__/layoutBlock.test.ts +3 -3
- package/src/__tests__/majorApiTypes.test.ts +18 -0
- package/src/__tests__/markdown.test.ts +41 -0
- package/src/__tests__/markdownToDoc.test.ts +5 -2
- package/src/__tests__/matchFontFamily.test.ts +114 -0
- package/src/__tests__/materializeBlockLayers.test.ts +135 -0
- package/src/__tests__/materializeTestUtils.ts +17 -0
- package/src/__tests__/narrationAlign.test.ts +150 -0
- package/src/__tests__/narrationFeatures.test.ts +71 -0
- package/src/__tests__/narrationNuclei.test.ts +45 -0
- package/src/__tests__/narrationPacing.test.ts +189 -0
- package/src/__tests__/narrationScript.test.ts +149 -0
- package/src/__tests__/narrationSessionRepro.test.ts +62 -0
- package/src/__tests__/narrationSidecar.test.ts +88 -0
- package/src/__tests__/narrationSyllables.test.ts +65 -0
- package/src/__tests__/narrationTestSignals.ts +167 -0
- package/src/__tests__/narrationVad.test.ts +66 -0
- package/src/__tests__/resolveDocTheme.test.ts +16 -2
- package/src/__tests__/rootExports.test.ts +18 -0
- package/src/__tests__/scopedContentContainer.test.ts +20 -0
- package/src/__tests__/shapeGeometry.test.ts +49 -0
- package/src/__tests__/templateAnnotationParse.test.ts +10 -0
- package/src/__tests__/templateInputs.test.ts +69 -0
- package/src/__tests__/templateMetadata.test.ts +2 -1
- package/src/__tests__/templates.test.ts +143 -17
- package/src/__tests__/themeCompile.test.ts +49 -22
- package/src/__tests__/transformProvenance.test.ts +160 -0
- package/src/__tests__/transformV2.test.ts +40 -14
- package/src/__tests__/treeviewBattle.test.ts +76 -0
- package/src/__tests__/treeviewDetect.test.ts +168 -0
- package/src/__tests__/treeviewParse.test.ts +118 -0
- package/src/__tests__/treeviewPipeline.test.ts +183 -0
- package/src/__tests__/treeviewRender.test.ts +91 -0
- package/src/__tests__/versions.test.ts +44 -0
- package/src/doc/applyNarrationTiming.ts +244 -0
- package/src/doc/asciiDiagram/chars.ts +210 -0
- package/src/doc/asciiDiagram/detect.ts +175 -0
- package/src/doc/asciiDiagram/index.ts +38 -0
- package/src/doc/asciiDiagram/mapping.ts +225 -0
- package/src/doc/asciiDiagram/parse.ts +1095 -0
- package/src/doc/asciiDiagram/render.ts +963 -0
- package/src/doc/asciiDiagram/repair.ts +400 -0
- package/src/doc/asciiDiagram/types.ts +65 -0
- package/src/doc/asciiTimeline/detect.ts +138 -0
- package/src/doc/asciiTimeline/index.ts +26 -0
- package/src/doc/asciiTimeline/mapping.ts +43 -0
- package/src/doc/asciiTimeline/parse.ts +870 -0
- package/src/doc/asciiTimeline/render.ts +427 -0
- package/src/doc/asciiTimeline/types.ts +73 -0
- package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
- package/src/doc/audioMapping.ts +60 -46
- package/src/doc/customTemplatesFrontmatter.ts +0 -41
- package/src/doc/docToMarkdown.ts +126 -39
- package/src/doc/index.ts +76 -4
- package/src/doc/markdownToDoc.ts +135 -1
- package/src/doc/materializeBlockLayers.ts +304 -0
- package/src/doc/mediaAnnotations.ts +20 -0
- package/src/doc/resolveDocTheme.ts +8 -7
- package/src/doc/templateInputs.ts +185 -2
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
- package/src/doc/templates/coverBlock.ts +49 -13
- package/src/doc/templates/diagramBlock.ts +237 -38
- package/src/doc/templates/drawingBlock.ts +2 -2
- package/src/doc/templates/fallbackBlock.ts +1 -1
- package/src/doc/templates/index.ts +134 -279
- package/src/doc/templates/inputDescriptors.ts +20 -1
- package/src/doc/templates/metadata.ts +10 -0
- package/src/doc/templates/persistentLayers.ts +12 -126
- package/src/doc/templates/registry.ts +88 -0
- package/src/doc/templates/templateNames.ts +22 -0
- package/src/doc/templates/timelineBlock.ts +613 -0
- package/src/doc/templates/treeBlock.ts +131 -0
- package/src/doc/treeview/chars.ts +72 -0
- package/src/doc/treeview/detect.ts +145 -0
- package/src/doc/treeview/index.ts +30 -0
- package/src/doc/treeview/mapping.ts +95 -0
- package/src/doc/treeview/parse.ts +201 -0
- package/src/doc/treeview/render.ts +41 -0
- package/src/doc/treeview/types.ts +43 -0
- package/src/doc/utils/applyRenderStyle.ts +2 -9
- package/src/doc/utils/diagramText.ts +78 -0
- package/src/doc/utils/shapeGeometry.ts +157 -7
- package/src/doc/validate.ts +1 -1
- package/src/generate/templateMapper.ts +7 -0
- package/src/imageEdit/export.ts +6 -5
- package/src/imageEdit/versionPaths.ts +13 -26
- package/src/imageEdit/versions.ts +21 -100
- package/src/index.ts +1 -0
- package/src/internal/immutable.ts +58 -0
- package/src/jsonForm/index.ts +9 -1
- package/src/jsonForm/pointer.ts +68 -23
- package/src/markdown/attrTokens.ts +7 -5
- package/src/markdown/convert.ts +11 -1
- package/src/markdown/index.ts +1 -0
- package/src/markdown/stringify.ts +4 -9
- package/src/markdown/utils.ts +36 -14
- package/src/narration/align.ts +514 -0
- package/src/narration/features.ts +231 -0
- package/src/narration/index.ts +86 -0
- package/src/narration/nuclei.ts +118 -0
- package/src/narration/pacing.ts +211 -0
- package/src/narration/script.ts +239 -0
- package/src/narration/session.ts +100 -0
- package/src/narration/sidecar.ts +187 -0
- package/src/narration/syllables.ts +85 -0
- package/src/narration/trace.ts +50 -0
- package/src/narration/types.ts +299 -0
- package/src/narration/vad.ts +131 -0
- package/src/recommend/numberHighlight.ts +25 -0
- package/src/recommend/templates.ts +108 -12
- package/src/schemas/BlockTemplates.ts +156 -7
- package/src/schemas/Doc.ts +72 -9
- package/src/schemas/Media.ts +12 -2
- package/src/schemas/Theme.ts +45 -25
- package/src/schemas/Viewport.ts +2 -2
- package/src/schemas/fontStacks.ts +79 -0
- package/src/schemas/themeCompile.ts +15 -0
- package/src/schemas/themeConstants.ts +2 -0
- package/src/schemas/themeLibrary.ts +25 -19
- package/src/schemas/themeValidator.ts +1 -1
- package/src/spatial/Geohash.ts +0 -3
- package/src/storage/ContentContainer.ts +17 -9
- package/src/storage/ScopedContentContainer.ts +45 -15
- package/src/transform/applyTransform.ts +5 -5
- package/src/transform/index.ts +3 -1
- package/src/transform/registry.ts +169 -49
- package/src/transform/templateSelector.ts +21 -2
- package/src/transform/timingAllocator.ts +152 -21
- package/src/transform/types.ts +14 -1
- package/src/versions/gitignore.ts +34 -0
- package/src/versions/index.ts +2 -0
- package/src/versions/operations.ts +18 -104
- package/src/versions/paths.ts +13 -28
- package/src/versions/snapshotStore.ts +202 -0
- package/src/versions/types.ts +3 -1
- package/dist/chunk-2OIBZYKP.js.map +0 -1
- package/dist/chunk-6ATE2PSM.js.map +0 -1
- package/dist/chunk-A4O7GIWE.js.map +0 -1
- package/dist/chunk-DLXZMS5K.js.map +0 -1
- package/dist/chunk-HWVFJAAH.js.map +0 -1
- package/dist/chunk-IMSCRLLV.js.map +0 -1
- package/dist/chunk-JELIRVDP.js +0 -24
- package/dist/chunk-JELIRVDP.js.map +0 -1
- package/dist/chunk-KKNUBQ6Y.js.map +0 -1
- package/dist/chunk-LDQ2HJIX.js.map +0 -1
- package/dist/chunk-LH7I6SH7.js.map +0 -1
- package/dist/chunk-RUDYOTA6.js.map +0 -1
- package/dist/chunk-T5UK6YOB.js.map +0 -1
- package/dist/chunk-TEEEILMP.js.map +0 -1
- package/dist/chunk-UY7KGQ5R.js +0 -192
- package/dist/chunk-UY7KGQ5R.js.map +0 -1
- package/dist/chunk-VLJJHDAC.js +0 -218
- package/dist/chunk-VLJJHDAC.js.map +0 -1
- package/dist/chunk-VWUFZ6ZG.js.map +0 -1
- package/dist/story/index.d.ts +0 -1777
- package/dist/story/index.js +0 -273
- package/src/doc/getLayers.ts +0 -195
- /package/dist/{story → narration}/index.js.map +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical block-to-layer materialization contract.
|
|
3
|
+
*
|
|
4
|
+
* Timeline scheduling and UI components both delegate here so template
|
|
5
|
+
* lookup, custom templates, ownership, render style, persistent layers,
|
|
6
|
+
* failure policy, and diagnostics cannot drift by render mode.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Block, Layer } from '../schemas/Doc.js';
|
|
10
|
+
import type {
|
|
11
|
+
DocBlock,
|
|
12
|
+
PersistentLayerConfig,
|
|
13
|
+
TemplateBlock,
|
|
14
|
+
TemplateContext,
|
|
15
|
+
} from '../schemas/BlockTemplates.js';
|
|
16
|
+
import type { CustomTemplateDefinition } from '../schemas/CustomTemplates.js';
|
|
17
|
+
import type { Theme } from '../schemas/Theme.js';
|
|
18
|
+
import type { ViewportConfig } from '../schemas/Viewport.js';
|
|
19
|
+
import { createTemplateContext, isTemplateBlock } from '../schemas/BlockTemplates.js';
|
|
20
|
+
import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
|
|
21
|
+
import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
|
|
22
|
+
import { cloneData } from '../internal/immutable.js';
|
|
23
|
+
import { applyRenderStyleToLayers } from './utils/applyRenderStyle.js';
|
|
24
|
+
import { coerceTemplateParams } from './templates/inputDescriptors.js';
|
|
25
|
+
import { fallbackBlockLayers } from './templates/fallbackBlock.js';
|
|
26
|
+
import { expandPersistentLayers, wrapWithPersistentLayers } from './templates/persistentLayers.js';
|
|
27
|
+
import { resolveTemplateName } from './templates/templateNames.js';
|
|
28
|
+
import {
|
|
29
|
+
buildRegistry,
|
|
30
|
+
templateRegistry,
|
|
31
|
+
type RuntimeTemplateRegistry,
|
|
32
|
+
} from './templates/registry.js';
|
|
33
|
+
|
|
34
|
+
/** Internal result from executing one template function. */
|
|
35
|
+
type TemplateLayerMaterialization =
|
|
36
|
+
| { status: 'ok'; layers: Layer[] }
|
|
37
|
+
| { status: 'unknown-template'; layers: [] }
|
|
38
|
+
| { status: 'invalid-result'; layers: []; receivedType: string }
|
|
39
|
+
| { status: 'error'; layers: []; error: unknown };
|
|
40
|
+
|
|
41
|
+
/** How a failed template render is represented in the returned layer graph. */
|
|
42
|
+
export type LayerMaterializationFailureMode = 'fallback' | 'empty';
|
|
43
|
+
|
|
44
|
+
/** A structured, inspectable explanation for a template render failure. */
|
|
45
|
+
export type LayerMaterializationDiagnostic =
|
|
46
|
+
| {
|
|
47
|
+
code: 'unknown-template';
|
|
48
|
+
template: string;
|
|
49
|
+
message: string;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
code: 'invalid-template-result';
|
|
53
|
+
template: string;
|
|
54
|
+
message: string;
|
|
55
|
+
receivedType: string;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
code: 'template-error';
|
|
59
|
+
template: string;
|
|
60
|
+
message: string;
|
|
61
|
+
cause: unknown;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Identifies which path produced a materialized layer graph. */
|
|
65
|
+
export type LayerMaterializationSource = 'authored' | 'template' | 'fallback' | 'empty';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Complete result from {@link materializeBlockLayers}. Failures are data, not
|
|
69
|
+
* console side effects: callers can display, report, or ignore the diagnostic.
|
|
70
|
+
*/
|
|
71
|
+
export interface BlockLayerMaterialization {
|
|
72
|
+
layers: Layer[];
|
|
73
|
+
source: LayerMaterializationSource;
|
|
74
|
+
diagnostic?: LayerMaterializationDiagnostic;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The single public context for turning a block into renderable layers.
|
|
79
|
+
*
|
|
80
|
+
* Theme persistent layers are inherited by default. Pass
|
|
81
|
+
* `persistentLayers: false` to opt out, or a config to replace the theme's
|
|
82
|
+
* persistent layers wholesale.
|
|
83
|
+
*/
|
|
84
|
+
export interface MaterializeBlockLayersOptions {
|
|
85
|
+
/** Theme used by templates and render-style post-processing. */
|
|
86
|
+
theme?: Theme;
|
|
87
|
+
/** Target viewport. Defaults to the landscape preset. */
|
|
88
|
+
viewport?: ViewportConfig;
|
|
89
|
+
/** Theme inheritance override for persistent layers. */
|
|
90
|
+
persistentLayers?: PersistentLayerConfig | false;
|
|
91
|
+
/** Zero-based position in the flattened render sequence. */
|
|
92
|
+
blockIndex?: number;
|
|
93
|
+
/** Number of blocks in the flattened render sequence. */
|
|
94
|
+
totalBlocks?: number;
|
|
95
|
+
/** Document-scoped custom templates; built-ins win name collisions. */
|
|
96
|
+
customTemplates?: readonly CustomTemplateDefinition[];
|
|
97
|
+
/** Failed templates render a visible fallback by default. */
|
|
98
|
+
failureMode?: LayerMaterializationFailureMode;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Pre-expanded scheduling cache; not part of the public materialization API. */
|
|
102
|
+
export interface ExpandedPersistentLayerSet {
|
|
103
|
+
bottomLayers: Layer[];
|
|
104
|
+
topLayers: Layer[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Internal dependencies and compatibility switches supplied by orchestrators. */
|
|
108
|
+
export interface MaterializationRuntime {
|
|
109
|
+
registry: RuntimeTemplateRegistry;
|
|
110
|
+
templateContext?: TemplateContext;
|
|
111
|
+
applyRenderStyle?: boolean;
|
|
112
|
+
expandedPersistentLayers?: ExpandedPersistentLayerSet;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Turn any doc block into an owned, render-ready layer graph.
|
|
117
|
+
*
|
|
118
|
+
* This is the only recommended public materialization entry point.
|
|
119
|
+
*/
|
|
120
|
+
export function materializeBlockLayers(
|
|
121
|
+
block: DocBlock,
|
|
122
|
+
options: MaterializeBlockLayersOptions = {},
|
|
123
|
+
): BlockLayerMaterialization {
|
|
124
|
+
const registry =
|
|
125
|
+
options.customTemplates && options.customTemplates.length > 0
|
|
126
|
+
? buildRegistry(options.customTemplates)
|
|
127
|
+
: (templateRegistry as unknown as RuntimeTemplateRegistry);
|
|
128
|
+
return materializeBlockLayersWithRuntime(block, options, { registry });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Internal entry point for schedulers that cache registry and persistent expansion. */
|
|
132
|
+
export function materializeBlockLayersWithRuntime(
|
|
133
|
+
block: DocBlock,
|
|
134
|
+
options: MaterializeBlockLayersOptions,
|
|
135
|
+
runtime: MaterializationRuntime,
|
|
136
|
+
): BlockLayerMaterialization {
|
|
137
|
+
const theme = options.theme ?? DEFAULT_THEME;
|
|
138
|
+
const viewport = options.viewport ?? VIEWPORT_PRESETS.landscape;
|
|
139
|
+
const blockIndex = options.blockIndex ?? 0;
|
|
140
|
+
const totalBlocks = options.totalBlocks ?? 1;
|
|
141
|
+
const effectivePersistentLayers =
|
|
142
|
+
options.persistentLayers === false
|
|
143
|
+
? undefined
|
|
144
|
+
: (options.persistentLayers ?? theme.persistentLayers);
|
|
145
|
+
|
|
146
|
+
const existingLayers = (block as Block).layers;
|
|
147
|
+
if (existingLayers && existingLayers.length > 0 && !isTemplateBlock(block)) {
|
|
148
|
+
return {
|
|
149
|
+
layers: injectPersistentLayers(
|
|
150
|
+
cloneData(existingLayers),
|
|
151
|
+
block,
|
|
152
|
+
theme,
|
|
153
|
+
effectivePersistentLayers,
|
|
154
|
+
runtime.expandedPersistentLayers,
|
|
155
|
+
),
|
|
156
|
+
source: 'authored',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!isTemplateBlock(block)) {
|
|
161
|
+
return {
|
|
162
|
+
layers: injectPersistentLayers(
|
|
163
|
+
[],
|
|
164
|
+
block,
|
|
165
|
+
theme,
|
|
166
|
+
effectivePersistentLayers,
|
|
167
|
+
runtime.expandedPersistentLayers,
|
|
168
|
+
),
|
|
169
|
+
source: 'empty',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const context = runtime.templateContext
|
|
174
|
+
? cloneData(runtime.templateContext)
|
|
175
|
+
: createTemplateContext(theme, blockIndex, totalBlocks, viewport);
|
|
176
|
+
context.block = cloneData(block as Block);
|
|
177
|
+
|
|
178
|
+
const execution = executeTemplateMaterialization(block, context, runtime.registry);
|
|
179
|
+
if (execution.status === 'ok') {
|
|
180
|
+
const styledLayers =
|
|
181
|
+
runtime.applyRenderStyle !== false
|
|
182
|
+
? applyRenderStyleToLayers(execution.layers, block as Block, theme)
|
|
183
|
+
: execution.layers;
|
|
184
|
+
return {
|
|
185
|
+
layers: injectPersistentLayers(
|
|
186
|
+
styledLayers,
|
|
187
|
+
block,
|
|
188
|
+
theme,
|
|
189
|
+
effectivePersistentLayers,
|
|
190
|
+
runtime.expandedPersistentLayers,
|
|
191
|
+
),
|
|
192
|
+
source: 'template',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const diagnostic = toLayerMaterializationDiagnostic(block.template, execution);
|
|
197
|
+
if ((options.failureMode ?? 'fallback') === 'empty') {
|
|
198
|
+
return {
|
|
199
|
+
layers: injectPersistentLayers(
|
|
200
|
+
[],
|
|
201
|
+
block,
|
|
202
|
+
theme,
|
|
203
|
+
effectivePersistentLayers,
|
|
204
|
+
runtime.expandedPersistentLayers,
|
|
205
|
+
),
|
|
206
|
+
source: 'empty',
|
|
207
|
+
diagnostic,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
layers: injectPersistentLayers(
|
|
213
|
+
fallbackBlockLayers(block, context, diagnostic.message),
|
|
214
|
+
block,
|
|
215
|
+
theme,
|
|
216
|
+
effectivePersistentLayers,
|
|
217
|
+
runtime.expandedPersistentLayers,
|
|
218
|
+
),
|
|
219
|
+
source: 'fallback',
|
|
220
|
+
diagnostic,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function executeTemplateMaterialization(
|
|
225
|
+
templateBlock: TemplateBlock,
|
|
226
|
+
context: TemplateContext,
|
|
227
|
+
registry: RuntimeTemplateRegistry,
|
|
228
|
+
): TemplateLayerMaterialization {
|
|
229
|
+
const safeTemplateBlock = cloneData(templateBlock);
|
|
230
|
+
const safeContext = cloneData(context);
|
|
231
|
+
const maybeChildren = (safeTemplateBlock as Block).children;
|
|
232
|
+
if ((!safeContext.children || safeContext.children.length === 0) && maybeChildren?.length) {
|
|
233
|
+
safeContext.children = maybeChildren;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const templateFn = registry[resolveTemplateName(safeTemplateBlock.template)];
|
|
237
|
+
if (!templateFn) return { status: 'unknown-template', layers: [] };
|
|
238
|
+
|
|
239
|
+
const { templateData, templateOverrides } = safeTemplateBlock as Block;
|
|
240
|
+
const input =
|
|
241
|
+
templateData || templateOverrides
|
|
242
|
+
? ({
|
|
243
|
+
...safeTemplateBlock,
|
|
244
|
+
...templateData,
|
|
245
|
+
...coerceTemplateParams(safeTemplateBlock.template, templateOverrides ?? {}).input,
|
|
246
|
+
} as TemplateBlock)
|
|
247
|
+
: safeTemplateBlock;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const renderedLayers = templateFn(input, safeContext);
|
|
251
|
+
if (!Array.isArray(renderedLayers)) {
|
|
252
|
+
return {
|
|
253
|
+
status: 'invalid-result',
|
|
254
|
+
layers: [],
|
|
255
|
+
receivedType: typeof renderedLayers,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { status: 'ok', layers: cloneData(renderedLayers) };
|
|
259
|
+
} catch (error: unknown) {
|
|
260
|
+
return { status: 'error', layers: [], error };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toLayerMaterializationDiagnostic(
|
|
265
|
+
template: string,
|
|
266
|
+
execution: Exclude<TemplateLayerMaterialization, { status: 'ok' }>,
|
|
267
|
+
): LayerMaterializationDiagnostic {
|
|
268
|
+
if (execution.status === 'unknown-template') {
|
|
269
|
+
return {
|
|
270
|
+
code: 'unknown-template',
|
|
271
|
+
template,
|
|
272
|
+
message: `Unknown template "${template}"`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (execution.status === 'invalid-result') {
|
|
276
|
+
return {
|
|
277
|
+
code: 'invalid-template-result',
|
|
278
|
+
template,
|
|
279
|
+
receivedType: execution.receivedType,
|
|
280
|
+
message: `Template "${template}" returned ${execution.receivedType}, not a layer array`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
code: 'template-error',
|
|
285
|
+
template,
|
|
286
|
+
cause: execution.error,
|
|
287
|
+
message: `Template "${template}" failed`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function injectPersistentLayers(
|
|
292
|
+
layers: Layer[],
|
|
293
|
+
block: DocBlock,
|
|
294
|
+
theme: Theme,
|
|
295
|
+
persistentLayers?: PersistentLayerConfig,
|
|
296
|
+
expanded?: ExpandedPersistentLayerSet,
|
|
297
|
+
): Layer[] {
|
|
298
|
+
if (!persistentLayers) return layers;
|
|
299
|
+
const bottomLayers =
|
|
300
|
+
expanded?.bottomLayers ?? expandPersistentLayers(persistentLayers.bottomLayers, theme);
|
|
301
|
+
const topLayers =
|
|
302
|
+
expanded?.topLayers ?? expandPersistentLayers(persistentLayers.topLayers, theme);
|
|
303
|
+
return wrapWithPersistentLayers(layers, block as TemplateBlock, bottomLayers, topLayers);
|
|
304
|
+
}
|
|
@@ -73,15 +73,21 @@ export interface MediaExtraction {
|
|
|
73
73
|
/**
|
|
74
74
|
* Walk `contents`, lifting every standalone media annotation into a
|
|
75
75
|
* {@link MediaClip}. `makeId(src, index)` produces stable clip ids.
|
|
76
|
+
* When `blockId` is given, each clip records its round-trip `origin` —
|
|
77
|
+
* the owning block, its position in the ORIGINAL contents array, and
|
|
78
|
+
* the annotation paragraph's exact text — so `docToMarkdown` can
|
|
79
|
+
* re-insert it byte-stably.
|
|
76
80
|
*/
|
|
77
81
|
export function extractMediaFromContents(
|
|
78
82
|
contents: MarkdownBlockNode[] | undefined,
|
|
79
83
|
makeId: (src: string, index: number) => string,
|
|
84
|
+
blockId?: string,
|
|
80
85
|
): MediaExtraction {
|
|
81
86
|
const media: MediaClip[] = [];
|
|
82
87
|
const documentMedia: MediaClip[] = [];
|
|
83
88
|
const remaining: MarkdownBlockNode[] = [];
|
|
84
89
|
let index = 0;
|
|
90
|
+
let originalIndex = 0;
|
|
85
91
|
|
|
86
92
|
for (const node of contents ?? []) {
|
|
87
93
|
const parsed = parseStandaloneAnnotation(node);
|
|
@@ -90,10 +96,24 @@ export function extractMediaFromContents(
|
|
|
90
96
|
clip.id = makeId(clip.src, index++);
|
|
91
97
|
const line = node.position?.start.line;
|
|
92
98
|
if (line != null) clip.sourceLine = line;
|
|
99
|
+
if (blockId !== undefined) {
|
|
100
|
+
// parseStandaloneAnnotation only matches a paragraph with exactly
|
|
101
|
+
// one text child, so this read is safe when `parsed` is non-null.
|
|
102
|
+
const rawText =
|
|
103
|
+
node.type === 'paragraph' && node.children?.[0]?.type === 'text'
|
|
104
|
+
? node.children[0].value.trim()
|
|
105
|
+
: undefined;
|
|
106
|
+
clip.origin = {
|
|
107
|
+
blockId,
|
|
108
|
+
index: originalIndex,
|
|
109
|
+
...(rawText !== undefined ? { raw: rawText } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
93
112
|
(clip.anchor === 'document' ? documentMedia : media).push(clip);
|
|
94
113
|
} else {
|
|
95
114
|
remaining.push(node);
|
|
96
115
|
}
|
|
116
|
+
originalIndex++;
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
return { media, documentMedia, remaining };
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The theme analog of how `buildRegistry` resolves custom templates: pure and
|
|
5
5
|
* doc-scoped, with no global state. `resolveThemeForDoc` looks up the active
|
|
6
|
-
* theme id in the doc's own custom themes first, then falls back to
|
|
7
|
-
*
|
|
6
|
+
* theme id in the doc's own custom themes first, then falls back to an
|
|
7
|
+
* optional caller-owned registry, then built-ins via `resolveTheme`.
|
|
8
8
|
*
|
|
9
9
|
* It accepts either a squisq `Doc` (which carries a typed `customThemes`
|
|
10
10
|
* array) or anything with raw `frontmatter` — notably a `MarkdownDocument`,
|
|
11
11
|
* which is the shape every export pipeline pivots through. In the latter case
|
|
12
12
|
* the inline themes are decoded from the `squisq-custom-themes` frontmatter
|
|
13
|
-
* payload.
|
|
14
|
-
*
|
|
13
|
+
* payload. Inline custom themes ship with the doc, exactly like custom
|
|
14
|
+
* templates; host-level themes can be supplied through an explicit registry.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type { Theme } from '../schemas/Theme.js';
|
|
17
|
+
import type { Theme, ThemeRegistry } from '../schemas/Theme.js';
|
|
18
18
|
import { resolveTheme } from '../schemas/themeLibrary.js';
|
|
19
19
|
import { readFrontmatterThemeId } from '../markdown/utils.js';
|
|
20
20
|
import { readCustomThemesFromFrontmatter } from './customThemesFrontmatter.js';
|
|
@@ -39,11 +39,12 @@ export interface ThemeResolvable {
|
|
|
39
39
|
* editor's theme dropdown, or an export `--theme` option). When omitted, the
|
|
40
40
|
* doc's `themeId` / frontmatter theme id is used.
|
|
41
41
|
* @returns the matching inline custom theme (doc-scoped) when present, else
|
|
42
|
-
* the built-in
|
|
42
|
+
* the caller-registry or built-in theme for that id, else the default theme.
|
|
43
43
|
*/
|
|
44
44
|
export function resolveThemeForDoc(
|
|
45
45
|
doc: ThemeResolvable | null | undefined,
|
|
46
46
|
explicitId?: string,
|
|
47
|
+
registry?: ThemeRegistry,
|
|
47
48
|
): Theme {
|
|
48
49
|
const id = explicitId ?? doc?.themeId ?? readFrontmatterThemeId(doc?.frontmatter);
|
|
49
50
|
// Prefer the already-parsed list on a squisq Doc; otherwise decode the
|
|
@@ -51,5 +52,5 @@ export function resolveThemeForDoc(
|
|
|
51
52
|
const inline = doc?.customThemes ?? readCustomThemesFromFrontmatter(doc?.frontmatter);
|
|
52
53
|
const custom = id ? inline?.find((t) => t.id === id) : undefined;
|
|
53
54
|
if (custom) return custom;
|
|
54
|
-
return resolveTheme(id);
|
|
55
|
+
return resolveTheme(id, registry);
|
|
55
56
|
}
|
|
@@ -18,8 +18,24 @@
|
|
|
18
18
|
* preview surfaces use this so a half-authored block still renders.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
MarkdownBlockNode,
|
|
23
|
+
MarkdownCodeBlock,
|
|
24
|
+
MarkdownList,
|
|
25
|
+
MarkdownTable,
|
|
26
|
+
} from '../markdown/types.js';
|
|
22
27
|
import { extractPlainText } from '../markdown/utils.js';
|
|
28
|
+
import { matchNumberHighlight } from '../recommend/numberHighlight.js';
|
|
29
|
+
import {
|
|
30
|
+
detectAsciiDiagram,
|
|
31
|
+
isEligibleAsciiFenceLang,
|
|
32
|
+
isExplicitDiagramLang,
|
|
33
|
+
} from './asciiDiagram/detect.js';
|
|
34
|
+
import { asciiDiagramToTemplateData } from './asciiDiagram/mapping.js';
|
|
35
|
+
import { detectAsciiTimeline, isEligibleAsciiTimelineFenceLang } from './asciiTimeline/detect.js';
|
|
36
|
+
import { asciiTimelineToTemplateData } from './asciiTimeline/mapping.js';
|
|
37
|
+
import { detectTree, isEligibleTreeFenceLang, isExplicitTreeLang } from './treeview/detect.js';
|
|
38
|
+
import { treeToTemplateData, treeFromMarkdownList, findFirstList } from './treeview/mapping.js';
|
|
23
39
|
|
|
24
40
|
/** First image discovered in a block's body, with explicit dimensions when present. */
|
|
25
41
|
export interface FirstImage {
|
|
@@ -153,6 +169,125 @@ export function extractBlockquoteText(contents?: MarkdownBlockNode[]): string {
|
|
|
153
169
|
return '';
|
|
154
170
|
}
|
|
155
171
|
|
|
172
|
+
function firstParagraph(
|
|
173
|
+
contents: MarkdownBlockNode[] | undefined,
|
|
174
|
+
): { node: Extract<MarkdownBlockNode, { type: 'paragraph' }>; index: number } | null {
|
|
175
|
+
if (!contents) return null;
|
|
176
|
+
const index = contents.findIndex((node) => node.type === 'paragraph');
|
|
177
|
+
if (index < 0) return null;
|
|
178
|
+
return {
|
|
179
|
+
node: contents[index] as Extract<MarkdownBlockNode, { type: 'paragraph' }>,
|
|
180
|
+
index,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A leading bold number is an explicit authoring signal for the hero stat. */
|
|
185
|
+
function leadingStrongStat(
|
|
186
|
+
paragraph: Extract<MarkdownBlockNode, { type: 'paragraph' }>,
|
|
187
|
+
): string | null {
|
|
188
|
+
for (const child of paragraph.children) {
|
|
189
|
+
if (child.type === 'text' && child.value.trim() === '') continue;
|
|
190
|
+
if (child.type !== 'strong') return null;
|
|
191
|
+
const text = extractPlainText(child).trim();
|
|
192
|
+
// Explicit bold authoring is intentionally broader than automatic stat
|
|
193
|
+
// recommendation: a small integer such as `**42**` is a valid hero when
|
|
194
|
+
// the author chose statHighlight. Anchor at the beginning so a bold phrase
|
|
195
|
+
// like `**Revenue grew 42%**` does not become oversized wholesale.
|
|
196
|
+
const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
|
|
197
|
+
return match?.[0].trim() || null;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function removeStatPhrase(text: string, start: number, end: number): string {
|
|
203
|
+
const before = text.slice(0, start).trim();
|
|
204
|
+
let after = text.slice(end);
|
|
205
|
+
// A dash/colon visually separates inline prose, but is redundant once the
|
|
206
|
+
// stat and description render on separate lines.
|
|
207
|
+
if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, '');
|
|
208
|
+
else after = after.trim();
|
|
209
|
+
return [before, after].filter(Boolean).join(' ').trim();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function bodyWithoutParagraphStat(
|
|
213
|
+
contents: MarkdownBlockNode[] | undefined,
|
|
214
|
+
paragraphIndex: number,
|
|
215
|
+
paragraphText: string,
|
|
216
|
+
start: number,
|
|
217
|
+
end: number,
|
|
218
|
+
): string {
|
|
219
|
+
if (!contents) return '';
|
|
220
|
+
return contents
|
|
221
|
+
.map((node, index) =>
|
|
222
|
+
index === paragraphIndex
|
|
223
|
+
? removeStatPhrase(paragraphText, start, end)
|
|
224
|
+
: extractPlainText(node).trim(),
|
|
225
|
+
)
|
|
226
|
+
.filter(Boolean)
|
|
227
|
+
.join('\n')
|
|
228
|
+
.trim();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Map Markdown-authored stat content onto the template's visual roles.
|
|
233
|
+
*
|
|
234
|
+
* A leading bold metric in the body is the strongest signal (`**42%** of
|
|
235
|
+
* teams`), followed by a stat-looking heading for backward compatibility,
|
|
236
|
+
* then an unformatted metric in the first body paragraph (used by automatic
|
|
237
|
+
* templates). Generic content retains the historical heading/body mapping.
|
|
238
|
+
*/
|
|
239
|
+
function deriveStatHighlightInputs(
|
|
240
|
+
headingText: string,
|
|
241
|
+
contents: MarkdownBlockNode[] | undefined,
|
|
242
|
+
bodyText: string,
|
|
243
|
+
): Record<string, unknown> {
|
|
244
|
+
const paragraph = firstParagraph(contents);
|
|
245
|
+
if (paragraph) {
|
|
246
|
+
const paragraphText = extractPlainText(paragraph.node).trim();
|
|
247
|
+
const strongStat = leadingStrongStat(paragraph.node);
|
|
248
|
+
if (strongStat) {
|
|
249
|
+
const start = paragraphText.indexOf(strongStat);
|
|
250
|
+
const description = bodyWithoutParagraphStat(
|
|
251
|
+
contents,
|
|
252
|
+
paragraph.index,
|
|
253
|
+
paragraphText,
|
|
254
|
+
start,
|
|
255
|
+
start + strongStat.length,
|
|
256
|
+
);
|
|
257
|
+
return {
|
|
258
|
+
stat: strongStat,
|
|
259
|
+
description: description || headingText || strongStat,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const trimmedHeading = headingText.trim();
|
|
265
|
+
const headingStat = matchNumberHighlight(trimmedHeading);
|
|
266
|
+
if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
|
|
267
|
+
return { stat: headingText, description: bodyText || headingText };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (paragraph) {
|
|
271
|
+
const paragraphText = extractPlainText(paragraph.node).trim();
|
|
272
|
+
const bodyStat = matchNumberHighlight(paragraphText);
|
|
273
|
+
if (bodyStat) {
|
|
274
|
+
const description = bodyWithoutParagraphStat(
|
|
275
|
+
contents,
|
|
276
|
+
paragraph.index,
|
|
277
|
+
paragraphText,
|
|
278
|
+
bodyStat.index,
|
|
279
|
+
bodyStat.end,
|
|
280
|
+
);
|
|
281
|
+
return {
|
|
282
|
+
stat: bodyStat.value,
|
|
283
|
+
description: description || headingText || bodyStat.value,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { stat: headingText, description: bodyText || headingText };
|
|
289
|
+
}
|
|
290
|
+
|
|
156
291
|
export interface DeriveTemplateInputsOptions {
|
|
157
292
|
/**
|
|
158
293
|
* Return visible placeholder values instead of `null` when an essential
|
|
@@ -177,7 +312,7 @@ export function deriveTemplateInputs(
|
|
|
177
312
|
|
|
178
313
|
switch (templateName) {
|
|
179
314
|
case 'statHighlight':
|
|
180
|
-
return
|
|
315
|
+
return deriveStatHighlightInputs(headingText, contents, bodyText);
|
|
181
316
|
case 'quote': {
|
|
182
317
|
const quote = extractBlockquoteText(contents) || bodyText || headingText;
|
|
183
318
|
return { quote };
|
|
@@ -234,6 +369,54 @@ export function deriveTemplateInputs(
|
|
|
234
369
|
body: bodyText,
|
|
235
370
|
};
|
|
236
371
|
}
|
|
372
|
+
case 'diagram': {
|
|
373
|
+
// Nodes/edges from an ASCII-art fence in the body (the fence itself
|
|
374
|
+
// stays in `contents`, untouched, so round-trips are lossless).
|
|
375
|
+
const fences = (contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
|
|
376
|
+
const fence =
|
|
377
|
+
fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : undefined;
|
|
378
|
+
const detection = fence
|
|
379
|
+
? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) })
|
|
380
|
+
: undefined;
|
|
381
|
+
if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
|
|
382
|
+
const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
|
|
383
|
+
return { nodes, edges, ...(headingText ? { title: headingText } : {}) };
|
|
384
|
+
}
|
|
385
|
+
case 'timeline': {
|
|
386
|
+
// Tracks/events from a spatial ASCII timeline fence. Calling this case
|
|
387
|
+
// already implies author/template intent, so accept the detector's
|
|
388
|
+
// explicit one-event form even when the fence itself is untagged.
|
|
389
|
+
const fences = (contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
|
|
390
|
+
const fence =
|
|
391
|
+
fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang)
|
|
392
|
+
? fences[0]
|
|
393
|
+
: undefined;
|
|
394
|
+
const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : undefined;
|
|
395
|
+
if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
|
|
396
|
+
const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
|
|
397
|
+
return { tracks, links, ...(headingText ? { title: headingText } : {}) };
|
|
398
|
+
}
|
|
399
|
+
case 'tree': {
|
|
400
|
+
// Items from an ASCII tree fence, or a nested markdown bullet list when
|
|
401
|
+
// the author explicitly annotates a listed body. The fence/list stays
|
|
402
|
+
// in `contents`, so round-trips are lossless.
|
|
403
|
+
const fences = (contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
|
|
404
|
+
const fence =
|
|
405
|
+
fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : undefined;
|
|
406
|
+
if (fence) {
|
|
407
|
+
const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
|
|
408
|
+
if (detection.isTree && detection.tree) {
|
|
409
|
+
const { items } = treeToTemplateData(detection.tree);
|
|
410
|
+
return { items, ...(headingText ? { title: headingText } : {}) };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const list = findFirstList(contents);
|
|
414
|
+
if (list) {
|
|
415
|
+
const { items } = treeToTemplateData(treeFromMarkdownList(list));
|
|
416
|
+
if (items.length > 0) return { items, ...(headingText ? { title: headingText } : {}) };
|
|
417
|
+
}
|
|
418
|
+
return placeholders ? {} : null;
|
|
419
|
+
}
|
|
237
420
|
default:
|
|
238
421
|
return placeholders ? {} : null;
|
|
239
422
|
}
|
|
@@ -89,9 +89,7 @@ describe('expandDocBlocks with customTemplates', () => {
|
|
|
89
89
|
expect(expanded[0].layers![0].position.width).toBe('90%');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('
|
|
93
|
-
// Suppress the expected console.warn from `expandTemplateBlock`.
|
|
94
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
92
|
+
it('returns a visible fallback and structured diagnostic when the template is unknown', () => {
|
|
95
93
|
const block: Block = {
|
|
96
94
|
id: 'b1',
|
|
97
95
|
startTime: 0,
|
|
@@ -100,11 +98,27 @@ describe('expandDocBlocks with customTemplates', () => {
|
|
|
100
98
|
title: 'Y',
|
|
101
99
|
template: 'nonexistent',
|
|
102
100
|
};
|
|
103
|
-
const
|
|
101
|
+
const diagnostics: string[] = [];
|
|
102
|
+
const expanded = expandDocBlocks([block], {
|
|
103
|
+
customTemplates: [heroDef],
|
|
104
|
+
onDiagnostic: (diagnostic) => diagnostics.push(diagnostic.code),
|
|
105
|
+
});
|
|
106
|
+
expect(expanded[0].layers?.some((layer) => layer.id === 'fallback-notice')).toBe(true);
|
|
107
|
+
expect(diagnostics).toEqual(['unknown-template']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('retains an explicit empty policy for hosts that do not want fallback UI', () => {
|
|
111
|
+
const block: Block = {
|
|
112
|
+
id: 'b1',
|
|
113
|
+
startTime: 0,
|
|
114
|
+
duration: 1,
|
|
115
|
+
audioSegment: 0,
|
|
116
|
+
template: 'nonexistent',
|
|
117
|
+
};
|
|
118
|
+
const expanded = expandDocBlocks([block], {
|
|
119
|
+
customTemplates: [heroDef],
|
|
120
|
+
failureMode: 'empty',
|
|
121
|
+
});
|
|
104
122
|
expect(expanded[0].layers).toBeUndefined();
|
|
105
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
106
|
-
warnSpy.mockRestore();
|
|
107
123
|
});
|
|
108
124
|
});
|
|
109
|
-
|
|
110
|
-
import { vi } from 'vitest';
|