@bendyline/squisq 1.5.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -10
- package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
- package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
- package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
- package/dist/{chunk-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
- package/dist/chunk-2PWOO4ZE.js.map +1 -0
- package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-4X3JQXNM.js +35 -0
- package/dist/chunk-4X3JQXNM.js.map +1 -0
- package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
- package/dist/chunk-5DKBFXPY.js.map +1 -0
- package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
- package/dist/chunk-5J4HKRJF.js.map +1 -0
- package/dist/chunk-5UYI3RJB.js +2503 -0
- package/dist/chunk-5UYI3RJB.js.map +1 -0
- package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
- package/dist/chunk-66QO7XFB.js.map +1 -0
- package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
- package/dist/chunk-AMIR72JP.js.map +1 -0
- package/dist/chunk-CY2R2VPR.js +884 -0
- package/dist/chunk-CY2R2VPR.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
- package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
- package/dist/chunk-JOQYPRDZ.js +127 -0
- package/dist/chunk-JOQYPRDZ.js.map +1 -0
- package/dist/{chunk-A4O7GIWE.js → chunk-MMPBOUK7.js} +10 -73
- package/dist/chunk-MMPBOUK7.js.map +1 -0
- package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
- package/dist/chunk-PXLVXTLW.js.map +1 -0
- package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
- package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
- package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
- package/dist/chunk-R2GC6RLD.js.map +1 -0
- package/dist/chunk-R2YZTCLO.js +292 -0
- package/dist/chunk-R2YZTCLO.js.map +1 -0
- package/dist/chunk-S7A5D7ZJ.js +174 -0
- package/dist/chunk-S7A5D7ZJ.js.map +1 -0
- package/dist/chunk-TFJVXUHH.js +2358 -0
- package/dist/chunk-TFJVXUHH.js.map +1 -0
- package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
- package/dist/chunk-TUWMK62B.js.map +1 -0
- package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
- package/dist/chunk-VR7UPJ7C.js.map +1 -0
- package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
- package/dist/chunk-W4O2TX5I.js.map +1 -0
- package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
- package/dist/chunk-X6DUONGT.js.map +1 -0
- package/dist/doc/index.d.ts +2334 -5
- package/dist/doc/index.js +127 -33
- package/dist/generate/index.d.ts +7 -2
- package/dist/generate/index.js +1 -1
- package/dist/imageEdit/index.d.ts +9 -6
- package/dist/imageEdit/index.js +3 -3
- package/dist/index.d.ts +12 -10
- package/dist/index.js +246 -80
- package/dist/jsonForm/index.d.ts +7 -3
- package/dist/jsonForm/index.js +9 -4
- package/dist/markdown/index.d.ts +6 -70
- package/dist/markdown/index.js +13 -9
- package/dist/narration/index.d.ts +618 -0
- package/dist/narration/index.js +82 -0
- package/dist/recommend/index.d.ts +15 -1
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +28 -6
- package/dist/schemas/index.js +15 -13
- package/dist/spatial/index.d.ts +0 -1
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
- package/dist/timing/index.js +5 -3
- package/dist/transform/index.d.ts +35 -24
- package/dist/transform/index.js +9 -10
- package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
- package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
- package/dist/versions/index.d.ts +2 -1
- package/dist/versions/index.js +2 -2
- package/package.json +6 -6
- package/src/__tests__/applyNarrationTiming.test.ts +190 -0
- package/src/__tests__/applyRenderStyle.test.ts +3 -3
- package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
- package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
- package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
- package/src/__tests__/asciiDiagramParse.test.ts +233 -0
- package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
- package/src/__tests__/asciiDiagramRender.test.ts +162 -0
- package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
- package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
- package/src/__tests__/asciiTimelineParse.test.ts +205 -0
- package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
- package/src/__tests__/asciiTimelineRender.test.ts +376 -0
- package/src/__tests__/attrTokens.test.ts +5 -0
- package/src/__tests__/contentContainer.test.ts +43 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
- package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
- package/src/__tests__/dataTable.test.ts +25 -15
- package/src/__tests__/diagramBlock.test.ts +3 -3
- package/src/__tests__/diagramText.test.ts +26 -0
- package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
- package/src/__tests__/drawingBlock.test.ts +3 -3
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
- package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
- package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
- package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
- package/src/__tests__/getLayers.test.ts +32 -18
- package/src/__tests__/getLayersFallback.test.ts +8 -8
- package/src/__tests__/imageEditVersions.test.ts +2 -0
- package/src/__tests__/jsonForm.pointer.test.ts +47 -1
- package/src/__tests__/layoutBlock.test.ts +3 -3
- package/src/__tests__/majorApiTypes.test.ts +18 -0
- package/src/__tests__/markdown.test.ts +54 -4
- package/src/__tests__/markdownSanitize.test.ts +7 -5
- package/src/__tests__/matchFontFamily.test.ts +114 -0
- package/src/__tests__/materializeBlockLayers.test.ts +135 -0
- package/src/__tests__/materializeTestUtils.ts +17 -0
- package/src/__tests__/narrationAlign.test.ts +150 -0
- package/src/__tests__/narrationFeatures.test.ts +71 -0
- package/src/__tests__/narrationNuclei.test.ts +45 -0
- package/src/__tests__/narrationPacing.test.ts +189 -0
- package/src/__tests__/narrationScript.test.ts +149 -0
- package/src/__tests__/narrationSessionRepro.test.ts +62 -0
- package/src/__tests__/narrationSidecar.test.ts +88 -0
- package/src/__tests__/narrationSyllables.test.ts +65 -0
- package/src/__tests__/narrationTestSignals.ts +167 -0
- package/src/__tests__/narrationVad.test.ts +66 -0
- package/src/__tests__/resolveDocTheme.test.ts +16 -2
- package/src/__tests__/rootExports.test.ts +18 -0
- package/src/__tests__/scopedContentContainer.test.ts +20 -0
- package/src/__tests__/shapeGeometry.test.ts +12 -0
- package/src/__tests__/templateAnnotationParse.test.ts +10 -0
- package/src/__tests__/templateInputs.test.ts +69 -0
- package/src/__tests__/templateMetadata.test.ts +2 -1
- package/src/__tests__/templates.test.ts +143 -17
- package/src/__tests__/themeCompile.test.ts +49 -22
- package/src/__tests__/transformProvenance.test.ts +160 -0
- package/src/__tests__/transformV2.test.ts +40 -14
- package/src/__tests__/treeviewBattle.test.ts +76 -0
- package/src/__tests__/treeviewDetect.test.ts +168 -0
- package/src/__tests__/treeviewParse.test.ts +118 -0
- package/src/__tests__/treeviewPipeline.test.ts +183 -0
- package/src/__tests__/treeviewRender.test.ts +91 -0
- package/src/__tests__/versions.test.ts +44 -0
- package/src/doc/applyNarrationTiming.ts +244 -0
- package/src/doc/asciiDiagram/chars.ts +210 -0
- package/src/doc/asciiDiagram/detect.ts +175 -0
- package/src/doc/asciiDiagram/index.ts +38 -0
- package/src/doc/asciiDiagram/mapping.ts +225 -0
- package/src/doc/asciiDiagram/parse.ts +1095 -0
- package/src/doc/asciiDiagram/render.ts +963 -0
- package/src/doc/asciiDiagram/repair.ts +400 -0
- package/src/doc/asciiDiagram/types.ts +65 -0
- package/src/doc/asciiTimeline/detect.ts +138 -0
- package/src/doc/asciiTimeline/index.ts +26 -0
- package/src/doc/asciiTimeline/mapping.ts +43 -0
- package/src/doc/asciiTimeline/parse.ts +870 -0
- package/src/doc/asciiTimeline/render.ts +427 -0
- package/src/doc/asciiTimeline/types.ts +73 -0
- package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
- package/src/doc/audioMapping.ts +60 -46
- package/src/doc/customTemplatesFrontmatter.ts +0 -41
- package/src/doc/docToMarkdown.ts +70 -2
- package/src/doc/index.ts +76 -4
- package/src/doc/markdownToDoc.ts +138 -2
- package/src/doc/materializeBlockLayers.ts +304 -0
- package/src/doc/mediaAnnotations.ts +20 -0
- package/src/doc/resolveDocTheme.ts +8 -7
- package/src/doc/templateInputs.ts +185 -2
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
- package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
- package/src/doc/templates/diagramBlock.ts +237 -38
- package/src/doc/templates/fallbackBlock.ts +1 -1
- package/src/doc/templates/index.ts +134 -279
- package/src/doc/templates/inputDescriptors.ts +20 -1
- package/src/doc/templates/listBlock.ts +88 -30
- package/src/doc/templates/metadata.ts +10 -0
- package/src/doc/templates/persistentLayers.ts +12 -126
- package/src/doc/templates/registry.ts +88 -0
- package/src/doc/templates/templateNames.ts +22 -0
- package/src/doc/templates/timelineBlock.ts +613 -0
- package/src/doc/templates/treeBlock.ts +131 -0
- package/src/doc/treeview/chars.ts +72 -0
- package/src/doc/treeview/detect.ts +145 -0
- package/src/doc/treeview/index.ts +30 -0
- package/src/doc/treeview/mapping.ts +95 -0
- package/src/doc/treeview/parse.ts +201 -0
- package/src/doc/treeview/render.ts +41 -0
- package/src/doc/treeview/types.ts +43 -0
- package/src/doc/utils/applyRenderStyle.ts +2 -9
- package/src/doc/utils/diagramText.ts +78 -0
- package/src/doc/utils/shapeGeometry.ts +67 -5
- package/src/doc/validate.ts +1 -1
- package/src/generate/templateMapper.ts +7 -0
- package/src/imageEdit/export.ts +6 -5
- package/src/imageEdit/versionPaths.ts +13 -26
- package/src/imageEdit/versions.ts +21 -100
- package/src/index.ts +1 -0
- package/src/internal/immutable.ts +58 -0
- package/src/jsonForm/index.ts +9 -1
- package/src/jsonForm/pointer.ts +68 -23
- package/src/markdown/attrTokens.ts +7 -5
- package/src/markdown/index.ts +1 -0
- package/src/markdown/stringify.ts +4 -9
- package/src/markdown/utils.ts +36 -14
- package/src/narration/align.ts +514 -0
- package/src/narration/features.ts +231 -0
- package/src/narration/index.ts +86 -0
- package/src/narration/nuclei.ts +118 -0
- package/src/narration/pacing.ts +211 -0
- package/src/narration/script.ts +239 -0
- package/src/narration/session.ts +100 -0
- package/src/narration/sidecar.ts +187 -0
- package/src/narration/syllables.ts +85 -0
- package/src/narration/trace.ts +50 -0
- package/src/narration/types.ts +299 -0
- package/src/narration/vad.ts +131 -0
- package/src/recommend/numberHighlight.ts +25 -0
- package/src/recommend/templates.ts +108 -12
- package/src/schemas/BlockTemplates.ts +156 -7
- package/src/schemas/Doc.ts +72 -9
- package/src/schemas/Media.ts +12 -2
- package/src/schemas/Theme.ts +45 -25
- package/src/schemas/Viewport.ts +2 -2
- package/src/schemas/fontStacks.ts +79 -0
- package/src/schemas/themeCompile.ts +15 -0
- package/src/schemas/themeConstants.ts +2 -0
- package/src/schemas/themeLibrary.ts +25 -19
- package/src/schemas/themeValidator.ts +1 -1
- package/src/schemas/themes/gezellig.json +1 -1
- package/src/spatial/Geohash.ts +0 -3
- package/src/storage/ContentContainer.ts +17 -9
- package/src/storage/ScopedContentContainer.ts +45 -15
- package/src/transform/applyTransform.ts +5 -5
- package/src/transform/index.ts +3 -1
- package/src/transform/registry.ts +169 -49
- package/src/transform/templateSelector.ts +21 -2
- package/src/transform/timingAllocator.ts +152 -21
- package/src/transform/types.ts +14 -1
- package/src/versions/gitignore.ts +34 -0
- package/src/versions/index.ts +2 -0
- package/src/versions/operations.ts +18 -104
- package/src/versions/paths.ts +13 -28
- package/src/versions/snapshotStore.ts +202 -0
- package/src/versions/types.ts +3 -1
- package/dist/chunk-5TYS5V3G.js.map +0 -1
- package/dist/chunk-A4O7GIWE.js.map +0 -1
- package/dist/chunk-BEQODJWV.js.map +0 -1
- package/dist/chunk-DLXZMS5K.js.map +0 -1
- package/dist/chunk-GPLTCSXO.js.map +0 -1
- package/dist/chunk-HWVFJAAH.js.map +0 -1
- package/dist/chunk-IMSCRLLV.js.map +0 -1
- package/dist/chunk-JELIRVDP.js +0 -24
- package/dist/chunk-JELIRVDP.js.map +0 -1
- package/dist/chunk-KKNUBQ6Y.js.map +0 -1
- package/dist/chunk-LDQ2HJIX.js.map +0 -1
- package/dist/chunk-LTN6I7QW.js.map +0 -1
- package/dist/chunk-P3K7NLHF.js.map +0 -1
- package/dist/chunk-T5UK6YOB.js.map +0 -1
- package/dist/chunk-UY7KGQ5R.js +0 -192
- package/dist/chunk-UY7KGQ5R.js.map +0 -1
- package/dist/chunk-VLJJHDAC.js +0 -218
- package/dist/chunk-VLJJHDAC.js.map +0 -1
- package/dist/chunk-VWUFZ6ZG.js.map +0 -1
- package/dist/story/index.d.ts +0 -1801
- package/dist/story/index.js +0 -279
- package/src/doc/getLayers.ts +0 -195
- /package/dist/{story → narration}/index.js.map +0 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -429,6 +429,11 @@ export interface ConnectorSnapPoint {
|
|
|
429
429
|
y: number;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
export interface ConnectorAnchor {
|
|
433
|
+
side: 'top' | 'right' | 'bottom' | 'left';
|
|
434
|
+
offset: number;
|
|
435
|
+
}
|
|
436
|
+
|
|
432
437
|
/** Intersection of the center-to-center line with `from`'s bounding box. */
|
|
433
438
|
export function clipPoint(from: ClipBox, to: ClipBox): { x: number; y: number } {
|
|
434
439
|
const dx = to.cx - from.cx;
|
|
@@ -466,6 +471,25 @@ export function snapPoints(box: ClipBox): ConnectorSnapPoint[] {
|
|
|
466
471
|
];
|
|
467
472
|
}
|
|
468
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
|
+
|
|
469
493
|
/** Nearest stable connector port on `box` to an arbitrary point. */
|
|
470
494
|
export function nearestSnapPoint(
|
|
471
495
|
box: ClipBox,
|
|
@@ -521,20 +545,58 @@ function portTieBreak(port: ConnectorPort): number {
|
|
|
521
545
|
/** Build the SVG `d` for a connector between two clipped endpoints. */
|
|
522
546
|
export function connectorPath(
|
|
523
547
|
routing: ConnectorRouting,
|
|
524
|
-
start: { x: number; y: number },
|
|
525
|
-
end: { x: number; y: number },
|
|
548
|
+
start: { x: number; y: number; port?: ConnectorPort },
|
|
549
|
+
end: { x: number; y: number; port?: ConnectorPort },
|
|
526
550
|
): string {
|
|
527
551
|
if (routing === 'straight') {
|
|
528
552
|
return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
|
|
529
553
|
}
|
|
530
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
|
+
}
|
|
531
567
|
const midX = (start.x + end.x) / 2;
|
|
532
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)}`;
|
|
533
569
|
}
|
|
534
570
|
// curved
|
|
535
|
-
const
|
|
536
|
-
const cp = Math.max(40,
|
|
537
|
-
|
|
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
|
+
}
|
|
538
600
|
}
|
|
539
601
|
|
|
540
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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clone JSON-shaped runtime configuration without relying on structuredClone
|
|
3
|
+
* (which is unavailable in some of the ES2020/browser runtimes core supports).
|
|
4
|
+
* Data objects are recreated with safe own-property definitions so even a key
|
|
5
|
+
* named `__proto__` remains ordinary data.
|
|
6
|
+
*/
|
|
7
|
+
export function cloneData<T>(value: T, seen: WeakMap<object, unknown> = new WeakMap()): T {
|
|
8
|
+
if (value === null || typeof value !== 'object') return value;
|
|
9
|
+
|
|
10
|
+
const existing = seen.get(value);
|
|
11
|
+
if (existing !== undefined) return existing as T;
|
|
12
|
+
|
|
13
|
+
const tag = Object.prototype.toString.call(value);
|
|
14
|
+
if (tag === '[object ArrayBuffer]') {
|
|
15
|
+
const source = new Uint8Array(value as unknown as ArrayBuffer);
|
|
16
|
+
const copy = new Uint8Array(source.byteLength);
|
|
17
|
+
copy.set(source);
|
|
18
|
+
return copy.buffer as T;
|
|
19
|
+
}
|
|
20
|
+
if (tag === '[object Date]') {
|
|
21
|
+
return new Date(Date.prototype.getTime.call(value)) as T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
const copy: unknown[] = [];
|
|
26
|
+
seen.set(value, copy);
|
|
27
|
+
for (const item of value) copy.push(cloneData(item, seen));
|
|
28
|
+
return copy as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const source = value as Record<string, unknown>;
|
|
32
|
+
const copy: Record<string, unknown> = {};
|
|
33
|
+
seen.set(value, copy);
|
|
34
|
+
for (const key of Object.keys(source)) {
|
|
35
|
+
Object.defineProperty(copy, key, {
|
|
36
|
+
value: cloneData(source[key], seen),
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return copy as T;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Recursively freeze a data graph. Cycles are supported. */
|
|
46
|
+
export function deepFreezeData<T>(value: T, seen: WeakSet<object> = new WeakSet()): T {
|
|
47
|
+
if (value === null || typeof value !== 'object' || seen.has(value)) return value;
|
|
48
|
+
seen.add(value);
|
|
49
|
+
for (const key of Object.keys(value)) {
|
|
50
|
+
deepFreezeData((value as Record<string, unknown>)[key], seen);
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Take an immutable snapshot suitable for storing in a process registry. */
|
|
56
|
+
export function cloneAndFreezeData<T>(value: T): T {
|
|
57
|
+
return deepFreezeData(cloneData(value));
|
|
58
|
+
}
|
package/src/jsonForm/index.ts
CHANGED
|
@@ -17,7 +17,15 @@ export type {
|
|
|
17
17
|
|
|
18
18
|
export { chooseControl, primaryType, arrayItemKind } from './chooseControl.js';
|
|
19
19
|
export { evaluateWhen, resolveFlag } from './evaluateWhen.js';
|
|
20
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
escapePointerSegment,
|
|
22
|
+
appendPointer,
|
|
23
|
+
toPointer,
|
|
24
|
+
pointerSegments,
|
|
25
|
+
getByPointer,
|
|
26
|
+
setByPointer,
|
|
27
|
+
resolveRef,
|
|
28
|
+
} from './pointer.js';
|
|
21
29
|
export { inferSchema } from './inferSchema.js';
|
|
22
30
|
export type { InferSchemaOptions } from './inferSchema.js';
|
|
23
31
|
export { buildJsonFormTokens, resolveJsonFormTheme } from './tokens.js';
|