@taloswork/ose-shared 0.1.9 → 0.1.11
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/mcp/gen/mcp.json +2 -2
- package/package.json +3 -3
- package/skills/diagram-design/SKILL.md +1 -1
- package/skills/document/SKILL.md +63 -75
- package/skills/document/bin/deck-build.cjs +51 -1
- package/skills/document/bin/deck-qa.cjs +18745 -279
- package/skills/document/bin/doc-convert.cjs +9280 -76489
- package/skills/document/bin/doc-fill.cjs +0 -0
- package/skills/document/bin/doc-gen.cjs +3043 -46825
- package/skills/document/bin/doc-run.cjs +65095 -1967
- package/skills/document/bin/doc-scan.cjs +0 -0
- package/skills/document/bin/doc-text.cjs +0 -0
- package/skills/document/bin/doc-validate.cjs +18067 -226
- package/skills/document/bin/doc-xml.cjs +1 -1
- package/skills/document/bin/docx-annotate.cjs +0 -0
- package/skills/document/bin/html2pptx.cjs +32498 -145280
- package/skills/document/bin/ocr-worker.cjs +0 -0
- package/skills/document/bin/ocr.cjs +0 -0
- package/skills/document/bin/pdf-form.cjs +0 -0
- package/skills/document/bin/pdf-images.cjs +0 -0
- package/skills/document/bin/pdf-ops.cjs +0 -0
- package/skills/document/bin/pdf-render.cjs +11 -9
- package/skills/document/bin/pdf-tables.cjs +0 -0
- package/skills/document/bin/pptx-clone.cjs +21 -25
- package/skills/document/bin/pptx-embed-fonts.cjs +8 -6
- package/skills/document/bin/pptx-set-image.cjs +0 -0
- package/skills/document/bin/pptx-slide.cjs +22 -32
- package/skills/document/bin/pptx-theme.cjs +1 -20
- package/skills/document/bin/pptx-tokenize.cjs +1 -1
- package/skills/document/bin/xlsx-query.cjs +0 -0
- package/skills/document/bin/xlsx-recalc.cjs +0 -0
- package/skills/document/bin-reference.md +35 -0
- package/skills/document/content-pipeline.md +19 -18
- package/skills/document/deck-design.md +110 -211
- package/skills/document/layouts/README.md +4 -33
- package/skills/document/motion/README.md +4 -2
- package/skills/document/office.md +5 -1
- package/skills/document/rewrite.md +43 -99
- package/skills/document/styles/README.md +0 -7
- package/skills/document/theme-system.md +1 -0
- package/skills/document/themes/README.md +5 -5
- package/skills/document/themes/anthropic-clay.json +1 -7
- package/skills/document/themes/apple-keynote.json +1 -7
- package/skills/document/themes/black-gold.json +1 -2
- package/skills/document/themes/cinereel.json +1 -2
- package/skills/document/themes/cool-tech.json +1 -2
- package/skills/document/themes/hand-drawn.json +1 -2
- package/skills/document/themes/ivory-gold.json +1 -2
- package/skills/document/themes/magazine-ink.json +1 -2
- package/skills/document/themes/mckinsey-consulting.json +1 -7
- package/skills/document/themes/mono.json +1 -2
- package/skills/document/themes/swiss-grid.json +1 -2
- package/skills/document/themes/warm-editorial.json +1 -2
- package/skills/image-layout/SKILL.md +1 -1
- package/skills/image-layout/bin/html2img.cjs +0 -0
- package/skills/cozy-static-svg/SKILL.md +0 -76
- package/skills/cozy-static-svg/references/approved-preview.png +0 -0
- package/skills/cozy-static-svg/references/cross-platform.md +0 -73
- package/skills/cozy-static-svg/references/moon_keeps_watch.svg +0 -229
- package/skills/cozy-static-svg/references/style-anchor.md +0 -271
- package/skills/cozy-static-svg/scripts/artkit.py +0 -321
- package/skills/cozy-static-svg/scripts/to_png.py +0 -333
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -47,20 +47,22 @@ function die(msg, code = 1) {
|
|
|
47
47
|
process.exit(code);
|
|
48
48
|
}
|
|
49
49
|
function parsePages(spec) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
const pages = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const part of spec.split(",")) {
|
|
52
|
+
const m = /^(\d+)(?:-(\d+))?$/.exec(part.trim());
|
|
53
|
+
if (!m) die(`bad page spec: ${spec} (use N, N-M or a list like 3,7-8)`, 64);
|
|
54
|
+
const from = Number(m[1]);
|
|
55
|
+
const to = m[2] ? Number(m[2]) : from;
|
|
56
|
+
if (from < 1 || to < from) die(`bad page range: ${part.trim()}`, 64);
|
|
57
|
+
for (let p = from; p <= to; p++) pages.add(p);
|
|
58
|
+
}
|
|
59
|
+
return [...pages].sort((a, b) => a - b);
|
|
58
60
|
}
|
|
59
61
|
async function main() {
|
|
60
62
|
const file = process.argv[2];
|
|
61
63
|
const spec = process.argv[3];
|
|
62
64
|
const scaleArg = process.argv[4];
|
|
63
|
-
if (!file || !spec) die("usage: pdf-render <file.pdf> <pages|all> [scale]", 64);
|
|
65
|
+
if (!file || !spec) die("usage: pdf-render <file.pdf> <pages|all> [scale] (pages: N | N-M | 3,7-8)", 64);
|
|
64
66
|
let scale = 2;
|
|
65
67
|
if (scaleArg !== void 0) {
|
|
66
68
|
scale = Number(scaleArg);
|
|
File without changes
|
|
@@ -9855,14 +9855,14 @@ function unescXml(s) {
|
|
|
9855
9855
|
return "'";
|
|
9856
9856
|
default: {
|
|
9857
9857
|
const n = code.startsWith("#x") || code.startsWith("#X") ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
|
|
9858
|
-
return Number.
|
|
9858
|
+
return Number.isInteger(n) && n >= 0 && n <= 1114111 ? String.fromCodePoint(n) : whole;
|
|
9859
9859
|
}
|
|
9860
9860
|
}
|
|
9861
9861
|
});
|
|
9862
9862
|
}
|
|
9863
|
-
function visualWidth(
|
|
9863
|
+
function visualWidth(text) {
|
|
9864
9864
|
let w = 0;
|
|
9865
|
-
for (const ch of
|
|
9865
|
+
for (const ch of text) {
|
|
9866
9866
|
const cp = ch.codePointAt(0);
|
|
9867
9867
|
if (ch === " " || ch === " ")
|
|
9868
9868
|
w += 0.35;
|
|
@@ -9891,9 +9891,9 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
9891
9891
|
const segs = [];
|
|
9892
9892
|
let flat = "";
|
|
9893
9893
|
for (const m of para.matchAll(textRe)) {
|
|
9894
|
-
const
|
|
9895
|
-
segs.push({ el: m[0], attrs: m[1] ?? "", text
|
|
9896
|
-
flat +=
|
|
9894
|
+
const text = unescXml(m[2]);
|
|
9895
|
+
segs.push({ el: m[0], attrs: m[1] ?? "", text, start: flat.length });
|
|
9896
|
+
flat += text;
|
|
9897
9897
|
}
|
|
9898
9898
|
if (!flat.includes(find))
|
|
9899
9899
|
return para;
|
|
@@ -9936,28 +9936,30 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
9936
9936
|
return { xml: rewritten, count };
|
|
9937
9937
|
}
|
|
9938
9938
|
|
|
9939
|
-
// packages/core/dist/document/pptx-
|
|
9940
|
-
|
|
9941
|
-
var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
9942
|
-
async function text(zip, path) {
|
|
9939
|
+
// packages/core/dist/document/pptx-parts.js
|
|
9940
|
+
async function readPptxPart(zip, path) {
|
|
9943
9941
|
const f = zip.file(path);
|
|
9944
9942
|
if (!f)
|
|
9945
9943
|
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
9946
9944
|
return f.async("string");
|
|
9947
9945
|
}
|
|
9946
|
+
|
|
9947
|
+
// packages/core/dist/document/pptx-register-slide.js
|
|
9948
|
+
var SLIDE_CT = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
9949
|
+
var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
9948
9950
|
async function registerSlidePart(zip, newNum, newXmlPath) {
|
|
9949
9951
|
const ctPath = "[Content_Types].xml";
|
|
9950
|
-
const ct = await
|
|
9952
|
+
const ct = await readPptxPart(zip, ctPath);
|
|
9951
9953
|
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
9952
9954
|
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
9953
9955
|
}
|
|
9954
9956
|
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
9955
|
-
const presRels = await
|
|
9957
|
+
const presRels = await readPptxPart(zip, presRelsPath);
|
|
9956
9958
|
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
9957
9959
|
const newRid = `rId${maxRid + 1}`;
|
|
9958
9960
|
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
9959
9961
|
const presPath = "ppt/presentation.xml";
|
|
9960
|
-
const pres = await
|
|
9962
|
+
const pres = await readPptxPart(zip, presPath);
|
|
9961
9963
|
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
9962
9964
|
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
9963
9965
|
if (updated === pres)
|
|
@@ -9998,12 +10000,6 @@ function capacityWarning(slideXml, token, value) {
|
|
|
9998
10000
|
return void 0;
|
|
9999
10001
|
return `${token}: replacement is ~${Math.ceil(width)} width-units, slot fits ~${Math.floor(capacity)} (${Math.round(widthPt)}\xD7${Math.round(heightPt)}pt box @ ${fontPt}pt) \u2014 likely overflow. Shorten the wording; do NOT shrink the font to make it fit.`;
|
|
10000
10002
|
}
|
|
10001
|
-
async function text2(zip, path) {
|
|
10002
|
-
const f = zip.file(path);
|
|
10003
|
-
if (!f)
|
|
10004
|
-
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
10005
|
-
return f.async("string");
|
|
10006
|
-
}
|
|
10007
10003
|
async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
10008
10004
|
if (!Number.isInteger(slideNumber) || slideNumber < 1) {
|
|
10009
10005
|
throw new Error(`slide number must be a positive integer, got ${slideNumber}`);
|
|
@@ -10023,7 +10019,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
10023
10019
|
const newNum = Math.max(...slideNums) + 1;
|
|
10024
10020
|
const srcXmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10025
10021
|
const newXmlPath = `ppt/slides/slide${newNum}.xml`;
|
|
10026
|
-
let slideXml = await
|
|
10022
|
+
let slideXml = await readPptxPart(zip, srcXmlPath);
|
|
10027
10023
|
const replaced = [];
|
|
10028
10024
|
const missing = [];
|
|
10029
10025
|
const warnings = [];
|
|
@@ -10076,7 +10072,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10076
10072
|
const xmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10077
10073
|
const relsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
|
|
10078
10074
|
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10079
|
-
const presRels = await
|
|
10075
|
+
const presRels = await readPptxPart(zip, presRelsPath);
|
|
10080
10076
|
const targetRe = new RegExp(`Target="(/ppt/)?slides/slide${slideNumber}\\.xml"`);
|
|
10081
10077
|
const relMatch = findElement(presRels, "Relationship", (r) => r.includes(SLIDE_REL_TYPE) && targetRe.test(r));
|
|
10082
10078
|
if (!relMatch)
|
|
@@ -10086,19 +10082,19 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10086
10082
|
throw new Error(`slide ${slideNumber}'s relationship carries no Id (corrupt deck?)`);
|
|
10087
10083
|
zip.file(presRelsPath, presRels.replace(relMatch, ""));
|
|
10088
10084
|
const presPath = "ppt/presentation.xml";
|
|
10089
|
-
const pres = await
|
|
10085
|
+
const pres = await readPptxPart(zip, presPath);
|
|
10090
10086
|
const sld = removeElement(pres, "p:sldId", (el) => el.includes(`r:id="${rId}"`));
|
|
10091
10087
|
if (!sld.removed)
|
|
10092
10088
|
throw new Error(`slide ${slideNumber} (${rId}) not found in the slide list (corrupt deck?)`);
|
|
10093
10089
|
zip.file(presPath, sld.xml);
|
|
10094
|
-
const slideRels = zip.file(relsPath) ? await
|
|
10090
|
+
const slideRels = zip.file(relsPath) ? await readPptxPart(zip, relsPath) : "";
|
|
10095
10091
|
const notesTarget = /<Relationship\b[^>]*Type="[^"]*\/notesSlide"[^>]*Target="\.\.\/(notesSlides\/notesSlide\d+\.xml)"/.exec(slideRels)?.[1];
|
|
10096
10092
|
const removedParts = [xmlPath, relsPath];
|
|
10097
10093
|
if (notesTarget) {
|
|
10098
10094
|
const otherSlideRels = Object.keys(zip.files).filter((p) => /^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/.test(p) && p !== relsPath);
|
|
10099
10095
|
let shared = false;
|
|
10100
10096
|
for (const p of otherSlideRels) {
|
|
10101
|
-
if ((await
|
|
10097
|
+
if ((await readPptxPart(zip, p)).includes(notesTarget)) {
|
|
10102
10098
|
shared = true;
|
|
10103
10099
|
break;
|
|
10104
10100
|
}
|
|
@@ -10106,7 +10102,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10106
10102
|
if (!shared)
|
|
10107
10103
|
removedParts.push(`ppt/${notesTarget}`, `ppt/notesSlides/_rels/${notesTarget.split("/")[1]}.rels`);
|
|
10108
10104
|
}
|
|
10109
|
-
let ct = await
|
|
10105
|
+
let ct = await readPptxPart(zip, "[Content_Types].xml");
|
|
10110
10106
|
for (const part of removedParts) {
|
|
10111
10107
|
zip.remove(part);
|
|
10112
10108
|
ct = removeElement(ct, "Override", (el) => el.includes(`PartName="/${part}"`)).xml;
|
|
@@ -9826,14 +9826,16 @@ function escXml(s) {
|
|
|
9826
9826
|
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9827
9827
|
}
|
|
9828
9828
|
|
|
9829
|
-
// packages/core/dist/document/pptx-
|
|
9830
|
-
|
|
9831
|
-
async function text(zip, path) {
|
|
9829
|
+
// packages/core/dist/document/pptx-parts.js
|
|
9830
|
+
async function readPptxPart(zip, path) {
|
|
9832
9831
|
const f = zip.file(path);
|
|
9833
9832
|
if (!f)
|
|
9834
9833
|
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
9835
9834
|
return f.async("string");
|
|
9836
9835
|
}
|
|
9836
|
+
|
|
9837
|
+
// packages/core/dist/document/pptx-fonts.js
|
|
9838
|
+
var FONT_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
|
|
9837
9839
|
async function embedFonts(bytes, fonts) {
|
|
9838
9840
|
if (!fonts.length)
|
|
9839
9841
|
return bytes;
|
|
@@ -9843,13 +9845,13 @@ async function embedFonts(bytes, fonts) {
|
|
|
9843
9845
|
} catch (e) {
|
|
9844
9846
|
throw new Error(`not a readable .pptx: ${e.message}`);
|
|
9845
9847
|
}
|
|
9846
|
-
let ct = await
|
|
9848
|
+
let ct = await readPptxPart(zip, "[Content_Types].xml");
|
|
9847
9849
|
if (!ct.includes('Extension="fntdata"')) {
|
|
9848
9850
|
ct = ct.replace("</Types>", `<Default Extension="fntdata" ContentType="application/x-fontdata"/></Types>`);
|
|
9849
9851
|
zip.file("[Content_Types].xml", ct);
|
|
9850
9852
|
}
|
|
9851
9853
|
const relsPath = "ppt/_rels/presentation.xml.rels";
|
|
9852
|
-
let rels = await
|
|
9854
|
+
let rels = await readPptxPart(zip, relsPath);
|
|
9853
9855
|
let rid = Math.max(0, ...[...rels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
9854
9856
|
let fontNo = Object.keys(zip.files).filter((p) => /^ppt\/fonts\/font\d+\.fntdata$/.test(p)).length;
|
|
9855
9857
|
const byFace = /* @__PURE__ */ new Map();
|
|
@@ -9875,7 +9877,7 @@ async function embedFonts(bytes, fonts) {
|
|
|
9875
9877
|
}
|
|
9876
9878
|
zip.file(relsPath, rels);
|
|
9877
9879
|
const presPath = "ppt/presentation.xml";
|
|
9878
|
-
let pres = await
|
|
9880
|
+
let pres = await readPptxPart(zip, presPath);
|
|
9879
9881
|
if (!/<p:presentation\b[^>]*\bembedTrueTypeFonts=/.test(pres)) {
|
|
9880
9882
|
pres = pres.replace(/<p:presentation\b/, '<p:presentation embedTrueTypeFonts="1"');
|
|
9881
9883
|
}
|
|
File without changes
|
|
@@ -9821,28 +9821,30 @@ var import_promises = require("node:fs/promises");
|
|
|
9821
9821
|
// packages/core/dist/document/pptx-slide.js
|
|
9822
9822
|
var import_jszip = __toESM(require_lib3(), 1);
|
|
9823
9823
|
|
|
9824
|
-
// packages/core/dist/document/pptx-
|
|
9825
|
-
|
|
9826
|
-
var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
9827
|
-
async function text(zip, path) {
|
|
9824
|
+
// packages/core/dist/document/pptx-parts.js
|
|
9825
|
+
async function readPptxPart(zip, path) {
|
|
9828
9826
|
const f = zip.file(path);
|
|
9829
9827
|
if (!f)
|
|
9830
9828
|
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
9831
9829
|
return f.async("string");
|
|
9832
9830
|
}
|
|
9831
|
+
|
|
9832
|
+
// packages/core/dist/document/pptx-register-slide.js
|
|
9833
|
+
var SLIDE_CT = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
|
|
9834
|
+
var SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
|
|
9833
9835
|
async function registerSlidePart(zip, newNum, newXmlPath) {
|
|
9834
9836
|
const ctPath = "[Content_Types].xml";
|
|
9835
|
-
const ct = await
|
|
9837
|
+
const ct = await readPptxPart(zip, ctPath);
|
|
9836
9838
|
if (!ct.includes(`PartName="/${newXmlPath}"`)) {
|
|
9837
9839
|
zip.file(ctPath, ct.replace("</Types>", `<Override PartName="/${newXmlPath}" ContentType="${SLIDE_CT}"/></Types>`));
|
|
9838
9840
|
}
|
|
9839
9841
|
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
9840
|
-
const presRels = await
|
|
9842
|
+
const presRels = await readPptxPart(zip, presRelsPath);
|
|
9841
9843
|
const maxRid = Math.max(0, ...[...presRels.matchAll(/Id="rId(\d+)"/g)].map((m) => Number(m[1])));
|
|
9842
9844
|
const newRid = `rId${maxRid + 1}`;
|
|
9843
9845
|
zip.file(presRelsPath, presRels.replace("</Relationships>", `<Relationship Id="${newRid}" Type="${SLIDE_REL_TYPE}" Target="slides/slide${newNum}.xml"/></Relationships>`));
|
|
9844
9846
|
const presPath = "ppt/presentation.xml";
|
|
9845
|
-
const pres = await
|
|
9847
|
+
const pres = await readPptxPart(zip, presPath);
|
|
9846
9848
|
const maxSldId = Math.max(255, ...[...pres.matchAll(/<p:sldId id="(\d+)"/g)].map((m) => Number(m[1])));
|
|
9847
9849
|
const updated = pres.replace("</p:sldIdLst>", `<p:sldId id="${maxSldId + 1}" r:id="${newRid}"/></p:sldIdLst>`);
|
|
9848
9850
|
if (updated === pres)
|
|
@@ -9853,12 +9855,6 @@ async function registerSlidePart(zip, newNum, newXmlPath) {
|
|
|
9853
9855
|
// packages/core/dist/document/pptx-slide.js
|
|
9854
9856
|
var LAYOUT_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
|
|
9855
9857
|
var BLANK_SLIDE_XML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>';
|
|
9856
|
-
async function text2(zip, path) {
|
|
9857
|
-
const f = zip.file(path);
|
|
9858
|
-
if (!f)
|
|
9859
|
-
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
9860
|
-
return f.async("string");
|
|
9861
|
-
}
|
|
9862
9858
|
async function load(bytes) {
|
|
9863
9859
|
try {
|
|
9864
9860
|
return await import_jszip.default.loadAsync(Buffer.from(bytes));
|
|
@@ -9908,7 +9904,7 @@ async function addBlankSlide(bytes, opts = {}) {
|
|
|
9908
9904
|
async function reorderPptxSlides(bytes, order) {
|
|
9909
9905
|
const zip = await load(bytes);
|
|
9910
9906
|
const presPath = "ppt/presentation.xml";
|
|
9911
|
-
const pres = await
|
|
9907
|
+
const pres = await readPptxPart(zip, presPath);
|
|
9912
9908
|
const listMatch = /(<p:sldIdLst\b[^>]*>)([\s\S]*?)(<\/p:sldIdLst>)/.exec(pres);
|
|
9913
9909
|
if (!listMatch)
|
|
9914
9910
|
throw new Error("no <p:sldIdLst> in presentation.xml (corrupt deck?)");
|
|
@@ -9974,14 +9970,14 @@ function unescXml(s) {
|
|
|
9974
9970
|
return "'";
|
|
9975
9971
|
default: {
|
|
9976
9972
|
const n = code.startsWith("#x") || code.startsWith("#X") ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
|
|
9977
|
-
return Number.
|
|
9973
|
+
return Number.isInteger(n) && n >= 0 && n <= 1114111 ? String.fromCodePoint(n) : whole;
|
|
9978
9974
|
}
|
|
9979
9975
|
}
|
|
9980
9976
|
});
|
|
9981
9977
|
}
|
|
9982
|
-
function visualWidth(
|
|
9978
|
+
function visualWidth(text) {
|
|
9983
9979
|
let w = 0;
|
|
9984
|
-
for (const ch of
|
|
9980
|
+
for (const ch of text) {
|
|
9985
9981
|
const cp = ch.codePointAt(0);
|
|
9986
9982
|
if (ch === " " || ch === " ")
|
|
9987
9983
|
w += 0.35;
|
|
@@ -10010,9 +10006,9 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
|
|
|
10010
10006
|
const segs = [];
|
|
10011
10007
|
let flat = "";
|
|
10012
10008
|
for (const m of para.matchAll(textRe)) {
|
|
10013
|
-
const
|
|
10014
|
-
segs.push({ el: m[0], attrs: m[1] ?? "", text
|
|
10015
|
-
flat +=
|
|
10009
|
+
const text = unescXml(m[2]);
|
|
10010
|
+
segs.push({ el: m[0], attrs: m[1] ?? "", text, start: flat.length });
|
|
10011
|
+
flat += text;
|
|
10016
10012
|
}
|
|
10017
10013
|
if (!flat.includes(find))
|
|
10018
10014
|
return para;
|
|
@@ -10088,12 +10084,6 @@ function capacityWarning(slideXml, token, value) {
|
|
|
10088
10084
|
return void 0;
|
|
10089
10085
|
return `${token}: replacement is ~${Math.ceil(width)} width-units, slot fits ~${Math.floor(capacity)} (${Math.round(widthPt)}\xD7${Math.round(heightPt)}pt box @ ${fontPt}pt) \u2014 likely overflow. Shorten the wording; do NOT shrink the font to make it fit.`;
|
|
10090
10086
|
}
|
|
10091
|
-
async function text3(zip, path) {
|
|
10092
|
-
const f = zip.file(path);
|
|
10093
|
-
if (!f)
|
|
10094
|
-
throw new Error(`not a PowerPoint file (${path} missing)`);
|
|
10095
|
-
return f.async("string");
|
|
10096
|
-
}
|
|
10097
10087
|
async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
10098
10088
|
if (!Number.isInteger(slideNumber) || slideNumber < 1) {
|
|
10099
10089
|
throw new Error(`slide number must be a positive integer, got ${slideNumber}`);
|
|
@@ -10113,7 +10103,7 @@ async function clonePptxSlide(bytes, slideNumber, replacements = {}) {
|
|
|
10113
10103
|
const newNum = Math.max(...slideNums) + 1;
|
|
10114
10104
|
const srcXmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10115
10105
|
const newXmlPath = `ppt/slides/slide${newNum}.xml`;
|
|
10116
|
-
let slideXml = await
|
|
10106
|
+
let slideXml = await readPptxPart(zip, srcXmlPath);
|
|
10117
10107
|
const replaced = [];
|
|
10118
10108
|
const missing = [];
|
|
10119
10109
|
const warnings = [];
|
|
@@ -10166,7 +10156,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10166
10156
|
const xmlPath = `ppt/slides/slide${slideNumber}.xml`;
|
|
10167
10157
|
const relsPath = `ppt/slides/_rels/slide${slideNumber}.xml.rels`;
|
|
10168
10158
|
const presRelsPath = "ppt/_rels/presentation.xml.rels";
|
|
10169
|
-
const presRels = await
|
|
10159
|
+
const presRels = await readPptxPart(zip, presRelsPath);
|
|
10170
10160
|
const targetRe = new RegExp(`Target="(/ppt/)?slides/slide${slideNumber}\\.xml"`);
|
|
10171
10161
|
const relMatch = findElement(presRels, "Relationship", (r) => r.includes(SLIDE_REL_TYPE) && targetRe.test(r));
|
|
10172
10162
|
if (!relMatch)
|
|
@@ -10176,19 +10166,19 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10176
10166
|
throw new Error(`slide ${slideNumber}'s relationship carries no Id (corrupt deck?)`);
|
|
10177
10167
|
zip.file(presRelsPath, presRels.replace(relMatch, ""));
|
|
10178
10168
|
const presPath = "ppt/presentation.xml";
|
|
10179
|
-
const pres = await
|
|
10169
|
+
const pres = await readPptxPart(zip, presPath);
|
|
10180
10170
|
const sld = removeElement(pres, "p:sldId", (el) => el.includes(`r:id="${rId}"`));
|
|
10181
10171
|
if (!sld.removed)
|
|
10182
10172
|
throw new Error(`slide ${slideNumber} (${rId}) not found in the slide list (corrupt deck?)`);
|
|
10183
10173
|
zip.file(presPath, sld.xml);
|
|
10184
|
-
const slideRels = zip.file(relsPath) ? await
|
|
10174
|
+
const slideRels = zip.file(relsPath) ? await readPptxPart(zip, relsPath) : "";
|
|
10185
10175
|
const notesTarget = /<Relationship\b[^>]*Type="[^"]*\/notesSlide"[^>]*Target="\.\.\/(notesSlides\/notesSlide\d+\.xml)"/.exec(slideRels)?.[1];
|
|
10186
10176
|
const removedParts = [xmlPath, relsPath];
|
|
10187
10177
|
if (notesTarget) {
|
|
10188
10178
|
const otherSlideRels = Object.keys(zip.files).filter((p) => /^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/.test(p) && p !== relsPath);
|
|
10189
10179
|
let shared = false;
|
|
10190
10180
|
for (const p of otherSlideRels) {
|
|
10191
|
-
if ((await
|
|
10181
|
+
if ((await readPptxPart(zip, p)).includes(notesTarget)) {
|
|
10192
10182
|
shared = true;
|
|
10193
10183
|
break;
|
|
10194
10184
|
}
|
|
@@ -10196,7 +10186,7 @@ async function removePptxSlide(bytes, slideNumber) {
|
|
|
10196
10186
|
if (!shared)
|
|
10197
10187
|
removedParts.push(`ppt/${notesTarget}`, `ppt/notesSlides/_rels/${notesTarget.split("/")[1]}.rels`);
|
|
10198
10188
|
}
|
|
10199
|
-
let ct = await
|
|
10189
|
+
let ct = await readPptxPart(zip, "[Content_Types].xml");
|
|
10200
10190
|
for (const part of removedParts) {
|
|
10201
10191
|
zip.remove(part);
|
|
10202
10192
|
ct = removeElement(ct, "Override", (el) => el.includes(`PartName="/${part}"`)).xml;
|
|
@@ -9852,18 +9852,6 @@ function fontFace(fontScheme, which) {
|
|
|
9852
9852
|
const ea = /<a:ea typeface="([^"]*)"/.exec(seg)?.[1];
|
|
9853
9853
|
return latin || ea || void 0;
|
|
9854
9854
|
}
|
|
9855
|
-
function masterSize(master, block) {
|
|
9856
|
-
const seg = new RegExp(`<p:${block}>([\\s\\S]*?)</p:${block}>`).exec(master)?.[1];
|
|
9857
|
-
const lvl1 = seg && /<a:lvl1pPr[\s\S]*?<a:defRPr[^>]*\bsz="(\d+)"/.exec(seg)?.[1];
|
|
9858
|
-
return lvl1 ? Number(lvl1) / 100 : void 0;
|
|
9859
|
-
}
|
|
9860
|
-
function clamp(v, lo, hi, label, warnings) {
|
|
9861
|
-
if (v < lo || v > hi) {
|
|
9862
|
-
warnings.push(`${label} ${v}pt is outside the generated-deck band [${lo}, ${hi}] \u2014 clamped`);
|
|
9863
|
-
return Math.min(hi, Math.max(lo, v));
|
|
9864
|
-
}
|
|
9865
|
-
return v;
|
|
9866
|
-
}
|
|
9867
9855
|
async function extractPptxTheme(bytes) {
|
|
9868
9856
|
let zip;
|
|
9869
9857
|
try {
|
|
@@ -9905,10 +9893,6 @@ async function extractPptxTheme(bytes) {
|
|
|
9905
9893
|
const fontScheme = /<a:fontScheme[\s\S]*?<\/a:fontScheme>/.exec(themeXml)?.[0] ?? "";
|
|
9906
9894
|
const heading = fontFace(fontScheme, "majorFont");
|
|
9907
9895
|
const body = fontFace(fontScheme, "minorFont");
|
|
9908
|
-
const masterFile = zip.file("ppt/slideMasters/slideMaster1.xml") ?? zip.file(/^ppt\/slideMasters\/slideMaster\d+\.xml$/)[0];
|
|
9909
|
-
const master = masterFile ? await masterFile.async("string") : "";
|
|
9910
|
-
const rawTitle = masterSize(master, "titleStyle");
|
|
9911
|
-
const rawBody = masterSize(master, "bodyStyle");
|
|
9912
9896
|
const theme = {
|
|
9913
9897
|
...heading || body ? { fonts: { ...heading ? { heading } : {}, ...body ? { body } : {} } } : {},
|
|
9914
9898
|
colors: {
|
|
@@ -9922,10 +9906,7 @@ async function extractPptxTheme(bytes) {
|
|
|
9922
9906
|
pptx: {
|
|
9923
9907
|
background: lt1,
|
|
9924
9908
|
titleColor: accent1,
|
|
9925
|
-
textColor: dk1
|
|
9926
|
-
...rawTitle ? { titleSize: clamp(rawTitle, 20, 40, "master title size", warnings) } : {},
|
|
9927
|
-
...rawBody ? { bodySize: clamp(rawBody, 12, 20, "master body size", warnings) } : {},
|
|
9928
|
-
slideNumber: true
|
|
9909
|
+
textColor: dk1
|
|
9929
9910
|
}
|
|
9930
9911
|
};
|
|
9931
9912
|
return { theme, warnings };
|
|
@@ -9837,7 +9837,7 @@ function unescXml(s) {
|
|
|
9837
9837
|
return "'";
|
|
9838
9838
|
default: {
|
|
9839
9839
|
const n = code.startsWith("#x") || code.startsWith("#X") ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
|
|
9840
|
-
return Number.
|
|
9840
|
+
return Number.isInteger(n) && n >= 0 && n <= 1114111 ? String.fromCodePoint(n) : whole;
|
|
9841
9841
|
}
|
|
9842
9842
|
}
|
|
9843
9843
|
});
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Script reference (L3)
|
|
2
|
+
|
|
3
|
+
Every bin in `bin/`, with its full contract. SKILL.md lists them one line each;
|
|
4
|
+
read the entry here before the first call of a bin you have not used in this task.
|
|
5
|
+
|
|
6
|
+
- `bin/doc-scan.cjs <file.pdf>` — per-page signals as JSON.
|
|
7
|
+
- `bin/doc-text.cjs <file.pdf>` — extract a PDF's text layer.
|
|
8
|
+
- `bin/pdf-render.cjs <file.pdf> <pages|all> [scale]` — render pages to PNG for vision (`pages` = `3` | `2-5` | `3,7-8` | `all`). `scale` (default `2` ≈ 144dpi) trades resolution for vision tokens: pass `1` (≈72dpi, ¼ the pixels ≈ ¼ the tokens) for slide-composition/layout QA; keep `2` when fine print must stay legible (formulas/tables). Reading previews is the priciest step in the deck loop — render cheap + read only what changed (a deck's final Office pass hands you its images: `deck-qa --final`).
|
|
9
|
+
- `bin/pdf-images.cjs <file.pdf> [pages]` — extract EMBEDDED images at native resolution with page placement (`pages` = `N` | `N-M` | `all`, default all). Prints a JSON manifest (page/index/bbox/size/path). Use this — not a full-page `pdf-render` — when you need the original figures/photos as their own files (e.g. rebuilding a deck). Vector/text-only pages yield nothing → fall back to `pdf-render`.
|
|
10
|
+
- `bin/pdf-tables.cjs <file.pdf> [page] [minCols]` — recover text-layer tables as rows (ruled/scanned tables need render + vision instead).
|
|
11
|
+
- `bin/ocr.cjs <image.png|jpg> [lang]` — read text off a scanned image (tesseract.js). For a scanned PDF, `pdf-render` a page first, then OCR the PNG. Language data downloads on first use / set `$TALOS_TESSDATA` for offline. **Prefer vision** when a vision model is available; OCR is the text-only fallback.
|
|
12
|
+
- `bin/xlsx-query.cjs <file> [--summary | --agg <sheet> <col> <op> | --sample <sheet> [n]]` — spreadsheet compute.
|
|
13
|
+
- `bin/doc-gen.cjs <docx|xlsx|pdf> <out> <spec.json> [--theme <theme.json>] [--topic <subject> | --palette <id>] [--no-preview]` — generate documents from scratch. DOCX/PDF body fields are exactly `paragraphs` and/or `blocks` (never `sections`); in a PDF, any http(s) URL written in body text, list items or table cells becomes a clickable link and wraps inside the page on its own — never split a URL by hand (there is no other link syntax); unknown fields and empty bodies fail with `DOC_SPEC_INVALID` before any output is written (`allowCoverOnly:true` is the explicit title-only opt-in). Correct the reported JSON path and rerun. `--theme` applies a presentation theme (colors/fonts/cover/header/footer — see rewrite.md "Themes"; built-ins in `themes/`). **`--topic <subject>` (when no `--theme`) picks a topic-adaptive palette** instead of the same default every time. `doc-gen pptx` exits 64: decks are built with html2pptx. `--palette <id>` forces a specific palette (`doc-gen --list-palettes` prints them). **`--accent <#hex>` / `--style <skin>` (docx/pdf)**: white-paper-black-text base with ONE brand color on H1/H2 + a faint table-header tint — the practical doc shape (a document is read/printed/edited, not projected, so body text stays near-black). `--accent` takes a literal hex; `--style <skin>` pulls just the `--accent` from `styles/<skin>/skin.css` (the deck skin's full palette is NOT carried onto paper). Precedence: `--theme` > spec `"theme"` > `--accent`/`--style` > `--palette`/`--topic` > default.
|
|
14
|
+
- `bin/doc-convert.cjs <in.docx|pptx|xlsx> <out.pdf> [--cache-dir <dir>]` — faithful docx/pptx/xlsx → PDF conversion. Engine precedence (best fidelity first, each downgrade tagged on stdout `[pdf (…)]` + warned on stderr): (1) **LibreOffice** if a system `soffice` is found — true layout render (page geometry, headers/footers, text-box positions, native charts), offline; (2) **LibreOffice sidecar** if `TALOS_OFFICE_URL` is set — same true-layout fidelity via a remote converter (e.g. Gotenberg), needs network; (3) **Chromium** — reconstructed HTML, content/structure/images preserved but in-document layout is not (pptx = one page per slide): the app's service in-app, else a headless Chromium; with none of these it exits 69 — say the PDF could not be made. `--cache-dir` (or `TALOS_OFFICE_CACHE_DIR`) reuses a content-hashed local LibreOffice result for the exact same immutable artifact; caching is opt-in so document bytes are not retained by default. Surface the resulting `[pdf (engine)]` caveat to the user. NOTE: spreadsheets (xlsx/xls/ods) render ONLY via (1)–(2), a real LibreOffice; Chromium does not rebuild spreadsheets, so with no LibreOffice available the command errors (never a degraded text-only sheet).
|
|
15
|
+
- `bin/doc-validate.cjs <file.docx|pptx|xlsx|pdf> [--strict] [--design-lint]` — structural validation gate: OOXML zip/parts/rels/Content_Types/XML well-formedness; a PDF must parse and hold at least one page (reports `pages` and a `pdf-parse-only` warning — how it looks is render-and-look's job; `--strict` / `--design-lint` are OOXML-only; `--strict` on a PDF adds a `strict-not-applicable` warning). Run after every generation or edit; exit 1 = broken, fix before delivering. `--strict` adds full ECMA-376 schema (XSD) validation of every recognised part — REQUIRED after custom-code (doc-run/html2pptx) or raw-XML output; a strict failure means the file is non-standard even if Office opens it. `--design-lint` (pptx only) adds an ADVISORY design pass — flags `text-only-slide` (a wall of text with no visual anchor), `low-contrast-text`, `placeholder-text` (a layout pasted but never filled — 替换为…/占位/Lorem still in the slide), `font-mismatch` (CJK text set in a Latin-only font → 豆腐; expected language defaults to the system locale, override with `TALOS_DECK_LANG`), `off-canvas`, `tiny-font`, `shape-overlap`; findings are warnings (never change the exit code) but are real design bugs — fix them. See deck-design.md "Design principles".
|
|
16
|
+
- `bin/deck-qa.cjs <file.pptx> [--json] [--rendered <office.pdf> | --final [--cache-dir <dir>]]` — MECHANICAL design QA (deterministic, token-free): reads slide XML and flags `off-canvas` (content clipped past the edge), `tiny-font` (<10pt), `shape-overlap` (colliding text), plus `text-only-slide` / `low-contrast-text` / `placeholder-text` / `font-mismatch` (CJK text in a Latin-only font → 豆腐). Advisory (exit 0 even with findings). `--json` → `{ok,findings:[{slide,code,message}]}` for the visual-QA loop. It is the CHEAP GATE before spending vision tokens — run it every QA round and fix all findings first; then do the vision critic + per-slide regeneration (deck-design.md "Verify before delivering"). `html2pptx` already prints this lint as `qa` after every build. **`--final` is the deck's delivery sign-off in one call**: strict XSD validation (failure exits 1), the lint, an Office render through `doc-convert` (cached), `render-overflow` / `render-collision` where Office re-laid the native text past its box, and `review` + `previews` — the only slides (findings + native charts) to look at again, already rendered at ≈72dpi (deck-design.md "Verify before delivering"). `--rendered <pdf>` is the same render check against a PDF you converted yourself; a page count that differs from the deck exits 1.
|
|
17
|
+
- `bin/doc-xml.cjs unpack <file> <dir>` / `pack <dir> <out>` — raw-XML escape hatch (pretty-printed unpack → edit → repack); always doc-validate afterwards. `replace <in.docx> <out.docx> <find> <replacement>` — cross-run character-offset replace (finds text even when Word split it over runs, preserves formatting; covers body + headers/footers) — try this FIRST for plain text edits before unpacking. `merge-runs <in.docx> <out.docx>` repairs run fragmentation when you do need manual XML edits.
|
|
18
|
+
- `bin/pptx-clone.cjs <template.pptx> <slideNo> <out.pptx> [replacements.json]` — clone a styled template slide once per data item, replacing `{{tokens}}` in the clone only (tokens split across runs are still found); `pptx-clone remove <in> <slideNo> <out>` drops a slide (e.g. the leftover token template). CAPACITY warnings on stderr mean the text likely overflows its slot — shorten the wording, never shrink the font. See rewrite.md.
|
|
19
|
+
- `bin/pptx-slide.cjs add <in.pptx> <out.pptx> [--layout N]` / `dup <in> <slideNo> <out>` / `remove <in> <slideNo> <out>` / `reorder <in> <order> <out>` — slide-lifecycle surgery on an EXISTING deck WITHOUT a `{{token}}` template: append a blank slide bound to a layout (inherits the deck's master/theme), duplicate or drop a slide, or permute deck order (`pptx-slide reorder in.pptx 3,1,2 out.pptx` — comma-list of current 1-based positions in the new sequence). Lossless & structural — parts/rels/Content_Types are bookkept for you, but in-slide TEXT/content edits still go through doc-xml. Always doc-validate.
|
|
20
|
+
- `bin/pptx-embed-fonts.cjs <in.pptx> <out.pptx> --font "<Typeface>=<path.ttf>" [--bold …] [--italic …]` — EMBED fonts into a deck (OOXML font embedding) so it looks identical on a machine lacking the font. Full-font embed → use for SMALL custom/brand fonts; NOT big CJK fonts (openers have system CJK, and the file would balloon). `<Typeface>` must match the deck's text-run font name. doc-validate after.
|
|
21
|
+
- `bin/pptx-theme.cjs <ref.pptx> [out.json]` — extract a theme.json (palette/fonts) from an existing deck, then `html2pptx --theme` (or `doc-gen --theme` for a matching docx/pdf) it: new content in that deck's look. Palette only — reusing actual layouts is pptx-clone/doc-fill territory.
|
|
22
|
+
- `bin/doc-fill.cjs <template.docx|pptx|xlsx> <values.json> <out>` — fill `{{placeholders}}` in an existing template, preserving its styling (see rewrite.md).
|
|
23
|
+
- `bin/pptx-tokenize.cjs <user.pptx> <template.pptx> [schema.json]` — turn a user's UPLOADED deck into a reusable `{{token}}` template: keeps its exact layouts/master/brand, replaces each content slot's text with a clean single-run token, and emits a `schema.json` cataloging every token (slide/role/type/multiline/original/capacity). Plain text boxes (no `<p:ph>`) are tokenized too. Then `doc-fill` it — fill EVERY token (pass `original` for slots you keep, else they blank). The B2 "reuse their layout" path; see rewrite.md.
|
|
24
|
+
- `bin/pptx-set-image.cjs <in.pptx> <slideNo> <rId> <image> <out.pptx>` — swap the picture behind a slide's `rId` (from pptx-tokenize's image tokens) with a new image, keeping the frame (position/size) put. add-and-repoint (old media orphaned). Aspect-mismatch STRETCHES — pre-crop to the slot ratio. Always doc-validate.
|
|
25
|
+
- `bin/docx-annotate.cjs comment <in.docx> <out.docx> <comments.json>` / `suggest <in.docx> <out.docx> <author> <text>` / `accept <in.docx> <out.docx>` / `redline-check <original.docx> <edited.docx>` — add Word comments (paragraph-anchored, threaded replies via `replyTo`), a tracked insertion (reviewer suggestion), accept ALL tracked changes, or PROVE an edited file changed nothing outside tracked changes (run after every redline edit; non-zero exit = an untracked content change slipped in).
|
|
26
|
+
- `bin/pdf-form.cjs list <in.pdf>` / `fill <in.pdf> <values.json> <out.pdf> [--flatten]` / `overlay <filled.pdf> <out.pdf>` — inspect/fill PDF forms; `overlay` boxes every field in red for a render-and-look fill check (see pdf.md).
|
|
27
|
+
- `bin/pdf-ops.cjs merge <out> <in…>` / `split <in> <from-to> <out>` / `rotate <in> <deg> <out> [pages]` / `watermark <in> <text> <out>` / `stamp <in.pdf> <stamps.json> <out.pdf>` / `encrypt <in> <out> <user-pw> [owner-pw]` / `decrypt <in> <pw> <out>` — PDF page ops, coordinate text stamping onto a flat (non-form) PDF (see pdf.md), and AES-256 password protection.
|
|
28
|
+
- `bin/xlsx-recalc.cjs <in.xlsx> <out.xlsx>` — re-evaluate all formulas (HyperFormula) and cache the results, keeping the formulas. Use when a spreadsheet's formula cells show stale/blank values.
|
|
29
|
+
- `bin/doc-run.cjs <script.js> [args…]` — run a custom Node script with the engine's libraries bundled in (`require("pptxgenjs"|"docx"|"xlsx"|"exceljs"|"pdf-lib"|"jszip"|"pizzip"|"hyperformula")` guaranteed on any machine; `--list` prints the registry; nothing else is installed). For a **formatted workbook** (fills, font colors, borders, merged cells) use `exceljs` — `xlsx` (SheetJS community) writes no cell styles at all and drops them without an error; `doc-gen xlsx` covers only a bold header, column widths, number formats and formulas. The long-tail escape hatch when no verb fits — see rewrite.md "Custom scripts"; ALWAYS doc-validate the output.
|
|
30
|
+
- `bin/html2pptx.cjs <deck.html|snapshot.layout.json> [out.pptx|out.pdf] [--style <id>] [--theme-v2] [--media-style <id>] [--backdrop <variant>] [--editable] [--rasterize] [--preview-dir <dir>] [--preview-pages <all|N,N-M>]` — **THE deck path**: write slide HTML (`<section class="slide" data-layout="…">` on a 1280×720 canvas), Chromium lays it out → NATIVE editable pptx. An `out.pdf` prints the same styled deck instead: one 16:9 vector page per slide, text selectable, links clickable (HTML input only; not with `--editable`/`--rasterize`/`--notes-doc`). `--preview-dir` is a preview-only fast path that writes slide PNGs directly from the styled HTML and skips PPTX/LibreOffice; use all pages once, then `--preview-pages` for changed pages. `--theme-v2` composes the selected family's existing paint with its semantic component profile, default media style and bound art direction; `--media-style` overrides only the media treatment. Entries in a `classic-v1` catalog collection reject both flags; omit them for the frozen V1 path. `--backdrop <variant>` picks a signature backdrop. **DEFAULT = hybrid**: each slide's visuals bake into a full-fidelity raster backdrop with native EDITABLE text on top; auto-falls back to plain native if backdrop capture is unavailable. `--editable` = plain native only (effects flatten). `--rasterize` = full image slides (ZERO editability — tell the user). Advisory warnings on hardcoded brand colors + repeated layouts (P3). Runs in the app's Chromium OR bundled headless Chromium (Playwright) — works off-app (CI/cloud); layout JSON works anywhere. **Tailwind + `size="1".."7"` scale available** (vendored offline). See theme-system.md + deck-design.md + layouts/README.md. `[--theme <theme.json>]` is a separate legacy document recolor path.
|
|
31
|
+
- `bin/deck-build.cjs <spec.json> <deck.html>` — **THE FAST deck path** for catalog slides: emit a COMPACT spec with `style`, independent `layoutPack`, and per-slide `intent` + `layout` + content. Modern entries add `themeVersion:2` plus optional `mediaStyle`/`decorRhythm`; frozen `classic-v1` entries must omit those fields and use only pre-V2 layouts. Use `layouts/profiles.json` to match content intent before selecting geometry; under V2 the same intent derives the art scene and deck-level decoration rhythm. The CLI warns on an intent/layout mismatch and errors on an unknown intent. `scenario` (the confirmed scenario) decides whether the family's motion preset is applied — read decks (内审/报告/培训…) get none, `motion: true|false` overrides, and the result JSON reports `motion: {on, reason}` (deck-design.md "Motion"). `layoutPack:"none"` forces shared layouts; omitting `layoutPack` preserves the old behavior where a `kind:"pack"` style implicitly uses its same-name layouts. `slots`, `rows`, and `chart` are still validated against `layouts/slots.json`; any unknown layout/slot/field or missing figure input exits 1 and writes nothing. See theme-system.md for the full schema and compatibility contract.
|
|
32
|
+
|
|
33
|
+
All scripts are Node and print a machine-readable result plus a human note. On a
|
|
34
|
+
missing dependency or unreadable file they exit non-zero with a clear message —
|
|
35
|
+
report that to the user, do not fabricate content.
|
|
@@ -269,24 +269,23 @@ that matches its content structure (by P1/P2/P3, `layouts/README.md`), **copy th
|
|
|
269
269
|
content** — leave color to the skin (never hardcode a brand hex). Render
|
|
270
270
|
`bin/html2pptx.cjs deck.html out.pptx --style <skin>`. Where the catalog doesn't fit
|
|
271
271
|
(bespoke hero covers, layered architecture, magazine) author bespoke HTML per
|
|
272
|
-
**deck-design.md** (still using the skin's semantic classes).
|
|
273
|
-
|
|
274
|
-
structured-spec deck is wanted.
|
|
272
|
+
**deck-design.md** (still using the skin's semantic classes). With no browser at
|
|
273
|
+
all, say so and stop — there is no spec-deck fallback.
|
|
275
274
|
|
|
276
275
|
### 4.1 Content structure → layout
|
|
277
276
|
|
|
278
277
|
| Content structure | Core elements | → `layouts/*` (P1/P2/P3) |
|
|
279
278
|
|---|---|---|
|
|
280
|
-
| **Data / Trend** | 数据标题 + 指标 + 一句数据故事 | `chart-kpi`
|
|
281
|
-
| **Single impact** | 一个关键数字 + 上下文 | `big-stat`
|
|
282
|
-
| **Argument / Concept+visual** | 论点/定义 + 3–4 证据/组件 | `split-two-col`
|
|
283
|
-
| **Cards (并列要点)** | 3 个并列要点 | `cards-3`
|
|
284
|
-
| **Comparison** | 框架 + 2–3 选项 × 维度 + 推荐 |
|
|
285
|
-
| **Process** | 目标 + 3–5 步骤 |
|
|
286
|
-
| **Framework/Matrix** | 两轴 + 四象限 |
|
|
287
|
-
| **Case Study** | 背景/挑战/方案/前后 | `split-two-col` + `big-stat`
|
|
279
|
+
| **Data / Trend** | 数据标题 + 指标 + 一句数据故事 | `chart-kpi` |
|
|
280
|
+
| **Single impact** | 一个关键数字 + 上下文 | `big-stat` |
|
|
281
|
+
| **Argument / Concept+visual** | 论点/定义 + 3–4 证据/组件 | `split-two-col` |
|
|
282
|
+
| **Cards (并列要点)** | 3 个并列要点 | `cards-3` |
|
|
283
|
+
| **Comparison** | 框架 + 2–3 选项 × 维度 + 推荐 | `comparison` |
|
|
284
|
+
| **Process** | 目标 + 3–5 步骤 | `steps` / `steps-vertical` |
|
|
285
|
+
| **Framework/Matrix** | 两轴 + 四象限 | `matrix-2x2` |
|
|
286
|
+
| **Case Study** | 背景/挑战/方案/前后 | `split-two-col` + `big-stat` |
|
|
288
287
|
|
|
289
|
-
Role pages: `cover`
|
|
288
|
+
Role pages: `cover` · `toc` · `section` · `ending` (and their variants in `layouts/README.md`). Anything no layout fits → a bespoke slide (deck-design.md).
|
|
290
289
|
|
|
291
290
|
### 4.2 Pick the exact template — priority order (P1 → P2 → P3)
|
|
292
291
|
|
|
@@ -296,7 +295,7 @@ this **priority order** — P1 outranks P2 outranks P3; never trade layout-fit f
|
|
|
296
295
|
- **P1 — Layout match (most important): image presence + position.** No image → a
|
|
297
296
|
text-only template (`cards-3` / a text-column split). Image present → `split-two-col`
|
|
298
297
|
with the visual on the side it reads best (mirror the `.tpl` for image-left). Data →
|
|
299
|
-
`chart-kpi` (native `data-chart`). Never force text into an image slot or an image
|
|
298
|
+
`chart-kpi` (chart choice — inline SVG vs native `data-chart` — per deck-design.md "Charts"). Never force text into an image slot or an image
|
|
300
299
|
into a text slot; delete an unused image slot rather than leaving a placeholder.
|
|
301
300
|
- **P2 — Density match: point count → variant.** 1–2 points / one hero number →
|
|
302
301
|
spacious·centered (`big-stat`, hero cover); 3–4 points → balanced two-column
|
|
@@ -330,7 +329,7 @@ this **priority order** — P1 outranks P2 outranks P3; never trade layout-fit f
|
|
|
330
329
|
- **Rich backgrounds** — never pure white/black; theme gradient + a decorative motif.
|
|
331
330
|
**≥1–2 dark-accent slides** across the deck (Visual Rhythm).
|
|
332
331
|
- **≥2 decorative/visual elements per content slide**; **charts > text for data**, one
|
|
333
|
-
insight per chart (native `data-chart
|
|
332
|
+
insight per chart (SVG or native `data-chart` per deck-design.md "Charts"), highlight the key datapoint, no junk.
|
|
334
333
|
- **Emotional opening & closing** — cover and ending are STATEMENTS (feeling, not
|
|
335
334
|
facts): strong visual + few powerful words.
|
|
336
335
|
- **Visual diversity** — enforced by P3 (§4.2); a deck of one repeated layout reads as
|
|
@@ -346,10 +345,12 @@ Use a two-stage loop; do not pay the Office cold-start cost on every visual edit
|
|
|
346
345
|
`bin/html2pptx.cjs deck.html --preview-dir <dir> [--preview-pages <spec>]` and inspect every page on
|
|
347
346
|
the first pass. Fix HTML/CSS, then preview only changed pages. Repeat until the HTML
|
|
348
347
|
composition is clean.
|
|
349
|
-
2. **Delivery loop (PPTX, once after HTML is clean):**
|
|
350
|
-
`
|
|
351
|
-
|
|
352
|
-
|
|
348
|
+
2. **Delivery loop (PPTX, once after HTML is clean):** build the PPTX and fix the
|
|
349
|
+
`qa` findings the build prints. Then sign off with one `deck-qa deck.pptx --final`:
|
|
350
|
+
strict validation, the Office render and its drift check, and `previews` — the
|
|
351
|
+
only slides to look at again; every other slide was already judged in the HTML
|
|
352
|
+
preview (deck-design.md "Verify before delivering", step 3, and its
|
|
353
|
+
read-everything exceptions). If that pass finds drift, fix and rerun only as
|
|
353
354
|
required; never use PPTX→PDF as the routine authoring preview.
|
|
354
355
|
|
|
355
356
|
Assume there are defects and inspect text overflow/clipping, collisions, tofu/blank
|