@bendyline/squisq 1.5.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -10
- package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
- package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
- package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
- package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-4X3JQXNM.js +35 -0
- package/dist/chunk-4X3JQXNM.js.map +1 -0
- package/dist/{chunk-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-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
- package/dist/chunk-6PJRB2CF.js.map +1 -0
- package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
- package/dist/chunk-AMIR72JP.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-5TYS5V3G.js → chunk-GF3BVNLU.js} +4127 -3538
- package/dist/chunk-GF3BVNLU.js.map +1 -0
- package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
- package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
- package/dist/chunk-JOQYPRDZ.js +127 -0
- package/dist/chunk-JOQYPRDZ.js.map +1 -0
- package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
- package/dist/chunk-LYNJEOAC.js.map +1 -0
- package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
- package/dist/chunk-PXLVXTLW.js.map +1 -0
- package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
- package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
- package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
- package/dist/chunk-QTSJOY54.js.map +1 -0
- package/dist/chunk-QVANWDPH.js +2358 -0
- package/dist/chunk-QVANWDPH.js.map +1 -0
- package/dist/{chunk-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-T3TI7GSZ.js +884 -0
- package/dist/chunk-T3TI7GSZ.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/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__/dataTable.test.ts +25 -15
- package/src/__tests__/diagramBlock.test.ts +3 -3
- package/src/__tests__/diagramText.test.ts +26 -0
- package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
- package/src/__tests__/drawingBlock.test.ts +3 -3
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
- package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
- package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
- package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
- package/src/__tests__/getLayers.test.ts +32 -18
- package/src/__tests__/getLayersFallback.test.ts +8 -8
- package/src/__tests__/imageEditVersions.test.ts +2 -0
- package/src/__tests__/jsonForm.pointer.test.ts +47 -1
- package/src/__tests__/layoutBlock.test.ts +3 -3
- package/src/__tests__/majorApiTypes.test.ts +18 -0
- package/src/__tests__/markdown.test.ts +41 -0
- package/src/__tests__/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 +135 -1
- package/src/doc/materializeBlockLayers.ts +304 -0
- package/src/doc/mediaAnnotations.ts +20 -0
- package/src/doc/resolveDocTheme.ts +8 -7
- package/src/doc/templateInputs.ts +185 -2
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
- package/src/doc/templates/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/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/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,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII diagram REPAIR — an opt-in, best-effort pass that reconstructs a
|
|
3
|
+
* clean, well-formed diagram from broken box-and-line art.
|
|
4
|
+
*
|
|
5
|
+
* The motivating case: hand-drawn architecture art whose labels overflow
|
|
6
|
+
* their boxes and shove every following column out of alignment row-to-row
|
|
7
|
+
* (e.g. a rename widened the labels). Conservative detection correctly
|
|
8
|
+
* declines such art (`garbled-labels`) so it renders as a faithful code
|
|
9
|
+
* block — but a user can then ask to "repair" it. This is that repair.
|
|
10
|
+
*
|
|
11
|
+
* It is deliberately AGGRESSIVE and lossy — never on the auto-detection path
|
|
12
|
+
* (a mistake there would hide real code). It is only invoked when the user
|
|
13
|
+
* explicitly opts in, and the result is a fully editable diagram, so an
|
|
14
|
+
* imperfect edge or two is fine (the canvas lets the user fix them).
|
|
15
|
+
*
|
|
16
|
+
* Pipeline:
|
|
17
|
+
* 1. Aggressive box tracing (walls may cross edge routing).
|
|
18
|
+
* 2. Row-band label recovery — the key trick: even when a box's columns are
|
|
19
|
+
* desynced from its borders, each label ROW is internally consistent, so
|
|
20
|
+
* its `│…│` segments map to the band's boxes by reading order.
|
|
21
|
+
* 3. Best-effort edge recovery (facing-border adjacency + arrowhead
|
|
22
|
+
* direction).
|
|
23
|
+
* 4. Re-render clean via `renderAsciiDiagram` — the output re-parses cleanly.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { isHoriz, isVert, isJunction, arrowDirection } from './chars.js';
|
|
27
|
+
import { toGrid, traceBoxes, type TracedRect } from './parse.js';
|
|
28
|
+
import { renderAsciiDiagram } from './render.js';
|
|
29
|
+
import { detectAsciiDiagram } from './detect.js';
|
|
30
|
+
import type { AsciiDiagram, AsciiDiagramEdge, AsciiDiagramNode } from './types.js';
|
|
31
|
+
|
|
32
|
+
export interface RepairResult {
|
|
33
|
+
/** The reconstructed diagram model (grid-native coordinates). */
|
|
34
|
+
diagram: AsciiDiagram;
|
|
35
|
+
/** Clean, re-rendered box-and-line art — re-parses to `diagram`. */
|
|
36
|
+
art: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Minimum boxes for a repair to be worthwhile. */
|
|
40
|
+
const MIN_REPAIR_BOXES = 2;
|
|
41
|
+
/** Max vertical gap (rows) between two boxes to infer a connecting edge. */
|
|
42
|
+
const MAX_EDGE_ROW_GAP = 8;
|
|
43
|
+
/** Max horizontal gap (cols) between two boxes to infer a connecting edge. */
|
|
44
|
+
const MAX_EDGE_COL_GAP = 12;
|
|
45
|
+
/** Blank rows inserted between stacked bands so edges route without overlap. */
|
|
46
|
+
const BAND_ROW_GAP = 4;
|
|
47
|
+
/** Blank cols inserted between boxes in a band so edges route without overlap. */
|
|
48
|
+
const BAND_COL_GAP = 4;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* True when a fence is a box diagram too broken for clean detection but worth
|
|
52
|
+
* offering to repair: it has box corners and aggressively traces ≥2 boxes,
|
|
53
|
+
* yet conservative detection declines it. Clean diagrams (already detected)
|
|
54
|
+
* and non-box art both return false, so the three states partition cleanly.
|
|
55
|
+
*/
|
|
56
|
+
export function isRepairableDiagram(text: string): boolean {
|
|
57
|
+
// Already a clean diagram → not a repair candidate (the canvas handles it).
|
|
58
|
+
if (detectAsciiDiagram(text).isDiagram) return false;
|
|
59
|
+
const grid = toGrid(text);
|
|
60
|
+
const rects = traceBoxes(grid, true);
|
|
61
|
+
return dedupeRects(rects).length >= MIN_REPAIR_BOXES;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reconstruct a clean diagram from broken art. Returns `null` when fewer than
|
|
66
|
+
* two boxes can be recovered (nothing worth repairing).
|
|
67
|
+
*/
|
|
68
|
+
export function repairAsciiDiagram(text: string): RepairResult | null {
|
|
69
|
+
const grid = toGrid(text);
|
|
70
|
+
const rects = dedupeRects(traceBoxes(grid, true));
|
|
71
|
+
if (rects.length < MIN_REPAIR_BOXES) return null;
|
|
72
|
+
|
|
73
|
+
const nodes = buildNodes(grid, rects);
|
|
74
|
+
const edges = recoverEdges(grid, rects, nodes);
|
|
75
|
+
const style = detectGridStyle(grid);
|
|
76
|
+
const diagram: AsciiDiagram = { nodes, edges, style, warnings: ['repaired'] };
|
|
77
|
+
return { diagram, art: renderAsciiDiagram(diagram) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Box de-duplication (aggressive tracing can emit the same rect twice, and
|
|
82
|
+
// nested duplicates where a container and a decomposed cell coincide).
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function dedupeRects(rects: TracedRect[]): TracedRect[] {
|
|
86
|
+
const seen = new Set<string>();
|
|
87
|
+
const out: TracedRect[] = [];
|
|
88
|
+
for (const r of rects) {
|
|
89
|
+
const key = `${r.r0},${r.c0},${r.r1},${r.c1}`;
|
|
90
|
+
if (seen.has(key)) continue;
|
|
91
|
+
seen.add(key);
|
|
92
|
+
out.push(r);
|
|
93
|
+
}
|
|
94
|
+
// Reading order (row, then col).
|
|
95
|
+
return out.sort((a, b) => a.r0 - b.r0 || a.c0 - b.c0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Label recovery (row-band segment matching)
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
function buildNodes(grid: string[][], rects: TracedRect[]): AsciiDiagramNode[] {
|
|
103
|
+
const W = grid[0]?.length ?? 0;
|
|
104
|
+
const labels = rects.map(() => [] as string[]);
|
|
105
|
+
const index = new Map(rects.map((r, i) => [r, i] as const));
|
|
106
|
+
|
|
107
|
+
// Group boxes into horizontal bands (row ranges overlap).
|
|
108
|
+
const bands: TracedRect[][] = [];
|
|
109
|
+
for (const r of rects) {
|
|
110
|
+
let band = bands.find((bd) => bd.some((x) => x.r0 <= r.r1 && r.r0 <= x.r1));
|
|
111
|
+
if (!band) {
|
|
112
|
+
band = [];
|
|
113
|
+
bands.push(band);
|
|
114
|
+
}
|
|
115
|
+
band.push(r);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const band of bands) {
|
|
119
|
+
band.sort((a, b) => a.c0 - b.c0);
|
|
120
|
+
const minR = Math.min(...band.map((b) => b.r0 + 1));
|
|
121
|
+
const maxR = Math.max(...band.map((b) => b.r1 - 1));
|
|
122
|
+
for (let r = minR; r <= maxR; r++) {
|
|
123
|
+
const inRow = band.filter((b) => r > b.r0 && r < b.r1);
|
|
124
|
+
if (inRow.length === 0) continue;
|
|
125
|
+
const rowText = grid[r].join('');
|
|
126
|
+
// Split the row into `│`-delimited segments; a clean band row yields one
|
|
127
|
+
// non-empty segment per box, matched by reading order.
|
|
128
|
+
const segs = rowText
|
|
129
|
+
.split(/[│┃║]/)
|
|
130
|
+
.map(cleanLabelText)
|
|
131
|
+
.filter((s) => s.length > 0);
|
|
132
|
+
if (segs.length === inRow.length) {
|
|
133
|
+
inRow.forEach((b, i) => {
|
|
134
|
+
const idx = index.get(b);
|
|
135
|
+
if (idx !== undefined && segs[i]) labels[idx].push(segs[i]);
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
// Fallback: slice each box's own columns (works when its border cols
|
|
139
|
+
// still line up — isolated boxes, or when the band-split is ambiguous).
|
|
140
|
+
for (const b of inRow) {
|
|
141
|
+
const idx = index.get(b);
|
|
142
|
+
if (idx === undefined) continue;
|
|
143
|
+
const t = cleanLabelText(grid[r].slice(b.c0 + 1, Math.min(b.c1, W)).join(''));
|
|
144
|
+
if (t) labels[idx].push(t);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Normalize rows onto well-spaced band offsets. Preserving the original
|
|
151
|
+
// (tight, broken) row positions makes the renderer route edges THROUGH
|
|
152
|
+
// boxes; stacking bands with a routing gap gives every band-to-band edge
|
|
153
|
+
// clean vertical space so the rendered art re-parses losslessly. Columns
|
|
154
|
+
// keep their original relative order (the horizontal layout was fine).
|
|
155
|
+
const bandRow = new Map<TracedRect[], number>();
|
|
156
|
+
const orderedBands = [...bands].sort(
|
|
157
|
+
(a, b) => Math.min(...a.map((x) => x.r0)) - Math.min(...b.map((x) => x.r0)),
|
|
158
|
+
);
|
|
159
|
+
let rowCursor = 0;
|
|
160
|
+
for (const band of orderedBands) {
|
|
161
|
+
bandRow.set(band, rowCursor);
|
|
162
|
+
const bandHeight = Math.max(...band.map((b) => b.r1 - b.r0 + 1));
|
|
163
|
+
rowCursor += bandHeight + BAND_ROW_GAP;
|
|
164
|
+
}
|
|
165
|
+
const rectBand = new Map<TracedRect, TracedRect[]>();
|
|
166
|
+
for (const band of bands) for (const r of band) rectBand.set(r, band);
|
|
167
|
+
|
|
168
|
+
// Normalize columns within each band by the boxes' FITTED widths. Recovered
|
|
169
|
+
// labels are usually wider than the broken art's boxes, so the original
|
|
170
|
+
// column spacing would make fitted boxes overlap — the renderer would then
|
|
171
|
+
// shove a box into the wrong band and route edges through it. Laying each
|
|
172
|
+
// band out left-to-right by fitted width + a routing gap keeps bands intact.
|
|
173
|
+
const fittedW = rects.map((rect, i) =>
|
|
174
|
+
Math.max(rect.c1 - rect.c0 + 1, ...labels[i].map((l) => l.length + 4), 3),
|
|
175
|
+
);
|
|
176
|
+
const rectIndex = new Map(rects.map((r, i) => [r, i] as const));
|
|
177
|
+
const bandCol = new Map<TracedRect, number>();
|
|
178
|
+
for (const band of bands) {
|
|
179
|
+
const ordered = [...band].sort((a, b) => a.c0 - b.c0);
|
|
180
|
+
let colCursor = Math.min(...band.map((b) => b.c0));
|
|
181
|
+
for (const r of ordered) {
|
|
182
|
+
bandCol.set(r, colCursor);
|
|
183
|
+
colCursor += (fittedW[rectIndex.get(r) ?? 0] ?? 3) + BAND_COL_GAP;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Assign ids (slug of the first label line, deduped), then resolve
|
|
188
|
+
// containment (a box strictly inside another becomes its child).
|
|
189
|
+
const parentIdx = rects.map((_, i) => smallestContainer(rects, i));
|
|
190
|
+
const usedIds = new Map<string, number>();
|
|
191
|
+
const nodes: AsciiDiagramNode[] = rects.map((rect, i) => {
|
|
192
|
+
let id = slugify(labels[i][0] ?? '');
|
|
193
|
+
const n = usedIds.get(id) ?? 0;
|
|
194
|
+
usedIds.set(id, n + 1);
|
|
195
|
+
if (n > 0) id = `${id}-${n + 1}`;
|
|
196
|
+
const band = rectBand.get(rect);
|
|
197
|
+
const nested = parentIdx[i] >= 0;
|
|
198
|
+
const row = band && !nested ? (bandRow.get(band) ?? rect.r0) : rect.r0;
|
|
199
|
+
const col = !nested ? (bandCol.get(rect) ?? rect.c0) : rect.c0;
|
|
200
|
+
return {
|
|
201
|
+
id,
|
|
202
|
+
label: labels[i].join('\n'),
|
|
203
|
+
col,
|
|
204
|
+
row,
|
|
205
|
+
wCols: rect.c1 - rect.c0 + 1,
|
|
206
|
+
hRows: rect.r1 - rect.r0 + 1,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
nodes.forEach((node, i) => {
|
|
210
|
+
if (parentIdx[i] >= 0) node.containerId = nodes[parentIdx[i]].id;
|
|
211
|
+
});
|
|
212
|
+
return nodes;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Strip UNICODE box-drawing chars and arrowheads (edge routing that bled into
|
|
217
|
+
* a label). ASCII `-`/`|`/`+` are kept — they're common in real label text
|
|
218
|
+
* (`molen-tooling`, `read-only`, `stdin | stdout`), and a unicode diagram
|
|
219
|
+
* (the common case) never uses them as borders anyway.
|
|
220
|
+
*/
|
|
221
|
+
function cleanLabelText(seg: string): string {
|
|
222
|
+
let out = '';
|
|
223
|
+
for (const ch of seg) {
|
|
224
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
225
|
+
const box = code >= 0x2500 && code <= 0x257f; // Box Drawing block
|
|
226
|
+
if (box || arrowDirection(ch) !== null) out += ' ';
|
|
227
|
+
else out += ch;
|
|
228
|
+
}
|
|
229
|
+
return out.replace(/\s+/g, ' ').trim();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function smallestContainer(rects: TracedRect[], i: number): number {
|
|
233
|
+
const child = rects[i];
|
|
234
|
+
let best = -1;
|
|
235
|
+
let bestArea = Infinity;
|
|
236
|
+
for (let j = 0; j < rects.length; j++) {
|
|
237
|
+
if (j === i) continue;
|
|
238
|
+
const p = rects[j];
|
|
239
|
+
if (p.r0 < child.r0 && p.c0 < child.c0 && p.r1 > child.r1 && p.c1 > child.c1) {
|
|
240
|
+
const a = (p.r1 - p.r0) * (p.c1 - p.c0);
|
|
241
|
+
if (a < bestArea) {
|
|
242
|
+
bestArea = a;
|
|
243
|
+
best = j;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return best;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function slugify(s: string): string {
|
|
251
|
+
const slug = s
|
|
252
|
+
.toLowerCase()
|
|
253
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
254
|
+
.replace(/^-+|-+$/g, '');
|
|
255
|
+
return slug || 'node';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Edge recovery (facing-border adjacency + arrowhead direction)
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
function recoverEdges(
|
|
263
|
+
grid: string[][],
|
|
264
|
+
rects: TracedRect[],
|
|
265
|
+
nodes: AsciiDiagramNode[],
|
|
266
|
+
): AsciiDiagramEdge[] {
|
|
267
|
+
const H = grid.length;
|
|
268
|
+
const W = grid[0]?.length ?? 0;
|
|
269
|
+
const at = (r: number, c: number): string =>
|
|
270
|
+
r >= 0 && r < H && c >= 0 && c < W ? grid[r][c] : ' ';
|
|
271
|
+
const isConnector = (ch: string): boolean =>
|
|
272
|
+
isVert(ch) || isHoriz(ch) || isJunction(ch) || arrowDirection(ch) !== null;
|
|
273
|
+
|
|
274
|
+
const colOverlap = (a: TracedRect, b: TracedRect): boolean =>
|
|
275
|
+
Math.max(a.c0, b.c0) <= Math.min(a.c1, b.c1);
|
|
276
|
+
const rowOverlap = (a: TracedRect, b: TracedRect): boolean =>
|
|
277
|
+
Math.max(a.r0, b.r0) <= Math.min(a.r1, b.r1);
|
|
278
|
+
|
|
279
|
+
const edges: AsciiDiagramEdge[] = [];
|
|
280
|
+
const seen = new Set<string>();
|
|
281
|
+
const push = (s: number, t: number, directed: boolean) => {
|
|
282
|
+
if (s === t) return;
|
|
283
|
+
const key = `${nodes[s].id}|${nodes[t].id}`;
|
|
284
|
+
if (seen.has(key)) return;
|
|
285
|
+
seen.add(key);
|
|
286
|
+
edges.push({ source: nodes[s].id, target: nodes[t].id, directed });
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < rects.length; i++) {
|
|
290
|
+
for (let j = 0; j < rects.length; j++) {
|
|
291
|
+
if (i === j) continue;
|
|
292
|
+
const A = rects[i];
|
|
293
|
+
const B = rects[j];
|
|
294
|
+
|
|
295
|
+
// Vertical: A above B, columns overlap, a connector spans the gap, and
|
|
296
|
+
// no third box sits directly between them in that column band.
|
|
297
|
+
if (A.r1 < B.r0 && B.r0 - A.r1 <= MAX_EDGE_ROW_GAP && colOverlap(A, B)) {
|
|
298
|
+
const lo = Math.max(A.c0, B.c0);
|
|
299
|
+
const hi = Math.min(A.c1, B.c1);
|
|
300
|
+
if (
|
|
301
|
+
gapHasConnector(at, A.r1 + 1, B.r0 - 1, lo - 1, hi + 1, isConnector) &&
|
|
302
|
+
!boxBetweenVert(rects, i, j, lo, hi)
|
|
303
|
+
) {
|
|
304
|
+
const directed = gapHasArrow(at, A.r1 + 1, B.r0, lo - 1, hi + 1, 'down');
|
|
305
|
+
push(i, j, directed);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Horizontal: A left of B, rows overlap. Require an actual arrowhead in
|
|
310
|
+
// the gap — a label that overflowed its box leaks `│`/text into the gap
|
|
311
|
+
// between horizontally-adjacent boxes, which a plain-connector test
|
|
312
|
+
// would read as a phantom edge. A real horizontal edge carries `►`/`◄`.
|
|
313
|
+
if (A.c1 < B.c0 && B.c0 - A.c1 <= MAX_EDGE_COL_GAP && rowOverlap(A, B) && i < j) {
|
|
314
|
+
const lo = Math.max(A.r0, B.r0);
|
|
315
|
+
const hi = Math.min(A.r1, B.r1);
|
|
316
|
+
const rightArrow = gapHasArrow(at, lo - 1, hi + 1, A.c1 + 1, B.c0, 'right');
|
|
317
|
+
const leftArrow = gapHasArrow(at, lo - 1, hi + 1, A.c1 + 1, B.c0, 'left');
|
|
318
|
+
if ((rightArrow || leftArrow) && !boxBetweenHoriz(rects, i, j, lo, hi)) {
|
|
319
|
+
if (leftArrow && !rightArrow) push(j, i, true);
|
|
320
|
+
else push(i, j, true);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return edges;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function gapHasConnector(
|
|
329
|
+
at: (r: number, c: number) => string,
|
|
330
|
+
r0: number,
|
|
331
|
+
r1: number,
|
|
332
|
+
c0: number,
|
|
333
|
+
c1: number,
|
|
334
|
+
isConnector: (ch: string) => boolean,
|
|
335
|
+
): boolean {
|
|
336
|
+
for (let r = r0; r <= r1; r++)
|
|
337
|
+
for (let c = c0; c <= c1; c++) if (isConnector(at(r, c))) return true;
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function gapHasArrow(
|
|
342
|
+
at: (r: number, c: number) => string,
|
|
343
|
+
r0: number,
|
|
344
|
+
r1: number,
|
|
345
|
+
c0: number,
|
|
346
|
+
c1: number,
|
|
347
|
+
dir: 'down' | 'up' | 'left' | 'right',
|
|
348
|
+
): boolean {
|
|
349
|
+
for (let r = r0; r <= r1; r++) {
|
|
350
|
+
for (let c = c0; c <= c1; c++) {
|
|
351
|
+
if (arrowDirection(at(r, c)) === dir) return true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function boxBetweenVert(
|
|
358
|
+
rects: TracedRect[],
|
|
359
|
+
i: number,
|
|
360
|
+
j: number,
|
|
361
|
+
lo: number,
|
|
362
|
+
hi: number,
|
|
363
|
+
): boolean {
|
|
364
|
+
const A = rects[i];
|
|
365
|
+
const B = rects[j];
|
|
366
|
+
return rects.some((k, ki) => {
|
|
367
|
+
if (ki === i || ki === j) return false;
|
|
368
|
+
return k.r0 > A.r1 && k.r1 < B.r0 && Math.max(k.c0, lo) <= Math.min(k.c1, hi);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function boxBetweenHoriz(
|
|
373
|
+
rects: TracedRect[],
|
|
374
|
+
i: number,
|
|
375
|
+
j: number,
|
|
376
|
+
lo: number,
|
|
377
|
+
hi: number,
|
|
378
|
+
): boolean {
|
|
379
|
+
const A = rects[i];
|
|
380
|
+
const B = rects[j];
|
|
381
|
+
return rects.some((k, ki) => {
|
|
382
|
+
if (ki === i || ki === j) return false;
|
|
383
|
+
return k.c0 > A.c1 && k.c1 < B.c0 && Math.max(k.r0, lo) <= Math.min(k.r1, hi);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
function detectGridStyle(grid: string[][]): 'unicode' | 'ascii' {
|
|
390
|
+
let unicode = 0;
|
|
391
|
+
let ascii = 0;
|
|
392
|
+
for (const row of grid) {
|
|
393
|
+
for (const ch of row) {
|
|
394
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
395
|
+
if (code >= 0x2500 && code <= 0x257f) unicode++;
|
|
396
|
+
else if (ch === '+' || ch === '|' || ch === '-') ascii++;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return ascii > unicode ? 'ascii' : 'unicode';
|
|
400
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the ASCII diagram codec.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates are grid-native: `col`/`row` count character cells from the
|
|
5
|
+
* top-left of the art (0-based), `wCols`/`hRows` include the borders. The
|
|
6
|
+
* mapping helpers in `mapping.ts` convert to/from diagram canvas units
|
|
7
|
+
* via {@link ASCII_CHAR_W} / {@link ASCII_CHAR_H}.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface AsciiDiagramNode {
|
|
11
|
+
/**
|
|
12
|
+
* Stable id: slug of the first label line, deduplicated with `-2`/`-3`
|
|
13
|
+
* suffixes in reading order. Identical art always re-parses to
|
|
14
|
+
* identical ids (editor selection and edges resolve by id).
|
|
15
|
+
*/
|
|
16
|
+
id: string;
|
|
17
|
+
/** Interior text lines joined with `\n` (trimmed, blank lines dropped). May be `''`. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** Left column of the box (0-based, includes the border). */
|
|
20
|
+
col: number;
|
|
21
|
+
/** Top row of the box (0-based, includes the border). */
|
|
22
|
+
row: number;
|
|
23
|
+
/** Full box width in columns, borders included. */
|
|
24
|
+
wCols: number;
|
|
25
|
+
/** Full box height in rows, borders included. */
|
|
26
|
+
hRows: number;
|
|
27
|
+
/** Id of the box that strictly contains this one, when nested. */
|
|
28
|
+
containerId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AsciiDiagramEdge {
|
|
32
|
+
source: string;
|
|
33
|
+
target: string;
|
|
34
|
+
/** Edge label parsed from an embedded horizontal run (`──label──▶`). */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** False when the line carried no arrowhead. */
|
|
37
|
+
directed: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AsciiDiagram {
|
|
41
|
+
/** Nodes in reading order (row, then col). Containers are ordinary nodes with children pointing at them. */
|
|
42
|
+
nodes: AsciiDiagramNode[];
|
|
43
|
+
/** Deduplicated edges, sorted by (source, target, label). */
|
|
44
|
+
edges: AsciiDiagramEdge[];
|
|
45
|
+
/** Character vocabulary detected in the source; render emits the same style. */
|
|
46
|
+
style: 'unicode' | 'ascii';
|
|
47
|
+
/** Non-fatal parse notes (loose text, dropped labels, ambiguous buses, …). */
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AsciiDiagramDetection {
|
|
52
|
+
isDiagram: boolean;
|
|
53
|
+
/** Present when `isDiagram` — avoids a second parse in callers. */
|
|
54
|
+
diagram?: AsciiDiagram;
|
|
55
|
+
/** Machine-greppable accept/reject reasons, e.g. `too-few-boxes(1)`, `loose-ratio(0.41)`. */
|
|
56
|
+
reasons: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Canvas pixels per grid column/row. Sized so a typical 12-col × 3-row
|
|
61
|
+
* ASCII box maps close to the native 180×64 diagram card, and a 60–100
|
|
62
|
+
* column diagram fits the 1600×900 diagram viewport.
|
|
63
|
+
*/
|
|
64
|
+
export const ASCII_CHAR_W = 14;
|
|
65
|
+
export const ASCII_CHAR_H = 28;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** Conservative detection for authored ASCII timeline fences. */
|
|
2
|
+
|
|
3
|
+
import type { MarkdownCodeBlock } from '../../markdown/types.js';
|
|
4
|
+
import { parseAsciiDiagramWithStats } from '../asciiDiagram/parse.js';
|
|
5
|
+
import { parseAsciiTimelineWithStats } from './parse.js';
|
|
6
|
+
import type { AsciiTimelineDetection } from './types.js';
|
|
7
|
+
|
|
8
|
+
export const ASCII_TIMELINE_FENCE_LANGS: ReadonlySet<string> = new Set([
|
|
9
|
+
'text',
|
|
10
|
+
'txt',
|
|
11
|
+
'plaintext',
|
|
12
|
+
'plain',
|
|
13
|
+
'ascii',
|
|
14
|
+
'timeline',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const MAX_LINES = 400;
|
|
18
|
+
const MAX_CANONICAL_LINES = 2000;
|
|
19
|
+
const MAX_COLS = 400;
|
|
20
|
+
const MAX_EXPLICIT_COLS = 4096;
|
|
21
|
+
const BOX_TOP_EDGE_RE = /[┌┏╔╭].*[┐┓╗╮]/u;
|
|
22
|
+
const CANONICAL_TRACK_RE = /\{#[^{}\s]+ start=[^{}\s]+ end=[^{}\s]+\}:\s/u;
|
|
23
|
+
|
|
24
|
+
export function isEligibleAsciiTimelineFenceLang(lang: string | null | undefined): boolean {
|
|
25
|
+
if (lang === null || lang === undefined) return true;
|
|
26
|
+
const normalized = lang.trim().toLowerCase();
|
|
27
|
+
return normalized === '' || ASCII_TIMELINE_FENCE_LANGS.has(normalized);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isExplicitTimelineLang(lang: string | null | undefined): boolean {
|
|
31
|
+
return typeof lang === 'string' && lang.trim().toLowerCase() === 'timeline';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DetectAsciiTimelineOptions {
|
|
35
|
+
/** Explicit `timeline` language: allow one-point/ASCII-marker tracks. */
|
|
36
|
+
explicit?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detectAsciiTimeline(
|
|
40
|
+
text: string,
|
|
41
|
+
options: DetectAsciiTimelineOptions = {},
|
|
42
|
+
): AsciiTimelineDetection {
|
|
43
|
+
const explicit = options.explicit === true;
|
|
44
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
45
|
+
const canonicalTrackRows = lines.filter((line) => CANONICAL_TRACK_RE.test(line)).length;
|
|
46
|
+
const canonicalLineBudget = Math.min(
|
|
47
|
+
MAX_CANONICAL_LINES,
|
|
48
|
+
canonicalTrackRows * 6 +
|
|
49
|
+
lines.filter((line) => /^\s*(?:branch|link)\s*:/iu.test(line)).length +
|
|
50
|
+
2,
|
|
51
|
+
);
|
|
52
|
+
if (
|
|
53
|
+
lines.length > MAX_LINES &&
|
|
54
|
+
(canonicalTrackRows === 0 || lines.length > canonicalLineBudget)
|
|
55
|
+
) {
|
|
56
|
+
return { isTimeline: false, reasons: [`too-many-lines(${lines.length})`] };
|
|
57
|
+
}
|
|
58
|
+
const maxCols = Math.max(0, ...lines.map((line) => Array.from(line).length));
|
|
59
|
+
// Canonical renderer output carries bounded track metadata on every long
|
|
60
|
+
// row. Permit that high-confidence form up to the explicit-fence cap while
|
|
61
|
+
// retaining the conservative 400-column limit for arbitrary untagged art.
|
|
62
|
+
const canonicalLongForm =
|
|
63
|
+
canonicalTrackRows > 0 &&
|
|
64
|
+
lines.every((line) => Array.from(line).length <= MAX_COLS || CANONICAL_TRACK_RE.test(line));
|
|
65
|
+
const maxAllowedCols = explicit || canonicalLongForm ? MAX_EXPLICIT_COLS : MAX_COLS;
|
|
66
|
+
if (maxCols > maxAllowedCols) {
|
|
67
|
+
return { isTimeline: false, reasons: [`too-wide(${maxCols})`] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const nonBlank = lines.filter((line) => line.trim().length > 0);
|
|
71
|
+
const pipeRows = nonBlank.filter((line) => /^\s*\|.*\|\s*$/.test(line)).length;
|
|
72
|
+
const hasGfmSeparator = nonBlank.some(
|
|
73
|
+
(line) => /^\s*\|?[\s:|-]+\|?\s*$/.test(line) && line.includes('-'),
|
|
74
|
+
);
|
|
75
|
+
if (pipeRows > nonBlank.length / 2 && hasGfmSeparator) {
|
|
76
|
+
return { isTimeline: false, reasons: ['markdown-table'] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!explicit && lines.some((line) => BOX_TOP_EDGE_RE.test(line))) {
|
|
80
|
+
return { isTimeline: false, reasons: ['has-box-corners'] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Closed boxes own the art even if their borders happen to contain dots.
|
|
84
|
+
// Untagged detection rejects even one box; the diagram auto-template needs
|
|
85
|
+
// two, but silently turning a single boxed callout into a timeline is worse
|
|
86
|
+
// than leaving it as authored code. An explicit timeline may use one boxed
|
|
87
|
+
// callout, while two or more still belong to the diagram codec.
|
|
88
|
+
const boxes = parseAsciiDiagramWithStats(text).diagram.nodes.length;
|
|
89
|
+
if (boxes >= (explicit ? 2 : 1)) {
|
|
90
|
+
return { isTimeline: false, reasons: [`has-boxes(${boxes})`] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A tree item may itself contain a marker rail in its label. Connector
|
|
94
|
+
// prefixes (`├── ●──●`, `|-- ●--●`) identify the outer structure as a
|
|
95
|
+
// tree, so conservative untagged detection must not claim that row.
|
|
96
|
+
if (!explicit && /^[ \t]*(?:[│|][ \t]*)*[├└+|`][─━═-]{1,4}[ \t]+[●○◉◆◇•]/mu.test(text)) {
|
|
97
|
+
return { isTimeline: false, reasons: ['tree-connector-prefix'] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { timeline, stats } = parseAsciiTimelineWithStats(text);
|
|
101
|
+
if (stats.wrappedFlow) {
|
|
102
|
+
return {
|
|
103
|
+
isTimeline: true,
|
|
104
|
+
timeline,
|
|
105
|
+
reasons: [`tracks(${stats.axisLines})`, `events(${stats.markerCount})`, 'wrapped-flow'],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (stats.axisLines === 0) return { isTimeline: false, reasons: ['no-axis-lines'] };
|
|
109
|
+
const explicitCadence = explicit && stats.horizontalChars === 0 && stats.markerCount >= 4;
|
|
110
|
+
if (!explicitCadence && stats.horizontalChars < (explicit ? 2 : 8)) {
|
|
111
|
+
return { isTimeline: false, reasons: [`too-few-horizontal(${stats.horizontalChars})`] };
|
|
112
|
+
}
|
|
113
|
+
if (stats.markerCount < (explicit ? 1 : 2)) {
|
|
114
|
+
return { isTimeline: false, reasons: [`too-few-markers(${stats.markerCount})`] };
|
|
115
|
+
}
|
|
116
|
+
// Untagged auto-detection requires unmistakable Unicode point markers.
|
|
117
|
+
// `foo * bar *` or `o---o` in real code therefore stays ordinary code.
|
|
118
|
+
if (!explicit && stats.strongMarkerCount < 2) {
|
|
119
|
+
return { isTimeline: false, reasons: [`too-few-strong-markers(${stats.strongMarkerCount})`] };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
isTimeline: true,
|
|
124
|
+
timeline,
|
|
125
|
+
reasons: [
|
|
126
|
+
`tracks(${stats.axisLines})`,
|
|
127
|
+
`events(${stats.markerCount + stats.pointerCount})`,
|
|
128
|
+
explicit ? 'explicit' : 'strong-markers',
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isAsciiTimelineFence(node: MarkdownCodeBlock): boolean {
|
|
134
|
+
if (node.type !== 'code') return false;
|
|
135
|
+
if (!isEligibleAsciiTimelineFenceLang(node.lang)) return false;
|
|
136
|
+
return detectAsciiTimeline(node.value, { explicit: isExplicitTimelineLang(node.lang) })
|
|
137
|
+
.isTimeline;
|
|
138
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Public authored ASCII timeline codec. */
|
|
2
|
+
|
|
3
|
+
export { parseAsciiTimeline, parseAsciiTimelineWithStats } from './parse.js';
|
|
4
|
+
export type { AsciiTimelineStats } from './parse.js';
|
|
5
|
+
export { renderAsciiTimeline } from './render.js';
|
|
6
|
+
export type { RenderAsciiTimelineOptions } from './render.js';
|
|
7
|
+
export {
|
|
8
|
+
ASCII_TIMELINE_FENCE_LANGS,
|
|
9
|
+
detectAsciiTimeline,
|
|
10
|
+
isAsciiTimelineFence,
|
|
11
|
+
isEligibleAsciiTimelineFenceLang,
|
|
12
|
+
isExplicitTimelineLang,
|
|
13
|
+
} from './detect.js';
|
|
14
|
+
export type { DetectAsciiTimelineOptions } from './detect.js';
|
|
15
|
+
export { asciiTimelineToTemplateData } from './mapping.js';
|
|
16
|
+
export { isWrappedFlowTimelineSource, parseWrappedFlowTimeline } from './wrappedFlow.js';
|
|
17
|
+
export type {
|
|
18
|
+
AsciiTimeline,
|
|
19
|
+
AsciiTimelineDetection,
|
|
20
|
+
AsciiTimelineEvent,
|
|
21
|
+
AsciiTimelineLink,
|
|
22
|
+
AsciiTimelineMarker,
|
|
23
|
+
AsciiTimelineSide,
|
|
24
|
+
AsciiTimelineStyle,
|
|
25
|
+
AsciiTimelineTrack,
|
|
26
|
+
} from './types.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Mapping from source-grid timeline data to normalized template inputs. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
TimelineTemplateEvent,
|
|
5
|
+
TimelineTemplateLink,
|
|
6
|
+
TimelineTemplateTrack,
|
|
7
|
+
} from '../../schemas/BlockTemplates.js';
|
|
8
|
+
import type { AsciiTimeline } from './types.js';
|
|
9
|
+
|
|
10
|
+
export function asciiTimelineToTemplateData(timeline: AsciiTimeline): {
|
|
11
|
+
tracks: TimelineTemplateTrack[];
|
|
12
|
+
links: TimelineTemplateLink[];
|
|
13
|
+
} {
|
|
14
|
+
const globalStart =
|
|
15
|
+
timeline.tracks.length > 0 ? Math.min(...timeline.tracks.map((track) => track.startColumn)) : 0;
|
|
16
|
+
const globalEnd =
|
|
17
|
+
timeline.tracks.length > 0 ? Math.max(...timeline.tracks.map((track) => track.endColumn)) : 1;
|
|
18
|
+
const span = Math.max(1, globalEnd - globalStart);
|
|
19
|
+
|
|
20
|
+
const tracks: TimelineTemplateTrack[] = timeline.tracks.map((track) => ({
|
|
21
|
+
id: track.id,
|
|
22
|
+
...(track.label ? { label: track.label } : {}),
|
|
23
|
+
...(track.endLabel ? { endLabel: track.endLabel } : {}),
|
|
24
|
+
events: track.events.map(
|
|
25
|
+
(event): TimelineTemplateEvent => ({
|
|
26
|
+
id: event.id,
|
|
27
|
+
label: event.label,
|
|
28
|
+
...(event.description ? { description: event.description } : {}),
|
|
29
|
+
position: clamp01((event.column - globalStart) / span),
|
|
30
|
+
...(event.side ? { side: event.side } : {}),
|
|
31
|
+
...(event.descriptionSide ? { descriptionSide: event.descriptionSide } : {}),
|
|
32
|
+
...(event.callout === false ? { callout: false } : {}),
|
|
33
|
+
...(event.marker ? { marker: event.marker } : {}),
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
}));
|
|
37
|
+
const links: TimelineTemplateLink[] = timeline.links.map((link) => ({ ...link }));
|
|
38
|
+
return { tracks, links };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function clamp01(value: number): number {
|
|
42
|
+
return Math.max(0, Math.min(1, value));
|
|
43
|
+
}
|