From 28b6edac82a3c6ce7553417966f7c5d4022eb8c6 Mon Sep 17 00:00:00 2001 From: Anthony GAEREMYNCK <1@anthony.sh> Date: Fri, 31 Jul 2026 08:29:55 +0200 Subject: [PATCH] feat: add MIME plumbing for markdown (.md) format Co-Authored-By: Claude Sonnet 5 --- package-lock.json | 116 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + src/mime.js | 2 + test/mime.test.js | 12 +++++ 4 files changed, 132 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9d687fa..eb31bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "express-rate-limit": "^8.6.1", "file-type": "^22.0.1", "mammoth": "^1.12.0", + "markdown-it": "^15.0.0", "multer": "^2.2.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^6.2.108", @@ -24,6 +25,7 @@ "react": "^19.2.8", "react-dom": "^19.2.8", "sharp": "^0.35.3", + "turndown": "^7.2.4", "uuid": "^14.0.1" }, "devDependencies": { @@ -674,6 +676,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@napi-rs/canvas": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.3.tgz", @@ -2557,6 +2565,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3663,6 +3683,25 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -3714,6 +3753,49 @@ "node": ">=12.0.0" } }, + "node_modules/markdown-it": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.0.tgz", + "integrity": "sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.0", + "entities": "^8.0.0", + "linkify-it": "^6.0.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.0.tgz", + "integrity": "sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "Python-2.0" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3723,6 +3805,12 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", @@ -4488,6 +4576,15 @@ "node": ">= 0.10" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/puppeteer": { "version": "25.4.0", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.4.0.tgz", @@ -5226,6 +5323,19 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/turndown": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz", + "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, "node_modules/tv4": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", @@ -5296,6 +5406,12 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "license": "MIT" + }, "node_modules/uint8array-extras": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", diff --git a/package.json b/package.json index cbcb676..433ea5a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "express-rate-limit": "^8.6.1", "file-type": "^22.0.1", "mammoth": "^1.12.0", + "markdown-it": "^15.0.0", "multer": "^2.2.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^6.2.108", @@ -27,6 +28,7 @@ "react": "^19.2.8", "react-dom": "^19.2.8", "sharp": "^0.35.3", + "turndown": "^7.2.4", "uuid": "^14.0.1" }, "devDependencies": { diff --git a/src/mime.js b/src/mime.js index 63e06c0..5493548 100644 --- a/src/mime.js +++ b/src/mime.js @@ -13,6 +13,7 @@ const OUTPUT_MIME_TYPES = { html: 'text/html', txt: 'text/plain', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + md: 'text/markdown', }; export async function detectInputMime(filePath) { @@ -31,6 +32,7 @@ export function outputMimeType(targetFormat) { const UNDETECTABLE_TEXT_FORMATS = { txt: 'text/plain', html: 'text/html', + md: 'text/markdown', }; function normalizeFormat(format) { diff --git a/test/mime.test.js b/test/mime.test.js index 12753b5..f6175f2 100644 --- a/test/mime.test.js +++ b/test/mime.test.js @@ -20,6 +20,7 @@ describe('outputMimeType', () => { expect(outputMimeType('docx')).toBe( 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ); + expect(outputMimeType('md')).toBe('text/markdown'); }); it('throws for an unknown target format', () => { @@ -61,6 +62,17 @@ describe('resolveInputFormat', () => { await fs.unlink(fixturePath); }); + it('trusts the declared format for undetectable md files', async () => { + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample.md'); + await fs.writeFile(fixturePath, '# plain markdown, no magic bytes'); + + const result = await resolveInputFormat(fixturePath, 'md'); + + expect(result).toEqual({ mime: 'text/markdown', valid: true }); + + await fs.unlink(fixturePath); + }); + it('rejects an undetectable file declared as a binary format', async () => { const fixturePath = path.join(import.meta.dirname, 'fixtures', 'sample-fake.png'); await fs.writeFile(fixturePath, 'this is not really a PNG');