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,412 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import JSZip from "jszip";
4
+ import { DOMParser } from "@xmldom/xmldom";
5
+
6
+ import { fingerprint, sha256File } from "./io.mjs";
7
+
8
+ const parser = new DOMParser();
9
+ const TARGET_UNIT_CHARS = 300;
10
+ const MAX_UNIT_CHARS = 400;
11
+
12
+ function localName(node) {
13
+ return String(node?.localName || node?.nodeName || "").split(":").pop();
14
+ }
15
+
16
+ function descendants(node, wanted) {
17
+ const result = [];
18
+ const visit = (current) => {
19
+ for (const child of Array.from(current?.childNodes || [])) {
20
+ if (child.nodeType === 1 && localName(child) === wanted) result.push(child);
21
+ visit(child);
22
+ }
23
+ };
24
+ visit(node);
25
+ return result;
26
+ }
27
+
28
+ function attribute(node, wanted) {
29
+ for (const item of Array.from(node?.attributes || [])) {
30
+ if (localName(item) === wanted) return item.value;
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ function nodeText(node) {
36
+ return descendants(node, "t")
37
+ .map((item) => item.textContent || "")
38
+ .join("")
39
+ .replace(/\s+/g, " ")
40
+ .trim();
41
+ }
42
+
43
+ function compactText(value) {
44
+ return String(value || "").replace(/\s+/g, " ").trim();
45
+ }
46
+
47
+ function splitOversizedToken(token, maximum = MAX_UNIT_CHARS) {
48
+ const value = compactText(token);
49
+ if (value.length <= maximum) return value ? [value] : [];
50
+ const chunks = [];
51
+ let remaining = value;
52
+ while (remaining.length > maximum) {
53
+ const window = remaining.slice(0, maximum + 1);
54
+ const breakAt = Math.max(
55
+ window.lastIndexOf(","),
56
+ window.lastIndexOf(","),
57
+ window.lastIndexOf("、"),
58
+ window.lastIndexOf(" "),
59
+ );
60
+ const boundary =
61
+ breakAt >= Math.floor(maximum * 0.55) ? breakAt + 1 : maximum;
62
+ chunks.push(compactText(remaining.slice(0, boundary)));
63
+ remaining = compactText(remaining.slice(boundary));
64
+ }
65
+ if (remaining) chunks.push(remaining);
66
+ return chunks;
67
+ }
68
+
69
+ export function splitSourceText(
70
+ value,
71
+ {
72
+ targetChars = TARGET_UNIT_CHARS,
73
+ maxChars = MAX_UNIT_CHARS,
74
+ } = {},
75
+ ) {
76
+ const normalized = compactText(value);
77
+ if (!normalized) return [];
78
+ if (normalized.length <= maxChars) return [normalized];
79
+ const sentences =
80
+ normalized.match(/[^。!?!?;;\n]+[。!?!?;;]?/gu)?.flatMap((sentence) =>
81
+ splitOversizedToken(sentence, maxChars),
82
+ ) || splitOversizedToken(normalized, maxChars);
83
+ const chunks = [];
84
+ let current = "";
85
+ for (const sentence of sentences) {
86
+ const candidate = compactText(`${current} ${sentence}`);
87
+ if (current && candidate.length > maxChars) {
88
+ chunks.push(current);
89
+ current = sentence;
90
+ continue;
91
+ }
92
+ current = candidate;
93
+ if (current.length >= targetChars) {
94
+ chunks.push(current);
95
+ current = "";
96
+ }
97
+ }
98
+ if (current) {
99
+ const previous = chunks.at(-1);
100
+ if (
101
+ previous &&
102
+ current.length < Math.floor(targetChars * 0.35) &&
103
+ previous.length + current.length + 1 <= maxChars
104
+ ) {
105
+ chunks[chunks.length - 1] = compactText(`${previous} ${current}`);
106
+ } else {
107
+ chunks.push(current);
108
+ }
109
+ }
110
+ return chunks;
111
+ }
112
+
113
+ function splitTableUnit(unit) {
114
+ const rows = unit.metadata?.rows;
115
+ if (!Array.isArray(rows) || rows.length <= 1) return [unit];
116
+ const chunks = [];
117
+ let current = [];
118
+ let currentChars = 0;
119
+ for (const row of rows) {
120
+ const rowText = row.map((cell) => compactText(cell)).join(" | ");
121
+ if (current.length && currentChars + rowText.length > MAX_UNIT_CHARS) {
122
+ chunks.push(current);
123
+ current = [];
124
+ currentChars = 0;
125
+ }
126
+ current.push(row);
127
+ currentChars += rowText.length;
128
+ }
129
+ if (current.length) chunks.push(current);
130
+ if (chunks.length === 1) return [unit];
131
+ return chunks.map((chunk, index) => ({
132
+ ...unit,
133
+ text: chunk.map((row) => row.join(" | ")).join("\n"),
134
+ anchor: `${unit.anchor}:segment:${index + 1}`,
135
+ parentAnchor: unit.anchor,
136
+ segmentIndex: index + 1,
137
+ segmentCount: chunks.length,
138
+ metadata: { ...unit.metadata, rows: chunk },
139
+ }));
140
+ }
141
+
142
+ function segmentRawUnit(unit) {
143
+ if (unit.kind === "heading") return [unit];
144
+ if (unit.kind === "table") return splitTableUnit(unit);
145
+ const segments = splitSourceText(unit.text);
146
+ if (segments.length <= 1) return [unit];
147
+ return segments.map((text, index) => ({
148
+ ...unit,
149
+ text,
150
+ anchor: `${unit.anchor}:segment:${index + 1}`,
151
+ parentAnchor: unit.anchor,
152
+ segmentIndex: index + 1,
153
+ segmentCount: segments.length,
154
+ }));
155
+ }
156
+
157
+ function makeUnit({
158
+ index,
159
+ kind,
160
+ text,
161
+ anchor,
162
+ parentAnchor,
163
+ segmentIndex,
164
+ segmentCount,
165
+ title,
166
+ page,
167
+ metadata = {},
168
+ }) {
169
+ const normalized = compactText(text);
170
+ return {
171
+ id: `U${String(index).padStart(4, "0")}`,
172
+ anchor,
173
+ parentAnchor: parentAnchor || undefined,
174
+ segmentIndex: segmentIndex || undefined,
175
+ segmentCount: segmentCount || undefined,
176
+ extractionKind: kind,
177
+ sourcePage: page || undefined,
178
+ topicTitle: title || undefined,
179
+ text: normalized,
180
+ summary: normalized.slice(0, 180),
181
+ detail: normalized,
182
+ ...metadata,
183
+ };
184
+ }
185
+
186
+ async function extractTextLike(sourcePath, extension) {
187
+ const text = await fs.readFile(sourcePath, "utf8");
188
+ const lines = text.split(/\r?\n/);
189
+ const units = [];
190
+ let heading = "";
191
+ let buffer = [];
192
+ const flush = () => {
193
+ const value = buffer.join(" ").replace(/\s+/g, " ").trim();
194
+ if (value) {
195
+ units.push({
196
+ kind: "paragraph",
197
+ text: value,
198
+ title: heading,
199
+ anchor: `${path.basename(sourcePath)}#paragraph:${units.length + 1}`,
200
+ });
201
+ }
202
+ buffer = [];
203
+ };
204
+ for (const raw of lines) {
205
+ const line = raw.trim();
206
+ const markdownHeading = extension === ".md" && line.match(/^#{1,6}\s+(.+)$/);
207
+ if (markdownHeading) {
208
+ flush();
209
+ heading = markdownHeading[1].trim();
210
+ units.push({
211
+ kind: "heading",
212
+ text: heading,
213
+ title: heading,
214
+ anchor: `${path.basename(sourcePath)}#heading:${units.length + 1}`,
215
+ });
216
+ } else if (!line) {
217
+ flush();
218
+ } else {
219
+ buffer.push(line.replace(/^[-*+]\s+/, ""));
220
+ }
221
+ }
222
+ flush();
223
+ return { units, scannedPages: [] };
224
+ }
225
+
226
+ async function extractDocx(sourcePath) {
227
+ const zip = await JSZip.loadAsync(await fs.readFile(sourcePath));
228
+ const xml = await zip.file("word/document.xml")?.async("string");
229
+ if (!xml) throw new Error("DOCX is missing word/document.xml");
230
+ const document = parser.parseFromString(xml, "application/xml");
231
+ const body = descendants(document, "body")[0];
232
+ const units = [];
233
+ let heading = "";
234
+ let paragraphIndex = 0;
235
+ let tableIndex = 0;
236
+ for (const child of Array.from(body?.childNodes || [])) {
237
+ if (child.nodeType !== 1) continue;
238
+ if (localName(child) === "p") {
239
+ const text = nodeText(child);
240
+ if (!text) continue;
241
+ paragraphIndex += 1;
242
+ const style = descendants(child, "pStyle")
243
+ .map((item) => attribute(item, "val"))
244
+ .find(Boolean);
245
+ const isHeading = /heading|标题/i.test(String(style || ""));
246
+ if (isHeading) heading = text;
247
+ units.push({
248
+ kind: isHeading ? "heading" : "paragraph",
249
+ text,
250
+ title: heading,
251
+ anchor: `${path.basename(sourcePath)}#paragraph:${paragraphIndex}`,
252
+ metadata: { paragraphStyle: style || undefined },
253
+ });
254
+ } else if (localName(child) === "tbl") {
255
+ tableIndex += 1;
256
+ const rows = descendants(child, "tr").map((row) =>
257
+ descendants(row, "tc").map((cell) => nodeText(cell)),
258
+ );
259
+ const text = rows.map((row) => row.join(" | ")).join("\n").trim();
260
+ if (!text) continue;
261
+ units.push({
262
+ kind: "table",
263
+ text,
264
+ title: heading,
265
+ anchor: `${path.basename(sourcePath)}#table:${tableIndex}`,
266
+ metadata: { rows },
267
+ });
268
+ }
269
+ }
270
+ return { units, scannedPages: [] };
271
+ }
272
+
273
+ function relationshipMap(xml) {
274
+ const document = parser.parseFromString(xml, "application/xml");
275
+ return new Map(
276
+ descendants(document, "Relationship").map((node) => [
277
+ attribute(node, "Id"),
278
+ attribute(node, "Target"),
279
+ ]),
280
+ );
281
+ }
282
+
283
+ async function extractPptx(sourcePath) {
284
+ const zip = await JSZip.loadAsync(await fs.readFile(sourcePath));
285
+ const presentationXml = await zip.file("ppt/presentation.xml")?.async("string");
286
+ const relsXml = await zip
287
+ .file("ppt/_rels/presentation.xml.rels")
288
+ ?.async("string");
289
+ if (!presentationXml || !relsXml) {
290
+ throw new Error("PPTX is missing presentation relationships");
291
+ }
292
+ const rels = relationshipMap(relsXml);
293
+ const presentation = parser.parseFromString(
294
+ presentationXml,
295
+ "application/xml",
296
+ );
297
+ const slideIds = descendants(presentation, "sldId");
298
+ const units = [];
299
+ for (let index = 0; index < slideIds.length; index += 1) {
300
+ const relId =
301
+ slideIds[index].getAttribute("r:id") ||
302
+ Array.from(slideIds[index].attributes || []).find(
303
+ (item) => item.name === "r:id",
304
+ )?.value;
305
+ const target = rels.get(relId);
306
+ if (!target) continue;
307
+ const slidePath = path.posix.normalize(path.posix.join("ppt", target));
308
+ const slideXml = await zip.file(slidePath)?.async("string");
309
+ if (!slideXml) continue;
310
+ const slide = parser.parseFromString(slideXml, "application/xml");
311
+ const paragraphs = descendants(slide, "p")
312
+ .map((item) => nodeText(item))
313
+ .filter(Boolean);
314
+ if (!paragraphs.length) continue;
315
+ units.push({
316
+ kind: "slide",
317
+ text: paragraphs.join("\n"),
318
+ title: paragraphs[0],
319
+ page: index + 1,
320
+ anchor: `${path.basename(sourcePath)}#slide:${index + 1}`,
321
+ metadata: { paragraphs },
322
+ });
323
+ }
324
+ return { units, scannedPages: [] };
325
+ }
326
+
327
+ async function extractPdf(sourcePath) {
328
+ const canvas = await import("@napi-rs/canvas").catch(() => null);
329
+ for (const name of ["DOMMatrix", "ImageData", "Path2D"]) {
330
+ if (!globalThis[name] && canvas?.[name]) globalThis[name] = canvas[name];
331
+ }
332
+ const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
333
+ const bytes = new Uint8Array(await fs.readFile(sourcePath));
334
+ const loadingTask = pdfjs.getDocument({
335
+ data: bytes,
336
+ disableFontFace: true,
337
+ isEvalSupported: false,
338
+ useSystemFonts: true,
339
+ });
340
+ const document = await loadingTask.promise;
341
+ const units = [];
342
+ const scannedPages = [];
343
+ for (let pageNumber = 1; pageNumber <= document.numPages; pageNumber += 1) {
344
+ const page = await document.getPage(pageNumber);
345
+ const content = await page.getTextContent();
346
+ const text = content.items
347
+ .map((item) => String(item.str || ""))
348
+ .join(" ")
349
+ .replace(/\s+/g, " ")
350
+ .trim();
351
+ if (text.replace(/\s+/g, "").length < 20) {
352
+ scannedPages.push(pageNumber);
353
+ continue;
354
+ }
355
+ units.push({
356
+ kind: "page",
357
+ text,
358
+ title: text.slice(0, 80),
359
+ page: pageNumber,
360
+ anchor: `${path.basename(sourcePath)}#page:${pageNumber}`,
361
+ });
362
+ }
363
+ return { units, scannedPages, pageCount: document.numPages };
364
+ }
365
+
366
+ export async function extractSource(sourcePath) {
367
+ const resolved = path.resolve(sourcePath);
368
+ const extension = path.extname(resolved).toLowerCase();
369
+ let extracted;
370
+ if ([".md", ".txt"].includes(extension)) {
371
+ extracted = await extractTextLike(resolved, extension);
372
+ } else if (extension === ".docx") {
373
+ extracted = await extractDocx(resolved);
374
+ } else if (extension === ".pptx") {
375
+ extracted = await extractPptx(resolved);
376
+ } else if (extension === ".pdf") {
377
+ extracted = await extractPdf(resolved);
378
+ } else {
379
+ throw new Error(
380
+ `Unsupported source format ${extension || "(none)"}; use DOCX, PDF, PPTX, Markdown, or text.`,
381
+ );
382
+ }
383
+ const units = extracted.units
384
+ .flatMap(segmentRawUnit)
385
+ .filter((item) => String(item.text || "").trim())
386
+ .map((item, index) => makeUnit({ ...item, index: index + 1 }));
387
+ const result = {
388
+ schemaVersion: 1,
389
+ source: {
390
+ path: resolved,
391
+ sha256: await sha256File(resolved),
392
+ extension,
393
+ },
394
+ units,
395
+ scannedPages: extracted.scannedPages || [],
396
+ pageCount: extracted.pageCount,
397
+ };
398
+ result.extractionFingerprint = fingerprint(result);
399
+ result.actionRequired = {
400
+ type: "source-semantics",
401
+ instructions: [
402
+ "Read every extracted unit and retain its id and anchor.",
403
+ "Label each unit as background, fact, chronology, issue, rule, method, evidence, data, application, analysis, counterargument, rebuttal, conclusion, or recommendation.",
404
+ "Assign critical, supporting, or optional priority and core, appendix, or exclude presentation relevance.",
405
+ "Add summary, detail, claim, evidence, whyItMatters, and explicit counterargument/rebuttal links where applicable.",
406
+ "Add short source-grounded displayFragments for dates, labels, steps, metric labels, and descriptions when a structured template layout needs them.",
407
+ "Do not omit, merge away, or invent source units. Segmented units retain parentAnchor and segmentIndex so long source blocks remain traceable.",
408
+ ],
409
+ scannedPages: result.scannedPages,
410
+ };
411
+ return result;
412
+ }