@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
|
@@ -33,6 +33,24 @@ export function allocateTiming(
|
|
|
33
33
|
): Block[] {
|
|
34
34
|
if (blocks.length === 0) return [];
|
|
35
35
|
|
|
36
|
+
// Template blocks carrying a numeric `sourceStartTime` are ANCHORED to
|
|
37
|
+
// the doc timeline — when the source doc is narration-timed
|
|
38
|
+
// (`applyNarrationTiming`), those anchors are where the words are
|
|
39
|
+
// actually spoken, and rescaling would tear the slides away from the
|
|
40
|
+
// voice. Anchored blocks keep their positions; floating blocks
|
|
41
|
+
// (intro/outro bookends, section headers, interleaved images) fill the
|
|
42
|
+
// gaps between anchors. With no anchors at all, behavior is the
|
|
43
|
+
// historical rescale-to-fit.
|
|
44
|
+
const hasAnchors = blocks.some(
|
|
45
|
+
(b) => isTemplateBlock(b) && typeof b.sourceStartTime === 'number',
|
|
46
|
+
);
|
|
47
|
+
return hasAnchors
|
|
48
|
+
? allocateAnchored(blocks, totalDuration)
|
|
49
|
+
: allocateRescaled(blocks, totalDuration);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Historical behavior: scale every duration to fill the total, clamp 3–20 s. */
|
|
53
|
+
function allocateRescaled(blocks: Array<Block | TemplateBlock>, totalDuration: number): Block[] {
|
|
36
54
|
const effectiveDuration = totalDuration > 0 ? totalDuration : estimateDefaultDuration(blocks);
|
|
37
55
|
|
|
38
56
|
// First pass: assign raw durations
|
|
@@ -59,37 +77,150 @@ export function allocateTiming(
|
|
|
59
77
|
// Second pass: assign startTimes and build output Blocks
|
|
60
78
|
let currentTime = 0;
|
|
61
79
|
const result: Block[] = [];
|
|
62
|
-
|
|
63
80
|
for (let i = 0; i < blocks.length; i++) {
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
result.push(toOutputBlock(blocks[i], currentTime, durations[i]));
|
|
82
|
+
currentTime += durations[i];
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Minimum duration for a floating block squeezed between anchors. */
|
|
88
|
+
const MIN_FLOATING_DURATION = 1;
|
|
66
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Anchor-aware allocation: anchored entries (template blocks with a
|
|
92
|
+
* numeric `sourceStartTime`, and untransformed source Blocks with their
|
|
93
|
+
* own real timing) are PLACED at their timeline positions; runs of
|
|
94
|
+
* floating blocks (bookends, headers, images) distribute across the gap
|
|
95
|
+
* before the next anchor. Durations then derive from consecutive starts
|
|
96
|
+
* so the timeline stays CONTIGUOUS — a slide summarizing unpromoted
|
|
97
|
+
* source content simply holds until the next slide begins, which is
|
|
98
|
+
* what keeps every narration second covered by some block.
|
|
99
|
+
*/
|
|
100
|
+
function allocateAnchored(blocks: Array<Block | TemplateBlock>, totalDuration: number): Block[] {
|
|
101
|
+
interface Entry {
|
|
102
|
+
block: Block | TemplateBlock;
|
|
103
|
+
anchorStart: number | null;
|
|
104
|
+
rawDuration: number;
|
|
105
|
+
}
|
|
106
|
+
const entries: Entry[] = blocks.map((block) => {
|
|
67
107
|
if (isTemplateBlock(block)) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
return {
|
|
109
|
+
block,
|
|
110
|
+
anchorStart: typeof block.sourceStartTime === 'number' ? block.sourceStartTime : null,
|
|
111
|
+
rawDuration: block.duration > 0 ? block.duration : DEFAULT_BLOCK_DURATION,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Untransformed source blocks carry real (possibly narration) timing.
|
|
115
|
+
return {
|
|
116
|
+
block,
|
|
117
|
+
anchorStart: block.startTime,
|
|
118
|
+
rawDuration: block.duration > 0 ? block.duration : DEFAULT_BLOCK_DURATION,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Pass 1: starts. Anchors pin their positions (the cursor then jumps
|
|
123
|
+
// past the anchor's authored span, so following floats land AFTER it).
|
|
124
|
+
// Float runs place differently by position: LEADING floats (before any
|
|
125
|
+
// anchor — intro bookends) fill the opening gap from t=0; MID floats
|
|
126
|
+
// (section headers, images) end-align just before the next anchor —
|
|
127
|
+
// a header belongs to the section it introduces; TRAILING floats
|
|
128
|
+
// (outro) run sequentially after the last anchor's span.
|
|
129
|
+
const starts = new Array<number>(entries.length);
|
|
130
|
+
let cursor = 0;
|
|
131
|
+
let seenAnchor = false;
|
|
132
|
+
let i = 0;
|
|
133
|
+
while (i < entries.length) {
|
|
134
|
+
const entry = entries[i];
|
|
135
|
+
if (entry.anchorStart !== null) {
|
|
136
|
+
const start = Math.max(cursor, entry.anchorStart);
|
|
137
|
+
starts[i] = start;
|
|
138
|
+
const hint = isTemplateBlock(entry.block)
|
|
139
|
+
? (entry.block.sourceDuration ?? entry.rawDuration)
|
|
140
|
+
: entry.rawDuration;
|
|
141
|
+
cursor = start + Math.max(MIN_FLOATING_DURATION, hint);
|
|
142
|
+
seenAnchor = true;
|
|
143
|
+
i++;
|
|
144
|
+
continue;
|
|
85
145
|
}
|
|
146
|
+
const runStart = i;
|
|
147
|
+
while (i < entries.length && entries[i].anchorStart === null) i++;
|
|
148
|
+
const run = entries.slice(runStart, i);
|
|
149
|
+
const rawSum = run.reduce((sum, e) => sum + e.rawDuration, 0);
|
|
150
|
+
const nextAnchorStart =
|
|
151
|
+
i < entries.length ? Math.max(entries[i].anchorStart ?? cursor, cursor) : null;
|
|
86
152
|
|
|
87
|
-
|
|
153
|
+
if (nextAnchorStart === null) {
|
|
154
|
+
// Trailing floats: natural sizes after the last anchor's span.
|
|
155
|
+
let acc = cursor;
|
|
156
|
+
for (let k = 0; k < run.length; k++) {
|
|
157
|
+
starts[runStart + k] = acc;
|
|
158
|
+
acc += run[k].rawDuration;
|
|
159
|
+
}
|
|
160
|
+
cursor = acc;
|
|
161
|
+
} else if (!seenAnchor) {
|
|
162
|
+
// Leading floats: fill [cursor, nextAnchorStart) proportionally.
|
|
163
|
+
const gap = nextAnchorStart - cursor;
|
|
164
|
+
let acc = cursor;
|
|
165
|
+
for (let k = 0; k < run.length; k++) {
|
|
166
|
+
starts[runStart + k] = acc;
|
|
167
|
+
acc += rawSum > 0 ? (run[k].rawDuration / rawSum) * gap : gap / run.length;
|
|
168
|
+
}
|
|
169
|
+
cursor = nextAnchorStart;
|
|
170
|
+
} else {
|
|
171
|
+
// Mid floats: end-aligned at natural size, capped by the gap.
|
|
172
|
+
const gap = Math.max(0, nextAnchorStart - cursor);
|
|
173
|
+
const scale = rawSum > 0 ? Math.min(1, gap / rawSum) : 0;
|
|
174
|
+
let acc = Math.max(cursor, nextAnchorStart - rawSum * scale);
|
|
175
|
+
for (let k = 0; k < run.length; k++) {
|
|
176
|
+
starts[runStart + k] = acc;
|
|
177
|
+
acc += run[k].rawDuration * scale;
|
|
178
|
+
}
|
|
179
|
+
cursor = nextAnchorStart;
|
|
180
|
+
}
|
|
88
181
|
}
|
|
89
182
|
|
|
183
|
+
// Pass 2: strictly increasing starts, then contiguous durations.
|
|
184
|
+
for (let k = 0; k < starts.length; k++) {
|
|
185
|
+
const floor = k > 0 ? starts[k - 1] + MIN_FLOATING_DURATION : 0;
|
|
186
|
+
if (starts[k] < floor) starts[k] = floor;
|
|
187
|
+
}
|
|
188
|
+
const result: Block[] = [];
|
|
189
|
+
for (let k = 0; k < entries.length; k++) {
|
|
190
|
+
const entry = entries[k];
|
|
191
|
+
let duration: number;
|
|
192
|
+
if (k + 1 < entries.length) {
|
|
193
|
+
duration = Math.max(MIN_FLOATING_DURATION, starts[k + 1] - starts[k]);
|
|
194
|
+
} else {
|
|
195
|
+
const authored = isTemplateBlock(entry.block)
|
|
196
|
+
? (entry.block.sourceDuration ?? entry.rawDuration)
|
|
197
|
+
: entry.rawDuration;
|
|
198
|
+
// The final block covers through the end of the doc/narration when
|
|
199
|
+
// the caller knows it runs longer.
|
|
200
|
+
duration = Math.max(MIN_FLOATING_DURATION, authored, totalDuration - starts[k]);
|
|
201
|
+
}
|
|
202
|
+
result.push(toOutputBlock(entry.block, starts[k], duration));
|
|
203
|
+
}
|
|
90
204
|
return result;
|
|
91
205
|
}
|
|
92
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Convert an entry to an output Block shell. Template blocks preserve all
|
|
209
|
+
* template-specific fields (stat, quote, description, …) so they survive
|
|
210
|
+
* through to the player's template expansion step.
|
|
211
|
+
*/
|
|
212
|
+
function toOutputBlock(block: Block | TemplateBlock, startTime: number, duration: number): Block {
|
|
213
|
+
if (isTemplateBlock(block)) {
|
|
214
|
+
return {
|
|
215
|
+
...block,
|
|
216
|
+
startTime,
|
|
217
|
+
duration,
|
|
218
|
+
title: getTemplateTitle(block),
|
|
219
|
+
} as unknown as Block;
|
|
220
|
+
}
|
|
221
|
+
return { ...block, startTime, duration };
|
|
222
|
+
}
|
|
223
|
+
|
|
93
224
|
/** Clamp a duration to the min/max range. */
|
|
94
225
|
function clampDuration(d: number): number {
|
|
95
226
|
return Math.max(MIN_BLOCK_DURATION, Math.min(MAX_BLOCK_DURATION, d));
|
package/src/transform/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { ColorScheme } from '../schemas/BlockTemplates.js';
|
|
|
12
12
|
|
|
13
13
|
// ── Style identification ─────────────────────────────────────────
|
|
14
14
|
|
|
15
|
-
/** Identifier for a registered transform style. */
|
|
15
|
+
/** Identifier for a built-in or caller-registered transform style. */
|
|
16
16
|
export type TransformStyleId = string;
|
|
17
17
|
|
|
18
18
|
/** Summary info for UI dropdowns (mirrors getThemeSummaries pattern). */
|
|
@@ -97,6 +97,17 @@ export interface TransformStyleConfig {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** Built-in id or a call-scoped declarative style definition. */
|
|
101
|
+
export type TransformStyleInput = TransformStyleId | TransformStyleConfig;
|
|
102
|
+
|
|
103
|
+
/** Explicit caller-owned registry for custom transform styles. */
|
|
104
|
+
export interface TransformStyleRegistry {
|
|
105
|
+
register(style: TransformStyleConfig): void;
|
|
106
|
+
unregister(id: string): boolean;
|
|
107
|
+
get(id: string): TransformStyleConfig | undefined;
|
|
108
|
+
list(): TransformStyleConfig[];
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
// ── Transform options ────────────────────────────────────────────
|
|
101
112
|
|
|
102
113
|
/** Image metadata for image interleaving. */
|
|
@@ -121,6 +132,8 @@ export interface TransformOptions {
|
|
|
121
132
|
themeId?: string;
|
|
122
133
|
/** Override specific style config values. */
|
|
123
134
|
overrides?: Partial<TransformStyleConfig>;
|
|
135
|
+
/** Caller-owned registry used when `style` is an id. */
|
|
136
|
+
registry?: TransformStyleRegistry;
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
// ── Transform result ─────────────────────────────────────────────
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep version snapshots out of Git when a ContentContainer is persisted
|
|
3
|
+
* as a real folder (typically a `<basename>_files/` sidecar).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ContentContainer } from '../storage/ContentContainer.js';
|
|
7
|
+
|
|
8
|
+
const decoder = new TextDecoder();
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
|
|
11
|
+
const GITIGNORE_PATH = '.gitignore';
|
|
12
|
+
const VERSIONS_IGNORE_RULE = '.versions/';
|
|
13
|
+
const EQUIVALENT_VERSIONS_RULE = /^(!?)\/?\.versions\/?$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ensure the container-root `.gitignore` ignores its `.versions/` folder.
|
|
17
|
+
* Existing rules and newline style are preserved.
|
|
18
|
+
*/
|
|
19
|
+
export async function ensureVersionsGitIgnored(container: ContentContainer): Promise<void> {
|
|
20
|
+
const data = await container.readFile(GITIGNORE_PATH);
|
|
21
|
+
const existing = data ? decoder.decode(data) : '';
|
|
22
|
+
|
|
23
|
+
let alreadyIgnored = false;
|
|
24
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
25
|
+
const match = EQUIVALENT_VERSIONS_RULE.exec(line.trim());
|
|
26
|
+
if (match) alreadyIgnored = match[1] !== '!';
|
|
27
|
+
}
|
|
28
|
+
if (alreadyIgnored) return;
|
|
29
|
+
|
|
30
|
+
const newline = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
31
|
+
const separator = existing.length > 0 && !existing.endsWith('\n') ? newline : '';
|
|
32
|
+
const next = `${existing}${separator}${VERSIONS_IGNORE_RULE}${newline}`;
|
|
33
|
+
await container.writeFile(GITIGNORE_PATH, encoder.encode(next), 'text/plain');
|
|
34
|
+
}
|
package/src/versions/index.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Snapshots of the primary markdown document live inside the same
|
|
5
5
|
* {@link ContentContainer} at `.versions/<basename>.<timestamp>.md`.
|
|
6
|
+
* When a snapshot is retained, the container root also gets a `.gitignore`
|
|
7
|
+
* rule for `.versions/`, preserving any existing ignore rules.
|
|
6
8
|
* Hosts call {@link DocumentVersionManager.saveVersion} (or the
|
|
7
9
|
* underlying {@link saveVersion}) when they want to stamp a new version;
|
|
8
10
|
* the editor's auto-save effect calls it on idle. Either way, snapshots
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ContentContainer } from '../storage/ContentContainer.js';
|
|
9
|
-
import {
|
|
9
|
+
import { DOCUMENT_VERSION_PATHS, getDocBasename } from './paths.js';
|
|
10
|
+
import {
|
|
11
|
+
coalesceSnapshots,
|
|
12
|
+
listSnapshots,
|
|
13
|
+
pruneSnapshots,
|
|
14
|
+
readTextSnapshot,
|
|
15
|
+
saveTextSnapshot,
|
|
16
|
+
} from './snapshotStore.js';
|
|
10
17
|
import type {
|
|
11
18
|
CoalesceOptions,
|
|
12
19
|
PrunePolicy,
|
|
@@ -16,9 +23,6 @@ import type {
|
|
|
16
23
|
Version,
|
|
17
24
|
} from './types.js';
|
|
18
25
|
|
|
19
|
-
const decoder = new TextDecoder();
|
|
20
|
-
const encoder = new TextEncoder();
|
|
21
|
-
|
|
22
26
|
async function resolveBasename(
|
|
23
27
|
container: ContentContainer,
|
|
24
28
|
override: string | undefined,
|
|
@@ -46,30 +50,7 @@ export async function listVersions(
|
|
|
46
50
|
container: ContentContainer,
|
|
47
51
|
basename?: string,
|
|
48
52
|
): Promise<Version[]> {
|
|
49
|
-
|
|
50
|
-
const versions: Version[] = [];
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
const parsed = parseVersionPath(entry.path);
|
|
53
|
-
if (!parsed) continue;
|
|
54
|
-
if (basename !== undefined && parsed.basename !== basename) continue;
|
|
55
|
-
versions.push({
|
|
56
|
-
path: entry.path,
|
|
57
|
-
basename: parsed.basename,
|
|
58
|
-
timestamp: parsed.timestamp,
|
|
59
|
-
size: entry.size,
|
|
60
|
-
collision: parsed.collision,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
// Newest first. Tie-break on collision (higher = newer). Compare the
|
|
64
|
-
// numeric collision rather than the path string: lexicographically a path
|
|
65
|
-
// with no suffix sorts *after* one with `-N`, because `.` (0x2E) is
|
|
66
|
-
// greater than `-` (0x2D), which would invert the order.
|
|
67
|
-
versions.sort((a, b) => {
|
|
68
|
-
const dt = b.timestamp.getTime() - a.timestamp.getTime();
|
|
69
|
-
if (dt !== 0) return dt;
|
|
70
|
-
return b.collision - a.collision;
|
|
71
|
-
});
|
|
72
|
-
return versions;
|
|
53
|
+
return listSnapshots(container, DOCUMENT_VERSION_PATHS, basename);
|
|
73
54
|
}
|
|
74
55
|
|
|
75
56
|
/**
|
|
@@ -80,10 +61,7 @@ export async function readVersion(
|
|
|
80
61
|
container: ContentContainer,
|
|
81
62
|
version: Version | string,
|
|
82
63
|
): Promise<string | null> {
|
|
83
|
-
|
|
84
|
-
const data = await container.readFile(path);
|
|
85
|
-
if (!data) return null;
|
|
86
|
-
return decoder.decode(data);
|
|
64
|
+
return readTextSnapshot(container, version);
|
|
87
65
|
}
|
|
88
66
|
|
|
89
67
|
/**
|
|
@@ -107,34 +85,13 @@ export async function saveVersion(
|
|
|
107
85
|
return { saved: false, version: null, reason: 'no-document' };
|
|
108
86
|
}
|
|
109
87
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const latest = versions[0]!;
|
|
113
|
-
const existing = await readVersion(container, latest);
|
|
114
|
-
if (existing === content) {
|
|
115
|
-
return { saved: false, version: null, reason: 'unchanged' };
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const now = options.now ?? new Date();
|
|
120
|
-
let collision = 0;
|
|
121
|
-
let path = buildVersionPath(basename, now, collision);
|
|
122
|
-
while (await container.exists(path)) {
|
|
123
|
-
collision += 1;
|
|
124
|
-
path = buildVersionPath(basename, now, collision);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const data = encoder.encode(content);
|
|
128
|
-
await container.writeFile(path, data, 'text/markdown');
|
|
129
|
-
|
|
130
|
-
const version: Version = {
|
|
131
|
-
path,
|
|
88
|
+
return saveTextSnapshot(container, DOCUMENT_VERSION_PATHS, {
|
|
89
|
+
content,
|
|
132
90
|
basename,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
};
|
|
137
|
-
return { saved: true, version, reason: 'saved' };
|
|
91
|
+
mimeType: 'text/markdown',
|
|
92
|
+
now: options.now,
|
|
93
|
+
force: options.force,
|
|
94
|
+
});
|
|
138
95
|
}
|
|
139
96
|
|
|
140
97
|
/**
|
|
@@ -171,27 +128,7 @@ export async function pruneVersions(
|
|
|
171
128
|
policy: PrunePolicy,
|
|
172
129
|
basename?: string,
|
|
173
130
|
): Promise<Version[]> {
|
|
174
|
-
|
|
175
|
-
const toDelete: Version[] = [];
|
|
176
|
-
|
|
177
|
-
if (policy.type === 'keep-last-n') {
|
|
178
|
-
const n = Math.max(0, Math.floor(policy.n));
|
|
179
|
-
toDelete.push(...versions.slice(n));
|
|
180
|
-
} else if (policy.type === 'older-than') {
|
|
181
|
-
const cutoff = policy.date.getTime();
|
|
182
|
-
for (const v of versions) {
|
|
183
|
-
if (v.timestamp.getTime() < cutoff) toDelete.push(v);
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
for (const v of versions) {
|
|
187
|
-
if (!policy.keep(v, versions)) toDelete.push(v);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (const v of toDelete) {
|
|
192
|
-
await container.removeFile(v.path);
|
|
193
|
-
}
|
|
194
|
-
return toDelete;
|
|
131
|
+
return pruneSnapshots(container, DOCUMENT_VERSION_PATHS, policy, basename);
|
|
195
132
|
}
|
|
196
133
|
|
|
197
134
|
/**
|
|
@@ -203,28 +140,5 @@ export async function coalesceVersions(
|
|
|
203
140
|
options: CoalesceOptions = {},
|
|
204
141
|
basename?: string,
|
|
205
142
|
): Promise<Version[]> {
|
|
206
|
-
|
|
207
|
-
const versions = await listVersions(container, basename);
|
|
208
|
-
const toDelete: Version[] = [];
|
|
209
|
-
// Walk newest -> oldest, anchored to the last *kept* snapshot. Any
|
|
210
|
-
// candidate within `windowMs` of the anchor is dropped; the first one
|
|
211
|
-
// outside the window becomes the new anchor. Comparing each pair against
|
|
212
|
-
// its previous neighbor instead would chain through deleted snapshots
|
|
213
|
-
// and over-prune any tightly-clustered run (e.g. 30s apart with a 60s
|
|
214
|
-
// window collapses to a single survivor).
|
|
215
|
-
if (versions.length > 0) {
|
|
216
|
-
let anchor = versions[0]!;
|
|
217
|
-
for (let i = 1; i < versions.length; i++) {
|
|
218
|
-
const candidate = versions[i]!;
|
|
219
|
-
if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {
|
|
220
|
-
toDelete.push(candidate);
|
|
221
|
-
} else {
|
|
222
|
-
anchor = candidate;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
for (const v of toDelete) {
|
|
227
|
-
await container.removeFile(v.path);
|
|
228
|
-
}
|
|
229
|
-
return toDelete;
|
|
143
|
+
return coalesceSnapshots(container, DOCUMENT_VERSION_PATHS, options, basename);
|
|
230
144
|
}
|
package/src/versions/paths.ts
CHANGED
|
@@ -6,11 +6,21 @@
|
|
|
6
6
|
* snapshots collide on the same UTC second.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildSnapshotPath,
|
|
11
|
+
parseSnapshotPath,
|
|
12
|
+
type SnapshotPathStrategy,
|
|
13
|
+
} from './snapshotStore.js';
|
|
10
14
|
|
|
11
15
|
/** Subfolder (inside the ContentContainer) that holds version snapshots. */
|
|
12
16
|
export const VERSIONS_PREFIX = '.versions/';
|
|
13
17
|
|
|
18
|
+
/** Internal strategy shared with the generic snapshot store. */
|
|
19
|
+
export const DOCUMENT_VERSION_PATHS: SnapshotPathStrategy = Object.freeze({
|
|
20
|
+
prefix: VERSIONS_PREFIX,
|
|
21
|
+
extension: 'md',
|
|
22
|
+
});
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* Strip the directory and extension from a document path to produce the
|
|
16
26
|
* basename used in version filenames. `subdir/index.md` -> `index`.
|
|
@@ -27,9 +37,7 @@ export function getDocBasename(documentPath: string): string {
|
|
|
27
37
|
* (e.g. `2`, `3`) when two saves land on the same UTC second.
|
|
28
38
|
*/
|
|
29
39
|
export function buildVersionPath(basename: string, date: Date, collision = 0): string {
|
|
30
|
-
|
|
31
|
-
const suffix = collision > 0 ? `-${collision + 1}` : '';
|
|
32
|
-
return `${VERSIONS_PREFIX}${basename}.${stamp}${suffix}.md`;
|
|
40
|
+
return buildSnapshotPath(DOCUMENT_VERSION_PATHS, basename, date, collision);
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/**
|
|
@@ -39,28 +47,5 @@ export function buildVersionPath(basename: string, date: Date, collision = 0): s
|
|
|
39
47
|
export function parseVersionPath(
|
|
40
48
|
path: string,
|
|
41
49
|
): { basename: string; timestamp: Date; collision: number } | null {
|
|
42
|
-
|
|
43
|
-
const rest = path.slice(VERSIONS_PREFIX.length);
|
|
44
|
-
if (!rest.endsWith('.md')) return null;
|
|
45
|
-
const stem = rest.slice(0, -'.md'.length);
|
|
46
|
-
// basename can contain dots; the timestamp is always the *last* dot-segment
|
|
47
|
-
// (optionally followed by a `-N` collision suffix).
|
|
48
|
-
const lastDot = stem.lastIndexOf('.');
|
|
49
|
-
if (lastDot <= 0) return null;
|
|
50
|
-
const basename = stem.slice(0, lastDot);
|
|
51
|
-
const tail = stem.slice(lastDot + 1);
|
|
52
|
-
const dash = tail.indexOf('-');
|
|
53
|
-
let stamp: string;
|
|
54
|
-
let collision = 0;
|
|
55
|
-
if (dash >= 0) {
|
|
56
|
-
stamp = tail.slice(0, dash);
|
|
57
|
-
const n = Number(tail.slice(dash + 1));
|
|
58
|
-
if (!Number.isInteger(n) || n < 2) return null;
|
|
59
|
-
collision = n - 1;
|
|
60
|
-
} else {
|
|
61
|
-
stamp = tail;
|
|
62
|
-
}
|
|
63
|
-
const timestamp = parseVersionTimestamp(stamp);
|
|
64
|
-
if (!timestamp) return null;
|
|
65
|
-
return { basename, timestamp, collision };
|
|
50
|
+
return parseSnapshotPath(DOCUMENT_VERSION_PATHS, path);
|
|
66
51
|
}
|