docx-generator

docx-generator is a skill for Claude Code from guyoung/boxagnts. It costs 38 tokens per session (10,943 once invoked), scanned A, original, MIT.

A document generator that creates Microsoft Word DOCX files from Markdown or structured data. DOCX is the file format used by Microsoft Word, and OOXML is the internal document standard it must follow.

In plain words
What is it for?
Use it to produce Word documents from Markdown or a structured document tree, with a specified output path when needed.
Why use it?
It turns text or structured content into a Word file while following the required file format rules. This avoids relying on a manually assembled or invalid DOCX package.

Skill for Claude Code

Written for Claude Code: when-to-use in frontmatter.

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add skills/guyoung/boxagnts/docx-generator
Any agent
npx skills add guyoung/boxagnts --skill docx-generator
Clone the repo
git clone --depth 1 https://github.com/guyoung/boxagnts

Made for: Claude Code.

Wrote this? Show the measurements

A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.

agentmods badge for docx-generator

README.md
[![agentmods](https://agentmods.dev/badge/skills/guyoung/boxagnts/docx-generator.svg)](https://agentmods.dev/skills/guyoung/boxagnts/docx-generator)
Your own site
<a href="https://agentmods.dev/skills/guyoung/boxagnts/docx-generator"><img src="https://agentmods.dev/badge/skills/guyoung/boxagnts/docx-generator.svg" alt="Measured on agentmods" height="20"></a>
Per session 38 Skills are progressive disclosure: only the name and description are preloaded; the body loads when the skill is used.
When invoked 10,943 The whole file, excluding the scripts and references it only reads on demand.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5.1 $0.00038 $0.10943
Opus 5 $0.00019 $0.05471
Sonnet 5 $0.00008 $0.02189
Haiku 4.5 $0.00004 $0.01094

Measured 6d ago against content hash 6b3a2ef87c1a, method: parsed. Prices are Anthropic first-party input rates as of 2026-09-06, from the pricing page.

Security

Grade A, and why

docx-generator scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 6d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

app/extensions/skills/docx-generator/SKILL.md · 553 lines

How it starts

The opening of the file, as written. The whole thing — 553 lines — stays where its author put it; the contents beside it link to each section on GitHub.

CODE SKILL — VERBATIM REQUIRED This skill targets a formal specification (OOXML). Adaptation is prohibited per C2-A Rule 3.

CRITICAL: Execution Constraints

The code below is the SOLE IMPLEMENTATION — it MUST NOT be replaced, simplified, or reimplemented.

Prohibited deviations:

  • Do NOT write your own crc32() — use ONLY the one in Runnable Entry Point
  • Do NOT write your own ZipStream class — use ONLY the one in Runnable Entry Point
  • Do NOT skip any OOXML file from the 10-file baseline
  • Do NOT use compression (method 8 / DEFLATE) — STORE mode only
  • Do NOT assemble document body XML manually outside markdownToDocxBody() or astToDocxBody()

Correct Invocation Pattern

  1. Copy the complete script from ## Runnable Entry Point below
  2. Replace ONLY {{input}}, {{output}}, {{mode}} with actual values
  3. Execute as a single jsexec call with NO modifications

Argument Details

Arg Type Required Default Description
input string yes Markdown string or structured AST
output string no /generated_document.docx Output .docx file path
mode string no markdown Input mode: markdown

Runnable Entry Point (COMPLETE IIFE, DO NOT MODIFY)

// ===== ⛔ Runnable Entry Point (IIFE) — DO NOT MODIFY ANY CODE BELOW ⛔ =====
(function() {
   const fs = require("fs");
   const path = require("path");

   const CRC32_TABLE = new Int32Array(256);
   (function(){for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?(0xEDB88320^(c>>>1)):(c>>>1);CRC32_TABLE[n]=c;}})();
   function crc32(buf) {
      let crc = 0xFFFFFFFF;
      for (let i = 0; i < buf.length; i++)
         crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ buf[i]) & 0xFF];
      return (crc ^ 0xFFFFFFFF) >>> 0;
   }

   function validateDocx(buf) {
      let eocdOff = -1;
      for (let i = buf.length - 22; i >= 0; i--) {
         if (buf[i]===0x50 && buf[i+1]===0x4B && buf[i+2]===0x05 && buf[i+3]===0x06) {
            eocdOff = i; break;
         }
      }
      if (eocdOff === -1) throw new Error("DOCX validation FAILED: no EOCD found");
      const total = buf.readUInt16LE(eocdOff + 8);
      const cdOff = buf.readUInt32LE(eocdOff + 16);
      let pos = cdOff;
      const errors = [];
      for (let k = 0; k < total; k++) {
         if (buf.readUInt32LE(pos) !== 0x02014B50) { errors.push("Bad CD sig at "+k); break; }
         const compMethod = buf.readUInt16LE(pos + 10);
         const crcStored = buf.readUInt32LE(pos + 16);
         const uncompSize = buf.readUInt32LE(pos + 24);
         const nameLen = buf.readUInt16LE(pos + 28);
         const extraLen = buf.readUInt16LE(pos + 30);
         const localOff = buf.readUInt32LE(pos + 42);
         const fname = buf.slice(pos + 46, pos + 46 + nameLen).toString('utf8');
         if (compMethod !== 0) errors.push(`COMPRESSION: "${fname}" method=${compMethod}, MUST be 0`);
         try {
            if (!fname.startsWith('word/media/')) {
               const localNameLen = buf.readUInt16LE(localOff + 26);
               const localExtraLen = buf.readUInt16LE(localOff + 28);
               const dataStart = localOff + 30 + localNameLen + localExtraLen;
               const storedData = buf.slice(dataStart, dataStart + uncompSize);
               const actualCrc = crc32(storedData);
               if (actualCrc !== crcStored)
                  errors.push(`CRC MISMATCH: "${fname}" stored=0x${crcStored.toString(16)} actual=0x${actualCrc.toString(16)}`);
            }
         } catch (e) { errors.push(`VALIDATION: "${fname}" - ${e.message}`); }
         pos += 46 + nameLen + extraLen;
      }
      if (errors.length > 0)
         throw new Error("DOCX validation FAILED before writing:\n" + errors.map(e=>"  - "+e).join('\n'));
      console.log("OK DOCX validation: " + total + " entries, CRC OK, STORE mode");
   }

   class ZipStream {
      constructor() { this.entries = []; this.chunks = []; this.offset = 0; }
      writeFile(name, data) {
         const raw = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
         const nameBuf = Buffer.from(name, 'utf8');
         const fileCrc = crc32(raw);
         const header = Buffer.alloc(30);
         header.writeUInt32LE(0x04034b50, 0);
         header.writeUInt16LE(20, 4); header.writeUInt16LE(0, 6); header.writeUInt16LE(0, 8);
         header.writeUInt32LE(fileCrc, 14); header.writeUInt32LE(raw.length, 18);
         header.writeUInt32LE(raw.length, 22); header.writeUInt16LE(nameBuf.length, 26);
         const local = Buffer.concat([header, nameBuf]);
         this.chunks.push(local, raw);
         this.entries.push({ name, crc: fileCrc, size: raw.length, offset: this.offset });
         this.offset += local.length + raw.length;
      }
      finalize() {
         const central = [];
         for (const e of this.entries) {
            const nameBuf = Buffer.from(e.name, 'utf8');
            const c = Buffer.alloc(46);
            c.writeUInt32LE(0x02014B50, 0); c.writeUInt16LE(20, 4); c.writeUInt16LE(20, 6);
            c.writeUInt16LE(0, 8); c.writeUInt16LE(0, 10); c.writeUInt32LE(e.crc, 16);
            c.writeUInt32LE(e.size, 20); c.writeUInt32LE(e.size, 24);
            c.writeUInt16LE(nameBuf.length, 28); c.writeUInt16LE(0, 30); c.writeUInt16LE(0, 32);
            c.writeUInt16LE(0, 34); c.writeUInt16LE(0, 36); c.writeUInt32LE(0, 38); c.writeUInt32LE(e.offset, 42);
            central.push(c, nameBuf);
         }
         const centralBuf = Buffer.concat(central);
         const eocd = Buffer.alloc(22);
         eocd.writeUInt32LE(0x06054b50, 0); eocd.writeUInt16LE(this.entries.length, 8);
         eocd.writeUInt16LE(this.entries.length, 10); eocd.writeUInt32LE(centralBuf.length, 12);
         eocd.writeUInt32LE(this.offset, 16);
         return Buffer.concat([...this.chunks, centralBuf, eocd]);
      }
   }

   // ── OOXML Generators ──
   function contentTypes(images) {
      const imgOverrides = images.map(img => `  <Override PartName="/word/media/${img.name}" ContentType="${img.mime}"/>`).join('\n');
      return `<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
  <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
  <Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>
  <Override PartName="/word/webSettings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"/>
  <Override PartName="/word/fontTable.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"/>
  <Override PartName="/word/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
  <Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
${imgOverrides}
</Types>`;
   }
   function rootRels() {
      return `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>`;
   }
   function docRels(images) {
      const imgRels = images.map(img => `  <Relationship Id="${img.rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${img.name}"/>`).join('\n');
      return `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
${imgRels}
</Relationships>`;
   }
   function documentXml(body, imageCount) {
      const imgNs = imageCount > 0
              ? '\n xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"\n xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"\n xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"'
              : '';
      return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document
 xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"${imgNs}>
  <w:body>${body}<w:sectPr/></w:body>
</w:document>`;
   }
   function stylesXml() {
      return `<?xml version="1.0" encoding="UTF-8"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:style w:type="paragraph" w:styleId="Normal" w:default="1"><w:name w:val="Normal"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:sz w:val="22"/></w:rPr></w:style>
  <w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:b/><w:color w:val="1F3864"/><w:sz w:val="36"/></w:rPr></w:style>
  <w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:b/><w:color w:val="2E75B6"/><w:sz w:val="28"/></w:rPr></w:style>
  <w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="heading 3"/><w:basedOn w:val="Normal"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:b/><w:color w:val="2E75B6"/><w:sz w:val="24"/></w:rPr></w:style>
  <w:style w:type="paragraph" w:styleId="Heading4"><w:name w:val="heading 4"/><w:basedOn w:val="Normal"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:b/><w:color w:val="2E75B6"/><w:sz w:val="22"/></w:rPr></w:style>
  <w:style w:type="character" w:styleId="Hyperlink"><w:name w:val="Hyperlink"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:color w:val="0563C1"/><w:u w:val="single"/></w:rPr></w:style>
  <w:style w:type="paragraph" w:styleId="ListBullet"><w:name w:val="List Bullet"/><w:basedOn w:val="Normal"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:sz w:val="22"/></w:rPr></w:style>
  <w:style w:type="paragraph" w:styleId="ListNumber"><w:name w:val="List Number"/><w:basedOn w:val="Normal"/><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/><w:sz w:val="22"/></w:rPr></w:style>
</w:styles>`;
   }
   function settingsXml(){return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>';}
   function webSettingsXml(){return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:webSettings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>';}
   function fontTableXml(){
      return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:fonts xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:font w:name="Calibri"><w:panose1 w:val="020F0502020204030204"/><w:charset w:val="00"/><w:family w:val="swiss"/><w:pitch w:val="variable"/></w:font>
  <w:font w:name="Courier New"><w:panose1 w:val="02070309020205040304"/><w:charset w:val="00"/><w:family w:val="modern"/><w:pitch w:val="fixed"/></w:font>
</w:fonts>`;
   }
   function themeXml(){
      return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme"><a:themeElements>
<a:clrScheme name="Office"><a:dk1><a:srgbClr val="000000"/></a:dk1><a:lt1><a:srgbClr val="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F3864"/></a:dk2><a:lt2><a:srgbClr val="D9E2F3"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme>
<a:fontScheme name="Office"><a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme>
<a:fmtScheme name="Office"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>`;
   }
   function numberingXml(){
      const bulletNums = Array.from({length:10},(_,i)=>`<w:num w:numId="${i+1}"><w:abstractNumId w:val="0"/></w:num>`).join('');
      const decimalNums = Array.from({length:10},(_,i)=>`<w:num w:numId="${i+11}"><w:abstractNumId w:val="1"/></w:num>`).join('');
      return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:abstractNum w:abstractNumId="0"><w:multiLevelType w:val="hybridMultilevel"/>
    <w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="●"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl>
    <w:lvl w:ilvl="1"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="○"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="1440" w:hanging="360"/></w:pPr></w:lvl>
    <w:lvl w:ilvl="2"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="■"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="2160" w:hanging="360"/></w:pPr></w:lvl>
  </w:abstractNum>
  <w:abstractNum w:abstractNumId="1"><w:multiLevelType w:val="hybridMultilevel"/>
    <w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr></w:lvl>
    <w:lvl w:ilvl="1"><w:start w:val="1"/><w:numFmt w:val="lowerLetter"/><w:lvlText w:val="%2."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="1440" w:hanging="360"/></w:pPr></w:lvl>
    <w:lvl w:ilvl="2"><w:start w:val="1"/><w:numFmt w:val="lowerRoman"/><w:lvlText w:val="%3."/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="2160" w:hanging="360"/></w:pPr></w:lvl>
  </w:abstractNum>
${bulletNums}${decimalNums}
</w:numbering>`;
   }

   // ── Renderers ──
   function esc(s) {
      const clean = String(s).replace(/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu, '');
      return clean.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
   }
   function run(text, opts = {}) {
      let parts = [];
      if (opts.code) parts.push('<w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/>');
      if (opts.bold) parts.push('<w:b/>');
      if (opts.italic) parts.push('<w:i/>');
      parts.push('<w:sz w:val="22"/>');
      return `<w:r><w:rPr>${parts.join('')}</w:rPr><w:t xml:space="preserve">${esc(String(text))}</w:t></w:r>`;
   }
   function paragraph(runs, style) {
      const pPr = style ? `<w:pPr><w:pStyle w:val="${style}"/></w:pPr>` : '';
      return `<w:p>${pPr}${runs}</w:p>`;
   }
   function blockquoteParagraph(text, indentLevel) {
      const left = 360 * (indentLevel || 1);
      return `<w:p><w:pPr><w:ind w:left="${left}"/></w:pPr>${parseInline(text)}</w:p>`;
   }
   function parseInline(text) {
      const runs = [];
      let i = 0;
      while (i < text.length) {
         if (text[i] === '!') { i++; continue; }
         if (text[i]==='*'&&text[i+1]==='*'&&text[i+2]==='*') {
            const end=text.indexOf('***',i+3); if(end!==-1){runs.push(run(text.slice(i+3,end),{bold:true,italic:true}));i=end+3;continue;}
            runs.push(run('*'));i++;continue;
         }
         if (text[i]==='*'&&text[i+1]==='*') {
            const end=text.indexOf('**',i+2); if(end!==-1){runs.push(run(text.slice(i+2,end),{bold:true}));i=end+2;continue;}
            runs.push(run('*'));i++;continue;
         }
         if (text[i]==='*'&&text[i+1]!=='*') {
            const end=text.indexOf('*',i+1); if(end!==-1){runs.push(run(text.slice(i+1,end),{italic:true}));i=end+1;continue;}
            runs.push(run('*'));i++;continue;
         }
         if (text[i]==='`') {
            const end=text.indexOf('`',i+1); if(end!==-1){runs.push(run(text.slice(i+1,end),{code:true}));i=end+1;continue;}
            runs.push(run('`'));i++;continue;
         }
         if (text[i]==='[') {
            const isImage=i>0&&text[i-1]==='!';
            const isImageWS=i>1&&text[i-2]==='!'&&text[i-1]===' ';
            if(isImage||isImageWS) {
               const close=text.indexOf(']',i); const op=text.indexOf('(',close); const cp=text.indexOf(')',op);
               if(close!==-1&&op===close+1&&cp!==-1) {
                  const alt=text.slice(i+1,close); const imgPath=text.slice(op+1,cp);
                  if(isImageWS)runs.pop();
                  runs.push('\x00IMG:'+Buffer.from(JSON.stringify({alt,path:imgPath})).toString('base64')+'\x00');
                  i=cp+1;continue;
               }
            }
            const close=text.indexOf(']',i); const op=text.indexOf('(',close); const cp=text.indexOf(')',op);
            if(close!==-1&&op===close+1&&cp!==-1){
               runs.push(`<w:r><w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr><w:t xml:space="preserve">${esc(text.slice(i+1,close))}</w:t></w:r>`);
               i=cp+1;continue;
            }
            runs.push(run('['));i++;continue;
         }
         let j=i; while(j<text.length&&text[j]!=='*'&&text[j]!=='`'&&text[j]!=='[')j++;
         if(j>i)runs.push(run(text.slice(i,j)));
         i=j;
      }
      return runs.join('');
   }
   function renderTable(headers, dataRows) {
      const maxCols = Math.max(headers.length, ...dataRows.map(r=>r.length), 1);
      const colWidth = Math.floor(9000/maxCols);
      const gridCols = Array.from({length:maxCols},()=>`<w:gridCol w:w="${colWidth}"/>`).join('');
      const border = '<w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="BFBFBF"/><w:left w:val="single" w:sz="4" w:space="0" w:color="BFBFBF"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="BFBFBF"/><w:right w:val="single" w:sz="4" w:space="0" w:color="BFBFBF"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="BFBFBF"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="BFBFBF"/></w:tblBorders>';
      function cell(t,isH){
         const shd=isH?'<w:shd w:val="clear" w:color="auto" w:fill="D9E2F3"/>':'';
         const rPr=isH?'<w:rPr><w:b/></w:rPr>':'';
         return`<w:tc><w:tcPr>${shd}</w:tcPr><w:p><w:r>${rPr}<w:t xml:space="preserve">${esc(String(t))}</w:t></w:r></w:p></w:tc>`;}
      const pad=r=>[...r,...Array(Math.max(0,maxCols-r.length)).fill('')].slice(0,maxCols);
      const hRow=`<w:tr>${pad(headers).map(h=>cell(h,true)).join('')}</w:tr>`;
      const dRows=dataRows.map(r=>`<w:tr>${pad(r).map(c=>cell(c,false)).join('')}</w:tr>`).join('');
      return`<w:tbl><w:tblPr><w:tblW w:w="9000" w:type="dxa"/>${border}</w:tblPr><w:tblGrid>${gridCols}</w:tblGrid>${hRow}${dRows}</w:tbl>`;
   }
   function markdownToDocxBody(md) {
      const lines=md.split('\n'); const body=[]; let i=0;
      let bulletNumId=1,decimalNumId=11;
      let currentListType=null,currentNumId=0;
      function resetList(){currentListType=null;currentNumId=0;}
      while(i<lines.length){
         const line=lines[i];
         if(line.trim()===''){resetList();body.push('<w:p/>');i++;continue;}
         const hMatch=line.match(/^(#{1,6})\s+(.+)/);
         if(hMatch){resetList();const lv=Math.min(hMatch[1].length,6);body.push(paragraph(parseInline(hMatch[2]),`Heading${lv}`));i++;continue;}
         if(/^[-*_]{3,}\s*$/.test(line.trim())){resetList();body.push('<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/></w:pBdr></w:pPr></w:p>');i++;continue;}
         let bqMatch=line.match(/^(>+\s*)(.*)/);
         if(bqMatch){
            const indent=bqMatch[1].split('>').length-1;
            body.push(blockquoteParagraph(bqMatch[2],indent));
            resetList();i++;continue;
         }
         const olMatch=line.match(/^(\s*)(\d+)\.\s+(.*)/);
         if(olMatch){const indent=Math.floor(olMatch[1].length/2);if(currentListType!=='decimal'){currentListType='decimal';currentNumId=decimalNumId++;}body.push(`<w:p><w:pPr><w:numPr><w:ilvl w:val="${indent}"/><w:numId w:val="${currentNumId}"/></w:numPr></w:pPr>${parseInline(olMatch[3])}</w:p>`);i++;continue;}
         const ulMatch=line.match(/^(\s*)[-*+]\s+(.*)/);
         if(ulMatch){const indent=Math.floor(ulMatch[1].length/2);if(currentListType!=='bullet'){currentListType='bullet';currentNumId=bulletNumId++;}body.push(`<w:p><w:pPr><w:numPr><w:ilvl w:val="${indent}"/><w:numId w:val="${currentNumId}"/></w:numPr></w:pPr>${parseInline(ulMatch[2])}</w:p>`);i++;continue;}
         if(line.trim().startsWith('```')){
            resetList(); i++;
            const codeLines=[];
            let lang='';
            const firstLine=lines[i]||'';
            const langMatch=firstLine.match(/^([\w+#]+)\s*$/);
            if(!langMatch||['python','javascript','js','bash','sh','sql','json','xml','java','cpp','c','go','rust','ruby','swift'].includes(langMatch[1].toLowerCase())){
               if(langMatch) lang=langMatch[1];
            }
            if(langMatch) i++;
            while(i<lines.length&&!lines[i].trim().startsWith('```')){codeLines.push(lines[i]);i++;}
            if(i<lines.length) i++;
            body.push(`<w:p><w:pPr><w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/></w:pPr>${run(lang||'',{bold:true,code:true})}</w:p>`);
            for(const cl of codeLines){body.push(`<w:p><w:pPr><w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/></w:pPr>${run(cl,{code:true})}</w:p>`);}
            continue;
         }
         if(line.trim().startsWith('|')&&line.trim().endsWith('|')){
            resetList();const tblLines=[];
            while(i<lines.length&&lines[i].trim().startsWith('|')&&lines[i].trim().endsWith('|')){tblLines.push(lines[i]);i++;}
            if(tblLines.length>=2){
               const pr=l=>l.trim().slice(1,-1).split('|').map(c=>c.trim());
               const hRow=pr(tblLines[0]);
               const sRow=pr(tblLines[1]);
               if(sRow.every(c=>/^:?-{2,}:?$/.test(c))){
                  const dRows=tblLines.slice(2).map(pr);
                  body.push(renderTable(hRow,dRows));body.push('<w:p/>');
                  continue;
               }
            }
            i=tblLines[0]?lines.indexOf(tblLines[0]):i;
         }
         resetList();
         body.push(paragraph(parseInline(line)));
         i++;
      }
      return body.join('');
   }

   // ── Image processing ──
   function detectImageDimensions(buf, ext) {
      const h=buf; try {
         if(ext==='png'&&h[0]===0x89&&h[1]===0x50) return {width:h.readUInt32BE(16),height:h.readUInt32BE(20)};
         if(ext==='jpg'||ext==='jpeg') {
            let off=2; while(off<h.length-4){if(h[off]===0xFF&&(h[off+1]>=0xC0&&h[off+1]<=0xC3)){return{height:h.readUInt16BE(off+5),width:h.readUInt16BE(off+7)};} off+=h.readUInt16BE(off+2)+2;} }
         if(ext==='gif') return {width:h.readUInt16LE(6),height:h.readUInt16LE(8)};
         if(ext==='bmp') return {width:h.readUInt32LE(18),height:Math.abs(h.readInt32LE(22))};
      }catch(_){}
      return null;
   }
   function dimsToEmu(px){
      if(!px) return {cx:3810000,cy:2857500};
      const MAX_W=600; const scale=px.width>MAX_W?MAX_W/px.width:1;
      return {cx:Math.round(px.width*scale*9525),cy:Math.round(px.height*scale*9525)};
   }
   function mimeFromExt(ext){
      const m={png:'image/png',jpg:'image/jpeg',jpeg:'image/jpeg',gif:'image/gif',bmp:'image/bmp'};
      return m[ext]||'image/png';
   }
   function validImageSig(buf,ext){
      const h=buf.slice(0,8);
      if(ext==='png') return h[0]===0x89&&h[1]===0x50;
      if(ext==='jpg'||ext==='jpeg') return h[0]===0xFF&&h[1]===0xD8&&h[2]===0xFF;
      return true;
   }
   function imageDrawing(rId,alt,cx,cy){
      return`<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="${cx}" cy="${cy}"/><wp:effectExtent l="0" t="0" r="0" b="0"/>
<wp:docPr id="1" name="${esc(alt||'Chart')}" descr="${esc(alt||'')}"/>
<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic><pic:nvPicPr><pic:cNvPr id="0" name="${esc(alt||'Chart')}"/><pic:cNvPicPr/></pic:nvPicPr>
<pic:blipFill><a:blip r:embed="${rId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic>
</a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
   }

   // ── Build Pipeline ──
   function buildDocx(input, mode, output) {
      const outDir = path.dirname(output);
      try { fs.mkdirSync(outDir, { recursive: true }); } catch (e) {}
      const zip = new ZipStream();
      let body;
      if (mode === 'markdown') body = markdownToDocxBody(String(input));
      else if (mode === 'ast') body = astToDocxBody(input);
      else body = paragraph(parseInline(String(input)));

      const images = [];
      body = body.replace(/\x00IMG:([A-Za-z0-9+/=]+)\x00/g, (match, b64) => {
         try {
            const info = JSON.parse(Buffer.from(b64, 'base64').toString());
            const imgPath = info.path;
            let imgBuf;
            // ★ FIXED: fallback wrapped in <w:p>
            try { imgBuf = fs.readFileSync(imgPath); } catch (e) { return `<w:p><w:r><w:t>[Image: ${esc(info.alt||imgPath)}]</w:t></w:r></w:p>`; }
            const ext = path.extname(imgPath).slice(1).toLowerCase() || 'png';
            if (!validImageSig(imgBuf, ext)) return `<w:p><w:r><w:t>[Invalid image: ${esc(info.alt||imgPath)}]</w:t></w:r></w:p>`;
            const name = `image${images.length + 1}.${ext}`;
            const rId = `rIdImg${images.length + 1}`;
            images.push({ name, ext, mime: mimeFromExt(ext), rId, data: imgBuf });
            const emu = dimsToEmu(detectImageDimensions(imgBuf, ext));
            return imageDrawing(rId, info.alt, emu.cx, emu.cy);
         } catch (e) { return `<w:p><w:r><w:t>[Image error]</w:t></w:r></w:p>`; }
      });

      zip.writeFile("[Content_Types].xml", contentTypes(images));
      zip.writeFile("_rels/.rels", rootRels());
      zip.writeFile("word/document.xml", documentXml(body, images.length));
      zip.writeFile("word/_rels/document.xml.rels", docRels(images));
      zip.writeFile("word/styles.xml", stylesXml());
      zip.writeFile("word/settings.xml", settingsXml());
      zip.writeFile("word/webSettings.xml", webSettingsXml());
      zip.writeFile("word/fontTable.xml", fontTableXml());
      zip.writeFile("word/theme/theme1.xml", themeXml());
      zip.writeFile("word/numbering.xml", numberingXml());
      for (const img of images) zip.writeFile(`word/media/${img.name}`, img.data);
      const buf = zip.finalize();
      validateDocx(buf);
      fs.writeFileSync(output, buf);
      console.log("Done:", output, buf.length);
   }

   // ★ FIXED: image case now wraps token in <w:p> for OOXML compliance
   // Before: parts.push(`\x00IMG:...\x00`);  → 裸 <w:r> 在 <w:body> 下 → 违反 schema
   // After:  parts.push(`<w:p>\x00IMG:...\x00</w:p>`);  →  <w:r> 在 <w:p> 内 → 合法
   function astToDocxBody(ast) {
      if (!Array.isArray(ast)) return paragraph(parseInline(String(ast)));
      let bulletNumId = 1, decimalNumId = 11;
      const parts = [];
      for (const node of ast) {
         switch (node.type) {
            case 'heading': parts.push(paragraph(parseInline(node.text||''), `Heading${Math.min(node.level||1,6)}`)); break;
            case 'paragraph': parts.push(paragraph(parseInline(node.text||''))); break;
            case 'blockquote': parts.push(blockquoteParagraph(node.text||'', node.indentLevel||1)); break;
            case 'table': if(node.headers&&node.rows){parts.push(renderTable(node.headers,node.rows));parts.push('<w:p/>');} break;
                 // ★ FIXED: image path → token wrapped in <w:p> to keep <w:r> inside block-level element
            case 'image': if(node.path) parts.push(`<w:p>\x00IMG:${Buffer.from(JSON.stringify({alt:node.alt||'',path:node.path})).toString('base64')}\x00</w:p>`); break;
            case 'horizontal_rule': parts.push('<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/></w:pBdr></w:pPr></w:p>'); break;
            case 'list': {
               const listNumId = node.ordered ? (decimalNumId++) : (bulletNumId++);
               for (const item of (node.items||[])) {
                  const itemText = typeof item === 'string' ? item : (item.text||'');
                  const itemIndent = typeof item === 'object' ? (item.level||0) : 0;
                  parts.push(`<w:p><w:pPr><w:pStyle w:val="${node.ordered?'ListNumber':'ListBullet'}"/><w:numPr><w:ilvl w:val="${itemIndent}"/><w:numId w:val="${listNumId}"/></w:numPr></w:pPr>${parseInline(itemText)}</w:p>`);
               }
            } break;
         }
      }
      return parts.join('');
   }

   // ⛔ ENTRY POINT
   const input = `{{input}}`;
   const output = `{{output}}`.replace(/^\{\{output\}\}$/, '') || '/generated_document.docx';
   const mode = (`{{mode}}`.replace(/^\{\{mode\}\}$/, '') || 'markdown').toLowerCase();
   buildDocx(input, mode, output);
})();

Read the full file on GitHub · 553 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 6d ago First seen · 553 lines · 38 tokens per session scan A 6b3a2ef87c1a

Subscribe to this mod's changes

docx-generator is a skill published in the GitHub repository guyoung/boxagnts (11 stars, last pushed 1mo ago), licensed MIT. It adds 38 tokens to every session and 10,943 once invoked, about $0.0002 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.

Related

Other skills, from other repositories

pdf

当用户需要对PDF文件进行任何操作时,请使用此技能。包括从 PDF 中读取或提取文本/表格、合并多个 PDF、拆分 PDF、旋转页面、添加水印、创建新PDF、填写PDF表单、加密/解密 PDF、提取图片,以及对扫描版 PDF 进行 OCR 使其可搜索。如果用户提到 .pdf 文件或要求生成 PDF,请使用此技能。.

agentscope-ai/QwenPaw · 95 tokens

pptx

当涉及到 .pptx 文件的任何操作时使用此技能——无论是作为输入、输出还是两者兼有。包括:创建幻灯片、演示文稿或路演材料;读取、解析或提取任何 .pptx 文件中的文本(即使提取的内容将用于其他地方,如邮件或摘要);编辑、修改或更新现有演示文稿;合并或拆分幻灯片文件;处理模板、布局、演讲者备注或批注。当用户提到“演示文稿”、”幻灯片“、”PPT“或引用 .pptx 文件名时触发,无论他们之后打算如何使用内容。如果需要打开、创建或操作 .pptx 文件,就使用此技能。.

agentscope-ai/QwenPaw · 165 tokens

nano-pdf

Edits PDF files using natural-language instructions via the nano-pdf CLI. Supports modifying text, changing titles, fixing typos, and updating content on specific pages. Use when the user wants to edit a PDF, modify PDF content, update PDF text, fix a typo in a PDF, change a PDF title, or rewrite part of a PDF page.

elizaOS/eliza · 75 tokens

hive.pdf

Read, write, merge, split, rotate, watermark, encrypt, and OCR PDF files using Python (pypdf, pdfplumber, reportlab, pypdfium2) and command-line tools (poppler-utils, qpdf). Use when the user asks to extract text/tables/images from a PDF, create or modify a PDF, combine or split PDFs, OCR a scanned PDF…

aden-hive/hive · 98 tokens

pdf-toolkit

Structured .pdf operations: extract text/tables, merge pages from multiple PDFs, split a PDF by page ranges, fill PDF form fields, and generate fresh PDFs from JSON. Trigger when the user wants programmatic PDF work without natural-language rewriting — examples: pull tables from a report, combine three PDFs, extract…

opensquilla/opensquilla · 127 tokens

pdf-analysis

PDF 文档解析。自动区分文字型 PDF 与扫描型 PDF,覆盖:文本/表格提取、多页全量扫描、嵌入图表 caption、单位感知数值计算。.

OpenSenseNova/SenseNova-Skills · 48 tokens