UNPKG

@mdream/vite

Version:

Vite plugin for HTML to Markdown conversion with on-demand generation

180 lines (177 loc) 6.64 kB
import fs from "node:fs"; import path from "node:path"; import { htmlToMarkdown } from "mdream"; //#region src/plugin.ts const DEFAULT_OPTIONS = { include: ["*.html", "**/*.html"], exclude: ["**/node_modules/**"], outputDir: "", cacheEnabled: true, mdreamOptions: {}, cacheTTL: 36e5, verbose: false }; function viteHtmlToMarkdownPlugin(userOptions = {}) { const options = { ...DEFAULT_OPTIONS, ...userOptions }; const markdownCache = /* @__PURE__ */ new Map(); function log(message) { if (options.verbose) console.log(`[vite-html-to-markdown] ${message}`); } function isValidCache(entry) { return Date.now() - entry.timestamp < entry.ttl; } function getCachedMarkdown(key) { if (!options.cacheEnabled) return null; const entry = markdownCache.get(key); if (entry && isValidCache(entry)) return entry.content; if (entry) markdownCache.delete(key); return null; } function setCachedMarkdown(key, content, ttl = options.cacheTTL) { if (!options.cacheEnabled) return; markdownCache.set(key, { content, timestamp: Date.now(), ttl }); } async function convertHtmlToMarkdown(htmlContent, source) { try { const markdownContent = htmlToMarkdown(htmlContent, options.mdreamOptions); log(`Converted ${source} to markdown (${markdownContent.length} chars)`); return markdownContent; } catch (error) { throw new Error(`Failed to convert HTML to markdown: ${error instanceof Error ? error.message : String(error)}`); } } async function handleMarkdownRequest(url, server = null, outDir) { let basePath = url.endsWith(".md") ? url.slice(0, -3) : url; if (basePath === "/index") basePath = "/"; const source = server ? "dev" : outDir ? "preview" : "build"; const cacheKey = `${source}:${basePath}`; const cached = getCachedMarkdown(cacheKey); if (cached) { log(`Cache hit for ${url}`); return { content: cached, cached: true, source }; } let htmlContent = null; if (server) { const possiblePaths = [ basePath.endsWith(".html") ? basePath : `${basePath}.html`, basePath, "/index.html" ]; for (const htmlPath of possiblePaths) try { const result = await server.transformRequest(htmlPath); if (result?.code) { htmlContent = result.code; log(`Found HTML content for ${htmlPath}`); break; } } catch { continue; } } else if (outDir) { const possiblePaths = [ path.join(outDir, `${basePath}.html`), path.join(outDir, basePath, "index.html"), path.join(outDir, "index.html") ]; for (const htmlPath of possiblePaths) if (fs.existsSync(htmlPath)) { htmlContent = fs.readFileSync(htmlPath, "utf-8"); log(`Read HTML file from ${htmlPath}`); break; } } if (!htmlContent) throw new Error(`No HTML content found for ${url}`); const markdownContent = await convertHtmlToMarkdown(htmlContent, url); setCachedMarkdown(cacheKey, markdownContent); return { content: markdownContent, cached: false, source }; } function matchesPattern(fileName, patterns) { return patterns.some((pattern) => { const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, "."); return (/* @__PURE__ */ new RegExp(`^${regexPattern}$`)).test(fileName); }); } function shouldServeMarkdown(acceptHeader, secFetchDest) { if (secFetchDest === "document") return false; const accept = acceptHeader || ""; if (accept.includes("text/html")) return false; return accept.includes("*/*") || accept.includes("text/markdown"); } function createMarkdownMiddleware(getServer, getOutDir, cacheControl) { return async (req, res, next) => { const path$1 = new URL(req.url || "", "http://localhost").pathname; const hasMarkdownExtension = path$1.endsWith(".md"); const clientPrefersMarkdown = shouldServeMarkdown(req.headers.accept, req.headers["sec-fetch-dest"]); if (path$1.startsWith("/api") || path$1.startsWith("/_") || path$1.startsWith("/@")) return next(); const lastSegment = path$1.split("/").pop() || ""; const hasExtension = lastSegment.includes("."); const extension = hasExtension ? lastSegment.substring(lastSegment.lastIndexOf(".")) : ""; if (hasExtension && extension !== ".md") return next(); if (!hasMarkdownExtension && !clientPrefersMarkdown) return next(); const url = req.url; try { const result = await handleMarkdownRequest(url, getServer(), getOutDir()); res.setHeader("Content-Type", "text/markdown; charset=utf-8"); res.setHeader("Cache-Control", cacheControl); res.setHeader("X-Markdown-Source", result.source); res.setHeader("X-Markdown-Cached", result.cached.toString()); res.end(result.content); log(`Served ${url} from ${result.source} (cached: ${result.cached})`); } catch (error) { log(`Error serving ${url}: ${error instanceof Error ? error.message : String(error)}`); res.statusCode = 404; res.end(`HTML content not found for ${url}`); } }; } return { name: "vite-html-to-markdown", configureServer(server) { server.middlewares.use(createMarkdownMiddleware(() => server, () => void 0, "no-cache")); }, generateBundle(_outputOptions, bundle) { const htmlFiles = Object.entries(bundle).filter(([fileName, file]) => { return fileName.endsWith(".html") && file.type === "asset" && matchesPattern(fileName, options.include) && !matchesPattern(fileName, options.exclude); }); if (htmlFiles.length > 0) log(`Processing ${htmlFiles.length} HTML files for markdown generation`); for (const [fileName, htmlFile] of htmlFiles) try { if (htmlFile.type !== "asset" || !("source" in htmlFile)) continue; const htmlContent = htmlFile.source; const markdownContent = htmlToMarkdown(htmlContent, options.mdreamOptions); const markdownFileName = fileName.replace(".html", ".md"); const outputPath = options.outputDir ? `${options.outputDir}/${markdownFileName}` : markdownFileName; this.emitFile({ type: "asset", fileName: outputPath, source: markdownContent }); log(`Generated markdown: ${outputPath}`); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`[vite-html-to-markdown] Failed to convert ${fileName}: ${message}`); } }, configurePreviewServer(server) { server.middlewares.use(createMarkdownMiddleware(() => null, () => server.config.build?.outDir || "dist", "public, max-age=3600")); } }; } //#endregion //#region src/index.ts var src_default = viteHtmlToMarkdownPlugin; //#endregion export { src_default as default, viteHtmlToMarkdownPlugin };