@taloswork/ose-shared 0.1.11 → 0.1.13

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 (159) hide show
  1. package/package.json +3 -3
  2. package/skills/document/SKILL.md +135 -219
  3. package/skills/document/art-directions/base.css +27 -2
  4. package/skills/document/art-directions/cinematic-depth.css +3 -1
  5. package/skills/document/art-directions/collage-paper.css +5 -1
  6. package/skills/document/art-directions/editorial-cut.css +5 -1
  7. package/skills/document/art-directions/geometric-pop.css +6 -0
  8. package/skills/document/art-directions/gradient-mesh.css +5 -1
  9. package/skills/document/art-directions/illustrated-doodle.css +5 -0
  10. package/skills/document/art-directions/luxury-ornament.css +4 -1
  11. package/skills/document/art-directions/maximalist-blocks.css +6 -0
  12. package/skills/document/art-directions/technical-grid.css +4 -0
  13. package/skills/document/assets/fonts/fonts.json +2 -2
  14. package/skills/document/assets/pdf-fonts/NotoSansSC-PDF-Bold.ttf +0 -0
  15. package/skills/document/assets/pdf-fonts/NotoSansSC-PDF-Regular.ttf +0 -0
  16. package/skills/document/assets/pdf-fonts/OFL.txt +93 -0
  17. package/skills/document/assets/pdf-fonts/README.md +35 -0
  18. package/skills/document/assets/tessdata/LICENSE-Apache-2.0.txt +202 -0
  19. package/skills/document/assets/tessdata/README.md +21 -0
  20. package/skills/document/assets/tessdata/chi_sim.traineddata.gz +0 -0
  21. package/skills/document/assets/tessdata/eng.traineddata.gz +0 -0
  22. package/skills/document/bin/deck-build.cjs +12563 -83
  23. package/skills/document/bin/deck-chart.cjs +771 -0
  24. package/skills/document/bin/deck-plan.cjs +12414 -0
  25. package/skills/document/bin/deck-qa.cjs +1075 -266
  26. package/skills/document/bin/deck-scenario.cjs +1363 -0
  27. package/skills/document/bin/deck-styles.cjs +1604 -0
  28. package/skills/document/bin/doc-convert.cjs +308 -292
  29. package/skills/document/bin/doc-fill.cjs +73264 -192
  30. package/skills/document/bin/doc-gen.cjs +2491 -312
  31. package/skills/document/bin/doc-run.cjs +162 -24
  32. package/skills/document/bin/doc-scan.cjs +13 -0
  33. package/skills/document/bin/doc-text.cjs +13 -0
  34. package/skills/document/bin/doc-validate.cjs +838 -68
  35. package/skills/document/bin/doc-xml.cjs +9 -5
  36. package/skills/document/bin/docx-annotate.cjs +95 -19
  37. package/skills/document/bin/docx-defaults.cjs +9907 -0
  38. package/skills/document/bin/fontkit.cjs +1 -0
  39. package/skills/document/bin/html2pptx.cjs +36622 -31394
  40. package/skills/document/bin/ocr-worker.cjs +1 -0
  41. package/skills/document/bin/ocr.cjs +131 -40
  42. package/skills/document/bin/pdf-form.cjs +682 -177
  43. package/skills/document/bin/pdf-images.cjs +13 -0
  44. package/skills/document/bin/pdf-ops.cjs +642 -199
  45. package/skills/document/bin/pdf-render.cjs +19 -3
  46. package/skills/document/bin/pdf-tables.cjs +13 -0
  47. package/skills/document/bin/pptx-clone.cjs +3 -3
  48. package/skills/document/bin/pptx-notes.cjs +10038 -0
  49. package/skills/document/bin/pptx-slide.cjs +3 -3
  50. package/skills/document/bin/pptx-tokenize.cjs +19 -4
  51. package/skills/document/bin/template-skin.cjs +10910 -0
  52. package/skills/document/bin/xlsx-query.cjs +192 -61
  53. package/skills/document/bin-reference.md +61 -33
  54. package/skills/document/catalog.json +33 -32
  55. package/skills/document/content-pipeline.md +179 -257
  56. package/skills/document/deck-design.md +378 -638
  57. package/skills/document/generated-visuals.md +99 -0
  58. package/skills/document/layouts/MAINTAINERS.md +33 -0
  59. package/skills/document/layouts/README.md +49 -36
  60. package/skills/document/layouts/big-stat.tpl +1 -1
  61. package/skills/document/layouts/capacity.json +1 -0
  62. package/skills/document/layouts/cards-3.tpl +1 -1
  63. package/skills/document/layouts/chart-full.tpl +1 -1
  64. package/skills/document/layouts/chart-kpi.tpl +1 -1
  65. package/skills/document/layouts/comparison.tpl +1 -1
  66. package/skills/document/layouts/data-table.tpl +1 -1
  67. package/skills/document/layouts/funnel.tpl +1 -1
  68. package/skills/document/layouts/gantt.tpl +1 -1
  69. package/skills/document/layouts/hub-flow.tpl +4 -0
  70. package/skills/document/layouts/icon-row.tpl +1 -1
  71. package/skills/document/layouts/image-grid.tpl +1 -1
  72. package/skills/document/layouts/matrix-2x2.tpl +1 -1
  73. package/skills/document/layouts/mock-qa.tpl +4 -0
  74. package/skills/document/layouts/profiles.json +2 -2
  75. package/skills/document/layouts/pyramid.tpl +1 -1
  76. package/skills/document/layouts/radar.tpl +1 -1
  77. package/skills/document/layouts/ring-progress.tpl +1 -1
  78. package/skills/document/layouts/slots.json +1 -1
  79. package/skills/document/layouts/steps-vertical.tpl +1 -1
  80. package/skills/document/layouts/steps.tpl +1 -1
  81. package/skills/document/layouts/three-kpi.tpl +1 -1
  82. package/skills/document/layouts/timeline.tpl +1 -1
  83. package/skills/document/layouts/toc-panel.tpl +2 -2
  84. package/skills/document/layouts/two-kpi.tpl +2 -2
  85. package/skills/document/layouts/versus.tpl +1 -1
  86. package/skills/document/motion/README.md +3 -2
  87. package/skills/document/office.md +3 -3
  88. package/skills/document/packs/README.md +1 -1
  89. package/skills/document/packs/board-brief/backdrop.html +2 -0
  90. package/skills/document/packs/board-brief/pack.css +2 -1
  91. package/skills/document/packs/mckinsey-photo/backdrop.html +2 -0
  92. package/skills/document/packs/mckinsey-photo/pack.css +2 -1
  93. package/skills/document/packs/press-room/backdrop.html +2 -0
  94. package/skills/document/packs/press-room/pack.css +2 -1
  95. package/skills/document/packs/prismatic-3d/backdrop.html +2 -0
  96. package/skills/document/packs/prismatic-3d/pack.css +4 -1
  97. package/skills/document/pdf.md +60 -43
  98. package/skills/document/rewrite.md +257 -309
  99. package/skills/document/scenarios.json +1307 -0
  100. package/skills/document/scenarios.schema.json +104 -0
  101. package/skills/document/styles/README.md +15 -25
  102. package/skills/document/styles/_backdrops/rimlight.html +3 -2
  103. package/skills/document/styles/_contract-defaults.css +31 -2
  104. package/skills/document/styles/_samples/_groups/biz-premium.webp +0 -0
  105. package/skills/document/styles/_samples/mckinsey.webp +0 -0
  106. package/skills/document/styles/anthropic/skin.css +6 -3
  107. package/skills/document/styles/apple/skin.css +4 -3
  108. package/skills/document/styles/bistro-warm/backdrop.html +2 -0
  109. package/skills/document/styles/bistro-warm/skin.css +4 -1
  110. package/skills/document/styles/black-gold/skin.css +6 -5
  111. package/skills/document/styles/bold-clash/backdrop.html +2 -0
  112. package/skills/document/styles/bold-clash/skin.css +2 -1
  113. package/skills/document/styles/branding/skin.css +4 -4
  114. package/skills/document/styles/campus-teal/backdrop.html +2 -0
  115. package/skills/document/styles/campus-teal/skin.css +2 -1
  116. package/skills/document/styles/cinereel/skin.css +7 -3
  117. package/skills/document/styles/clinic-care/backdrop.html +2 -0
  118. package/skills/document/styles/clinic-care/skin.css +2 -1
  119. package/skills/document/styles/cool/skin.css +6 -5
  120. package/skills/document/styles/dark-glass/backdrop.html +2 -0
  121. package/skills/document/styles/dark-glass/skin.css +2 -1
  122. package/skills/document/styles/gov-brief/backdrop.html +2 -0
  123. package/skills/document/styles/gov-brief/skin.css +2 -1
  124. package/skills/document/styles/grid-tech/backdrop.html +2 -0
  125. package/skills/document/styles/grid-tech/skin.css +2 -1
  126. package/skills/document/styles/hand-drawn/skin.css +3 -3
  127. package/skills/document/styles/ivory-gold/skin.css +7 -6
  128. package/skills/document/styles/legal-slate/backdrop.html +2 -0
  129. package/skills/document/styles/legal-slate/skin.css +2 -1
  130. package/skills/document/styles/magazine-ink/skin.css +4 -3
  131. package/skills/document/styles/mckinsey/backdrop.html +7 -5
  132. package/skills/document/styles/mckinsey/skin.css +2 -1
  133. package/skills/document/styles/memphis/backdrop.html +2 -0
  134. package/skills/document/styles/memphis/skin.css +2 -1
  135. package/skills/document/styles/mono/skin.css +6 -2
  136. package/skills/document/styles/neon-cyber/backdrop.html +2 -0
  137. package/skills/document/styles/neon-cyber/skin.css +2 -1
  138. package/skills/document/styles/olive-gold/skin.css +7 -6
  139. package/skills/document/styles/organic-earth/backdrop.html +2 -0
  140. package/skills/document/styles/organic-earth/skin.css +2 -1
  141. package/skills/document/styles/pastel-saas/backdrop.html +2 -0
  142. package/skills/document/styles/pastel-saas/skin.css +2 -1
  143. package/skills/document/styles/portfolio-noir/backdrop.html +2 -0
  144. package/skills/document/styles/portfolio-noir/skin.css +2 -1
  145. package/skills/document/styles/retro-press/backdrop.html +2 -0
  146. package/skills/document/styles/retro-press/skin.css +2 -1
  147. package/skills/document/styles/sport-arena/backdrop.html +2 -0
  148. package/skills/document/styles/sport-arena/skin.css +4 -1
  149. package/skills/document/styles/swiss-grid/skin.css +10 -5
  150. package/skills/document/styles/wedding-blush/backdrop.html +2 -0
  151. package/skills/document/styles/wedding-blush/skin.css +4 -1
  152. package/skills/document/theme-system.md +85 -77
  153. package/skills/document/themes/README.md +7 -5
  154. package/skills/document/vision.md +8 -5
  155. package/skills/document/xlsx.md +15 -25
  156. package/skills/grill-me/SKILL.md +6 -0
  157. package/skills/grill-me/agents/openai.yaml +5 -0
  158. package/skills/grilling/SKILL.md +28 -0
  159. package/skills/grilling/agents/openai.yaml +3 -0
@@ -9863,7 +9863,7 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
9863
9863
  let flat = "";
9864
9864
  for (const m of para.matchAll(textRe)) {
9865
9865
  const text = unescXml(m[2]);
9866
- segs.push({ el: m[0], attrs: m[1] ?? "", text, start: flat.length });
9866
+ segs.push({ el: m[0], at: m.index, attrs: m[1] ?? "", text, start: flat.length });
9867
9867
  flat += text;
9868
9868
  }
9869
9869
  if (!flat.includes(find))
@@ -9892,7 +9892,7 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
9892
9892
  return pieces.join("");
9893
9893
  });
9894
9894
  let out = para;
9895
- for (let i = 0; i < segs.length; i++) {
9895
+ for (let i = segs.length - 1; i >= 0; i--) {
9896
9896
  if (newTexts[i] === segs[i].text)
9897
9897
  continue;
9898
9898
  const escaped = escXml(newTexts[i]);
@@ -9900,7 +9900,7 @@ function replaceTextAcrossRuns(xml, find, replacement, opts) {
9900
9900
  if (textTag === "w:t" && /^\s|\s$/.test(newTexts[i]) && !/xml:space="preserve"/.test(attrs)) {
9901
9901
  attrs = `${attrs} xml:space="preserve"`;
9902
9902
  }
9903
- out = out.replace(segs[i].el, `<${textTag}${attrs}>${escaped}</${textTag}>`);
9903
+ out = out.slice(0, segs[i].at) + `<${textTag}${attrs}>${escaped}</${textTag}>` + out.slice(segs[i].at + segs[i].el.length);
9904
9904
  }
9905
9905
  return out;
9906
9906
  });
@@ -10029,7 +10029,7 @@ async function replaceTextInDocx(bytes, find, replacement) {
10029
10029
  }
10030
10030
  if (!zip.file("word/document.xml"))
10031
10031
  throw new Error("not a Word document (word/document.xml missing)");
10032
- const parts = Object.keys(zip.files).filter((p) => p === "word/document.xml" || /^word\/(?:header|footer)\d+\.xml$/.test(p));
10032
+ const parts = Object.keys(zip.files).filter((p) => p === "word/document.xml" || p === "word/footnotes.xml" || p === "word/endnotes.xml" || /^word\/(?:header|footer)\d+\.xml$/.test(p));
10033
10033
  let count = 0;
10034
10034
  for (const part of parts) {
10035
10035
  const xml = await zip.file(part).async("string");
@@ -10100,7 +10100,11 @@ async function main() {
10100
10100
  const r = await replaceTextInDocx(bytes, find, replacement);
10101
10101
  await (0, import_promises2.writeFile)(out, Buffer.from(r.bytes));
10102
10102
  process.stdout.write(JSON.stringify({ ok: true, out, replaced: r.count }) + "\n");
10103
- if (r.count === 0) process.stderr.write("doc-xml: no occurrence found (text may differ or sit in a table/textbox part)\n");
10103
+ if (r.count === 0) {
10104
+ process.stderr.write(
10105
+ "doc-xml: no occurrence found \u2014 the text must match exactly within one paragraph (body, tables, headers/footers and footnotes/endnotes are searched; comments are not)\n"
10106
+ );
10107
+ }
10104
10108
  return;
10105
10109
  }
10106
10110
  } catch (e) {
@@ -9830,6 +9830,7 @@ function escXml(s) {
9830
9830
  var import_node_crypto = require("node:crypto");
9831
9831
  var W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
9832
9832
  var W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
9833
+ var MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
9833
9834
  var W15_NS = "http://schemas.microsoft.com/office/word/2012/wordml";
9834
9835
  var W16CID_NS = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
9835
9836
  var MODERN_PARTS = [
@@ -9926,11 +9927,34 @@ async function ensureCommentsWiring(zip) {
9926
9927
  }
9927
9928
  const existing = zip.file("word/comments.xml");
9928
9929
  if (!existing) {
9929
- zip.file("word/comments.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments xmlns:w="${W_NS}" xmlns:w14="${W14_NS}"></w:comments>`);
9930
+ zip.file("word/comments.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments xmlns:w="${W_NS}" xmlns:mc="${MC_NS}" xmlns:w14="${W14_NS}" mc:Ignorable="w14"></w:comments>`);
9930
9931
  return 0;
9931
9932
  }
9932
9933
  const body = await existing.async("string");
9933
- return (body.match(/<w:comment\b/g) ?? []).length;
9934
+ return maxId(body, /<w:comment\b[^>]*?\sw:id="(\d+)"/g) + 1;
9935
+ }
9936
+ function maxId(xml, re) {
9937
+ let max = -1;
9938
+ for (const m of xml.matchAll(re))
9939
+ max = Math.max(max, Number(m[1]));
9940
+ return max;
9941
+ }
9942
+ function prepareCommentsRoot(xml) {
9943
+ xml = xml.replace(/<w:comments\b([^>]*?)\s*\/>/, "<w:comments$1></w:comments>");
9944
+ return xml.replace(/<w:comments\b[^>]*>/, (open) => {
9945
+ let tag = open.slice(0, -1);
9946
+ if (!/\sxmlns:w14=/.test(tag))
9947
+ tag += ` xmlns:w14="${W14_NS}"`;
9948
+ if (!/\sxmlns:mc=/.test(tag))
9949
+ tag += ` xmlns:mc="${MC_NS}"`;
9950
+ const ign = /\smc:Ignorable="([^"]*)"/.exec(tag);
9951
+ if (!ign)
9952
+ tag += ` mc:Ignorable="w14"`;
9953
+ else if (!ign[1].split(/\s+/).includes("w14")) {
9954
+ tag = tag.replace(ign[0], ` mc:Ignorable="${`${ign[1]} w14`.trim()}"`);
9955
+ }
9956
+ return `${tag}>`;
9957
+ });
9934
9958
  }
9935
9959
  async function addComments(bytes, comments) {
9936
9960
  if (!comments.length)
@@ -9943,12 +9967,13 @@ async function addComments(bytes, comments) {
9943
9967
  let doc = documentXml;
9944
9968
  let commentsBody = "";
9945
9969
  const skipped = [];
9970
+ const warnings = [];
9946
9971
  const paraIds = comments.map(() => void 0);
9947
9972
  const modern = [];
9948
9973
  comments.forEach((c, batchIdx) => {
9949
9974
  const target = c.anchor ? paras.find((p) => textOf(p.full).includes(c.anchor)) ?? null : paras[0];
9950
9975
  if (c.anchor && !target) {
9951
- skipped.push(c.anchor);
9976
+ skipped.push({ index: batchIdx, anchor: c.anchor });
9952
9977
  return;
9953
9978
  }
9954
9979
  const id = startId++;
@@ -9958,27 +9983,67 @@ async function addComments(bytes, comments) {
9958
9983
  doc = doc.replace(target.full, wrapped);
9959
9984
  target.full = wrapped;
9960
9985
  commentsBody += commentXml(id, c, paraId);
9961
- const parentParaId = c.replyTo != null ? paraIds[c.replyTo] : void 0;
9986
+ let parentParaId;
9987
+ if (c.replyTo != null) {
9988
+ parentParaId = paraIds[c.replyTo];
9989
+ if (!parentParaId) {
9990
+ warnings.push(c.replyTo >= 0 && c.replyTo < comments.length && skipped.some((s) => s.index === c.replyTo) ? `comment ${batchIdx}: replyTo ${c.replyTo} was skipped (anchor not found) \u2014 written as a top-level comment` : `comment ${batchIdx}: replyTo ${c.replyTo} does not refer to an earlier written comment in this batch \u2014 written as a top-level comment`);
9991
+ }
9992
+ }
9962
9993
  modern.push({ paraId, parentParaId, author: c.author });
9963
9994
  });
9964
9995
  if (skipped.length === comments.length)
9965
- throw new Error(`no anchor matched: ${skipped.join(", ")}`);
9996
+ throw new Error(`no anchor matched: ${skipped.map((s) => s.anchor).join(", ")}`);
9966
9997
  zip.file("word/document.xml", doc);
9967
- const cx = await zip.file("word/comments.xml").async("string");
9968
- const cxWithNs = cx.includes("xmlns:w14") ? cx : cx.replace(`xmlns:w="${W_NS}"`, `xmlns:w="${W_NS}" xmlns:w14="${W14_NS}"`);
9969
- zip.file("word/comments.xml", cxWithNs.replace("</w:comments>", `${commentsBody}</w:comments>`));
9998
+ const cx = prepareCommentsRoot(await zip.file("word/comments.xml").async("string"));
9999
+ if (!cx.includes("</w:comments>"))
10000
+ throw new Error("word/comments.xml has no <w:comments> root");
10001
+ const cxOut = cx.replace("</w:comments>", `${commentsBody}</w:comments>`);
10002
+ const bodies = new Set([...cxOut.matchAll(/<w:comment\b[^>]*?\sw:id="(\d+)"/g)].map((m) => m[1]));
10003
+ const dangling = [...new Set([...doc.matchAll(/<w:comment(?:RangeStart|RangeEnd|Reference)\b[^>]*?\sw:id="(\d+)"/g)].map((m) => m[1]))].filter((id) => !bodies.has(id));
10004
+ if (dangling.length)
10005
+ throw new Error(`internal: comment id(s) ${dangling.join(", ")} referenced in document.xml have no <w:comment> body`);
10006
+ zip.file("word/comments.xml", cxOut);
9970
10007
  await writeModernCommentParts(zip, modern);
9971
10008
  const out = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
9972
- return new Uint8Array(out);
10009
+ return { bytes: new Uint8Array(out), written: comments.length - skipped.length, skipped, warnings };
10010
+ }
10011
+ function bodySectPrOffset(documentXml, bodyEnd) {
10012
+ let found = -1;
10013
+ let pPrDepth = 0;
10014
+ let changeDepth = 0;
10015
+ const re = /<(\/?)w:(pPr|sectPrChange|sectPr)\b[^>]*?(\/?)>/g;
10016
+ re.lastIndex = Math.max(0, documentXml.indexOf("<w:body"));
10017
+ let m;
10018
+ while ((m = re.exec(documentXml)) && m.index < bodyEnd) {
10019
+ const [, close, name, selfClose] = m;
10020
+ if (selfClose) {
10021
+ if (name === "sectPr" && pPrDepth === 0 && changeDepth === 0)
10022
+ found = m.index;
10023
+ continue;
10024
+ }
10025
+ const d = close ? -1 : 1;
10026
+ if (name === "pPr")
10027
+ pPrDepth += d;
10028
+ else if (name === "sectPrChange")
10029
+ changeDepth += d;
10030
+ else if (!close && pPrDepth === 0 && changeDepth === 0)
10031
+ found = m.index;
10032
+ }
10033
+ return found;
9973
10034
  }
9974
10035
  async function addTrackedInsertion(bytes, ins) {
9975
10036
  const { zip, documentXml } = await loadDocx(bytes);
9976
- const date = ins.date ?? "1970-01-01T00:00:00Z";
9977
- const para = `<w:p><w:ins w:id="900001" w:author="${escXml(ins.author)}" w:date="${escXml(date)}"><w:r><w:t xml:space="preserve">${escXml(ins.text)}</w:t></w:r></w:ins></w:p>`;
9978
- const doc = documentXml.includes("<w:sectPr") ? documentXml.replace(/(<w:sectPr[\s>])/, `${para}$1`) : documentXml.replace("</w:body>", `${para}</w:body>`);
9979
- if (doc === documentXml)
10037
+ const date = escXml(ins.date ?? "1970-01-01T00:00:00Z");
10038
+ const author = escXml(ins.author);
10039
+ const base = maxId(documentXml, /\sw:id="(\d+)"/g) + 1;
10040
+ const para = `<w:p><w:pPr><w:rPr><w:ins w:id="${base}" w:author="${author}" w:date="${date}"/></w:rPr></w:pPr><w:ins w:id="${base + 1}" w:author="${author}" w:date="${date}"><w:r><w:t xml:space="preserve">${escXml(ins.text)}</w:t></w:r></w:ins></w:p>`;
10041
+ const bodyEnd = documentXml.lastIndexOf("</w:body>");
10042
+ if (bodyEnd < 0)
9980
10043
  throw new Error("could not locate document body");
9981
- zip.file("word/document.xml", doc);
10044
+ const sect = bodySectPrOffset(documentXml, bodyEnd);
10045
+ const at = sect >= 0 ? sect : bodyEnd;
10046
+ zip.file("word/document.xml", documentXml.slice(0, at) + para + documentXml.slice(at));
9982
10047
  const out = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
9983
10048
  return new Uint8Array(out);
9984
10049
  }
@@ -10095,10 +10160,12 @@ function die(msg, code = 1) {
10095
10160
  }
10096
10161
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
10097
10162
  async function main() {
10098
- const [mode, inDocx, out, a, b] = process.argv.slice(2);
10163
+ const argv = process.argv.slice(2);
10164
+ const strictAnchors = argv.includes("--strict-anchors");
10165
+ const [mode, inDocx, out, a, b] = argv.filter((x) => x !== "--strict-anchors");
10099
10166
  try {
10100
10167
  if (mode === "comment") {
10101
- if (!inDocx || !out || !a) die("usage: docx-annotate comment <in.docx> <out.docx> <comments.json>", 64);
10168
+ if (!inDocx || !out || !a) die("usage: docx-annotate comment <in.docx> <out.docx> <comments.json> [--strict-anchors]", 64);
10102
10169
  let comments;
10103
10170
  try {
10104
10171
  comments = JSON.parse(await (0, import_promises.readFile)(a, "utf8"));
@@ -10107,9 +10174,18 @@ async function main() {
10107
10174
  }
10108
10175
  if (!Array.isArray(comments) || !comments.length) die("comments json must be a non-empty array", 64);
10109
10176
  const bytes = new Uint8Array(await (0, import_promises.readFile)(inDocx).catch((e) => die(`cannot read ${inDocx}: ${e.message}`)));
10110
- const result = await addComments(bytes, comments.map((c) => ({ date: nowIso(), ...c })));
10111
- await (0, import_promises.writeFile)(out, Buffer.from(result));
10112
- process.stdout.write(JSON.stringify({ ok: true, out, comments: comments.length }) + "\n");
10177
+ const r = await addComments(bytes, comments.map((c) => ({ date: nowIso(), ...c })));
10178
+ await (0, import_promises.writeFile)(out, Buffer.from(r.bytes));
10179
+ for (const w of r.warnings) process.stderr.write(`docx-annotate: warning: ${w}
10180
+ `);
10181
+ if (r.skipped.length) {
10182
+ process.stderr.write(
10183
+ `docx-annotate: warning: ${r.skipped.length} of ${comments.length} comment(s) NOT written \u2014 anchor matched no paragraph: ` + r.skipped.map((s) => `#${s.index} ${JSON.stringify(s.anchor)}`).join(", ") + " (anchors must match text within one paragraph)\n"
10184
+ );
10185
+ }
10186
+ const ok = !(strictAnchors && r.skipped.length);
10187
+ process.stdout.write(JSON.stringify({ ok, out, written: r.written, skipped: r.skipped, warnings: r.warnings }) + "\n");
10188
+ if (!ok) process.exit(1);
10113
10189
  return;
10114
10190
  }
10115
10191
  if (mode === "suggest") {