
Md To Docx
- 1 installs
- 37.5k repo stars
- Updated August 5, 2026
- github/awesome-copilot
md-to-docx skill documents Convert Markdown files to professionally formatted Word (.
About
md-to-docx skill documents Convert Markdown files to professionally formatted Word (.docx) documents with embedded PNG images - pure JavaScript, no external tools required. name: md-to-docx description: Convert Markdown files to professionally formatted Word (.docx) documents with embedded PNG images - pure JavaScript, no external tools required
- Convert Markdown files to professionally formatted Word (.
- Platform-specific setup patterns for md-to-docx.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for md-to-docx versus alternatives.
Md To Docx by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,983 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
md-to-docx capabilities & compatibility
- Capabilities
- md to docx quick start · md to docx when to use guidance · md to docx integration patterns
What md-to-docx says it does
cd skills/md-to-docx/scripts && npm install
node skills/md-to-docx/scripts/md-to-docx.mjs <input.md> [output.docx]
npx skills add https://github.com/github/awesome-copilot --skill md-to-docxAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 37.5k |
| Last updated | August 5, 2026 |
| Repository | github/awesome-copilot ↗ |
How do I use md-to-docx correctly?
Convert Markdown files to professionally formatted Word (.docx) documents with embedded PNG images - pure JavaScript, no external tools required
Who is it for?
Teams implementing md-to-docx workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about md-to-docx, convert markdown files to professionally formatted word (.docx) documents with embedded pn.
What you get
Working md-to-docx setup with validated configuration and next steps.
Files
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 page - Generates 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 document - Styled output — Calibri font, colored headings (
#1F3864), styled tables with alternating row colors, code blocks in Consolas - Handles 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.
/**
* 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)`);
{
"private": true,
"type": "module",
"description": "Dependencies for the Markdown to Word converter skill",
"dependencies": {
"docx": "^9.6.1",
"marked": "^17.0.4"
}
}
Related skills
FAQ
What does md-to-docx do?
md-to-docx skill documents Convert Markdown files to professionally formatted Word (.
When should I use md-to-docx?
User asks about md-to-docx, convert markdown files to professionally formatted word (.docx) documents with embedded pn.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.