@cylixlee/mdocx
Advanced tools
+2030
| 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 |
+18
-7
@@ -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 |
+5
-16
| { | ||
| "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" | ||
| } | ||
| } |
-93
| #!/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`) | ||
| } |
-115
| 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 }; |
164711
40.82%6153
44.2%7
-12.5%