ppt-template-reuse 0.4.0-rc.2

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +8 -0
  3. package/README.md +233 -0
  4. package/adapters/README.md +13 -0
  5. package/adapters/claude/.claude-plugin/plugin.json +16 -0
  6. package/adapters/claude/.mcp.json +8 -0
  7. package/adapters/claude/skills/learn-ppt-template/SKILL.md +105 -0
  8. package/adapters/kimi/kimi.plugin.json +12 -0
  9. package/adapters/kimi/skills/learn-ppt-template/SKILL.md +105 -0
  10. package/adapters/kimi-work/mcp.json +8 -0
  11. package/adapters/kimi-work/skills/learn-ppt-template/SKILL.md +105 -0
  12. package/adapters/workbuddy/mcp.json +8 -0
  13. package/adapters/workbuddy/skills/learn-ppt-template/SKILL.md +105 -0
  14. package/package.json +53 -0
  15. package/plugins/ppt-template-reuse/.codex-plugin/plugin.json +38 -0
  16. package/plugins/ppt-template-reuse/.mcp.json +9 -0
  17. package/plugins/ppt-template-reuse/skills/learn-ppt-template/SKILL.md +105 -0
  18. package/plugins/ppt-template-reuse/skills/learn-ppt-template/agents/openai.yaml +6 -0
  19. package/skills/ppt-template-reuse/SKILL.md +105 -0
  20. package/skills/ppt-template-reuse/agents/openai.yaml +6 -0
  21. package/src/cli/ppt-reuse.mjs +203 -0
  22. package/src/core/adaptive-planner.mjs +1192 -0
  23. package/src/core/content-audit.mjs +573 -0
  24. package/src/core/frame-map.mjs +91 -0
  25. package/src/core/index.mjs +38 -0
  26. package/src/core/io.mjs +81 -0
  27. package/src/core/legacy-capsule-import.mjs +382 -0
  28. package/src/core/ooxml-editor.mjs +753 -0
  29. package/src/core/paths.mjs +60 -0
  30. package/src/core/planner-lib.mjs +59 -0
  31. package/src/core/pptx-package.mjs +227 -0
  32. package/src/core/renderer.mjs +402 -0
  33. package/src/core/source-extractor.mjs +412 -0
  34. package/src/core/template-inspector.mjs +489 -0
  35. package/src/core/template-intelligence.mjs +675 -0
  36. package/src/core/universal-engine.mjs +1075 -0
  37. package/src/index.mjs +2 -0
  38. package/src/install/installer.mjs +293 -0
  39. package/src/mcp/server.mjs +377 -0
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export function resolveEngineHome(explicitHome) {
6
+ return path.resolve(
7
+ explicitHome ||
8
+ process.env.PPT_TEMPLATE_REUSE_HOME ||
9
+ path.join(os.homedir(), ".ppt-template-reuse"),
10
+ );
11
+ }
12
+
13
+ export function resolveLibraryDir(explicitHome) {
14
+ return path.join(resolveEngineHome(explicitHome), "library");
15
+ }
16
+
17
+ export function resolveSessionsDir(explicitHome) {
18
+ return path.join(resolveEngineHome(explicitHome), "sessions");
19
+ }
20
+
21
+ export function legacyLibraryDir() {
22
+ return path.join(
23
+ path.join(os.homedir(), ".codex"),
24
+ "ppt-template-reuse",
25
+ "library",
26
+ );
27
+ }
28
+
29
+ async function exists(candidate) {
30
+ return Boolean(await fs.stat(candidate).catch(() => undefined));
31
+ }
32
+
33
+ export async function migrateLegacyCapsules(explicitHome) {
34
+ const home = resolveEngineHome(explicitHome);
35
+ const library = resolveLibraryDir(home);
36
+ const legacy = legacyLibraryDir();
37
+ await fs.mkdir(library, { recursive: true });
38
+ const indexPath = path.join(library, "legacy-capsules.json");
39
+ const prior = (await exists(indexPath))
40
+ ? JSON.parse(await fs.readFile(indexPath, "utf8"))
41
+ : { schemaVersion: 1, capsules: [] };
42
+ const known = new Set(prior.capsules.map((entry) => entry.sha256));
43
+ if (await exists(legacy)) {
44
+ const entries = await fs.readdir(legacy, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory() || known.has(entry.name)) continue;
47
+ const manifest = path.join(legacy, entry.name, "manifest.json");
48
+ if (!(await exists(manifest))) continue;
49
+ prior.capsules.push({
50
+ sha256: entry.name,
51
+ path: path.join(legacy, entry.name),
52
+ mode: "read-only-reference",
53
+ });
54
+ known.add(entry.name);
55
+ }
56
+ }
57
+ prior.capsules.sort((a, b) => a.sha256.localeCompare(b.sha256));
58
+ await fs.writeFile(indexPath, `${JSON.stringify(prior, null, 2)}\n`, "utf8");
59
+ return { home, library, legacy, imported: prior.capsules.length, indexPath };
60
+ }
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export function parseArgs(argv) {
5
+ const result = {};
6
+ for (let index = 0; index < argv.length; index += 1) {
7
+ const token = argv[index];
8
+ if (!token.startsWith("--")) continue;
9
+ const key = token.slice(2);
10
+ const next = argv[index + 1];
11
+ if (!next || next.startsWith("--")) result[key] = true;
12
+ else {
13
+ result[key] = next;
14
+ index += 1;
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+
20
+ export function requireArg(args, key) {
21
+ const value = args[key];
22
+ if (typeof value !== "string" || value.trim() === "") {
23
+ throw new Error(`Missing required argument --${key}`);
24
+ }
25
+ return value;
26
+ }
27
+
28
+ export async function readJson(filePath) {
29
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
30
+ }
31
+
32
+ export async function writeJson(filePath, value) {
33
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
34
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
35
+ }
36
+
37
+ export async function exists(filePath) {
38
+ return Boolean(await fs.stat(filePath).catch(() => undefined));
39
+ }
40
+
41
+ export function textWidthUnits(value) {
42
+ let units = 0;
43
+ for (const character of [...String(value ?? "")]) {
44
+ if (/[\r\n]/u.test(character)) continue;
45
+ if (/\s/u.test(character)) units += 0.33;
46
+ else if (
47
+ /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\u3000-\u303f\uff01-\uff60]/u.test(
48
+ character,
49
+ )
50
+ ) {
51
+ units += 1;
52
+ } else if (/[A-Z]/u.test(character)) units += 0.68;
53
+ else if (/[a-z0-9]/u.test(character)) units += 0.55;
54
+ else if (/[\p{Punctuation}]/u.test(character)) units += 0.45;
55
+ else if (/[\p{Symbol}]/u.test(character)) units += 1;
56
+ else units += 0.75;
57
+ }
58
+ return Number(units.toFixed(3));
59
+ }
@@ -0,0 +1,227 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import JSZip from "jszip";
4
+ import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
5
+
6
+ const parser = new DOMParser();
7
+ const serializer = new XMLSerializer();
8
+
9
+ export function parseXml(xml) {
10
+ return parser.parseFromString(String(xml), "application/xml");
11
+ }
12
+
13
+ export function serializeXml(document) {
14
+ return serializer.serializeToString(document);
15
+ }
16
+
17
+ export function localName(node) {
18
+ return String(node?.localName || node?.nodeName || "").split(":").pop();
19
+ }
20
+
21
+ export function elements(node, wanted) {
22
+ const result = [];
23
+ const visit = (current) => {
24
+ for (const child of Array.from(current?.childNodes || [])) {
25
+ if (child.nodeType === 1 && (!wanted || localName(child) === wanted)) {
26
+ result.push(child);
27
+ }
28
+ visit(child);
29
+ }
30
+ };
31
+ visit(node);
32
+ return result;
33
+ }
34
+
35
+ export function firstElement(node, wanted) {
36
+ return elements(node, wanted)[0];
37
+ }
38
+
39
+ export function attr(node, wanted) {
40
+ for (const item of Array.from(node?.attributes || [])) {
41
+ if (localName(item) === wanted) return item.value;
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ export function attrQName(node, wanted) {
47
+ return node?.getAttribute?.(wanted) || undefined;
48
+ }
49
+
50
+ export function setAttr(node, qualifiedName, value) {
51
+ node.setAttribute(qualifiedName, String(value));
52
+ }
53
+
54
+ export function textOf(node) {
55
+ return elements(node, "t")
56
+ .map((item) => item.textContent || "")
57
+ .join("")
58
+ .replace(/\s+/g, " ")
59
+ .trim();
60
+ }
61
+
62
+ export function parentElement(node, wanted) {
63
+ let current = node?.parentNode;
64
+ while (current) {
65
+ if (current.nodeType === 1 && (!wanted || localName(current) === wanted)) {
66
+ return current;
67
+ }
68
+ current = current.parentNode;
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ export async function loadPptx(pptxPath) {
74
+ const bytes = await fs.readFile(pptxPath);
75
+ return JSZip.loadAsync(bytes, {
76
+ checkCRC32: true,
77
+ createFolders: true,
78
+ });
79
+ }
80
+
81
+ export async function xmlFrom(zip, partName) {
82
+ const entry = zip.file(partName);
83
+ if (!entry) throw new Error(`PPTX part is missing: ${partName}`);
84
+ return entry.async("string");
85
+ }
86
+
87
+ export function resolvePart(basePart, target) {
88
+ if (String(target).startsWith("/")) return String(target).slice(1);
89
+ return path.posix.normalize(
90
+ path.posix.join(path.posix.dirname(basePart), String(target)),
91
+ );
92
+ }
93
+
94
+ export function relationshipPart(partName) {
95
+ return path.posix.join(
96
+ path.posix.dirname(partName),
97
+ "_rels",
98
+ `${path.posix.basename(partName)}.rels`,
99
+ );
100
+ }
101
+
102
+ export async function relationships(zip, partName) {
103
+ const relPart = relationshipPart(partName);
104
+ const entry = zip.file(relPart);
105
+ if (!entry) return { partName: relPart, document: null, records: [] };
106
+ const document = parseXml(await entry.async("string"));
107
+ const records = elements(document, "Relationship").map((node) => ({
108
+ node,
109
+ id: attr(node, "Id"),
110
+ type: attr(node, "Type"),
111
+ target: attr(node, "Target"),
112
+ targetMode: attr(node, "TargetMode"),
113
+ resolved:
114
+ attr(node, "TargetMode") === "External"
115
+ ? undefined
116
+ : resolvePart(partName, attr(node, "Target")),
117
+ }));
118
+ return { partName: relPart, document, records };
119
+ }
120
+
121
+ export async function slideRecords(zip) {
122
+ const presentationPart = "ppt/presentation.xml";
123
+ const presentationXml = await xmlFrom(zip, presentationPart);
124
+ const presentation = parseXml(presentationXml);
125
+ const presentationRels = await relationships(zip, presentationPart);
126
+ const byId = new Map(
127
+ presentationRels.records.map((record) => [record.id, record]),
128
+ );
129
+ const slides = [];
130
+ for (const [index, node] of elements(presentation, "sldId").entries()) {
131
+ const relationshipId =
132
+ attrQName(node, "r:id") ||
133
+ Array.from(node?.attributes || [])
134
+ .filter((item) => item.prefix === "r")
135
+ .map((item) => item.value)[0];
136
+ const relationship = byId.get(relationshipId);
137
+ if (!relationship?.resolved) continue;
138
+ slides.push({
139
+ slideNumber: index + 1,
140
+ slideId: Number(attrQName(node, "id") || attr(node, "id") || 0),
141
+ relationshipId,
142
+ partName: relationship.resolved,
143
+ relationship,
144
+ });
145
+ }
146
+ return {
147
+ presentationPart,
148
+ presentation,
149
+ presentationRels,
150
+ slides,
151
+ };
152
+ }
153
+
154
+ export function emuToPoints(value) {
155
+ return Number(value || 0) / 12700;
156
+ }
157
+
158
+ export function shapeTransform(shape) {
159
+ const xfrm = elements(shape, "xfrm")[0];
160
+ const off = xfrm ? elements(xfrm, "off")[0] : undefined;
161
+ const ext = xfrm ? elements(xfrm, "ext")[0] : undefined;
162
+ return {
163
+ x: emuToPoints(attr(off, "x")),
164
+ y: emuToPoints(attr(off, "y")),
165
+ width: emuToPoints(attr(ext, "cx")),
166
+ height: emuToPoints(attr(ext, "cy")),
167
+ };
168
+ }
169
+
170
+ export function shapeRecords(slideDocument) {
171
+ const candidates = elements(slideDocument).filter((node) =>
172
+ ["sp", "pic", "graphicFrame"].includes(localName(node)),
173
+ );
174
+ return candidates
175
+ .map((shape) => {
176
+ const cNvPr = elements(shape, "cNvPr")[0];
177
+ if (!cNvPr) return null;
178
+ const placeholder = elements(shape, "ph")[0];
179
+ const blip = elements(shape, "blip")[0];
180
+ const chart = elements(shape, "chart")[0];
181
+ const table = elements(shape, "tbl")[0];
182
+ const text = textOf(shape);
183
+ const shapeProperties = elements(shape, "spPr")[0];
184
+ const solidFill = shapeProperties
185
+ ? elements(shapeProperties, "solidFill")[0]
186
+ : undefined;
187
+ const rgbNode = solidFill
188
+ ? elements(solidFill, "srgbClr")[0]
189
+ : undefined;
190
+ const schemeNode = solidFill
191
+ ? elements(solidFill, "schemeClr")[0]
192
+ : undefined;
193
+ const fontSizes = elements(shape, "rPr")
194
+ .map((node) => Number(attr(node, "sz") || 0) / 100)
195
+ .filter((value) => value > 0);
196
+ const kind =
197
+ localName(shape) === "pic"
198
+ ? "image"
199
+ : chart
200
+ ? "chart"
201
+ : table
202
+ ? "table"
203
+ : "text";
204
+ return {
205
+ node: shape,
206
+ id: attr(cNvPr, "id"),
207
+ name: attr(cNvPr, "name") || "",
208
+ description: attr(cNvPr, "descr") || "",
209
+ placeholderType: attr(placeholder, "type") || "",
210
+ kind,
211
+ text,
212
+ fontSize: fontSizes.length ? Math.max(...fontSizes) : 0,
213
+ fillColor:
214
+ attr(rgbNode, "val") ||
215
+ (attr(schemeNode, "val")
216
+ ? `scheme:${attr(schemeNode, "val")}`
217
+ : undefined),
218
+ bbox: shapeTransform(shape),
219
+ relationshipId:
220
+ attrQName(blip, "r:embed") ||
221
+ attrQName(chart, "r:id") ||
222
+ attr(blip, "embed") ||
223
+ undefined,
224
+ };
225
+ })
226
+ .filter(Boolean);
227
+ }