@office-kit/pptx-dev 0.11.0 → 0.13.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 +6 -6
- package/dist/animation-player.js +1 -1
- package/dist/cli.mjs +43 -18
- package/dist/cli.mjs.map +1 -1
- package/dist/editor-chunks/agent-tools-CEZAOXNe-QRX546OP.js +425 -0
- package/dist/editor-chunks/chunk-YPK7M34W.js +738 -0
- package/dist/editor.js +24 -162
- package/dist/index.mjs +1 -1
- package/dist/src-DASlwPDd.mjs +457 -0
- package/dist/src-DASlwPDd.mjs.map +1 -0
- package/package.json +6 -6
- package/dist/editor.css +0 -1
- package/dist/src-CI73YbYS.mjs +0 -1741
- package/dist/src-CI73YbYS.mjs.map +0 -1
package/dist/src-CI73YbYS.mjs
DELETED
|
@@ -1,1741 +0,0 @@
|
|
|
1
|
-
import { n as renderDeck } from "./build-cpmt-r9Q.mjs";
|
|
2
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { Worker } from "node:worker_threads";
|
|
4
|
-
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
|
-
import { mkdtempSync } from "node:fs";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { context, transform } from "esbuild";
|
|
8
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
|
|
11
|
-
import { getShapeId, getShapeKind, getShapeName, getShapePlaceholderIdx, getShapePlaceholderType, getSlideLayoutName, getSlideLayoutPartName, getSlideLayoutType, getSlideLayouts, getSlidePartName, getSlideShapes, getSlideTitle, getSlides, loadPresentation } from "@office-kit/pptx";
|
|
12
|
-
//#region src/bundle.ts
|
|
13
|
-
const resolvePackage = (name) => fileURLToPath(import.meta.resolve(name));
|
|
14
|
-
async function createDeckCompiler(entry) {
|
|
15
|
-
const transformedSources = /* @__PURE__ */ new Map();
|
|
16
|
-
const compiler = await context({
|
|
17
|
-
entryPoints: [resolve(entry)],
|
|
18
|
-
outfile: "deck.mjs",
|
|
19
|
-
write: false,
|
|
20
|
-
bundle: true,
|
|
21
|
-
platform: "node",
|
|
22
|
-
format: "esm",
|
|
23
|
-
target: "node22",
|
|
24
|
-
jsx: "automatic",
|
|
25
|
-
jsxImportSource: "@office-kit/pptx-dsl",
|
|
26
|
-
sourcemap: "inline",
|
|
27
|
-
sourceRoot: pathToFileURL(process.cwd() + sep).href,
|
|
28
|
-
metafile: true,
|
|
29
|
-
logLevel: "silent",
|
|
30
|
-
plugins: [{
|
|
31
|
-
name: "source-location",
|
|
32
|
-
setup(builder) {
|
|
33
|
-
builder.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async ({ path }) => {
|
|
34
|
-
const source = await readFile(path, "utf8");
|
|
35
|
-
const cached = transformedSources.get(path);
|
|
36
|
-
if (cached?.source === source) return {
|
|
37
|
-
contents: cached.code,
|
|
38
|
-
loader: "js"
|
|
39
|
-
};
|
|
40
|
-
const transformed = await transform(source, {
|
|
41
|
-
loader: path.endsWith(".tsx") ? "tsx" : path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") ? "ts" : path.endsWith(".jsx") ? "jsx" : "js",
|
|
42
|
-
sourcefile: path,
|
|
43
|
-
sourcemap: "inline",
|
|
44
|
-
jsx: "automatic",
|
|
45
|
-
jsxDev: true,
|
|
46
|
-
jsxImportSource: "@office-kit/pptx-dsl",
|
|
47
|
-
define: { "import.meta.url": JSON.stringify(pathToFileURL(path).href) }
|
|
48
|
-
});
|
|
49
|
-
transformedSources.set(path, {
|
|
50
|
-
source,
|
|
51
|
-
code: transformed.code
|
|
52
|
-
});
|
|
53
|
-
return {
|
|
54
|
-
contents: transformed.code,
|
|
55
|
-
loader: "js"
|
|
56
|
-
};
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}, {
|
|
60
|
-
name: "shared-office-kit",
|
|
61
|
-
setup(builder) {
|
|
62
|
-
builder.onResolve({ filter: /^@office-kit\/pptx(?:-dsl)?(?:\/.*)?$/ }, ({ path }) => ({
|
|
63
|
-
path: resolvePackage(path),
|
|
64
|
-
external: true
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
}]
|
|
68
|
-
});
|
|
69
|
-
return {
|
|
70
|
-
async build(output) {
|
|
71
|
-
const result = await compiler.rebuild();
|
|
72
|
-
await writeFile(output, result.outputFiles[0].contents);
|
|
73
|
-
const dependencies = Object.keys(result.metafile.inputs).map((path) => resolve(path));
|
|
74
|
-
const used = new Set(dependencies);
|
|
75
|
-
for (const path of transformedSources.keys()) if (!used.has(path)) transformedSources.delete(path);
|
|
76
|
-
return dependencies;
|
|
77
|
-
},
|
|
78
|
-
cancel: () => compiler.cancel(),
|
|
79
|
-
close: () => compiler.dispose()
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
//#endregion
|
|
83
|
-
//#region src/build-runner.ts
|
|
84
|
-
const BUILD_TIMEOUT_MS = 6e4;
|
|
85
|
-
function prepareWorker() {
|
|
86
|
-
const directory = mkdtempSync(join(tmpdir(), "office-pptx-"));
|
|
87
|
-
const worker = new Worker(new URL("./worker.mjs", import.meta.url), {
|
|
88
|
-
workerData: { directory },
|
|
89
|
-
execArgv: ["--enable-source-maps"]
|
|
90
|
-
});
|
|
91
|
-
const result = new Promise((resolveBuild, reject) => {
|
|
92
|
-
worker.once("message", resolveBuild);
|
|
93
|
-
worker.once("error", reject);
|
|
94
|
-
worker.once("exit", (code) => reject(/* @__PURE__ */ new Error(`Build worker exited without a result (code ${code}).`)));
|
|
95
|
-
});
|
|
96
|
-
result.catch(() => {});
|
|
97
|
-
return {
|
|
98
|
-
worker,
|
|
99
|
-
result,
|
|
100
|
-
directory
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
/** Internal watch session. Preload libraries, but never reuse a user module cache. */
|
|
104
|
-
function createDeckBuilder(entry, keepWarm = true) {
|
|
105
|
-
const compiler = createDeckCompiler(entry);
|
|
106
|
-
compiler.catch(() => {});
|
|
107
|
-
let prepared = prepareWorker();
|
|
108
|
-
let cache;
|
|
109
|
-
let closed = false;
|
|
110
|
-
let active = false;
|
|
111
|
-
return {
|
|
112
|
-
async build() {
|
|
113
|
-
if (closed || active) throw new Error("Deck builder is closed or already building.");
|
|
114
|
-
active = true;
|
|
115
|
-
const current = prepared ?? prepareWorker();
|
|
116
|
-
prepared = current;
|
|
117
|
-
let timer;
|
|
118
|
-
try {
|
|
119
|
-
const built = await Promise.race([(async () => {
|
|
120
|
-
const dependencies = await (await compiler).build(join(current.directory, "deck.mjs"));
|
|
121
|
-
current.worker.postMessage({
|
|
122
|
-
dependencies,
|
|
123
|
-
cache
|
|
124
|
-
});
|
|
125
|
-
return current.result;
|
|
126
|
-
})(), new Promise((_, reject) => {
|
|
127
|
-
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Build exceeded 60 seconds. Check the deck for loops or unresolved promises.")), BUILD_TIMEOUT_MS);
|
|
128
|
-
})]);
|
|
129
|
-
cache = built.cache;
|
|
130
|
-
return built.result;
|
|
131
|
-
} finally {
|
|
132
|
-
clearTimeout(timer);
|
|
133
|
-
await compiler.then((compiler) => compiler.cancel(), () => {});
|
|
134
|
-
await current.worker.terminate();
|
|
135
|
-
await rm(current.directory, {
|
|
136
|
-
recursive: true,
|
|
137
|
-
force: true
|
|
138
|
-
});
|
|
139
|
-
active = false;
|
|
140
|
-
prepared = !closed && keepWarm ? prepareWorker() : void 0;
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
async cancel() {
|
|
144
|
-
if (!active) return;
|
|
145
|
-
const current = prepared;
|
|
146
|
-
await compiler.then((compiler) => compiler.cancel(), () => {});
|
|
147
|
-
await current?.worker.terminate();
|
|
148
|
-
},
|
|
149
|
-
async close() {
|
|
150
|
-
closed = true;
|
|
151
|
-
const current = prepared;
|
|
152
|
-
await compiler.then((compiler) => compiler.cancel(), () => {});
|
|
153
|
-
await current?.worker.terminate();
|
|
154
|
-
await compiler.then((compiler) => compiler.close(), () => {});
|
|
155
|
-
if (current) await rm(current.directory, {
|
|
156
|
-
recursive: true,
|
|
157
|
-
force: true
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
//#endregion
|
|
163
|
-
//#region ../../src/internal/opc/zip.ts
|
|
164
|
-
const STORE_BY_EXTENSION = new Set([
|
|
165
|
-
"png",
|
|
166
|
-
"jpg",
|
|
167
|
-
"jpeg",
|
|
168
|
-
"gif",
|
|
169
|
-
"bmp",
|
|
170
|
-
"tif",
|
|
171
|
-
"tiff",
|
|
172
|
-
"webp",
|
|
173
|
-
"mp3",
|
|
174
|
-
"mp4",
|
|
175
|
-
"m4a",
|
|
176
|
-
"m4v",
|
|
177
|
-
"wav",
|
|
178
|
-
"wmv",
|
|
179
|
-
"mov",
|
|
180
|
-
"avi",
|
|
181
|
-
"webm",
|
|
182
|
-
"ogg",
|
|
183
|
-
"wma",
|
|
184
|
-
"pdf",
|
|
185
|
-
"zip",
|
|
186
|
-
"gz",
|
|
187
|
-
"7z",
|
|
188
|
-
"rar"
|
|
189
|
-
]);
|
|
190
|
-
const extensionOf = (name) => {
|
|
191
|
-
const dot = name.lastIndexOf(".");
|
|
192
|
-
return dot < 0 ? "" : name.slice(dot + 1).toLowerCase();
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* Returns true if the entry should be stored uncompressed. PPTX writers
|
|
196
|
-
* conventionally STORE already-compressed payloads to avoid wasting CPU on
|
|
197
|
-
* negligible savings.
|
|
198
|
-
*/
|
|
199
|
-
const shouldStore = (name) => STORE_BY_EXTENSION.has(extensionOf(name));
|
|
200
|
-
/**
|
|
201
|
-
* Reads a ZIP archive (a `.pptx` file is a ZIP) and returns entries in
|
|
202
|
-
* central-directory order. Throws if `input` is not a valid ZIP.
|
|
203
|
-
*/
|
|
204
|
-
const readZip = (input) => {
|
|
205
|
-
const obj = unzipSync(input);
|
|
206
|
-
const entries = [];
|
|
207
|
-
for (const name of Object.keys(obj)) {
|
|
208
|
-
const data = obj[name];
|
|
209
|
-
if (data === void 0) continue;
|
|
210
|
-
entries.push({
|
|
211
|
-
name,
|
|
212
|
-
data
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
return { entries };
|
|
216
|
-
};
|
|
217
|
-
const writeZip = (entries, options = {}) => {
|
|
218
|
-
const defaultLevel = options.level ?? 6;
|
|
219
|
-
const obj = {};
|
|
220
|
-
for (const e of entries) {
|
|
221
|
-
const level = shouldStore(e.name) ? 0 : defaultLevel;
|
|
222
|
-
obj[e.name] = [e.data, { level }];
|
|
223
|
-
}
|
|
224
|
-
return zipSync(obj);
|
|
225
|
-
};
|
|
226
|
-
//#endregion
|
|
227
|
-
//#region ../../src/internal/opc/part-name.ts
|
|
228
|
-
/**
|
|
229
|
-
* Asserts the given string is a syntactically valid OPC part name and returns
|
|
230
|
-
* it as a branded `PartName`. Throws otherwise.
|
|
231
|
-
*
|
|
232
|
-
* Rules enforced (subset; see ECMA-376 Part 2 §9.1.1.1-9.1.1.6):
|
|
233
|
-
*
|
|
234
|
-
* - Must start with `/`.
|
|
235
|
-
* - Must not end with `/`.
|
|
236
|
-
* - Must not contain `//` (empty segments).
|
|
237
|
-
* - Each segment must be non-empty and must not be `.` or `..`.
|
|
238
|
-
*/
|
|
239
|
-
const partName = (raw) => {
|
|
240
|
-
if (raw === "") throw new Error("part name is empty");
|
|
241
|
-
if (!raw.startsWith("/")) throw new Error(`part name must start with "/": ${raw}`);
|
|
242
|
-
if (raw.length > 1 && raw.endsWith("/")) throw new Error(`part name must not end with "/": ${raw}`);
|
|
243
|
-
const segments = raw.slice(1).split("/");
|
|
244
|
-
for (const s of segments) {
|
|
245
|
-
if (s === "") throw new Error(`part name contains empty segment: ${raw}`);
|
|
246
|
-
if (s === "." || s === "..") throw new Error(`part name contains "." or ".." segment: ${raw}`);
|
|
247
|
-
}
|
|
248
|
-
return raw;
|
|
249
|
-
};
|
|
250
|
-
/** Convert a part name to the corresponding ZIP entry path (no leading slash). */
|
|
251
|
-
const toZipPath = (name) => name.slice(1);
|
|
252
|
-
/** Convert a ZIP entry path to a part name (adds leading slash). */
|
|
253
|
-
const fromZipPath = (zipPath) => partName(`/${zipPath}`);
|
|
254
|
-
/**
|
|
255
|
-
* Returns the directory portion of a part name (everything before the final
|
|
256
|
-
* `/`). The result keeps the leading slash but has no trailing slash, except
|
|
257
|
-
* for the root which returns `/`.
|
|
258
|
-
*/
|
|
259
|
-
const dirname$1 = (name) => {
|
|
260
|
-
const idx = name.lastIndexOf("/");
|
|
261
|
-
if (idx <= 0) return "/";
|
|
262
|
-
return name.slice(0, idx);
|
|
263
|
-
};
|
|
264
|
-
/**
|
|
265
|
-
* Resolves a relationship Target (which may be relative) against a base part
|
|
266
|
-
* name. Returns the absolute part name (always starts with `/`).
|
|
267
|
-
*
|
|
268
|
-
* `target` is from a Relationship element's `Target` attribute when
|
|
269
|
-
* `TargetMode="Internal"`. External targets must not be passed through here.
|
|
270
|
-
*/
|
|
271
|
-
const resolveTarget = (basePart, target) => {
|
|
272
|
-
if (target === "") throw new Error("relationship target is empty");
|
|
273
|
-
if (target.startsWith("/")) return partName(target);
|
|
274
|
-
const baseDir = dirname$1(basePart);
|
|
275
|
-
const stack = baseDir === "/" ? [] : baseDir.slice(1).split("/");
|
|
276
|
-
for (const segment of target.split("/")) {
|
|
277
|
-
if (segment === "" || segment === ".") continue;
|
|
278
|
-
if (segment === "..") {
|
|
279
|
-
if (stack.length === 0) throw new Error(`relationship target "${target}" escapes the package root`);
|
|
280
|
-
stack.pop();
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
stack.push(segment);
|
|
284
|
-
}
|
|
285
|
-
return partName(`/${stack.join("/")}`);
|
|
286
|
-
};
|
|
287
|
-
//#endregion
|
|
288
|
-
//#region ../../src/internal/xml/ast.ts
|
|
289
|
-
const qname = (prefix, localName, namespaceURI) => ({
|
|
290
|
-
prefix,
|
|
291
|
-
localName,
|
|
292
|
-
namespaceURI
|
|
293
|
-
});
|
|
294
|
-
const attr = (name, value) => ({
|
|
295
|
-
name,
|
|
296
|
-
value
|
|
297
|
-
});
|
|
298
|
-
const elem = (name, init = {}) => ({
|
|
299
|
-
kind: "element",
|
|
300
|
-
name,
|
|
301
|
-
attrs: init.attrs ?? [],
|
|
302
|
-
prefixDecls: init.prefixDecls ?? /* @__PURE__ */ new Map(),
|
|
303
|
-
children: init.children ?? []
|
|
304
|
-
});
|
|
305
|
-
const text = (data) => ({
|
|
306
|
-
kind: "text",
|
|
307
|
-
data
|
|
308
|
-
});
|
|
309
|
-
const cdata = (data) => ({
|
|
310
|
-
kind: "cdata",
|
|
311
|
-
data
|
|
312
|
-
});
|
|
313
|
-
const comment = (data) => ({
|
|
314
|
-
kind: "comment",
|
|
315
|
-
data
|
|
316
|
-
});
|
|
317
|
-
const pi = (target, data) => ({
|
|
318
|
-
kind: "pi",
|
|
319
|
-
target,
|
|
320
|
-
data
|
|
321
|
-
});
|
|
322
|
-
//#endregion
|
|
323
|
-
//#region ../../src/internal/xml/namespaces.ts
|
|
324
|
-
const NS = {
|
|
325
|
-
contentTypes: "http://schemas.openxmlformats.org/package/2006/content-types",
|
|
326
|
-
relationships: "http://schemas.openxmlformats.org/package/2006/relationships",
|
|
327
|
-
officeDocRels: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
328
|
-
pml: "http://schemas.openxmlformats.org/presentationml/2006/main",
|
|
329
|
-
dml: "http://schemas.openxmlformats.org/drawingml/2006/main",
|
|
330
|
-
diagram: "http://schemas.openxmlformats.org/drawingml/2006/diagram",
|
|
331
|
-
chart: "http://schemas.openxmlformats.org/drawingml/2006/chart",
|
|
332
|
-
chartDrawing: "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing",
|
|
333
|
-
sml: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
334
|
-
wml: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
|
335
|
-
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
336
|
-
a14: "http://schemas.microsoft.com/office/drawing/2010/main",
|
|
337
|
-
a15: "http://schemas.microsoft.com/office/drawing/2012/main",
|
|
338
|
-
a16: "http://schemas.microsoft.com/office/drawing/2014/main",
|
|
339
|
-
p14: "http://schemas.microsoft.com/office/powerpoint/2010/main",
|
|
340
|
-
p15: "http://schemas.microsoft.com/office/powerpoint/2012/main",
|
|
341
|
-
p16: "http://schemas.microsoft.com/office/powerpoint/2015/main",
|
|
342
|
-
xml: "http://www.w3.org/XML/1998/namespace",
|
|
343
|
-
xmlns: "http://www.w3.org/2000/xmlns/"
|
|
344
|
-
};
|
|
345
|
-
NS.contentTypes, NS.relationships, NS.officeDocRels, NS.pml, NS.dml, NS.diagram, NS.chart, NS.chartDrawing, NS.sml, NS.wml, NS.mc, NS.a14, NS.a15, NS.a16, NS.p14, NS.p15, NS.p16, NS.xml;
|
|
346
|
-
//#endregion
|
|
347
|
-
//#region ../../src/internal/xml/parse.ts
|
|
348
|
-
var XmlParseError = class extends Error {
|
|
349
|
-
offset;
|
|
350
|
-
line;
|
|
351
|
-
column;
|
|
352
|
-
constructor(message, offset, line, column) {
|
|
353
|
-
super(`${message} at line ${line}:${column} (offset ${offset})`);
|
|
354
|
-
this.offset = offset;
|
|
355
|
-
this.line = line;
|
|
356
|
-
this.column = column;
|
|
357
|
-
this.name = "XmlParseError";
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
const isNameStartChar = (c) => {
|
|
361
|
-
const code = c.charCodeAt(0);
|
|
362
|
-
return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code === 58;
|
|
363
|
-
};
|
|
364
|
-
const isNameChar = (c) => {
|
|
365
|
-
if (isNameStartChar(c)) return true;
|
|
366
|
-
const code = c.charCodeAt(0);
|
|
367
|
-
return code >= 48 && code <= 57 || code === 45 || code === 46;
|
|
368
|
-
};
|
|
369
|
-
const isWhitespace = (c) => c === " " || c === " " || c === "\n" || c === "\r";
|
|
370
|
-
const lineColOf = (src, pos) => {
|
|
371
|
-
let line = 1;
|
|
372
|
-
let column = 1;
|
|
373
|
-
for (let i = 0; i < pos && i < src.length; i++) if (src.charCodeAt(i) === 10) {
|
|
374
|
-
line++;
|
|
375
|
-
column = 1;
|
|
376
|
-
} else column++;
|
|
377
|
-
return {
|
|
378
|
-
line,
|
|
379
|
-
column
|
|
380
|
-
};
|
|
381
|
-
};
|
|
382
|
-
const fail = (cur, message) => {
|
|
383
|
-
const { line, column } = lineColOf(cur.src, cur.pos);
|
|
384
|
-
throw new XmlParseError(message, cur.pos, line, column);
|
|
385
|
-
};
|
|
386
|
-
const expect = (cur, literal) => {
|
|
387
|
-
if (cur.src.slice(cur.pos, cur.pos + literal.length) !== literal) fail(cur, `expected "${literal}"`);
|
|
388
|
-
cur.pos += literal.length;
|
|
389
|
-
};
|
|
390
|
-
const accept = (cur, literal) => {
|
|
391
|
-
if (cur.src.slice(cur.pos, cur.pos + literal.length) !== literal) return false;
|
|
392
|
-
cur.pos += literal.length;
|
|
393
|
-
return true;
|
|
394
|
-
};
|
|
395
|
-
const skipWhitespace = (cur) => {
|
|
396
|
-
while (cur.pos < cur.src.length && isWhitespace(cur.src[cur.pos] ?? "")) cur.pos++;
|
|
397
|
-
};
|
|
398
|
-
const readName = (cur) => {
|
|
399
|
-
const start = cur.pos;
|
|
400
|
-
if (cur.pos >= cur.src.length || !isNameStartChar(cur.src[cur.pos] ?? "")) fail(cur, "expected XML name");
|
|
401
|
-
cur.pos++;
|
|
402
|
-
while (cur.pos < cur.src.length && isNameChar(cur.src[cur.pos] ?? "")) cur.pos++;
|
|
403
|
-
return cur.src.slice(start, cur.pos);
|
|
404
|
-
};
|
|
405
|
-
const splitQName = (raw) => {
|
|
406
|
-
const idx = raw.indexOf(":");
|
|
407
|
-
if (idx < 0) return {
|
|
408
|
-
prefix: "",
|
|
409
|
-
localName: raw
|
|
410
|
-
};
|
|
411
|
-
return {
|
|
412
|
-
prefix: raw.slice(0, idx),
|
|
413
|
-
localName: raw.slice(idx + 1)
|
|
414
|
-
};
|
|
415
|
-
};
|
|
416
|
-
const decodeRefs = (cur, raw) => {
|
|
417
|
-
let out = "";
|
|
418
|
-
let i = 0;
|
|
419
|
-
while (i < raw.length) {
|
|
420
|
-
if (raw.charCodeAt(i) !== 38) {
|
|
421
|
-
out += raw[i];
|
|
422
|
-
i++;
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
const end = raw.indexOf(";", i + 1);
|
|
426
|
-
if (end < 0) {
|
|
427
|
-
cur.pos -= raw.length - i;
|
|
428
|
-
fail(cur, "unterminated entity reference");
|
|
429
|
-
}
|
|
430
|
-
const ref = raw.slice(i + 1, end);
|
|
431
|
-
if (ref === "amp") out += "&";
|
|
432
|
-
else if (ref === "lt") out += "<";
|
|
433
|
-
else if (ref === "gt") out += ">";
|
|
434
|
-
else if (ref === "quot") out += "\"";
|
|
435
|
-
else if (ref === "apos") out += "'";
|
|
436
|
-
else if (ref.startsWith("#x") || ref.startsWith("#X")) {
|
|
437
|
-
const code = Number.parseInt(ref.slice(2), 16);
|
|
438
|
-
if (!Number.isFinite(code)) fail(cur, `invalid hex numeric reference: &${ref};`);
|
|
439
|
-
out += String.fromCodePoint(code);
|
|
440
|
-
} else if (ref.startsWith("#")) {
|
|
441
|
-
const code = Number.parseInt(ref.slice(1), 10);
|
|
442
|
-
if (!Number.isFinite(code)) fail(cur, `invalid decimal numeric reference: &${ref};`);
|
|
443
|
-
out += String.fromCodePoint(code);
|
|
444
|
-
} else fail(cur, `unknown entity reference: &${ref};`);
|
|
445
|
-
i = end + 1;
|
|
446
|
-
}
|
|
447
|
-
return out;
|
|
448
|
-
};
|
|
449
|
-
const readAttrValue = (cur) => {
|
|
450
|
-
const quote = cur.src[cur.pos];
|
|
451
|
-
if (quote !== "\"" && quote !== "'") fail(cur, "expected attribute value");
|
|
452
|
-
cur.pos++;
|
|
453
|
-
const start = cur.pos;
|
|
454
|
-
while (cur.pos < cur.src.length && cur.src[cur.pos] !== quote) {
|
|
455
|
-
if (cur.src.charCodeAt(cur.pos) === 60) fail(cur, "\"<\" is not allowed inside an attribute value");
|
|
456
|
-
cur.pos++;
|
|
457
|
-
}
|
|
458
|
-
if (cur.pos >= cur.src.length) fail(cur, "unterminated attribute value");
|
|
459
|
-
const raw = cur.src.slice(start, cur.pos);
|
|
460
|
-
cur.pos++;
|
|
461
|
-
return decodeRefs(cur, raw);
|
|
462
|
-
};
|
|
463
|
-
const xmlScope = {
|
|
464
|
-
parent: null,
|
|
465
|
-
bindings: new Map([["xml", NS.xml]])
|
|
466
|
-
};
|
|
467
|
-
const resolvePrefix = (scope, prefix) => {
|
|
468
|
-
let cursor = scope;
|
|
469
|
-
while (cursor) {
|
|
470
|
-
const found = cursor.bindings.get(prefix);
|
|
471
|
-
if (found !== void 0) return found;
|
|
472
|
-
cursor = cursor.parent;
|
|
473
|
-
}
|
|
474
|
-
return null;
|
|
475
|
-
};
|
|
476
|
-
const resolveQName = (cur, scope, raw, isAttribute) => {
|
|
477
|
-
const { prefix, localName } = splitQName(raw);
|
|
478
|
-
let namespaceURI = "";
|
|
479
|
-
if (prefix !== "") {
|
|
480
|
-
const found = resolvePrefix(scope, prefix);
|
|
481
|
-
if (found === null) return fail(cur, `unbound namespace prefix "${prefix}"`);
|
|
482
|
-
namespaceURI = found;
|
|
483
|
-
} else if (!isAttribute) namespaceURI = resolvePrefix(scope, "") ?? "";
|
|
484
|
-
return qname(prefix, localName, namespaceURI);
|
|
485
|
-
};
|
|
486
|
-
const parseDeclaration = (cur) => {
|
|
487
|
-
if (!accept(cur, "<?xml")) return null;
|
|
488
|
-
const end = cur.src.indexOf("?>", cur.pos);
|
|
489
|
-
if (end < 0) fail(cur, "unterminated XML declaration");
|
|
490
|
-
const body = cur.src.slice(cur.pos, end);
|
|
491
|
-
cur.pos = end + 2;
|
|
492
|
-
const get = (key) => {
|
|
493
|
-
return body.match(new RegExp(`\\b${key}\\s*=\\s*(['"])(.*?)\\1`))?.[2];
|
|
494
|
-
};
|
|
495
|
-
const version = get("version");
|
|
496
|
-
if (version === void 0) return fail(cur, "XML declaration missing \"version\"");
|
|
497
|
-
const encoding = get("encoding");
|
|
498
|
-
const standaloneRaw = get("standalone");
|
|
499
|
-
let standalone;
|
|
500
|
-
if (standaloneRaw === "yes" || standaloneRaw === "no") standalone = standaloneRaw;
|
|
501
|
-
else if (standaloneRaw !== void 0) return fail(cur, `invalid standalone value "${standaloneRaw}"`);
|
|
502
|
-
return {
|
|
503
|
-
version,
|
|
504
|
-
...encoding !== void 0 ? { encoding } : {},
|
|
505
|
-
...standalone !== void 0 ? { standalone } : {}
|
|
506
|
-
};
|
|
507
|
-
};
|
|
508
|
-
const parseComment = (cur) => {
|
|
509
|
-
expect(cur, "<!--");
|
|
510
|
-
const end = cur.src.indexOf("-->", cur.pos);
|
|
511
|
-
if (end < 0) fail(cur, "unterminated comment");
|
|
512
|
-
const data = cur.src.slice(cur.pos, end);
|
|
513
|
-
if (data.includes("--")) fail(cur, "\"--\" is not allowed inside an XML comment");
|
|
514
|
-
cur.pos = end + 3;
|
|
515
|
-
return comment(data);
|
|
516
|
-
};
|
|
517
|
-
const parsePI = (cur) => {
|
|
518
|
-
expect(cur, "<?");
|
|
519
|
-
const targetStart = cur.pos;
|
|
520
|
-
while (cur.pos < cur.src.length && !isWhitespace(cur.src[cur.pos] ?? "") && cur.src.slice(cur.pos, cur.pos + 2) !== "?>") cur.pos++;
|
|
521
|
-
const target = cur.src.slice(targetStart, cur.pos);
|
|
522
|
-
if (target.toLowerCase() === "xml") fail(cur, "PI target may not be \"xml\"");
|
|
523
|
-
skipWhitespace(cur);
|
|
524
|
-
const end = cur.src.indexOf("?>", cur.pos);
|
|
525
|
-
if (end < 0) fail(cur, "unterminated processing instruction");
|
|
526
|
-
const data = cur.src.slice(cur.pos, end);
|
|
527
|
-
cur.pos = end + 2;
|
|
528
|
-
return pi(target, data);
|
|
529
|
-
};
|
|
530
|
-
const parseCData = (cur) => {
|
|
531
|
-
expect(cur, "<![CDATA[");
|
|
532
|
-
const end = cur.src.indexOf("]]>", cur.pos);
|
|
533
|
-
if (end < 0) fail(cur, "unterminated CDATA section");
|
|
534
|
-
const data = cur.src.slice(cur.pos, end);
|
|
535
|
-
cur.pos = end + 3;
|
|
536
|
-
return cdata(data);
|
|
537
|
-
};
|
|
538
|
-
const skipDoctype = (cur) => {
|
|
539
|
-
expect(cur, "<!DOCTYPE");
|
|
540
|
-
while (cur.pos < cur.src.length) {
|
|
541
|
-
const c = cur.src[cur.pos];
|
|
542
|
-
if (c === "[") fail(cur, "DOCTYPE internal subset is not supported");
|
|
543
|
-
if (c === ">") {
|
|
544
|
-
cur.pos++;
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
cur.pos++;
|
|
548
|
-
}
|
|
549
|
-
fail(cur, "unterminated DOCTYPE");
|
|
550
|
-
};
|
|
551
|
-
const parseStartOrEmptyTag = (cur, parentScope) => {
|
|
552
|
-
expect(cur, "<");
|
|
553
|
-
const rawName = readName(cur);
|
|
554
|
-
const ownAttrs = [];
|
|
555
|
-
const prefixDecls = /* @__PURE__ */ new Map();
|
|
556
|
-
let empty = false;
|
|
557
|
-
while (true) {
|
|
558
|
-
const wsStart = cur.pos;
|
|
559
|
-
skipWhitespace(cur);
|
|
560
|
-
if (cur.pos === wsStart && cur.src[cur.pos] !== "/" && cur.src[cur.pos] !== ">") fail(cur, "expected whitespace before next attribute");
|
|
561
|
-
if (accept(cur, "/>")) {
|
|
562
|
-
empty = true;
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
if (accept(cur, ">")) break;
|
|
566
|
-
const attrRawName = readName(cur);
|
|
567
|
-
skipWhitespace(cur);
|
|
568
|
-
expect(cur, "=");
|
|
569
|
-
skipWhitespace(cur);
|
|
570
|
-
const value = readAttrValue(cur);
|
|
571
|
-
if (attrRawName === "xmlns") prefixDecls.set("", value);
|
|
572
|
-
else if (attrRawName.startsWith("xmlns:")) {
|
|
573
|
-
const p = attrRawName.slice(6);
|
|
574
|
-
if (p === "") fail(cur, "empty namespace prefix in xmlns:");
|
|
575
|
-
if (p === "xmlns") fail(cur, "prefix \"xmlns\" cannot be redeclared");
|
|
576
|
-
prefixDecls.set(p, value);
|
|
577
|
-
} else ownAttrs.push({
|
|
578
|
-
raw: attrRawName,
|
|
579
|
-
rawValue: value
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
const scope = {
|
|
583
|
-
parent: parentScope,
|
|
584
|
-
bindings: new Map(prefixDecls)
|
|
585
|
-
};
|
|
586
|
-
return {
|
|
587
|
-
element: elem(resolveQName(cur, scope, rawName, false), {
|
|
588
|
-
attrs: ownAttrs.map(({ raw, rawValue }) => attr(resolveQName(cur, scope, raw, true), rawValue)),
|
|
589
|
-
prefixDecls,
|
|
590
|
-
children: []
|
|
591
|
-
}),
|
|
592
|
-
scope,
|
|
593
|
-
empty
|
|
594
|
-
};
|
|
595
|
-
};
|
|
596
|
-
const parseEndTag = (cur) => {
|
|
597
|
-
expect(cur, "</");
|
|
598
|
-
const name = readName(cur);
|
|
599
|
-
skipWhitespace(cur);
|
|
600
|
-
expect(cur, ">");
|
|
601
|
-
return name;
|
|
602
|
-
};
|
|
603
|
-
const parseTextContent = (cur) => {
|
|
604
|
-
const start = cur.pos;
|
|
605
|
-
while (cur.pos < cur.src.length && cur.src.charCodeAt(cur.pos) !== 60) cur.pos++;
|
|
606
|
-
return decodeRefs(cur, cur.src.slice(start, cur.pos));
|
|
607
|
-
};
|
|
608
|
-
const parseElement = (cur, parentScope) => {
|
|
609
|
-
const { element, scope, empty } = parseStartOrEmptyTag(cur, parentScope);
|
|
610
|
-
if (empty) return element;
|
|
611
|
-
while (cur.pos < cur.src.length) if (cur.src.charCodeAt(cur.pos) === 60) {
|
|
612
|
-
if (cur.src.startsWith("</", cur.pos)) {
|
|
613
|
-
const closingRaw = parseEndTag(cur);
|
|
614
|
-
const expectedRaw = element.name.prefix === "" ? element.name.localName : `${element.name.prefix}:${element.name.localName}`;
|
|
615
|
-
if (closingRaw !== expectedRaw) fail(cur, `mismatched closing tag: expected </${expectedRaw}>, got </${closingRaw}>`);
|
|
616
|
-
return element;
|
|
617
|
-
}
|
|
618
|
-
if (cur.src.startsWith("<![CDATA[", cur.pos)) element.children.push(parseCData(cur));
|
|
619
|
-
else if (cur.src.startsWith("<!--", cur.pos)) element.children.push(parseComment(cur));
|
|
620
|
-
else if (cur.src.startsWith("<?", cur.pos)) element.children.push(parsePI(cur));
|
|
621
|
-
else element.children.push(parseElement(cur, scope));
|
|
622
|
-
} else {
|
|
623
|
-
const t = parseTextContent(cur);
|
|
624
|
-
if (t.length > 0) element.children.push(text(t));
|
|
625
|
-
}
|
|
626
|
-
return fail(cur, `unterminated element <${element.name.localName}>`);
|
|
627
|
-
};
|
|
628
|
-
const parseXml = (src) => {
|
|
629
|
-
const cur = {
|
|
630
|
-
src: (src.charCodeAt(0) === 65279 ? src.slice(1) : src).replace(/\r\n?/g, "\n"),
|
|
631
|
-
pos: 0
|
|
632
|
-
};
|
|
633
|
-
const decl = parseDeclaration(cur);
|
|
634
|
-
skipWhitespace(cur);
|
|
635
|
-
const prolog = [];
|
|
636
|
-
while (cur.pos < cur.src.length) {
|
|
637
|
-
skipWhitespace(cur);
|
|
638
|
-
if (cur.src.startsWith("<!--", cur.pos)) prolog.push(parseComment(cur));
|
|
639
|
-
else if (cur.src.startsWith("<!DOCTYPE", cur.pos)) skipDoctype(cur);
|
|
640
|
-
else if (cur.src.startsWith("<?", cur.pos)) prolog.push(parsePI(cur));
|
|
641
|
-
else break;
|
|
642
|
-
}
|
|
643
|
-
skipWhitespace(cur);
|
|
644
|
-
if (cur.pos >= cur.src.length || cur.src[cur.pos] !== "<") fail(cur, "expected root element");
|
|
645
|
-
const root = parseElement(cur, xmlScope);
|
|
646
|
-
const epilog = [];
|
|
647
|
-
while (cur.pos < cur.src.length) {
|
|
648
|
-
skipWhitespace(cur);
|
|
649
|
-
if (cur.pos >= cur.src.length) break;
|
|
650
|
-
if (cur.src.startsWith("<!--", cur.pos)) epilog.push(parseComment(cur));
|
|
651
|
-
else if (cur.src.startsWith("<?", cur.pos)) epilog.push(parsePI(cur));
|
|
652
|
-
else fail(cur, "unexpected content after root element");
|
|
653
|
-
}
|
|
654
|
-
return {
|
|
655
|
-
kind: "document",
|
|
656
|
-
decl,
|
|
657
|
-
prolog,
|
|
658
|
-
root,
|
|
659
|
-
epilog
|
|
660
|
-
};
|
|
661
|
-
};
|
|
662
|
-
//#endregion
|
|
663
|
-
//#region ../../src/internal/xml/query.ts
|
|
664
|
-
const isElement = (n) => n.kind === "element";
|
|
665
|
-
/**
|
|
666
|
-
* Walks the element tree depth-first, yielding every element including the
|
|
667
|
-
* root. The visitor returns `false` to skip descending into the current
|
|
668
|
-
* element's children. Returning nothing (or `true`) descends as usual.
|
|
669
|
-
*/
|
|
670
|
-
const walkElements = (root, visit) => {
|
|
671
|
-
const stack = [root];
|
|
672
|
-
while (stack.length > 0) {
|
|
673
|
-
const node = stack.pop();
|
|
674
|
-
if (!node) continue;
|
|
675
|
-
if (visit(node) === false) continue;
|
|
676
|
-
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
677
|
-
const c = node.children[i];
|
|
678
|
-
if (c && isElement(c)) stack.push(c);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
//#endregion
|
|
683
|
-
//#region ../../src/internal/xml/serialize.ts
|
|
684
|
-
const rejectForbiddenChar = (c) => {
|
|
685
|
-
const control = c < 32 && c !== 9 && c !== 10 && c !== 13;
|
|
686
|
-
if (control || c >= 55296 && c <= 57343 || c === 65534 || c === 65535) {
|
|
687
|
-
const hex = c.toString(16).toUpperCase().padStart(4, "0");
|
|
688
|
-
throw new Error(`XML-illegal ${control ? "control character" : "character"} U+${hex} in text; strip ${control ? "control" : "illegal"} characters before authoring`);
|
|
689
|
-
}
|
|
690
|
-
};
|
|
691
|
-
const escapeAttr = (s) => {
|
|
692
|
-
let out = "";
|
|
693
|
-
for (const char of s) {
|
|
694
|
-
const c = char.codePointAt(0);
|
|
695
|
-
rejectForbiddenChar(c);
|
|
696
|
-
if (c === 38) out += "&";
|
|
697
|
-
else if (c === 60) out += "<";
|
|
698
|
-
else if (c === 62) out += ">";
|
|
699
|
-
else if (c === 34) out += """;
|
|
700
|
-
else if (c === 9) out += "	";
|
|
701
|
-
else if (c === 10) out += " ";
|
|
702
|
-
else if (c === 13) out += " ";
|
|
703
|
-
else out += char;
|
|
704
|
-
}
|
|
705
|
-
return out;
|
|
706
|
-
};
|
|
707
|
-
const escapeText = (s) => {
|
|
708
|
-
let out = "";
|
|
709
|
-
for (const char of s) {
|
|
710
|
-
const c = char.codePointAt(0);
|
|
711
|
-
rejectForbiddenChar(c);
|
|
712
|
-
if (c === 38) out += "&";
|
|
713
|
-
else if (c === 60) out += "<";
|
|
714
|
-
else if (c === 62) out += ">";
|
|
715
|
-
else if (c === 13) out += " ";
|
|
716
|
-
else out += char;
|
|
717
|
-
}
|
|
718
|
-
return out;
|
|
719
|
-
};
|
|
720
|
-
const rawName = (prefix, localName) => prefix === "" ? localName : `${prefix}:${localName}`;
|
|
721
|
-
const writeAttr = (a) => {
|
|
722
|
-
return ` ${rawName(a.name.prefix, a.name.localName)}="${escapeAttr(a.value)}"`;
|
|
723
|
-
};
|
|
724
|
-
const writeElement = (e, parts) => {
|
|
725
|
-
const name = rawName(e.name.prefix, e.name.localName);
|
|
726
|
-
parts.push("<");
|
|
727
|
-
parts.push(name);
|
|
728
|
-
for (const [prefix, uri] of e.prefixDecls) {
|
|
729
|
-
const decl = prefix === "" ? "xmlns" : `xmlns:${prefix}`;
|
|
730
|
-
parts.push(` ${decl}="${escapeAttr(uri)}"`);
|
|
731
|
-
}
|
|
732
|
-
for (const a of e.attrs) parts.push(writeAttr(a));
|
|
733
|
-
if (e.children.length === 0) {
|
|
734
|
-
parts.push("/>");
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
parts.push(">");
|
|
738
|
-
for (const child of e.children) writeNode(child, parts);
|
|
739
|
-
parts.push("</");
|
|
740
|
-
parts.push(name);
|
|
741
|
-
parts.push(">");
|
|
742
|
-
};
|
|
743
|
-
const writeNode = (n, parts) => {
|
|
744
|
-
switch (n.kind) {
|
|
745
|
-
case "element":
|
|
746
|
-
writeElement(n, parts);
|
|
747
|
-
return;
|
|
748
|
-
case "text":
|
|
749
|
-
parts.push(escapeText(n.data));
|
|
750
|
-
return;
|
|
751
|
-
case "cdata":
|
|
752
|
-
parts.push("<![CDATA[");
|
|
753
|
-
parts.push(n.data.split("]]>").join("]]]]><![CDATA[>"));
|
|
754
|
-
parts.push("]]>");
|
|
755
|
-
return;
|
|
756
|
-
case "comment":
|
|
757
|
-
parts.push("<!--");
|
|
758
|
-
parts.push(n.data);
|
|
759
|
-
parts.push("-->");
|
|
760
|
-
return;
|
|
761
|
-
case "pi":
|
|
762
|
-
parts.push("<?");
|
|
763
|
-
parts.push(n.target);
|
|
764
|
-
if (n.data.length > 0) {
|
|
765
|
-
parts.push(" ");
|
|
766
|
-
parts.push(n.data);
|
|
767
|
-
}
|
|
768
|
-
parts.push("?>");
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
const writeDeclaration = (doc, parts) => {
|
|
773
|
-
if (!doc.decl) return;
|
|
774
|
-
parts.push("<?xml version=\"");
|
|
775
|
-
parts.push(escapeAttr(doc.decl.version));
|
|
776
|
-
parts.push("\"");
|
|
777
|
-
if (doc.decl.encoding !== void 0) {
|
|
778
|
-
parts.push(" encoding=\"");
|
|
779
|
-
parts.push(escapeAttr(doc.decl.encoding));
|
|
780
|
-
parts.push("\"");
|
|
781
|
-
}
|
|
782
|
-
if (doc.decl.standalone !== void 0) {
|
|
783
|
-
parts.push(" standalone=\"");
|
|
784
|
-
parts.push(doc.decl.standalone);
|
|
785
|
-
parts.push("\"");
|
|
786
|
-
}
|
|
787
|
-
parts.push("?>");
|
|
788
|
-
};
|
|
789
|
-
const serializeXml = (doc, options = {}) => {
|
|
790
|
-
const parts = [];
|
|
791
|
-
if (doc.decl) writeDeclaration(doc, parts);
|
|
792
|
-
else if (options.standaloneDeclaration ?? true) parts.push("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
|
|
793
|
-
for (const p of doc.prolog) writeNode(p, parts);
|
|
794
|
-
writeElement(doc.root, parts);
|
|
795
|
-
for (const e of doc.epilog) writeNode(e, parts);
|
|
796
|
-
return parts.join("");
|
|
797
|
-
};
|
|
798
|
-
const serializeFragment = (element) => {
|
|
799
|
-
const parts = [];
|
|
800
|
-
writeElement(element, parts);
|
|
801
|
-
return parts.join("");
|
|
802
|
-
};
|
|
803
|
-
//#endregion
|
|
804
|
-
//#region ../../src/internal/opc/content-types.ts
|
|
805
|
-
const CT_NS = NS.contentTypes;
|
|
806
|
-
const NAME_TYPES = qname("", "Types", CT_NS);
|
|
807
|
-
const NAME_DEFAULT = qname("", "Default", CT_NS);
|
|
808
|
-
const NAME_OVERRIDE = qname("", "Override", CT_NS);
|
|
809
|
-
const ATTR_EXTENSION = qname("", "Extension", "");
|
|
810
|
-
const ATTR_CONTENT_TYPE = qname("", "ContentType", "");
|
|
811
|
-
const ATTR_PART_NAME = qname("", "PartName", "");
|
|
812
|
-
const getAttrValueRaw = (e, local) => {
|
|
813
|
-
for (const a of e.attrs) if (a.name.localName === local) return a.value;
|
|
814
|
-
return null;
|
|
815
|
-
};
|
|
816
|
-
const parseContentTypes = (xml) => {
|
|
817
|
-
const doc = parseXml(xml);
|
|
818
|
-
if (doc.root.name.namespaceURI !== CT_NS || doc.root.name.localName !== "Types") throw new Error(`expected root <Types xmlns="${CT_NS}">, got <${doc.root.name.localName}>`);
|
|
819
|
-
const defaults = [];
|
|
820
|
-
const overrides = [];
|
|
821
|
-
for (const child of doc.root.children) {
|
|
822
|
-
if (child.kind !== "element") continue;
|
|
823
|
-
if (child.name.namespaceURI !== CT_NS) continue;
|
|
824
|
-
if (child.name.localName === "Default") {
|
|
825
|
-
const ext = getAttrValueRaw(child, "Extension");
|
|
826
|
-
const ct = getAttrValueRaw(child, "ContentType");
|
|
827
|
-
if (ext === null || ct === null) throw new Error("Content_Types Default missing Extension or ContentType");
|
|
828
|
-
defaults.push({
|
|
829
|
-
extension: ext.toLowerCase(),
|
|
830
|
-
contentType: ct
|
|
831
|
-
});
|
|
832
|
-
} else if (child.name.localName === "Override") {
|
|
833
|
-
const pn = getAttrValueRaw(child, "PartName");
|
|
834
|
-
const ct = getAttrValueRaw(child, "ContentType");
|
|
835
|
-
if (pn === null || ct === null) throw new Error("Content_Types Override missing PartName or ContentType");
|
|
836
|
-
overrides.push({
|
|
837
|
-
partName: partName(pn),
|
|
838
|
-
contentType: ct
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return {
|
|
843
|
-
defaults,
|
|
844
|
-
overrides
|
|
845
|
-
};
|
|
846
|
-
};
|
|
847
|
-
const serializeContentTypes = (ct) => {
|
|
848
|
-
const children = [...ct.defaults.map((d) => elem(NAME_DEFAULT, { attrs: [{
|
|
849
|
-
name: ATTR_EXTENSION,
|
|
850
|
-
value: d.extension
|
|
851
|
-
}, {
|
|
852
|
-
name: ATTR_CONTENT_TYPE,
|
|
853
|
-
value: d.contentType
|
|
854
|
-
}] })), ...ct.overrides.map((o) => elem(NAME_OVERRIDE, { attrs: [{
|
|
855
|
-
name: ATTR_PART_NAME,
|
|
856
|
-
value: o.partName
|
|
857
|
-
}, {
|
|
858
|
-
name: ATTR_CONTENT_TYPE,
|
|
859
|
-
value: o.contentType
|
|
860
|
-
}] }))];
|
|
861
|
-
return serializeXml({
|
|
862
|
-
kind: "document",
|
|
863
|
-
decl: {
|
|
864
|
-
version: "1.0",
|
|
865
|
-
encoding: "UTF-8",
|
|
866
|
-
standalone: "yes"
|
|
867
|
-
},
|
|
868
|
-
prolog: [],
|
|
869
|
-
root: elem(NAME_TYPES, {
|
|
870
|
-
prefixDecls: new Map([["", CT_NS]]),
|
|
871
|
-
children
|
|
872
|
-
}),
|
|
873
|
-
epilog: []
|
|
874
|
-
});
|
|
875
|
-
};
|
|
876
|
-
//#endregion
|
|
877
|
-
//#region ../../src/internal/opc/rels.ts
|
|
878
|
-
const REL_NS = NS.relationships;
|
|
879
|
-
const NAME_RELATIONSHIPS = qname("", "Relationships", REL_NS);
|
|
880
|
-
const NAME_RELATIONSHIP = qname("", "Relationship", REL_NS);
|
|
881
|
-
const ATTR_ID = qname("", "Id", "");
|
|
882
|
-
const ATTR_TYPE = qname("", "Type", "");
|
|
883
|
-
const ATTR_TARGET = qname("", "Target", "");
|
|
884
|
-
const ATTR_TARGET_MODE = qname("", "TargetMode", "");
|
|
885
|
-
const findAttr = (e, local) => {
|
|
886
|
-
for (const a of e.attrs) if (a.name.localName === local) return a.value;
|
|
887
|
-
return null;
|
|
888
|
-
};
|
|
889
|
-
const parseRels = (xml) => {
|
|
890
|
-
const doc = parseXml(xml);
|
|
891
|
-
if (doc.root.name.namespaceURI !== REL_NS || doc.root.name.localName !== "Relationships") throw new Error(`expected root <Relationships xmlns="${REL_NS}">, got <${doc.root.name.localName}>`);
|
|
892
|
-
const items = [];
|
|
893
|
-
for (const child of doc.root.children) {
|
|
894
|
-
if (child.kind !== "element") continue;
|
|
895
|
-
if (child.name.namespaceURI !== REL_NS || child.name.localName !== "Relationship") continue;
|
|
896
|
-
const id = findAttr(child, "Id");
|
|
897
|
-
const type = findAttr(child, "Type");
|
|
898
|
-
const target = findAttr(child, "Target");
|
|
899
|
-
if (id === null || type === null || target === null) throw new Error("Relationship missing Id, Type, or Target");
|
|
900
|
-
const modeRaw = findAttr(child, "TargetMode");
|
|
901
|
-
let targetMode = "Internal";
|
|
902
|
-
if (modeRaw !== null) {
|
|
903
|
-
if (modeRaw !== "Internal" && modeRaw !== "External") throw new Error(`invalid TargetMode "${modeRaw}"`);
|
|
904
|
-
targetMode = modeRaw;
|
|
905
|
-
}
|
|
906
|
-
items.push({
|
|
907
|
-
id,
|
|
908
|
-
type,
|
|
909
|
-
target,
|
|
910
|
-
targetMode
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
return { items };
|
|
914
|
-
};
|
|
915
|
-
const serializeRels = (rels) => {
|
|
916
|
-
const children = rels.items.map((r) => {
|
|
917
|
-
const attrs = [
|
|
918
|
-
{
|
|
919
|
-
name: ATTR_ID,
|
|
920
|
-
value: r.id
|
|
921
|
-
},
|
|
922
|
-
{
|
|
923
|
-
name: ATTR_TYPE,
|
|
924
|
-
value: r.type
|
|
925
|
-
},
|
|
926
|
-
{
|
|
927
|
-
name: ATTR_TARGET,
|
|
928
|
-
value: r.target
|
|
929
|
-
}
|
|
930
|
-
];
|
|
931
|
-
if (r.targetMode === "External") attrs.push({
|
|
932
|
-
name: ATTR_TARGET_MODE,
|
|
933
|
-
value: "External"
|
|
934
|
-
});
|
|
935
|
-
return elem(NAME_RELATIONSHIP, { attrs });
|
|
936
|
-
});
|
|
937
|
-
return serializeXml({
|
|
938
|
-
kind: "document",
|
|
939
|
-
decl: {
|
|
940
|
-
version: "1.0",
|
|
941
|
-
encoding: "UTF-8",
|
|
942
|
-
standalone: "yes"
|
|
943
|
-
},
|
|
944
|
-
prolog: [],
|
|
945
|
-
root: elem(NAME_RELATIONSHIPS, {
|
|
946
|
-
prefixDecls: new Map([["", REL_NS]]),
|
|
947
|
-
children
|
|
948
|
-
}),
|
|
949
|
-
epilog: []
|
|
950
|
-
});
|
|
951
|
-
};
|
|
952
|
-
/**
|
|
953
|
-
* Convenience: an empty Relationships value with no items.
|
|
954
|
-
*/
|
|
955
|
-
const emptyRels = () => ({ items: [] });
|
|
956
|
-
//#endregion
|
|
957
|
-
//#region src/fingerprint.ts
|
|
958
|
-
const CORE_PROPERTIES = "docProps/core.xml";
|
|
959
|
-
const CORE_TIMESTAMPS = /<(dcterms:(?:created|modified))\b[^>]*>[\s\S]*?<\/\1>/g;
|
|
960
|
-
/** Every build stamps fresh creation and modification times into the core properties. */
|
|
961
|
-
function comparablePart(name, bytes) {
|
|
962
|
-
if (name !== CORE_PROPERTIES) return bytes;
|
|
963
|
-
return strToU8(strFromU8(bytes).replace(CORE_TIMESTAMPS, ""));
|
|
964
|
-
}
|
|
965
|
-
/** ZIP container dates and generated core timestamps are not source edits. */
|
|
966
|
-
function sourceFingerprint(bytes) {
|
|
967
|
-
const parts = unzipSync(bytes);
|
|
968
|
-
const hash = createHash("sha256");
|
|
969
|
-
for (const name of Object.keys(parts).sort()) {
|
|
970
|
-
const content = comparablePart(name, parts[name]);
|
|
971
|
-
hash.update(JSON.stringify([name, createHash("sha256").update(content).digest("hex")]));
|
|
972
|
-
}
|
|
973
|
-
return hash.digest("hex");
|
|
974
|
-
}
|
|
975
|
-
//#endregion
|
|
976
|
-
//#region src/deck-merge.ts
|
|
977
|
-
const CONTENT_TYPES = "[Content_Types].xml";
|
|
978
|
-
const PRESENTATION = "ppt/presentation.xml";
|
|
979
|
-
const SHAPE_TREE_PART = /^ppt\/(?:slides|slideLayouts|slideMasters|notesSlides|notesMasters|handoutMasters)\/[^/]+\.xml$/;
|
|
980
|
-
const RELATIONSHIPS_PART = /(?:^|\/)_rels\/[^/]*\.rels$/;
|
|
981
|
-
const SLIDE_RELATIONSHIP = `${NS.officeDocRels}/slide`;
|
|
982
|
-
const NOTES_RELATIONSHIP = `${NS.officeDocRels}/notesSlide`;
|
|
983
|
-
const SHAPE_TREE_PROPERTIES = new Set([
|
|
984
|
-
"nvGrpSpPr",
|
|
985
|
-
"grpSpPr",
|
|
986
|
-
"extLst"
|
|
987
|
-
]);
|
|
988
|
-
function sameBytes(a, b) {
|
|
989
|
-
if (a.length !== b.length) return false;
|
|
990
|
-
for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
|
|
991
|
-
return true;
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Three-way merge of keyed items. An item changed on one side only takes that
|
|
995
|
-
* side; an item changed differently on both sides is a conflict.
|
|
996
|
-
*/
|
|
997
|
-
function mergeEntries(base, ours, theirs, options) {
|
|
998
|
-
const byKey = (entries) => new Map(entries.map((entry) => [entry.key, entry]));
|
|
999
|
-
const [b, o, t] = [
|
|
1000
|
-
byKey(base),
|
|
1001
|
-
byKey(ours),
|
|
1002
|
-
byKey(theirs)
|
|
1003
|
-
];
|
|
1004
|
-
const keys = new Set([
|
|
1005
|
-
...t.keys(),
|
|
1006
|
-
...o.keys(),
|
|
1007
|
-
...b.keys()
|
|
1008
|
-
]);
|
|
1009
|
-
const chosen = /* @__PURE__ */ new Map();
|
|
1010
|
-
const conflicts = [];
|
|
1011
|
-
const same = (x, y) => x?.value === y?.value;
|
|
1012
|
-
for (const key of keys) {
|
|
1013
|
-
const [be, oe, te] = [
|
|
1014
|
-
b.get(key),
|
|
1015
|
-
o.get(key),
|
|
1016
|
-
t.get(key)
|
|
1017
|
-
];
|
|
1018
|
-
let pick;
|
|
1019
|
-
if (same(oe, be)) pick = te;
|
|
1020
|
-
else if (same(te, be) || same(oe, te)) pick = oe;
|
|
1021
|
-
else {
|
|
1022
|
-
const refined = be && oe && te ? options.refine?.(key, be.item, oe.item, te.item) : null;
|
|
1023
|
-
if (refined?.ok) chosen.set(key, refined.value);
|
|
1024
|
-
else if (refined) conflicts.push(...refined.conflicts);
|
|
1025
|
-
else {
|
|
1026
|
-
const reason = !be ? "both-added" : !oe || !te ? "deleted-and-changed" : "both-changed";
|
|
1027
|
-
conflicts.push(options.conflict(key, reason, (oe ?? te ?? be).item));
|
|
1028
|
-
}
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
if (pick) chosen.set(key, pick.item);
|
|
1032
|
-
}
|
|
1033
|
-
if (conflicts.length) return {
|
|
1034
|
-
ok: false,
|
|
1035
|
-
conflicts
|
|
1036
|
-
};
|
|
1037
|
-
const order = mergeOrder(base.map((entry) => entry.key), ours.map((entry) => entry.key), theirs.map((entry) => entry.key), new Set(chosen.keys()), options.ordering);
|
|
1038
|
-
if (!order) return {
|
|
1039
|
-
ok: false,
|
|
1040
|
-
conflicts: [options.conflict("", "both-changed")]
|
|
1041
|
-
};
|
|
1042
|
-
return {
|
|
1043
|
-
ok: true,
|
|
1044
|
-
value: order.map((key) => chosen.get(key))
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
/** True when the keys both lists share appear in the same order in each. */
|
|
1048
|
-
function sameRelativeOrder(a, b) {
|
|
1049
|
-
const [inA, inB] = [new Set(a), new Set(b)];
|
|
1050
|
-
const sharedA = a.filter((key) => inB.has(key));
|
|
1051
|
-
const sharedB = b.filter((key) => inA.has(key));
|
|
1052
|
-
return sharedA.every((key, index) => key === sharedB[index]);
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Orders the kept keys. The side that reordered (or the source when neither
|
|
1056
|
-
* did) gives the order; the other side's additions follow their nearest
|
|
1057
|
-
* preceding sibling. `null` means the order cannot be decided.
|
|
1058
|
-
*/
|
|
1059
|
-
function mergeOrder(base, ours, theirs, keep, ordering) {
|
|
1060
|
-
const oursMoved = ordering !== "none" && !sameRelativeOrder(base, ours);
|
|
1061
|
-
const theirsMoved = ordering !== "none" && !sameRelativeOrder(base, theirs);
|
|
1062
|
-
if (oursMoved && theirsMoved && !sameRelativeOrder(ours, theirs)) return null;
|
|
1063
|
-
const [primary, secondary] = oursMoved ? [ours, theirs] : [theirs, ours];
|
|
1064
|
-
const kept = primary.filter((key) => keep.has(key));
|
|
1065
|
-
const placed = new Set(kept);
|
|
1066
|
-
const inSecondary = new Set(secondary);
|
|
1067
|
-
const next = new Map(kept.map((key, index) => [index === 0 ? null : kept[index - 1], key]));
|
|
1068
|
-
if (kept.length) next.set(kept.at(-1), void 0);
|
|
1069
|
-
const after = /* @__PURE__ */ new Map();
|
|
1070
|
-
let anchor = null;
|
|
1071
|
-
let chained = false;
|
|
1072
|
-
for (const key of secondary) {
|
|
1073
|
-
if (!keep.has(key)) continue;
|
|
1074
|
-
if (placed.has(key)) {
|
|
1075
|
-
anchor = key;
|
|
1076
|
-
chained = false;
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
if (!chained) {
|
|
1080
|
-
const following = next.get(anchor);
|
|
1081
|
-
if (ordering === "schema" && following !== void 0 && !inSecondary.has(following)) return null;
|
|
1082
|
-
}
|
|
1083
|
-
after.set(anchor, key);
|
|
1084
|
-
anchor = key;
|
|
1085
|
-
chained = true;
|
|
1086
|
-
}
|
|
1087
|
-
const result = [];
|
|
1088
|
-
const emit = (key) => {
|
|
1089
|
-
for (let at = key; at !== void 0; at = after.get(at)) if (at !== null) result.push(at);
|
|
1090
|
-
};
|
|
1091
|
-
emit(null);
|
|
1092
|
-
for (const key of kept) emit(key);
|
|
1093
|
-
return result;
|
|
1094
|
-
}
|
|
1095
|
-
const partConflict = (context, reason, shape = null) => ({
|
|
1096
|
-
part: context.part,
|
|
1097
|
-
slide: null,
|
|
1098
|
-
shape,
|
|
1099
|
-
reason
|
|
1100
|
-
});
|
|
1101
|
-
function shapeIdentity(element) {
|
|
1102
|
-
let found;
|
|
1103
|
-
walkElements(element, (child) => {
|
|
1104
|
-
if (found) return false;
|
|
1105
|
-
if (child.name.localName === "cNvPr") found = child;
|
|
1106
|
-
});
|
|
1107
|
-
if (!found) return null;
|
|
1108
|
-
const value = (name) => found.attrs.find((item) => item.name.localName === name)?.value;
|
|
1109
|
-
const id = value("id");
|
|
1110
|
-
return id === void 0 ? null : {
|
|
1111
|
-
id,
|
|
1112
|
-
name: value("name") ?? ""
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
const elementKey = (element) => `${element.name.namespaceURI} ${element.name.localName}`;
|
|
1116
|
-
/** Keys an element's children, or `null` when they cannot be told apart. */
|
|
1117
|
-
function childEntries(element, kind) {
|
|
1118
|
-
const entries = [];
|
|
1119
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1120
|
-
const occurrences = /* @__PURE__ */ new Map();
|
|
1121
|
-
for (const child of element.children) {
|
|
1122
|
-
if (child.kind !== "element") {
|
|
1123
|
-
if (child.kind === "text" && !child.data.trim()) continue;
|
|
1124
|
-
return null;
|
|
1125
|
-
}
|
|
1126
|
-
let key;
|
|
1127
|
-
if (kind === "spTree" && !(child.name.namespaceURI === NS.pml && SHAPE_TREE_PROPERTIES.has(child.name.localName))) {
|
|
1128
|
-
const identity = shapeIdentity(child);
|
|
1129
|
-
if (!identity) return null;
|
|
1130
|
-
key = `shape ${identity.id}`;
|
|
1131
|
-
} else {
|
|
1132
|
-
const name = elementKey(child);
|
|
1133
|
-
const occurrence = occurrences.get(name) ?? 0;
|
|
1134
|
-
occurrences.set(name, occurrence + 1);
|
|
1135
|
-
key = `${name} ${occurrence}`;
|
|
1136
|
-
}
|
|
1137
|
-
if (keys.has(key)) return null;
|
|
1138
|
-
keys.add(key);
|
|
1139
|
-
entries.push({
|
|
1140
|
-
key,
|
|
1141
|
-
value: serializeFragment(child),
|
|
1142
|
-
item: child
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
return entries;
|
|
1146
|
-
}
|
|
1147
|
-
const attributesValue = (attrs) => JSON.stringify(attrs.map((item) => [
|
|
1148
|
-
item.name.namespaceURI,
|
|
1149
|
-
item.name.localName,
|
|
1150
|
-
item.value
|
|
1151
|
-
]));
|
|
1152
|
-
const NESTED = {
|
|
1153
|
-
root: {
|
|
1154
|
-
localName: "cSld",
|
|
1155
|
-
kind: "cSld"
|
|
1156
|
-
},
|
|
1157
|
-
cSld: {
|
|
1158
|
-
localName: "spTree",
|
|
1159
|
-
kind: "spTree"
|
|
1160
|
-
}
|
|
1161
|
-
};
|
|
1162
|
-
/** Merges a slide-like element: its attributes as one unit, its children by key. */
|
|
1163
|
-
function mergeContainer(context, base, ours, theirs, kind) {
|
|
1164
|
-
const conflicts = [];
|
|
1165
|
-
const [ba, oa, ta] = [
|
|
1166
|
-
base.attrs,
|
|
1167
|
-
ours.attrs,
|
|
1168
|
-
theirs.attrs
|
|
1169
|
-
].map(attributesValue);
|
|
1170
|
-
let attrs = theirs.attrs;
|
|
1171
|
-
if (oa !== ba && (ta === ba || oa === ta)) attrs = ours.attrs;
|
|
1172
|
-
else if (oa !== ba) conflicts.push(partConflict(context, "both-changed"));
|
|
1173
|
-
const prefixDecls = new Map(theirs.prefixDecls);
|
|
1174
|
-
for (const [prefix, uri] of ours.prefixDecls) {
|
|
1175
|
-
const existing = prefixDecls.get(prefix);
|
|
1176
|
-
if (existing === void 0) prefixDecls.set(prefix, uri);
|
|
1177
|
-
else if (existing !== uri) conflicts.push(partConflict(context, "both-changed"));
|
|
1178
|
-
}
|
|
1179
|
-
const entries = [
|
|
1180
|
-
base,
|
|
1181
|
-
ours,
|
|
1182
|
-
theirs
|
|
1183
|
-
].map((element) => childEntries(element, kind));
|
|
1184
|
-
if (entries.some((list) => !list)) conflicts.push(partConflict(context, "both-changed"));
|
|
1185
|
-
if (conflicts.length) return {
|
|
1186
|
-
ok: false,
|
|
1187
|
-
conflicts
|
|
1188
|
-
};
|
|
1189
|
-
const nested = NESTED[kind];
|
|
1190
|
-
const children = mergeEntries(entries[0], entries[1], entries[2], {
|
|
1191
|
-
ordering: kind === "spTree" ? "free" : "schema",
|
|
1192
|
-
refine: (_key, b, o, t) => nested && t.name.namespaceURI === NS.pml && t.name.localName === nested.localName ? mergeContainer(context, b, o, t, nested.kind) : null,
|
|
1193
|
-
conflict: (key, reason, sample) => partConflict(context, reason, sample && key.startsWith("shape ") ? shapeIdentity(sample) : null)
|
|
1194
|
-
});
|
|
1195
|
-
if (!children.ok) return children;
|
|
1196
|
-
return {
|
|
1197
|
-
ok: true,
|
|
1198
|
-
value: elem(theirs.name, {
|
|
1199
|
-
attrs,
|
|
1200
|
-
prefixDecls,
|
|
1201
|
-
children: children.value
|
|
1202
|
-
})
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
function mergeShapeTreePart(context, base, ours, theirs) {
|
|
1206
|
-
const [b, o, t] = [
|
|
1207
|
-
base,
|
|
1208
|
-
ours,
|
|
1209
|
-
theirs
|
|
1210
|
-
].map((bytes) => parseXml(strFromU8(bytes)));
|
|
1211
|
-
const root = mergeContainer(context, b.root, o.root, t.root, "root");
|
|
1212
|
-
if (!root.ok) return root;
|
|
1213
|
-
return {
|
|
1214
|
-
ok: true,
|
|
1215
|
-
value: strToU8(serializeXml({
|
|
1216
|
-
...t,
|
|
1217
|
-
root: root.value
|
|
1218
|
-
}))
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
function relationshipEntries(bytes) {
|
|
1222
|
-
return (bytes ? parseRels(strFromU8(bytes)) : emptyRels()).items.map((item) => ({
|
|
1223
|
-
key: item.id,
|
|
1224
|
-
value: JSON.stringify([
|
|
1225
|
-
item.type,
|
|
1226
|
-
item.target,
|
|
1227
|
-
item.targetMode
|
|
1228
|
-
]),
|
|
1229
|
-
item
|
|
1230
|
-
}));
|
|
1231
|
-
}
|
|
1232
|
-
function mergeRelationshipsPart(context, base, ours, theirs) {
|
|
1233
|
-
const items = mergeEntries(relationshipEntries(base), relationshipEntries(ours), relationshipEntries(theirs), {
|
|
1234
|
-
ordering: "none",
|
|
1235
|
-
conflict: (_key, reason) => partConflict(context, reason)
|
|
1236
|
-
});
|
|
1237
|
-
if (!items.ok) return items;
|
|
1238
|
-
return {
|
|
1239
|
-
ok: true,
|
|
1240
|
-
value: strToU8(serializeRels({ items: items.value }))
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* Content types are declarations about parts, so both sides' entries are kept
|
|
1245
|
-
* and only overrides for parts the merged package no longer has are dropped.
|
|
1246
|
-
*/
|
|
1247
|
-
function mergeContentTypes(ours, theirs, parts) {
|
|
1248
|
-
const [o, t] = [ours, theirs].map((bytes) => parseContentTypes(strFromU8(bytes)));
|
|
1249
|
-
const merged = {
|
|
1250
|
-
defaults: [],
|
|
1251
|
-
overrides: []
|
|
1252
|
-
};
|
|
1253
|
-
const defaults = /* @__PURE__ */ new Map();
|
|
1254
|
-
const overrides = /* @__PURE__ */ new Map();
|
|
1255
|
-
const conflicts = [];
|
|
1256
|
-
const context = { part: CONTENT_TYPES };
|
|
1257
|
-
for (const types of [t, o]) {
|
|
1258
|
-
for (const item of types.defaults) {
|
|
1259
|
-
const existing = defaults.get(item.extension);
|
|
1260
|
-
if (existing === void 0) {
|
|
1261
|
-
defaults.set(item.extension, item.contentType);
|
|
1262
|
-
merged.defaults.push(item);
|
|
1263
|
-
} else if (existing !== item.contentType) conflicts.push(partConflict(context, "both-changed"));
|
|
1264
|
-
}
|
|
1265
|
-
for (const item of types.overrides) {
|
|
1266
|
-
const key = item.partName.toLowerCase();
|
|
1267
|
-
if (!parts.has(key)) continue;
|
|
1268
|
-
const existing = overrides.get(key);
|
|
1269
|
-
if (existing === void 0) {
|
|
1270
|
-
overrides.set(key, item.contentType);
|
|
1271
|
-
merged.overrides.push(item);
|
|
1272
|
-
} else if (existing !== item.contentType) conflicts.push(partConflict(context, "both-changed"));
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (conflicts.length) return {
|
|
1276
|
-
ok: false,
|
|
1277
|
-
conflicts
|
|
1278
|
-
};
|
|
1279
|
-
return {
|
|
1280
|
-
ok: true,
|
|
1281
|
-
value: strToU8(serializeContentTypes(merged))
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
/** Maps each slide's parts (the slide, its notes and their relationships) to its number. */
|
|
1285
|
-
function slideNumbers(parts) {
|
|
1286
|
-
const numbers = /* @__PURE__ */ new Map();
|
|
1287
|
-
const presentation = parts.get(PRESENTATION);
|
|
1288
|
-
const presentationRels = parts.get("ppt/_rels/presentation.xml.rels");
|
|
1289
|
-
if (!presentation || !presentationRels) return numbers;
|
|
1290
|
-
const targets = new Map(parseRels(strFromU8(presentationRels)).items.filter((item) => item.type === SLIDE_RELATIONSHIP).map((item) => [item.id, toZipPath(resolveTarget(fromZipPath(PRESENTATION), item.target))]));
|
|
1291
|
-
let number = 0;
|
|
1292
|
-
walkElements(parseXml(strFromU8(presentation)).root, (element) => {
|
|
1293
|
-
if (element.name.localName !== "sldId") return void 0;
|
|
1294
|
-
number++;
|
|
1295
|
-
const id = element.attrs.find((item) => item.name.namespaceURI === NS.officeDocRels && item.name.localName === "id")?.value;
|
|
1296
|
-
const slide = id === void 0 ? void 0 : targets.get(id);
|
|
1297
|
-
if (!slide) return false;
|
|
1298
|
-
const relsPart = relsName(slide);
|
|
1299
|
-
numbers.set(slide, number).set(relsPart, number);
|
|
1300
|
-
const rels = parts.get(relsPart);
|
|
1301
|
-
if (!rels) return false;
|
|
1302
|
-
for (const item of parseRels(strFromU8(rels)).items) {
|
|
1303
|
-
if (item.type !== NOTES_RELATIONSHIP) continue;
|
|
1304
|
-
const notes = toZipPath(resolveTarget(fromZipPath(slide), item.target));
|
|
1305
|
-
numbers.set(notes, number).set(relsName(notes), number);
|
|
1306
|
-
}
|
|
1307
|
-
return false;
|
|
1308
|
-
});
|
|
1309
|
-
return numbers;
|
|
1310
|
-
}
|
|
1311
|
-
const relsName = (part) => {
|
|
1312
|
-
const slash = part.lastIndexOf("/");
|
|
1313
|
-
return `${part.slice(0, slash + 1)}_rels/${part.slice(slash + 1)}.rels`;
|
|
1314
|
-
};
|
|
1315
|
-
/**
|
|
1316
|
-
* Both sides allocate the next free relationship ID, so a relationship the
|
|
1317
|
-
* editor added and a different one the source added often share an ID. IDs
|
|
1318
|
-
* are private to a part and its relationships, so the editor's is renamed
|
|
1319
|
-
* (in its relationships and in the part referring to it) instead of
|
|
1320
|
-
* reporting a conflict.
|
|
1321
|
-
*/
|
|
1322
|
-
function renumberAddedRelationships(base, ours, theirs) {
|
|
1323
|
-
for (const [name, bytes] of ours) {
|
|
1324
|
-
const owner = relationshipsOwner(name);
|
|
1325
|
-
const theirsBytes = theirs.get(name);
|
|
1326
|
-
const ownerBytes = owner && ours.get(owner);
|
|
1327
|
-
if (!ownerBytes || !theirsBytes) continue;
|
|
1328
|
-
const parse = (value) => value ? parseRels(strFromU8(value)) : emptyRels();
|
|
1329
|
-
const [b, o, t] = [
|
|
1330
|
-
parse(base.get(name)),
|
|
1331
|
-
parse(bytes),
|
|
1332
|
-
parse(theirsBytes)
|
|
1333
|
-
];
|
|
1334
|
-
const inBase = new Set(b.items.map((item) => item.id));
|
|
1335
|
-
const theirsById = new Map(t.items.map((item) => [item.id, item]));
|
|
1336
|
-
const used = new Set([
|
|
1337
|
-
...inBase,
|
|
1338
|
-
...theirsById.keys(),
|
|
1339
|
-
...o.items.map((item) => item.id)
|
|
1340
|
-
]);
|
|
1341
|
-
const renamed = /* @__PURE__ */ new Map();
|
|
1342
|
-
let next = 1;
|
|
1343
|
-
for (const item of o.items) {
|
|
1344
|
-
const other = theirsById.get(item.id);
|
|
1345
|
-
if (inBase.has(item.id) || !other || sameRelationship(item, other)) continue;
|
|
1346
|
-
while (used.has(`rId${next}`)) next++;
|
|
1347
|
-
renamed.set(item.id, `rId${next}`);
|
|
1348
|
-
used.add(`rId${next}`);
|
|
1349
|
-
item.id = `rId${next}`;
|
|
1350
|
-
}
|
|
1351
|
-
if (!renamed.size) continue;
|
|
1352
|
-
ours.set(name, strToU8(serializeRels(o)));
|
|
1353
|
-
const document = parseXml(strFromU8(ownerBytes));
|
|
1354
|
-
walkElements(document.root, (element) => {
|
|
1355
|
-
element.attrs = element.attrs.map((item) => item.name.namespaceURI === NS.officeDocRels && renamed.has(item.value) ? {
|
|
1356
|
-
name: item.name,
|
|
1357
|
-
value: renamed.get(item.value)
|
|
1358
|
-
} : item);
|
|
1359
|
-
});
|
|
1360
|
-
ours.set(owner, strToU8(serializeXml(document)));
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
const sameRelationship = (a, b) => a.type === b.type && a.target === b.target && a.targetMode === b.targetMode;
|
|
1364
|
-
/** The part whose relationships `name` holds; `null` for the package's own. */
|
|
1365
|
-
function relationshipsOwner(name) {
|
|
1366
|
-
const match = /^(.*?)_rels\/([^/]+)\.rels$/.exec(name);
|
|
1367
|
-
return match ? `${match[1]}${match[2]}` : null;
|
|
1368
|
-
}
|
|
1369
|
-
function unzip(bytes) {
|
|
1370
|
-
return new Map(readZip(bytes).entries.map((entry) => [entry.name, entry.data]));
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* Three-way merge of presentation packages: `base` is the source build the
|
|
1374
|
-
* editor's copy was made from, `ours` the editor's copy and `theirs` the new
|
|
1375
|
-
* source build. Changes on different parts, shapes or relationships combine;
|
|
1376
|
-
* the same item changed differently on both sides is reported as a conflict.
|
|
1377
|
-
*/
|
|
1378
|
-
function mergeDecks(base, ours, theirs) {
|
|
1379
|
-
const [b, o, t] = [
|
|
1380
|
-
unzip(base),
|
|
1381
|
-
unzip(ours),
|
|
1382
|
-
unzip(theirs)
|
|
1383
|
-
];
|
|
1384
|
-
renumberAddedRelationships(b, o, t);
|
|
1385
|
-
const names = new Set([
|
|
1386
|
-
...t.keys(),
|
|
1387
|
-
...o.keys(),
|
|
1388
|
-
...b.keys()
|
|
1389
|
-
]);
|
|
1390
|
-
const result = /* @__PURE__ */ new Map();
|
|
1391
|
-
const conflicts = [];
|
|
1392
|
-
const same = (name, x, y) => x === void 0 || y === void 0 ? x === y : sameBytes(comparablePart(name, x), comparablePart(name, y));
|
|
1393
|
-
for (const name of names) {
|
|
1394
|
-
if (name === CONTENT_TYPES) continue;
|
|
1395
|
-
const [bp, op, tp] = [
|
|
1396
|
-
b.get(name),
|
|
1397
|
-
o.get(name),
|
|
1398
|
-
t.get(name)
|
|
1399
|
-
];
|
|
1400
|
-
let pick;
|
|
1401
|
-
if (same(name, op, bp)) pick = tp;
|
|
1402
|
-
else if (same(name, tp, bp) || same(name, op, tp)) pick = op;
|
|
1403
|
-
else {
|
|
1404
|
-
const context = { part: name };
|
|
1405
|
-
let merged = null;
|
|
1406
|
-
if (op && tp && RELATIONSHIPS_PART.test(name)) merged = mergeRelationshipsPart(context, bp, op, tp);
|
|
1407
|
-
else if (bp && op && tp && SHAPE_TREE_PART.test(name)) merged = mergeShapeTreePart(context, bp, op, tp);
|
|
1408
|
-
if (merged?.ok) result.set(name, merged.value);
|
|
1409
|
-
else if (merged) conflicts.push(...merged.conflicts);
|
|
1410
|
-
else conflicts.push(partConflict(context, !bp ? "both-added" : !op || !tp ? "deleted-and-changed" : "both-changed"));
|
|
1411
|
-
continue;
|
|
1412
|
-
}
|
|
1413
|
-
if (pick) result.set(name, pick);
|
|
1414
|
-
}
|
|
1415
|
-
const [bt, ot, tt] = [
|
|
1416
|
-
b,
|
|
1417
|
-
o,
|
|
1418
|
-
t
|
|
1419
|
-
].map((parts) => parts.get(CONTENT_TYPES));
|
|
1420
|
-
let contentTypes;
|
|
1421
|
-
if (same(CONTENT_TYPES, ot, bt)) contentTypes = tt;
|
|
1422
|
-
else if (same(CONTENT_TYPES, tt, bt) || same(CONTENT_TYPES, ot, tt)) contentTypes = ot;
|
|
1423
|
-
else {
|
|
1424
|
-
const merged = mergeContentTypes(ot, tt, new Set([...result.keys()].map((name) => `/${name}`.toLowerCase())));
|
|
1425
|
-
if (merged.ok) contentTypes = merged.value;
|
|
1426
|
-
else conflicts.push(...merged.conflicts);
|
|
1427
|
-
}
|
|
1428
|
-
if (conflicts.length) {
|
|
1429
|
-
const numbers = [
|
|
1430
|
-
b,
|
|
1431
|
-
t,
|
|
1432
|
-
o
|
|
1433
|
-
].reduce((all, parts) => new Map([...all, ...slideNumbers(parts)]), /* @__PURE__ */ new Map());
|
|
1434
|
-
return {
|
|
1435
|
-
ok: false,
|
|
1436
|
-
conflicts: conflicts.map((conflict) => ({
|
|
1437
|
-
...conflict,
|
|
1438
|
-
slide: numbers.get(conflict.part) ?? null
|
|
1439
|
-
}))
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
return {
|
|
1443
|
-
ok: true,
|
|
1444
|
-
bytes: writeZip([{
|
|
1445
|
-
name: CONTENT_TYPES,
|
|
1446
|
-
data: contentTypes
|
|
1447
|
-
}, ...[...result].map(([name, data]) => ({
|
|
1448
|
-
name,
|
|
1449
|
-
data
|
|
1450
|
-
}))])
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
/** One line per conflict, for the CLI and server messages. */
|
|
1454
|
-
function describeConflict(conflict) {
|
|
1455
|
-
const where = conflict.slide === null ? conflict.part : `Slide ${conflict.slide}`;
|
|
1456
|
-
const what = conflict.shape ? `${where}: ${conflict.shape.name || `shape ${conflict.shape.id}`}` : where;
|
|
1457
|
-
return conflict.reason === "deleted-and-changed" ? `${what} was deleted on one side and changed on the other.` : `${what} was changed both in the editor and in the source.`;
|
|
1458
|
-
}
|
|
1459
|
-
//#endregion
|
|
1460
|
-
//#region src/editor-store.ts
|
|
1461
|
-
const STORE_VERSION = 2;
|
|
1462
|
-
/** One atomic sidecar keeps the saved deck and its source basis in agreement. */
|
|
1463
|
-
function editorStore(entry) {
|
|
1464
|
-
const path = join(dirname(resolve(entry)), ".office-kit", `${basename(entry)}.editor.zip`);
|
|
1465
|
-
return {
|
|
1466
|
-
path,
|
|
1467
|
-
async read() {
|
|
1468
|
-
let bytes;
|
|
1469
|
-
try {
|
|
1470
|
-
bytes = await readFile(path);
|
|
1471
|
-
} catch (cause) {
|
|
1472
|
-
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return;
|
|
1473
|
-
throw cause;
|
|
1474
|
-
}
|
|
1475
|
-
const parts = unzipSync(bytes);
|
|
1476
|
-
if (!parts["state.json"] || !parts["document.pptx"]) throw new Error(`Invalid editor file: ${path}`);
|
|
1477
|
-
const state = JSON.parse(strFromU8(parts["state.json"]));
|
|
1478
|
-
if (!state || typeof state !== "object" || !("version" in state) || state.version !== 1 && state.version !== STORE_VERSION || !("sourceHash" in state) || typeof state.sourceHash !== "string" || !/^[a-f0-9]{64}$/.test(state.sourceHash)) throw new Error(`Invalid editor metadata: ${path}`);
|
|
1479
|
-
const base = parts["base.pptx"];
|
|
1480
|
-
if (state.version === STORE_VERSION && !base) throw new Error(`Invalid editor file: ${path}`);
|
|
1481
|
-
return {
|
|
1482
|
-
sourceHash: state.sourceHash,
|
|
1483
|
-
bytes: parts["document.pptx"],
|
|
1484
|
-
base: base ?? null
|
|
1485
|
-
};
|
|
1486
|
-
},
|
|
1487
|
-
async write(edits) {
|
|
1488
|
-
await mkdir(dirname(path), { recursive: true });
|
|
1489
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1490
|
-
try {
|
|
1491
|
-
await writeFile(temporary, zipSync({
|
|
1492
|
-
"state.json": strToU8(JSON.stringify({
|
|
1493
|
-
version: STORE_VERSION,
|
|
1494
|
-
sourceHash: edits.sourceHash
|
|
1495
|
-
})),
|
|
1496
|
-
"document.pptx": edits.bytes,
|
|
1497
|
-
"base.pptx": edits.base
|
|
1498
|
-
}), { flag: "wx" });
|
|
1499
|
-
await rename(temporary, path);
|
|
1500
|
-
} finally {
|
|
1501
|
-
await rm(temporary, { force: true });
|
|
1502
|
-
}
|
|
1503
|
-
},
|
|
1504
|
-
async clear() {
|
|
1505
|
-
await rm(path, { force: true });
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
async function applySavedEdits(entry, source) {
|
|
1510
|
-
const edits = await editorStore(entry).read();
|
|
1511
|
-
if (!edits) return source;
|
|
1512
|
-
let bytes = edits.bytes;
|
|
1513
|
-
if (edits.sourceHash !== sourceFingerprint(source.bytes)) {
|
|
1514
|
-
const merged = edits.base ? mergeDecks(edits.base, edits.bytes, source.bytes) : null;
|
|
1515
|
-
if (!merged?.ok) throw new Error(["Source changed since the last editor save. Open office-pptx dev and resolve the editor conflict before exporting.", ...merged?.conflicts.map(describeConflict) ?? []].join("\n"));
|
|
1516
|
-
bytes = merged.bytes;
|
|
1517
|
-
}
|
|
1518
|
-
return (await renderDeck(bytes, source.dependencies)).result;
|
|
1519
|
-
}
|
|
1520
|
-
//#endregion
|
|
1521
|
-
//#region src/init.ts
|
|
1522
|
-
const starter = `import { Presentation } from '@office-kit/pptx-dsl';
|
|
1523
|
-
import { Cover } from './slides/cover.tsx';
|
|
1524
|
-
|
|
1525
|
-
export default (
|
|
1526
|
-
<Presentation>
|
|
1527
|
-
<Cover />
|
|
1528
|
-
</Presentation>
|
|
1529
|
-
);
|
|
1530
|
-
`;
|
|
1531
|
-
const theme = `export const theme = {
|
|
1532
|
-
background: '#15171C',
|
|
1533
|
-
accent: '#D6336C',
|
|
1534
|
-
text: '#FFFFFF',
|
|
1535
|
-
muted: '#B4B9C4',
|
|
1536
|
-
} as const;
|
|
1537
|
-
`;
|
|
1538
|
-
const cover = `import { Slide, Text, Shape } from '@office-kit/pptx-dsl';
|
|
1539
|
-
import { theme } from '../theme.ts';
|
|
1540
|
-
|
|
1541
|
-
export function Cover() {
|
|
1542
|
-
return (
|
|
1543
|
-
<Slide background={theme.background}>
|
|
1544
|
-
<Shape preset="rect" x={0.9} y={2.55} width={0.14} height={1.75} fill={theme.accent} stroke={false} />
|
|
1545
|
-
<Text x={1.25} y={2.4} width={10} height={1.1} size={48} bold color={theme.text}>
|
|
1546
|
-
Your next presentation
|
|
1547
|
-
</Text>
|
|
1548
|
-
<Text x={1.25} y={3.55} width={10} height={0.6} size={22} color={theme.muted}>
|
|
1549
|
-
Edit this TSX and save to update the preview.
|
|
1550
|
-
</Text>
|
|
1551
|
-
</Slide>
|
|
1552
|
-
);
|
|
1553
|
-
}
|
|
1554
|
-
`;
|
|
1555
|
-
const guide = `# Writing this presentation
|
|
1556
|
-
|
|
1557
|
-
Use deck.tsx for slide order, slides/*.tsx for individual slides, and theme.ts
|
|
1558
|
-
for shared design values. The JSX runtime is
|
|
1559
|
-
@office-kit/pptx-dsl, with no React or Vue dependency.
|
|
1560
|
-
|
|
1561
|
-
- Run npm run dev, then open its local URL to preview. Saving source files
|
|
1562
|
-
updates the preview. A failed build retains the last successful output.
|
|
1563
|
-
- Select an area in the preview to request a focused AI edit, or use Edit text
|
|
1564
|
-
for a direct text save. Changes are persisted in TSX. Use the thumbnails, zoom
|
|
1565
|
-
and Present to review slides. After edits, inspect every screenshot supplied
|
|
1566
|
-
by the preview and correct clipping, overlap, spacing and contrast.
|
|
1567
|
-
- For a local revision, locate the slide through deck.tsx or a text search and
|
|
1568
|
-
read only the relevant source and dependencies. Patch the requested text, data
|
|
1569
|
-
or props; do not regenerate the deck or reformat unrelated code. Keep descriptive
|
|
1570
|
-
filenames stable when reordering slides. Preserve existing project structures.
|
|
1571
|
-
- Change shared theme values/components only for changes intended for all their
|
|
1572
|
-
consumers; use a local prop override for a one-slide exception.
|
|
1573
|
-
- Keep the preview running during revisions and inspect affected slides after
|
|
1574
|
-
saving. Review all affected consumers for shared changes. Do not run a separate
|
|
1575
|
-
export for every intermediate edit. Before delivery, run npm run check and
|
|
1576
|
-
npm run build to export deck.pptx, then review the whole deck.
|
|
1577
|
-
- Coordinates and dimensions are inches; font sizes and stroke widths are points.
|
|
1578
|
-
The default 16:9 canvas is 13.333 × 7.5 inches.
|
|
1579
|
-
- Use native Text, Shape, Image, Table and Chart elements. Use JS functions,
|
|
1580
|
-
arrays and map to compose elements; do not import React.
|
|
1581
|
-
- Chart takes the core ChartSpec in its spec prop. Text accepts rich ParagraphSpec
|
|
1582
|
-
arrays through paragraphs. Raw is an escape hatch for public core APIs.
|
|
1583
|
-
- For templates: readFile(new URL('./template.pptx', import.meta.url)), then use
|
|
1584
|
-
Presentation source={bytes} mode="edit" and Slide target={{index: 0}}.
|
|
1585
|
-
Fill target={{name: 'Title 1'}} changes an existing shape. Indices are zero-based.
|
|
1586
|
-
Use office-pptx inspect template.pptx to discover names, layouts and placeholders.
|
|
1587
|
-
- Use mode="compose" and Slide from={{index: 0}} to build a new sequence from a
|
|
1588
|
-
source deck. This intentionally removes the original slide sequence.
|
|
1589
|
-
- Keep assets inside this project so the dev server sees changes. Use source-relative
|
|
1590
|
-
new URL('./image.png', import.meta.url) when loading local assets.
|
|
1591
|
-
- Preserve existing slides and formatting unless the request calls for changes.
|
|
1592
|
-
Never rasterize a chart/table as a shortcut. Raw-only features are not DSL coverage.
|
|
1593
|
-
- Preview is a rendering aid, not a guarantee of identical PowerPoint rendering.
|
|
1594
|
-
Check the exported file in the target application for final delivery.
|
|
1595
|
-
|
|
1596
|
-
VSCode uses tsconfig.json for completion and diagnostics. Start the
|
|
1597
|
-
"Preview presentation" task, then run "Simple Browser: Show" with the printed URL
|
|
1598
|
-
and move that editor to a side group. Type errors appear in the Problems panel.
|
|
1599
|
-
`;
|
|
1600
|
-
/** Creates a new directory; existing projects are never overwritten. */
|
|
1601
|
-
async function initProject(directory) {
|
|
1602
|
-
const root = resolve(directory);
|
|
1603
|
-
const packageLocations = [
|
|
1604
|
-
import.meta.resolve("@office-kit/pptx/package.json"),
|
|
1605
|
-
import.meta.resolve("@office-kit/pptx-dsl/package.json"),
|
|
1606
|
-
new URL("../package.json", import.meta.url)
|
|
1607
|
-
];
|
|
1608
|
-
const versions = await Promise.all(packageLocations.map(async (location) => {
|
|
1609
|
-
return `^${JSON.parse(await readFile(new URL(location), "utf8")).version}`;
|
|
1610
|
-
}));
|
|
1611
|
-
await mkdir(root);
|
|
1612
|
-
const files = {
|
|
1613
|
-
"package.json": JSON.stringify({
|
|
1614
|
-
name: "my-presentation",
|
|
1615
|
-
private: true,
|
|
1616
|
-
type: "module",
|
|
1617
|
-
scripts: {
|
|
1618
|
-
dev: "office-pptx dev deck.tsx",
|
|
1619
|
-
build: "office-pptx build deck.tsx --out deck.pptx",
|
|
1620
|
-
check: "tsc --noEmit"
|
|
1621
|
-
},
|
|
1622
|
-
dependencies: {
|
|
1623
|
-
"@office-kit/pptx": versions[0],
|
|
1624
|
-
"@office-kit/pptx-dsl": versions[1]
|
|
1625
|
-
},
|
|
1626
|
-
devDependencies: {
|
|
1627
|
-
"@office-kit/pptx-dev": versions[2],
|
|
1628
|
-
"@types/node": "^24.13.2",
|
|
1629
|
-
typescript: "^6.0.3"
|
|
1630
|
-
}
|
|
1631
|
-
}, null, 2) + "\n",
|
|
1632
|
-
"tsconfig.json": JSON.stringify({
|
|
1633
|
-
compilerOptions: {
|
|
1634
|
-
target: "ES2022",
|
|
1635
|
-
module: "ESNext",
|
|
1636
|
-
moduleResolution: "Bundler",
|
|
1637
|
-
jsx: "react-jsx",
|
|
1638
|
-
jsxImportSource: "@office-kit/pptx-dsl",
|
|
1639
|
-
strict: true,
|
|
1640
|
-
exactOptionalPropertyTypes: true,
|
|
1641
|
-
noEmit: true,
|
|
1642
|
-
allowImportingTsExtensions: true,
|
|
1643
|
-
types: ["node"]
|
|
1644
|
-
},
|
|
1645
|
-
include: ["**/*.ts", "**/*.tsx"],
|
|
1646
|
-
exclude: ["node_modules"]
|
|
1647
|
-
}, null, 2) + "\n",
|
|
1648
|
-
"deck.tsx": starter,
|
|
1649
|
-
"theme.ts": theme,
|
|
1650
|
-
"slides/cover.tsx": cover,
|
|
1651
|
-
"CLAUDE.md": guide,
|
|
1652
|
-
"AGENTS.md": guide,
|
|
1653
|
-
"README.md": "# Your slides\n\nInstall once with `npm install`, then run `npm run dev` (or `npx office-pptx dev deck.tsx`). Open the printed URL.\n\nEdit `slides/cover.tsx`, select an area for an AI instruction, or choose Edit text in the preview. Start Claude Code in an agent pane before sending instructions. An installed Chrome enables automatic screenshot review.\n\nRun `npm run check` and `npm run build` to check and export `deck.pptx`.\n",
|
|
1654
|
-
".gitignore": "node_modules/\ndeck.pptx\n.office-kit/\n",
|
|
1655
|
-
".vscode/tasks.json": JSON.stringify({
|
|
1656
|
-
version: "2.0.0",
|
|
1657
|
-
tasks: [
|
|
1658
|
-
{
|
|
1659
|
-
label: "Preview presentation",
|
|
1660
|
-
type: "shell",
|
|
1661
|
-
command: "npm run dev",
|
|
1662
|
-
isBackground: true,
|
|
1663
|
-
problemMatcher: {
|
|
1664
|
-
pattern: {
|
|
1665
|
-
regexp: "^NEVER_MATCH$",
|
|
1666
|
-
file: 1,
|
|
1667
|
-
message: 2
|
|
1668
|
-
},
|
|
1669
|
-
background: {
|
|
1670
|
-
activeOnStart: true,
|
|
1671
|
-
beginsPattern: "^.*office-pptx dev",
|
|
1672
|
-
endsPattern: "^Preview: "
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
},
|
|
1676
|
-
{
|
|
1677
|
-
label: "Check presentation",
|
|
1678
|
-
type: "shell",
|
|
1679
|
-
command: "npm run check",
|
|
1680
|
-
problemMatcher: "$tsc"
|
|
1681
|
-
},
|
|
1682
|
-
{
|
|
1683
|
-
label: "Export presentation",
|
|
1684
|
-
type: "shell",
|
|
1685
|
-
command: "npm run build",
|
|
1686
|
-
problemMatcher: []
|
|
1687
|
-
}
|
|
1688
|
-
]
|
|
1689
|
-
}, null, 2) + "\n"
|
|
1690
|
-
};
|
|
1691
|
-
await Promise.all([".vscode", "slides"].map((directory) => mkdir(join(root, directory))));
|
|
1692
|
-
await Promise.all(Object.entries(files).map(([name, content]) => writeFile(join(root, name), content, { flag: "wx" })));
|
|
1693
|
-
return root;
|
|
1694
|
-
}
|
|
1695
|
-
//#endregion
|
|
1696
|
-
//#region src/inspect.ts
|
|
1697
|
-
/** Source references usable directly in Slide, Fill and Remove props. */
|
|
1698
|
-
async function inspectTemplate(file) {
|
|
1699
|
-
const presentation = await loadPresentation(await readFile(file));
|
|
1700
|
-
return {
|
|
1701
|
-
slides: getSlides(presentation).map((slide, index) => ({
|
|
1702
|
-
index,
|
|
1703
|
-
part: getSlidePartName(slide),
|
|
1704
|
-
title: getSlideTitle(slide),
|
|
1705
|
-
shapes: getSlideShapes(slide).map((shape) => ({
|
|
1706
|
-
id: getShapeId(shape),
|
|
1707
|
-
name: getShapeName(shape),
|
|
1708
|
-
kind: getShapeKind(shape),
|
|
1709
|
-
placeholder: {
|
|
1710
|
-
type: getShapePlaceholderType(shape),
|
|
1711
|
-
idx: getShapePlaceholderIdx(shape)
|
|
1712
|
-
}
|
|
1713
|
-
}))
|
|
1714
|
-
})),
|
|
1715
|
-
layouts: getSlideLayouts(presentation).map((layout) => ({
|
|
1716
|
-
part: getSlideLayoutPartName(layout),
|
|
1717
|
-
name: getSlideLayoutName(layout),
|
|
1718
|
-
type: getSlideLayoutType(layout)
|
|
1719
|
-
}))
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
//#endregion
|
|
1723
|
-
//#region src/index.ts
|
|
1724
|
-
/** Each evaluation gets a fresh module cache and releases it when finished. */
|
|
1725
|
-
async function buildDeck(entry) {
|
|
1726
|
-
const builder = createDeckBuilder(entry, false);
|
|
1727
|
-
try {
|
|
1728
|
-
return await applySavedEdits(entry, await builder.build());
|
|
1729
|
-
} finally {
|
|
1730
|
-
await builder.close();
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
async function exportDeck(entry, output) {
|
|
1734
|
-
const result = await buildDeck(entry);
|
|
1735
|
-
await writeFile(output, result.bytes);
|
|
1736
|
-
return result;
|
|
1737
|
-
}
|
|
1738
|
-
//#endregion
|
|
1739
|
-
export { editorStore as a, createDeckBuilder as c, initProject as i, exportDeck as n, mergeDecks as o, inspectTemplate as r, sourceFingerprint as s, buildDeck as t };
|
|
1740
|
-
|
|
1741
|
-
//# sourceMappingURL=src-CI73YbYS.mjs.map
|