🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@cylixlee/mdocx

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cylixlee/mdocx - npm Package Compare versions

Comparing version
0.2.1
to
0.2.3
+2030
dist/cli.mjs
import fs from "node:fs/promises";
import path from "node:path";
import { Command } from "commander";
import { AlignmentType, BorderStyle, CheckBox, Document, ExternalHyperlink, FootnoteReferenceRun, HeadingLevel, ImageRun, LevelFormat, Math as Math$1, MathFraction, MathIntegral, MathRadical, MathRun, MathSubScript, MathSubSuperScript, MathSum, MathSuperScript, Packer, Paragraph, Table, TableCell, TableRow, TextRun, UnderlineType, VerticalAlign, WidthType, XmlComponent } from "docx";
import katex from "katex";
import { XMLParser } from "fast-xml-parser";
import { Lexer } from "marked";
import imagesize from "image-size";
import http from "node:http";
import https from "node:https";
import { z } from "zod/v4-mini";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
//#region src/styles/classes.ts
const classes = {
Space: "MdSpace",
Code: "MdCode",
Hr: "MdHr",
Blockquote: "MdBlockquote",
Html: "MdHtml",
Def: "MdDef",
Paragraph: "MdParagraph",
Text: "MdText",
Footnote: "MdFootnote",
ListItem: "MdListItem",
Table: "MdTable",
TableHeader: "MdTableHeader",
TableCell: "MdTableCell",
Heading1: "MdHeading1",
Heading2: "MdHeading2",
Heading3: "MdHeading3",
Heading4: "MdHeading4",
Heading5: "MdHeading5",
Heading6: "MdHeading6",
Tag: "MdTag",
Link: "MdLink",
Strong: "MdStrong",
Em: "MdEm",
Codespan: "MdCodespan",
Del: "MdDel",
Br: "MdBr"
};
//#endregion
//#region src/styles/markdown.ts
const inlineTokens = new Set([
"tag",
"link",
"strong",
"em",
"codespan",
"del",
"br"
]);
function headingOutlineLevel(token) {
return {
heading1: 0,
heading2: 1,
heading3: 2,
heading4: 3,
heading5: 4,
heading6: 5
}[token];
}
function toHalfPoints(pt) {
if (pt == null) return void 0;
return pt * 2;
}
function toTwips(pt) {
if (pt == null) return void 0;
return pt * 20;
}
function toLineTwips(value) {
if (value == null) return void 0;
return Math.round(value * 240);
}
function buildRunStyle(el, defaults) {
const size = toHalfPoints(el?.size ?? defaults.size);
const color = el?.color ?? defaults.color;
const font = el?.font ?? defaults.font;
const bold = el?.bold ?? defaults.bold;
const italics = el?.italics ?? defaults.italics;
const underline = el?.underline ?? defaults.underline;
const strike = el?.strike ?? defaults.strike;
const result = {};
if (font != null) result.font = font;
if (size != null) result.size = size;
if (color != null) result.color = color;
if (bold != null) result.bold = bold;
if (italics != null) result.italics = italics;
if (strike != null) result.strike = strike;
if (underline === true) result.underline = { type: UnderlineType.SINGLE };
else if (underline !== void 0 && underline !== null) result.underline = underline;
return Object.keys(result).length ? result : void 0;
}
function buildParagraphStyle(el, defaults) {
const spacingBefore = el?.spacingBefore ?? defaults.spacingBefore;
const spacingAfter = el?.spacingAfter ?? defaults.spacingAfter;
const lineSpacing = el?.lineSpacing ?? defaults.lineSpacing;
const alignment = el?.alignment ?? defaults.alignment;
const indentLeft = el?.indentLeft ?? defaults.indentLeft;
const indentHanging = el?.indentHanging ?? defaults.indentHanging;
const indentFirstLine = el?.indentFirstLine ?? defaults.indentFirstLine;
const keepNext = el?.keepNext ?? defaults.keepNext;
const background = el?.background ?? defaults.background;
const spacing = {};
const sb = toTwips(spacingBefore);
const sa = toTwips(spacingAfter);
const sl = toLineTwips(lineSpacing);
if (sb != null) spacing.before = sb;
if (sa != null) spacing.after = sa;
if (sl != null) {
spacing.line = sl;
spacing.lineRule = "auto";
}
const indent = {};
if (indentLeft != null) indent.left = indentLeft;
if (indentHanging != null) indent.hanging = indentHanging;
if (indentFirstLine != null) indent.firstLine = indentFirstLine;
const border = {};
for (const pos of [
"top",
"bottom",
"left",
"right"
]) {
const b = el?.[`border${pos.charAt(0).toUpperCase() + pos.slice(1)}`] ?? defaults?.[`border${pos.charAt(0).toUpperCase() + pos.slice(1)}`];
if (b) {
const bs = {};
if (b.style != null) bs.style = b.style;
if (b.size != null) bs.size = b.size;
if (b.color != null) bs.color = b.color;
if (b.space != null) bs.space = b.space;
border[pos] = bs;
}
}
const result = {};
if (Object.keys(spacing).length) result.spacing = spacing;
if (Object.keys(indent).length) result.indent = indent;
if (alignment) result.alignment = alignment === "both" ? "both" : alignment === "center" ? "center" : alignment === "left" ? "left" : "right";
if (keepNext != null) result.keepNext = keepNext;
if (background) result.shading = { fill: background };
if (Object.keys(border).length) result.border = border;
return Object.keys(result).length ? result : void 0;
}
function createMarkdownStyle(config) {
const defaults = {
font: config.defaultFont,
size: config.defaultSize,
lineSpacing: config.lineSpacing
};
const tokens = [
{
token: "space",
className: classes.Space,
element: config.space
},
{
token: "code",
className: classes.Code,
element: config.code
},
{
token: "hr",
className: classes.Hr,
element: config.hr
},
{
token: "blockquote",
className: classes.Blockquote,
element: config.blockquote
},
{
token: "html",
className: classes.Html,
element: config.html
},
{
token: "def",
className: classes.Def,
element: void 0
},
{
token: "paragraph",
className: classes.Paragraph,
element: config.paragraph
},
{
token: "text",
className: classes.Text,
element: config.paragraph
},
{
token: "footnote",
className: classes.Footnote,
element: config.footnote
},
{
token: "listItem",
className: classes.ListItem,
element: config.listItem
},
{
token: "table",
className: classes.Table,
element: config.table
},
{
token: "tableHeader",
className: classes.TableHeader,
element: config.tableHeader
},
{
token: "tableCell",
className: classes.TableCell,
element: config.tableCell
},
{
token: "heading1",
className: classes.Heading1,
element: config.heading1
},
{
token: "heading2",
className: classes.Heading2,
element: config.heading2
},
{
token: "heading3",
className: classes.Heading3,
element: config.heading3
},
{
token: "heading4",
className: classes.Heading4,
element: config.heading4
},
{
token: "heading5",
className: classes.Heading5,
element: config.heading5
},
{
token: "heading6",
className: classes.Heading6,
element: config.heading6
},
{
token: "tag",
className: classes.Tag,
element: config.tag
},
{
token: "link",
className: classes.Link,
element: config.link
},
{
token: "strong",
className: classes.Strong,
element: config.strong
},
{
token: "em",
className: classes.Em,
element: config.em
},
{
token: "codespan",
className: classes.Codespan,
element: config.codespan
},
{
token: "del",
className: classes.Del,
element: config.del
},
{
token: "br",
className: classes.Br,
element: config.br
}
];
const result = {};
for (const { token, className, element } of tokens) {
const el = element;
const inline = inlineTokens.has(token);
const outlineLevel = headingOutlineLevel(token);
const style = {
inline: inline || void 0,
className
};
const run = buildRunStyle(el, defaults);
if (run) style.run = run;
if (!inline) {
const para = buildParagraphStyle(el, defaults);
if (para) {
if (outlineLevel != null) para.outlineLevel = outlineLevel;
style.paragraph = para;
} else if (outlineLevel != null) style.paragraph = { outlineLevel };
}
if (token === "tableHeader" || token === "tableCell" || token === "table") style.properties = element;
result[token] = style;
}
return result;
}
const markdown = createMarkdownStyle({});
//#endregion
//#region src/styles/numbering.ts
const numbering = { config: [{
reference: "numbering-points",
levels: [
makeNumbering(0),
makeNumbering(1),
makeNumbering(2),
makeNumbering(3),
makeNumbering(4),
makeNumbering(5),
makeNumbering(6),
makeNumbering(7),
makeNumbering(8)
]
}, {
reference: "bullet-points",
levels: [
makeBullet(0, "•"),
makeBullet(1, "■"),
makeBullet(2, "▶"),
makeBullet(3, "▲"),
makeBullet(4, "◆"),
makeBullet(5, "●"),
makeBullet(6, "□")
]
}] };
function makeNumbering(level) {
return {
level,
format: LevelFormat.DECIMAL,
text: level < 1 ? "%1" : level < 2 ? "%1.%2" : level < 3 ? "%1.%2.%3" : `%${level + 1})`
};
}
function makeBullet(level, charset) {
return {
level,
format: LevelFormat.BULLET,
text: charset
};
}
//#endregion
//#region src/styles/styles.ts
function createDefaultStyle(config) {
const size = config.defaultSize ?? 12;
const lineSpacing = config.lineSpacing ?? 1.15;
return {
document: {
run: {
size: size * 2,
font: config.defaultFont
},
paragraph: { spacing: {
line: Math.round(lineSpacing * 240),
lineRule: "auto"
} }
},
hyperlink: {},
heading1: {},
heading2: {},
heading3: {},
heading4: {},
heading5: {},
heading6: {},
strong: {},
listParagraph: {},
footnoteReference: {},
footnoteText: {},
footnoteTextChar: {},
title: {}
};
}
function createDocumentStyle(config) {
const paragraphStyles = [];
const characterStyles = [];
const markdownTheme = createMarkdownStyle(config);
const keys = Object.keys(markdownTheme);
const styles = { ...createDefaultStyle(config) };
for (const key of keys) {
const style = markdownTheme[key];
if (!style) continue;
const { className, run, inline, paragraph, basedOn = "Normal", next = "Normal", quickFormat = true } = style;
if (inline) characterStyles.push({
id: className,
name: className,
basedOn,
next,
quickFormat,
run
});
else paragraphStyles.push({
id: className,
name: className,
basedOn,
next,
quickFormat,
run,
paragraph
});
if (key in styles) styles[key] = {
...styles[key],
...style
};
}
return {
default: styles,
paragraphStyles,
characterStyles
};
}
//#endregion
//#region src/styles/index.ts
const styles = {
classes,
markdown,
numbering,
createDefaultStyle,
createDocumentStyle
};
//#endregion
//#region src/renders/render-list.ts
const countSymbol = Symbol();
function renderList(render, block, attr) {
let instance = void 0;
if (block.ordered) {
instance = (render.store.get(countSymbol) || 0) + 1;
render.store.set(countSymbol, instance);
}
const list = {
level: typeof attr.list?.level === "number" ? attr.list.level + 1 : 0,
type: block.ordered ? "number" : "bullet",
instance
};
return block.items.map((item) => {
const tokens = item.tokens;
return renderBlocks(render, tokens, {
...attr,
style: classes.ListItem,
list: {
...list,
task: item.task,
checked: item.checked
}
});
}).flat();
}
//#endregion
//#region src/utils.ts
function getHeadingLevel(level) {
if (level == null) return;
switch (level) {
case 0: return HeadingLevel.TITLE;
case 1: return HeadingLevel.HEADING_1;
case 2: return HeadingLevel.HEADING_2;
case 3: return HeadingLevel.HEADING_3;
case 4: return HeadingLevel.HEADING_4;
case 5: return HeadingLevel.HEADING_5;
case 6: return HeadingLevel.HEADING_6;
default: return HeadingLevel.HEADING_6;
}
}
function getTextAlignment(align) {
switch (align) {
case "left": return AlignmentType.LEFT;
case "center": return AlignmentType.CENTER;
case "right": return AlignmentType.RIGHT;
default: return;
}
}
function getImageTokens(tokenList, tokens = []) {
for (const token of tokenList) {
if (!token) continue;
switch (token.type) {
case "image":
tokens.push(token);
break;
case "table":
if (token.header?.length) getImageTokens(token.header, tokens);
if (token.rows?.length) for (const row of token.rows) getImageTokens(row, tokens);
break;
default:
if (token.tokens?.length) getImageTokens(token.tokens, tokens);
break;
}
}
return tokens;
}
const ImageTypeWhitelist = new Set([
"jpg",
"png",
"gif",
"bmp",
"webp",
"svg"
]);
function getImageExtension(filename = "", mime) {
let ext = "";
switch (mime) {
case "image/jpeg":
ext = "jpg";
break;
case "image/png":
ext = "png";
break;
case "image/gif":
ext = "gif";
break;
case "image/bmp":
ext = "bmp";
break;
case "image/webp":
ext = "webp";
break;
case "image/svg+xml":
ext = "svg";
break;
default:
const name = filename.split("?").pop() || "";
const index = name.lastIndexOf(".");
if (index > -1) ext = name.substring(index + 1);
break;
}
if (!ext) throw new Error(`Cannot get Image extension from mime type: ${mime}`);
else if (!ImageTypeWhitelist.has(ext)) throw new Error(`Image extension ${ext} is not supported`);
return ext;
}
function isHttp(src) {
return /^https?:\/\//.test(src);
}
//#endregion
//#region src/renders/render-checkbox.ts
function renderCheckbox(render, checked) {
return new CheckBox({
checked: !!checked,
checkedState: {
value: "2611",
font: "MS Gothic"
},
uncheckedState: {
value: "2610",
font: "MS Gothic"
}
});
}
//#endregion
//#region src/renders/render-text.ts
function renderText(render, text, attr) {
const multipleLines = text.trim().split(/\n/);
const totalLine = multipleLines.length;
const options = {
style: attr.style,
italics: attr.italics,
bold: attr.bold,
underline: attr.underline ? {} : void 0,
strike: attr.strike,
break: attr.break ? typeof attr.break === "number" ? attr.break : 1 : void 0
};
if (totalLine > 1) {
const textNodes = [];
textNodes.push(...multipleLines.map((line, index) => new TextRun({
...options,
text: line,
break: index > 0 ? 1 : void 0
})));
return textNodes;
}
return [new TextRun({
text,
...options
})];
}
//#endregion
//#region src/renders/render-image.ts
const SVG_FALLBACK_PNG = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64");
function renderImage(render, block, attr) {
if (render.ignoreImage) return false;
const image = render.findImage(block);
if (!image || !image.type) return renderText(render, `[!${block.text}](${block.href})`, attr);
const { width, height, title } = parseImageTitleSize(block, image);
const options = {
type: image.type,
data: image.data,
transformation: {
width,
height
},
altText: {
title: title || block.text,
description: block.text,
name: block.text
}
};
if (image.type === "svg") options.fallback = {
type: "png",
data: SVG_FALLBACK_PNG,
transformation: {
width,
height
}
};
return new ImageRun(options);
}
/**
* Parse image size from token title
* Supports format like "600x400" or "50%x50%" in title attribute
*/
function parseImageTitleSize(block, image) {
const title = block.title?.trim();
const match = title ? title.match(/^(\d+%?)x(\d+%?)$/) : null;
if (!match) return {
width: image.width,
height: image.height,
title: block.title
};
return {
width: match[1].endsWith("%") ? parseInt(match[1], 10) / 100 * image.width : parseInt(match[1], 10),
height: match[2].endsWith("%") ? parseInt(match[2], 10) / 100 * image.height : parseInt(match[2], 10),
title: ""
};
}
//#endregion
//#region src/renders/render-tokens.ts
function renderTokens(render, tokens, attr = {}) {
const children = [];
for (const token of tokens) {
const child = flatInlineToken(render, token, attr);
if (Array.isArray(child)) children.push(...child);
else if (child) children.push(child);
else if (child == null) console.warn(`Inline token is empty: ${token.type}`);
}
return children;
}
function flatInlineToken(render, token, attr) {
switch (token.type) {
case "escape": return renderText(render, token.text, attr);
case "html":
if (render.ignoreHtml) return false;
return renderText(render, token.text, {
...attr,
html: true,
style: classes.Tag
});
case "link": return new ExternalHyperlink({
children: renderTokens(render, token.tokens, {
...attr,
link: true,
style: classes.Link
}),
link: token.href
});
case "em": return renderTokens(render, token.tokens, {
...attr,
em: true,
style: classes.Em
});
case "strong": return renderTokens(render, token.tokens, {
...attr,
strong: true,
style: classes.Strong
});
case "codespan": return renderText(render, token.text, {
...attr,
codespan: true,
style: classes.Codespan
});
case "br": return renderText(render, "", {
break: 1,
br: true,
style: classes.Br
});
case "del": return renderTokens(render, token.tokens, {
...attr,
del: true,
style: classes.Del
});
case "text":
if (token.tokens?.length) return renderTokens(render, token.tokens, attr);
return renderText(render, token.text, attr);
case "image": return renderImage(render, token, attr);
default: return render.useInlineRender(token, attr);
}
}
//#endregion
//#region src/renders/render-paragraph.ts
function renderParagraph(render, tokens, attr) {
const heading = getHeadingLevel(attr.heading);
const alignment = getTextAlignment(attr.align);
const hasList = !attr.listNone && attr.list;
const isMdHeading = attr.style?.startsWith("MdHeading") ?? false;
const options = {
heading: heading && !isMdHeading ? heading : void 0,
alignment,
bullet: hasList && attr.list?.type === "bullet" ? { level: Math.min(attr.list.level, 9) } : void 0,
numbering: hasList && attr.list?.type === "number" ? {
level: Math.min(attr.list.level, 9),
reference: "numbering-points",
instance: attr.list.instance
} : void 0,
style: attr.style
};
const children = typeof tokens === "string" ? renderText(render, tokens, {}) : renderTokens(render, tokens, {});
if (attr.list?.task) children.unshift(renderCheckbox(render, attr.list.checked));
return new Paragraph({
children,
...options
});
}
//#endregion
//#region src/renders/render-table.ts
function renderTable(render, block, attrs) {
const toProps = (token, isHeader) => {
return {
...attrs,
align: token?.align,
style: isHeader ? classes.TableHeader : classes.TableCell
};
};
const style = render.styles.markdown;
const isThreeLine = !!render._styleConfig?.table?.threeLine;
const defaultColumnWidth = 100 / block.header.length * 100;
const tableOptions = {
style: classes.Table,
width: {
size: "100%",
type: WidthType.PERCENTAGE
},
columnWidths: block.header.map(() => defaultColumnWidth)
};
if (isThreeLine) tableOptions.borders = {
top: {
style: BorderStyle.SINGLE,
size: 12,
color: "000000"
},
bottom: {
style: BorderStyle.SINGLE,
size: 8,
color: "000000"
},
left: { style: BorderStyle.NONE },
right: { style: BorderStyle.NONE },
insideHorizontal: { style: BorderStyle.NONE },
insideVertical: { style: BorderStyle.NONE }
};
const headerCellOpts = (cell) => {
const opts = {
verticalAlign: VerticalAlign.CENTER,
...style.tableHeader.properties,
children: [renderParagraph(render, cell.tokens, toProps(cell, true))]
};
if (isThreeLine) opts.borders = { bottom: {
style: BorderStyle.SINGLE,
size: 8,
color: "000000"
} };
return opts;
};
const dataCellOpts = (cell) => ({
verticalAlign: VerticalAlign.CENTER,
...style.tableCell.properties,
children: [renderParagraph(render, cell.tokens, toProps(cell))]
});
tableOptions.rows = [new TableRow({
tableHeader: true,
cantSplit: true,
children: block.header.map((cell) => new TableCell(headerCellOpts(cell)))
}), ...block.rows.map((row) => {
return new TableRow({
cantSplit: true,
children: row.map((cell) => new TableCell(dataCellOpts(cell)))
});
})];
return new Table(tableOptions);
}
//#endregion
//#region src/extensions/mathml-to-docx.ts
let LO_COMPAT = false;
var MathMatrixElement = class extends XmlComponent {
constructor(children) {
super("m:e");
for (const child of children) this.root.push(child);
}
};
var MathMatrixRow = class extends XmlComponent {
constructor(cells) {
super("m:mr");
for (const cell of cells) this.root.push(new MathMatrixElement(cell));
}
};
var MathMatrix = class extends XmlComponent {
constructor(rows) {
super("m:m");
for (const row of rows) this.root.push(new MathMatrixRow(row));
}
};
function mathmlToDocxChildren(mathml, opts) {
const mathNode = findFirst(new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "",
textNodeName: "text",
preserveOrder: true,
trimValues: false
}).parse(mathml), "math");
LO_COMPAT = !!opts?.libreOfficeCompat;
if (!mathNode) return [];
const semantics = findFirst(childrenOf(mathNode), "semantics");
return walkChildren(childrenOf(semantics ? findFirst(childrenOf(semantics), "mrow") || semantics : findFirst(childrenOf(mathNode), "mrow") || mathNode));
}
function walkChildren(nodes) {
let out = [];
for (let i = 0; i < nodes.length; i++) {
const n = nodes[i];
const tag = tagName(n);
if (tag === "munderover" || tag === "munder" || tag === "mover") {
const kids = childrenOf(n);
const moNode = findFirst(kids, "mo");
const opText = moNode ? directText(childrenOf(moNode)) : "";
const lower = tag !== "mover" ? kids[1] ? walkNode(kids[1]) : [] : [];
const upper = tag !== "munder" ? kids[2] ? walkNode(kids[2]) : [] : [];
const base = walkChildren(nodes.slice(i + 1));
if (opText.includes("∑")) {
if (LO_COMPAT) out.push(...naryAsSubSup("∑", lower, upper, base));
else out.push(new MathSum({
children: base,
subScript: lower,
superScript: upper
}));
break;
}
if (opText.includes("∫")) {
if (LO_COMPAT) out.push(...naryAsSubSup("∫", lower, upper, base));
else out.push(new MathIntegral({
children: base,
subScript: lower,
superScript: upper
}));
break;
}
}
if (tag === "msubsup") {
const ks = childrenOf(n);
const base = ks[0];
if (tagName(base) === "mo") {
const op = directText(childrenOf(base));
const lower = ks[1] ? walkNode(ks[1]) : [];
const upper = ks[2] ? walkNode(ks[2]) : [];
const body = walkChildren(nodes.slice(i + 1));
if (op.includes("∑")) {
out.push(...LO_COMPAT ? naryAsSubSup("∑", lower, upper, body) : [new MathSum({
children: body,
subScript: lower,
superScript: upper
})]);
break;
}
if (op.includes("∫")) {
out.push(...LO_COMPAT ? naryAsSubSup("∫", lower, upper, body) : [new MathIntegral({
children: body,
subScript: lower,
superScript: upper
})]);
break;
}
}
}
out = out.concat(walkNode(n));
}
return out;
}
function walkNode(node) {
const tag = tagName(node);
if (!tag) {
const t = node.text?.toString() || "";
return t ? [new MathRun(t)] : [];
}
const kids = childrenOf(node);
switch (tag) {
case "mrow": return walkChildren(kids);
case "mi":
case "mn":
case "mo": return textFrom(kids);
case "msup": {
const [base, sup] = firstN(kids, 2);
return [new MathSuperScript({
children: walkNode(base),
superScript: walkNode(sup)
})];
}
case "msub": {
const [base, sub] = firstN(kids, 2);
return [new MathSubScript({
children: walkNode(base),
subScript: walkNode(sub)
})];
}
case "msubsup": {
const [base, sub, sup] = firstN(kids, 3);
return [new MathSubSuperScript({
children: walkNode(base),
subScript: walkNode(sub),
superScript: walkNode(sup)
})];
}
case "mfrac": {
const [num, den] = firstN(kids, 2);
return [new MathFraction({
numerator: walkNode(num),
denominator: walkNode(den)
})];
}
case "msqrt": {
const [body] = firstN(kids, 1);
return [new MathRadical({ children: walkNode(body) })];
}
case "mroot": {
const [body, degree] = firstN(kids, 2);
return [new MathRadical({
children: walkNode(body),
degree: walkNode(degree)
})];
}
case "mtable": {
const rows = kids.filter((k) => tagName(k) === "mtr");
if (LO_COMPAT) {
const parts = [];
parts.push(new MathRun("["));
rows.forEach((row, ri) => {
if (ri > 0) parts.push(new MathRun("; "));
childrenOf(row).filter((c) => tagName(c) === "mtd").forEach((cell, ci) => {
if (ci > 0) parts.push(new MathRun(", "));
parts.push(...walkChildren(childrenOf(cell)));
});
});
parts.push(new MathRun("]"));
return parts;
}
return [new MathMatrix(rows.map((row) => {
return childrenOf(row).filter((c) => tagName(c) === "mtd").map((cell) => walkChildren(childrenOf(cell)));
}))];
}
case "munderover":
case "munder":
case "mover": {
const m = childrenOf(node);
const op = textFrom(childrenOf(findFirst(m, "mo") || {}));
const low = tag !== "mover" ? m[1] ? walkNode(m[1]) : [] : [];
const up = tag !== "munder" ? m[2] ? walkNode(m[2]) : [] : [];
return op.concat(low).concat(up);
}
default: return walkChildren(kids);
}
}
function tagName(node) {
return Object.keys(node).filter((k) => k !== "text" && k !== ":@")[0] || null;
}
function childrenOf(node) {
const tag = tagName(node);
if (!tag) return [];
const val = node[tag];
return Array.isArray(val) ? val : val ? [val] : [];
}
function textFrom(nodes) {
const texts = nodes.map((n) => (n.text ?? "").toString()).join("");
return texts ? [new MathRun(texts)] : [];
}
function directText(nodes) {
return nodes.map((n) => (n.text ?? "").toString()).join("");
}
function naryAsSubSup(op, lower, upper, body) {
return [new MathSubSuperScript({
children: [new MathRun(op)],
subScript: lower,
superScript: upper
}), ...body];
}
function findFirst(nodes, name) {
for (const n of nodes) {
if (tagName(n) === name) return n;
const inner = findFirst(childrenOf(n), name);
if (inner) return inner;
}
return null;
}
function firstN(nodes, n) {
return nodes.slice(0, n);
}
//#endregion
//#region src/renders/render-blocks.ts
function renderBlocks(render, blocks, attr = {}) {
const paragraphs = [];
for (const block of blocks) {
const child = renderBlock$2(render, block, attr);
if (Array.isArray(child)) paragraphs.push(...child);
else if (child) paragraphs.push(child);
else if (child == null) console.warn(`Block is empty: ${block.type}`);
}
return paragraphs;
}
function renderBlock$2(render, block, attr) {
switch (block.type) {
case "space": return false;
case "code": {
const lang = block.lang?.trim().toLowerCase();
if (lang && /^(math|latex|katex)$/.test(lang)) {
const tex = block.text.trim();
if (render.options?.math?.engine === "katex") try {
const children = mathmlToDocxChildren(katex.renderToString(tex, {
output: "mathml",
throwOnError: false,
displayMode: true,
...render.options.math?.katexOptions || {}
}), { libreOfficeCompat: !!render.options.math?.libreOfficeCompat });
if (children && children.length) return new Paragraph({
children: [new Math$1({ children })],
style: classes.Paragraph
});
} catch {}
return renderParagraph(render, tex, {
...attr,
code: true,
style: "MdCode",
listNone: true
});
}
return renderParagraph(render, block.text, {
...attr,
code: true,
style: "MdCode",
listNone: true
});
}
case "heading": return renderParagraph(render, block.tokens, {
...attr,
heading: block.depth,
style: classes[`Heading${block.depth}`]
});
case "hr": return new Paragraph({
text: "",
thematicBreak: true,
style: classes.Hr
});
case "blockquote": return renderBlocks(render, block.tokens, {
...attr,
listNone: true,
blockquote: true,
style: classes.Blockquote
});
case "list": return renderList(render, block, attr);
case "html":
if (render.ignoreHtml) return false;
return renderParagraph(render, block.text, {
...attr,
code: true,
style: classes.Html
});
case "def": return new Paragraph({
text: block.title,
style: classes.Def
});
case "table": return renderTable(render, block, {
...attr,
listNone: true
});
case "paragraph": return renderParagraph(render, block.tokens, {
style: classes.Paragraph,
...attr
});
case "text":
if (block.tokens?.length) return renderParagraph(render, block.tokens, {
style: classes.Text,
...attr
});
return renderParagraph(render, block.text, attr);
default: return render.useBlockRender(block, attr);
}
}
//#endregion
//#region src/extensions/footnote.ts
/**
* @see https://github.com/bent10/marked-extensions/blob/main/packages/footnote/src/footnote.ts
*/
function footnote(lexer) {
let hasFootnotes = false;
let footnoteId = 0;
const footnotes = /* @__PURE__ */ new Map();
return {
name: "footnote",
init,
block: tokenizerBlock,
inline: tokenizerInline
};
function tokenizerBlock(src) {
const match = /^\[\^([^\]\n]+)\]:(?:[ \t]+|[\n]*?|$)([^\n]*?(?:\n|$)(?:\n*?[ ]{4,}[^\n]*)*)/.exec(src);
if (!match) return;
if (!hasFootnotes) {
hasFootnotes = true;
footnoteId = 0;
footnotes.clear();
}
const [raw, label, text = ""] = match;
let content = text.split("\n").reduce((acc, curr) => {
return acc + "\n" + curr.replace(/^(?:[ ]{4}|[\t])/, "");
}, "");
const contentLastLine = content.trimEnd().split("\n").pop();
content += contentLastLine && /^[ \t]*?[>\-*][ ]|[`]{3,}$|^[ \t]*?[|].+[|]$/.test(contentLastLine) ? "\n\n" : "";
const token = {
id: ++footnoteId,
type: "footnote",
raw,
label,
tokens: lexer.blockTokens(content.trim())
};
footnotes.set(label, token);
return token;
}
function tokenizerInline(src) {
const match = /^\[\^([^\]\n]+)\]/.exec(src);
if (match) {
const [raw, label] = match;
const note = footnotes.get(label);
if (!note) return;
return {
id: note.id,
type: "footnoteRef",
raw,
label
};
}
}
}
function init(render) {
render.addInlineRender("footnoteRef", renderInline$1);
render.addBlockRender("footnote", renderBlock$1);
}
function renderInline$1(render, token, attr) {
return new FootnoteReferenceRun(token.id);
}
function renderBlock$1(render, block, attr) {
const noteList = render.toBlocks(block.tokens, {
...attr,
style: classes.Footnote,
footnote: true
});
render.addFootnote(block.id, noteList);
return false;
}
//#endregion
//#region src/extensions/latex.ts
const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:;%)\]–—?!。,:-]|$)/;
const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
/**
* @see https://github.com/UziTech/marked-katex-extension/blob/main/src/index.js
*/
function latex(lexer) {
const ruleReg = inlineRule;
return {
name: "latex",
startInline: (src) => {
let index;
let indexSrc = src;
while (indexSrc) {
index = indexSrc.indexOf("$");
if (index === -1) return;
if (index === 0 || indexSrc.charAt(index - 1) !== "$") {
if (indexSrc.substring(index).match(ruleReg)) return index;
}
indexSrc = indexSrc.substring(index + 1).replace(/^\$+/, "");
}
},
inline: (src, tokens) => {
const match = src.match(ruleReg);
if (!match) return;
return {
type: "inlineKatex",
raw: match[0],
text: match[2].trim(),
displayMode: match[1].length === 2
};
},
block: (src, tokens) => {
const match = src.match(blockRule);
if (!match) return;
return {
type: "blockKatex",
raw: match[0],
text: match[2].trim(),
displayMode: match[1].length === 2
};
},
init: (render) => {
render.addInlineRender("inlineKatex", renderInline);
render.addBlockRender("blockKatex", renderBlock);
}
};
}
const macroMap = new Map([
["alpha", "α"],
["beta", "β"],
["gamma", "γ"],
["delta", "δ"],
["epsilon", "ε"],
["zeta", "ζ"],
["eta", "η"],
["theta", "θ"],
["iota", "ι"],
["kappa", "κ"],
["lambda", "λ"],
["mu", "μ"],
["nu", "ν"],
["xi", "ξ"],
["omicron", "ο"],
["pi", "π"],
["rho", "ρ"],
["sigma", "σ"],
["tau", "τ"],
["upsilon", "υ"],
["phi", "φ"],
["chi", "χ"],
["psi", "ψ"],
["omega", "ω"],
["textasciitilde", "~"],
["textbackslash", "\\"],
["textasciicircum", "^"],
["textbar", "|"],
["textless", "<"],
["textgreater", ">"],
["textunderscore", "_"],
["neq", "≠"],
["leq", "≤"],
["leqq", "≦"],
["geq", "≥"],
["geqq", "≧"],
["sim", "∼"],
["simeq", "≃"],
["approx", "≈"],
["infty", "∞"],
["fallingdotseq", "≒"],
["risingdotseq", "≓"],
["equiv", "≡"],
["ll", "≪"],
["gg", "≫"],
["times", "×"],
["div", "÷"],
["pm", "±"],
["mp", "∓"],
["oplus", "⊕"],
["otimes", "⊗"],
["ominus", "⊖"],
["oslash", "⊘"],
["odot", "⊙"],
["circ", "∘"],
["bullet", "•"],
["cdot", "⋅"],
["ltimes", "⋉"],
["rtimes", "⋊"],
["in", "∈"],
["notin", "∉"],
["ni", "∋"],
["notni", "∌"]
]);
/**
* Parse LaTeX text and convert to simple text representation
* This is a basic implementation that handles common LaTeX commands
*/
function parseLatexToText(latex) {
let text = latex;
for (const [macro, symbol] of macroMap.entries()) {
const regex = new RegExp(`\\\\${macro}(?![a-zA-Z])`, "g");
text = text.replace(regex, symbol);
}
text = text.replace(/\^(\d)/g, (_, digit) => {
return "⁰¹²³⁴⁵⁶⁷⁸⁹"[parseInt(digit)];
});
text = text.replace(/_(\d)/g, (_, digit) => {
return "₀₁₂₃₄₅₆₇₈₉"[parseInt(digit)];
});
text = text.replace(/\^{([^}]+)}/g, "^$1");
text = text.replace(/_{([^}]+)}/g, "_$1");
text = text.replace(/\\([a-zA-Z]+)/g, "$1");
text = text.replace(/[{}]/g, "");
return text;
}
function renderInline(render, token) {
const text = token.text;
if (render.options?.math?.engine === "katex") try {
const children = mathmlToDocxChildren(katex.renderToString(text, {
output: "mathml",
throwOnError: false,
displayMode: !!token.displayMode,
...render.options.math?.katexOptions || {}
}), { libreOfficeCompat: !!render.options.math?.libreOfficeCompat });
if (children && children.length) return new Math$1({ children });
} catch {}
let parsedText = parseLatexToText(text);
if (!parsedText) parsedText = text;
if (!parsedText) return new TextRun(text || "");
return new Math$1({ children: [new MathRun(parsedText)] });
}
function renderBlock(render, token) {
const text = token.text;
if (render.options?.math?.engine === "katex") try {
const children = mathmlToDocxChildren(katex.renderToString(text, {
output: "mathml",
throwOnError: false,
displayMode: !!token.displayMode,
...render.options.math?.katexOptions || {}
}), { libreOfficeCompat: !!render.options.math?.libreOfficeCompat });
if (children && children.length) return new Paragraph({ children: [new Math$1({ children })] });
} catch {}
let parsedText = parseLatexToText(text);
if (!parsedText) parsedText = text;
if (!parsedText) return new Paragraph({ children: [new TextRun(text || "")] });
return new Paragraph({ children: [new Math$1({ children: [new MathRun(parsedText)] })] });
}
//#endregion
//#region src/extensions/index.ts
function useExtensions(render) {
const lexer = new Lexer(render.options);
usePlugin(render, lexer, footnote);
usePlugin(render, lexer, latex);
return lexer;
}
function usePlugin(render, lexer, fn) {
const plugin = fn(lexer);
const extensions = lexer.options.extensions || (lexer.options.extensions = {});
if (plugin.startBlock) (extensions.startBlock || (extensions.startBlock = [])).push(plugin.startBlock);
if (plugin.startInline) (extensions.startInline || (extensions.startInline = [])).push(plugin.startInline);
if (plugin.block) (extensions.block || (extensions.block = [])).push(plugin.block);
if (plugin.inline) (extensions.inline || (extensions.inline = [])).push(plugin.inline);
if (plugin.init) plugin.init(render);
}
//#endregion
//#region src/tokenize.ts
function tokenize(render) {
return useExtensions(render).lex(render.markdown);
}
//#endregion
//#region src/presets/index.ts
const presets = {
academic: {
defaultFont: {
ascii: "Times New Roman",
eastAsia: "宋体"
},
defaultSize: 12,
lineSpacing: 1.5,
strong: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
bold: false
},
em: { italics: true },
heading1: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
size: 22,
bold: true,
spacingBefore: 24,
spacingAfter: 12
},
heading2: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
size: 16,
bold: true,
spacingBefore: 20,
spacingAfter: 10
},
heading3: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
size: 14,
bold: true,
spacingBefore: 16,
spacingAfter: 8
},
heading4: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
size: 12,
bold: true,
spacingBefore: 14,
spacingAfter: 6
},
heading5: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
size: 12,
bold: true,
spacingBefore: 12,
spacingAfter: 6
},
heading6: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
size: 12,
bold: true,
spacingBefore: 12,
spacingAfter: 6
},
paragraph: {
spacingBefore: 6,
spacingAfter: 6,
indentFirstLine: 480
},
code: {
font: "Courier New",
size: 11,
background: "f6f6f7",
borderTop: {
style: "single",
size: 1,
color: "A5A5A5",
space: 8
},
borderBottom: {
style: "single",
size: 1,
color: "A5A5A5",
space: 8
},
borderLeft: {
style: "single",
size: 1,
color: "A5A5A5",
space: 8
},
borderRight: {
style: "single",
size: 1,
color: "A5A5A5",
space: 8
},
spacingBefore: 10,
spacingAfter: 10
},
codespan: { font: "Courier New" },
blockquote: {
italics: true,
color: "666666",
background: "F9F9F9",
borderLeft: {
style: "single",
size: 20,
color: "666666",
space: 12
},
indentLeft: 360,
spacingBefore: 10,
spacingAfter: 10
},
hr: {
borderBottom: {
style: "single",
size: 1,
color: "D9D9D9",
space: 1
},
spacingBefore: 12,
spacingAfter: 12
},
link: {
color: "0563C1",
underline: true
},
del: {
strike: true,
color: "FF0000"
},
tableHeader: {
font: {
ascii: "Times New Roman",
eastAsia: "黑体"
},
size: 10.5,
bold: false
},
tableCell: {
font: {
ascii: "Times New Roman",
eastAsia: "宋体"
},
size: 10.5
},
table: {
threeLine: true,
spacingBefore: 3,
spacingAfter: 3
},
listItem: {
indentLeft: 720,
indentHanging: 360,
spacingBefore: 3,
spacingAfter: 3
},
tag: {
font: "Courier New",
color: "ED7D31"
},
html: {
font: "Courier New",
color: "4472C4"
},
footnote: { bold: false },
space: {
spacingBefore: 0,
spacingAfter: 0
}
},
minimal: {
defaultFont: "Calibri",
defaultSize: 11,
lineSpacing: 1.15,
strong: { bold: true },
em: { italics: true },
heading1: {
size: 20,
bold: true,
spacingBefore: 20,
spacingAfter: 10
},
heading2: {
size: 16,
bold: true,
spacingBefore: 16,
spacingAfter: 8
},
heading3: {
size: 14,
bold: true,
spacingBefore: 12,
spacingAfter: 6
},
heading4: {
size: 12,
bold: true,
spacingBefore: 10,
spacingAfter: 4
},
heading5: {
size: 11,
bold: true,
italics: true,
spacingBefore: 8,
spacingAfter: 4
},
heading6: {
size: 11,
italics: true,
spacingBefore: 8,
spacingAfter: 4
},
paragraph: {
spacingBefore: 6,
spacingAfter: 6
},
code: {
font: "Consolas",
size: 10,
background: "F5F5F5",
borderTop: {
style: "single",
size: 1,
color: "D4D4D4",
space: 6
},
borderBottom: {
style: "single",
size: 1,
color: "D4D4D4",
space: 6
},
borderLeft: {
style: "single",
size: 1,
color: "D4D4D4",
space: 6
},
borderRight: {
style: "single",
size: 1,
color: "D4D4D4",
space: 6
},
spacingBefore: 8,
spacingAfter: 8
},
codespan: {
font: "Consolas",
color: "D63384"
},
blockquote: {
italics: true,
color: "6B7280",
borderLeft: {
style: "single",
size: 16,
color: "D1D5DB",
space: 10
},
indentLeft: 360,
spacingBefore: 8,
spacingAfter: 8
},
hr: {
borderBottom: {
style: "single",
size: 1,
color: "E5E7EB",
space: 1
},
spacingBefore: 10,
spacingAfter: 10
},
link: {
color: "2563EB",
underline: true
},
del: {
strike: true,
color: "DC2626"
},
tableHeader: {
background: "F3F4F6",
bold: true
},
table: {
spacingBefore: 3,
spacingAfter: 3
},
listItem: {
indentLeft: 720,
indentHanging: 360,
spacingBefore: 3,
spacingAfter: 3
},
tag: {
font: "Consolas",
color: "059669"
},
html: {
font: "Consolas",
color: "7C3AED"
},
footnote: { bold: false },
space: {
spacingBefore: 0,
spacingAfter: 0
}
}
};
function getPreset(name) {
const preset = presets[name];
if (!preset) throw new Error(`Unknown preset "${name}". Available presets: ${Object.keys(presets).join(", ")}`);
return preset;
}
function resolveStyleConfig(preset, overrides) {
const base = typeof preset === "string" ? getPreset(preset) : preset;
if (!overrides) return base;
return deepMerge(base, overrides);
}
function deepMerge(base, overrides) {
const result = { ...base };
for (const key of Object.keys(overrides)) {
const ov = overrides[key];
const bv = base[key];
if (ov && typeof ov === "object" && !Array.isArray(ov) && bv && typeof bv === "object" && !Array.isArray(bv)) result[key] = {
...bv,
...ov
};
else result[key] = ov;
}
return result;
}
//#endregion
//#region src/MarkdownDocx.ts
var MarkdownDocx = class MarkdownDocx {
static {
this.defaultOptions = {
gfm: true,
math: { engine: "katex" },
preset: "academic"
};
}
static covert(markdown, _options = {}) {
return new MarkdownDocx(markdown, _options).toDocument();
}
constructor(markdown, options = {}) {
this.markdown = markdown;
this.options = options;
this.styles = styles;
this.store = /* @__PURE__ */ new Map();
this._imageStore = /* @__PURE__ */ new Map();
this.footnotes = {};
this._blockRender = /* @__PURE__ */ new Map();
this._inlineRender = /* @__PURE__ */ new Map();
this.options = {
...MarkdownDocx.defaultOptions,
...options
};
}
get ignoreImage() {
return !!this.options.ignoreImage;
}
get ignoreFootnote() {
return !!this.options.ignoreFootnote;
}
get ignoreHtml() {
return !!this.options.ignoreHtml;
}
async toDocument(options) {
this.footnotes = {};
const styleConfig = resolveStyleConfig(this.options.preset ?? "academic", this.options.style);
this._styleConfig = styleConfig;
const section = await this.toSection();
return new Document({
numbering,
styles: createDocumentStyle(styleConfig),
...this.options.document,
...options,
footnotes: this.footnotes,
sections: [{ children: section }]
});
}
async toSection() {
const tokenList = tokenize(this);
if (!this.ignoreImage) {
const imageList = getImageTokens(tokenList);
if (imageList.length) await this.downloadImageList(imageList);
}
return this.toBlocks(tokenList);
}
async downloadImageList(tokens) {
const imageAdapter = this.options.imageAdapter;
if (typeof imageAdapter !== "function") throw new Error("MarkdownDocx.imageAdapter is not a function");
const store = this._imageStore;
const promises = tokens.map((token) => {
if (store.has(token.href)) return Promise.resolve(store.get(token.href));
const cache = {};
store.set(token.href, cache);
return imageAdapter(token, this.options.baseDir).then((item) => {
Object.assign(cache, item);
return cache;
});
});
return Promise.all(promises);
}
toBlocks(tokens, attr = {}) {
return renderBlocks(this, tokens, attr);
}
toTexts(tokens, attr = {}) {
return renderTokens(this, tokens, attr);
}
addFootnote(id, children) {
this.footnotes[id] = { children };
}
findImage(token) {
const image = this._imageStore.get(token.href);
if (!image) return null;
return image;
}
addBlockRender(blockType, renderFn) {
this._blockRender.set(blockType, renderFn);
}
addInlineRender(inlineType, renderFn) {
this._inlineRender.set(inlineType, renderFn);
}
useBlockRender(block, attr) {
const renderFn = this._blockRender.get(block.type);
if (renderFn) return renderFn(this, block, attr);
return null;
}
useInlineRender(token, attr) {
const renderFn = this._inlineRender.get(token.type);
if (renderFn) return renderFn(this, token, attr);
return null;
}
};
//#endregion
//#region src/index.ts
function markdownDocx(markdown, options = {}) {
return MarkdownDocx.covert(markdown, options);
}
//#endregion
//#region src/adapters/nodejs.ts
const MAX_IMAGE_WIDTH = 600;
const SVG_HEAD = Buffer.from("<svg");
const downloadImage = async function(token, srcBaseDir) {
const src = token.href;
if (!src) return null;
try {
const buffer = await loadImage(src, srcBaseDir);
if (isSvgBuffer(buffer)) return handleSvgImage(buffer);
const { width, height, type } = imagesize(buffer);
const supportType = getImageExtension(src, type);
if (!supportType) return null;
if (supportType === "webp") {
console.error(`[MarkdownDocx] Webp is not supported in the nodejs environment`);
return null;
}
return {
type: supportType,
data: buffer,
width,
height
};
} catch (error) {
console.error(`[MarkdownDocx] downloadImageError`, error);
return null;
}
};
function isSvgBuffer(buffer) {
return buffer.indexOf(SVG_HEAD) !== -1;
}
function handleSvgImage(buffer) {
const { width, height } = parseSvgDimensions(buffer);
return {
type: "svg",
data: buffer,
width: Math.min(width, MAX_IMAGE_WIDTH),
height: width ? Math.round(height * Math.min(1, MAX_IMAGE_WIDTH / width)) : height
};
}
function parseSvgDimensions(buffer) {
const head = buffer.toString("utf-8", 0, 2e3);
const widthMatch = head.match(/width\s*=\s*["'](\d+(?:\.\d+)?)\s*(?:px|pt|in|mm|cm)?["']/i);
const heightMatch = head.match(/height\s*=\s*["'](\d+(?:\.\d+)?)\s*(?:px|pt|in|mm|cm)?["']/i);
if (widthMatch && heightMatch) return {
width: parseFloat(widthMatch[1]),
height: parseFloat(heightMatch[1])
};
const viewBoxMatch = head.match(/viewBox\s*=\s*["']([-\d.]+)\s+([-\d.]+)\s+([\d.]+)\s+([\d.]+)["']/i);
if (viewBoxMatch) return {
width: parseFloat(viewBoxMatch[3]),
height: parseFloat(viewBoxMatch[4])
};
return {
width: 600,
height: 450
};
}
function loadImage(src, srcBaseDir) {
if (isHttp(src)) return new Promise((resolve, reject) => {
(src.startsWith("https") ? https : http).get(src, (res) => {
const chunks = [];
res.on("data", (chunk) => {
chunks.push(chunk);
});
res.on("end", () => {
resolve(Buffer.concat(chunks));
});
res.on("error", (err) => {
reject(/* @__PURE__ */ new Error(`Failed to load image: ${err.message || err}`));
});
});
});
const resolvedPath = srcBaseDir && !isHttp(src) ? path.resolve(srcBaseDir, src) : src;
return fs.readFile(resolvedPath);
}
//#endregion
//#region src/mcp.ts
MarkdownDocx.defaultOptions.imageAdapter = downloadImage;
function descStr(d) {
return z.string({ description: d });
}
function descEnum(values, d) {
return z.enum(values, { description: d });
}
function resolveOutputPath(inputPath, outputPath) {
if (outputPath) return outputPath;
return inputPath.replace(/\.mdx?$/, ".docx");
}
async function loadConfigFile(configPath) {
const content = await fs.readFile(configPath, "utf-8");
return JSON.parse(content);
}
async function start() {
const server = new McpServer({
name: "mdocx",
version: JSON.parse(await fs.readFile(new URL("../package.json", import.meta.url), "utf-8")).version
}, { capabilities: { tools: {} } });
server.registerTool("convert_markdown_to_docx", {
description: "Convert a Markdown file to DOCX format.",
inputSchema: {
inputPath: descStr("Path to the input Markdown file (.md)"),
outputPath: z.optional(descStr("Path for the output DOCX file (defaults to input filename with .docx extension)")),
preset: z.optional(descEnum(Object.keys(presets), `Style preset: ${Object.keys(presets).join(", ")} (default: "academic")`)),
config: z.optional(descStr("Path to a JSON config file (may include preset, style, ignoreImage, math, etc.)"))
}
}, async (args) => {
const { inputPath, outputPath, preset, config: configPath } = args;
const resolvedOutput = resolveOutputPath(inputPath, outputPath);
const ext = path.extname(resolvedOutput);
if (ext && ext.toLowerCase() !== ".docx") return {
content: [{
type: "text",
text: `Output file must be a .docx file, but got ${ext}`
}],
isError: true
};
const options = {};
if (configPath) try {
const configOptions = await loadConfigFile(configPath);
Object.assign(options, configOptions);
} catch (err) {
return {
content: [{
type: "text",
text: `Failed to load config file "${configPath}": ${err.message}`
}],
isError: true
};
}
if (preset) options.preset = preset;
let content;
try {
content = await fs.readFile(inputPath, "utf-8");
} catch (err) {
return {
content: [{
type: "text",
text: `Failed to read input file "${inputPath}": ${err.message}`
}],
isError: true
};
}
if (!content) return {
content: [{
type: "text",
text: `Input file "${inputPath}" is empty`
}],
isError: true
};
options.baseDir = path.dirname(path.resolve(inputPath));
let docx;
try {
docx = await markdownDocx(content, options);
} catch (err) {
return {
content: [{
type: "text",
text: `Conversion failed: ${err.message}`
}],
isError: true
};
}
const buffer = Buffer.from(await Packer.toBuffer(docx));
await fs.writeFile(resolvedOutput, buffer);
return { content: [{
type: "text",
text: `DOCX file created: ${resolvedOutput}`
}] };
});
const transport = new StdioServerTransport();
process.on("SIGINT", () => {
server.close().then(() => process.exit(0));
});
process.on("SIGTERM", () => {
server.close().then(() => process.exit(0));
});
await server.connect(transport);
}
//#endregion
//#region src/cli.ts
MarkdownDocx.defaultOptions.imageAdapter = downloadImage;
const pkg = JSON.parse(await fs.readFile(new URL("../package.json", import.meta.url), "utf-8"));
const NAME = "mdocx";
const DESCRIPTION = "Convert Markdown file to DOCX format";
const VERSION = pkg.version;
const presetNames = Object.keys(presets);
const program = new Command();
program.name(NAME).description(DESCRIPTION).version(VERSION, "-v, --version", "output the version number").addHelpText("after", `
Presets: ${presetNames.join(", ")}
See examples/sample-config.json for a full config file reference.
`.trim()).option("-i, --input <file>", "input markdown file").option("-o, --output <file>", "output docx file (defaults to input filename with .docx extension)").option("-p, --preset <name>", `style preset: ${presetNames.join(", ")} (default: "academic")`).option("-c, --config <file>", "JSON config file (may include preset, style, ignoreImage, math, etc.)");
program.command("mcp").description("Start MCP server (stdio transport)").action(async () => {
await start();
});
program.action(doCommand);
program.parseAsync(process.argv).catch((err) => {
console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
if (err.message === "Input file is required") program.help();
else console.error(err.stack);
process.exit(1);
});
async function doCommand(options) {
if (!options.input) throw new Error("Input file is required");
if (!options.output) options.output = options.input.replace(/\.mdx?$/, ".docx");
const ext = path.extname(options.output);
if (!ext) options.output += ".docx";
else if (ext.toLowerCase() !== ".docx") throw new Error(`[${NAME}] Output file must be a .docx file, but got ${ext}`);
const markdownDocxOptions = {};
if (options.config) try {
const configContent = await fs.readFile(options.config, "utf-8");
const baseOptions = JSON.parse(configContent);
Object.assign(markdownDocxOptions, baseOptions);
} catch (err) {
throw new Error(`Failed to load config file "${options.config}": ${err.message}`);
}
if (options.preset) markdownDocxOptions.preset = options.preset;
markdownDocxOptions.baseDir = path.dirname(path.resolve(options.input));
const content = await fs.readFile(options.input, "utf-8");
if (!content) throw new Error(`[${NAME}] File ${options.input} is empty`);
const docx = await markdownDocx(content, markdownDocxOptions);
const buffer = Buffer.from(await Packer.toBuffer(docx));
await fs.writeFile(options.output, buffer);
console.log(`[${NAME}] File ${options.output} created successfully`);
}
//#endregion
export {};
import * as docx1 from "docx";
import { Document, FileChild, IParagraphStylePropertiesOptions, IPropertiesOptions, IRunStylePropertiesOptions, IStylesOptions, Packer, Paragraph, ParagraphChild } from "docx";
import { Lexer, MarkedOptions, Token, Tokens } from "marked";
//#region src/extensions/types.d.ts
/**
* Represents a single footnote.
*/
type Footnote = {
id: number;
type: 'footnote';
raw: string;
label: string;
tokens: Token[];
};
/**
* Represents a reference to a footnote.
*/
type FootnoteRef = {
type: 'footnoteRef';
raw: string;
id: number;
label: string;
};
type InlineKatex = {
type: 'inlineKatex';
raw: string;
displayMode: boolean;
text: string;
};
type BlockKatex = {
type: 'blockKatex';
raw: string;
displayMode: boolean;
text: string;
};
//#endregion
//#region src/types.d.ts
type MarkdownImageType = 'jpg' | 'png' | 'gif' | 'bmp' | 'svg' | 'webp';
type MarkdownImageItem = {
type: MarkdownImageType;
data: Buffer | string | Uint8Array | ArrayBuffer;
width: number;
height: number;
};
type MarkdownImageAdapter = (token: Tokens.Image, srcBaseDir?: string) => Promise<null | MarkdownImageItem>;
interface MarkdownDocxOptions extends MarkedOptions {
imageAdapter?: MarkdownImageAdapter;
/**
* Built-in style preset name
* @default "academic"
*/
preset?: string;
/**
* Style overrides on top of the preset
*/
style?: Partial<IMarkdownStyleConfig>;
/**
* Math engine configuration
* builtin: simple unicode mapping
* katex: KaTeX -> MathML -> docx Math
*/
math?: {
engine?: 'builtin' | 'katex';
katexOptions?: Record<string, any>;
/** Prefer constructs that are broadly supported by LibreOffice (e.g., avoid true OMML matrices and n-ary) */
libreOfficeCompat?: boolean;
};
/**
* do not download image
* @default false
*/
ignoreImage?: boolean;
/**
* do not parse footnote
* @default false
*/
ignoreFootnote?: boolean;
/**
* do not parse html
* @default false
*/
ignoreHtml?: boolean;
/**
* Base directory for resolving relative image references.
* When set, relative image paths in the markdown are resolved relative to this directory.
*/
baseDir?: string;
/**
* Properties for the document
*/
document?: Omit<IPropertiesOptions, 'sections'>;
}
type IBlockToken = Tokens.Space | Tokens.Code | Tokens.Heading | Tokens.Hr | Tokens.Blockquote | Tokens.List | Tokens.HTML | Tokens.Def | Tokens.Table | Tokens.Heading | Tokens.Paragraph | Tokens.Text | Footnote;
type IInlineToken = Tokens.Escape | Tokens.Tag | Tokens.Link | Tokens.Em | Tokens.Strong | Tokens.Codespan | Tokens.Br | Tokens.Del | Tokens.Text | Tokens.Image | FootnoteRef | InlineKatex | BlockKatex;
type IParagraphToken = Tokens.Paragraph | Tokens.Blockquote | Tokens.Heading;
type ITextAttr = {
style?: string;
bold?: boolean;
italics?: boolean;
underline?: boolean;
strike?: boolean;
break?: boolean | number;
html?: boolean;
link?: boolean;
strong?: boolean;
em?: boolean;
codespan?: boolean;
del?: boolean;
br?: boolean;
};
type IBlockAttr = {
style?: string;
blockquote?: boolean;
list?: {
task?: boolean;
checked?: boolean;
level: number;
type?: 'number' | 'bullet';
/**
* @link https://github.com/dolanmiu/docx/pull/816
* @link https://github.com/dolanmiu/docx/issues/3037#issuecomment-3164253396
*/
instance?: number;
};
listNone?: boolean;
heading?: number;
code?: boolean;
align?: 'left' | 'center' | 'right' | null;
footnote?: boolean;
};
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
type IMarkdownToken = 'space' | 'code' | 'hr' | 'blockquote' | 'html' | 'def' | 'paragraph' | 'text' | 'footnote' | 'listItem' | 'table' | 'tableHeader' | 'tableCell' | 'heading1' | 'heading2' | 'heading3' | 'heading4' | 'heading5' | 'heading6' | 'tag' | 'link' | 'strong' | 'em' | 'codespan' | 'del' | 'br';
type IMarkdownStyle = {
inline?: boolean;
className: string;
name?: string;
basedOn?: string;
next?: string;
run?: IRunStylePropertiesOptions;
paragraph?: IParagraphStylePropertiesOptions;
quickFormat?: boolean;
properties?: any;
};
type IMarkdownRenderFunction = (render: MarkdownDocx, token: IInlineToken | IBlockToken, attr?: ITextAttr | IBlockAttr) => ParagraphChild | ParagraphChild[] | FileChild | FileChild[] | false | null;
type IFontConfig = string | {
ascii?: string;
eastAsia?: string;
hAnsi?: string;
cs?: string;
};
interface IBorderConfig {
style?: string;
size?: number;
color?: string;
space?: number;
}
interface IElementStyle {
font?: IFontConfig;
size?: number;
color?: string;
bold?: boolean;
italics?: boolean;
underline?: boolean;
strike?: boolean;
spacingBefore?: number;
spacingAfter?: number;
lineSpacing?: number;
alignment?: 'left' | 'center' | 'right' | 'both';
indentLeft?: number;
indentHanging?: number;
indentFirstLine?: number;
keepNext?: boolean;
outlineLevel?: number;
threeLine?: boolean;
borderTop?: IBorderConfig;
borderBottom?: IBorderConfig;
borderLeft?: IBorderConfig;
borderRight?: IBorderConfig;
background?: string;
}
interface IMarkdownStyleConfig {
defaultFont?: IFontConfig;
defaultSize?: number;
lineSpacing?: number;
paragraph?: Partial<IElementStyle>;
heading1?: Partial<IElementStyle>;
heading2?: Partial<IElementStyle>;
heading3?: Partial<IElementStyle>;
heading4?: Partial<IElementStyle>;
heading5?: Partial<IElementStyle>;
heading6?: Partial<IElementStyle>;
code?: Partial<IElementStyle>;
codespan?: Partial<IElementStyle>;
blockquote?: Partial<IElementStyle>;
link?: Partial<IElementStyle>;
strong?: Partial<IElementStyle>;
em?: Partial<IElementStyle>;
del?: Partial<IElementStyle>;
hr?: Partial<IElementStyle>;
listItem?: Partial<IElementStyle>;
table?: Partial<IElementStyle>;
tableHeader?: Partial<IElementStyle>;
tableCell?: Partial<IElementStyle>;
tag?: Partial<IElementStyle>;
html?: Partial<IElementStyle>;
space?: Partial<IElementStyle>;
footnote?: Partial<IElementStyle>;
br?: Partial<IElementStyle>;
}
//#endregion
//#region src/styles/styles.d.ts
declare function createDefaultStyle(config: IMarkdownStyleConfig): IStylesOptions['default'];
declare function createDocumentStyle(config: IMarkdownStyleConfig): IStylesOptions;
//#endregion
//#region src/styles/index.d.ts
declare const styles: {
classes: {
readonly Space: "MdSpace";
readonly Code: "MdCode";
readonly Hr: "MdHr";
readonly Blockquote: "MdBlockquote";
readonly Html: "MdHtml";
readonly Def: "MdDef";
readonly Paragraph: "MdParagraph";
readonly Text: "MdText";
readonly Footnote: "MdFootnote";
readonly ListItem: "MdListItem";
readonly Table: "MdTable";
readonly TableHeader: "MdTableHeader";
readonly TableCell: "MdTableCell";
readonly Heading1: "MdHeading1";
readonly Heading2: "MdHeading2";
readonly Heading3: "MdHeading3";
readonly Heading4: "MdHeading4";
readonly Heading5: "MdHeading5";
readonly Heading6: "MdHeading6";
readonly Tag: "MdTag";
readonly Link: "MdLink";
readonly Strong: "MdStrong";
readonly Em: "MdEm";
readonly Codespan: "MdCodespan";
readonly Del: "MdDel";
readonly Br: "MdBr";
};
markdown: Record<IMarkdownToken, IMarkdownStyle>;
numbering: docx1.INumberingOptions;
createDefaultStyle: typeof createDefaultStyle;
createDocumentStyle: typeof createDocumentStyle;
};
//#endregion
//#region src/MarkdownDocx.d.ts
declare class MarkdownDocx {
markdown: string;
options: MarkdownDocxOptions;
static defaultOptions: MarkdownDocxOptions;
styles: {
classes: {
readonly Space: "MdSpace";
readonly Code: "MdCode";
readonly Hr: "MdHr";
readonly Blockquote: "MdBlockquote";
readonly Html: "MdHtml";
readonly Def: "MdDef";
readonly Paragraph: "MdParagraph";
readonly Text: "MdText";
readonly Footnote: "MdFootnote";
readonly ListItem: "MdListItem";
readonly Table: "MdTable";
readonly TableHeader: "MdTableHeader";
readonly TableCell: "MdTableCell";
readonly Heading1: "MdHeading1";
readonly Heading2: "MdHeading2";
readonly Heading3: "MdHeading3";
readonly Heading4: "MdHeading4";
readonly Heading5: "MdHeading5";
readonly Heading6: "MdHeading6";
readonly Tag: "MdTag";
readonly Link: "MdLink";
readonly Strong: "MdStrong";
readonly Em: "MdEm";
readonly Codespan: "MdCodespan";
readonly Del: "MdDel";
readonly Br: "MdBr";
};
markdown: Record<IMarkdownToken, IMarkdownStyle>;
numbering: docx1.INumberingOptions;
createDefaultStyle: typeof createDefaultStyle;
createDocumentStyle: typeof createDocumentStyle;
};
_styleConfig: IMarkdownStyleConfig | undefined;
store: Map<Symbol, any>;
static covert(markdown: string, _options?: MarkdownDocxOptions): Promise<Document>;
protected _imageStore: Map<string, MarkdownImageItem>;
private footnotes;
constructor(markdown: string, options?: MarkdownDocxOptions);
get ignoreImage(): boolean;
get ignoreFootnote(): boolean;
get ignoreHtml(): boolean;
toDocument(options?: Omit<IPropertiesOptions, 'sections'>): Promise<Document>;
toSection(): Promise<FileChild[]>;
downloadImageList(tokens: Tokens.Image[]): Promise<(MarkdownImageItem | undefined)[]>;
toBlocks(tokens: IBlockToken[], attr?: IBlockAttr): FileChild[];
toTexts(tokens: IInlineToken[], attr?: ITextAttr): ParagraphChild[];
addFootnote(id: number, children: Paragraph[]): void;
findImage(token: Tokens.Image): MarkdownImageItem | null;
_blockRender: Map<string, Function>;
_inlineRender: Map<string, Function>;
addBlockRender(blockType: string, renderFn: Function): void;
addInlineRender(inlineType: string, renderFn: Function): void;
useBlockRender(block: IBlockToken, attr: IBlockAttr): FileChild | FileChild[] | false | null;
useInlineRender(token: IInlineToken, attr: ITextAttr): ParagraphChild | ParagraphChild[] | false | null;
}
//#endregion
//#region src/presets/index.d.ts
declare const presets: Record<string, IMarkdownStyleConfig>;
declare function getPreset(name: string): IMarkdownStyleConfig;
declare function resolveStyleConfig(preset: string | IMarkdownStyleConfig, overrides?: Partial<IMarkdownStyleConfig>): IMarkdownStyleConfig;
//#endregion
//#region src/index.d.ts
declare function markdownDocx(markdown: string, options?: MarkdownDocxOptions): Promise<docx1.Document>;
//#endregion
export { IBlockAttr, IBlockToken, IBorderConfig, IElementStyle, IFontConfig, IInlineToken, IMarkdownRenderFunction, IMarkdownStyle, IMarkdownStyleConfig, IMarkdownToken, IParagraphToken, ITextAttr, MarkdownDocx, MarkdownDocxOptions, MarkdownImageAdapter, MarkdownImageItem, MarkdownImageType, Packer, Writeable, markdownDocx as default, markdownDocx, getPreset, presets, resolveStyleConfig, styles };
+19
-7

@@ -40,2 +40,4 @@ Object.defineProperties(exports, {

node_https = __toESM(node_https, 1);
let node_path = require("node:path");
node_path = __toESM(node_path, 1);
//#region src/styles/classes.ts

@@ -328,3 +330,3 @@ const classes = {

}
if (token === "tableHeader" || token === "table") style.properties = element;
if (token === "tableHeader" || token === "tableCell" || token === "table") style.properties = element;
result[token] = style;

@@ -723,4 +725,5 @@ }

const hasList = !attr.listNone && attr.list;
const isMdHeading = attr.style?.startsWith("MdHeading") ?? false;
const options = {
heading,
heading: heading && !isMdHeading ? heading : void 0,
alignment,

@@ -1523,4 +1526,12 @@ bullet: hasList && attr.list?.type === "bullet" ? { level: Math.min(attr.list.level, 9) } : void 0,

},
size: 10.5,
bold: false
},
tableCell: {
font: {
ascii: "Times New Roman",
eastAsia: "宋体"
},
size: 10.5
},
table: {

@@ -1782,3 +1793,3 @@ threeLine: true,

store.set(token.href, cache);
return imageAdapter(token).then((item) => {
return imageAdapter(token, this.options.baseDir).then((item) => {
Object.assign(cache, item);

@@ -1830,7 +1841,7 @@ return cache;

const SVG_HEAD = Buffer.from("<svg");
const downloadImage = async function(token) {
const downloadImage = async function(token, srcBaseDir) {
const src = token.href;
if (!src) return null;
try {
const buffer = await loadImage(src);
const buffer = await loadImage(src, srcBaseDir);
if (isSvgBuffer(buffer)) return handleSvgImage(buffer);

@@ -1885,3 +1896,3 @@ const { width, height, type } = (0, image_size.default)(buffer);

}
function loadImage(src) {
function loadImage(src, srcBaseDir) {
if (isHttp(src)) return new Promise((resolve, reject) => {

@@ -1901,3 +1912,4 @@ (src.startsWith("https") ? node_https.default : node_http.default).get(src, (res) => {

});
return node_fs_promises.default.readFile(src);
const resolvedPath = srcBaseDir && !isHttp(src) ? node_path.default.resolve(srcBaseDir, src) : src;
return node_fs_promises.default.readFile(resolvedPath);
}

@@ -1904,0 +1916,0 @@ //#endregion

@@ -9,2 +9,3 @@ import { AlignmentType, BorderStyle, CheckBox, Document, ExternalHyperlink, FootnoteReferenceRun, HeadingLevel, ImageRun, LevelFormat, Math as Math$1, MathFraction, MathIntegral, MathRadical, MathRun, MathSubScript, MathSubSuperScript, MathSum, MathSuperScript, Packer, Paragraph, Table, TableCell, TableRow, TextRun, UnderlineType, VerticalAlign, WidthType, XmlComponent } from "docx";

import https from "node:https";
import path from "node:path";
//#region src/styles/classes.ts

@@ -297,3 +298,3 @@ const classes = {

}
if (token === "tableHeader" || token === "table") style.properties = element;
if (token === "tableHeader" || token === "tableCell" || token === "table") style.properties = element;
result[token] = style;

@@ -692,4 +693,5 @@ }

const hasList = !attr.listNone && attr.list;
const isMdHeading = attr.style?.startsWith("MdHeading") ?? false;
const options = {
heading,
heading: heading && !isMdHeading ? heading : void 0,
alignment,

@@ -1492,4 +1494,12 @@ bullet: hasList && attr.list?.type === "bullet" ? { level: Math.min(attr.list.level, 9) } : void 0,

},
size: 10.5,
bold: false
},
tableCell: {
font: {
ascii: "Times New Roman",
eastAsia: "宋体"
},
size: 10.5
},
table: {

@@ -1751,3 +1761,3 @@ threeLine: true,

store.set(token.href, cache);
return imageAdapter(token).then((item) => {
return imageAdapter(token, this.options.baseDir).then((item) => {
Object.assign(cache, item);

@@ -1799,7 +1809,7 @@ return cache;

const SVG_HEAD = Buffer.from("<svg");
const downloadImage = async function(token) {
const downloadImage = async function(token, srcBaseDir) {
const src = token.href;
if (!src) return null;
try {
const buffer = await loadImage(src);
const buffer = await loadImage(src, srcBaseDir);
if (isSvgBuffer(buffer)) return handleSvgImage(buffer);

@@ -1854,3 +1864,3 @@ const { width, height, type } = imagesize(buffer);

}
function loadImage(src) {
function loadImage(src, srcBaseDir) {
if (isHttp(src)) return new Promise((resolve, reject) => {

@@ -1870,3 +1880,4 @@ (src.startsWith("https") ? https : http).get(src, (res) => {

});
return fs.readFile(src);
const resolvedPath = srcBaseDir && !isHttp(src) ? path.resolve(srcBaseDir, src) : src;
return fs.readFile(resolvedPath);
}

@@ -1873,0 +1884,0 @@ //#endregion

{
"name": "@cylixlee/mdocx",
"version": "0.2.1",
"version": "0.2.3",
"description": "Convert Markdown file to DOCX format",

@@ -42,11 +42,6 @@ "keywords": [

"bin": {
"mdocx": "bin/cli.mjs"
"mdocx": "dist/cli.mjs"
},
"directories": {
"example": "examples",
"test": "tests"
},
"files": [
"dist",
"bin"
"dist"
],

@@ -69,9 +64,3 @@ "dependencies": {

},
"module": "dist/index.node.mjs",
"sideEffects": false,
"release-it": {
"npm": {
"publish": false
}
},
"scripts": {

@@ -82,5 +71,5 @@ "dev": "tsdown --watch",

"test:coverage": "vitest run --coverage",
"release-it": "release-it",
"ts-check": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"start": "node dist/cli.mjs"
}
}
#!/usr/bin/env node
import fs from 'node:fs/promises'
import path from 'node:path'
import { Command } from 'commander'
import markdownToDocx, { Packer, presets } from '../dist/index.node.mjs'
const pkg = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8'))
const { name, description, version } = pkg
const presetNames = Object.keys(presets)
const program = new Command()
program
.name(name)
.description(description)
.version(version, '-v, --version', 'output the version number')
.addHelpText('after', `
Presets: ${presetNames.join(', ')}
See examples/sample-config.json for a full config file reference.
`.trim())
.option('-i, --input <file>', 'input markdown file')
.option('-o, --output <file>', 'output docx file (defaults to input filename with .docx extension)')
.option('-p, --preset <name>', `style preset: ${presetNames.join(', ')} (default: "academic")`)
.option('-c, --config <file>', 'JSON config file (may include preset, style, ignoreImage, math, etc.)')
program
.command('mcp')
.description('Start MCP server (stdio transport)')
.action(async () => {
const { start } = await import('./mcp.mjs')
await start()
})
program
.action(doCommand)
program
.parseAsync(process.argv)
.catch((err) => {
console.error(`\x1b[31mError: ${err.message}\x1b[0m`)
if (err.message === 'Input file is required') {
program.help()
} else {
console.error(err.stack)
}
process.exit(1)
})
async function doCommand(options) {
if (!options.input) {
throw new Error('Input file is required')
}
if (!options.output) {
options.output = options.input.replace(/\.mdx?$/, '.docx')
}
const ext = path.extname(options.output)
if (!ext) {
options.output += '.docx'
} else if (ext.toLowerCase() !== '.docx') {
throw new Error(`[${name}] Output file must be a .docx file, but got ${ext}`)
}
const markdownDocxOptions = {}
if (options.config) {
try {
const configContent = await fs.readFile(options.config, 'utf-8')
const baseOptions = JSON.parse(configContent)
Object.assign(markdownDocxOptions, baseOptions)
} catch (err) {
throw new Error(`Failed to load config file "${options.config}": ${err.message}`)
}
}
if (options.preset) {
markdownDocxOptions.preset = options.preset
}
const content = await fs.readFile(options.input, 'utf-8')
if (!content) {
throw new Error(`[${name}] File ${options.input} is empty`)
}
const docx = await markdownToDocx(content, markdownDocxOptions)
const buffer = await Packer.toBuffer(docx)
await fs.writeFile(options.output, buffer)
console.log(`[${name}] File ${options.output} created successfully`)
}
import fs from 'node:fs/promises'
import path from 'node:path'
import { z } from 'zod/v4-mini'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import markdownToDocx, { Packer, presets } from '../dist/index.node.mjs'
function resolveOutputPath(inputPath, outputPath) {
if (outputPath) return outputPath
return inputPath.replace(/\.mdx?$/, '.docx')
}
async function loadConfigFile(configPath) {
const content = await fs.readFile(configPath, 'utf-8')
return JSON.parse(content)
}
export async function start() {
const pkg = JSON.parse(
await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8')
)
const server = new McpServer(
{ name: 'mdocx', version: pkg.version },
{ capabilities: { tools: {} } }
)
server.registerTool(
'convert_markdown_to_docx',
{
description: 'Convert a Markdown file to DOCX format.',
inputSchema: {
inputPath: z.string({ description: 'Path to the input Markdown file (.md)' }),
outputPath: z.optional(z.string({ description: 'Path for the output DOCX file (defaults to input filename with .docx extension)' })),
preset: z.optional(z.enum(Object.keys(presets), { description: `Style preset: ${Object.keys(presets).join(', ')} (default: "academic")` })),
config: z.optional(z.string({ description: 'Path to a JSON config file (may include preset, style, ignoreImage, math, etc.)' })),
},
},
async (args) => {
const { inputPath, outputPath, preset, config: configPath } = args
const resolvedOutput = resolveOutputPath(inputPath, outputPath)
const ext = path.extname(resolvedOutput)
if (ext && ext.toLowerCase() !== '.docx') {
return {
content: [{ type: 'text', text: `Output file must be a .docx file, but got ${ext}` }],
isError: true,
}
}
const options = {}
if (configPath) {
try {
const configOptions = await loadConfigFile(configPath)
Object.assign(options, configOptions)
} catch (err) {
return {
content: [{ type: 'text', text: `Failed to load config file "${configPath}": ${err.message}` }],
isError: true,
}
}
}
if (preset) {
options.preset = preset
}
let content
try {
content = await fs.readFile(inputPath, 'utf-8')
} catch (err) {
return {
content: [{ type: 'text', text: `Failed to read input file "${inputPath}": ${err.message}` }],
isError: true,
}
}
if (!content) {
return {
content: [{ type: 'text', text: `Input file "${inputPath}" is empty` }],
isError: true,
}
}
let docx
try {
docx = await markdownToDocx(content, options)
} catch (err) {
return {
content: [{ type: 'text', text: `Conversion failed: ${err.message}` }],
isError: true,
}
}
const buffer = Buffer.from(await Packer.toBuffer(docx))
await fs.writeFile(resolvedOutput, buffer)
return {
content: [{ type: 'text', text: `DOCX file created: ${resolvedOutput}` }],
}
}
)
const transport = new StdioServerTransport()
process.on('SIGINT', () => {
server.close().then(() => process.exit(0))
})
process.on('SIGTERM', () => {
server.close().then(() => process.exit(0))
})
await server.connect(transport)
}
import * as docx0 from "docx";
import { Document, FileChild, IParagraphStylePropertiesOptions, IPropertiesOptions, IRunStylePropertiesOptions, IStylesOptions, Packer, Paragraph, ParagraphChild } from "docx";
import { Lexer, MarkedOptions, Token, Tokens } from "marked";
//#region src/extensions/types.d.ts
/**
* Represents a single footnote.
*/
type Footnote = {
id: number;
type: 'footnote';
raw: string;
label: string;
tokens: Token[];
};
/**
* Represents a reference to a footnote.
*/
type FootnoteRef = {
type: 'footnoteRef';
raw: string;
id: number;
label: string;
};
type InlineKatex = {
type: 'inlineKatex';
raw: string;
displayMode: boolean;
text: string;
};
type BlockKatex = {
type: 'blockKatex';
raw: string;
displayMode: boolean;
text: string;
};
//#endregion
//#region src/types.d.ts
type MarkdownImageType = 'jpg' | 'png' | 'gif' | 'bmp' | 'svg' | 'webp';
type MarkdownImageItem = {
type: MarkdownImageType;
data: Buffer | string | Uint8Array | ArrayBuffer;
width: number;
height: number;
};
type MarkdownImageAdapter = (token: Tokens.Image) => Promise<null | MarkdownImageItem>;
interface MarkdownDocxOptions extends MarkedOptions {
imageAdapter?: MarkdownImageAdapter;
/**
* Built-in style preset name
* @default "academic"
*/
preset?: string;
/**
* Style overrides on top of the preset
*/
style?: Partial<IMarkdownStyleConfig>;
/**
* Math engine configuration
* builtin: simple unicode mapping
* katex: KaTeX -> MathML -> docx Math
*/
math?: {
engine?: 'builtin' | 'katex';
katexOptions?: Record<string, any>;
/** Prefer constructs that are broadly supported by LibreOffice (e.g., avoid true OMML matrices and n-ary) */
libreOfficeCompat?: boolean;
};
/**
* do not download image
* @default false
*/
ignoreImage?: boolean;
/**
* do not parse footnote
* @default false
*/
ignoreFootnote?: boolean;
/**
* do not parse html
* @default false
*/
ignoreHtml?: boolean;
/**
* Properties for the document
*/
document?: Omit<IPropertiesOptions, 'sections'>;
}
type IBlockToken = Tokens.Space | Tokens.Code | Tokens.Heading | Tokens.Hr | Tokens.Blockquote | Tokens.List | Tokens.HTML | Tokens.Def | Tokens.Table | Tokens.Heading | Tokens.Paragraph | Tokens.Text | Footnote;
type IInlineToken = Tokens.Escape | Tokens.Tag | Tokens.Link | Tokens.Em | Tokens.Strong | Tokens.Codespan | Tokens.Br | Tokens.Del | Tokens.Text | Tokens.Image | FootnoteRef | InlineKatex | BlockKatex;
type IParagraphToken = Tokens.Paragraph | Tokens.Blockquote | Tokens.Heading;
type ITextAttr = {
style?: string;
bold?: boolean;
italics?: boolean;
underline?: boolean;
strike?: boolean;
break?: boolean | number;
html?: boolean;
link?: boolean;
strong?: boolean;
em?: boolean;
codespan?: boolean;
del?: boolean;
br?: boolean;
};
type IBlockAttr = {
style?: string;
blockquote?: boolean;
list?: {
task?: boolean;
checked?: boolean;
level: number;
type?: 'number' | 'bullet';
/**
* @link https://github.com/dolanmiu/docx/pull/816
* @link https://github.com/dolanmiu/docx/issues/3037#issuecomment-3164253396
*/
instance?: number;
};
listNone?: boolean;
heading?: number;
code?: boolean;
align?: 'left' | 'center' | 'right' | null;
footnote?: boolean;
};
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
type IMarkdownToken = 'space' | 'code' | 'hr' | 'blockquote' | 'html' | 'def' | 'paragraph' | 'text' | 'footnote' | 'listItem' | 'table' | 'tableHeader' | 'tableCell' | 'heading1' | 'heading2' | 'heading3' | 'heading4' | 'heading5' | 'heading6' | 'tag' | 'link' | 'strong' | 'em' | 'codespan' | 'del' | 'br';
type IMarkdownStyle = {
inline?: boolean;
className: string;
name?: string;
basedOn?: string;
next?: string;
run?: IRunStylePropertiesOptions;
paragraph?: IParagraphStylePropertiesOptions;
quickFormat?: boolean;
properties?: any;
};
type IMarkdownRenderFunction = (render: MarkdownDocx, token: IInlineToken | IBlockToken, attr?: ITextAttr | IBlockAttr) => ParagraphChild | ParagraphChild[] | FileChild | FileChild[] | false | null;
type IFontConfig = string | {
ascii?: string;
eastAsia?: string;
hAnsi?: string;
cs?: string;
};
interface IBorderConfig {
style?: string;
size?: number;
color?: string;
space?: number;
}
interface IElementStyle {
font?: IFontConfig;
size?: number;
color?: string;
bold?: boolean;
italics?: boolean;
underline?: boolean;
strike?: boolean;
spacingBefore?: number;
spacingAfter?: number;
lineSpacing?: number;
alignment?: 'left' | 'center' | 'right' | 'both';
indentLeft?: number;
indentHanging?: number;
indentFirstLine?: number;
keepNext?: boolean;
outlineLevel?: number;
threeLine?: boolean;
borderTop?: IBorderConfig;
borderBottom?: IBorderConfig;
borderLeft?: IBorderConfig;
borderRight?: IBorderConfig;
background?: string;
}
interface IMarkdownStyleConfig {
defaultFont?: IFontConfig;
defaultSize?: number;
lineSpacing?: number;
paragraph?: Partial<IElementStyle>;
heading1?: Partial<IElementStyle>;
heading2?: Partial<IElementStyle>;
heading3?: Partial<IElementStyle>;
heading4?: Partial<IElementStyle>;
heading5?: Partial<IElementStyle>;
heading6?: Partial<IElementStyle>;
code?: Partial<IElementStyle>;
codespan?: Partial<IElementStyle>;
blockquote?: Partial<IElementStyle>;
link?: Partial<IElementStyle>;
strong?: Partial<IElementStyle>;
em?: Partial<IElementStyle>;
del?: Partial<IElementStyle>;
hr?: Partial<IElementStyle>;
listItem?: Partial<IElementStyle>;
table?: Partial<IElementStyle>;
tableHeader?: Partial<IElementStyle>;
tableCell?: Partial<IElementStyle>;
tag?: Partial<IElementStyle>;
html?: Partial<IElementStyle>;
space?: Partial<IElementStyle>;
footnote?: Partial<IElementStyle>;
br?: Partial<IElementStyle>;
}
//#endregion
//#region src/styles/styles.d.ts
declare function createDefaultStyle(config: IMarkdownStyleConfig): IStylesOptions['default'];
declare function createDocumentStyle(config: IMarkdownStyleConfig): IStylesOptions;
//#endregion
//#region src/styles/index.d.ts
declare const styles: {
classes: {
readonly Space: "MdSpace";
readonly Code: "MdCode";
readonly Hr: "MdHr";
readonly Blockquote: "MdBlockquote";
readonly Html: "MdHtml";
readonly Def: "MdDef";
readonly Paragraph: "MdParagraph";
readonly Text: "MdText";
readonly Footnote: "MdFootnote";
readonly ListItem: "MdListItem";
readonly Table: "MdTable";
readonly TableHeader: "MdTableHeader";
readonly TableCell: "MdTableCell";
readonly Heading1: "MdHeading1";
readonly Heading2: "MdHeading2";
readonly Heading3: "MdHeading3";
readonly Heading4: "MdHeading4";
readonly Heading5: "MdHeading5";
readonly Heading6: "MdHeading6";
readonly Tag: "MdTag";
readonly Link: "MdLink";
readonly Strong: "MdStrong";
readonly Em: "MdEm";
readonly Codespan: "MdCodespan";
readonly Del: "MdDel";
readonly Br: "MdBr";
};
markdown: Record<IMarkdownToken, IMarkdownStyle>;
numbering: docx0.INumberingOptions;
createDefaultStyle: typeof createDefaultStyle;
createDocumentStyle: typeof createDocumentStyle;
};
//#endregion
//#region src/MarkdownDocx.d.ts
declare class MarkdownDocx {
markdown: string;
options: MarkdownDocxOptions;
static defaultOptions: MarkdownDocxOptions;
styles: {
classes: {
readonly Space: "MdSpace";
readonly Code: "MdCode";
readonly Hr: "MdHr";
readonly Blockquote: "MdBlockquote";
readonly Html: "MdHtml";
readonly Def: "MdDef";
readonly Paragraph: "MdParagraph";
readonly Text: "MdText";
readonly Footnote: "MdFootnote";
readonly ListItem: "MdListItem";
readonly Table: "MdTable";
readonly TableHeader: "MdTableHeader";
readonly TableCell: "MdTableCell";
readonly Heading1: "MdHeading1";
readonly Heading2: "MdHeading2";
readonly Heading3: "MdHeading3";
readonly Heading4: "MdHeading4";
readonly Heading5: "MdHeading5";
readonly Heading6: "MdHeading6";
readonly Tag: "MdTag";
readonly Link: "MdLink";
readonly Strong: "MdStrong";
readonly Em: "MdEm";
readonly Codespan: "MdCodespan";
readonly Del: "MdDel";
readonly Br: "MdBr";
};
markdown: Record<IMarkdownToken, IMarkdownStyle>;
numbering: docx0.INumberingOptions;
createDefaultStyle: typeof createDefaultStyle;
createDocumentStyle: typeof createDocumentStyle;
};
_styleConfig: IMarkdownStyleConfig | undefined;
store: Map<Symbol, any>;
static covert(markdown: string, _options?: MarkdownDocxOptions): Promise<Document>;
protected _imageStore: Map<string, MarkdownImageItem>;
private footnotes;
constructor(markdown: string, options?: MarkdownDocxOptions);
get ignoreImage(): boolean;
get ignoreFootnote(): boolean;
get ignoreHtml(): boolean;
toDocument(options?: Omit<IPropertiesOptions, 'sections'>): Promise<Document>;
toSection(): Promise<FileChild[]>;
downloadImageList(tokens: Tokens.Image[]): Promise<(MarkdownImageItem | undefined)[]>;
toBlocks(tokens: IBlockToken[], attr?: IBlockAttr): FileChild[];
toTexts(tokens: IInlineToken[], attr?: ITextAttr): ParagraphChild[];
addFootnote(id: number, children: Paragraph[]): void;
findImage(token: Tokens.Image): MarkdownImageItem | null;
_blockRender: Map<string, Function>;
_inlineRender: Map<string, Function>;
addBlockRender(blockType: string, renderFn: Function): void;
addInlineRender(inlineType: string, renderFn: Function): void;
useBlockRender(block: IBlockToken, attr: IBlockAttr): FileChild | FileChild[] | false | null;
useInlineRender(token: IInlineToken, attr: ITextAttr): ParagraphChild | ParagraphChild[] | false | null;
}
//#endregion
//#region src/presets/index.d.ts
declare const presets: Record<string, IMarkdownStyleConfig>;
declare function getPreset(name: string): IMarkdownStyleConfig;
declare function resolveStyleConfig(preset: string | IMarkdownStyleConfig, overrides?: Partial<IMarkdownStyleConfig>): IMarkdownStyleConfig;
//#endregion
//#region src/index.d.ts
declare function markdownDocx(markdown: string, options?: MarkdownDocxOptions): Promise<docx0.Document>;
//#endregion
export { IBlockAttr, IBlockToken, IBorderConfig, IElementStyle, IFontConfig, IInlineToken, IMarkdownRenderFunction, IMarkdownStyle, IMarkdownStyleConfig, IMarkdownToken, IParagraphToken, ITextAttr, MarkdownDocx, MarkdownDocxOptions, MarkdownImageAdapter, MarkdownImageItem, MarkdownImageType, Packer, Writeable, markdownDocx as default, markdownDocx, getPreset, presets, resolveStyleConfig, styles };