feat: add md<->docx document converters
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,118 @@ async function convertMdToPdf(inputPath, outputPath) {
|
||||
await renderHtmlToPdf(html, outputPath);
|
||||
}
|
||||
|
||||
const MARKDOWN_HEADING_LEVELS = {
|
||||
h1: HeadingLevel.HEADING_1,
|
||||
h2: HeadingLevel.HEADING_2,
|
||||
h3: HeadingLevel.HEADING_3,
|
||||
h4: HeadingLevel.HEADING_4,
|
||||
h5: HeadingLevel.HEADING_5,
|
||||
h6: HeadingLevel.HEADING_6,
|
||||
};
|
||||
|
||||
function markdownInlineTokensToRuns(children) {
|
||||
const runs = [];
|
||||
let bold = false;
|
||||
let italics = false;
|
||||
for (const token of children ?? []) {
|
||||
if (token.type === 'strong_open') bold = true;
|
||||
else if (token.type === 'strong_close') bold = false;
|
||||
else if (token.type === 'em_open') italics = true;
|
||||
else if (token.type === 'em_close') italics = false;
|
||||
else if (token.type === 'text' || token.type === 'code_inline') {
|
||||
if (token.content) runs.push(new TextRun({ text: token.content, bold, italics }));
|
||||
} else if (token.type === 'softbreak' || token.type === 'hardbreak') {
|
||||
runs.push(new TextRun({ text: ' ' }));
|
||||
}
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
function markdownToDocxParagraphs(markdownText) {
|
||||
const tokens = new MarkdownIt().parse(markdownText, {});
|
||||
const paragraphs = [];
|
||||
const listStack = [];
|
||||
|
||||
for (let i = 0; i < tokens.length; i += 1) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type === 'heading_open') {
|
||||
const inline = tokens[i + 1];
|
||||
paragraphs.push(
|
||||
new Paragraph({
|
||||
heading: MARKDOWN_HEADING_LEVELS[token.tag],
|
||||
children: markdownInlineTokensToRuns(inline.children),
|
||||
})
|
||||
);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.type === 'bullet_list_open') {
|
||||
listStack.push({ type: 'bullet' });
|
||||
continue;
|
||||
}
|
||||
if (token.type === 'ordered_list_open') {
|
||||
listStack.push({ type: 'ordered', counter: 0 });
|
||||
continue;
|
||||
}
|
||||
if (token.type === 'bullet_list_close' || token.type === 'ordered_list_close') {
|
||||
listStack.pop();
|
||||
continue;
|
||||
}
|
||||
if (token.type === 'list_item_open') {
|
||||
const current = listStack[listStack.length - 1];
|
||||
if (current?.type === 'ordered') current.counter += 1;
|
||||
continue;
|
||||
}
|
||||
if (token.type === 'list_item_close') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.type === 'paragraph_open') {
|
||||
const inline = tokens[i + 1];
|
||||
const runs = markdownInlineTokensToRuns(inline.children);
|
||||
const current = listStack[listStack.length - 1];
|
||||
if (current?.type === 'bullet') {
|
||||
paragraphs.push(new Paragraph({ children: runs, bullet: { level: listStack.length - 1 } }));
|
||||
} else if (current?.type === 'ordered') {
|
||||
paragraphs.push(
|
||||
new Paragraph({ children: [new TextRun({ text: `${current.counter}. ` }), ...runs] })
|
||||
);
|
||||
} else {
|
||||
paragraphs.push(new Paragraph({ children: runs }));
|
||||
}
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.type === 'fence' || token.type === 'code_block') {
|
||||
if (token.content.trim()) paragraphs.push(new Paragraph({ children: [new TextRun({ text: token.content })] }));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.type === 'hr') {
|
||||
paragraphs.push(new Paragraph({ children: [new TextRun({ text: '---' })] }));
|
||||
}
|
||||
}
|
||||
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
async function convertDocxToMd(inputPath, outputPath) {
|
||||
const result = await mammoth.convertToHtml({ path: inputPath });
|
||||
const markdown = new TurndownService({ headingStyle: 'atx' }).turndown(result.value);
|
||||
await fs.writeFile(outputPath, markdown);
|
||||
}
|
||||
|
||||
async function convertMdToDocx(inputPath, outputPath) {
|
||||
const markdown = await fs.readFile(inputPath, 'utf8');
|
||||
const paragraphs = markdownToDocxParagraphs(markdown);
|
||||
const doc = new Document({ sections: [{ children: paragraphs }] });
|
||||
const buffer = await Packer.toBuffer(doc);
|
||||
await fs.writeFile(outputPath, buffer);
|
||||
}
|
||||
|
||||
async function extractPdfPageTexts(inputPath) {
|
||||
const data = new Uint8Array(await fs.readFile(inputPath));
|
||||
const doc = await pdfjsLib.getDocument({ data }).promise;
|
||||
@@ -109,6 +221,8 @@ export function registerDocumentConverters() {
|
||||
register({ family: 'document', sourceFormat: 'html', targetFormat: 'md', convert: convertHtmlToMd });
|
||||
register({ family: 'document', sourceFormat: 'md', targetFormat: 'pdf', convert: convertMdToPdf });
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'md', convert: convertPdfToTxt });
|
||||
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'md', convert: convertDocxToMd });
|
||||
register({ family: 'document', sourceFormat: 'md', targetFormat: 'docx', convert: convertMdToDocx });
|
||||
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'pdf', convert: convertDocxToPdf });
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'txt', convert: convertPdfToTxt });
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'html', convert: convertPdfToHtml });
|
||||
|
||||
Reference in New Issue
Block a user