canopycms 0.0.67 → 0.0.68-int.101
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 +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print a reconciled YAML document by editing the source text it was parsed from, so the bytes
|
|
3
|
+
* the reconciler did not change stay as the author wrote them.
|
|
4
|
+
*
|
|
5
|
+
* `Document.toString()` re-emits every scalar from its VALUE: folded and long plain scalars are
|
|
6
|
+
* re-wrapped at the library's line width, so the first CMS save of a hand-written file rewrites
|
|
7
|
+
* lines nobody edited. Here a clean subtree is copied verbatim from the source, and only a slot
|
|
8
|
+
* the reconciler touched is re-rendered, at the column it already occupies:
|
|
9
|
+
*
|
|
10
|
+
* - **Map**: the reconciler keeps retained pairs in source order and appends new ones, so a map
|
|
11
|
+
* becomes a list of local edits — delete a dropped pair's lines, re-render a pair whose value
|
|
12
|
+
* was replaced, recurse into a value edited in place, insert new pairs after the last item.
|
|
13
|
+
* - **Sequence**: items can be reordered, so the item region is rebuilt as a concatenation of
|
|
14
|
+
* per-item chunks, each one either an original item's source (recursively patched) or a freshly
|
|
15
|
+
* rendered item.
|
|
16
|
+
*
|
|
17
|
+
* A chunk is an item's lines plus the comment and blank lines above it, which is where `yaml`
|
|
18
|
+
* attaches those comments (the item's `commentBefore`), so a chunk moves and dies with exactly
|
|
19
|
+
* the comments the reconciler moves and drops. Above a nested collection's FIRST item they
|
|
20
|
+
* belong to the collection instead, and stay put; above the root map's first key they are the
|
|
21
|
+
* key's (`ownLeadingLines`).
|
|
22
|
+
*
|
|
23
|
+
* Anything this does not handle returns `undefined` from that collection, and the parent
|
|
24
|
+
* re-renders the whole slot instead; only at the root does that reach the caller, which falls
|
|
25
|
+
* back to the reconciled document as `yaml` prints it. Every splice is also re-parsed and
|
|
26
|
+
* checked ({@link printsAs}) before it is returned, so a wrong splice costs formatting, never
|
|
27
|
+
* data.
|
|
28
|
+
*/
|
|
29
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
30
|
+
import { Document, parseDocument, isAlias, isCollection, isMap, isNode, isPair, isScalar, isSeq, YAMLMap, visit, YAMLSeq, } from 'yaml';
|
|
31
|
+
/** Successor markers: the end of a collection, and an item with no source counterpart. */
|
|
32
|
+
const END = Symbol('end');
|
|
33
|
+
const FRESH = Symbol('fresh');
|
|
34
|
+
/** `yaml`'s default `lineWidth`, which `Document.toString()` folds at. */
|
|
35
|
+
const LINE_WIDTH = 80;
|
|
36
|
+
/** Narrowest fold width a deeply indented fragment is rendered at. */
|
|
37
|
+
const MIN_LINE_WIDTH = 40;
|
|
38
|
+
export function snapshotDocument(doc) {
|
|
39
|
+
const items = new Map();
|
|
40
|
+
const visitNode = (node) => {
|
|
41
|
+
if (isMap(node)) {
|
|
42
|
+
items.set(node, node.items.map((pair) => ({ item: pair, value: pair.value })));
|
|
43
|
+
for (const pair of node.items)
|
|
44
|
+
visitNode(pair.value);
|
|
45
|
+
}
|
|
46
|
+
else if (isSeq(node)) {
|
|
47
|
+
items.set(node, node.items.map((item) => ({ item, value: item })));
|
|
48
|
+
for (const item of node.items)
|
|
49
|
+
visitNode(item);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
visitNode(doc.contents);
|
|
53
|
+
let anchored = false;
|
|
54
|
+
visit(doc, (_key, node) => {
|
|
55
|
+
if (isAlias(node) || (isNode(node) && node.anchor !== undefined)) {
|
|
56
|
+
anchored = true;
|
|
57
|
+
return visit.BREAK;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
});
|
|
61
|
+
return { items, anchored };
|
|
62
|
+
}
|
|
63
|
+
class SourceSplicer {
|
|
64
|
+
constructor(raw, eol, doc, snapshot, replaced) {
|
|
65
|
+
this.raw = raw;
|
|
66
|
+
this.eol = eol;
|
|
67
|
+
this.doc = doc;
|
|
68
|
+
this.snapshot = snapshot;
|
|
69
|
+
this.replaced = replaced;
|
|
70
|
+
this.dirtyMemo = new Map();
|
|
71
|
+
}
|
|
72
|
+
print() {
|
|
73
|
+
const root = this.doc.contents;
|
|
74
|
+
if (!isCollection(root) || !this.snapshot.items.has(root))
|
|
75
|
+
return undefined;
|
|
76
|
+
if (!this.isDirty(root))
|
|
77
|
+
return this.raw;
|
|
78
|
+
if (this.snapshot.anchored)
|
|
79
|
+
return undefined;
|
|
80
|
+
const edits = this.collectionEdits(root);
|
|
81
|
+
return edits === undefined ? undefined : this.applyEdits(0, this.raw.length, edits);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Did the reconciler change anything under this node? An original scalar is never mutated in
|
|
85
|
+
* place (a changed value gets a fresh node), so only collections can be dirty.
|
|
86
|
+
*/
|
|
87
|
+
isDirty(node) {
|
|
88
|
+
if (!isCollection(node))
|
|
89
|
+
return false;
|
|
90
|
+
const memo = this.dirtyMemo.get(node);
|
|
91
|
+
if (memo !== undefined)
|
|
92
|
+
return memo;
|
|
93
|
+
const snap = this.snapshot.items.get(node);
|
|
94
|
+
let dirty = snap === undefined || snap.length !== node.items.length;
|
|
95
|
+
for (let i = 0; !dirty && snap !== undefined && i < snap.length; i++) {
|
|
96
|
+
const now = node.items[i];
|
|
97
|
+
const before = snap[i];
|
|
98
|
+
if (now !== before.item)
|
|
99
|
+
dirty = true;
|
|
100
|
+
else if (isPair(now))
|
|
101
|
+
dirty = now.value !== before.value || this.isDirty(now.value);
|
|
102
|
+
else
|
|
103
|
+
dirty = this.isDirty(now);
|
|
104
|
+
}
|
|
105
|
+
this.dirtyMemo.set(node, dirty);
|
|
106
|
+
return dirty;
|
|
107
|
+
}
|
|
108
|
+
collectionEdits(node) {
|
|
109
|
+
if (node.flow)
|
|
110
|
+
return undefined;
|
|
111
|
+
const snap = this.snapshot.items.get(node);
|
|
112
|
+
// An emptied block collection has no block spelling (`key:` alone reads back as null).
|
|
113
|
+
if (snap === undefined || snap.length === 0 || node.items.length === 0)
|
|
114
|
+
return undefined;
|
|
115
|
+
const geometry = [];
|
|
116
|
+
for (const { item, value } of snap) {
|
|
117
|
+
const g = isPair(item) ? this.pairGeometry(item, value) : this.seqItemGeometry(item);
|
|
118
|
+
if (g === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
geometry.push(g);
|
|
121
|
+
}
|
|
122
|
+
if (!this.isBlockLayout(geometry))
|
|
123
|
+
return undefined;
|
|
124
|
+
return isMap(node) ? this.mapEdits(node, snap, geometry) : this.seqEdits(node, snap, geometry);
|
|
125
|
+
}
|
|
126
|
+
mapEdits(map, snap, geometry) {
|
|
127
|
+
const col = this.column(geometry[0].content);
|
|
128
|
+
const original = new Set(snap.map((s) => s.item));
|
|
129
|
+
const kept = new Set(map.items);
|
|
130
|
+
const edits = [];
|
|
131
|
+
// Retained pairs must still be in source order, followed only by new pairs.
|
|
132
|
+
const retainedOrder = map.items.filter((pair) => original.has(pair));
|
|
133
|
+
const expectedOrder = snap.map((s) => s.item).filter((pair) => kept.has(pair));
|
|
134
|
+
if (retainedOrder.some((pair, i) => pair !== expectedOrder[i]))
|
|
135
|
+
return undefined;
|
|
136
|
+
const added = map.items.slice(retainedOrder.length);
|
|
137
|
+
if (added.some((pair) => original.has(pair)))
|
|
138
|
+
return undefined;
|
|
139
|
+
const layout = [...geometry];
|
|
140
|
+
for (let i = 0; i < snap.length; i++) {
|
|
141
|
+
const { item, value: before } = snap[i];
|
|
142
|
+
const g = layout[i];
|
|
143
|
+
if (!isPair(item))
|
|
144
|
+
return undefined;
|
|
145
|
+
if (!kept.has(item)) {
|
|
146
|
+
if (g.inline) {
|
|
147
|
+
const next = this.promoteToInline(snap, layout, kept);
|
|
148
|
+
if (next === undefined)
|
|
149
|
+
return undefined;
|
|
150
|
+
edits.push({ start: g.content, end: layout[next].content, text: '' });
|
|
151
|
+
i = next - 1;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const start = i === 0 ? this.ownLeadingLines(map, item, g.lineStart) : this.chunkStart(layout, i);
|
|
155
|
+
edits.push({ start, end: g.end, text: '' });
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const now = item.value;
|
|
160
|
+
const clean = now === before && !this.isDirty(now);
|
|
161
|
+
const inner = clean
|
|
162
|
+
? []
|
|
163
|
+
: now === before && isCollection(now)
|
|
164
|
+
? this.collectionEdits(now)
|
|
165
|
+
: undefined;
|
|
166
|
+
if (inner !== undefined) {
|
|
167
|
+
const next = map.items[map.items.indexOf(item) + 1] ?? END;
|
|
168
|
+
if ((i + 1 < snap.length ? snap[i + 1].item : END) !== next) {
|
|
169
|
+
edits.push(...uncovered(this.outdentedCommentEdits(g, col), inner));
|
|
170
|
+
}
|
|
171
|
+
edits.push(...inner);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// The key is never replaced, so the comment lines above it stay in the source.
|
|
175
|
+
const text = withoutLeadingComment(item.key, () => this.render(mapOf([item]), col, g.inline));
|
|
176
|
+
edits.push({ start: g.lineStart, end: g.end, text: this.matchFinalNewline(g.end, text) });
|
|
177
|
+
}
|
|
178
|
+
if (added.length > 0) {
|
|
179
|
+
const at = layout[layout.length - 1].end;
|
|
180
|
+
const text = this.render(mapOf(added), col, false);
|
|
181
|
+
edits.push({ start: at, end: at, text: this.raw[at - 1] === '\n' ? text : this.eol + text });
|
|
182
|
+
}
|
|
183
|
+
return edits;
|
|
184
|
+
}
|
|
185
|
+
seqEdits(seq, snap, geometry) {
|
|
186
|
+
if (geometry[0].inline)
|
|
187
|
+
return undefined;
|
|
188
|
+
const col = this.column(geometry[0].content);
|
|
189
|
+
const oldIndex = new Map(snap.map((s, i) => [s.item, i]));
|
|
190
|
+
const parts = [];
|
|
191
|
+
const sourceOf = (item) => oldIndex.has(item) ? item : isNode(item) ? this.replaced.get(item) : undefined;
|
|
192
|
+
for (const [k, item] of seq.items.entries()) {
|
|
193
|
+
const j = oldIndex.get(item);
|
|
194
|
+
let part;
|
|
195
|
+
if (j !== undefined) {
|
|
196
|
+
const start = this.chunkStart(geometry, j);
|
|
197
|
+
const inner = isCollection(item) && this.isDirty(item) ? this.collectionEdits(item) : [];
|
|
198
|
+
const next = k + 1 < seq.items.length ? sourceOf(seq.items[k + 1]) : END;
|
|
199
|
+
const moved = (j + 1 < snap.length ? snap[j + 1].item : END) !== (next ?? FRESH);
|
|
200
|
+
part =
|
|
201
|
+
inner !== undefined
|
|
202
|
+
? this.applyEdits(start, geometry[j].end, [
|
|
203
|
+
...inner,
|
|
204
|
+
...(moved ? uncovered(this.outdentedCommentEdits(geometry[j], col), inner) : []),
|
|
205
|
+
])
|
|
206
|
+
: this.raw.slice(start, geometry[j].lineStart) +
|
|
207
|
+
withoutLeadingComment(item, () => this.render(seqOf(item), col, false));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// A fresh node keeps the comment lines above its slot only if the reconciler carried
|
|
211
|
+
// them across; it does not off a replaced collection.
|
|
212
|
+
const old = isNode(item) ? this.replaced.get(item) : undefined;
|
|
213
|
+
const k = old === undefined ? undefined : oldIndex.get(old);
|
|
214
|
+
part =
|
|
215
|
+
k !== undefined && sameLeadingComment(old, item)
|
|
216
|
+
? this.raw.slice(this.chunkStart(geometry, k), geometry[k].lineStart) +
|
|
217
|
+
withoutLeadingComment(item, () => this.render(seqOf(item), col, false))
|
|
218
|
+
: this.render(seqOf(item), col, false);
|
|
219
|
+
}
|
|
220
|
+
parts.push(part.endsWith('\n') ? part : part + this.eol);
|
|
221
|
+
}
|
|
222
|
+
const start = geometry[0].lineStart;
|
|
223
|
+
const end = geometry[geometry.length - 1].end;
|
|
224
|
+
return [{ start, end, text: this.matchFinalNewline(end, parts.join('')) }];
|
|
225
|
+
}
|
|
226
|
+
/** Rendered text always ends in a line break; drop it where the source it replaces had none. */
|
|
227
|
+
matchFinalNewline(end, text) {
|
|
228
|
+
return this.raw[end - 1] !== '\n' && text.endsWith(this.eol)
|
|
229
|
+
? text.slice(0, -this.eol.length)
|
|
230
|
+
: text;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The first pair shares the parent's `- ` line and was dropped: move the first retained pair up
|
|
234
|
+
* onto that line, so the pairs before it go and the rest of the map stays as written. Returns
|
|
235
|
+
* that pair's index, or undefined when no pair is retained or comment lines above it would have
|
|
236
|
+
* to go with the lines being removed.
|
|
237
|
+
*/
|
|
238
|
+
promoteToInline(snap, layout, kept) {
|
|
239
|
+
const next = snap.findIndex((s) => kept.has(s.item));
|
|
240
|
+
if (next <= 0)
|
|
241
|
+
return undefined;
|
|
242
|
+
if (this.raw.slice(this.chunkStart(layout, next), layout[next].lineStart).includes('#')) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
const g = layout[next];
|
|
246
|
+
layout[next] = { ...g, inline: true, lineStart: g.content };
|
|
247
|
+
return next;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Where a dropped FIRST pair's lines begin. The comment lines above a nested collection's first
|
|
251
|
+
* item are the collection's and stay, but above the root map's first key `yaml` gives them to
|
|
252
|
+
* the key, so the reconciler drops them with it and so must the splice.
|
|
253
|
+
*/
|
|
254
|
+
ownLeadingLines(map, pair, start) {
|
|
255
|
+
const key = pair.key;
|
|
256
|
+
if (map !== this.doc.contents || !key.commentBefore || this.doc.commentBefore)
|
|
257
|
+
return start;
|
|
258
|
+
let at = start;
|
|
259
|
+
while (at > 0) {
|
|
260
|
+
const previous = this.lineStart(at - 1);
|
|
261
|
+
if (previous >= at)
|
|
262
|
+
break;
|
|
263
|
+
if (!/^[ \t]*(#.*)?\r?\n?$/.test(this.raw.slice(previous, at)))
|
|
264
|
+
break;
|
|
265
|
+
at = previous;
|
|
266
|
+
}
|
|
267
|
+
return at;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* The run of comment lines that ends an item's region reads to a person as being about what
|
|
271
|
+
* comes next, but it is the item's: `yaml`'s parser (`pop()`) keeps a trailing run with the
|
|
272
|
+
* nested collection it ends when that collection is not at column 0 and any line of the run is
|
|
273
|
+
* at or right of its indent. While the neighbour below is unchanged that is harmless; once it
|
|
274
|
+
* changes, indent the run's shallower lines to its deepest comment, so they read as the item's
|
|
275
|
+
* and do not head content they were never about. Where that lands a line in a deeper scalar's comment or content
|
|
276
|
+
* instead, {@link printsAs} refuses the splice. A run at or left of the item's own column is a
|
|
277
|
+
* zero-indented list's tail, and is left alone.
|
|
278
|
+
*/
|
|
279
|
+
outdentedCommentEdits(g, col) {
|
|
280
|
+
let run = [];
|
|
281
|
+
let at = this.raw.indexOf('\n', g.content) + 1;
|
|
282
|
+
while (at > 0 && at < g.end) {
|
|
283
|
+
const lineEnd = this.raw.indexOf('\n', at);
|
|
284
|
+
const line = this.raw.slice(at, lineEnd === -1 ? g.end : lineEnd);
|
|
285
|
+
const indent = line.length - line.trimStart().length;
|
|
286
|
+
if (line.trimStart().startsWith('#'))
|
|
287
|
+
run.push({ start: at, indent });
|
|
288
|
+
else if (line.trim() !== '')
|
|
289
|
+
run = [];
|
|
290
|
+
at = lineEnd === -1 ? g.end : lineEnd + 1;
|
|
291
|
+
}
|
|
292
|
+
const pin = Math.max(-1, ...run.map((line) => line.indent));
|
|
293
|
+
if (pin <= col)
|
|
294
|
+
return [];
|
|
295
|
+
return run
|
|
296
|
+
.filter((line) => line.indent < pin)
|
|
297
|
+
.map((line) => ({ start: line.start, end: line.start + line.indent, text: ' '.repeat(pin) }));
|
|
298
|
+
}
|
|
299
|
+
/** Where item `i`'s chunk begins: its own line for the first item, else the previous item's end. */
|
|
300
|
+
chunkStart(geometry, i) {
|
|
301
|
+
return i === 0 ? geometry[0].lineStart : geometry[i - 1].end;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Every item starts at the same column, in order, and only the first may share its line with
|
|
305
|
+
* the parent's `- ` (a compact `- key: value`). Anything else — an explicit `? key`, a
|
|
306
|
+
* multi-line key, a node property on its own line — is not a layout this splices.
|
|
307
|
+
*/
|
|
308
|
+
isBlockLayout(geometry) {
|
|
309
|
+
const col = this.column(geometry[0].content);
|
|
310
|
+
let previousEnd = -1;
|
|
311
|
+
for (const [i, g] of geometry.entries()) {
|
|
312
|
+
if (this.column(g.content) !== col)
|
|
313
|
+
return false;
|
|
314
|
+
if (i > 0 && g.inline)
|
|
315
|
+
return false;
|
|
316
|
+
if (g.lineStart < previousEnd || g.end <= g.content)
|
|
317
|
+
return false;
|
|
318
|
+
previousEnd = g.end;
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
/** `value` is the pair's value as parsed: the reconciler may already have replaced it. */
|
|
323
|
+
pairGeometry(pair, value) {
|
|
324
|
+
const key = pair.key;
|
|
325
|
+
if (!isScalar(key) || key.range == null)
|
|
326
|
+
return undefined;
|
|
327
|
+
const content = key.range[0];
|
|
328
|
+
const range = isNode(value) && value.range != null ? value.range : key.range;
|
|
329
|
+
return this.geometry(content, range[1], range[2]);
|
|
330
|
+
}
|
|
331
|
+
seqItemGeometry(item) {
|
|
332
|
+
if (!isNode(item) || item.range == null)
|
|
333
|
+
return undefined;
|
|
334
|
+
let dash = item.range[0] - 1;
|
|
335
|
+
while (dash >= 0 && ' \t\r\n'.includes(this.raw[dash]))
|
|
336
|
+
dash--;
|
|
337
|
+
if (dash < 0 || this.raw[dash] !== '-')
|
|
338
|
+
return undefined;
|
|
339
|
+
return this.geometry(dash, item.range[1], item.range[2]);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Trailing blank lines are not the item's — `yaml` gives them to whatever follows — unless they
|
|
343
|
+
* are inside the value itself, as a keep-chomped (`|+`) block scalar's are. A node that ends in
|
|
344
|
+
* a comment can end inside the NEXT line's indentation; that line is not the item's either.
|
|
345
|
+
*/
|
|
346
|
+
geometry(content, valueEnd, nodeEnd) {
|
|
347
|
+
const nodeEndLine = this.lineStart(nodeEnd);
|
|
348
|
+
if (nodeEndLine > content && /^[ \t]*$/.test(this.raw.slice(nodeEndLine, nodeEnd))) {
|
|
349
|
+
nodeEnd = nodeEndLine;
|
|
350
|
+
valueEnd = Math.min(valueEnd, nodeEnd);
|
|
351
|
+
}
|
|
352
|
+
let last = Math.max(content, nodeEnd - 1);
|
|
353
|
+
while (last > content && last >= valueEnd && ' \t\r\n'.includes(this.raw[last]))
|
|
354
|
+
last--;
|
|
355
|
+
const newline = this.raw.indexOf('\n', last);
|
|
356
|
+
const lineStart = this.lineStart(content);
|
|
357
|
+
const onOwnLine = /^ *$/.test(this.raw.slice(lineStart, content));
|
|
358
|
+
return {
|
|
359
|
+
content,
|
|
360
|
+
inline: !onOwnLine,
|
|
361
|
+
lineStart: onOwnLine ? lineStart : content,
|
|
362
|
+
end: newline === -1 ? this.raw.length : newline + 1,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
lineStart(offset) {
|
|
366
|
+
// `lastIndexOf` clamps a negative index to 0, which would find a newline AT offset 0.
|
|
367
|
+
return offset <= 0 ? 0 : this.raw.lastIndexOf('\n', offset - 1) + 1;
|
|
368
|
+
}
|
|
369
|
+
column(offset) {
|
|
370
|
+
return offset - this.lineStart(offset);
|
|
371
|
+
}
|
|
372
|
+
applyEdits(start, end, edits) {
|
|
373
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
374
|
+
let out = '';
|
|
375
|
+
let cursor = start;
|
|
376
|
+
for (const edit of sorted) {
|
|
377
|
+
if (edit.start < cursor || edit.end > end)
|
|
378
|
+
throw new Error('overlapping source edits');
|
|
379
|
+
out += this.raw.slice(cursor, edit.start) + edit.text;
|
|
380
|
+
cursor = edit.end;
|
|
381
|
+
}
|
|
382
|
+
return out + this.raw.slice(cursor, end);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Render `items` as a block collection whose items sit at column `col`. `inline` leaves the
|
|
386
|
+
* first line unindented, for a slot that continues the parent's `- ` line. The fold width is
|
|
387
|
+
* reduced by the indent so the lines come out as wide as `toString()` would make them there.
|
|
388
|
+
*/
|
|
389
|
+
render(container, col, inline) {
|
|
390
|
+
const fragment = new Document();
|
|
391
|
+
fragment.schema = this.doc.schema;
|
|
392
|
+
fragment.contents = container;
|
|
393
|
+
// A comment parsed from a CRLF file keeps a `\r` on every line but its last; the line ending
|
|
394
|
+
// is this.eol's to write.
|
|
395
|
+
const text = fragment
|
|
396
|
+
.toString({ lineWidth: Math.max(MIN_LINE_WIDTH, LINE_WIDTH - col) })
|
|
397
|
+
.replace(/\r\n?/g, '\n');
|
|
398
|
+
const indent = ' '.repeat(col);
|
|
399
|
+
const lines = text.split('\n');
|
|
400
|
+
if (lines[lines.length - 1] === '')
|
|
401
|
+
lines.pop();
|
|
402
|
+
return lines
|
|
403
|
+
.map((line, i) => (line === '' || (i === 0 && inline) ? line : indent + line))
|
|
404
|
+
.map((line) => line + this.eol)
|
|
405
|
+
.join('');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The edits in `extra` that no edit in `edits` already rewrites. A run of comment lines inside
|
|
410
|
+
* a value that is being deleted or re-rendered goes, or is re-drawn, with that value.
|
|
411
|
+
*/
|
|
412
|
+
function uncovered(extra, edits) {
|
|
413
|
+
return extra.filter((e) => !edits.some((x) => x.end > x.start && x.start <= e.start && e.end <= x.end));
|
|
414
|
+
}
|
|
415
|
+
function mapOf(pairs) {
|
|
416
|
+
const map = new YAMLMap();
|
|
417
|
+
map.items = [...pairs];
|
|
418
|
+
return map;
|
|
419
|
+
}
|
|
420
|
+
function seqOf(item) {
|
|
421
|
+
const seq = new YAMLSeq();
|
|
422
|
+
seq.items = [item];
|
|
423
|
+
return seq;
|
|
424
|
+
}
|
|
425
|
+
function sameLeadingComment(a, b) {
|
|
426
|
+
if (!isNode(a) || !isNode(b))
|
|
427
|
+
return false;
|
|
428
|
+
const x = a;
|
|
429
|
+
const y = b;
|
|
430
|
+
return ((x.commentBefore ?? undefined) === (y.commentBefore ?? undefined) &&
|
|
431
|
+
!!x.spaceBefore === !!y.spaceBefore);
|
|
432
|
+
}
|
|
433
|
+
/** Run `fn` with `node`'s leading comment and blank line hidden, because the source keeps them. */
|
|
434
|
+
function withoutLeadingComment(node, fn) {
|
|
435
|
+
if (!isNode(node))
|
|
436
|
+
return fn();
|
|
437
|
+
const target = node;
|
|
438
|
+
const { commentBefore, spaceBefore } = target;
|
|
439
|
+
target.commentBefore = undefined;
|
|
440
|
+
target.spaceBefore = false;
|
|
441
|
+
try {
|
|
442
|
+
return fn();
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
target.commentBefore = commentBefore;
|
|
446
|
+
target.spaceBefore = spaceBefore;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The line ending a splice must write, or undefined for a file mixing them (or using bare `\r`),
|
|
451
|
+
* which no single choice would reproduce.
|
|
452
|
+
*/
|
|
453
|
+
function lineEndingOf(raw) {
|
|
454
|
+
const crlf = (raw.match(/\r\n/g) ?? []).length;
|
|
455
|
+
const lf = (raw.match(/\n/g) ?? []).length;
|
|
456
|
+
const cr = (raw.match(/\r/g) ?? []).length;
|
|
457
|
+
if (crlf === 0)
|
|
458
|
+
return cr === 0 ? '\n' : undefined;
|
|
459
|
+
return crlf === lf && crlf === cr ? '\r\n' : undefined;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* `printed` (a `toString()`) written with `raw`'s line endings: CRLF when `raw` is consistently
|
|
463
|
+
* CRLF, else LF. Either way without the stray `\r` that comments parsed from CRLF keep, so a
|
|
464
|
+
* whole-file re-print does not leave the file with mixed line endings.
|
|
465
|
+
*/
|
|
466
|
+
export function withSourceLineEndings(printed, raw) {
|
|
467
|
+
const text = printed.replace(/\r\n?/g, '\n');
|
|
468
|
+
return lineEndingOf(raw) === '\r\n' ? text.replace(/\n/g, '\r\n') : text;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* The source text of `raw` with only the reconciler's changes applied, or undefined when the
|
|
472
|
+
* document holds a construct this does not splice or the splice fails its self-check.
|
|
473
|
+
* `replaced` maps each node the reconciler created for an existing slot back to the node it
|
|
474
|
+
* replaced; `printed` is the reconciled document's `toString()`.
|
|
475
|
+
*/
|
|
476
|
+
export function spliceSource(raw, doc, snapshot, replaced, printed) {
|
|
477
|
+
const eol = lineEndingOf(raw);
|
|
478
|
+
if (eol === undefined)
|
|
479
|
+
return undefined;
|
|
480
|
+
const spliced = new SourceSplicer(raw, eol, doc, snapshot, replaced).print();
|
|
481
|
+
return spliced !== undefined && printsAs(spliced, doc, printed) ? spliced : undefined;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Does `candidate` read back as the text it replaces, `printed`, would? It must parse cleanly,
|
|
485
|
+
* hold the reconciled document's data, and print as `printed` re-parsed prints — the same keys
|
|
486
|
+
* in the same order, the same comments where `yaml` draws them, which is by the node it gives
|
|
487
|
+
* each to.
|
|
488
|
+
*
|
|
489
|
+
* Re-parsed, not `printed` itself: where `yaml` cannot write the reconciled document so that it
|
|
490
|
+
* reads back as itself — a comment following an outdented trailing comment is read as the
|
|
491
|
+
* previous item's, whoever it belonged to — the splice need only be no worse than `printed`.
|
|
492
|
+
* Wherever it can, the two are the same comparison.
|
|
493
|
+
*
|
|
494
|
+
* Blank lines and trailing whitespace are left out: they are formatting the splice copies from
|
|
495
|
+
* the source, and `yaml` prints them inconsistently (a blank line inside an indented block as the
|
|
496
|
+
* indent alone; a blank line before a list's first item doubled; a stray `\r` kept in some
|
|
497
|
+
* comments parsed from a CRLF file).
|
|
498
|
+
*/
|
|
499
|
+
function printsAs(candidate, reconciled, printed) {
|
|
500
|
+
if (candidate === printed)
|
|
501
|
+
return true;
|
|
502
|
+
const reparsed = parseDocument(candidate);
|
|
503
|
+
if (reparsed.errors.length > 0)
|
|
504
|
+
return false;
|
|
505
|
+
if (!isDeepStrictEqual(reparsed.toJS(), reconciled.toJS()))
|
|
506
|
+
return false;
|
|
507
|
+
return layoutFree(reparsed.toString()) === layoutFree(parseDocument(printed).toString());
|
|
508
|
+
}
|
|
509
|
+
function layoutFree(text) {
|
|
510
|
+
return text
|
|
511
|
+
.split('\n')
|
|
512
|
+
.map((line) => line.replace(/[ \t\r]+$/, ''))
|
|
513
|
+
.filter((line) => line !== '')
|
|
514
|
+
.join('\n');
|
|
515
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `ReferenceValidator` at the write boundary.
|
|
13
13
|
*/
|
|
14
14
|
import { fieldTypes } from '../config/index.js';
|
|
15
|
-
import { isValidCropRect } from '../assets/transform-directives.js';
|
|
15
|
+
import { isValidCropRect, roundCropRect } from '../assets/transform-directives.js';
|
|
16
16
|
import { BLOCK_STRUCTURAL_KEYS } from './block-structural-keys.js';
|
|
17
17
|
import { resolveBlockItem, traverseFields } from './field-traversal.js';
|
|
18
18
|
import { findBodyFieldName } from '../utils/body-field.js';
|
|
@@ -27,7 +27,10 @@ function isPlainRecord(value) {
|
|
|
27
27
|
function isPositiveInt(value) {
|
|
28
28
|
return typeof value === 'number' && Number.isInteger(value) && value > 0;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* True when `value` is a well-formed `{ x, y, w, h }` normalized crop rect that is still valid
|
|
32
|
+
* after `roundCropRect`, the rounding its transform URL applies.
|
|
33
|
+
*/
|
|
31
34
|
function isValidImageCropValue(value) {
|
|
32
35
|
if (!isPlainRecord(value))
|
|
33
36
|
return false;
|
|
@@ -38,7 +41,10 @@ function isValidImageCropValue(value) {
|
|
|
38
41
|
typeof h !== 'number') {
|
|
39
42
|
return false;
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
if (!isValidCropRect(x, y, w, h))
|
|
45
|
+
return false;
|
|
46
|
+
const rounded = roundCropRect({ x, y, w, h });
|
|
47
|
+
return isValidCropRect(rounded.x, rounded.y, rounded.w, rounded.h);
|
|
42
48
|
}
|
|
43
49
|
/**
|
|
44
50
|
* Validate a structured `image` field value: `{ src, alt, width?, height?, crop? }`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CANOPYCMS_VERSION = "0.0.68-int.101";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FileStatusResult, SimpleGit, StatusResult } from 'simple-git';
|
|
2
|
+
/**
|
|
3
|
+
* How the sync loop treats canopycms's own state (`.canopy-meta/`) in a branch clone. It is
|
|
4
|
+
* never content, so untracked state never makes a clone "dirty" for sync, but git refuses some
|
|
5
|
+
* operations over it while the adopter's repo tracks it. Only the adopter can stop that; once
|
|
6
|
+
* they have, the base clone follows on its next fast-forward ({@link untrackInIndex}), and a
|
|
7
|
+
* branch clone with modified tracked state needs an operator.
|
|
8
|
+
*/
|
|
9
|
+
/** The fix an operator applies when an adopter repo tracks `.canopy-meta/`. */
|
|
10
|
+
export declare const TRACKED_CANOPY_STATE_FIX = "run `git rm -r --cached .canopy-meta` in the site repo, add `.canopy-meta/` to its .gitignore, and commit";
|
|
11
|
+
/** Paths listed in worker-status.json per category; the admin panel shows them in a tooltip. */
|
|
12
|
+
export declare const MAX_REPORTED_PATHS = 10;
|
|
13
|
+
export declare function isUntracked(file: FileStatusResult): boolean;
|
|
14
|
+
/** Files under `.canopy-meta/` that the clone's index tracks. */
|
|
15
|
+
export declare function listTrackedCanopyState(git: SimpleGit): Promise<string[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Discard local changes to the schema cache's retired in-tree copy, so a clone of a repo that
|
|
18
|
+
* committed it can sync again. Nothing reads or writes that path in a clone any more, so its
|
|
19
|
+
* content is dead; but while it differs from HEAD, `git rebase` refuses to start and a
|
|
20
|
+
* fast-forward that touches it (including the adopter's own commit untracking it) refuses too.
|
|
21
|
+
* An untracked copy blocks neither and a newly staged one has nothing in HEAD to restore, so
|
|
22
|
+
* both are left alone. Returns whether it restored anything; the caller re-reads status if so.
|
|
23
|
+
*/
|
|
24
|
+
export declare function restoreRetiredSchemaCache(git: SimpleGit, status: StatusResult): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* The changed tracked `.canopy-meta/` files in `status`: what git refuses to rebase over, and
|
|
27
|
+
* what a fast-forward refuses to overwrite where it touches them.
|
|
28
|
+
*/
|
|
29
|
+
export declare function trackedCanopyStateChanges(status: StatusResult): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Split `paths` by whether the commit `tip` still tracks them. Those it no longer tracks, the
|
|
32
|
+
* adopter has untracked upstream, so a fast-forward onto `tip` would delete them from disk unless
|
|
33
|
+
* {@link untrackInIndex} takes them out of the index first.
|
|
34
|
+
*/
|
|
35
|
+
export declare function splitByUpstreamTracking(git: SimpleGit, paths: string[], tip: string): Promise<{
|
|
36
|
+
droppedUpstream: string[];
|
|
37
|
+
stillTracked: string[];
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Remove `paths` from this clone's index, leaving them on disk as untracked, excluded state.
|
|
41
|
+
* Index-only, so a concurrent write to them (branch metadata, comments) is not lost. Only for a
|
|
42
|
+
* clone with no commits of its own: a rebase that replays a commit touching these paths writes
|
|
43
|
+
* that commit's bytes over them. Paths already out of the index are ignored.
|
|
44
|
+
*/
|
|
45
|
+
export declare function untrackInIndex(git: SimpleGit, paths: string[]): Promise<void>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { SCHEMA_CACHE_FILE } from '../branch-schema-cache.js';
|
|
2
|
+
import { CANOPY_META_DIR, isCanopyInternalPath } from '../utils/git.js';
|
|
3
|
+
/**
|
|
4
|
+
* How the sync loop treats canopycms's own state (`.canopy-meta/`) in a branch clone. It is
|
|
5
|
+
* never content, so untracked state never makes a clone "dirty" for sync, but git refuses some
|
|
6
|
+
* operations over it while the adopter's repo tracks it. Only the adopter can stop that; once
|
|
7
|
+
* they have, the base clone follows on its next fast-forward ({@link untrackInIndex}), and a
|
|
8
|
+
* branch clone with modified tracked state needs an operator.
|
|
9
|
+
*/
|
|
10
|
+
/** The fix an operator applies when an adopter repo tracks `.canopy-meta/`. */
|
|
11
|
+
export const TRACKED_CANOPY_STATE_FIX = 'run `git rm -r --cached .canopy-meta` in the site repo, add `.canopy-meta/` to its .gitignore, and commit';
|
|
12
|
+
/** Paths listed in worker-status.json per category; the admin panel shows them in a tooltip. */
|
|
13
|
+
export const MAX_REPORTED_PATHS = 10;
|
|
14
|
+
/** The schema cache's location in a clone before it moved under `.git/` (branch-schema-cache.ts). */
|
|
15
|
+
const RETIRED_SCHEMA_CACHE_PATH = `${CANOPY_META_DIR}/${SCHEMA_CACHE_FILE}`;
|
|
16
|
+
export function isUntracked(file) {
|
|
17
|
+
return file.index === '?' && file.working_dir === '?';
|
|
18
|
+
}
|
|
19
|
+
/** NUL-separated git output, which is never quoted, unlike `core.quotePath` line output. */
|
|
20
|
+
function splitNul(out) {
|
|
21
|
+
return out.split('\0').filter((entry) => entry.length > 0);
|
|
22
|
+
}
|
|
23
|
+
/** Files under `.canopy-meta/` that the clone's index tracks. */
|
|
24
|
+
export async function listTrackedCanopyState(git) {
|
|
25
|
+
return splitNul(await git.raw(['ls-files', '-z', '--', CANOPY_META_DIR]));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Discard local changes to the schema cache's retired in-tree copy, so a clone of a repo that
|
|
29
|
+
* committed it can sync again. Nothing reads or writes that path in a clone any more, so its
|
|
30
|
+
* content is dead; but while it differs from HEAD, `git rebase` refuses to start and a
|
|
31
|
+
* fast-forward that touches it (including the adopter's own commit untracking it) refuses too.
|
|
32
|
+
* An untracked copy blocks neither and a newly staged one has nothing in HEAD to restore, so
|
|
33
|
+
* both are left alone. Returns whether it restored anything; the caller re-reads status if so.
|
|
34
|
+
*/
|
|
35
|
+
export async function restoreRetiredSchemaCache(git, status) {
|
|
36
|
+
const entry = status.files.find((f) => f.path === RETIRED_SCHEMA_CACHE_PATH);
|
|
37
|
+
if (!entry || entry.index === '?' || entry.index === 'A')
|
|
38
|
+
return false;
|
|
39
|
+
await git.raw(['checkout', 'HEAD', '--', RETIRED_SCHEMA_CACHE_PATH]);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The changed tracked `.canopy-meta/` files in `status`: what git refuses to rebase over, and
|
|
44
|
+
* what a fast-forward refuses to overwrite where it touches them.
|
|
45
|
+
*/
|
|
46
|
+
export function trackedCanopyStateChanges(status) {
|
|
47
|
+
return status.files
|
|
48
|
+
.filter((f) => isCanopyInternalPath(f.path) && !isUntracked(f))
|
|
49
|
+
.map((f) => f.path);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Split `paths` by whether the commit `tip` still tracks them. Those it no longer tracks, the
|
|
53
|
+
* adopter has untracked upstream, so a fast-forward onto `tip` would delete them from disk unless
|
|
54
|
+
* {@link untrackInIndex} takes them out of the index first.
|
|
55
|
+
*/
|
|
56
|
+
export async function splitByUpstreamTracking(git, paths, tip) {
|
|
57
|
+
if (paths.length === 0)
|
|
58
|
+
return { droppedUpstream: [], stillTracked: [] };
|
|
59
|
+
const tracked = new Set(splitNul(await git.raw(['ls-tree', '-r', '-z', '--name-only', tip, '--', CANOPY_META_DIR])));
|
|
60
|
+
return {
|
|
61
|
+
droppedUpstream: paths.filter((p) => !tracked.has(p)),
|
|
62
|
+
stillTracked: paths.filter((p) => tracked.has(p)),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Remove `paths` from this clone's index, leaving them on disk as untracked, excluded state.
|
|
67
|
+
* Index-only, so a concurrent write to them (branch metadata, comments) is not lost. Only for a
|
|
68
|
+
* clone with no commits of its own: a rebase that replays a commit touching these paths writes
|
|
69
|
+
* that commit's bytes over them. Paths already out of the index are ignored.
|
|
70
|
+
*/
|
|
71
|
+
export async function untrackInIndex(git, paths) {
|
|
72
|
+
if (paths.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
await git.raw(['rm', '--cached', '--ignore-unmatch', '-q', '--', ...paths]);
|
|
75
|
+
}
|