5.7 KiB
Markdown (.md) document conversion
Date: 2026-07-31
Goal
Add Markdown as a supported document format, convertible in both directions against every other format already handled by src/converters/document.js: html, txt, docx, pdf. No existing conversion pair changes behavior.
Scope
src/converters/document.js: 8 new registry entries (md<->html,md<->txt,md<->docx,md<->pdf).src/mime.js: MIME plumbing for the newmdformat.- Two new dependencies:
markdown-it(Markdown → HTML) andturndown(HTML → Markdown). - Out of scope:
src/app.js, the frontend, and the database./api/jobsand/api/formatsalready work purely off the registry (resolveConverter,listTargetFormats) and the upload flow has no format allowlist — confirmed by readingsrc/app.js. No frontend file survives a hardcoded format list either (confirmed viafrontend/src/App.jsx— target formats come fromGET /api/formats, and the<input type="file">has noacceptrestriction). Adding registry entries is sufficient for.mdto appear end-to-end.
Dependencies
Add via npm install markdown-it turndown (npm resolves current versions; no version numbers are hand-picked). Both are added to dependencies in package.json, alongside the existing mammoth/puppeteer/docx/pdfjs-dist document stack.
MIME plumbing (src/mime.js)
OUTPUT_MIME_TYPES: addmd: 'text/markdown'.UNDETECTABLE_TEXT_FORMATS: addmd: 'text/markdown'. A.mdfile has no distinguishing magic bytes, sofile-typecannot detect it (same situation astxtandhtmltoday) —resolveInputFormatmust trust the declared extension.
Conversion functions (src/converters/document.js)
All functions follow the existing async (inputPath, outputPath) => {...} shape and are wired up in registerDocumentConverters().
md -> html:new MarkdownIt().render(await fs.readFile(inputPath, 'utf8')), write result tooutputPath.html -> md:new TurndownService().turndown(await fs.readFile(inputPath, 'utf8')), write result tooutputPath.md -> pdf: render the Markdown to HTML viaMarkdownIt(same call asmd -> html), then pass the HTML string into the existingrenderHtmlToPdfhelper — no new PDF logic.pdf -> md: reuse the existingextractPdfPageTextshelper (already used byconvertPdfToTxt); write the page texts joined the same way (pageTexts.join('\n\n')) tooutputPath. Output is content-identical topdf -> txt, just registered under themdtarget key, since extracted plain text is already valid Markdown.docx -> md: reusemammoth.convertToHtml({ path: inputPath })(asconvertDocxToHtmlalready does), then piperesult.valuethroughTurndownService.md -> docx(the one pair with no existing reverse path to lean on): parse the Markdown withMarkdownIt().parse()and walk the resulting token stream to build adocxDocument:heading_open(h1-h6) → aParagraphwithheading: HeadingLevel.HEADING_1..HEADING_6.strong_open/em_openinline tokens →TextRun({ bold: true })/TextRun({ italics: true })for the enclosed text.bullet_list_open/ordered_list_open+ theirlist_item_openchildren → paragraphs withbullet: { level: 0 }(bulleted) or sequential numbering text prefix (ordered), consistent with the level of effort already in this file.- Any other/unrecognized block token (tables, images, blockquotes, code fences) falls back to a single plain-text
Paragraphof that block's inner text, rather than throwing — mirrors this codebase's existing precedent ofconvertPdfToDocxproducing flat, unstyled paragraphs rather than failing on content it can't fully model.
md -> txtandtxt -> md: straight passthrough copy of file content —fs.copyFile(inputPath, outputPath)or an equivalent read/write. No parsing: a Markdown file is already readable plain text and a plain text file is already valid Markdown. This mirrors the existing low-effort precedent in this file (e.g.convertTxtToPdfdoes no smart processing of its input either).
Testing
Following the existing pattern in test/converters/document.test.js and test/converters/documentFromPdf.test.js (fixture files in a beforeAll-created tmp dir, resolve(source, target) from the registry, assertions on output content/validity):
md -> html: output contains a rendered tag (e.g.<h1>for a# Headingfixture,<strong>for**bold**).html -> md: output contains Markdown syntax (e.g.#,**) for equivalent HTML input.md -> pdf: output is a valid PDF (detectInputMime(...).mime === 'application/pdf'), matching the assertion style already used fortxt -> pdf/html -> pdf/docx -> pdf.pdf -> md: output file contains the fixture's known text, matching the style already used intest/converters/documentFromPdf.test.jsforpdf -> txt.docx -> md: output contains the fixture docx's known text.md -> docx: produced docx is valid and its extracted text (via the same fixture-reading approach used elsewhere in the test file) contains the source Markdown's text content, including a heading and a bolded word from the fixture.md -> txt/txt -> md: output content is byte-identical to input.test/mime.test.js: extendoutputMimeTypecoverage withmd -> 'text/markdown', and add an "undetectable md" case mirroring the existingsample.txtcase inresolveInputFormat.
Non-goals
- No fidelity guarantees beyond what's listed above (e.g. tables, images, footnotes, nested blockquotes are not specially modeled in
md -> docx; they degrade to plain paragraphs). - No changes to job validation, storage, the database schema, or the frontend — the format is purely additive at the registry/mime layer.