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.

Good fit Use it to produce Word documents from Markdown or a structured document tree, with a specified output path when needed.

Compare 6 skills from other repositories ↓
Install with agentmods
npx agentmods add skills/guyoung/boxagnts/docx-generator
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.

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/github.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/github.svg" alt="Measured on agentmods" height="20"></a>

Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.

agentmods 80×15 button for docx-generator

Your own site · 80×15
<a href="https://agentmods.dev/skills/guyoung/boxagnts/docx-generator"><img src="https://agentmods.dev/badge/skills/guyoung/boxagnts/docx-generator.svg" alt="Reviewed on agentmods" width="80" 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. A grade says what 26 rules found in the file — not that it is safe.
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.5 $0.00015 $0.04377
Sonnet 5.5 $0.00008 $0.02189
Haiku 4.5 $0.00004 $0.01094

Measured 2mo ago against content hash 6b3a2ef87c1a, method: parsed. Prices are Anthropic first-party input rates as of 2026-10-07, 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 2mo 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. 2mo 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 3mo ago), licensed MIT. It adds 38 tokens to every session and 10,943 once invoked, about $0.0002 per session on Opus 5.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

docx-generator compared with skills from other repositories

docx-generator next to 6 skills from other repositories
ModTypeStarsLast pushLicenceScanTokensWorks with
docx-generatorguyoung/boxagntsSkill113mo agoMITA38Claude Code
pdfagentscope-ai/QwenPawSkill37kyesterdayApache-2.0A95Claude Code, Codex
officecli-academic-paperiOfficeAI/OfficeCLISkill33k7d agoApache-2.0A141Claude Code, Codex
officecliiOfficeAI/OfficeCLISkill33k7d agoApache-2.0A56Claude Code, Codex
surfsense-dataMODSetter/SurfSenseSkill17kyesterdayApache-2.0A0Claude Code, Codex
surfsense-htmlMODSetter/SurfSenseSkill17kyesterdayApache-2.0A89Claude Code, Codex
hive.pdfaden-hive/hiveSkill11k4d agoApache-2.0A98Claude Code, Codex