Generated banner for Md To DOCX
Generated logo for Md To DOCX

Md To DOCX

GitHubOfficialFirst committed by parveen-dotnetDistributed by GitHub

  • Skill
  • Tool integration
  • Automation and integrations
  • MIT
  • Low risk
  • Has scripts

From the author

Convert Markdown files to professionally formatted Word (.docx) documents with embedded PNG images — pure JavaScript, no external tools required
— the description by parveen-dotnet

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/github/md-to-docx/download
  2. Unzip 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

FilePurpose
SKILL.mdThis instruction file
scripts/md-to-docx.mjsNode.js Markdown-to-Word converter
scripts/package.jsonDependencies (docx, marked)

Prerequisites

RequirementVersionNotes
Node.js18+Required runtime
docx9+Pure JS Word document generator
marked15+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, audience for 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 ![alt](path) references relative to the input .md file, 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:

![High-Level Architecture](diagrams/high-level-architecture.drawio.png)

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

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(/&lt;/g, "<").replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"').replace(/&#39;/g, "'")
    .replace(/&amp;/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
GitHub avatar of parveen-dotnetparveen-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