slidemind 0.3.20 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
package/src/pptx/fill/engine.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
insertPictureAtGeom, findShape, esc, imageSize, cellText, blocks,
|
|
37
37
|
shapeFillXml, shapeRunRPr, setShapeFillXml,
|
|
38
38
|
} from "./xml.mjs";
|
|
39
|
+
import { deckPptxPath } from "../../lib/deck-paths.mjs";
|
|
39
40
|
|
|
40
41
|
const enc = (s) => new TextEncoder().encode(s);
|
|
41
42
|
const RESERVED_PROPS = new Set(["repeat", "tables", "patches", "highlight"]);
|
|
@@ -128,9 +129,7 @@ export async function buildFillTemplateDeck({ deckDir, skill, outPath, onLog })
|
|
|
128
129
|
throw new Error(`integrity check failed:\n - ${integrity.join("\n - ")}`);
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
const finalOut = outPath
|
|
132
|
-
? path.resolve(outPath)
|
|
133
|
-
: path.join(deckDir, `${path.basename(deckDir)}.pptx`);
|
|
132
|
+
const finalOut = outPath ? path.resolve(outPath) : deckPptxPath(deckDir);
|
|
134
133
|
fs.writeFileSync(finalOut, zip(files));
|
|
135
134
|
return { outPath: finalOut, slideCount: output.length, warnings };
|
|
136
135
|
}
|
|
@@ -19,6 +19,7 @@ import { unzip, readText } from "../zip.mjs";
|
|
|
19
19
|
import { slideOrderPaths } from "../ooxml.mjs";
|
|
20
20
|
import { foldPickedSlides } from "../slide-swap.mjs";
|
|
21
21
|
import { layoutNumber } from "./parse-slides.mjs";
|
|
22
|
+
import { deckBundleRefPath } from "../../bridge/bundle.mjs";
|
|
22
23
|
|
|
23
24
|
const SIDE_CAR = ".freeform-renders.json";
|
|
24
25
|
|
|
@@ -107,7 +108,7 @@ export async function ensureFreeformRenders({
|
|
|
107
108
|
// of the freshness key — a CD update re-renders the freeform slides.
|
|
108
109
|
let etag = "";
|
|
109
110
|
try {
|
|
110
|
-
const ref = JSON.parse(fs.readFileSync(
|
|
111
|
+
const ref = JSON.parse(fs.readFileSync(deckBundleRefPath(deckDir), "utf8"));
|
|
111
112
|
etag = ref?.etag ?? "";
|
|
112
113
|
} catch {
|
|
113
114
|
etag = "";
|
|
@@ -133,9 +134,11 @@ export async function ensureFreeformRenders({
|
|
|
133
134
|
if (fs.existsSync(assetsDir)) {
|
|
134
135
|
fs.cpSync(assetsDir, path.join(tmp, "assets"), { recursive: true });
|
|
135
136
|
}
|
|
136
|
-
const refPath =
|
|
137
|
+
const refPath = deckBundleRefPath(deckDir);
|
|
137
138
|
if (fs.existsSync(refPath)) {
|
|
138
|
-
|
|
139
|
+
const tmpRef = deckBundleRefPath(tmp);
|
|
140
|
+
fs.mkdirSync(path.dirname(tmpRef), { recursive: true });
|
|
141
|
+
fs.copyFileSync(refPath, tmpRef);
|
|
139
142
|
}
|
|
140
143
|
const outPath = path.join(tmp, "render.pptx");
|
|
141
144
|
try {
|
package/src/pptx/ooxml.mjs
CHANGED
|
@@ -145,6 +145,26 @@ function normalizeSegments(parts) {
|
|
|
145
145
|
return out.join("/");
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// One `<p:tag .../>` element: the whole element, its attributes in group 1.
|
|
149
|
+
// Quote-aware on purpose: PowerPoint writes `>` UNESCAPED inside an attribute
|
|
150
|
+
// value (valid XML), and a `[^>]*` match stops there — the value is cut off
|
|
151
|
+
// and the tag silently lost. Observed on a Mac PowerPoint save (2026-10-09):
|
|
152
|
+
// every SLIDEMIND_PROV whose selector read `svg > <line>` came back unreadable.
|
|
153
|
+
// Both quote styles XML allows, and both spellings of an empty element
|
|
154
|
+
// (`<p:tag …/>`, `<p:tag …></p:tag>`), are read.
|
|
155
|
+
export const TAG_ELEMENT = String.raw`<p:tag\b((?:[^>"']|"[^"]*"|'[^']*')*?)(?:\/>|><\/p:tag>)`;
|
|
156
|
+
|
|
157
|
+
/** One attribute of a `<p:tag>`'s attribute string, in either quote style — undecoded. */
|
|
158
|
+
export function tagAttr(attrs, name) {
|
|
159
|
+
const m = new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)')`).exec(String(attrs ?? ""));
|
|
160
|
+
return m ? (m[1] ?? m[2]) : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Every `<p:tag/>` element of a tag list, attributes in group 1. */
|
|
164
|
+
export function tagElements(tagXml) {
|
|
165
|
+
return String(tagXml ?? "").matchAll(new RegExp(TAG_ELEMENT, "g"));
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
// Decode the five predefined XML entities plus numeric character references
|
|
149
169
|
// (`é`, `é`). Ampersand is resolved LAST so a decoded `&` from a
|
|
150
170
|
// numeric ref can't be re-read as the head of another entity.
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// What a delivered package proves about one slide's objects.
|
|
2
|
+
//
|
|
3
|
+
// `src/lib/preservation-verdict.mjs` compares NORMALIZED projections keyed by
|
|
4
|
+
// element identity — deliberately, so that one pure module can serve both
|
|
5
|
+
// clients without importing either one's package machinery. It does not read
|
|
6
|
+
// packages. Nothing did, which is why the final delivery gate could never reach
|
|
7
|
+
// `passed`: a connector's endpoints and a native object's dependency closure
|
|
8
|
+
// only resolve against the delivered bytes, and no one produced that evidence.
|
|
9
|
+
//
|
|
10
|
+
// This module is that producer, on the CLI side. It takes an unzipped package
|
|
11
|
+
// and returns exactly what `comparePackageEvidence` and `preservationVerdict`
|
|
12
|
+
// consume:
|
|
13
|
+
//
|
|
14
|
+
// shapes { [elementId]: projection shape (+ native / chart) }
|
|
15
|
+
// resolvedTargets what this PACKAGE resolves — read the warning below
|
|
16
|
+
// tables per-element grid evidence (see the note below)
|
|
17
|
+
// connectors per-element { start, end } as each connector claims them
|
|
18
|
+
// natives per-element { kind, progId, parts, digests }
|
|
19
|
+
//
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// WARNING — `resolvedTargets` speaks ONE namespace, and a requirement's
|
|
22
|
+
// `target` may be written in any of three
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
//
|
|
25
|
+
// A preservation requirement's `relationships[].target`
|
|
26
|
+
// (`src/lib/slide-preservation.mjs`) is minted in the namespace its `role`
|
|
27
|
+
// belongs to, and they are not interchangeable:
|
|
28
|
+
//
|
|
29
|
+
// role target is resolved by
|
|
30
|
+
// start / end an ELEMENT id on this slide this module
|
|
31
|
+
// package-part a WORKSPACE path the caller
|
|
32
|
+
// (`conversions/<stem>.slide.pptx`)
|
|
33
|
+
// native-slot an INDEX into the conversion the caller
|
|
34
|
+
// record's `preserved[]`
|
|
35
|
+
//
|
|
36
|
+
// `resolvedTargets` contains element ids and the package's own part paths. It
|
|
37
|
+
// does NOT contain workspace paths or slot indices, and it never will — this
|
|
38
|
+
// module reads one .pptx and knows nothing about a deck's conversion records.
|
|
39
|
+
//
|
|
40
|
+
// **The consequence, stated so nobody has to rediscover it: resolving a
|
|
41
|
+
// non-element target against this set produces a FALSE DELETION.** The
|
|
42
|
+
// obligation looks unmet, the slide looks broken, and the gate refuses a
|
|
43
|
+
// delivery that lost nothing. That is strictly worse than the `unknown` it
|
|
44
|
+
// replaced, and it reached `PRESERVATION FAILED` on a clean deck once already.
|
|
45
|
+
//
|
|
46
|
+
// A caller holding requirements of mixed roles must resolve each one in its own
|
|
47
|
+
// namespace and union the results — which is what `packageEvidenceFor` in the
|
|
48
|
+
// delivery gate does. There is deliberately no `targetsByRole` here: one
|
|
49
|
+
// consumer exists and it has solved this, and a second mechanism for one job
|
|
50
|
+
// would only be a second thing to keep in step. Add it when a second consumer
|
|
51
|
+
// makes the shape obvious.
|
|
52
|
+
//
|
|
53
|
+
// Four rules it exists to keep:
|
|
54
|
+
//
|
|
55
|
+
// * **One normalizer.** The units, their order and the MC-block rule come
|
|
56
|
+
// from `computeProjection`'s own `topLevelShapes`. A second split would
|
|
57
|
+
// drift, and a drifted split relabels every element on the slide.
|
|
58
|
+
// * **No ZIP byte streams, and no part PATHS either.** Package timestamps,
|
|
59
|
+
// relationship ids, part ordering and part NAMES all legitimately change —
|
|
60
|
+
// delivery renames an imported embedding whenever the name is taken. What
|
|
61
|
+
// is compared is the normalized projection, the RESOLVED targets of
|
|
62
|
+
// relationships, and, for an opaque payload, the digest of its bytes. A
|
|
63
|
+
// path-keyed comparison reports a relocated object as a deleted one.
|
|
64
|
+
// * **Identity, never count.** A native object is bound to the parts it
|
|
65
|
+
// depends on and a connector to the shape ids at its ends. Swapping either
|
|
66
|
+
// for something else leaves the counts identical, which is exactly how E11
|
|
67
|
+
// and E17 get through a check that only counts.
|
|
68
|
+
// * **A moved object is still that object.** E17's clause: an allowed
|
|
69
|
+
// position change must not lose the native or its closure. Geometry lives
|
|
70
|
+
// in its own scope, so moving a frame changes `geom` and nothing else —
|
|
71
|
+
// the closure digests are computed from the parts, not from the placement.
|
|
72
|
+
|
|
73
|
+
import { createHash } from "node:crypto";
|
|
74
|
+
|
|
75
|
+
import { relsFor, resolvePartPath, slideOrderPaths } from "./ooxml.mjs";
|
|
76
|
+
import { computeProjection, topLevelShapes } from "./tags.mjs";
|
|
77
|
+
import { readText } from "./zip.mjs";
|
|
78
|
+
|
|
79
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
80
|
+
|
|
81
|
+
/** Relationship types whose target is a document of its own, not a payload. */
|
|
82
|
+
const NON_PAYLOAD_REL = /\/(slideLayout|slideMaster|notesSlide|theme|tags)$/;
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Identity
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The `p:cNvPr/@id` of one top-level unit, as a string.
|
|
90
|
+
*
|
|
91
|
+
* The FIRST id in the unit's own XML: for an `mc:AlternateContent` block that
|
|
92
|
+
* is the `mc:Choice` branch's frame, which is the object itself — the
|
|
93
|
+
* `mc:Fallback` picture beside it carries a different id and is a preview of
|
|
94
|
+
* the same thing, never a second element. This is the same spelling the
|
|
95
|
+
* converted dialect uses (`data-sm-source-id={60}`) and the same one a
|
|
96
|
+
* preservation requirement for an imported slide carries, so a record, a source
|
|
97
|
+
* file and a package all name an element one way.
|
|
98
|
+
*/
|
|
99
|
+
export function unitElementId(unitXml) {
|
|
100
|
+
const m = /<p:cNvPr\b[^>]*\bid="(\d+)"/.exec(String(unitXml ?? ""));
|
|
101
|
+
return m ? m[1] : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Dependency closure
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/** Every `r:id` / `r:embed` / `r:link` a unit's XML names. */
|
|
109
|
+
function relIdsIn(unitXml) {
|
|
110
|
+
const ids = new Set();
|
|
111
|
+
for (const m of String(unitXml ?? "").matchAll(/\br:(?:id|embed|link|pict|dm|lo|qs|cs)="([^"]+)"/g)) ids.add(m[1]);
|
|
112
|
+
return ids;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The parts reachable from `starts`, with each one's digest.
|
|
117
|
+
*
|
|
118
|
+
* Breadth-first over the package's own relationships, so a chart's embedded
|
|
119
|
+
* workbook and an OLE object's sidecars are in the closure even though the
|
|
120
|
+
* slide never names them directly. External targets are skipped: they are a
|
|
121
|
+
* URL, not bytes this package can lose. Layouts, masters, themes, notes and
|
|
122
|
+
* tag parts are skipped too — they are documents the whole deck shares, not
|
|
123
|
+
* this object's payload, and pulling them in would make every element's
|
|
124
|
+
* closure the entire deck.
|
|
125
|
+
*
|
|
126
|
+
* @returns {Record<string, string>} part path → sha256 of its bytes
|
|
127
|
+
*/
|
|
128
|
+
export function dependencyDigests(files, starts) {
|
|
129
|
+
const digests = {};
|
|
130
|
+
const queue = [...starts];
|
|
131
|
+
const seen = new Set(starts);
|
|
132
|
+
while (queue.length) {
|
|
133
|
+
const part = queue.shift();
|
|
134
|
+
const bytes = files[part];
|
|
135
|
+
if (bytes === undefined) continue; // a missing part is reported by its absence
|
|
136
|
+
digests[part] = sha256(bytes);
|
|
137
|
+
const dir = part.slice(0, part.lastIndexOf("/"));
|
|
138
|
+
for (const rel of relsFor(files, part)) {
|
|
139
|
+
if (rel.targetMode === "External") continue;
|
|
140
|
+
if (NON_PAYLOAD_REL.test(rel.type ?? "")) continue;
|
|
141
|
+
const target = resolvePartPath(dir, rel.target);
|
|
142
|
+
if (!target || seen.has(target)) continue;
|
|
143
|
+
seen.add(target);
|
|
144
|
+
queue.push(target);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return digests;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Turn a path -> digest map into the map `comparePackageEvidence` compares:
|
|
152
|
+
* keyed by CONTENT, valued by how many parts carry those bytes.
|
|
153
|
+
*
|
|
154
|
+
* Not by path, and that is the whole point. Delivery renames parts — an
|
|
155
|
+
* imported OLE embedding comes back as `oleObject2.bin` when its neighbour
|
|
156
|
+
* already holds `oleObject1.bin` — so a path-keyed map reports a relocated
|
|
157
|
+
* object as a DELETED one. A false deletion is the worst direction for a
|
|
158
|
+
* preservation gate to be wrong in: it refuses a delivery that lost nothing.
|
|
159
|
+
* Bytes are what a native object's dependency actually is; where the package
|
|
160
|
+
* files them is the package's business.
|
|
161
|
+
*
|
|
162
|
+
* The value is the COUNT rather than the digest again, so that dropping one of
|
|
163
|
+
* two parts that happen to share bytes still moves the map. The readable
|
|
164
|
+
* path->digest mapping stays in `natives[elementId].digests`, because a human
|
|
165
|
+
* reading a failure needs the path and a comparison must not.
|
|
166
|
+
*/
|
|
167
|
+
function contentKeyedParts(digests) {
|
|
168
|
+
const parts = {};
|
|
169
|
+
for (const sha of Object.values(digests)) {
|
|
170
|
+
const key = `sha256:${sha}`;
|
|
171
|
+
parts[key] = String((Number(parts[key] ?? 0)) + 1);
|
|
172
|
+
}
|
|
173
|
+
return parts;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Native objects
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
/** Does this unit carry an opaque object the package must keep whole? */
|
|
181
|
+
export function nativeKindOf(unitXml) {
|
|
182
|
+
const xml = String(unitXml ?? "");
|
|
183
|
+
if (/<p:oleObj\b/.test(xml)) return "ole";
|
|
184
|
+
if (/<p:contentPart\b/.test(xml)) return "contentPart";
|
|
185
|
+
if (/graphicData[^>]*uri="[^"]*\/(diagram|smartArt)"/.test(xml)) return "diagram";
|
|
186
|
+
if (/<mc:AlternateContent\b/.test(xml)) return "alternateContent";
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The `progId` an OLE object declares — its identity beyond its bytes. */
|
|
191
|
+
const progIdOf = (unitXml) => /\bprogId="([^"]*)"/.exec(String(unitXml ?? ""))?.[1] ?? null;
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Charts — data residue, not a re-serialization digest
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A chart's DATA as the package carries it: every series' name, its category
|
|
199
|
+
* cache and its value cache, with the formula each came from.
|
|
200
|
+
*
|
|
201
|
+
* Deliberately not a digest of the chart part. PowerPoint re-serializes a chart
|
|
202
|
+
* on every save and materializes its own defaults into it, so an XML hash
|
|
203
|
+
* reports drift on a deck nobody touched (the note in `src/pptx/tags.mjs` says
|
|
204
|
+
* the same thing about why this repo has no ChartSpec hash). The cached values
|
|
205
|
+
* and the refs they came from are what a build must not silently lose, and they
|
|
206
|
+
* survive that round trip.
|
|
207
|
+
*
|
|
208
|
+
* This is NOT the add-in's `ProjectionShape.chart`, which is a hash over its
|
|
209
|
+
* canonical ChartSpec. Both answer "did this chart's data change"; they are not
|
|
210
|
+
* interchangeable, which is why a verdict is always computed from ONE
|
|
211
|
+
* composer's own before and after.
|
|
212
|
+
*/
|
|
213
|
+
export function chartResidue(chartXml) {
|
|
214
|
+
const xml = String(chartXml ?? "");
|
|
215
|
+
const series = [];
|
|
216
|
+
for (const ser of xml.matchAll(/<c:ser\b[\s\S]*?<\/c:ser>/g)) {
|
|
217
|
+
const block = ser[0];
|
|
218
|
+
const cache = (section) => {
|
|
219
|
+
const found = block.match(new RegExp(`<c:${section}\\b[\\s\\S]*?</c:${section}>`));
|
|
220
|
+
if (!found) return null;
|
|
221
|
+
return {
|
|
222
|
+
f: /<c:f>([\s\S]*?)<\/c:f>/.exec(found[0])?.[1] ?? null,
|
|
223
|
+
pts: [...found[0].matchAll(/<c:pt\b[^>]*idx="(\d+)"[^>]*>\s*<c:v>([\s\S]*?)<\/c:v>/g)].map((m) => [
|
|
224
|
+
Number(m[1]),
|
|
225
|
+
m[2],
|
|
226
|
+
]),
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
series.push({ idx: Number(/<c:idx\b[^>]*val="(\d+)"/.exec(block)?.[1] ?? 0), tx: cache("tx"), cat: cache("cat"), val: cache("val") });
|
|
230
|
+
}
|
|
231
|
+
series.sort((a, b) => a.idx - b.idx);
|
|
232
|
+
return series.length ? series : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Tables
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A table's GRID: how many rows and columns, and which cells are merge
|
|
241
|
+
* continuations. Cell TEXT is already in the projection's `text` scope
|
|
242
|
+
* (`parseTextRuns` walks `a:tbl`), so this is the part that text alone cannot
|
|
243
|
+
* prove — a merged cell whose neighbour was absorbed keeps every word.
|
|
244
|
+
*
|
|
245
|
+
* Sibling evidence, not a property scope: `PROPERTY_SCOPES` is a shared
|
|
246
|
+
* contract (`src/lib/edit-operations.mjs`) and a table grid is not one of the
|
|
247
|
+
* seven things a requirement can protect. A round-trip test asserts it
|
|
248
|
+
* directly; a row that genuinely disappears also loses its text, which the
|
|
249
|
+
* verdict does see.
|
|
250
|
+
*/
|
|
251
|
+
export function tableGrid(unitXml) {
|
|
252
|
+
const tbl = String(unitXml ?? "").match(/<a:tbl\b[\s\S]*?<\/a:tbl>/)?.[0];
|
|
253
|
+
if (!tbl) return null;
|
|
254
|
+
const declared = (tbl.match(/<a:gridCol\b/g) ?? []).length;
|
|
255
|
+
const rows = [...tbl.matchAll(/<a:tr\b[\s\S]*?<\/a:tr>/g)].map((r) =>
|
|
256
|
+
[...r[0].matchAll(/<a:tc\b([^>]*)>/g)].map((c) => {
|
|
257
|
+
const attrs = c[1];
|
|
258
|
+
const span = /\bgridSpan="(\d+)"/.exec(attrs)?.[1];
|
|
259
|
+
const row = /\browSpan="(\d+)"/.exec(attrs)?.[1];
|
|
260
|
+
if (/\bhMerge="1"/.test(attrs)) return "h";
|
|
261
|
+
if (/\bvMerge="1"/.test(attrs)) return "v";
|
|
262
|
+
if (span || row) return `${span ?? 1}x${row ?? 1}`;
|
|
263
|
+
return ".";
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
// A table without an `a:tblGrid` still has a column count — its widest row.
|
|
267
|
+
return { cols: declared || Math.max(0, ...rows.map((r) => r.length)), rows };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// The composer
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Compose one slide's package evidence.
|
|
276
|
+
*
|
|
277
|
+
* @param {Record<string, Uint8Array>} files an unzipped package
|
|
278
|
+
* @param {{slidePart?: string|null}} [opts]
|
|
279
|
+
* @returns {{
|
|
280
|
+
* slidePart: string|null,
|
|
281
|
+
* shapes: Record<string, object>,
|
|
282
|
+
* resolvedTargets: string[],
|
|
283
|
+
* tables: Record<string, {cols: number, rows: string[][]}>,
|
|
284
|
+
* natives: Record<string, {kind: string, progId: string|null, parts: string[]}>,
|
|
285
|
+
* unidentified: number,
|
|
286
|
+
* }}
|
|
287
|
+
*/
|
|
288
|
+
export function composeSlideEvidence(files, { slidePart = null } = {}) {
|
|
289
|
+
const part = slidePart ?? slideOrderPaths(files)[0] ?? null;
|
|
290
|
+
const empty = { slidePart: part, shapes: {}, resolvedTargets: [], tables: {}, natives: {}, connectors: {}, unidentified: 0 };
|
|
291
|
+
if (!part) return empty;
|
|
292
|
+
const xml = readText(files, part) ?? "";
|
|
293
|
+
const spTree = xml.match(/<p:spTree>([\s\S]*?)<\/p:spTree>/)?.[1] ?? "";
|
|
294
|
+
const units = topLevelShapes(spTree);
|
|
295
|
+
const { shapes: projected } = computeProjection(files, part);
|
|
296
|
+
if (units.length !== projected.length) return empty; // two readers disagreeing is not evidence
|
|
297
|
+
|
|
298
|
+
const dir = part.slice(0, part.lastIndexOf("/"));
|
|
299
|
+
const relsById = new Map();
|
|
300
|
+
const targets = new Set();
|
|
301
|
+
for (const rel of relsFor(files, part)) {
|
|
302
|
+
if (rel.targetMode === "External") continue;
|
|
303
|
+
const resolved = resolvePartPath(dir, rel.target);
|
|
304
|
+
if (rel.id) relsById.set(rel.id, { type: rel.type ?? "", target: resolved });
|
|
305
|
+
if (resolved && files[resolved] !== undefined) targets.add(resolved);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const out = { ...empty, shapes: {}, tables: {}, natives: {}, connectors: {} };
|
|
309
|
+
units.forEach((unit, i) => {
|
|
310
|
+
const elementId = unitElementId(unit.xml);
|
|
311
|
+
if (!elementId) {
|
|
312
|
+
out.unidentified += 1;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// An id that appears twice is not an identity. Report the collision by
|
|
316
|
+
// dropping both rather than letting one silently answer for the other.
|
|
317
|
+
if (out.shapes[elementId]) {
|
|
318
|
+
// Both claimants lose their identity, so both are counted: one of them
|
|
319
|
+
// silently answering for the other is the failure mode this avoids.
|
|
320
|
+
delete out.shapes[elementId];
|
|
321
|
+
out.unidentified += 2;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const shape = { ...projected[i] };
|
|
325
|
+
|
|
326
|
+
// A connector's ENDS, exactly as it claims them. These are the only element
|
|
327
|
+
// ids that become resolvable targets: a target is something something
|
|
328
|
+
// points AT, and adding every id on the slide would make "the end is still
|
|
329
|
+
// there" true however the connector was repointed — the same
|
|
330
|
+
// count-is-not-identity hole E11 walks through. `connectors` below carries
|
|
331
|
+
// the per-element pairing for a precise check.
|
|
332
|
+
const ends = [...unit.xml.matchAll(/<a:(stCxn|endCxn)\b[^>]*\bid="(\d+)"/g)].map((m) => [
|
|
333
|
+
m[1] === "stCxn" ? "start" : "end",
|
|
334
|
+
m[2],
|
|
335
|
+
]);
|
|
336
|
+
if (ends.length) {
|
|
337
|
+
out.connectors[elementId] = Object.fromEntries(ends);
|
|
338
|
+
for (const [, id] of ends) targets.add(id);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// The parts this unit depends on, transitively.
|
|
342
|
+
const starts = [];
|
|
343
|
+
for (const id of relIdsIn(unit.xml)) {
|
|
344
|
+
const rel = relsById.get(id);
|
|
345
|
+
if (!rel?.target || NON_PAYLOAD_REL.test(rel.type)) continue;
|
|
346
|
+
starts.push(rel.target);
|
|
347
|
+
}
|
|
348
|
+
const digests = starts.length ? dependencyDigests(files, starts) : {};
|
|
349
|
+
|
|
350
|
+
const nativeKind = nativeKindOf(unit.xml);
|
|
351
|
+
if (nativeKind) {
|
|
352
|
+
// A native object's obligation IS its closure plus what it claims to be.
|
|
353
|
+
// `progId` rides in the map so a payload swapped for another application's
|
|
354
|
+
// object fails even when the byte count is identical.
|
|
355
|
+
const parts = contentKeyedParts(digests);
|
|
356
|
+
const progId = progIdOf(unit.xml);
|
|
357
|
+
if (progId) parts[`progId:${progId}`] = progId;
|
|
358
|
+
shape.native = { parts };
|
|
359
|
+
out.natives[elementId] = { kind: nativeKind, progId, parts: Object.keys(digests), digests };
|
|
360
|
+
} else if (Object.keys(digests).length) {
|
|
361
|
+
// An ordinary unit's payload (a picture's media, a chart's part) is still
|
|
362
|
+
// recorded, so a dropped dependency is visible even where the element
|
|
363
|
+
// itself survived.
|
|
364
|
+
shape.native = { parts: contentKeyedParts(digests) };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// A chart frame's data residue, hashed so the comparison stays a string
|
|
368
|
+
// equality like every other scope.
|
|
369
|
+
const chartRel = [...relIdsIn(unit.xml)].map((id) => relsById.get(id)).find((r) => r?.type.endsWith("/chart"));
|
|
370
|
+
if (chartRel?.target) {
|
|
371
|
+
const residue = chartResidue(readText(files, chartRel.target));
|
|
372
|
+
if (residue) shape.chart = sha256(JSON.stringify(residue));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const grid = tableGrid(unit.xml);
|
|
376
|
+
if (grid) out.tables[elementId] = grid;
|
|
377
|
+
|
|
378
|
+
out.shapes[elementId] = shape;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
out.resolvedTargets = [...targets].sort();
|
|
382
|
+
return out;
|
|
383
|
+
}
|