@bendyline/squisq 1.5.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -10
- package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
- package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
- package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
- package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-4X3JQXNM.js +35 -0
- package/dist/chunk-4X3JQXNM.js.map +1 -0
- package/dist/{chunk-2OIBZYKP.js → chunk-5DKBFXPY.js} +31 -121
- package/dist/chunk-5DKBFXPY.js.map +1 -0
- package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
- package/dist/chunk-5J4HKRJF.js.map +1 -0
- package/dist/chunk-5UYI3RJB.js +2503 -0
- package/dist/chunk-5UYI3RJB.js.map +1 -0
- package/dist/{chunk-TEEEILMP.js → chunk-66QO7XFB.js} +19 -292
- package/dist/chunk-66QO7XFB.js.map +1 -0
- package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
- package/dist/chunk-6PJRB2CF.js.map +1 -0
- package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
- package/dist/chunk-AMIR72JP.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-LH7I6SH7.js → chunk-GF3BVNLU.js} +6360 -5723
- package/dist/chunk-GF3BVNLU.js.map +1 -0
- package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
- package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
- package/dist/chunk-JOQYPRDZ.js +127 -0
- package/dist/chunk-JOQYPRDZ.js.map +1 -0
- package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
- package/dist/chunk-LYNJEOAC.js.map +1 -0
- package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
- package/dist/chunk-PXLVXTLW.js.map +1 -0
- package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
- package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
- package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
- package/dist/chunk-QTSJOY54.js.map +1 -0
- package/dist/chunk-QVANWDPH.js +2358 -0
- package/dist/chunk-QVANWDPH.js.map +1 -0
- package/dist/{chunk-6ATE2PSM.js → chunk-R2GC6RLD.js} +7 -7
- package/dist/chunk-R2GC6RLD.js.map +1 -0
- package/dist/chunk-R2YZTCLO.js +292 -0
- package/dist/chunk-R2YZTCLO.js.map +1 -0
- package/dist/chunk-S7A5D7ZJ.js +174 -0
- package/dist/chunk-S7A5D7ZJ.js.map +1 -0
- package/dist/chunk-T3TI7GSZ.js +884 -0
- package/dist/chunk-T3TI7GSZ.js.map +1 -0
- package/dist/{chunk-RUDYOTA6.js → chunk-TUWMK62B.js} +110 -4
- package/dist/chunk-TUWMK62B.js.map +1 -0
- package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
- package/dist/chunk-VR7UPJ7C.js.map +1 -0
- package/dist/doc/index.d.ts +2334 -5
- package/dist/doc/index.js +134 -34
- package/dist/generate/index.d.ts +7 -2
- package/dist/generate/index.js +1 -1
- package/dist/imageEdit/index.d.ts +9 -6
- package/dist/imageEdit/index.js +3 -3
- package/dist/index.d.ts +12 -10
- package/dist/index.js +253 -81
- package/dist/jsonForm/index.d.ts +7 -3
- package/dist/jsonForm/index.js +9 -4
- package/dist/markdown/index.d.ts +6 -70
- package/dist/markdown/index.js +13 -9
- package/dist/narration/index.d.ts +618 -0
- package/dist/narration/index.js +82 -0
- package/dist/recommend/index.d.ts +15 -1
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +28 -6
- package/dist/schemas/index.js +15 -13
- package/dist/spatial/index.d.ts +0 -1
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
- package/dist/timing/index.js +5 -3
- package/dist/transform/index.d.ts +35 -24
- package/dist/transform/index.js +9 -10
- package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
- package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
- package/dist/versions/index.d.ts +2 -1
- package/dist/versions/index.js +2 -2
- package/package.json +6 -6
- package/src/__tests__/applyNarrationTiming.test.ts +190 -0
- package/src/__tests__/applyRenderStyle.test.ts +3 -3
- package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
- package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
- package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
- package/src/__tests__/asciiDiagramParse.test.ts +233 -0
- package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
- package/src/__tests__/asciiDiagramRender.test.ts +162 -0
- package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
- package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
- package/src/__tests__/asciiTimelineParse.test.ts +205 -0
- package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
- package/src/__tests__/asciiTimelineRender.test.ts +376 -0
- package/src/__tests__/attrTokens.test.ts +5 -0
- package/src/__tests__/contentContainer.test.ts +43 -0
- package/src/__tests__/coverBlock.test.ts +33 -2
- package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
- package/src/__tests__/dataTable.test.ts +25 -15
- package/src/__tests__/diagramBlock.test.ts +3 -3
- package/src/__tests__/diagramText.test.ts +26 -0
- package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
- package/src/__tests__/drawingBlock.test.ts +3 -3
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
- package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
- package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
- package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
- package/src/__tests__/getLayers.test.ts +32 -18
- package/src/__tests__/getLayersFallback.test.ts +8 -8
- package/src/__tests__/imageEditVersions.test.ts +2 -0
- package/src/__tests__/jsonForm.pointer.test.ts +47 -1
- package/src/__tests__/layoutBlock.test.ts +3 -3
- package/src/__tests__/majorApiTypes.test.ts +18 -0
- package/src/__tests__/markdown.test.ts +41 -0
- package/src/__tests__/markdownToDoc.test.ts +5 -2
- package/src/__tests__/matchFontFamily.test.ts +114 -0
- package/src/__tests__/materializeBlockLayers.test.ts +135 -0
- package/src/__tests__/materializeTestUtils.ts +17 -0
- package/src/__tests__/narrationAlign.test.ts +150 -0
- package/src/__tests__/narrationFeatures.test.ts +71 -0
- package/src/__tests__/narrationNuclei.test.ts +45 -0
- package/src/__tests__/narrationPacing.test.ts +189 -0
- package/src/__tests__/narrationScript.test.ts +149 -0
- package/src/__tests__/narrationSessionRepro.test.ts +62 -0
- package/src/__tests__/narrationSidecar.test.ts +88 -0
- package/src/__tests__/narrationSyllables.test.ts +65 -0
- package/src/__tests__/narrationTestSignals.ts +167 -0
- package/src/__tests__/narrationVad.test.ts +66 -0
- package/src/__tests__/resolveDocTheme.test.ts +16 -2
- package/src/__tests__/rootExports.test.ts +18 -0
- package/src/__tests__/scopedContentContainer.test.ts +20 -0
- package/src/__tests__/shapeGeometry.test.ts +49 -0
- package/src/__tests__/templateAnnotationParse.test.ts +10 -0
- package/src/__tests__/templateInputs.test.ts +69 -0
- package/src/__tests__/templateMetadata.test.ts +2 -1
- package/src/__tests__/templates.test.ts +143 -17
- package/src/__tests__/themeCompile.test.ts +49 -22
- package/src/__tests__/transformProvenance.test.ts +160 -0
- package/src/__tests__/transformV2.test.ts +40 -14
- package/src/__tests__/treeviewBattle.test.ts +76 -0
- package/src/__tests__/treeviewDetect.test.ts +168 -0
- package/src/__tests__/treeviewParse.test.ts +118 -0
- package/src/__tests__/treeviewPipeline.test.ts +183 -0
- package/src/__tests__/treeviewRender.test.ts +91 -0
- package/src/__tests__/versions.test.ts +44 -0
- package/src/doc/applyNarrationTiming.ts +244 -0
- package/src/doc/asciiDiagram/chars.ts +210 -0
- package/src/doc/asciiDiagram/detect.ts +175 -0
- package/src/doc/asciiDiagram/index.ts +38 -0
- package/src/doc/asciiDiagram/mapping.ts +225 -0
- package/src/doc/asciiDiagram/parse.ts +1095 -0
- package/src/doc/asciiDiagram/render.ts +963 -0
- package/src/doc/asciiDiagram/repair.ts +400 -0
- package/src/doc/asciiDiagram/types.ts +65 -0
- package/src/doc/asciiTimeline/detect.ts +138 -0
- package/src/doc/asciiTimeline/index.ts +26 -0
- package/src/doc/asciiTimeline/mapping.ts +43 -0
- package/src/doc/asciiTimeline/parse.ts +870 -0
- package/src/doc/asciiTimeline/render.ts +427 -0
- package/src/doc/asciiTimeline/types.ts +73 -0
- package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
- package/src/doc/audioMapping.ts +60 -46
- package/src/doc/customTemplatesFrontmatter.ts +0 -41
- package/src/doc/docToMarkdown.ts +126 -39
- package/src/doc/index.ts +76 -4
- package/src/doc/markdownToDoc.ts +135 -1
- package/src/doc/materializeBlockLayers.ts +304 -0
- package/src/doc/mediaAnnotations.ts +20 -0
- package/src/doc/resolveDocTheme.ts +8 -7
- package/src/doc/templateInputs.ts +185 -2
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
- package/src/doc/templates/coverBlock.ts +49 -13
- package/src/doc/templates/diagramBlock.ts +237 -38
- package/src/doc/templates/drawingBlock.ts +2 -2
- package/src/doc/templates/fallbackBlock.ts +1 -1
- package/src/doc/templates/index.ts +134 -279
- package/src/doc/templates/inputDescriptors.ts +20 -1
- package/src/doc/templates/metadata.ts +10 -0
- package/src/doc/templates/persistentLayers.ts +12 -126
- package/src/doc/templates/registry.ts +88 -0
- package/src/doc/templates/templateNames.ts +22 -0
- package/src/doc/templates/timelineBlock.ts +613 -0
- package/src/doc/templates/treeBlock.ts +131 -0
- package/src/doc/treeview/chars.ts +72 -0
- package/src/doc/treeview/detect.ts +145 -0
- package/src/doc/treeview/index.ts +30 -0
- package/src/doc/treeview/mapping.ts +95 -0
- package/src/doc/treeview/parse.ts +201 -0
- package/src/doc/treeview/render.ts +41 -0
- package/src/doc/treeview/types.ts +43 -0
- package/src/doc/utils/applyRenderStyle.ts +2 -9
- package/src/doc/utils/diagramText.ts +78 -0
- package/src/doc/utils/shapeGeometry.ts +157 -7
- package/src/doc/validate.ts +1 -1
- package/src/generate/templateMapper.ts +7 -0
- package/src/imageEdit/export.ts +6 -5
- package/src/imageEdit/versionPaths.ts +13 -26
- package/src/imageEdit/versions.ts +21 -100
- package/src/index.ts +1 -0
- package/src/internal/immutable.ts +58 -0
- package/src/jsonForm/index.ts +9 -1
- package/src/jsonForm/pointer.ts +68 -23
- package/src/markdown/attrTokens.ts +7 -5
- package/src/markdown/convert.ts +11 -1
- package/src/markdown/index.ts +1 -0
- package/src/markdown/stringify.ts +4 -9
- package/src/markdown/utils.ts +36 -14
- package/src/narration/align.ts +514 -0
- package/src/narration/features.ts +231 -0
- package/src/narration/index.ts +86 -0
- package/src/narration/nuclei.ts +118 -0
- package/src/narration/pacing.ts +211 -0
- package/src/narration/script.ts +239 -0
- package/src/narration/session.ts +100 -0
- package/src/narration/sidecar.ts +187 -0
- package/src/narration/syllables.ts +85 -0
- package/src/narration/trace.ts +50 -0
- package/src/narration/types.ts +299 -0
- package/src/narration/vad.ts +131 -0
- package/src/recommend/numberHighlight.ts +25 -0
- package/src/recommend/templates.ts +108 -12
- package/src/schemas/BlockTemplates.ts +156 -7
- package/src/schemas/Doc.ts +72 -9
- package/src/schemas/Media.ts +12 -2
- package/src/schemas/Theme.ts +45 -25
- package/src/schemas/Viewport.ts +2 -2
- package/src/schemas/fontStacks.ts +79 -0
- package/src/schemas/themeCompile.ts +15 -0
- package/src/schemas/themeConstants.ts +2 -0
- package/src/schemas/themeLibrary.ts +25 -19
- package/src/schemas/themeValidator.ts +1 -1
- package/src/spatial/Geohash.ts +0 -3
- package/src/storage/ContentContainer.ts +17 -9
- package/src/storage/ScopedContentContainer.ts +45 -15
- package/src/transform/applyTransform.ts +5 -5
- package/src/transform/index.ts +3 -1
- package/src/transform/registry.ts +169 -49
- package/src/transform/templateSelector.ts +21 -2
- package/src/transform/timingAllocator.ts +152 -21
- package/src/transform/types.ts +14 -1
- package/src/versions/gitignore.ts +34 -0
- package/src/versions/index.ts +2 -0
- package/src/versions/operations.ts +18 -104
- package/src/versions/paths.ts +13 -28
- package/src/versions/snapshotStore.ts +202 -0
- package/src/versions/types.ts +3 -1
- package/dist/chunk-2OIBZYKP.js.map +0 -1
- package/dist/chunk-6ATE2PSM.js.map +0 -1
- package/dist/chunk-A4O7GIWE.js.map +0 -1
- package/dist/chunk-DLXZMS5K.js.map +0 -1
- package/dist/chunk-HWVFJAAH.js.map +0 -1
- package/dist/chunk-IMSCRLLV.js.map +0 -1
- package/dist/chunk-JELIRVDP.js +0 -24
- package/dist/chunk-JELIRVDP.js.map +0 -1
- package/dist/chunk-KKNUBQ6Y.js.map +0 -1
- package/dist/chunk-LDQ2HJIX.js.map +0 -1
- package/dist/chunk-LH7I6SH7.js.map +0 -1
- package/dist/chunk-RUDYOTA6.js.map +0 -1
- package/dist/chunk-T5UK6YOB.js.map +0 -1
- package/dist/chunk-TEEEILMP.js.map +0 -1
- package/dist/chunk-UY7KGQ5R.js +0 -192
- package/dist/chunk-UY7KGQ5R.js.map +0 -1
- package/dist/chunk-VLJJHDAC.js +0 -218
- package/dist/chunk-VLJJHDAC.js.map +0 -1
- package/dist/chunk-VWUFZ6ZG.js.map +0 -1
- package/dist/story/index.d.ts +0 -1777
- package/dist/story/index.js +0 -273
- package/src/doc/getLayers.ts +0 -195
- /package/dist/{story → narration}/index.js.map +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII tree renderer: nested nodes → clean tree art.
|
|
3
|
+
*
|
|
4
|
+
* A deterministic depth-first walk emitting rail/branch prefixes. The
|
|
5
|
+
* fixpoint is structural: `parseTree(renderTree(t))` preserves the node
|
|
6
|
+
* hierarchy + labels + comments, and `renderTree(parseTree(renderTree(t)))`
|
|
7
|
+
* is byte-identical (idempotent after one normalization cycle). Unlike the
|
|
8
|
+
* diagram renderer there is no spatial layout, so jitter is impossible.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ASCII_TREE_VOCAB, UNICODE_TREE_VOCAB } from './chars.js';
|
|
12
|
+
import type { Tree, TreeNode } from './types.js';
|
|
13
|
+
|
|
14
|
+
export interface RenderTreeOptions {
|
|
15
|
+
/** Character vocabulary; defaults to the tree's own detected style. */
|
|
16
|
+
style?: 'unicode' | 'ascii';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderTree(tree: Tree, options: RenderTreeOptions = {}): string {
|
|
20
|
+
const vocab = (options.style ?? tree.style) === 'ascii' ? ASCII_TREE_VOCAB : UNICODE_TREE_VOCAB;
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
|
|
23
|
+
const label = (node: TreeNode): string =>
|
|
24
|
+
node.comment ? `${node.label} # ${node.comment}` : node.label;
|
|
25
|
+
|
|
26
|
+
const walk = (nodes: readonly TreeNode[], prefix: string): void => {
|
|
27
|
+
nodes.forEach((node, i) => {
|
|
28
|
+
const last = i === nodes.length - 1;
|
|
29
|
+
lines.push(prefix + (last ? vocab.elbow : vocab.tee) + label(node));
|
|
30
|
+
walk(node.children, prefix + (last ? vocab.gap : vocab.rail));
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Roots sit at column 0 with no connector; their children get branches.
|
|
35
|
+
for (const root of tree.roots) {
|
|
36
|
+
lines.push(label(root));
|
|
37
|
+
walk(root.children, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the ASCII treeview codec.
|
|
3
|
+
*
|
|
4
|
+
* A tree is a hierarchy of labelled nodes (file trees, dependency trees,
|
|
5
|
+
* category outlines). Unlike the diagram codec's spatial grid model, the
|
|
6
|
+
* tree model is a plain nested structure — rendering is a deterministic
|
|
7
|
+
* tree-walk, so the parse↔render fixpoint is structural and trivially
|
|
8
|
+
* byte-stable.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface TreeNode {
|
|
12
|
+
/**
|
|
13
|
+
* Stable id: slug of the label, deduplicated with `-2`/`-3` suffixes in
|
|
14
|
+
* document (depth-first) order. Identical art re-parses to identical ids
|
|
15
|
+
* (the outline widget's selection resolves by id).
|
|
16
|
+
*/
|
|
17
|
+
id: string;
|
|
18
|
+
/** Display label, verbatim (may keep a trailing `/`). */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Child nodes (empty for leaves). */
|
|
21
|
+
children: TreeNode[];
|
|
22
|
+
/** True when this node reads as a container (trailing `/` or has children). */
|
|
23
|
+
isDir?: boolean;
|
|
24
|
+
/** Trailing `# …` / `<-- …` comment, preserved across round-trips. */
|
|
25
|
+
comment?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Tree {
|
|
29
|
+
/** Top-level nodes (a forest — usually one root for a file tree). */
|
|
30
|
+
roots: TreeNode[];
|
|
31
|
+
/** Character vocabulary detected in the source; render emits the same. */
|
|
32
|
+
style: 'unicode' | 'ascii';
|
|
33
|
+
/** Non-fatal parse notes (ragged dedents, dropped lines, …). */
|
|
34
|
+
warnings: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TreeDetection {
|
|
38
|
+
isTree: boolean;
|
|
39
|
+
/** Present when `isTree` — avoids a second parse in callers. */
|
|
40
|
+
tree?: Tree;
|
|
41
|
+
/** Machine-greppable accept/reject reasons. */
|
|
42
|
+
reasons: string[];
|
|
43
|
+
}
|
|
@@ -50,16 +50,9 @@ function isFullBleedCoverMedia(layer: Layer): boolean {
|
|
|
50
50
|
return fit === undefined || fit === 'cover';
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
* Whether the theme asks for ambient Ken Burns on otherwise-static imagery.
|
|
55
|
-
* `defaultImageAnimation: 'slowZoom'` is treated as an alias for the boolean
|
|
56
|
-
* (that's what themes authoring it were reaching for).
|
|
57
|
-
*/
|
|
53
|
+
/** Whether the theme asks for ambient Ken Burns on otherwise-static imagery. */
|
|
58
54
|
export function themeWantsAmbientMotion(theme: Theme): boolean {
|
|
59
|
-
return
|
|
60
|
-
theme.renderStyle.ambientMotion === true ||
|
|
61
|
-
theme.renderStyle.defaultImageAnimation === 'slowZoom'
|
|
62
|
-
);
|
|
55
|
+
return theme.renderStyle.ambientMotion === true;
|
|
63
56
|
}
|
|
64
57
|
|
|
65
58
|
/**
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Text fitting shared by the live diagram canvas and template renderer. */
|
|
2
|
+
|
|
3
|
+
export const DIAGRAM_LABEL_LINE_HEIGHT = 1.25;
|
|
4
|
+
export const DIAGRAM_LABEL_HORIZONTAL_PADDING = 24;
|
|
5
|
+
export const DIAGRAM_LABEL_VERTICAL_PADDING = 16;
|
|
6
|
+
export const DIAGRAM_LABEL_MIN_FONT_SIZE = 10;
|
|
7
|
+
|
|
8
|
+
export interface DiagramLabelFit {
|
|
9
|
+
fontSize: number;
|
|
10
|
+
lineCount: number;
|
|
11
|
+
/** Upward shift that centers the full line group around the node midpoint. */
|
|
12
|
+
firstLineOffset: number;
|
|
13
|
+
textWidth: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fit a plain diagram label inside a node card using the same character-width
|
|
18
|
+
* estimate and word wrapping as React's `TextLayer`.
|
|
19
|
+
*/
|
|
20
|
+
export function fitDiagramLabel(
|
|
21
|
+
text: string,
|
|
22
|
+
boxWidth: number,
|
|
23
|
+
boxHeight: number,
|
|
24
|
+
preferredFontSize: number,
|
|
25
|
+
): DiagramLabelFit {
|
|
26
|
+
const textWidth = Math.max(1, boxWidth - DIAGRAM_LABEL_HORIZONTAL_PADDING);
|
|
27
|
+
const textHeight = Math.max(1, boxHeight - DIAGRAM_LABEL_VERTICAL_PADDING);
|
|
28
|
+
const preferred = Math.max(DIAGRAM_LABEL_MIN_FONT_SIZE, Math.floor(preferredFontSize));
|
|
29
|
+
|
|
30
|
+
let fontSize = preferred;
|
|
31
|
+
let lineCount = countWrappedLines(text, fontSize, textWidth);
|
|
32
|
+
while (
|
|
33
|
+
fontSize > DIAGRAM_LABEL_MIN_FONT_SIZE &&
|
|
34
|
+
lineCount * fontSize * DIAGRAM_LABEL_LINE_HEIGHT > textHeight
|
|
35
|
+
) {
|
|
36
|
+
fontSize--;
|
|
37
|
+
lineCount = countWrappedLines(text, fontSize, textWidth);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
fontSize,
|
|
42
|
+
lineCount,
|
|
43
|
+
firstLineOffset:
|
|
44
|
+
lineCount === 1 ? 0 : -((lineCount - 1) * fontSize * DIAGRAM_LABEL_LINE_HEIGHT) / 2,
|
|
45
|
+
textWidth,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function countWrappedLines(text: string, fontSize: number, maxWidth: number): number {
|
|
50
|
+
return text
|
|
51
|
+
.split('\n')
|
|
52
|
+
.reduce((count, line) => count + wrappedLineCount(line, fontSize, maxWidth), 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function wrappedLineCount(text: string, fontSize: number, maxWidth: number): number {
|
|
56
|
+
if (!text.trim()) return 1;
|
|
57
|
+
const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
|
|
58
|
+
if (charsPerLine <= 0) return 1;
|
|
59
|
+
|
|
60
|
+
let lines = 0;
|
|
61
|
+
let currentLength = 0;
|
|
62
|
+
for (const word of text.split(/\s+/)) {
|
|
63
|
+
const testLength = currentLength > 0 ? currentLength + 1 + word.length : word.length;
|
|
64
|
+
if (testLength <= charsPerLine) {
|
|
65
|
+
currentLength = testLength;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (currentLength > 0) lines++;
|
|
69
|
+
if (word.length > charsPerLine) {
|
|
70
|
+
lines += Math.floor((word.length - 1) / charsPerLine);
|
|
71
|
+
currentLength = word.length % charsPerLine || charsPerLine;
|
|
72
|
+
} else {
|
|
73
|
+
currentLength = word.length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (currentLength > 0) lines++;
|
|
77
|
+
return Math.max(1, lines);
|
|
78
|
+
}
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* - `markerPath(style, dir)` → the `<marker>` path for a line end-style, in the
|
|
10
10
|
* standard 0–10 marker viewBox (shared by the SSR `PathLayer` renderer and the
|
|
11
11
|
* editor's edge renderer).
|
|
12
|
-
* - `connectorPath(routing, start, end)` + `
|
|
12
|
+
* - `connectorPath(routing, start, end)` + `snapEndpoints(a, b)` → the line/edge
|
|
13
13
|
* geometry between two shapes (straight / orthogonal / curved), lifted from the
|
|
14
|
-
* diagram template so drawings and diagrams draw identical connectors.
|
|
14
|
+
* diagram template so drawings and diagrams draw identical connectors. The older
|
|
15
|
+
* `clipEndpoints(a, b)` helper remains for free-angle clipping, but rendered
|
|
16
|
+
* connectors use snapped ports so authored lines land on stable attachment points.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import type { MarkerStyle } from '../../schemas/Doc.js';
|
|
@@ -411,6 +413,27 @@ export interface ClipBox {
|
|
|
411
413
|
ry: number;
|
|
412
414
|
}
|
|
413
415
|
|
|
416
|
+
export type ConnectorPort =
|
|
417
|
+
| 'top'
|
|
418
|
+
| 'right'
|
|
419
|
+
| 'bottom'
|
|
420
|
+
| 'left'
|
|
421
|
+
| 'top-left'
|
|
422
|
+
| 'top-right'
|
|
423
|
+
| 'bottom-right'
|
|
424
|
+
| 'bottom-left';
|
|
425
|
+
|
|
426
|
+
export interface ConnectorSnapPoint {
|
|
427
|
+
port: ConnectorPort;
|
|
428
|
+
x: number;
|
|
429
|
+
y: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export interface ConnectorAnchor {
|
|
433
|
+
side: 'top' | 'right' | 'bottom' | 'left';
|
|
434
|
+
offset: number;
|
|
435
|
+
}
|
|
436
|
+
|
|
414
437
|
/** Intersection of the center-to-center line with `from`'s bounding box. */
|
|
415
438
|
export function clipPoint(from: ClipBox, to: ClipBox): { x: number; y: number } {
|
|
416
439
|
const dx = to.cx - from.cx;
|
|
@@ -430,23 +453,150 @@ export function clipEndpoints(
|
|
|
430
453
|
return { start: clipPoint(a, b), end: clipPoint(b, a) };
|
|
431
454
|
}
|
|
432
455
|
|
|
456
|
+
/** The stable connector ports exposed around a shape/card box. */
|
|
457
|
+
export function snapPoints(box: ClipBox): ConnectorSnapPoint[] {
|
|
458
|
+
const left = box.cx - box.rx;
|
|
459
|
+
const right = box.cx + box.rx;
|
|
460
|
+
const top = box.cy - box.ry;
|
|
461
|
+
const bottom = box.cy + box.ry;
|
|
462
|
+
return [
|
|
463
|
+
{ port: 'top', x: box.cx, y: top },
|
|
464
|
+
{ port: 'right', x: right, y: box.cy },
|
|
465
|
+
{ port: 'bottom', x: box.cx, y: bottom },
|
|
466
|
+
{ port: 'left', x: left, y: box.cy },
|
|
467
|
+
{ port: 'top-left', x: left, y: top },
|
|
468
|
+
{ port: 'top-right', x: right, y: top },
|
|
469
|
+
{ port: 'bottom-right', x: right, y: bottom },
|
|
470
|
+
{ port: 'bottom-left', x: left, y: bottom },
|
|
471
|
+
];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Resolve a normalized authored anchor to a concrete point on a box. */
|
|
475
|
+
export function anchorPoint(box: ClipBox, anchor: ConnectorAnchor): ConnectorSnapPoint {
|
|
476
|
+
const offset = Math.max(0, Math.min(1, anchor.offset));
|
|
477
|
+
const left = box.cx - box.rx;
|
|
478
|
+
const right = box.cx + box.rx;
|
|
479
|
+
const top = box.cy - box.ry;
|
|
480
|
+
const bottom = box.cy + box.ry;
|
|
481
|
+
switch (anchor.side) {
|
|
482
|
+
case 'top':
|
|
483
|
+
return { port: 'top', x: left + (right - left) * offset, y: top };
|
|
484
|
+
case 'right':
|
|
485
|
+
return { port: 'right', x: right, y: top + (bottom - top) * offset };
|
|
486
|
+
case 'bottom':
|
|
487
|
+
return { port: 'bottom', x: left + (right - left) * offset, y: bottom };
|
|
488
|
+
case 'left':
|
|
489
|
+
return { port: 'left', x: left, y: top + (bottom - top) * offset };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Nearest stable connector port on `box` to an arbitrary point. */
|
|
494
|
+
export function nearestSnapPoint(
|
|
495
|
+
box: ClipBox,
|
|
496
|
+
point: { x: number; y: number },
|
|
497
|
+
): ConnectorSnapPoint {
|
|
498
|
+
let best = snapPoints(box)[0];
|
|
499
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
500
|
+
for (const candidate of snapPoints(box)) {
|
|
501
|
+
const score = distanceSq(candidate, point) + portTieBreak(candidate.port);
|
|
502
|
+
if (score < bestScore) {
|
|
503
|
+
best = candidate;
|
|
504
|
+
bestScore = score;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return best;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Pick the pair of connector ports, one on each box, with the shortest
|
|
512
|
+
* distance between them. This gives stable attachments to side/corner handles
|
|
513
|
+
* instead of arbitrary center-ray intersections.
|
|
514
|
+
*/
|
|
515
|
+
export function snapEndpoints(
|
|
516
|
+
a: ClipBox,
|
|
517
|
+
b: ClipBox,
|
|
518
|
+
): { start: ConnectorSnapPoint; end: ConnectorSnapPoint } {
|
|
519
|
+
let bestStart = snapPoints(a)[0];
|
|
520
|
+
let bestEnd = snapPoints(b)[0];
|
|
521
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
522
|
+
for (const start of snapPoints(a)) {
|
|
523
|
+
for (const end of snapPoints(b)) {
|
|
524
|
+
const score = distanceSq(start, end) + portTieBreak(start.port) + portTieBreak(end.port);
|
|
525
|
+
if (score < bestScore) {
|
|
526
|
+
bestStart = start;
|
|
527
|
+
bestEnd = end;
|
|
528
|
+
bestScore = score;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { start: bestStart, end: bestEnd };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function distanceSq(a: { x: number; y: number }, b: { x: number; y: number }): number {
|
|
536
|
+
const dx = a.x - b.x;
|
|
537
|
+
const dy = a.y - b.y;
|
|
538
|
+
return dx * dx + dy * dy;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function portTieBreak(port: ConnectorPort): number {
|
|
542
|
+
return port.includes('-') ? 0.02 : 0;
|
|
543
|
+
}
|
|
544
|
+
|
|
433
545
|
/** Build the SVG `d` for a connector between two clipped endpoints. */
|
|
434
546
|
export function connectorPath(
|
|
435
547
|
routing: ConnectorRouting,
|
|
436
|
-
start: { x: number; y: number },
|
|
437
|
-
end: { x: number; y: number },
|
|
548
|
+
start: { x: number; y: number; port?: ConnectorPort },
|
|
549
|
+
end: { x: number; y: number; port?: ConnectorPort },
|
|
438
550
|
): string {
|
|
439
551
|
if (routing === 'straight') {
|
|
440
552
|
return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
|
|
441
553
|
}
|
|
442
554
|
if (routing === 'orthogonal') {
|
|
555
|
+
const startAxis = portAxis(start.port);
|
|
556
|
+
const endAxis = portAxis(end.port);
|
|
557
|
+
if (startAxis === 'vertical' && endAxis === 'vertical') {
|
|
558
|
+
const midY = (start.y + end.y) / 2;
|
|
559
|
+
return `M ${r(start.x)} ${r(start.y)} L ${r(start.x)} ${r(midY)} L ${r(end.x)} ${r(midY)} L ${r(end.x)} ${r(end.y)}`;
|
|
560
|
+
}
|
|
561
|
+
if (startAxis === 'vertical' && endAxis === 'horizontal') {
|
|
562
|
+
return `M ${r(start.x)} ${r(start.y)} L ${r(start.x)} ${r(end.y)} L ${r(end.x)} ${r(end.y)}`;
|
|
563
|
+
}
|
|
564
|
+
if (startAxis === 'horizontal' && endAxis === 'vertical') {
|
|
565
|
+
return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
|
|
566
|
+
}
|
|
443
567
|
const midX = (start.x + end.x) / 2;
|
|
444
568
|
return `M ${r(start.x)} ${r(start.y)} L ${r(midX)} ${r(start.y)} L ${r(midX)} ${r(end.y)} L ${r(end.x)} ${r(end.y)}`;
|
|
445
569
|
}
|
|
446
570
|
// curved
|
|
447
|
-
const
|
|
448
|
-
const cp = Math.max(40,
|
|
449
|
-
|
|
571
|
+
const distance = Math.hypot(end.x - start.x, end.y - start.y);
|
|
572
|
+
const cp = Math.max(40, distance / 2);
|
|
573
|
+
const startVector = portVector(start.port, { x: 1, y: 0 });
|
|
574
|
+
const endVector = portVector(end.port, { x: -1, y: 0 });
|
|
575
|
+
return `M ${r(start.x)} ${r(start.y)} C ${r(start.x + startVector.x * cp)} ${r(start.y + startVector.y * cp)}, ${r(end.x + endVector.x * cp)} ${r(end.y + endVector.y * cp)}, ${r(end.x)} ${r(end.y)}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function portAxis(port: ConnectorPort | undefined): 'horizontal' | 'vertical' | null {
|
|
579
|
+
if (port === 'top' || port === 'bottom') return 'vertical';
|
|
580
|
+
if (port === 'left' || port === 'right') return 'horizontal';
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function portVector(
|
|
585
|
+
port: ConnectorPort | undefined,
|
|
586
|
+
fallback: { x: number; y: number },
|
|
587
|
+
): { x: number; y: number } {
|
|
588
|
+
switch (port) {
|
|
589
|
+
case 'top':
|
|
590
|
+
return { x: 0, y: -1 };
|
|
591
|
+
case 'right':
|
|
592
|
+
return { x: 1, y: 0 };
|
|
593
|
+
case 'bottom':
|
|
594
|
+
return { x: 0, y: 1 };
|
|
595
|
+
case 'left':
|
|
596
|
+
return { x: -1, y: 0 };
|
|
597
|
+
default:
|
|
598
|
+
return fallback;
|
|
599
|
+
}
|
|
450
600
|
}
|
|
451
601
|
|
|
452
602
|
/** Map a `lineStyle` (solid|dashed|dotted) to an SVG `stroke-dasharray`. */
|
package/src/doc/validate.ts
CHANGED
|
@@ -36,12 +36,12 @@ import { deriveTemplateInputs } from './templateInputs.js';
|
|
|
36
36
|
import { lintTemplateParams } from './templates/inputDescriptors.js';
|
|
37
37
|
import {
|
|
38
38
|
templateRegistry,
|
|
39
|
-
TEMPLATE_ALIASES,
|
|
40
39
|
resolveTemplateName,
|
|
41
40
|
isShapeName,
|
|
42
41
|
normalizeShapeKind,
|
|
43
42
|
SHAPE_NAMES,
|
|
44
43
|
} from './templates/index.js';
|
|
44
|
+
import { TEMPLATE_ALIASES } from './templates/templateNames.js';
|
|
45
45
|
import { nearestName } from './utils/nearest.js';
|
|
46
46
|
|
|
47
47
|
// ============================================
|
|
@@ -38,8 +38,13 @@ export interface MapOptions {
|
|
|
38
38
|
audioSegment: number;
|
|
39
39
|
colorScheme?: ColorScheme;
|
|
40
40
|
accentImage?: AccentImage;
|
|
41
|
+
/** Doc-timeline SECONDS when the extracted content is spoken. */
|
|
41
42
|
sourceStartTime?: number;
|
|
42
43
|
sourceDuration?: number;
|
|
44
|
+
/** Provenance: the source block the element was extracted from. */
|
|
45
|
+
sourceBlockId?: string;
|
|
46
|
+
/** Provenance: char offset of the extraction in the source plain text. */
|
|
47
|
+
sourceCharOffset?: number;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/**
|
|
@@ -52,6 +57,8 @@ export function mapElementToBlock(element: ExtractedElement, options: MapOptions
|
|
|
52
57
|
audioSegment: options.audioSegment,
|
|
53
58
|
sourceStartTime: options.sourceStartTime,
|
|
54
59
|
sourceDuration: options.sourceDuration,
|
|
60
|
+
sourceBlockId: options.sourceBlockId,
|
|
61
|
+
sourceCharOffset: options.sourceCharOffset,
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
switch (element.data.type) {
|
package/src/imageEdit/export.ts
CHANGED
|
@@ -203,9 +203,10 @@ function renderPathToSvg(
|
|
|
203
203
|
const fill = c.fill ?? 'none';
|
|
204
204
|
const dash = c.dasharray ? ` stroke-dasharray="${escapeAttr(c.dasharray)}"` : '';
|
|
205
205
|
|
|
206
|
-
//
|
|
207
|
-
const
|
|
208
|
-
const
|
|
206
|
+
// Keep old serialized documents readable without retaining `arrow` in the public type.
|
|
207
|
+
const legacyArrow = (c as typeof c & { arrow?: 'none' | 'end' | 'start' | 'both' }).arrow;
|
|
208
|
+
const startStyle = effectiveExportMarker(c.startMarker, legacyArrow, 'start');
|
|
209
|
+
const endStyle = effectiveExportMarker(c.endMarker, legacyArrow, 'end');
|
|
209
210
|
const start = markerPath(startStyle, 'start');
|
|
210
211
|
const end = markerPath(endStyle, 'end');
|
|
211
212
|
const startId = `marker-start-${layer.id}`;
|
|
@@ -228,11 +229,11 @@ function renderPathToSvg(
|
|
|
228
229
|
|
|
229
230
|
function effectiveExportMarker(
|
|
230
231
|
explicit: MarkerStyle | undefined,
|
|
231
|
-
|
|
232
|
+
legacyArrow: 'none' | 'end' | 'start' | 'both' | undefined,
|
|
232
233
|
end: 'start' | 'end',
|
|
233
234
|
): MarkerStyle {
|
|
234
235
|
if (explicit) return explicit;
|
|
235
|
-
return
|
|
236
|
+
return legacyArrow === 'both' || legacyArrow === end ? 'arrow' : 'none';
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
function markerDefSvg(
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* `.md` so the two histories live side-by-side without colliding.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
buildSnapshotPath,
|
|
15
|
+
parseSnapshotPath,
|
|
16
|
+
type SnapshotPathStrategy,
|
|
17
|
+
} from '../versions/snapshotStore.js';
|
|
14
18
|
|
|
15
19
|
/** Subfolder (inside the sidecar) that holds image-editor snapshots. */
|
|
16
20
|
export const IMAGE_EDIT_VERSIONS_PREFIX = '.versions/';
|
|
@@ -18,11 +22,15 @@ export const IMAGE_EDIT_VERSIONS_PREFIX = '.versions/';
|
|
|
18
22
|
/** Default basename when none is supplied. Matches `IMAGE_EDIT_STATE_FILENAME` stem. */
|
|
19
23
|
export const IMAGE_EDIT_DEFAULT_BASENAME = 'state';
|
|
20
24
|
|
|
25
|
+
/** Internal strategy shared with the generic snapshot store. */
|
|
26
|
+
export const IMAGE_EDIT_VERSION_PATHS: SnapshotPathStrategy = Object.freeze({
|
|
27
|
+
prefix: IMAGE_EDIT_VERSIONS_PREFIX,
|
|
28
|
+
extension: 'json',
|
|
29
|
+
});
|
|
30
|
+
|
|
21
31
|
/** Build a snapshot path. Optionally append a collision suffix. */
|
|
22
32
|
export function buildImageEditVersionPath(basename: string, date: Date, collision = 0): string {
|
|
23
|
-
|
|
24
|
-
const suffix = collision > 0 ? `-${collision + 1}` : '';
|
|
25
|
-
return `${IMAGE_EDIT_VERSIONS_PREFIX}${basename}.${stamp}${suffix}.json`;
|
|
33
|
+
return buildSnapshotPath(IMAGE_EDIT_VERSION_PATHS, basename, date, collision);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -32,26 +40,5 @@ export function buildImageEditVersionPath(basename: string, date: Date, collisio
|
|
|
32
40
|
export function parseImageEditVersionPath(
|
|
33
41
|
path: string,
|
|
34
42
|
): { basename: string; timestamp: Date; collision: number } | null {
|
|
35
|
-
|
|
36
|
-
const rest = path.slice(IMAGE_EDIT_VERSIONS_PREFIX.length);
|
|
37
|
-
if (!rest.endsWith('.json')) return null;
|
|
38
|
-
const stem = rest.slice(0, -'.json'.length);
|
|
39
|
-
const lastDot = stem.lastIndexOf('.');
|
|
40
|
-
if (lastDot <= 0) return null;
|
|
41
|
-
const basename = stem.slice(0, lastDot);
|
|
42
|
-
const tail = stem.slice(lastDot + 1);
|
|
43
|
-
const dash = tail.indexOf('-');
|
|
44
|
-
let stamp: string;
|
|
45
|
-
let collision = 0;
|
|
46
|
-
if (dash >= 0) {
|
|
47
|
-
stamp = tail.slice(0, dash);
|
|
48
|
-
const n = Number(tail.slice(dash + 1));
|
|
49
|
-
if (!Number.isInteger(n) || n < 2) return null;
|
|
50
|
-
collision = n - 1;
|
|
51
|
-
} else {
|
|
52
|
-
stamp = tail;
|
|
53
|
-
}
|
|
54
|
-
const timestamp = parseVersionTimestamp(stamp);
|
|
55
|
-
if (!timestamp) return null;
|
|
56
|
-
return { basename, timestamp, collision };
|
|
43
|
+
return parseSnapshotPath(IMAGE_EDIT_VERSION_PATHS, path);
|
|
57
44
|
}
|
|
@@ -5,23 +5,24 @@
|
|
|
5
5
|
* Mirrors `core/src/versions/operations.ts` but operates on
|
|
6
6
|
* {@link ImageEditDoc} JSON state at `.versions/<basename>.<timestamp>.json`
|
|
7
7
|
* instead of the markdown document. Reuses {@link PrunePolicy} so hosts
|
|
8
|
-
* can share their pruning configuration between the two histories.
|
|
8
|
+
* can share their pruning configuration between the two histories. The
|
|
9
|
+
* sidecar root gets a `.gitignore` rule for `.versions/` when snapshots
|
|
10
|
+
* are retained.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import type { ContentContainer } from '../storage/ContentContainer.js';
|
|
12
14
|
import type { ImageEditDoc } from '../schemas/ImageEditDoc.js';
|
|
13
15
|
import type { CoalesceOptions, PrunePolicy, Version } from '../versions/types.js';
|
|
16
|
+
import {
|
|
17
|
+
coalesceSnapshots,
|
|
18
|
+
listSnapshots,
|
|
19
|
+
pruneSnapshots,
|
|
20
|
+
readTextSnapshot,
|
|
21
|
+
saveTextSnapshot,
|
|
22
|
+
} from '../versions/snapshotStore.js';
|
|
14
23
|
import { readImageEditDoc, writeImageEditDoc } from './persistence.js';
|
|
15
24
|
import { IMAGE_EDIT_STATE_FILENAME } from './state.js';
|
|
16
|
-
import {
|
|
17
|
-
IMAGE_EDIT_VERSIONS_PREFIX,
|
|
18
|
-
IMAGE_EDIT_DEFAULT_BASENAME,
|
|
19
|
-
buildImageEditVersionPath,
|
|
20
|
-
parseImageEditVersionPath,
|
|
21
|
-
} from './versionPaths.js';
|
|
22
|
-
|
|
23
|
-
const decoder = new TextDecoder();
|
|
24
|
-
const encoder = new TextEncoder();
|
|
25
|
+
import { IMAGE_EDIT_DEFAULT_BASENAME, IMAGE_EDIT_VERSION_PATHS } from './versionPaths.js';
|
|
25
26
|
|
|
26
27
|
/** Options for {@link saveImageEditVersion}. */
|
|
27
28
|
export interface SaveImageEditVersionOptions {
|
|
@@ -60,26 +61,7 @@ export async function listImageEditVersions(
|
|
|
60
61
|
container: ContentContainer,
|
|
61
62
|
basename?: string,
|
|
62
63
|
): Promise<Version[]> {
|
|
63
|
-
|
|
64
|
-
const versions: Version[] = [];
|
|
65
|
-
for (const entry of entries) {
|
|
66
|
-
const parsed = parseImageEditVersionPath(entry.path);
|
|
67
|
-
if (!parsed) continue;
|
|
68
|
-
if (basename !== undefined && parsed.basename !== basename) continue;
|
|
69
|
-
versions.push({
|
|
70
|
-
path: entry.path,
|
|
71
|
-
basename: parsed.basename,
|
|
72
|
-
timestamp: parsed.timestamp,
|
|
73
|
-
size: entry.size,
|
|
74
|
-
collision: parsed.collision,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
versions.sort((a, b) => {
|
|
78
|
-
const dt = b.timestamp.getTime() - a.timestamp.getTime();
|
|
79
|
-
if (dt !== 0) return dt;
|
|
80
|
-
return b.collision - a.collision;
|
|
81
|
-
});
|
|
82
|
-
return versions;
|
|
64
|
+
return listSnapshots(container, IMAGE_EDIT_VERSION_PATHS, basename);
|
|
83
65
|
}
|
|
84
66
|
|
|
85
67
|
/** Read a snapshot's JSON text. Returns `null` if the snapshot is missing. */
|
|
@@ -87,10 +69,7 @@ export async function readImageEditVersionText(
|
|
|
87
69
|
container: ContentContainer,
|
|
88
70
|
version: Version | string,
|
|
89
71
|
): Promise<string | null> {
|
|
90
|
-
|
|
91
|
-
const data = await container.readFile(path);
|
|
92
|
-
if (!data) return null;
|
|
93
|
-
return decoder.decode(data);
|
|
72
|
+
return readTextSnapshot(container, version);
|
|
94
73
|
}
|
|
95
74
|
|
|
96
75
|
/** Read and parse a snapshot. Returns `null` if missing. */
|
|
@@ -124,34 +103,13 @@ export async function saveImageEditVersion(
|
|
|
124
103
|
const basename = options.basename ?? IMAGE_EDIT_DEFAULT_BASENAME;
|
|
125
104
|
const serialized = JSON.stringify(doc, null, 2);
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const latest = versions[0]!;
|
|
130
|
-
const existing = await readImageEditVersionText(container, latest);
|
|
131
|
-
if (existing === serialized) {
|
|
132
|
-
return { saved: false, version: null, reason: 'unchanged' };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const now = options.now ?? new Date();
|
|
137
|
-
let collision = 0;
|
|
138
|
-
let path = buildImageEditVersionPath(basename, now, collision);
|
|
139
|
-
while (await container.exists(path)) {
|
|
140
|
-
collision += 1;
|
|
141
|
-
path = buildImageEditVersionPath(basename, now, collision);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const data = encoder.encode(serialized);
|
|
145
|
-
await container.writeFile(path, data, 'application/json');
|
|
146
|
-
|
|
147
|
-
const version: Version = {
|
|
148
|
-
path,
|
|
106
|
+
return saveTextSnapshot(container, IMAGE_EDIT_VERSION_PATHS, {
|
|
107
|
+
content: serialized,
|
|
149
108
|
basename,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
};
|
|
154
|
-
return { saved: true, version, reason: 'saved' };
|
|
109
|
+
mimeType: 'application/json',
|
|
110
|
+
now: options.now,
|
|
111
|
+
force: options.force,
|
|
112
|
+
});
|
|
155
113
|
}
|
|
156
114
|
|
|
157
115
|
/** Revert `state.json` to a prior snapshot. Snapshots the current state first by default. */
|
|
@@ -181,27 +139,7 @@ export async function pruneImageEditVersions(
|
|
|
181
139
|
policy: PrunePolicy,
|
|
182
140
|
basename?: string,
|
|
183
141
|
): Promise<Version[]> {
|
|
184
|
-
|
|
185
|
-
const toDelete: Version[] = [];
|
|
186
|
-
|
|
187
|
-
if (policy.type === 'keep-last-n') {
|
|
188
|
-
const n = Math.max(0, Math.floor(policy.n));
|
|
189
|
-
toDelete.push(...versions.slice(n));
|
|
190
|
-
} else if (policy.type === 'older-than') {
|
|
191
|
-
const cutoff = policy.date.getTime();
|
|
192
|
-
for (const v of versions) {
|
|
193
|
-
if (v.timestamp.getTime() < cutoff) toDelete.push(v);
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
for (const v of versions) {
|
|
197
|
-
if (!policy.keep(v, versions)) toDelete.push(v);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
for (const v of toDelete) {
|
|
202
|
-
await container.removeFile(v.path);
|
|
203
|
-
}
|
|
204
|
-
return toDelete;
|
|
142
|
+
return pruneSnapshots(container, IMAGE_EDIT_VERSION_PATHS, policy, basename);
|
|
205
143
|
}
|
|
206
144
|
|
|
207
145
|
/** Collapse adjacent snapshots within `windowMs`, keeping the newer of each pair. */
|
|
@@ -210,22 +148,5 @@ export async function coalesceImageEditVersions(
|
|
|
210
148
|
options: CoalesceOptions = {},
|
|
211
149
|
basename?: string,
|
|
212
150
|
): Promise<Version[]> {
|
|
213
|
-
|
|
214
|
-
const versions = await listImageEditVersions(container, basename);
|
|
215
|
-
const toDelete: Version[] = [];
|
|
216
|
-
if (versions.length > 0) {
|
|
217
|
-
let anchor = versions[0]!;
|
|
218
|
-
for (let i = 1; i < versions.length; i++) {
|
|
219
|
-
const candidate = versions[i]!;
|
|
220
|
-
if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {
|
|
221
|
-
toDelete.push(candidate);
|
|
222
|
-
} else {
|
|
223
|
-
anchor = candidate;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
for (const v of toDelete) {
|
|
228
|
-
await container.removeFile(v.path);
|
|
229
|
-
}
|
|
230
|
-
return toDelete;
|
|
151
|
+
return coalesceSnapshots(container, IMAGE_EDIT_VERSION_PATHS, options, basename);
|
|
231
152
|
}
|
package/src/index.ts
CHANGED