Md To DOCX
GitHubOfficialFirst committed by parveen-dotnetDistributed by GitHub
- Skill
- Tool integration
- Automation and integrations
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a Node.js-based utility to transform Markdown files into structured .docx documents. It leverages the docx and marked libraries to handle document generation and Markdown parsing without requiring external software like Pandoc or LibreOffice. It supports advanced formatting including title pages derived from YAML front-matter, automated tables of contents, and inline PNG image embedding.
What it does
- Converts Markdown files to .docx format using pure JavaScript
- Generates professional title pages from YAML front-matter
- Creates automated tables of contents based on H1-H3 headings
- Embeds local PNG images with automatic scaling and aspect ratio preservation
- Applies consistent styling for headings, tables, and code blocks
- Handles standard Markdown elements like lists, links, and horizontal rules
When to use it
- When you need to generate Word documents from Markdown source files
- When you require a lightweight conversion tool without system-level dependencies
- When you need to automate the creation of technical reports or documentation
- When you want to maintain consistent branding and styling across generated documents
Advantages
- Zero native binary dependencies
- Cross-platform compatibility on Windows, macOS, and Linux
- Automated handling of image paths and document metadata
- Consistent, pre-defined professional styling
Usage examples
- “Convert my documentation.md file into a Word document.”
- The tool processes documentation.md, extracts the front-matter for the title page, embeds any referenced PNGs, and saves the result as documentation.docx.
- “Generate a report named final-report.docx from project-notes.md.”
- The tool parses project-notes.md and outputs a formatted Word document named final-report.docx in the current directory.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/github/md-to-docx/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip md-to-docx.zip -d .agents/skills
Documentation by parveen-dotnet
Markdown to Word (.docx) Skill
Convert Markdown (.md) files into professionally formatted Word (.docx) documents with embedded PNG images. Uses pure JavaScript via the docx and marked npm packages — no Pandoc, LibreOffice, or any native binary required.
How to Convert
# Install dependencies (one-time, from the scripts folder)
cd skills/md-to-docx/scripts && npm install
# Convert (run from workspace root)
node skills/md-to-docx/scripts/md-to-docx.mjs <input.md> [output.docx]If output.docx is omitted, it defaults to <input-basename>.docx in the current directory.
Skill Folder Contents
| File | Purpose |
|---|---|
SKILL.md | This instruction file |
scripts/md-to-docx.mjs | Node.js Markdown-to-Word converter |
scripts/package.json | Dependencies (docx, marked) |
Prerequisites
| Requirement | Version | Notes |
|---|---|---|
| Node.js | 18+ | Required runtime |
docx | 9+ | Pure JS Word document generator |
marked | 15+ | Markdown parser |
No native binaries. No system-level installs. Works on Windows, macOS, and Linux.
Features
The converter:
Extracts YAML front-matter — uses
title,date,version,audiencefor the title pageGenerates a title page — with project name, subtitle, date, version, and audience
Generates a table of contents — built from H1-H3 headings
Embeds PNG images — resolves
references relative to the input.mdfile, reads the PNG, and embeds it inline in the Word documentStyled output — Calibri font, colored headings (
#1F3864), styled tables with alternating row colors, code blocks in ConsolasHandles all Markdown elements — headings, paragraphs, tables, code blocks, lists, images, links, horizontal rules
Image Embedding
The converter automatically embeds PNG images referenced in the Markdown:
The image path is resolved relative to the input Markdown file. The PNG is read, dimensions are extracted from the PNG header, and the image is scaled to fit within 6 inches width while preserving aspect ratio.
If an image file is not found, a placeholder [Image not found: <path>] is inserted.
Front-Matter Format
---
title: Project Name — Project Summary
date: 2025-01-15
version: 1.0
audience: Engineering Team, Architects, Stakeholders
---The title is split on — or – into main title and subtitle for the title page.
Files
- ATTRIBUTION.mdAdded by AutoGineer865 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md2.7 KiB
- scripts/md-to-docx.mjsScript14.4 KiB
- scripts/package.json187 B
scripts/md-to-docx.mjs
/**
* md-to-docx.mjs - Markdown to Word converter
* Pure JavaScript, no external tools required.
* Usage: node md-to-docx.mjs <input.md> [output.docx]
*/
import { readFileSync, writeFileSync, existsSync } from "fs";
import { dirname, join, resolve } from "path";
import { marked } from "marked";
import {
Document, Packer, Paragraph, TextRun, HeadingLevel, ImageRun,
TableRow, TableCell, Table, WidthType, BorderStyle,
AlignmentType, ShadingType, PageBreak
} from "docx";
// --- Image dimensions from PNG header ---
function pngDimensions(buffer) {
// PNG signature check + IHDR chunk at offset 16 (width) and 20 (height)
if (buffer[0] === 0x89 && buffer[1] === 0x50) {
return {
width: buffer.readUInt32BE(16),
height: buffer.readUInt32BE(20),
};
}
return { width: 600, height: 400 }; // fallback
}
// --- CLI argument parsing ---
const inputPath = process.argv[2];
if (!inputPath) {
console.error("Usage: node md-to-docx.mjs <input.md> [output.docx]");
process.exit(1);
}
const outputPath = process.argv[3] || inputPath.replace(/\.md$/i, ".docx");
const inputDir = dirname(resolve(inputPath));
const mdSource = readFileSync(inputPath, "utf-8");
// --- Extract YAML front-matter metadata ---
let title = "Document";
let subtitle = "";
let date = new Date().toISOString().slice(0, 10);
let version = "1.0";
let audience = "";
const fmMatch = mdSource.match(/^---\n([\s\S]*?)\n---/m);
if (fmMatch) {
const fm = fmMatch[1];
title = fm.match(/^title:\s*(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, "") || title;
date = fm.match(/^date:\s*(.+)$/m)?.[1]?.trim() || date;
version = fm.match(/^version:\s*(.+)$/m)?.[1]?.trim() || version;
audience = fm.match(/^audience:\s*(.+)$/m)?.[1]?.trim() || "";
}
// Strip front-matter from markdown content
const md = mdSource.replace(/^---[\s\S]*?---\n*/m, "");
// Derive title / subtitle from front-matter title or first H1
const titleParts = title.split(/\s*[—–]\s*/);
const mainTitle = titleParts[0] || title;
subtitle = titleParts[1] || "";
if (!subtitle) {
const h1Match = md.match(/^#\s+(.+)$/m);
if (h1Match) {
const h1Parts = h1Match[1].split(/\s*[—–]\s*/);
if (h1Parts.length > 1) {
subtitle = h1Parts[1];
if (!mainTitle || mainTitle === "Document") title = h1Parts[0];
}
}
}
// --- Parse Markdown tokens ---
const tokens = marked.lexer(md);
// --- Style constants ---
const FONT = "Calibri";
const HEADER_COLOR = "1F3864";
const ACCENT_COLOR = "2E75B6";
const TABLE_HEADER_BG = "D6E4F0";
const TABLE_ALT_BG = "F2F7FB";
const CODE_BG = "F5F5F5";
const CODE_FONT = "Consolas";
const BORDER_COLOR = "B4C6E7";
const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: BORDER_COLOR };
const tableBorders = {
top: tableBorder, bottom: tableBorder,
left: tableBorder, right: tableBorder,
insideHorizontal: tableBorder, insideVertical: tableBorder,
};
// --- Utility: decode HTML entities ---
function decodeEntities(str) {
return str
.replace(/</g, "<").replace(/>/g, ">")
.replace(/"/g, '"').replace(/'/g, "'")
.replace(/&/g, "&");
}
// --- Inline tokens to TextRun[] ---
function inlineToRuns(inlineTokens, parentBold = false, parentItalic = false) {
const runs = [];
if (!inlineTokens) return runs;
for (const t of inlineTokens) {
switch (t.type) {
case "text":
runs.push(new TextRun({
text: decodeEntities(t.text || t.raw || ""),
bold: parentBold, italics: parentItalic, font: FONT, size: 22,
}));
break;
case "strong":
runs.push(...inlineToRuns(t.tokens, true, parentItalic));
break;
case "em":
runs.push(...inlineToRuns(t.tokens, parentBold, true));
break;
case "codespan":
runs.push(new TextRun({
text: t.text, font: CODE_FONT, size: 20, bold: parentBold,
shading: { type: ShadingType.SOLID, color: CODE_BG, fill: CODE_BG },
}));
break;
case "link":
runs.push(new TextRun({
text: t.text || t.href, bold: parentBold, italics: parentItalic,
font: FONT, size: 22, color: ACCENT_COLOR, underline: {},
}));
break;
case "image":
// Images handled at paragraph level; skip inline
break;
case "br":
runs.push(new TextRun({ break: 1, font: FONT }));
break;
default:
if (t.raw) {
runs.push(new TextRun({
text: decodeEntities(t.raw), bold: parentBold, italics: parentItalic,
font: FONT, size: 22,
}));
}
break;
}
}
return runs;
}
// --- Paragraph inline runs ---
function paragraphRuns(token) {
if (token.tokens) return inlineToRuns(token.tokens);
return [new TextRun({ text: token.text || token.raw || "", font: FONT, size: 22 })];
}
// --- Table builder ---
function buildTable(token) {
const rows = [];
if (token.header) {
rows.push(new TableRow({
tableHeader: true,
children: token.header.map(cell => new TableCell({
shading: { type: ShadingType.SOLID, color: TABLE_HEADER_BG, fill: TABLE_HEADER_BG },
children: [new Paragraph({
children: inlineToRuns(cell.tokens, true),
spacing: { before: 40, after: 40 },
})],
})),
}));
}
if (token.rows) {
token.rows.forEach((row, idx) => {
rows.push(new TableRow({
children: row.map(cell => new TableCell({
shading: idx % 2 === 1
? { type: ShadingType.SOLID, color: TABLE_ALT_BG, fill: TABLE_ALT_BG }
: undefined,
children: [new Paragraph({
children: inlineToRuns(cell.tokens),
spacing: { before: 30, after: 30 },
})],
})),
}));
});
}
return new Table({
rows, width: { size: 100, type: WidthType.PERCENTAGE }, borders: tableBorders,
});
}
// --- Code block builder ---
function buildCodeBlock(token) {
const lines = (token.text || "").split("\n");
return lines.map(line => new Paragraph({
children: [new TextRun({ text: line || " ", font: CODE_FONT, size: 18 })],
spacing: { before: 20, after: 20 },
shading: { type: ShadingType.SOLID, color: CODE_BG, fill: CODE_BG },
indent: { left: 360 },
}));
}
// --- List builder ---
function buildList(token, level = 0) {
const items = [];
for (const item of token.items) {
const textTokens = item.tokens?.find(t => t.type === "text");
const bullet = token.ordered ? `${item.raw?.match(/^\d+/)?.[0] || "1"}.` : "\u2022";
const indent = 720 + level * 360;
items.push(new Paragraph({
children: [
new TextRun({ text: `${bullet} `, font: FONT, size: 22 }),
...(textTokens ? inlineToRuns(textTokens.tokens) : [new TextRun({
text: decodeEntities(item.text || ""), font: FONT, size: 22,
})]),
],
spacing: { before: 40, after: 40 },
indent: { left: indent },
}));
const nestedList = item.tokens?.find(t => t.type === "list");
if (nestedList) items.push(...buildList(nestedList, level + 1));
}
return items;
}
// --- Build document children ---
const children = [];
// Title page (from front-matter metadata)
children.push(
new Paragraph({ spacing: { before: 2400 } }),
new Paragraph({
children: [new TextRun({ text: mainTitle, font: FONT, size: 56, bold: true, color: HEADER_COLOR })],
alignment: AlignmentType.CENTER,
}),
);
if (subtitle) {
children.push(new Paragraph({
children: [new TextRun({ text: subtitle, font: FONT, size: 36, color: ACCENT_COLOR })],
alignment: AlignmentType.CENTER, spacing: { after: 400 },
}));
}
children.push(
new Paragraph({
children: [new TextRun({
text: `Date: ${date} | Version: ${version}`,
font: FONT, size: 22, color: "666666",
})],
alignment: AlignmentType.CENTER,
}),
);
if (audience) {
children.push(new Paragraph({
children: [new TextRun({ text: `Audience: ${audience}`, font: FONT, size: 22, color: "666666" })],
alignment: AlignmentType.CENTER, spacing: { after: 600 },
}));
}
children.push(new Paragraph({ children: [new PageBreak()] }));
// Table of Contents (static, built from headings found in the markdown)
children.push(
new Paragraph({
children: [new TextRun({ text: "Table of Contents", font: FONT, size: 32, bold: true, color: HEADER_COLOR })],
spacing: { before: 200, after: 400 },
}),
);
// Pre-scan tokens for headings to build the TOC
for (const tok of tokens) {
if (tok.type !== "heading" || tok.depth > 3) continue;
// Skip the first H1 title and the TOC heading itself
if (tok.depth === 1 && mainTitle !== "Document" &&
decodeEntities(tok.text || "").includes(mainTitle)) continue;
if (tok.text === "Table of Contents") continue;
const indent = (tok.depth - 1) * 360;
const tocSize = tok.depth === 1 ? 24 : tok.depth === 2 ? 22 : 20;
const tocBold = tok.depth <= 2;
const tocColor = tok.depth <= 2 ? HEADER_COLOR : ACCENT_COLOR;
children.push(new Paragraph({
children: [new TextRun({
text: decodeEntities(tok.text),
font: FONT, size: tocSize, bold: tocBold, color: tocColor,
})],
spacing: { before: tok.depth === 2 ? 80 : 40, after: 40 },
indent: { left: indent },
}));
}
children.push(new Paragraph({ children: [new PageBreak()] }));
// --- Token walker ---
let skipToc = false;
for (const token of tokens) {
switch (token.type) {
case "heading": {
// Skip first H1 if it matches the front-matter title (already on title page)
if (token.depth === 1 && mainTitle !== "Document" &&
decodeEntities(token.text || "").includes(mainTitle)) {
continue;
}
// Skip markdown TOC section
if (token.text === "Table of Contents") { skipToc = true; continue; }
if (skipToc && token.depth > 2) continue;
skipToc = false;
const headingMap = {
1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2,
3: HeadingLevel.HEADING_3, 4: HeadingLevel.HEADING_4,
};
children.push(new Paragraph({
heading: headingMap[token.depth] || HeadingLevel.HEADING_4,
children: [new TextRun({
text: decodeEntities(token.text),
font: FONT, bold: true,
color: token.depth <= 2 ? HEADER_COLOR : ACCENT_COLOR,
size: token.depth === 2 ? 32 : token.depth === 3 ? 26 : 24,
})],
spacing: { before: token.depth === 2 ? 360 : 240, after: 120 },
}));
break;
}
case "paragraph": {
if (skipToc) continue;
// Check if the paragraph is a standalone image
const imgToken = token.tokens && token.tokens.length === 1 && token.tokens[0].type === "image"
? token.tokens[0] : null;
if (imgToken) {
const href = imgToken.href || "";
const imgPath = resolve(inputDir, href);
if (existsSync(imgPath)) {
const imgBuf = readFileSync(imgPath);
const dims = pngDimensions(imgBuf);
const maxW = 580; // max width in points (~6 inches)
const scale = dims.width > maxW ? maxW / dims.width : 1;
const w = Math.round(dims.width * scale);
const h = Math.round(dims.height * scale);
children.push(new Paragraph({
children: [new ImageRun({ data: imgBuf, transformation: { width: w, height: h }, type: "png" })],
alignment: AlignmentType.CENTER,
spacing: { before: 120, after: 40 },
}));
// Add caption if alt text exists
if (imgToken.text) {
children.push(new Paragraph({
children: [new TextRun({ text: imgToken.text, font: FONT, size: 18, italics: true, color: "666666" })],
alignment: AlignmentType.CENTER,
spacing: { before: 0, after: 120 },
}));
}
} else {
children.push(new Paragraph({
children: [new TextRun({ text: `[Image not found: ${href}]`, font: FONT, size: 20, italics: true, color: "888888" })],
spacing: { before: 80, after: 80 },
}));
}
} else {
children.push(new Paragraph({
children: paragraphRuns(token), spacing: { before: 80, after: 80 },
}));
}
break;
}
case "table":
if (skipToc) continue;
children.push(buildTable(token));
children.push(new Paragraph({ spacing: { after: 120 } }));
break;
case "code":
if (skipToc) continue;
if (token.lang === "mermaid") {
children.push(new Paragraph({
children: [new TextRun({
text: "[Diagram: See source .md file for interactive Mermaid diagram]",
font: FONT, size: 20, italics: true, color: "888888",
})],
spacing: { before: 80, after: 80 },
shading: { type: ShadingType.SOLID, color: CODE_BG, fill: CODE_BG },
indent: { left: 360 },
}));
} else {
children.push(...buildCodeBlock(token));
}
children.push(new Paragraph({ spacing: { after: 80 } }));
break;
case "list":
if (skipToc) continue;
children.push(...buildList(token));
break;
case "hr":
skipToc = false;
children.push(new Paragraph({
spacing: { before: 200, after: 200 },
border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: BORDER_COLOR } },
}));
break;
case "space":
break;
default:
if (token.raw && !skipToc) {
children.push(new Paragraph({
children: [new TextRun({ text: decodeEntities(token.raw.trim()), font: FONT, size: 22 })],
spacing: { before: 80, after: 80 },
}));
}
break;
}
}
// --- Create and write document ---
const doc = new Document({
styles: {
default: {
document: { run: { font: FONT, size: 22 } },
heading1: {
run: { font: FONT, size: 36, bold: true, color: HEADER_COLOR },
paragraph: { spacing: { before: 360, after: 160 } },
},
heading2: {
run: { font: FONT, size: 32, bold: true, color: HEADER_COLOR },
paragraph: { spacing: { before: 320, after: 120 } },
},
heading3: {
run: { font: FONT, size: 26, bold: true, color: ACCENT_COLOR },
paragraph: { spacing: { before: 240, after: 100 } },
},
},
},
sections: [{
properties: {
page: { margin: { top: 1440, bottom: 1440, left: 1440, right: 1440 } },
},
children,
}],
features: { updateFields: false },
});
const buffer = await Packer.toBuffer(doc);
writeFileSync(outputPath, buffer);
console.log(`Generated: ${outputPath} (${(buffer.length / 1024).toFixed(0)} KB)`);
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
parveen-dotnet
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/md-to-docx · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- aaronpowell, parveen-dotnet
AutoGineer is not affiliated with or endorsed by parveen-dotnet or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download md-to-docx.zip