@bendyline/squisq 1.5.2 → 2.0.1
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-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
- package/dist/chunk-2PWOO4ZE.js.map +1 -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-P3K7NLHF.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-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
- package/dist/chunk-66QO7XFB.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-CY2R2VPR.js +884 -0
- package/dist/chunk-CY2R2VPR.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.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-MMPBOUK7.js} +10 -73
- package/dist/chunk-MMPBOUK7.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-GPLTCSXO.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-TFJVXUHH.js +2358 -0
- package/dist/chunk-TFJVXUHH.js.map +1 -0
- package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -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/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
- package/dist/chunk-W4O2TX5I.js.map +1 -0
- package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
- package/dist/chunk-X6DUONGT.js.map +1 -0
- package/dist/doc/index.d.ts +2334 -5
- package/dist/doc/index.js +127 -33
- 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 +246 -80
- 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__/customTemplatesFrontmatter.test.ts +0 -15
- package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
- 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 +54 -4
- package/src/__tests__/markdownSanitize.test.ts +7 -5
- 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 +12 -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 +70 -2
- package/src/doc/index.ts +76 -4
- package/src/doc/markdownToDoc.ts +138 -2
- 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/__tests__/listBlock.test.ts +64 -0
- package/src/doc/templates/diagramBlock.ts +237 -38
- 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/listBlock.ts +88 -30
- 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 +67 -5
- 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/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/schemas/themes/gezellig.json +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-5TYS5V3G.js.map +0 -1
- package/dist/chunk-A4O7GIWE.js.map +0 -1
- package/dist/chunk-BEQODJWV.js.map +0 -1
- package/dist/chunk-DLXZMS5K.js.map +0 -1
- package/dist/chunk-GPLTCSXO.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-LTN6I7QW.js.map +0 -1
- package/dist/chunk-P3K7NLHF.js.map +0 -1
- package/dist/chunk-T5UK6YOB.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 -1801
- package/dist/story/index.js +0 -279
- package/src/doc/getLayers.ts +0 -195
- /package/dist/{story → narration}/index.js.map +0 -0
|
@@ -0,0 +1,2358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SHAPE_NAMES,
|
|
3
|
+
TEMPLATE_ALIASES,
|
|
4
|
+
buildNarrationScript,
|
|
5
|
+
deriveTemplateInputs,
|
|
6
|
+
flattenBlocks,
|
|
7
|
+
flattenRenderableBlocks,
|
|
8
|
+
getBlockBodyText,
|
|
9
|
+
getPinnedBlockMeta,
|
|
10
|
+
isDataFence,
|
|
11
|
+
isShapeName,
|
|
12
|
+
lintTemplateParams,
|
|
13
|
+
markdownToDoc,
|
|
14
|
+
nearestName,
|
|
15
|
+
normalizeShapeKind,
|
|
16
|
+
parseDataFence,
|
|
17
|
+
parseNarrationTimingJson,
|
|
18
|
+
parseStandaloneAnnotation,
|
|
19
|
+
readCustomThemesFromFrontmatter,
|
|
20
|
+
resolveTemplateName,
|
|
21
|
+
templateRegistry,
|
|
22
|
+
writeCustomTemplatesToFrontmatter,
|
|
23
|
+
writeCustomThemesToFrontmatter
|
|
24
|
+
} from "./chunk-X6DUONGT.js";
|
|
25
|
+
import {
|
|
26
|
+
ASCII_TREE_VOCAB,
|
|
27
|
+
ASCII_VOCAB,
|
|
28
|
+
UNICODE_TREE_VOCAB,
|
|
29
|
+
UNICODE_VOCAB,
|
|
30
|
+
arrowDirection,
|
|
31
|
+
detectAsciiDiagram,
|
|
32
|
+
isHoriz,
|
|
33
|
+
isJunction,
|
|
34
|
+
isVert,
|
|
35
|
+
toGrid,
|
|
36
|
+
traceBoxes
|
|
37
|
+
} from "./chunk-5UYI3RJB.js";
|
|
38
|
+
import {
|
|
39
|
+
resolveMediaSchedule
|
|
40
|
+
} from "./chunk-FVJUUTEM.js";
|
|
41
|
+
import {
|
|
42
|
+
FRONTMATTER_CUSTOM_TEMPLATES_KEY,
|
|
43
|
+
FRONTMATTER_CUSTOM_THEMES_KEY,
|
|
44
|
+
hexHueDegrees,
|
|
45
|
+
resolveTheme
|
|
46
|
+
} from "./chunk-W4O2TX5I.js";
|
|
47
|
+
import {
|
|
48
|
+
parseMarkdown
|
|
49
|
+
} from "./chunk-66QO7XFB.js";
|
|
50
|
+
import {
|
|
51
|
+
KNOWN_BLOCK_META_KEYS,
|
|
52
|
+
coerceAnnotationValues,
|
|
53
|
+
serializeAnnotation
|
|
54
|
+
} from "./chunk-R2YZTCLO.js";
|
|
55
|
+
import {
|
|
56
|
+
extractPlainText,
|
|
57
|
+
getChildren,
|
|
58
|
+
readFrontmatterThemeId
|
|
59
|
+
} from "./chunk-AMIR72JP.js";
|
|
60
|
+
import {
|
|
61
|
+
estimateTimeFromText
|
|
62
|
+
} from "./chunk-3E5F2XMR.js";
|
|
63
|
+
|
|
64
|
+
// src/doc/utils/animationUtils.ts
|
|
65
|
+
function getAnimationStyle(animation, _currentTime = 0) {
|
|
66
|
+
if (!animation || animation.type === "none") {
|
|
67
|
+
return { className: "", style: {} };
|
|
68
|
+
}
|
|
69
|
+
const className = getAnimationClassName(animation);
|
|
70
|
+
const style = getAnimationCSSVars(animation);
|
|
71
|
+
return { className, style };
|
|
72
|
+
}
|
|
73
|
+
function getAnimationClassName(animation) {
|
|
74
|
+
const { type, direction, panDirection } = animation;
|
|
75
|
+
switch (type) {
|
|
76
|
+
case "fadeIn":
|
|
77
|
+
return "anim-fadeIn";
|
|
78
|
+
case "fadeOut":
|
|
79
|
+
return "anim-fadeOut";
|
|
80
|
+
case "slowZoom":
|
|
81
|
+
if (panDirection === "left") return "anim-slowZoom-panLeft";
|
|
82
|
+
if (panDirection === "right") return "anim-slowZoom-panRight";
|
|
83
|
+
if (direction === "out") return "anim-slowZoom-out";
|
|
84
|
+
return "anim-slowZoom-in";
|
|
85
|
+
case "zoomIn":
|
|
86
|
+
return "anim-zoomIn";
|
|
87
|
+
case "zoomOut":
|
|
88
|
+
return "anim-zoomOut";
|
|
89
|
+
case "panLeft":
|
|
90
|
+
return "anim-panLeft";
|
|
91
|
+
case "panRight":
|
|
92
|
+
return "anim-panRight";
|
|
93
|
+
case "typewriter":
|
|
94
|
+
return "anim-typewriter";
|
|
95
|
+
default:
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function getAnimationCSSVars(animation) {
|
|
100
|
+
const vars = {};
|
|
101
|
+
if (animation.duration !== void 0) {
|
|
102
|
+
vars["--anim-duration"] = `${animation.duration}s`;
|
|
103
|
+
}
|
|
104
|
+
if (animation.delay !== void 0) {
|
|
105
|
+
vars["--anim-delay"] = `${animation.delay}s`;
|
|
106
|
+
}
|
|
107
|
+
if (animation.easing) {
|
|
108
|
+
vars["--anim-easing"] = animation.easing;
|
|
109
|
+
}
|
|
110
|
+
return vars;
|
|
111
|
+
}
|
|
112
|
+
function getDefaultAnimationDuration(type) {
|
|
113
|
+
switch (type) {
|
|
114
|
+
case "slowZoom":
|
|
115
|
+
case "panLeft":
|
|
116
|
+
case "panRight":
|
|
117
|
+
return 8;
|
|
118
|
+
// Long, slow animations
|
|
119
|
+
case "typewriter":
|
|
120
|
+
return 3;
|
|
121
|
+
case "fadeIn":
|
|
122
|
+
case "fadeOut":
|
|
123
|
+
return 1;
|
|
124
|
+
case "zoomIn":
|
|
125
|
+
case "zoomOut":
|
|
126
|
+
return 0.5;
|
|
127
|
+
default:
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function getTransitionClass(type, entering, direction) {
|
|
132
|
+
if (type === "cut") return "";
|
|
133
|
+
const mode = entering ? "enter" : "exit";
|
|
134
|
+
return `transition-${getTransitionVisualClass(type, direction)}-${mode}`;
|
|
135
|
+
}
|
|
136
|
+
var TRANSITION_VISUAL_CLASS_BY_TYPE = {
|
|
137
|
+
airplane: "airplane",
|
|
138
|
+
blinds: "blindsHorizontal",
|
|
139
|
+
box: "box",
|
|
140
|
+
checker: "checkerboard",
|
|
141
|
+
checkerboard: "checkerboard",
|
|
142
|
+
circle: "shapeCircle",
|
|
143
|
+
clock: "clock",
|
|
144
|
+
comb: "combHorizontal",
|
|
145
|
+
conveyor: "conveyor",
|
|
146
|
+
cover: "coverLeft",
|
|
147
|
+
crush: "crush",
|
|
148
|
+
cube: "cube",
|
|
149
|
+
curtains: "curtains",
|
|
150
|
+
cut: "cut",
|
|
151
|
+
diamond: "shapeDiamond",
|
|
152
|
+
dissolve: "dissolve",
|
|
153
|
+
doors: "doors",
|
|
154
|
+
drape: "drape",
|
|
155
|
+
fade: "fade",
|
|
156
|
+
fallOver: "fallOver",
|
|
157
|
+
ferris: "ferrisWheel",
|
|
158
|
+
ferrisWheel: "ferrisWheel",
|
|
159
|
+
flash: "flash",
|
|
160
|
+
flip: "flip",
|
|
161
|
+
flyThrough: "flyThrough",
|
|
162
|
+
flythrough: "flyThrough",
|
|
163
|
+
fracture: "fracture",
|
|
164
|
+
gallery: "gallery",
|
|
165
|
+
glitter: "glitter",
|
|
166
|
+
honeycomb: "honeycomb",
|
|
167
|
+
morph: "morph",
|
|
168
|
+
newsflash: "newsflash",
|
|
169
|
+
orbit: "orbit",
|
|
170
|
+
origami: "origami",
|
|
171
|
+
pageCurl: "pageCurl",
|
|
172
|
+
pageCurlDouble: "pageCurlDouble",
|
|
173
|
+
pageCurlSingle: "pageCurlSingle",
|
|
174
|
+
pan: "panLeft",
|
|
175
|
+
peelOff: "peelOff",
|
|
176
|
+
plus: "shapePlus",
|
|
177
|
+
prestige: "prestige",
|
|
178
|
+
prism: "prism",
|
|
179
|
+
pull: "pullLeft",
|
|
180
|
+
push: "pushLeft",
|
|
181
|
+
random: "random",
|
|
182
|
+
randomBar: "randomBarsHorizontal",
|
|
183
|
+
randomBars: "randomBarsVertical",
|
|
184
|
+
reveal: "revealLeft",
|
|
185
|
+
ripple: "ripple",
|
|
186
|
+
rotate: "rotate",
|
|
187
|
+
shape: "shapeCircle",
|
|
188
|
+
shred: "shred",
|
|
189
|
+
slideDown: "slideDown",
|
|
190
|
+
slideLeft: "slideLeft",
|
|
191
|
+
slideRight: "slideRight",
|
|
192
|
+
slideUp: "slideUp",
|
|
193
|
+
split: "splitHorizontal",
|
|
194
|
+
strips: "stripsLeft",
|
|
195
|
+
switch: "switch",
|
|
196
|
+
uncover: "uncoverLeft",
|
|
197
|
+
vortex: "vortex",
|
|
198
|
+
warp: "warp",
|
|
199
|
+
wedge: "wedge",
|
|
200
|
+
wheel: "wheel",
|
|
201
|
+
wheelReverse: "wheelReverse",
|
|
202
|
+
wind: "wind",
|
|
203
|
+
window: "window",
|
|
204
|
+
wipe: "wipeLeft",
|
|
205
|
+
zoom: "zoom"
|
|
206
|
+
};
|
|
207
|
+
function getTransitionVisualClass(type, direction) {
|
|
208
|
+
switch (type) {
|
|
209
|
+
case "randomBars":
|
|
210
|
+
case "randomBar":
|
|
211
|
+
return axisClass("randomBars", direction, type === "randomBar" ? "Horizontal" : "Vertical");
|
|
212
|
+
case "comb":
|
|
213
|
+
return axisClass("comb", direction, "Horizontal");
|
|
214
|
+
case "blinds":
|
|
215
|
+
return axisClass("blinds", direction, "Horizontal");
|
|
216
|
+
case "split":
|
|
217
|
+
return axisClass("split", direction, "Horizontal");
|
|
218
|
+
case "push":
|
|
219
|
+
return directionalClass("push", direction, "Left");
|
|
220
|
+
case "wipe":
|
|
221
|
+
return directionalClass("wipe", direction, "Left");
|
|
222
|
+
case "cover":
|
|
223
|
+
return directionalClass("cover", direction, "Left");
|
|
224
|
+
case "uncover":
|
|
225
|
+
return directionalClass("uncover", direction, "Left");
|
|
226
|
+
case "pull":
|
|
227
|
+
return directionalClass("pull", direction, "Left");
|
|
228
|
+
case "reveal":
|
|
229
|
+
return directionalClass("reveal", direction, "Left");
|
|
230
|
+
case "strips":
|
|
231
|
+
return directionalClass("strips", direction, "Left");
|
|
232
|
+
case "pan":
|
|
233
|
+
return directionalClass("pan", direction, "Left");
|
|
234
|
+
default:
|
|
235
|
+
return TRANSITION_VISUAL_CLASS_BY_TYPE[type];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function directionalClass(prefix, direction, fallback) {
|
|
239
|
+
const suffix = directionSuffix(direction, fallback);
|
|
240
|
+
return `${prefix}${suffix}`;
|
|
241
|
+
}
|
|
242
|
+
function axisClass(prefix, direction, fallback) {
|
|
243
|
+
const suffix = axisSuffix(direction, fallback);
|
|
244
|
+
return `${prefix}${suffix}`;
|
|
245
|
+
}
|
|
246
|
+
function directionSuffix(direction, fallback) {
|
|
247
|
+
switch (direction) {
|
|
248
|
+
case "right":
|
|
249
|
+
return "Right";
|
|
250
|
+
case "up":
|
|
251
|
+
return "Up";
|
|
252
|
+
case "down":
|
|
253
|
+
return "Down";
|
|
254
|
+
case "left":
|
|
255
|
+
default:
|
|
256
|
+
return fallback;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function axisSuffix(direction, fallback) {
|
|
260
|
+
switch (direction) {
|
|
261
|
+
case "vertical":
|
|
262
|
+
case "up":
|
|
263
|
+
case "down":
|
|
264
|
+
return "Vertical";
|
|
265
|
+
case "horizontal":
|
|
266
|
+
case "left":
|
|
267
|
+
case "right":
|
|
268
|
+
return "Horizontal";
|
|
269
|
+
default:
|
|
270
|
+
return fallback;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function getAnimationProgress(animation, currentTime, slideDuration) {
|
|
274
|
+
const delay = animation.delay || 0;
|
|
275
|
+
const duration = animation.duration || slideDuration;
|
|
276
|
+
if (currentTime < delay) return 0;
|
|
277
|
+
if (currentTime >= delay + duration) return 1;
|
|
278
|
+
return (currentTime - delay) / duration;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/doc/utils/imageTreatment.ts
|
|
282
|
+
var SEPIA_HUE_DEGREES = 40;
|
|
283
|
+
var clamp01 = (v) => Math.max(0, Math.min(1, v));
|
|
284
|
+
var r2 = (v) => Math.round(v * 100) / 100;
|
|
285
|
+
function cssFilterForTreatment(treatment, blur) {
|
|
286
|
+
const parts = [];
|
|
287
|
+
if (blur != null && blur > 0) {
|
|
288
|
+
parts.push(`blur(${Math.round(blur)}px)`);
|
|
289
|
+
}
|
|
290
|
+
if (treatment && treatment.type !== "none") {
|
|
291
|
+
const s = clamp01(treatment.strength ?? 0.6);
|
|
292
|
+
switch (treatment.type) {
|
|
293
|
+
case "mono":
|
|
294
|
+
parts.push(`grayscale(${r2(s)})`, `contrast(${r2(1 + 0.05 * s)})`);
|
|
295
|
+
break;
|
|
296
|
+
case "duotone": {
|
|
297
|
+
const hue = hexHueDegrees(treatment.color ?? "#3d5a80");
|
|
298
|
+
parts.push(
|
|
299
|
+
"grayscale(1)",
|
|
300
|
+
`sepia(${r2(Math.max(0.35, s))})`,
|
|
301
|
+
`hue-rotate(${Math.round(hue - SEPIA_HUE_DEGREES)}deg)`,
|
|
302
|
+
`saturate(${r2(1 + s)})`
|
|
303
|
+
);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case "warm":
|
|
307
|
+
parts.push(`sepia(${r2(0.4 * s)})`, `saturate(${r2(1 + 0.15 * s)})`);
|
|
308
|
+
break;
|
|
309
|
+
case "cool":
|
|
310
|
+
parts.push(
|
|
311
|
+
`saturate(${r2(1 - 0.25 * s)})`,
|
|
312
|
+
`hue-rotate(${Math.round(-12 * s)}deg)`,
|
|
313
|
+
`contrast(${r2(1 + 0.06 * s)})`
|
|
314
|
+
);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return parts.length > 0 ? parts.join(" ") : void 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/doc/docToMarkdown.ts
|
|
322
|
+
var TRANSITION_PARAM_KEYS = ["transition", "transitionDuration", "transitionDirection"];
|
|
323
|
+
function docToMarkdown(doc) {
|
|
324
|
+
const children = [];
|
|
325
|
+
const blockIds = new Set(flattenBlocks(doc.blocks).map((b) => b.id));
|
|
326
|
+
const docMediaByBlock = /* @__PURE__ */ new Map();
|
|
327
|
+
const homelessDocMedia = [];
|
|
328
|
+
for (const clip of doc.documentMedia ?? []) {
|
|
329
|
+
if (clip.origin && blockIds.has(clip.origin.blockId)) {
|
|
330
|
+
const list = docMediaByBlock.get(clip.origin.blockId) ?? [];
|
|
331
|
+
list.push(clip);
|
|
332
|
+
docMediaByBlock.set(clip.origin.blockId, list);
|
|
333
|
+
} else {
|
|
334
|
+
homelessDocMedia.push(clip);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const clip of homelessDocMedia) {
|
|
338
|
+
children.push(synthesizeMediaParagraph(clip));
|
|
339
|
+
}
|
|
340
|
+
function emitBlock(block) {
|
|
341
|
+
if (block.sourceHeading) {
|
|
342
|
+
const heading = ensureAnnotation(block, block.sourceHeading);
|
|
343
|
+
children.push(heading);
|
|
344
|
+
} else if (block.standaloneAnnotation) {
|
|
345
|
+
children.push(synthesizeAnnotationParagraph(block));
|
|
346
|
+
}
|
|
347
|
+
const clips = [...block.media ?? [], ...docMediaByBlock.get(block.id) ?? []];
|
|
348
|
+
if (clips.length > 0) {
|
|
349
|
+
const contents = [...block.contents ?? []];
|
|
350
|
+
const anchored = clips.filter((clip) => clip.origin).sort((a, b) => a.origin.index - b.origin.index);
|
|
351
|
+
for (const clip of anchored) {
|
|
352
|
+
contents.splice(
|
|
353
|
+
Math.min(clip.origin.index, contents.length),
|
|
354
|
+
0,
|
|
355
|
+
synthesizeMediaParagraph(clip)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
for (const clip of clips.filter((c) => !c.origin).reverse()) {
|
|
359
|
+
contents.unshift(synthesizeMediaParagraph(clip));
|
|
360
|
+
}
|
|
361
|
+
children.push(...contents);
|
|
362
|
+
} else if (block.contents) {
|
|
363
|
+
children.push(...block.contents);
|
|
364
|
+
}
|
|
365
|
+
if (block.children) {
|
|
366
|
+
for (const child of block.children) {
|
|
367
|
+
emitBlock(child);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const block of doc.blocks) {
|
|
372
|
+
emitBlock(block);
|
|
373
|
+
}
|
|
374
|
+
const customPayload = writeCustomTemplatesToFrontmatter(doc.customTemplates);
|
|
375
|
+
let frontmatter = doc.frontmatter;
|
|
376
|
+
if (customPayload) {
|
|
377
|
+
frontmatter = { ...frontmatter ?? {}, [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: customPayload };
|
|
378
|
+
} else if (frontmatter && FRONTMATTER_CUSTOM_TEMPLATES_KEY in frontmatter) {
|
|
379
|
+
const { [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: _drop, ...rest } = frontmatter;
|
|
380
|
+
void _drop;
|
|
381
|
+
frontmatter = rest;
|
|
382
|
+
}
|
|
383
|
+
const customThemesPayload = writeCustomThemesToFrontmatter(doc.customThemes);
|
|
384
|
+
if (customThemesPayload) {
|
|
385
|
+
frontmatter = { ...frontmatter ?? {}, [FRONTMATTER_CUSTOM_THEMES_KEY]: customThemesPayload };
|
|
386
|
+
} else if (frontmatter && FRONTMATTER_CUSTOM_THEMES_KEY in frontmatter) {
|
|
387
|
+
const { [FRONTMATTER_CUSTOM_THEMES_KEY]: _dropTheme, ...rest } = frontmatter;
|
|
388
|
+
void _dropTheme;
|
|
389
|
+
frontmatter = rest;
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
type: "document",
|
|
393
|
+
children,
|
|
394
|
+
...frontmatter && Object.keys(frontmatter).length > 0 ? { frontmatter } : {}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function synthesizeAnnotationParagraph(block) {
|
|
398
|
+
const text = serializeAnnotation(block.sourceAnnotation?.template, block.templateOverrides);
|
|
399
|
+
return { type: "paragraph", children: [{ type: "text", value: text }] };
|
|
400
|
+
}
|
|
401
|
+
function synthesizeMediaParagraph(clip) {
|
|
402
|
+
if (clip.origin?.raw) {
|
|
403
|
+
return { type: "paragraph", children: [{ type: "text", value: clip.origin.raw }] };
|
|
404
|
+
}
|
|
405
|
+
const params = { src: clip.src };
|
|
406
|
+
if (clip.anchor === "document") params.anchor = "document";
|
|
407
|
+
if (clip.startAt) params.startAt = String(clip.startAt);
|
|
408
|
+
if (clip.clipStart != null) params.clipStart = String(clip.clipStart);
|
|
409
|
+
if (clip.clipEnd != null) params.clipEnd = String(clip.clipEnd);
|
|
410
|
+
if (clip.spillover) params.spillover = "true";
|
|
411
|
+
return {
|
|
412
|
+
type: "paragraph",
|
|
413
|
+
children: [{ type: "text", value: serializeAnnotation(clip.kind, params) }]
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function ensureAnnotation(block, heading) {
|
|
417
|
+
const { attributes, transitionParams } = ensureTransitionMetadata(block, heading);
|
|
418
|
+
const hasExplicitTemplate = block.template && block.template !== "sectionHeader" && block.autoTemplate !== true;
|
|
419
|
+
const hasOverrides = block.templateOverrides && Object.keys(block.templateOverrides).length > 0;
|
|
420
|
+
let templateAnnotation = heading.templateAnnotation;
|
|
421
|
+
if (!templateAnnotation && (hasExplicitTemplate || hasOverrides || transitionParams)) {
|
|
422
|
+
templateAnnotation = {
|
|
423
|
+
...hasExplicitTemplate ? { template: block.template ?? "sectionHeader" } : {},
|
|
424
|
+
...hasOverrides ? { params: block.templateOverrides } : {}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (transitionParams) {
|
|
428
|
+
templateAnnotation = {
|
|
429
|
+
...templateAnnotation ?? {},
|
|
430
|
+
params: {
|
|
431
|
+
...templateAnnotation?.params ?? {},
|
|
432
|
+
...transitionParams
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (attributes === heading.attributes && templateAnnotation === heading.templateAnnotation) {
|
|
437
|
+
return heading;
|
|
438
|
+
}
|
|
439
|
+
return { ...heading, children: [...heading.children], attributes, templateAnnotation };
|
|
440
|
+
}
|
|
441
|
+
function ensureTransitionMetadata(block, heading) {
|
|
442
|
+
const transition = block.transition;
|
|
443
|
+
if (!transition) {
|
|
444
|
+
return { attributes: heading.attributes, transitionParams: void 0 };
|
|
445
|
+
}
|
|
446
|
+
const attrs = removeTransitionParams(heading.attributes) ?? {};
|
|
447
|
+
const attributes = {
|
|
448
|
+
...attrs,
|
|
449
|
+
blockMeta: {
|
|
450
|
+
...attrs.blockMeta ?? {},
|
|
451
|
+
transition
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
return {
|
|
455
|
+
attributes,
|
|
456
|
+
transitionParams: {
|
|
457
|
+
transition: transition.type,
|
|
458
|
+
...transition.duration !== void 0 ? { transitionDuration: String(transition.duration) } : {},
|
|
459
|
+
...transition.direction ? { transitionDirection: transition.direction } : {}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function removeTransitionParams(attrs) {
|
|
464
|
+
if (!attrs?.params) return attrs;
|
|
465
|
+
const params = { ...attrs.params };
|
|
466
|
+
for (const key of TRANSITION_PARAM_KEYS) delete params[key];
|
|
467
|
+
return {
|
|
468
|
+
...attrs,
|
|
469
|
+
...Object.keys(params).length > 0 ? { params } : { params: void 0 }
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/doc/resolveDocTheme.ts
|
|
474
|
+
function resolveThemeForDoc(doc, explicitId, registry) {
|
|
475
|
+
const id = explicitId ?? doc?.themeId ?? readFrontmatterThemeId(doc?.frontmatter);
|
|
476
|
+
const inline = doc?.customThemes ?? readCustomThemesFromFrontmatter(doc?.frontmatter);
|
|
477
|
+
const custom = id ? inline?.find((t) => t.id === id) : void 0;
|
|
478
|
+
if (custom) return custom;
|
|
479
|
+
return resolveTheme(id, registry);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/doc/applyNarrationTiming.ts
|
|
483
|
+
var MIN_BLOCK_SIMILARITY = 0.5;
|
|
484
|
+
var MIN_V1_SCRIPT_SIMILARITY = 0.8;
|
|
485
|
+
var PIN_CONFLICT_TOLERANCE_SEC = 1;
|
|
486
|
+
function blockSpokenText(block) {
|
|
487
|
+
const title = block.title ? block.title.trim() : "";
|
|
488
|
+
const body = getBlockBodyText(block);
|
|
489
|
+
if (title && body) return `${title}
|
|
490
|
+
${body}`;
|
|
491
|
+
return title || body;
|
|
492
|
+
}
|
|
493
|
+
function matchSidecarBlocks(flat, timing) {
|
|
494
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
495
|
+
const remainingBlocks = new Set(flat.map((b) => b.id));
|
|
496
|
+
const remainingSidecar = [];
|
|
497
|
+
const byId = new Map(flat.map((b) => [b.id, b]));
|
|
498
|
+
for (const entry of timing.blocks) {
|
|
499
|
+
if (entry.blockId && byId.has(entry.blockId) && remainingBlocks.has(entry.blockId)) {
|
|
500
|
+
ranges.set(entry.blockId, { startSec: entry.startSec, endSec: entry.endSec });
|
|
501
|
+
remainingBlocks.delete(entry.blockId);
|
|
502
|
+
} else {
|
|
503
|
+
remainingSidecar.push(entry);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (remainingSidecar.length > 0 && remainingBlocks.size > 0) {
|
|
507
|
+
const candidates = [];
|
|
508
|
+
for (const entry of remainingSidecar) {
|
|
509
|
+
const slice = timing.sourceText.slice(entry.charStart, entry.charEnd);
|
|
510
|
+
for (const block of flat) {
|
|
511
|
+
if (!remainingBlocks.has(block.id)) continue;
|
|
512
|
+
const score = scoreTextSimilarity(slice, blockSpokenText(block));
|
|
513
|
+
if (score >= MIN_BLOCK_SIMILARITY) candidates.push({ score, entry, blockId: block.id });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
candidates.sort((a, b) => b.score - a.score || a.entry.blockIndex - b.entry.blockIndex);
|
|
517
|
+
const usedEntries = /* @__PURE__ */ new Set();
|
|
518
|
+
for (const candidate of candidates) {
|
|
519
|
+
if (usedEntries.has(candidate.entry) || !remainingBlocks.has(candidate.blockId)) continue;
|
|
520
|
+
ranges.set(candidate.blockId, {
|
|
521
|
+
startSec: candidate.entry.startSec,
|
|
522
|
+
endSec: candidate.entry.endSec
|
|
523
|
+
});
|
|
524
|
+
usedEntries.add(candidate.entry);
|
|
525
|
+
remainingBlocks.delete(candidate.blockId);
|
|
526
|
+
}
|
|
527
|
+
const leftoverEntries = remainingSidecar.filter((e) => !usedEntries.has(e));
|
|
528
|
+
if (leftoverEntries.length > 0 && leftoverEntries.length === remainingBlocks.size) {
|
|
529
|
+
const leftoverBlocks = flat.filter((b) => remainingBlocks.has(b.id));
|
|
530
|
+
leftoverEntries.sort((a, b) => a.blockIndex - b.blockIndex);
|
|
531
|
+
leftoverBlocks.forEach((block, i) => {
|
|
532
|
+
const entry = leftoverEntries[i];
|
|
533
|
+
ranges.set(block.id, { startSec: entry.startSec, endSec: entry.endSec });
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return ranges;
|
|
538
|
+
}
|
|
539
|
+
function proportionalRanges(doc, timing) {
|
|
540
|
+
const script = buildNarrationScript(doc);
|
|
541
|
+
if (script.blocks.length === 0) return null;
|
|
542
|
+
if (scoreTextSimilarity(script.sourceText, timing.sourceText) < MIN_V1_SCRIPT_SIMILARITY) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
546
|
+
const starts = script.blocks.map(
|
|
547
|
+
(range) => estimateTimeFromText(script.sourceText, range.charStart, timing.duration)
|
|
548
|
+
);
|
|
549
|
+
script.blocks.forEach((range, i) => {
|
|
550
|
+
ranges.set(range.blockId, {
|
|
551
|
+
startSec: starts[i],
|
|
552
|
+
endSec: i + 1 < starts.length ? starts[i + 1] : timing.duration
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
return ranges;
|
|
556
|
+
}
|
|
557
|
+
function retimeBlocks(blocks, ctx) {
|
|
558
|
+
return blocks.map((block) => {
|
|
559
|
+
const range = ctx.ranges.get(block.id);
|
|
560
|
+
const pinned = getPinnedBlockMeta(block);
|
|
561
|
+
const next = { ...block };
|
|
562
|
+
if (range && pinned.duration == null && pinned.startTime == null) {
|
|
563
|
+
next.startTime = ctx.clipStart + range.startSec;
|
|
564
|
+
next.duration = Math.max(0, range.endSec - range.startSec);
|
|
565
|
+
ctx.cursor = Math.max(ctx.cursor, next.startTime + next.duration);
|
|
566
|
+
} else if (range) {
|
|
567
|
+
const pinnedStart = pinned.startTime ?? block.startTime;
|
|
568
|
+
const pinnedDuration = pinned.duration ?? block.duration;
|
|
569
|
+
const narrStart = ctx.clipStart + range.startSec;
|
|
570
|
+
const narrDuration = Math.max(0, range.endSec - range.startSec);
|
|
571
|
+
if (Math.abs(pinnedStart - narrStart) > PIN_CONFLICT_TOLERANCE_SEC || Math.abs(pinnedDuration - narrDuration) > PIN_CONFLICT_TOLERANCE_SEC) {
|
|
572
|
+
ctx.diagnostics.push({
|
|
573
|
+
severity: "info",
|
|
574
|
+
code: "narration-pin-conflict",
|
|
575
|
+
message: `Block timing is pinned (duration=/startTime=) but the recorded narration says ~${narrDuration.toFixed(1)}s starting at ~${narrStart.toFixed(1)}s. The pin wins; remove it to follow the narration.`,
|
|
576
|
+
blockId: block.id
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
ctx.cursor = Math.max(ctx.cursor, pinnedStart + pinnedDuration);
|
|
580
|
+
} else {
|
|
581
|
+
next.startTime = ctx.cursor;
|
|
582
|
+
ctx.cursor += next.duration;
|
|
583
|
+
}
|
|
584
|
+
if (block.children && block.children.length > 0) {
|
|
585
|
+
next.children = retimeBlocks(block.children, ctx);
|
|
586
|
+
}
|
|
587
|
+
return next;
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
async function applyNarrationTiming(doc, container) {
|
|
591
|
+
const clips = (doc.documentMedia ?? []).filter((c) => c.anchor === "document");
|
|
592
|
+
for (const clip of clips) {
|
|
593
|
+
if (!clip.src) continue;
|
|
594
|
+
const sidecarData = await container.readFile(`${clip.src}.timing.json`);
|
|
595
|
+
if (!sidecarData) continue;
|
|
596
|
+
const timing = parseNarrationTimingJson(sidecarData);
|
|
597
|
+
if (!timing) continue;
|
|
598
|
+
const flat = flattenRenderableBlocks(doc.blocks);
|
|
599
|
+
const ranges = timing.blocks.length > 0 ? matchSidecarBlocks(flat, timing) : proportionalRanges(doc, timing);
|
|
600
|
+
if (!ranges || ranges.size === 0) continue;
|
|
601
|
+
const ctx = {
|
|
602
|
+
clipStart: clip.startAt,
|
|
603
|
+
ranges,
|
|
604
|
+
diagnostics: [],
|
|
605
|
+
cursor: 0
|
|
606
|
+
};
|
|
607
|
+
const blocks = retimeBlocks(doc.blocks, ctx);
|
|
608
|
+
const duration = Math.max(clip.startAt + timing.duration, ctx.cursor);
|
|
609
|
+
const retimed = {
|
|
610
|
+
...doc,
|
|
611
|
+
blocks,
|
|
612
|
+
duration,
|
|
613
|
+
...ctx.diagnostics.length > 0 ? { diagnostics: [...doc.diagnostics ?? [], ...ctx.diagnostics] } : {}
|
|
614
|
+
};
|
|
615
|
+
return { doc: retimed, applied: true, clipSrc: clip.src };
|
|
616
|
+
}
|
|
617
|
+
return { doc, applied: false };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/doc/audioMapping.ts
|
|
621
|
+
function parseTimingJson(data) {
|
|
622
|
+
try {
|
|
623
|
+
const text = new TextDecoder().decode(data);
|
|
624
|
+
const parsed = JSON.parse(text);
|
|
625
|
+
if (parsed && typeof parsed.sourceText === "string" && typeof parsed.duration === "number") {
|
|
626
|
+
return {
|
|
627
|
+
sourceText: parsed.sourceText,
|
|
628
|
+
bookmarks: Array.isArray(parsed.bookmarks) ? parsed.bookmarks : [],
|
|
629
|
+
duration: parsed.duration
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
} catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function normalizeToWords(text) {
|
|
638
|
+
const words = text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2);
|
|
639
|
+
return new Set(words);
|
|
640
|
+
}
|
|
641
|
+
function scoreTextSimilarity(a, b) {
|
|
642
|
+
const wordsA = normalizeToWords(a);
|
|
643
|
+
const wordsB = normalizeToWords(b);
|
|
644
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
645
|
+
let intersection = 0;
|
|
646
|
+
for (const w of wordsA) {
|
|
647
|
+
if (wordsB.has(w)) intersection++;
|
|
648
|
+
}
|
|
649
|
+
const union = (/* @__PURE__ */ new Set([...wordsA, ...wordsB])).size;
|
|
650
|
+
return union > 0 ? intersection / union : 0;
|
|
651
|
+
}
|
|
652
|
+
function slugify(text) {
|
|
653
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
654
|
+
}
|
|
655
|
+
function blockToPlainText(block) {
|
|
656
|
+
const parts = [];
|
|
657
|
+
if (block.title) parts.push(block.title);
|
|
658
|
+
if (block.contents) {
|
|
659
|
+
for (const node of block.contents) {
|
|
660
|
+
parts.push(extractPlainText(node));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return parts.join("\n");
|
|
664
|
+
}
|
|
665
|
+
function flattenForAudio(blocks) {
|
|
666
|
+
const result = [];
|
|
667
|
+
for (const block of blocks) {
|
|
668
|
+
if (block.children && block.children.length > 0) {
|
|
669
|
+
result.push(...flattenForAudio(block.children));
|
|
670
|
+
} else {
|
|
671
|
+
result.push(block);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
676
|
+
var AUDIO_ONLY_EXTENSIONS = [".mp3", ".wav", ".ogg", ".m4a"];
|
|
677
|
+
var AMBIGUOUS_EXTENSIONS = [".webm", ".mp4"];
|
|
678
|
+
function isNarrationAudioPath(path) {
|
|
679
|
+
const lower = path.toLowerCase();
|
|
680
|
+
if (AUDIO_ONLY_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
681
|
+
if (AMBIGUOUS_EXTENSIONS.some((ext) => lower.endsWith(ext))) {
|
|
682
|
+
return lower.startsWith("audio/");
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
function stripAudioExtension(filename) {
|
|
687
|
+
const lower = filename.toLowerCase();
|
|
688
|
+
for (const ext of [...AUDIO_ONLY_EXTENSIONS, ...AMBIGUOUS_EXTENSIONS]) {
|
|
689
|
+
if (lower.endsWith(ext)) return filename.slice(0, -ext.length);
|
|
690
|
+
}
|
|
691
|
+
return filename;
|
|
692
|
+
}
|
|
693
|
+
function parseConsolidatedTiming(data) {
|
|
694
|
+
try {
|
|
695
|
+
const text = new TextDecoder().decode(data);
|
|
696
|
+
const parsed = JSON.parse(text);
|
|
697
|
+
if (parsed && parsed.version === 2 && parsed.sections) {
|
|
698
|
+
return parsed.sections;
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
} catch {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function discoverNarrationAudio(container) {
|
|
706
|
+
const files = await container.listFiles();
|
|
707
|
+
const audioFiles = files.filter((f) => isNarrationAudioPath(f.path));
|
|
708
|
+
const results = [];
|
|
709
|
+
let consolidatedSections = null;
|
|
710
|
+
const consolidatedData = await container.readFile("timing.json");
|
|
711
|
+
if (consolidatedData) {
|
|
712
|
+
consolidatedSections = parseConsolidatedTiming(consolidatedData);
|
|
713
|
+
}
|
|
714
|
+
for (const file of audioFiles) {
|
|
715
|
+
const filename = file.path.split("/").pop() ?? file.path;
|
|
716
|
+
let timing = null;
|
|
717
|
+
if (consolidatedSections) {
|
|
718
|
+
const audioBase = stripAudioExtension(filename);
|
|
719
|
+
for (const [sectionName, sectionTiming] of Object.entries(consolidatedSections)) {
|
|
720
|
+
if (audioBase.endsWith(`-${sectionName}`)) {
|
|
721
|
+
timing = sectionTiming;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (!timing) {
|
|
727
|
+
const timingPath = `${file.path}.timing.json`;
|
|
728
|
+
const timingData = await container.readFile(timingPath);
|
|
729
|
+
if (timingData) {
|
|
730
|
+
timing = parseTimingJson(timingData);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
results.push({ path: file.path, filename, timing });
|
|
734
|
+
}
|
|
735
|
+
return results;
|
|
736
|
+
}
|
|
737
|
+
function resolveFromAnnotations(blocks, audioFiles) {
|
|
738
|
+
const matches = [];
|
|
739
|
+
const flat = flattenForAudio(blocks);
|
|
740
|
+
for (const block of flat) {
|
|
741
|
+
const audioRef = block.templateOverrides?.audio;
|
|
742
|
+
if (!audioRef) continue;
|
|
743
|
+
const audio = audioFiles.find(
|
|
744
|
+
(m) => m.filename === audioRef || m.path === audioRef || m.path.endsWith(`/${audioRef}`)
|
|
745
|
+
);
|
|
746
|
+
if (audio) {
|
|
747
|
+
matches.push({ block, audio });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return matches;
|
|
751
|
+
}
|
|
752
|
+
var MIN_CONTENT_SIMILARITY = 0.35;
|
|
753
|
+
var MIN_FILENAME_SIMILARITY = 0.5;
|
|
754
|
+
function autoMatchBlocks(blocks, audioFiles) {
|
|
755
|
+
const flat = flattenForAudio(blocks);
|
|
756
|
+
if (flat.length === 0 || audioFiles.length === 0) return [];
|
|
757
|
+
const scoredPairs = [];
|
|
758
|
+
for (let audioIndex = 0; audioIndex < audioFiles.length; audioIndex++) {
|
|
759
|
+
const audio = audioFiles[audioIndex];
|
|
760
|
+
for (let bi = 0; bi < flat.length; bi++) {
|
|
761
|
+
const block = flat[bi];
|
|
762
|
+
const blockText = blockToPlainText(block);
|
|
763
|
+
if (!blockText.trim()) continue;
|
|
764
|
+
let score = 0;
|
|
765
|
+
if (audio.timing?.sourceText) {
|
|
766
|
+
score = scoreTextSimilarity(audio.timing.sourceText, blockText);
|
|
767
|
+
}
|
|
768
|
+
if (score < MIN_CONTENT_SIMILARITY && block.title) {
|
|
769
|
+
const titleSlug = slugify(block.title);
|
|
770
|
+
const fileSlug = slugify(stripAudioExtension(audio.filename));
|
|
771
|
+
if (fileSlug.includes(titleSlug) && titleSlug.length >= 3) {
|
|
772
|
+
score = Math.max(score, MIN_FILENAME_SIMILARITY + 0.1);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (score >= MIN_CONTENT_SIMILARITY) {
|
|
776
|
+
scoredPairs.push({ block, blockIndex: bi, audio, audioIndex, score });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
scoredPairs.sort((a, b) => b.score - a.score);
|
|
781
|
+
const usedBlocks = /* @__PURE__ */ new Set();
|
|
782
|
+
const usedAudio = /* @__PURE__ */ new Set();
|
|
783
|
+
const matches = [];
|
|
784
|
+
for (const pair of scoredPairs) {
|
|
785
|
+
if (usedBlocks.has(pair.blockIndex) || usedAudio.has(pair.audioIndex)) continue;
|
|
786
|
+
usedBlocks.add(pair.blockIndex);
|
|
787
|
+
usedAudio.add(pair.audioIndex);
|
|
788
|
+
matches.push({ block: pair.block, audio: pair.audio, score: pair.score });
|
|
789
|
+
}
|
|
790
|
+
return matches;
|
|
791
|
+
}
|
|
792
|
+
async function resolveAudioMapping(doc, container) {
|
|
793
|
+
const narration = await applyNarrationTiming(doc, container);
|
|
794
|
+
doc = narration.doc;
|
|
795
|
+
const audioFiles = await discoverNarrationAudio(container);
|
|
796
|
+
const scheduledSrcs = new Set(resolveMediaSchedule(doc).map((clip) => clip.src));
|
|
797
|
+
const available = audioFiles.filter((f) => !scheduledSrcs.has(f.path));
|
|
798
|
+
if (available.length === 0) return doc;
|
|
799
|
+
let matches = resolveFromAnnotations(doc.blocks, available);
|
|
800
|
+
if (matches.length === 0 && !narration.applied) {
|
|
801
|
+
matches = autoMatchBlocks(doc.blocks, available).map((m) => ({
|
|
802
|
+
block: m.block,
|
|
803
|
+
audio: m.audio
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
if (matches.length === 0) return doc;
|
|
807
|
+
const flat = flattenForAudio(doc.blocks);
|
|
808
|
+
const blockOrder = new Map(flat.map((b, i) => [b.id, i]));
|
|
809
|
+
matches.sort((a, b) => (blockOrder.get(a.block.id) ?? 0) - (blockOrder.get(b.block.id) ?? 0));
|
|
810
|
+
const segments = [];
|
|
811
|
+
const blockToSegment = /* @__PURE__ */ new Map();
|
|
812
|
+
let startTime = 0;
|
|
813
|
+
for (let i = 0; i < matches.length; i++) {
|
|
814
|
+
const { block, audio } = matches[i];
|
|
815
|
+
const duration = audio.timing?.duration ?? 30;
|
|
816
|
+
segments.push({
|
|
817
|
+
src: audio.path,
|
|
818
|
+
name: block.title ?? block.id,
|
|
819
|
+
duration,
|
|
820
|
+
startTime
|
|
821
|
+
});
|
|
822
|
+
blockToSegment.set(block.id, i);
|
|
823
|
+
startTime += duration;
|
|
824
|
+
}
|
|
825
|
+
const newBlocks = assignSegmentIndices(doc.blocks, blockToSegment);
|
|
826
|
+
return {
|
|
827
|
+
...doc,
|
|
828
|
+
blocks: newBlocks,
|
|
829
|
+
// A narration take owns the overall duration; explicit per-block
|
|
830
|
+
// segments only ever extend it.
|
|
831
|
+
duration: narration.applied ? Math.max(doc.duration, startTime) : startTime,
|
|
832
|
+
audio: { segments }
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function assignSegmentIndices(blocks, blockToSegment, inheritedSegment) {
|
|
836
|
+
return blocks.map((block) => {
|
|
837
|
+
const segment = blockToSegment.get(block.id) ?? inheritedSegment ?? block.audioSegment;
|
|
838
|
+
const newBlock = {
|
|
839
|
+
...block,
|
|
840
|
+
audioSegment: segment
|
|
841
|
+
};
|
|
842
|
+
if (block.children) {
|
|
843
|
+
newBlock.children = assignSegmentIndices(block.children, blockToSegment, segment);
|
|
844
|
+
}
|
|
845
|
+
return newBlock;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/doc/validate.ts
|
|
850
|
+
function validateMarkdownSource(source, options) {
|
|
851
|
+
return validateMarkdownDoc(parseMarkdown(source), options);
|
|
852
|
+
}
|
|
853
|
+
function validateMarkdownDoc(markdownDoc, options) {
|
|
854
|
+
const doc = markdownToDoc(markdownDoc);
|
|
855
|
+
const diagnostics = [...doc.diagnostics ?? []];
|
|
856
|
+
const blocks = flattenBlocks(doc.blocks);
|
|
857
|
+
const knownTemplates = collectKnownTemplates(doc, options);
|
|
858
|
+
const customNames = new Set(doc.customTemplates?.map((t) => t.name) ?? []);
|
|
859
|
+
const visit = (block, parent) => {
|
|
860
|
+
checkTemplate(block, parent, knownTemplates, diagnostics);
|
|
861
|
+
checkAttributeCoercion(block, parent, diagnostics);
|
|
862
|
+
checkConflictingAnnotationKeys(block, diagnostics);
|
|
863
|
+
checkTemplateInputs(block, parent, customNames, diagnostics);
|
|
864
|
+
checkPossibleDataFences(block, diagnostics);
|
|
865
|
+
if (isDrawingBlock(block)) checkDrawingConnectors(block, diagnostics);
|
|
866
|
+
for (const child of block.children ?? []) visit(child, block);
|
|
867
|
+
};
|
|
868
|
+
for (const block of doc.blocks) visit(block, void 0);
|
|
869
|
+
checkUnrecognizedAnnotations(markdownDoc, diagnostics);
|
|
870
|
+
if (options?.assets) {
|
|
871
|
+
const hasAsset = normalizeAssetCheck(options.assets);
|
|
872
|
+
checkAssets(markdownDoc, blocks, hasAsset, diagnostics);
|
|
873
|
+
}
|
|
874
|
+
diagnostics.sort(
|
|
875
|
+
(a, b) => (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER)
|
|
876
|
+
);
|
|
877
|
+
return {
|
|
878
|
+
diagnostics,
|
|
879
|
+
errorCount: diagnostics.filter((d) => d.severity === "error").length,
|
|
880
|
+
warningCount: diagnostics.filter((d) => d.severity === "warning").length,
|
|
881
|
+
infoCount: diagnostics.filter((d) => d.severity === "info").length,
|
|
882
|
+
doc
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function collectKnownTemplates(doc, options) {
|
|
886
|
+
return /* @__PURE__ */ new Set([
|
|
887
|
+
...Object.keys(templateRegistry),
|
|
888
|
+
...Object.keys(TEMPLATE_ALIASES),
|
|
889
|
+
...doc.customTemplates?.map((t) => t.name) ?? [],
|
|
890
|
+
...options?.extraTemplates ?? []
|
|
891
|
+
]);
|
|
892
|
+
}
|
|
893
|
+
function isDrawingBlock(block) {
|
|
894
|
+
return resolveTemplateName(block.template ?? "") === "drawing";
|
|
895
|
+
}
|
|
896
|
+
function checkTemplate(block, parent, known, diagnostics) {
|
|
897
|
+
const requested = block.sourceHeading?.templateAnnotation?.template ?? block.sourceAnnotation?.template;
|
|
898
|
+
if (!requested) return;
|
|
899
|
+
if (parent && isDrawingBlock(parent)) {
|
|
900
|
+
if (isShapeName(requested)) return;
|
|
901
|
+
const suggestion2 = nearestName(requested, new Set(SHAPE_NAMES), {
|
|
902
|
+
map: resolveTemplateName
|
|
903
|
+
});
|
|
904
|
+
diagnostics.push({
|
|
905
|
+
severity: "warning",
|
|
906
|
+
code: "unknown-shape",
|
|
907
|
+
message: `Unknown shape "${requested}" in drawing \u2014 the child will be skipped` + (suggestion2 ? `. Did you mean "${suggestion2}"?` : ""),
|
|
908
|
+
blockId: block.id,
|
|
909
|
+
...lineOf(block)
|
|
910
|
+
});
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (isShapeName(requested)) {
|
|
914
|
+
diagnostics.push({
|
|
915
|
+
severity: "warning",
|
|
916
|
+
code: "shape-outside-drawing",
|
|
917
|
+
message: `Shape "${requested}" is only recognized on a child heading of a {[drawing]} block`,
|
|
918
|
+
blockId: block.id,
|
|
919
|
+
...lineOf(block)
|
|
920
|
+
});
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (known.has(requested) || known.has(resolveTemplateName(requested))) return;
|
|
924
|
+
const suggestion = nearestName(requested, known, { map: resolveTemplateName });
|
|
925
|
+
diagnostics.push({
|
|
926
|
+
severity: "warning",
|
|
927
|
+
code: "unknown-template",
|
|
928
|
+
message: `Unknown template "${requested}" \u2014 the block will render as a plain fallback card` + (suggestion ? `. Did you mean "${suggestion}"?` : ""),
|
|
929
|
+
blockId: block.id,
|
|
930
|
+
...lineOf(block)
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
function checkTemplateInputs(block, parent, customNames, diagnostics) {
|
|
934
|
+
const requested = block.sourceHeading?.templateAnnotation?.template;
|
|
935
|
+
if (!requested) return;
|
|
936
|
+
if (parent && isDrawingBlock(parent)) return;
|
|
937
|
+
if (isShapeName(requested)) return;
|
|
938
|
+
const canonical = resolveTemplateName(requested);
|
|
939
|
+
if (customNames.has(requested) || customNames.has(canonical)) return;
|
|
940
|
+
const params = block.sourceHeading?.templateAnnotation?.params ?? {};
|
|
941
|
+
for (const finding of lintTemplateParams(canonical, params)) {
|
|
942
|
+
if (finding.kind === "missing-input" && isInputSatisfiable(canonical, block, finding.key)) {
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
diagnostics.push({
|
|
946
|
+
severity: "warning",
|
|
947
|
+
code: finding.kind,
|
|
948
|
+
message: `Block "${block.id}": ${finding.message}`,
|
|
949
|
+
blockId: block.id,
|
|
950
|
+
...lineOf(block)
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function isInputSatisfiable(canonical, block, key) {
|
|
955
|
+
if (block.templateData && key in block.templateData) return true;
|
|
956
|
+
if (block[key] != null) return true;
|
|
957
|
+
const derived = deriveTemplateInputs(canonical, block.title ?? "", block.contents);
|
|
958
|
+
return derived != null && key in derived;
|
|
959
|
+
}
|
|
960
|
+
var NUMERIC_SHAPE_KEYS = ["x", "y", "width", "height", "w", "h", "strokeWidth", "borderRadius"];
|
|
961
|
+
function checkAttributeCoercion(block, parent, diagnostics) {
|
|
962
|
+
const params = block.sourceHeading?.attributes?.params;
|
|
963
|
+
if (params) {
|
|
964
|
+
const { warnings } = coerceAnnotationValues(params);
|
|
965
|
+
for (const warning of warnings) {
|
|
966
|
+
diagnostics.push({
|
|
967
|
+
severity: "warning",
|
|
968
|
+
code: "invalid-attribute",
|
|
969
|
+
message: `Block "${block.id}": ${warning}`,
|
|
970
|
+
blockId: block.id,
|
|
971
|
+
...lineOf(block)
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (parent && isDrawingBlock(parent) && isShapeName(block.template)) {
|
|
976
|
+
const overrides = block.templateOverrides ?? {};
|
|
977
|
+
for (const key of NUMERIC_SHAPE_KEYS) {
|
|
978
|
+
const raw = overrides[key];
|
|
979
|
+
if (raw == null) continue;
|
|
980
|
+
const value = raw.replace(/,\s*$/, "").trim();
|
|
981
|
+
if (value !== "" && !Number.isFinite(Number(value))) {
|
|
982
|
+
diagnostics.push({
|
|
983
|
+
severity: "warning",
|
|
984
|
+
code: "invalid-attribute",
|
|
985
|
+
message: `Drawing shape "${block.id}": "${key}" is not a number (${JSON.stringify(raw)})`,
|
|
986
|
+
blockId: block.id,
|
|
987
|
+
...lineOf(block)
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function checkConflictingAnnotationKeys(block, diagnostics) {
|
|
994
|
+
const annotationParams = block.sourceHeading?.templateAnnotation?.params;
|
|
995
|
+
const pandocParams = block.sourceHeading?.attributes?.params;
|
|
996
|
+
if (!annotationParams || !pandocParams) return;
|
|
997
|
+
for (const key of Object.keys(KNOWN_BLOCK_META_KEYS)) {
|
|
998
|
+
const fromAnnotation = annotationParams[key];
|
|
999
|
+
const fromPandoc = pandocParams[key];
|
|
1000
|
+
if (fromAnnotation == null || fromPandoc == null || fromAnnotation === fromPandoc) continue;
|
|
1001
|
+
diagnostics.push({
|
|
1002
|
+
severity: "info",
|
|
1003
|
+
code: "conflicting-annotation-key",
|
|
1004
|
+
message: `"${key}" is set in both {[\u2026]} (${fromAnnotation}) and {\u2026} (${fromPandoc}) \u2014 the {\u2026} value wins.`,
|
|
1005
|
+
blockId: block.id,
|
|
1006
|
+
...lineOf(block)
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function checkDrawingConnectors(block, diagnostics) {
|
|
1011
|
+
const children = block.children ?? [];
|
|
1012
|
+
const ids = new Set(children.map((c) => c.id));
|
|
1013
|
+
for (const child of children) {
|
|
1014
|
+
const kind = normalizeShapeKind(child.sourceHeading?.templateAnnotation?.template);
|
|
1015
|
+
if (kind !== "line" && kind !== "arrow") continue;
|
|
1016
|
+
const overrides = child.templateOverrides ?? {};
|
|
1017
|
+
for (const key of ["from", "to"]) {
|
|
1018
|
+
const raw = overrides[key];
|
|
1019
|
+
if (raw == null) continue;
|
|
1020
|
+
const ref = raw.replace(/,\s*$/, "").trim();
|
|
1021
|
+
if (ref && !ids.has(ref)) {
|
|
1022
|
+
diagnostics.push({
|
|
1023
|
+
severity: "warning",
|
|
1024
|
+
code: "unresolved-connection",
|
|
1025
|
+
message: `Drawing connector "${child.id}" references unknown ${key} "${ref}"`,
|
|
1026
|
+
blockId: child.id,
|
|
1027
|
+
...lineOf(child)
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function checkPossibleDataFences(block, diagnostics) {
|
|
1034
|
+
const annotated = !!(block.sourceHeading?.templateAnnotation?.template || block.sourceAnnotation?.template);
|
|
1035
|
+
if (!annotated) return;
|
|
1036
|
+
for (const node of block.contents ?? []) {
|
|
1037
|
+
if (node.type !== "code") continue;
|
|
1038
|
+
const lang = node.lang?.toLowerCase();
|
|
1039
|
+
if (lang !== "json" && lang !== "yaml" && lang !== "yml") continue;
|
|
1040
|
+
if (isDataFence(node)) continue;
|
|
1041
|
+
const result = parseDataFence(node);
|
|
1042
|
+
if (!result.data || Object.keys(result.data).length === 0) continue;
|
|
1043
|
+
diagnostics.push({
|
|
1044
|
+
severity: "info",
|
|
1045
|
+
code: "possible-data-fence",
|
|
1046
|
+
message: `This ${lang} block renders as code \u2014 add "data" to the fence (\`\`\`${lang} data) if it's meant as template input.`,
|
|
1047
|
+
blockId: block.id,
|
|
1048
|
+
...node.position ? { line: node.position.start.line } : lineOf(block)
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
var ANNOTATION_LIKE_RE = /\{\[/;
|
|
1053
|
+
function checkUnrecognizedAnnotations(markdownDoc, diagnostics) {
|
|
1054
|
+
const walk = (node, headingLine, topLevel) => {
|
|
1055
|
+
if (node.type === "heading") {
|
|
1056
|
+
const heading = node;
|
|
1057
|
+
const text = extractPlainText(heading);
|
|
1058
|
+
if (ANNOTATION_LIKE_RE.test(text)) {
|
|
1059
|
+
diagnostics.push({
|
|
1060
|
+
severity: "warning",
|
|
1061
|
+
code: "unparsed-annotation",
|
|
1062
|
+
message: `Heading "${truncate(text)}" contains "{[\u2026]}" text that was not recognized \u2014 check the quoting, or whether the token is a known inline icon`,
|
|
1063
|
+
...heading.position ? { line: heading.position.start.line } : {}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (node.type === "paragraph" && topLevel) {
|
|
1069
|
+
const parsed = parseStandaloneAnnotation(node);
|
|
1070
|
+
if (parsed && parsed.template != null) return;
|
|
1071
|
+
}
|
|
1072
|
+
if (node.type === "text" && ANNOTATION_LIKE_RE.test(node.value)) {
|
|
1073
|
+
diagnostics.push({
|
|
1074
|
+
severity: "warning",
|
|
1075
|
+
code: "unparsed-annotation",
|
|
1076
|
+
message: `Body text contains "{[\u2026]}" \u2014 template annotations are recognized on headings and as standalone paragraphs (or the token is not a known inline icon)`,
|
|
1077
|
+
...node.position ? { line: node.position.start.line } : headingLine != null ? { line: headingLine } : {}
|
|
1078
|
+
});
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (node.type === "code") return;
|
|
1082
|
+
for (const child of getChildren(node)) {
|
|
1083
|
+
walk(child, headingLine, false);
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
for (const child of markdownDoc.children) {
|
|
1087
|
+
walk(child, child.position?.start.line, true);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function normalizeAssetCheck(assets) {
|
|
1091
|
+
if (typeof assets === "function") return assets;
|
|
1092
|
+
return (path) => assets.has(path) || assets.has(path.replace(/^\.\//, ""));
|
|
1093
|
+
}
|
|
1094
|
+
var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
|
|
1095
|
+
function isBundleRelative(url) {
|
|
1096
|
+
return url.length > 0 && !URL_SCHEME_RE.test(url) && !url.startsWith("//") && !url.startsWith("#");
|
|
1097
|
+
}
|
|
1098
|
+
var ASSET_PARAM_KEYS = ["src", "imageSrc", "videoSrc", "poster", "backgroundImage"];
|
|
1099
|
+
function checkAssets(markdownDoc, blocks, hasAsset, diagnostics) {
|
|
1100
|
+
const report = (url, line, blockId) => {
|
|
1101
|
+
if (!isBundleRelative(url) || hasAsset(url)) return;
|
|
1102
|
+
diagnostics.push({
|
|
1103
|
+
severity: "warning",
|
|
1104
|
+
code: "missing-asset",
|
|
1105
|
+
message: `Referenced asset "${url}" was not found in the bundle`,
|
|
1106
|
+
...blockId ? { blockId } : {},
|
|
1107
|
+
...line != null ? { line } : {}
|
|
1108
|
+
});
|
|
1109
|
+
};
|
|
1110
|
+
const walk = (node) => {
|
|
1111
|
+
if (node.type === "image") {
|
|
1112
|
+
report(node.url, node.position?.start.line);
|
|
1113
|
+
}
|
|
1114
|
+
if (node.type === "htmlBlock" || node.type === "htmlInline") {
|
|
1115
|
+
walkHtmlImages(node.htmlChildren, (src) => report(src, node.position?.start.line));
|
|
1116
|
+
}
|
|
1117
|
+
for (const child of getChildren(node)) walk(child);
|
|
1118
|
+
};
|
|
1119
|
+
for (const child of markdownDoc.children) walk(child);
|
|
1120
|
+
for (const block of blocks) {
|
|
1121
|
+
const sources = [
|
|
1122
|
+
block.templateOverrides,
|
|
1123
|
+
block.templateData
|
|
1124
|
+
];
|
|
1125
|
+
for (const source of sources) {
|
|
1126
|
+
if (!source) continue;
|
|
1127
|
+
for (const key of ASSET_PARAM_KEYS) {
|
|
1128
|
+
const value = source[key];
|
|
1129
|
+
if (typeof value === "string") {
|
|
1130
|
+
report(value, block.sourceHeading?.position?.start.line, block.id);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const images = source["images"];
|
|
1134
|
+
const list = typeof images === "string" ? images.split(",").map((s) => s.trim()) : Array.isArray(images) ? images.filter((i) => typeof i === "string") : [];
|
|
1135
|
+
for (const img of list) {
|
|
1136
|
+
report(img, block.sourceHeading?.position?.start.line, block.id);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function walkHtmlImages(nodes, report) {
|
|
1142
|
+
for (const node of nodes) {
|
|
1143
|
+
if (node.type !== "htmlElement") continue;
|
|
1144
|
+
if (node.tagName === "img" && node.attributes.src) {
|
|
1145
|
+
report(node.attributes.src);
|
|
1146
|
+
}
|
|
1147
|
+
walkHtmlImages(node.children, report);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function lineOf(block) {
|
|
1151
|
+
const line = block.sourceHeading?.position?.start.line;
|
|
1152
|
+
return line != null ? { line } : {};
|
|
1153
|
+
}
|
|
1154
|
+
function truncate(text, max = 40) {
|
|
1155
|
+
return text.length <= max ? text : `${text.slice(0, max)}\u2026`;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/doc/asciiDiagram/render.ts
|
|
1159
|
+
var SIDE_MARGIN = 1;
|
|
1160
|
+
function renderAsciiDiagram(diagram, options = {}) {
|
|
1161
|
+
const vocab = (options.style ?? diagram.style) === "ascii" ? ASCII_VOCAB : UNICODE_VOCAB;
|
|
1162
|
+
if (diagram.nodes.length === 0) return "";
|
|
1163
|
+
const placed = layoutNodes(diagram.nodes, diagram.edges);
|
|
1164
|
+
const canvas = new Canvas();
|
|
1165
|
+
const drawOrder = [...placed.values()].sort(
|
|
1166
|
+
(a, b) => a.depth - b.depth || a.rect.r0 - b.rect.r0 || a.rect.c0 - b.rect.c0
|
|
1167
|
+
);
|
|
1168
|
+
for (const p of drawOrder) drawBox(canvas, p, vocab);
|
|
1169
|
+
drawEdges(canvas, diagram.edges, placed, vocab);
|
|
1170
|
+
return canvas.emit();
|
|
1171
|
+
}
|
|
1172
|
+
function computeSideDegrees(nodes, edges) {
|
|
1173
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
1174
|
+
const deg = /* @__PURE__ */ new Map();
|
|
1175
|
+
for (const n of nodes) deg.set(n.id, { top: 0, bottom: 0, left: 0, right: 0 });
|
|
1176
|
+
const rectOf = (n) => ({
|
|
1177
|
+
r0: n.row,
|
|
1178
|
+
c0: n.col,
|
|
1179
|
+
r1: n.row + n.hRows - 1,
|
|
1180
|
+
c1: n.col + n.wCols - 1
|
|
1181
|
+
});
|
|
1182
|
+
for (const e of edges) {
|
|
1183
|
+
const s = byId.get(e.source);
|
|
1184
|
+
const t = byId.get(e.target);
|
|
1185
|
+
if (!s || !t || s === t) continue;
|
|
1186
|
+
const { exit, enter } = pickSides(rectOf(s), rectOf(t));
|
|
1187
|
+
deg.get(e.source)[exit]++;
|
|
1188
|
+
deg.get(e.target)[enter]++;
|
|
1189
|
+
}
|
|
1190
|
+
return deg;
|
|
1191
|
+
}
|
|
1192
|
+
function layoutNodes(nodes, edges) {
|
|
1193
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const node of nodes) byId.set(node.id, node);
|
|
1195
|
+
const sideDeg = computeSideDegrees(nodes, edges);
|
|
1196
|
+
const childIds = /* @__PURE__ */ new Map();
|
|
1197
|
+
const roots = [];
|
|
1198
|
+
for (const node of nodes) {
|
|
1199
|
+
if (node.containerId && byId.has(node.containerId) && node.containerId !== node.id) {
|
|
1200
|
+
const list = childIds.get(node.containerId) ?? [];
|
|
1201
|
+
list.push(node.id);
|
|
1202
|
+
childIds.set(node.containerId, list);
|
|
1203
|
+
} else {
|
|
1204
|
+
roots.push(node.id);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1208
|
+
const translateSubtree = (id, dRows, dCols) => {
|
|
1209
|
+
const p = placed.get(id);
|
|
1210
|
+
if (p) p.rect = shiftRect(p.rect, dRows, dCols);
|
|
1211
|
+
for (const childId of childIds.get(id) ?? []) translateSubtree(childId, dRows, dCols);
|
|
1212
|
+
};
|
|
1213
|
+
const placeSiblings = (ids, layoutUnit) => {
|
|
1214
|
+
const order = [...ids].sort((a, b) => {
|
|
1215
|
+
const na = byId.get(a);
|
|
1216
|
+
const nb = byId.get(b);
|
|
1217
|
+
return na.row - nb.row || na.col - nb.col || na.id.localeCompare(nb.id);
|
|
1218
|
+
});
|
|
1219
|
+
const placedRects = [];
|
|
1220
|
+
for (const id of order) {
|
|
1221
|
+
let rect = layoutUnit(id);
|
|
1222
|
+
let guard = 0;
|
|
1223
|
+
for (; ; ) {
|
|
1224
|
+
const hit = placedRects.find((p) => overlapsWithMargin(rect, p, SIDE_MARGIN));
|
|
1225
|
+
if (!hit || guard++ > 64) break;
|
|
1226
|
+
const dxRight = hit.c1 + SIDE_MARGIN + 1 - rect.c0;
|
|
1227
|
+
const dyDown = hit.r1 + SIDE_MARGIN + 1 - rect.r0;
|
|
1228
|
+
if (dxRight < dyDown) {
|
|
1229
|
+
translateSubtree(id, 0, dxRight);
|
|
1230
|
+
rect = shiftRect(rect, 0, dxRight);
|
|
1231
|
+
} else {
|
|
1232
|
+
translateSubtree(id, dyDown, 0);
|
|
1233
|
+
rect = shiftRect(rect, dyDown, 0);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
placedRects.push(rect);
|
|
1237
|
+
}
|
|
1238
|
+
return placedRects;
|
|
1239
|
+
};
|
|
1240
|
+
const layoutSubtree = (id, depth) => {
|
|
1241
|
+
const node = byId.get(id);
|
|
1242
|
+
const labelLines = node.label.length > 0 ? node.label.split("\n") : [];
|
|
1243
|
+
const children = childIds.get(id) ?? [];
|
|
1244
|
+
if (children.length === 0) {
|
|
1245
|
+
const innerW = labelLines.reduce((w, l) => Math.max(w, l.length), 0);
|
|
1246
|
+
const deg = sideDeg.get(id) ?? { top: 0, bottom: 0, left: 0, right: 0 };
|
|
1247
|
+
const horizDeg = Math.max(deg.top, deg.bottom);
|
|
1248
|
+
const vertDeg = Math.max(deg.left, deg.right);
|
|
1249
|
+
const wCols = Math.max(node.wCols, innerW + 4, 3, horizDeg > 0 ? 2 * horizDeg + 1 : 0);
|
|
1250
|
+
const hRows = Math.max(
|
|
1251
|
+
node.hRows,
|
|
1252
|
+
labelLines.length + 2,
|
|
1253
|
+
3,
|
|
1254
|
+
vertDeg > 0 ? 2 * vertDeg + 1 : 0
|
|
1255
|
+
);
|
|
1256
|
+
const rect2 = {
|
|
1257
|
+
r0: node.row,
|
|
1258
|
+
c0: node.col,
|
|
1259
|
+
r1: node.row + hRows - 1,
|
|
1260
|
+
c1: node.col + wCols - 1
|
|
1261
|
+
};
|
|
1262
|
+
placed.set(id, { node, rect: rect2, labelLines, isContainer: false, depth });
|
|
1263
|
+
return rect2;
|
|
1264
|
+
}
|
|
1265
|
+
const childRects = placeSiblings(children, (childId) => layoutSubtree(childId, depth + 1));
|
|
1266
|
+
const bbox = unionRects(childRects);
|
|
1267
|
+
const extraRows = Math.max(0, labelLines.length - 1);
|
|
1268
|
+
const titleLen = (labelLines[0] ?? "").length;
|
|
1269
|
+
const rect = {
|
|
1270
|
+
r0: bbox.r0 - 2 - extraRows,
|
|
1271
|
+
c0: bbox.c0 - 2,
|
|
1272
|
+
r1: bbox.r1 + 2,
|
|
1273
|
+
c1: bbox.c1 + 2
|
|
1274
|
+
};
|
|
1275
|
+
const minW = titleLen > 0 ? titleLen + 6 : 3;
|
|
1276
|
+
if (rect.c1 - rect.c0 + 1 < minW) rect.c1 = rect.c0 + minW - 1;
|
|
1277
|
+
if (rect.c1 - rect.c0 + 1 < node.wCols) rect.c1 = rect.c0 + node.wCols - 1;
|
|
1278
|
+
placed.set(id, { node, rect, labelLines, isContainer: true, depth });
|
|
1279
|
+
return rect;
|
|
1280
|
+
};
|
|
1281
|
+
placeSiblings(roots, (id) => layoutSubtree(id, 0));
|
|
1282
|
+
let minR = Infinity;
|
|
1283
|
+
let minC = Infinity;
|
|
1284
|
+
for (const p of placed.values()) {
|
|
1285
|
+
minR = Math.min(minR, p.rect.r0);
|
|
1286
|
+
minC = Math.min(minC, p.rect.c0);
|
|
1287
|
+
}
|
|
1288
|
+
const dR = minR < 0 ? -minR : 0;
|
|
1289
|
+
const dC = minC < 0 ? -minC : 0;
|
|
1290
|
+
if (dR > 0 || dC > 0) {
|
|
1291
|
+
for (const p of placed.values()) p.rect = shiftRect(p.rect, dR, dC);
|
|
1292
|
+
}
|
|
1293
|
+
return placed;
|
|
1294
|
+
}
|
|
1295
|
+
function overlapsWithMargin(a, b, margin) {
|
|
1296
|
+
return a.r0 - margin <= b.r1 && b.r0 - margin <= a.r1 && a.c0 - margin <= b.c1 && b.c0 - margin <= a.c1;
|
|
1297
|
+
}
|
|
1298
|
+
function shiftRect(rect, dRows, dCols) {
|
|
1299
|
+
return { r0: rect.r0 + dRows, c0: rect.c0 + dCols, r1: rect.r1 + dRows, c1: rect.c1 + dCols };
|
|
1300
|
+
}
|
|
1301
|
+
function unionRects(rects) {
|
|
1302
|
+
return rects.reduce((acc, r) => ({
|
|
1303
|
+
r0: Math.min(acc.r0, r.r0),
|
|
1304
|
+
c0: Math.min(acc.c0, r.c0),
|
|
1305
|
+
r1: Math.max(acc.r1, r.r1),
|
|
1306
|
+
c1: Math.max(acc.c1, r.c1)
|
|
1307
|
+
}));
|
|
1308
|
+
}
|
|
1309
|
+
var Canvas = class {
|
|
1310
|
+
constructor() {
|
|
1311
|
+
this.rows = [];
|
|
1312
|
+
this.kinds = [];
|
|
1313
|
+
}
|
|
1314
|
+
ensure(r, c) {
|
|
1315
|
+
while (this.rows.length <= r) {
|
|
1316
|
+
this.rows.push([]);
|
|
1317
|
+
this.kinds.push([]);
|
|
1318
|
+
}
|
|
1319
|
+
const row = this.rows[r];
|
|
1320
|
+
const kindRow = this.kinds[r];
|
|
1321
|
+
while (row.length <= c) {
|
|
1322
|
+
row.push(" ");
|
|
1323
|
+
kindRow.push("empty");
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
kindAt(r, c) {
|
|
1327
|
+
return this.kinds[r]?.[c] ?? "empty";
|
|
1328
|
+
}
|
|
1329
|
+
/** Unconditional write (boxes, labels, junction upgrades). */
|
|
1330
|
+
set(r, c, ch, kind) {
|
|
1331
|
+
if (r < 0 || c < 0) return;
|
|
1332
|
+
this.ensure(r, c);
|
|
1333
|
+
this.rows[r][c] = ch;
|
|
1334
|
+
this.kinds[r][c] = kind;
|
|
1335
|
+
}
|
|
1336
|
+
/** Edge-aware write: respects borders, upgrades crossings, lets arrows win. */
|
|
1337
|
+
drawEdgeCell(r, c, ch, kind, vocab) {
|
|
1338
|
+
if (r < 0 || c < 0) return;
|
|
1339
|
+
this.ensure(r, c);
|
|
1340
|
+
const existing = this.kinds[r][c];
|
|
1341
|
+
if (existing === "border" || existing === "label" || existing === "junction") return;
|
|
1342
|
+
if (kind === "arrow") {
|
|
1343
|
+
this.rows[r][c] = ch;
|
|
1344
|
+
this.kinds[r][c] = kind;
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (existing === "arrow") return;
|
|
1348
|
+
if (existing === "line-h" && kind === "line-v" || existing === "line-v" && kind === "line-h") {
|
|
1349
|
+
this.rows[r][c] = vocab.cross;
|
|
1350
|
+
this.kinds[r][c] = "turn";
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
if (existing === "empty") {
|
|
1354
|
+
this.rows[r][c] = ch;
|
|
1355
|
+
this.kinds[r][c] = kind;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
emit() {
|
|
1359
|
+
const lines = this.rows.map((row) => row.join("").replace(/\s+$/u, ""));
|
|
1360
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1361
|
+
return lines.join("\n");
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
function drawBox(canvas, p, vocab) {
|
|
1365
|
+
const { rect, labelLines, isContainer } = p;
|
|
1366
|
+
const innerW = rect.c1 - rect.c0 - 1;
|
|
1367
|
+
canvas.set(rect.r0, rect.c0, vocab.tl, "border");
|
|
1368
|
+
canvas.set(rect.r0, rect.c1, vocab.tr, "border");
|
|
1369
|
+
const title = isContainer && labelLines.length > 0 ? ` ${labelLines[0]} ` : null;
|
|
1370
|
+
if (title && title.length <= innerW - 2) {
|
|
1371
|
+
const left = Math.floor((innerW - title.length) / 2);
|
|
1372
|
+
for (let i = 0; i < innerW; i++) {
|
|
1373
|
+
const inTitle = i >= left && i < left + title.length;
|
|
1374
|
+
const ch = inTitle ? title[i - left] : vocab.h;
|
|
1375
|
+
canvas.set(rect.r0, rect.c0 + 1 + i, ch, inTitle ? "label" : "border");
|
|
1376
|
+
}
|
|
1377
|
+
} else {
|
|
1378
|
+
for (let c = rect.c0 + 1; c <= rect.c1 - 1; c++) canvas.set(rect.r0, c, vocab.h, "border");
|
|
1379
|
+
}
|
|
1380
|
+
canvas.set(rect.r1, rect.c0, vocab.bl, "border");
|
|
1381
|
+
canvas.set(rect.r1, rect.c1, vocab.br, "border");
|
|
1382
|
+
for (let c = rect.c0 + 1; c <= rect.c1 - 1; c++) canvas.set(rect.r1, c, vocab.h, "border");
|
|
1383
|
+
for (let r = rect.r0 + 1; r <= rect.r1 - 1; r++) {
|
|
1384
|
+
canvas.set(r, rect.c0, vocab.v, "border");
|
|
1385
|
+
canvas.set(r, rect.c1, vocab.v, "border");
|
|
1386
|
+
}
|
|
1387
|
+
const interiorLines = isContainer ? labelLines.slice(1) : labelLines;
|
|
1388
|
+
if (interiorLines.length > 0) {
|
|
1389
|
+
const innerH = rect.r1 - rect.r0 - 1;
|
|
1390
|
+
const startRow = isContainer ? rect.r0 + 1 : rect.r0 + 1 + Math.max(0, Math.floor((innerH - interiorLines.length) / 2));
|
|
1391
|
+
interiorLines.forEach((line, i) => {
|
|
1392
|
+
const pad = Math.max(0, Math.floor((innerW - line.length) / 2));
|
|
1393
|
+
for (let j = 0; j < line.length && pad + j < innerW; j++) {
|
|
1394
|
+
canvas.set(startRow + i, rect.c0 + 1 + pad + j, line[j], "label");
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function drawEdges(canvas, edges, placed, vocab) {
|
|
1400
|
+
const sorted = [...edges].sort(
|
|
1401
|
+
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || (a.label ?? "").localeCompare(b.label ?? "")
|
|
1402
|
+
);
|
|
1403
|
+
const skip = /* @__PURE__ */ new Set();
|
|
1404
|
+
const plans = [];
|
|
1405
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1406
|
+
if (skip.has(i)) continue;
|
|
1407
|
+
const e = sorted[i];
|
|
1408
|
+
if (!placed.has(e.source) || !placed.has(e.target)) continue;
|
|
1409
|
+
let biDir = false;
|
|
1410
|
+
if (e.directed) {
|
|
1411
|
+
const j = sorted.findIndex(
|
|
1412
|
+
(o, k) => k > i && !skip.has(k) && o.directed && o.source === e.target && o.target === e.source && (o.label ?? "") === (e.label ?? "")
|
|
1413
|
+
);
|
|
1414
|
+
if (j >= 0) {
|
|
1415
|
+
skip.add(j);
|
|
1416
|
+
biDir = true;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const sides = pickSides(
|
|
1420
|
+
placed.get(e.source).rect,
|
|
1421
|
+
placed.get(e.target).rect
|
|
1422
|
+
);
|
|
1423
|
+
plans.push({
|
|
1424
|
+
edge: e,
|
|
1425
|
+
sourceId: e.source,
|
|
1426
|
+
targetId: e.target,
|
|
1427
|
+
exitSide: sides.exit,
|
|
1428
|
+
enterSide: sides.enter,
|
|
1429
|
+
biDir
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
const attachment = assignAttachments(plans, placed);
|
|
1433
|
+
const sideLabelRequests = [];
|
|
1434
|
+
plans.forEach(
|
|
1435
|
+
(plan, i) => routeEdge(
|
|
1436
|
+
canvas,
|
|
1437
|
+
plan,
|
|
1438
|
+
i,
|
|
1439
|
+
placed,
|
|
1440
|
+
attachment,
|
|
1441
|
+
vocab,
|
|
1442
|
+
(candidates, label) => sideLabelRequests.push({ candidates, label })
|
|
1443
|
+
)
|
|
1444
|
+
);
|
|
1445
|
+
for (const req of sideLabelRequests) {
|
|
1446
|
+
let done = false;
|
|
1447
|
+
for (const side of ["right", "left"]) {
|
|
1448
|
+
if (done) break;
|
|
1449
|
+
for (const { row, col } of req.candidates) {
|
|
1450
|
+
if (canvas.kindAt(row, col) !== "line-v") continue;
|
|
1451
|
+
const startCol = side === "right" ? col + 2 : col - 1 - req.label.length;
|
|
1452
|
+
if (startCol < 0) continue;
|
|
1453
|
+
const gapCol = side === "right" ? col + 1 : col - 1;
|
|
1454
|
+
let free = canvas.kindAt(row, gapCol) === "empty";
|
|
1455
|
+
for (let i = 0; free && i < req.label.length; i++) {
|
|
1456
|
+
if (canvas.kindAt(row, startCol + i) !== "empty") free = false;
|
|
1457
|
+
}
|
|
1458
|
+
if (free && side === "left" && startCol > 0) {
|
|
1459
|
+
free = canvas.kindAt(row, startCol - 1) === "empty";
|
|
1460
|
+
}
|
|
1461
|
+
if (!free) continue;
|
|
1462
|
+
for (let i = 0; i < req.label.length; i++) {
|
|
1463
|
+
canvas.set(row, startCol + i, req.label[i], "label");
|
|
1464
|
+
}
|
|
1465
|
+
done = true;
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
function pickSides(s, t) {
|
|
1472
|
+
if (t.r0 > s.r1) return { exit: "bottom", enter: "top" };
|
|
1473
|
+
if (t.r1 < s.r0) return { exit: "top", enter: "bottom" };
|
|
1474
|
+
if (t.c0 > s.c1) return { exit: "right", enter: "left" };
|
|
1475
|
+
if (t.c1 < s.c0) return { exit: "left", enter: "right" };
|
|
1476
|
+
const gaps = [
|
|
1477
|
+
{ side: "top", gap: Math.abs(s.r0 - t.r0) },
|
|
1478
|
+
{ side: "bottom", gap: Math.abs(t.r1 - s.r1) },
|
|
1479
|
+
{ side: "left", gap: Math.abs(s.c0 - t.c0) },
|
|
1480
|
+
{ side: "right", gap: Math.abs(t.c1 - s.c1) }
|
|
1481
|
+
];
|
|
1482
|
+
gaps.sort((a, b) => a.gap - b.gap);
|
|
1483
|
+
return { exit: gaps[0].side, enter: gaps[0].side };
|
|
1484
|
+
}
|
|
1485
|
+
function assignAttachments(plans, placed) {
|
|
1486
|
+
const bySide = /* @__PURE__ */ new Map();
|
|
1487
|
+
plans.forEach((plan, i) => {
|
|
1488
|
+
const exitKey = `${plan.sourceId}|${plan.exitSide}`;
|
|
1489
|
+
const enterKey = `${plan.targetId}|${plan.enterSide}`;
|
|
1490
|
+
bySide.set(exitKey, [
|
|
1491
|
+
...bySide.get(exitKey) ?? [],
|
|
1492
|
+
{ key: `${i}:exit`, otherId: plan.targetId }
|
|
1493
|
+
]);
|
|
1494
|
+
bySide.set(enterKey, [
|
|
1495
|
+
...bySide.get(enterKey) ?? [],
|
|
1496
|
+
{ key: `${i}:enter`, otherId: plan.sourceId }
|
|
1497
|
+
]);
|
|
1498
|
+
});
|
|
1499
|
+
const result = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const [sideKey, list] of [...bySide.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
1501
|
+
const [nodeId, side] = sideKey.split("|");
|
|
1502
|
+
const node = placed.get(nodeId);
|
|
1503
|
+
list.sort((a, b) => {
|
|
1504
|
+
const ra = placed.get(a.otherId).rect;
|
|
1505
|
+
const rb = placed.get(b.otherId).rect;
|
|
1506
|
+
return ra.r0 - rb.r0 || ra.c0 - rb.c0 || a.otherId.localeCompare(b.otherId);
|
|
1507
|
+
});
|
|
1508
|
+
const horizontal = side === "top" || side === "bottom";
|
|
1509
|
+
const from = horizontal ? node.rect.c0 + 1 : node.rect.r0 + 1;
|
|
1510
|
+
const to = horizontal ? node.rect.c1 - 1 : node.rect.r1 - 1;
|
|
1511
|
+
const inner = Math.max(1, to - from + 1);
|
|
1512
|
+
const k = list.length;
|
|
1513
|
+
if (k === 1) {
|
|
1514
|
+
result.set(list[0].key, { pos: from + Math.floor((inner - 1) / 2), index: 0, count: 1 });
|
|
1515
|
+
} else {
|
|
1516
|
+
const span = 2 * k - 1;
|
|
1517
|
+
const start = from + Math.max(0, Math.floor((inner - span) / 2));
|
|
1518
|
+
list.forEach((req, i) => {
|
|
1519
|
+
result.set(req.key, { pos: Math.min(to, start + 2 * i), index: i, count: k });
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return result;
|
|
1524
|
+
}
|
|
1525
|
+
function routeEdge(canvas, plan, planIdx, placed, attachment, vocab, deferSideLabel) {
|
|
1526
|
+
const s = placed.get(plan.sourceId).rect;
|
|
1527
|
+
const t = placed.get(plan.targetId).rect;
|
|
1528
|
+
const exitSlot = attachment.get(`${planIdx}:exit`);
|
|
1529
|
+
const enterSlot = attachment.get(`${planIdx}:enter`);
|
|
1530
|
+
const exitPos = exitSlot.pos;
|
|
1531
|
+
const enterPos = enterSlot.pos;
|
|
1532
|
+
const basis = enterSlot.count > exitSlot.count ? enterSlot : exitSlot;
|
|
1533
|
+
const stagger = basis.index - Math.floor((basis.count - 1) / 2);
|
|
1534
|
+
const junction = (r, c, side) => {
|
|
1535
|
+
const ch = side === "top" ? vocab.stemUp : side === "bottom" ? vocab.stemDown : side === "left" ? vocab.stemLeft : vocab.stemRight;
|
|
1536
|
+
canvas.set(r, c, ch, "junction");
|
|
1537
|
+
};
|
|
1538
|
+
const args = {
|
|
1539
|
+
canvas,
|
|
1540
|
+
directed: plan.edge.directed,
|
|
1541
|
+
biDir: plan.biDir,
|
|
1542
|
+
label: plan.edge.label,
|
|
1543
|
+
vocab,
|
|
1544
|
+
junction,
|
|
1545
|
+
stagger,
|
|
1546
|
+
deferSideLabel
|
|
1547
|
+
};
|
|
1548
|
+
if (plan.exitSide === "bottom" && plan.enterSide === "top") {
|
|
1549
|
+
drawVertical(args, s, t, exitPos, enterPos, 1);
|
|
1550
|
+
} else if (plan.exitSide === "top" && plan.enterSide === "bottom") {
|
|
1551
|
+
drawVertical(args, s, t, exitPos, enterPos, -1);
|
|
1552
|
+
} else if (plan.exitSide === "right" && plan.enterSide === "left") {
|
|
1553
|
+
drawHorizontal(args, s, t, exitPos, enterPos, 1);
|
|
1554
|
+
} else if (plan.exitSide === "left" && plan.enterSide === "right") {
|
|
1555
|
+
drawHorizontal(args, s, t, exitPos, enterPos, -1);
|
|
1556
|
+
} else {
|
|
1557
|
+
drawNested(args, s, t, plan.exitSide);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function middleOut(from, to) {
|
|
1561
|
+
const lo = Math.min(from, to);
|
|
1562
|
+
const hi = Math.max(from, to);
|
|
1563
|
+
const rows = [];
|
|
1564
|
+
for (let r = lo; r <= hi; r++) rows.push(r);
|
|
1565
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
1566
|
+
return rows.sort((a, b) => Math.abs(a - mid) - Math.abs(b - mid) || a - b);
|
|
1567
|
+
}
|
|
1568
|
+
function clamp(value, lo, hi) {
|
|
1569
|
+
return Math.max(lo, Math.min(hi, value));
|
|
1570
|
+
}
|
|
1571
|
+
function drawVertical(args, s, t, exitCol, enterCol, dir) {
|
|
1572
|
+
const { canvas, directed, biDir, label, vocab, junction, stagger } = args;
|
|
1573
|
+
const exitRow = dir === 1 ? s.r1 : s.r0;
|
|
1574
|
+
const enterRow = dir === 1 ? t.r0 : t.r1;
|
|
1575
|
+
const firstFree = exitRow + dir;
|
|
1576
|
+
const lastFree = enterRow - dir;
|
|
1577
|
+
const gap = (lastFree - firstFree) * dir + 1;
|
|
1578
|
+
const drawV = (col, rA, rB) => {
|
|
1579
|
+
const lo = Math.min(rA, rB);
|
|
1580
|
+
const hi = Math.max(rA, rB);
|
|
1581
|
+
for (let r = lo; r <= hi; r++) canvas.drawEdgeCell(r, col, vocab.v, "line-v", vocab);
|
|
1582
|
+
};
|
|
1583
|
+
const straightOk = exitCol >= t.c0 + 1 && exitCol <= t.c1 - 1 && exitCol >= s.c0 + 1 && exitCol <= s.c1 - 1;
|
|
1584
|
+
if (straightOk || gap < 2) {
|
|
1585
|
+
const sCol = clamp(exitCol, s.c0 + 1, s.c1 - 1);
|
|
1586
|
+
const tCol = clamp(exitCol, t.c0 + 1, t.c1 - 1);
|
|
1587
|
+
junction(exitRow, sCol, dir === 1 ? "bottom" : "top");
|
|
1588
|
+
junction(enterRow, tCol, dir === 1 ? "top" : "bottom");
|
|
1589
|
+
if (sCol === tCol && gap >= 1) {
|
|
1590
|
+
drawV(sCol, firstFree, lastFree);
|
|
1591
|
+
if (directed) {
|
|
1592
|
+
canvas.drawEdgeCell(
|
|
1593
|
+
lastFree,
|
|
1594
|
+
sCol,
|
|
1595
|
+
dir === 1 ? vocab.arrowDown : vocab.arrowUp,
|
|
1596
|
+
"arrow",
|
|
1597
|
+
vocab
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
if (biDir && gap >= 2) {
|
|
1601
|
+
canvas.drawEdgeCell(
|
|
1602
|
+
firstFree,
|
|
1603
|
+
sCol,
|
|
1604
|
+
dir === 1 ? vocab.arrowUp : vocab.arrowDown,
|
|
1605
|
+
"arrow",
|
|
1606
|
+
vocab
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
if (label) {
|
|
1610
|
+
args.deferSideLabel(
|
|
1611
|
+
middleOut(firstFree, lastFree).map((row) => ({ row, col: sCol })),
|
|
1612
|
+
label
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const midRow = clamp(
|
|
1619
|
+
Math.floor((exitRow + enterRow) / 2) + stagger,
|
|
1620
|
+
Math.min(firstFree, lastFree),
|
|
1621
|
+
Math.max(firstFree, lastFree)
|
|
1622
|
+
);
|
|
1623
|
+
junction(exitRow, exitCol, dir === 1 ? "bottom" : "top");
|
|
1624
|
+
junction(enterRow, enterCol, dir === 1 ? "top" : "bottom");
|
|
1625
|
+
if (Math.abs(midRow - exitRow) >= 2) drawV(exitCol, firstFree, midRow - dir);
|
|
1626
|
+
if (Math.abs(enterRow - midRow) >= 2) drawV(enterCol, midRow + dir, lastFree);
|
|
1627
|
+
let labelEmbedded = false;
|
|
1628
|
+
if (exitCol === enterCol) {
|
|
1629
|
+
canvas.drawEdgeCell(midRow, exitCol, vocab.v, "line-v", vocab);
|
|
1630
|
+
} else {
|
|
1631
|
+
const goingRight = enterCol > exitCol;
|
|
1632
|
+
const exitTurn = dir === 1 ? goingRight ? vocab.turnUpRight : vocab.turnUpLeft : goingRight ? vocab.turnDownRight : vocab.turnDownLeft;
|
|
1633
|
+
const enterTurn = dir === 1 ? goingRight ? vocab.turnDownLeft : vocab.turnDownRight : goingRight ? vocab.turnUpLeft : vocab.turnUpRight;
|
|
1634
|
+
canvas.drawEdgeCell(midRow, exitCol, exitTurn, "turn", vocab);
|
|
1635
|
+
canvas.drawEdgeCell(midRow, enterCol, enterTurn, "turn", vocab);
|
|
1636
|
+
const lo = Math.min(exitCol, enterCol);
|
|
1637
|
+
const hi = Math.max(exitCol, enterCol);
|
|
1638
|
+
let segment = [];
|
|
1639
|
+
for (let c = lo + 1; c <= hi - 1; c++) segment.push(vocab.h);
|
|
1640
|
+
if (label && segment.length >= label.length + 4) {
|
|
1641
|
+
const text = ` ${label} `;
|
|
1642
|
+
const left = Math.floor((segment.length - text.length) / 2);
|
|
1643
|
+
segment = segment.map((ch, i) => i >= left && i < left + text.length ? text[i - left] : ch);
|
|
1644
|
+
labelEmbedded = true;
|
|
1645
|
+
}
|
|
1646
|
+
segment.forEach((ch, i) => {
|
|
1647
|
+
canvas.drawEdgeCell(midRow, lo + 1 + i, ch, ch === vocab.h ? "line-h" : "label", vocab);
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
if (directed) {
|
|
1651
|
+
canvas.drawEdgeCell(
|
|
1652
|
+
lastFree,
|
|
1653
|
+
enterCol,
|
|
1654
|
+
dir === 1 ? vocab.arrowDown : vocab.arrowUp,
|
|
1655
|
+
"arrow",
|
|
1656
|
+
vocab
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
if (biDir) {
|
|
1660
|
+
canvas.drawEdgeCell(
|
|
1661
|
+
firstFree,
|
|
1662
|
+
exitCol,
|
|
1663
|
+
dir === 1 ? vocab.arrowUp : vocab.arrowDown,
|
|
1664
|
+
"arrow",
|
|
1665
|
+
vocab
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
if (label && !labelEmbedded) {
|
|
1669
|
+
const candidates = [];
|
|
1670
|
+
if (exitCol === enterCol) {
|
|
1671
|
+
for (const row of middleOut(firstFree, lastFree)) candidates.push({ row, col: exitCol });
|
|
1672
|
+
} else {
|
|
1673
|
+
const exitLegLen = (midRow - dir - firstFree) * dir + 1;
|
|
1674
|
+
const enterLegLen = (lastFree - (midRow + dir)) * dir + 1;
|
|
1675
|
+
if (exitLegLen >= 1) {
|
|
1676
|
+
for (const row of middleOut(firstFree, midRow - dir)) {
|
|
1677
|
+
candidates.push({ row, col: exitCol });
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (enterLegLen >= 1) {
|
|
1681
|
+
for (const row of middleOut(midRow + dir, lastFree)) {
|
|
1682
|
+
candidates.push({ row, col: enterCol });
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (candidates.length > 0) args.deferSideLabel(candidates, label);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function drawHorizontal(args, s, t, exitRow, enterRow, dir) {
|
|
1690
|
+
const { canvas, directed, biDir, label, vocab, junction } = args;
|
|
1691
|
+
const exitCol = dir === 1 ? s.c1 : s.c0;
|
|
1692
|
+
const enterCol = dir === 1 ? t.c0 : t.c1;
|
|
1693
|
+
const firstFree = exitCol + dir;
|
|
1694
|
+
const lastFree = enterCol - dir;
|
|
1695
|
+
const gap = (lastFree - firstFree) * dir + 1;
|
|
1696
|
+
const drawH = (row, cA, cB, hostLabel) => {
|
|
1697
|
+
const lo = Math.min(cA, cB);
|
|
1698
|
+
const hi = Math.max(cA, cB);
|
|
1699
|
+
let segment = [];
|
|
1700
|
+
for (let c = lo; c <= hi; c++) segment.push(vocab.h);
|
|
1701
|
+
if (hostLabel && label && segment.length >= label.length + 4) {
|
|
1702
|
+
const text = ` ${label} `;
|
|
1703
|
+
const left = Math.floor((segment.length - text.length) / 2);
|
|
1704
|
+
segment = segment.map((ch, i) => i >= left && i < left + text.length ? text[i - left] : ch);
|
|
1705
|
+
}
|
|
1706
|
+
segment.forEach((ch, i) => {
|
|
1707
|
+
canvas.drawEdgeCell(row, lo + i, ch, ch === vocab.h ? "line-h" : "label", vocab);
|
|
1708
|
+
});
|
|
1709
|
+
};
|
|
1710
|
+
const straightOk = exitRow >= t.r0 + 1 && exitRow <= t.r1 - 1 && exitRow >= s.r0 + 1 && exitRow <= s.r1 - 1;
|
|
1711
|
+
if (straightOk || gap < 2) {
|
|
1712
|
+
const sRow = clamp(exitRow, s.r0 + 1, s.r1 - 1);
|
|
1713
|
+
const tRow = clamp(exitRow, t.r0 + 1, t.r1 - 1);
|
|
1714
|
+
junction(sRow, exitCol, dir === 1 ? "right" : "left");
|
|
1715
|
+
junction(tRow, enterCol, dir === 1 ? "left" : "right");
|
|
1716
|
+
if (sRow === tRow && gap >= 1) {
|
|
1717
|
+
drawH(sRow, firstFree, lastFree, true);
|
|
1718
|
+
if (directed) {
|
|
1719
|
+
canvas.drawEdgeCell(
|
|
1720
|
+
sRow,
|
|
1721
|
+
lastFree,
|
|
1722
|
+
dir === 1 ? vocab.arrowRight : vocab.arrowLeft,
|
|
1723
|
+
"arrow",
|
|
1724
|
+
vocab
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
if (biDir && gap >= 2) {
|
|
1728
|
+
canvas.drawEdgeCell(
|
|
1729
|
+
sRow,
|
|
1730
|
+
firstFree,
|
|
1731
|
+
dir === 1 ? vocab.arrowLeft : vocab.arrowRight,
|
|
1732
|
+
"arrow",
|
|
1733
|
+
vocab
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const midCol = clamp(
|
|
1740
|
+
Math.floor((exitCol + enterCol) / 2) + args.stagger,
|
|
1741
|
+
Math.min(firstFree, lastFree),
|
|
1742
|
+
Math.max(firstFree, lastFree)
|
|
1743
|
+
);
|
|
1744
|
+
junction(exitRow, exitCol, dir === 1 ? "right" : "left");
|
|
1745
|
+
junction(enterRow, enterCol, dir === 1 ? "left" : "right");
|
|
1746
|
+
if (Math.abs(midCol - exitCol) >= 2) drawH(exitRow, firstFree, midCol - dir, true);
|
|
1747
|
+
if (Math.abs(enterCol - midCol) >= 2) drawH(enterRow, midCol + dir, lastFree, false);
|
|
1748
|
+
if (exitRow === enterRow) {
|
|
1749
|
+
canvas.drawEdgeCell(exitRow, midCol, vocab.h, "line-h", vocab);
|
|
1750
|
+
} else {
|
|
1751
|
+
const goingDown = enterRow > exitRow;
|
|
1752
|
+
const exitTurn = dir === 1 ? goingDown ? vocab.turnDownLeft : vocab.turnUpLeft : goingDown ? vocab.turnDownRight : vocab.turnUpRight;
|
|
1753
|
+
const enterTurn = dir === 1 ? goingDown ? vocab.turnUpRight : vocab.turnDownRight : goingDown ? vocab.turnUpLeft : vocab.turnDownLeft;
|
|
1754
|
+
canvas.drawEdgeCell(exitRow, midCol, exitTurn, "turn", vocab);
|
|
1755
|
+
canvas.drawEdgeCell(enterRow, midCol, enterTurn, "turn", vocab);
|
|
1756
|
+
const lo = Math.min(exitRow, enterRow);
|
|
1757
|
+
const hi = Math.max(exitRow, enterRow);
|
|
1758
|
+
for (let r = lo + 1; r <= hi - 1; r++) {
|
|
1759
|
+
canvas.drawEdgeCell(r, midCol, vocab.v, "line-v", vocab);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
if (directed) {
|
|
1763
|
+
canvas.drawEdgeCell(
|
|
1764
|
+
enterRow,
|
|
1765
|
+
lastFree,
|
|
1766
|
+
dir === 1 ? vocab.arrowRight : vocab.arrowLeft,
|
|
1767
|
+
"arrow",
|
|
1768
|
+
vocab
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
if (biDir) {
|
|
1772
|
+
canvas.drawEdgeCell(
|
|
1773
|
+
exitRow,
|
|
1774
|
+
firstFree,
|
|
1775
|
+
dir === 1 ? vocab.arrowLeft : vocab.arrowRight,
|
|
1776
|
+
"arrow",
|
|
1777
|
+
vocab
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function drawNested(args, s, t, side) {
|
|
1782
|
+
const { canvas, directed, vocab, junction } = args;
|
|
1783
|
+
if (side === "top" || side === "bottom") {
|
|
1784
|
+
const overlapLo = Math.max(s.c0 + 1, t.c0 + 1);
|
|
1785
|
+
const overlapHi = Math.min(s.c1 - 1, t.c1 - 1);
|
|
1786
|
+
if (overlapLo > overlapHi) return;
|
|
1787
|
+
const col = Math.floor((overlapLo + overlapHi) / 2);
|
|
1788
|
+
const sRow = side === "top" ? s.r0 : s.r1;
|
|
1789
|
+
const tRow = side === "top" ? t.r0 : t.r1;
|
|
1790
|
+
if (sRow === tRow) return;
|
|
1791
|
+
junction(sRow, col, side);
|
|
1792
|
+
junction(tRow, col, side);
|
|
1793
|
+
const lo = Math.min(sRow, tRow);
|
|
1794
|
+
const hi = Math.max(sRow, tRow);
|
|
1795
|
+
for (let r = lo + 1; r <= hi - 1; r++) canvas.drawEdgeCell(r, col, vocab.v, "line-v", vocab);
|
|
1796
|
+
if (directed && hi - lo >= 2) {
|
|
1797
|
+
const arrowRow = tRow > sRow ? tRow - 1 : tRow + 1;
|
|
1798
|
+
canvas.drawEdgeCell(
|
|
1799
|
+
arrowRow,
|
|
1800
|
+
col,
|
|
1801
|
+
tRow > sRow ? vocab.arrowDown : vocab.arrowUp,
|
|
1802
|
+
"arrow",
|
|
1803
|
+
vocab
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
} else {
|
|
1807
|
+
const overlapLo = Math.max(s.r0 + 1, t.r0 + 1);
|
|
1808
|
+
const overlapHi = Math.min(s.r1 - 1, t.r1 - 1);
|
|
1809
|
+
if (overlapLo > overlapHi) return;
|
|
1810
|
+
const row = Math.floor((overlapLo + overlapHi) / 2);
|
|
1811
|
+
const sCol = side === "left" ? s.c0 : s.c1;
|
|
1812
|
+
const tCol = side === "left" ? t.c0 : t.c1;
|
|
1813
|
+
if (sCol === tCol) return;
|
|
1814
|
+
junction(row, sCol, side);
|
|
1815
|
+
junction(row, tCol, side);
|
|
1816
|
+
const lo = Math.min(sCol, tCol);
|
|
1817
|
+
const hi = Math.max(sCol, tCol);
|
|
1818
|
+
for (let c = lo + 1; c <= hi - 1; c++) canvas.drawEdgeCell(row, c, vocab.h, "line-h", vocab);
|
|
1819
|
+
if (directed && hi - lo >= 2) {
|
|
1820
|
+
const arrowCol = tCol > sCol ? tCol - 1 : tCol + 1;
|
|
1821
|
+
canvas.drawEdgeCell(
|
|
1822
|
+
row,
|
|
1823
|
+
arrowCol,
|
|
1824
|
+
tCol > sCol ? vocab.arrowRight : vocab.arrowLeft,
|
|
1825
|
+
"arrow",
|
|
1826
|
+
vocab
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// src/doc/asciiDiagram/repair.ts
|
|
1833
|
+
var MIN_REPAIR_BOXES = 2;
|
|
1834
|
+
var MAX_EDGE_ROW_GAP = 8;
|
|
1835
|
+
var MAX_EDGE_COL_GAP = 12;
|
|
1836
|
+
var BAND_ROW_GAP = 4;
|
|
1837
|
+
var BAND_COL_GAP = 4;
|
|
1838
|
+
function isRepairableDiagram(text) {
|
|
1839
|
+
if (detectAsciiDiagram(text).isDiagram) return false;
|
|
1840
|
+
const grid = toGrid(text);
|
|
1841
|
+
const rects = traceBoxes(grid, true);
|
|
1842
|
+
return dedupeRects(rects).length >= MIN_REPAIR_BOXES;
|
|
1843
|
+
}
|
|
1844
|
+
function repairAsciiDiagram(text) {
|
|
1845
|
+
const grid = toGrid(text);
|
|
1846
|
+
const rects = dedupeRects(traceBoxes(grid, true));
|
|
1847
|
+
if (rects.length < MIN_REPAIR_BOXES) return null;
|
|
1848
|
+
const nodes = buildNodes(grid, rects);
|
|
1849
|
+
const edges = recoverEdges(grid, rects, nodes);
|
|
1850
|
+
const style = detectGridStyle(grid);
|
|
1851
|
+
const diagram = { nodes, edges, style, warnings: ["repaired"] };
|
|
1852
|
+
return { diagram, art: renderAsciiDiagram(diagram) };
|
|
1853
|
+
}
|
|
1854
|
+
function dedupeRects(rects) {
|
|
1855
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1856
|
+
const out = [];
|
|
1857
|
+
for (const r of rects) {
|
|
1858
|
+
const key = `${r.r0},${r.c0},${r.r1},${r.c1}`;
|
|
1859
|
+
if (seen.has(key)) continue;
|
|
1860
|
+
seen.add(key);
|
|
1861
|
+
out.push(r);
|
|
1862
|
+
}
|
|
1863
|
+
return out.sort((a, b) => a.r0 - b.r0 || a.c0 - b.c0);
|
|
1864
|
+
}
|
|
1865
|
+
function buildNodes(grid, rects) {
|
|
1866
|
+
const W = grid[0]?.length ?? 0;
|
|
1867
|
+
const labels = rects.map(() => []);
|
|
1868
|
+
const index = new Map(rects.map((r, i) => [r, i]));
|
|
1869
|
+
const bands = [];
|
|
1870
|
+
for (const r of rects) {
|
|
1871
|
+
let band = bands.find((bd) => bd.some((x) => x.r0 <= r.r1 && r.r0 <= x.r1));
|
|
1872
|
+
if (!band) {
|
|
1873
|
+
band = [];
|
|
1874
|
+
bands.push(band);
|
|
1875
|
+
}
|
|
1876
|
+
band.push(r);
|
|
1877
|
+
}
|
|
1878
|
+
for (const band of bands) {
|
|
1879
|
+
band.sort((a, b) => a.c0 - b.c0);
|
|
1880
|
+
const minR = Math.min(...band.map((b) => b.r0 + 1));
|
|
1881
|
+
const maxR = Math.max(...band.map((b) => b.r1 - 1));
|
|
1882
|
+
for (let r = minR; r <= maxR; r++) {
|
|
1883
|
+
const inRow = band.filter((b) => r > b.r0 && r < b.r1);
|
|
1884
|
+
if (inRow.length === 0) continue;
|
|
1885
|
+
const rowText = grid[r].join("");
|
|
1886
|
+
const segs = rowText.split(/[│┃║]/).map(cleanLabelText).filter((s) => s.length > 0);
|
|
1887
|
+
if (segs.length === inRow.length) {
|
|
1888
|
+
inRow.forEach((b, i) => {
|
|
1889
|
+
const idx = index.get(b);
|
|
1890
|
+
if (idx !== void 0 && segs[i]) labels[idx].push(segs[i]);
|
|
1891
|
+
});
|
|
1892
|
+
} else {
|
|
1893
|
+
for (const b of inRow) {
|
|
1894
|
+
const idx = index.get(b);
|
|
1895
|
+
if (idx === void 0) continue;
|
|
1896
|
+
const t = cleanLabelText(grid[r].slice(b.c0 + 1, Math.min(b.c1, W)).join(""));
|
|
1897
|
+
if (t) labels[idx].push(t);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const bandRow = /* @__PURE__ */ new Map();
|
|
1903
|
+
const orderedBands = [...bands].sort(
|
|
1904
|
+
(a, b) => Math.min(...a.map((x) => x.r0)) - Math.min(...b.map((x) => x.r0))
|
|
1905
|
+
);
|
|
1906
|
+
let rowCursor = 0;
|
|
1907
|
+
for (const band of orderedBands) {
|
|
1908
|
+
bandRow.set(band, rowCursor);
|
|
1909
|
+
const bandHeight = Math.max(...band.map((b) => b.r1 - b.r0 + 1));
|
|
1910
|
+
rowCursor += bandHeight + BAND_ROW_GAP;
|
|
1911
|
+
}
|
|
1912
|
+
const rectBand = /* @__PURE__ */ new Map();
|
|
1913
|
+
for (const band of bands) for (const r of band) rectBand.set(r, band);
|
|
1914
|
+
const fittedW = rects.map(
|
|
1915
|
+
(rect, i) => Math.max(rect.c1 - rect.c0 + 1, ...labels[i].map((l) => l.length + 4), 3)
|
|
1916
|
+
);
|
|
1917
|
+
const rectIndex = new Map(rects.map((r, i) => [r, i]));
|
|
1918
|
+
const bandCol = /* @__PURE__ */ new Map();
|
|
1919
|
+
for (const band of bands) {
|
|
1920
|
+
const ordered = [...band].sort((a, b) => a.c0 - b.c0);
|
|
1921
|
+
let colCursor = Math.min(...band.map((b) => b.c0));
|
|
1922
|
+
for (const r of ordered) {
|
|
1923
|
+
bandCol.set(r, colCursor);
|
|
1924
|
+
colCursor += (fittedW[rectIndex.get(r) ?? 0] ?? 3) + BAND_COL_GAP;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
const parentIdx = rects.map((_, i) => smallestContainer(rects, i));
|
|
1928
|
+
const usedIds = /* @__PURE__ */ new Map();
|
|
1929
|
+
const nodes = rects.map((rect, i) => {
|
|
1930
|
+
let id = slugify2(labels[i][0] ?? "");
|
|
1931
|
+
const n = usedIds.get(id) ?? 0;
|
|
1932
|
+
usedIds.set(id, n + 1);
|
|
1933
|
+
if (n > 0) id = `${id}-${n + 1}`;
|
|
1934
|
+
const band = rectBand.get(rect);
|
|
1935
|
+
const nested = parentIdx[i] >= 0;
|
|
1936
|
+
const row = band && !nested ? bandRow.get(band) ?? rect.r0 : rect.r0;
|
|
1937
|
+
const col = !nested ? bandCol.get(rect) ?? rect.c0 : rect.c0;
|
|
1938
|
+
return {
|
|
1939
|
+
id,
|
|
1940
|
+
label: labels[i].join("\n"),
|
|
1941
|
+
col,
|
|
1942
|
+
row,
|
|
1943
|
+
wCols: rect.c1 - rect.c0 + 1,
|
|
1944
|
+
hRows: rect.r1 - rect.r0 + 1
|
|
1945
|
+
};
|
|
1946
|
+
});
|
|
1947
|
+
nodes.forEach((node, i) => {
|
|
1948
|
+
if (parentIdx[i] >= 0) node.containerId = nodes[parentIdx[i]].id;
|
|
1949
|
+
});
|
|
1950
|
+
return nodes;
|
|
1951
|
+
}
|
|
1952
|
+
function cleanLabelText(seg) {
|
|
1953
|
+
let out = "";
|
|
1954
|
+
for (const ch of seg) {
|
|
1955
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1956
|
+
const box = code >= 9472 && code <= 9599;
|
|
1957
|
+
if (box || arrowDirection(ch) !== null) out += " ";
|
|
1958
|
+
else out += ch;
|
|
1959
|
+
}
|
|
1960
|
+
return out.replace(/\s+/g, " ").trim();
|
|
1961
|
+
}
|
|
1962
|
+
function smallestContainer(rects, i) {
|
|
1963
|
+
const child = rects[i];
|
|
1964
|
+
let best = -1;
|
|
1965
|
+
let bestArea = Infinity;
|
|
1966
|
+
for (let j = 0; j < rects.length; j++) {
|
|
1967
|
+
if (j === i) continue;
|
|
1968
|
+
const p = rects[j];
|
|
1969
|
+
if (p.r0 < child.r0 && p.c0 < child.c0 && p.r1 > child.r1 && p.c1 > child.c1) {
|
|
1970
|
+
const a = (p.r1 - p.r0) * (p.c1 - p.c0);
|
|
1971
|
+
if (a < bestArea) {
|
|
1972
|
+
bestArea = a;
|
|
1973
|
+
best = j;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return best;
|
|
1978
|
+
}
|
|
1979
|
+
function slugify2(s) {
|
|
1980
|
+
const slug2 = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1981
|
+
return slug2 || "node";
|
|
1982
|
+
}
|
|
1983
|
+
function recoverEdges(grid, rects, nodes) {
|
|
1984
|
+
const H = grid.length;
|
|
1985
|
+
const W = grid[0]?.length ?? 0;
|
|
1986
|
+
const at = (r, c) => r >= 0 && r < H && c >= 0 && c < W ? grid[r][c] : " ";
|
|
1987
|
+
const isConnector = (ch) => isVert(ch) || isHoriz(ch) || isJunction(ch) || arrowDirection(ch) !== null;
|
|
1988
|
+
const colOverlap = (a, b) => Math.max(a.c0, b.c0) <= Math.min(a.c1, b.c1);
|
|
1989
|
+
const rowOverlap = (a, b) => Math.max(a.r0, b.r0) <= Math.min(a.r1, b.r1);
|
|
1990
|
+
const edges = [];
|
|
1991
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1992
|
+
const push = (s, t, directed) => {
|
|
1993
|
+
if (s === t) return;
|
|
1994
|
+
const key = `${nodes[s].id}|${nodes[t].id}`;
|
|
1995
|
+
if (seen.has(key)) return;
|
|
1996
|
+
seen.add(key);
|
|
1997
|
+
edges.push({ source: nodes[s].id, target: nodes[t].id, directed });
|
|
1998
|
+
};
|
|
1999
|
+
for (let i = 0; i < rects.length; i++) {
|
|
2000
|
+
for (let j = 0; j < rects.length; j++) {
|
|
2001
|
+
if (i === j) continue;
|
|
2002
|
+
const A = rects[i];
|
|
2003
|
+
const B = rects[j];
|
|
2004
|
+
if (A.r1 < B.r0 && B.r0 - A.r1 <= MAX_EDGE_ROW_GAP && colOverlap(A, B)) {
|
|
2005
|
+
const lo = Math.max(A.c0, B.c0);
|
|
2006
|
+
const hi = Math.min(A.c1, B.c1);
|
|
2007
|
+
if (gapHasConnector(at, A.r1 + 1, B.r0 - 1, lo - 1, hi + 1, isConnector) && !boxBetweenVert(rects, i, j, lo, hi)) {
|
|
2008
|
+
const directed = gapHasArrow(at, A.r1 + 1, B.r0, lo - 1, hi + 1, "down");
|
|
2009
|
+
push(i, j, directed);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
if (A.c1 < B.c0 && B.c0 - A.c1 <= MAX_EDGE_COL_GAP && rowOverlap(A, B) && i < j) {
|
|
2013
|
+
const lo = Math.max(A.r0, B.r0);
|
|
2014
|
+
const hi = Math.min(A.r1, B.r1);
|
|
2015
|
+
const rightArrow = gapHasArrow(at, lo - 1, hi + 1, A.c1 + 1, B.c0, "right");
|
|
2016
|
+
const leftArrow = gapHasArrow(at, lo - 1, hi + 1, A.c1 + 1, B.c0, "left");
|
|
2017
|
+
if ((rightArrow || leftArrow) && !boxBetweenHoriz(rects, i, j, lo, hi)) {
|
|
2018
|
+
if (leftArrow && !rightArrow) push(j, i, true);
|
|
2019
|
+
else push(i, j, true);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return edges;
|
|
2025
|
+
}
|
|
2026
|
+
function gapHasConnector(at, r0, r1, c0, c1, isConnector) {
|
|
2027
|
+
for (let r = r0; r <= r1; r++)
|
|
2028
|
+
for (let c = c0; c <= c1; c++) if (isConnector(at(r, c))) return true;
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
2031
|
+
function gapHasArrow(at, r0, r1, c0, c1, dir) {
|
|
2032
|
+
for (let r = r0; r <= r1; r++) {
|
|
2033
|
+
for (let c = c0; c <= c1; c++) {
|
|
2034
|
+
if (arrowDirection(at(r, c)) === dir) return true;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
2039
|
+
function boxBetweenVert(rects, i, j, lo, hi) {
|
|
2040
|
+
const A = rects[i];
|
|
2041
|
+
const B = rects[j];
|
|
2042
|
+
return rects.some((k, ki) => {
|
|
2043
|
+
if (ki === i || ki === j) return false;
|
|
2044
|
+
return k.r0 > A.r1 && k.r1 < B.r0 && Math.max(k.c0, lo) <= Math.min(k.c1, hi);
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
function boxBetweenHoriz(rects, i, j, lo, hi) {
|
|
2048
|
+
const A = rects[i];
|
|
2049
|
+
const B = rects[j];
|
|
2050
|
+
return rects.some((k, ki) => {
|
|
2051
|
+
if (ki === i || ki === j) return false;
|
|
2052
|
+
return k.c0 > A.c1 && k.c1 < B.c0 && Math.max(k.r0, lo) <= Math.min(k.r1, hi);
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
function detectGridStyle(grid) {
|
|
2056
|
+
let unicode = 0;
|
|
2057
|
+
let ascii = 0;
|
|
2058
|
+
for (const row of grid) {
|
|
2059
|
+
for (const ch of row) {
|
|
2060
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
2061
|
+
if (code >= 9472 && code <= 9599) unicode++;
|
|
2062
|
+
else if (ch === "+" || ch === "|" || ch === "-") ascii++;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return ascii > unicode ? "ascii" : "unicode";
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/doc/asciiTimeline/render.ts
|
|
2069
|
+
var UNICODE_VOCAB2 = {
|
|
2070
|
+
rail: "\u2500",
|
|
2071
|
+
arrow: "\u25BA",
|
|
2072
|
+
marker: {
|
|
2073
|
+
filled: "\u25CF",
|
|
2074
|
+
hollow: "\u25CB",
|
|
2075
|
+
diamond: "\u25C6"
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
var ASCII_VOCAB2 = {
|
|
2079
|
+
rail: "-",
|
|
2080
|
+
arrow: ">",
|
|
2081
|
+
marker: {
|
|
2082
|
+
filled: "*",
|
|
2083
|
+
hollow: "o",
|
|
2084
|
+
// The parser's ASCII vocabulary has no distinct diamond point. Folding
|
|
2085
|
+
// it to `*` is the canonical ASCII representation and is byte-stable.
|
|
2086
|
+
diamond: "*"
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
var MIN_AXIS_WIDTH = 48;
|
|
2090
|
+
var MAX_COORDINATE_AXIS_WIDTH = 64;
|
|
2091
|
+
var EVENT_SPACING = 3;
|
|
2092
|
+
var TERMINAL_RAIL_LENGTH = 3;
|
|
2093
|
+
function renderAsciiTimeline(timeline, options = {}) {
|
|
2094
|
+
const style = options.style ?? timeline.style;
|
|
2095
|
+
const vocab = style === "ascii" ? ASCII_VOCAB2 : UNICODE_VOCAB2;
|
|
2096
|
+
const { tracks, links } = prepareTimeline(timeline, style);
|
|
2097
|
+
if (tracks.length === 0) return "";
|
|
2098
|
+
const globalStart = Math.min(...tracks.map((track) => track.startColumn));
|
|
2099
|
+
const globalEnd = Math.max(...tracks.map((track) => track.endColumn));
|
|
2100
|
+
const globalSpan = Math.max(1, globalEnd - globalStart);
|
|
2101
|
+
const axisWidth = Math.max(
|
|
2102
|
+
MIN_AXIS_WIDTH,
|
|
2103
|
+
Math.min(MAX_COORDINATE_AXIS_WIDTH, Math.ceil(globalSpan)),
|
|
2104
|
+
...tracks.map((track) => track.events.length * EVENT_SPACING)
|
|
2105
|
+
);
|
|
2106
|
+
const prefixes = tracks.map((track) => renderTrackPrefix(track));
|
|
2107
|
+
const prefixWidth = Math.max(...prefixes.map(displayWidth));
|
|
2108
|
+
const lines = [];
|
|
2109
|
+
tracks.forEach((track, index) => {
|
|
2110
|
+
if (index > 0) lines.push("");
|
|
2111
|
+
lines.push(
|
|
2112
|
+
...renderTrack(
|
|
2113
|
+
track,
|
|
2114
|
+
style,
|
|
2115
|
+
vocab,
|
|
2116
|
+
prefixes[index],
|
|
2117
|
+
prefixWidth,
|
|
2118
|
+
axisWidth,
|
|
2119
|
+
globalStart,
|
|
2120
|
+
globalSpan
|
|
2121
|
+
)
|
|
2122
|
+
);
|
|
2123
|
+
});
|
|
2124
|
+
if (links.length > 0) {
|
|
2125
|
+
lines.push("");
|
|
2126
|
+
for (const link of links) lines.push(renderLink(link, style));
|
|
2127
|
+
}
|
|
2128
|
+
return lines.join("\n");
|
|
2129
|
+
}
|
|
2130
|
+
function renderTrack(track, style, vocab, prefix, prefixWidth, axisWidth, globalStart, globalSpan) {
|
|
2131
|
+
const paddedPrefix = `${prefix}${" ".repeat(Math.max(0, prefixWidth - displayWidth(prefix)))}`;
|
|
2132
|
+
const positioned = positionEvents(track.events, axisWidth, globalStart, globalSpan);
|
|
2133
|
+
const rail = Array.from({ length: axisWidth + 1 }, () => vocab.rail);
|
|
2134
|
+
for (const { event, offset } of positioned) rail[offset] = vocab.marker[event.marker];
|
|
2135
|
+
const callouts = positioned.map(({ event, offset }) => ({
|
|
2136
|
+
event,
|
|
2137
|
+
column: prefixWidth + offset,
|
|
2138
|
+
text: renderEventPayload(event)
|
|
2139
|
+
}));
|
|
2140
|
+
const pointer = style === "ascii" ? { above: "^", below: "v" } : { above: "\u25B2", below: "\u25BC" };
|
|
2141
|
+
const above = renderCalloutLanes(
|
|
2142
|
+
callouts.filter(({ event }) => event.side === "above"),
|
|
2143
|
+
pointer.above
|
|
2144
|
+
);
|
|
2145
|
+
const below = renderCalloutLanes(
|
|
2146
|
+
callouts.filter(({ event }) => event.side === "below"),
|
|
2147
|
+
pointer.below
|
|
2148
|
+
);
|
|
2149
|
+
const axis = `${paddedPrefix}${rail.join("")}${vocab.rail.repeat(TERMINAL_RAIL_LENGTH)}${vocab.arrow}${track.endLabel ? ` ${track.endLabel}` : ""}`;
|
|
2150
|
+
return [...above, axis, ...below];
|
|
2151
|
+
}
|
|
2152
|
+
function renderEventPayload(event) {
|
|
2153
|
+
const body = event.description ? `${event.label} :: ${event.description}` : event.label;
|
|
2154
|
+
const descriptionSide = event.descriptionSide ? ` descriptionSide=${event.descriptionSide}` : "";
|
|
2155
|
+
const callout = event.callout === false ? " callout=false" : "";
|
|
2156
|
+
return `${body} {#${event.id} column=${formatCoordinate(event.column)}${descriptionSide}${callout}}`;
|
|
2157
|
+
}
|
|
2158
|
+
function renderTrackPrefix(track) {
|
|
2159
|
+
const metadata = `{#${track.id} start=${formatCoordinate(track.startColumn)} end=${formatCoordinate(track.endColumn)}}`;
|
|
2160
|
+
return track.label ? `${track.label} ${metadata}: ` : `${metadata}: `;
|
|
2161
|
+
}
|
|
2162
|
+
function positionEvents(events, axisWidth, globalStart, globalSpan) {
|
|
2163
|
+
let previous = -1;
|
|
2164
|
+
return events.map((event, index) => {
|
|
2165
|
+
const ideal = Math.round((event.column - globalStart) / globalSpan * axisWidth);
|
|
2166
|
+
const remaining = events.length - index - 1;
|
|
2167
|
+
const latest = Math.max(0, axisWidth - remaining);
|
|
2168
|
+
const offset = Math.max(previous + 1, Math.min(latest, Math.max(0, ideal)));
|
|
2169
|
+
previous = offset;
|
|
2170
|
+
return { event, offset };
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
function renderCalloutLanes(callouts, pointer) {
|
|
2174
|
+
const lanes = [];
|
|
2175
|
+
const laneEnds = [];
|
|
2176
|
+
for (const callout of callouts) {
|
|
2177
|
+
const end = callout.column + 2 + displayWidth(callout.text);
|
|
2178
|
+
let lane = laneEnds.findIndex((laneEnd) => callout.column > laneEnd + 1);
|
|
2179
|
+
if (lane < 0) {
|
|
2180
|
+
lane = lanes.length;
|
|
2181
|
+
lanes.push([]);
|
|
2182
|
+
laneEnds.push(-1);
|
|
2183
|
+
}
|
|
2184
|
+
lanes[lane].push(callout);
|
|
2185
|
+
laneEnds[lane] = end;
|
|
2186
|
+
}
|
|
2187
|
+
return lanes.map((lane) => {
|
|
2188
|
+
const chars = [];
|
|
2189
|
+
for (const callout of lane) {
|
|
2190
|
+
while (chars.length < callout.column) chars.push(" ");
|
|
2191
|
+
chars.push(pointer, " ", ...Array.from(callout.text));
|
|
2192
|
+
}
|
|
2193
|
+
return chars.join("").trimEnd();
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
function displayWidth(value) {
|
|
2197
|
+
return Array.from(value).length;
|
|
2198
|
+
}
|
|
2199
|
+
function renderLink(link, style) {
|
|
2200
|
+
const arrow = style === "ascii" ? "->" : "\u2192";
|
|
2201
|
+
return `branch: ${link.source} ${arrow} ${link.target}${link.label ? ` : ${link.label}` : ""}`;
|
|
2202
|
+
}
|
|
2203
|
+
function prepareTimeline(timeline, style) {
|
|
2204
|
+
const sourceTracks = timeline.tracks.map((track, index) => ({ track, index })).filter(({ track }) => Array.isArray(track.events) && track.events.length > 0).sort(
|
|
2205
|
+
(a, b) => finiteOr(a.track.row, a.index) - finiteOr(b.track.row, b.index) || a.index - b.index || a.track.id.localeCompare(b.track.id)
|
|
2206
|
+
);
|
|
2207
|
+
const usedTrackIds = /* @__PURE__ */ new Set();
|
|
2208
|
+
const usedEventIds = /* @__PURE__ */ new Set();
|
|
2209
|
+
const endpointIds = /* @__PURE__ */ new Map();
|
|
2210
|
+
const labelIds = /* @__PURE__ */ new Map();
|
|
2211
|
+
const tracks = [];
|
|
2212
|
+
let fallbackIndex = 0;
|
|
2213
|
+
for (const { track } of sourceTracks) {
|
|
2214
|
+
const trackLabel = canonicalInlineText(track.label);
|
|
2215
|
+
const endLabel = canonicalInlineText(track.endLabel);
|
|
2216
|
+
const trackId = uniqueId(
|
|
2217
|
+
canonicalId(track.id, canonicalId(trackLabel, `track_${tracks.length + 1}`)),
|
|
2218
|
+
usedTrackIds
|
|
2219
|
+
);
|
|
2220
|
+
const sortedEvents = track.events.map((event, index) => ({ event, index })).sort(
|
|
2221
|
+
(a, b) => finiteOr(a.event.column, a.index) - finiteOr(b.event.column, b.index) || a.index - b.index || a.event.id.localeCompare(b.event.id)
|
|
2222
|
+
);
|
|
2223
|
+
const events = [];
|
|
2224
|
+
for (let index = 0; index < sortedEvents.length; index++) {
|
|
2225
|
+
const event = sortedEvents[index].event;
|
|
2226
|
+
const fallbackLabel = `Event ${index + 1}`;
|
|
2227
|
+
const label = canonicalInlineText(event.label) || fallbackLabel;
|
|
2228
|
+
const idBase = canonicalId(event.id, canonicalId(label, `event_${++fallbackIndex}`));
|
|
2229
|
+
const id = uniqueId(idBase, usedEventIds);
|
|
2230
|
+
const description = canonicalInlineText(event.description);
|
|
2231
|
+
const descriptionSide = description && (event.descriptionSide === "above" || event.descriptionSide === "below") ? event.descriptionSide : void 0;
|
|
2232
|
+
const column = canonicalCoordinate(event.column, index);
|
|
2233
|
+
const side = event.side === "below" ? "below" : event.side === "above" ? "above" : index % 2 === 0 ? "above" : "below";
|
|
2234
|
+
const marker = canonicalMarker(event.marker, style);
|
|
2235
|
+
events.push({
|
|
2236
|
+
id,
|
|
2237
|
+
label,
|
|
2238
|
+
...description ? { description } : {},
|
|
2239
|
+
...descriptionSide ? { descriptionSide } : {},
|
|
2240
|
+
column,
|
|
2241
|
+
side,
|
|
2242
|
+
...event.callout === false ? { callout: false } : {},
|
|
2243
|
+
marker
|
|
2244
|
+
});
|
|
2245
|
+
if (event.id && !endpointIds.has(event.id)) endpointIds.set(event.id, id);
|
|
2246
|
+
endpointIds.set(id, id);
|
|
2247
|
+
labelIds.set(slug(label), id);
|
|
2248
|
+
}
|
|
2249
|
+
const eventColumns = events.map((event) => event.column);
|
|
2250
|
+
const fallbackStart = eventColumns.length > 0 ? Math.min(...eventColumns) : 0;
|
|
2251
|
+
const fallbackEnd = eventColumns.length > 0 ? Math.max(...eventColumns) : fallbackStart + 1;
|
|
2252
|
+
tracks.push({
|
|
2253
|
+
id: trackId,
|
|
2254
|
+
...trackLabel ? { label: trackLabel } : {},
|
|
2255
|
+
...endLabel ? { endLabel } : {},
|
|
2256
|
+
startColumn: canonicalCoordinate(track.startColumn, fallbackStart),
|
|
2257
|
+
endColumn: canonicalCoordinate(track.endColumn, fallbackEnd),
|
|
2258
|
+
events
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
const resolveEndpoint = (value) => endpointIds.get(value) ?? labelIds.get(slug(value));
|
|
2262
|
+
const links = [];
|
|
2263
|
+
for (const link of Array.isArray(timeline.links) ? timeline.links : []) {
|
|
2264
|
+
const source = resolveEndpoint(link.source);
|
|
2265
|
+
const target = resolveEndpoint(link.target);
|
|
2266
|
+
if (!source || !target) continue;
|
|
2267
|
+
const label = canonicalBranchText(link.label);
|
|
2268
|
+
links.push({ source, target, ...label ? { label } : {} });
|
|
2269
|
+
}
|
|
2270
|
+
links.sort(
|
|
2271
|
+
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || (a.label ?? "").localeCompare(b.label ?? "")
|
|
2272
|
+
);
|
|
2273
|
+
return { tracks, links };
|
|
2274
|
+
}
|
|
2275
|
+
function canonicalMarker(marker, style) {
|
|
2276
|
+
if (marker === "hollow") return "hollow";
|
|
2277
|
+
if (marker === "diamond" && style !== "ascii") return "diamond";
|
|
2278
|
+
return "filled";
|
|
2279
|
+
}
|
|
2280
|
+
function canonicalInlineText(value) {
|
|
2281
|
+
if (!value) return "";
|
|
2282
|
+
const normalized = value.normalize("NFC").replace(/[\r\n\t]+/g, " ").replace(/[\u25cf\u25cb\u25c9\u25c6\u25c7\u2022]+/gu, "\xB7").replace(/\*/g, "\u2217").replace(/::/g, "\u2236").replace(/\s+/g, " ").trim();
|
|
2283
|
+
return normalized.replace(
|
|
2284
|
+
/^[\u2500\u2501\u2550\u254c\u254d\u2504\u2505\u2508\u2509-]+/u,
|
|
2285
|
+
(run) => Array.from(run, (character) => character === "-" ? "\u2212" : "\u2013").join("")
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
function canonicalBranchText(value) {
|
|
2289
|
+
return canonicalInlineText(value).replace(/\s+/g, " ").trim();
|
|
2290
|
+
}
|
|
2291
|
+
function canonicalId(value, fallback) {
|
|
2292
|
+
const normalized = (value ?? "").normalize("NFC").replace(/[\r\n\t\s]+/g, "_").replace(/[^A-Za-z0-9_.~-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2293
|
+
return normalized || fallback;
|
|
2294
|
+
}
|
|
2295
|
+
function uniqueId(base, used) {
|
|
2296
|
+
if (!used.has(base)) {
|
|
2297
|
+
used.add(base);
|
|
2298
|
+
return base;
|
|
2299
|
+
}
|
|
2300
|
+
let suffix = 2;
|
|
2301
|
+
let candidate = `${base}_${suffix}`;
|
|
2302
|
+
while (used.has(candidate)) candidate = `${base}_${++suffix}`;
|
|
2303
|
+
used.add(candidate);
|
|
2304
|
+
return candidate;
|
|
2305
|
+
}
|
|
2306
|
+
function slug(value) {
|
|
2307
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "event";
|
|
2308
|
+
}
|
|
2309
|
+
function finiteOr(value, fallback) {
|
|
2310
|
+
return Number.isFinite(value) ? value : fallback;
|
|
2311
|
+
}
|
|
2312
|
+
function canonicalCoordinate(value, fallback) {
|
|
2313
|
+
const finite = finiteOr(value, fallback);
|
|
2314
|
+
return Math.max(0, Object.is(finite, -0) ? 0 : finite);
|
|
2315
|
+
}
|
|
2316
|
+
function formatCoordinate(value) {
|
|
2317
|
+
return String(Object.is(value, -0) ? 0 : value);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// src/doc/treeview/render.ts
|
|
2321
|
+
function renderTree(tree, options = {}) {
|
|
2322
|
+
const vocab = (options.style ?? tree.style) === "ascii" ? ASCII_TREE_VOCAB : UNICODE_TREE_VOCAB;
|
|
2323
|
+
const lines = [];
|
|
2324
|
+
const label = (node) => node.comment ? `${node.label} # ${node.comment}` : node.label;
|
|
2325
|
+
const walk = (nodes, prefix) => {
|
|
2326
|
+
nodes.forEach((node, i) => {
|
|
2327
|
+
const last = i === nodes.length - 1;
|
|
2328
|
+
lines.push(prefix + (last ? vocab.elbow : vocab.tee) + label(node));
|
|
2329
|
+
walk(node.children, prefix + (last ? vocab.gap : vocab.rail));
|
|
2330
|
+
});
|
|
2331
|
+
};
|
|
2332
|
+
for (const root of tree.roots) {
|
|
2333
|
+
lines.push(label(root));
|
|
2334
|
+
walk(root.children, "");
|
|
2335
|
+
}
|
|
2336
|
+
return lines.join("\n");
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
export {
|
|
2340
|
+
getAnimationStyle,
|
|
2341
|
+
getDefaultAnimationDuration,
|
|
2342
|
+
getTransitionClass,
|
|
2343
|
+
getAnimationProgress,
|
|
2344
|
+
cssFilterForTreatment,
|
|
2345
|
+
docToMarkdown,
|
|
2346
|
+
resolveThemeForDoc,
|
|
2347
|
+
applyNarrationTiming,
|
|
2348
|
+
scoreTextSimilarity,
|
|
2349
|
+
resolveAudioMapping,
|
|
2350
|
+
validateMarkdownSource,
|
|
2351
|
+
validateMarkdownDoc,
|
|
2352
|
+
renderAsciiDiagram,
|
|
2353
|
+
isRepairableDiagram,
|
|
2354
|
+
repairAsciiDiagram,
|
|
2355
|
+
renderAsciiTimeline,
|
|
2356
|
+
renderTree
|
|
2357
|
+
};
|
|
2358
|
+
//# sourceMappingURL=chunk-TFJVXUHH.js.map
|