UNPKG

@mdream/vite

Version:

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

182 lines (179 loc) 6.35 kB
import fs from "node:fs"; import path from "node:path"; import { htmlToMarkdown } from "mdream"; //#region src/plugin.ts const DEFAULT_OPTIONS = { include: ["**/*.html"], exclude: ["**/node_modules/**"], outputDir: "", cacheEnabled: true, mdreamOptions: {}, preserveStructure: true, cacheTTL: 36e5, verbose: true }; function viteHtmlToMarkdownPlugin(userOptions = {}) { const options = { ...DEFAULT_OPTIONS, ...userOptions }; const markdownCache = 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.slice(0, -3); 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, "."); const regex = new RegExp(`^${regexPattern}$`); return regex.test(fileName); }); } return { name: "vite-html-to-markdown", configureServer(server) { server.middlewares.use(async (req, res, next) => { if (!req.url?.endsWith(".md")) return next(); try { const result = await handleMarkdownRequest(req.url, server); res.setHeader("Content-Type", "text/markdown; charset=utf-8"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("X-Markdown-Source", result.source); res.setHeader("X-Markdown-Cached", result.cached.toString()); res.end(result.content); log(`Served ${req.url} from ${result.source} (cached: ${result.cached})`); } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`Error serving ${req.url}: ${message}`); res.statusCode = 404; res.end(`HTML content not found for ${req.url}`); } }); }, 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); }); 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.preserveStructure ? `${options.outputDir}/${markdownFileName}` : `${options.outputDir}/${path.basename(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(async (req, res, next) => { if (!req.url?.endsWith(".md")) return next(); try { const outDir = server.config.build?.outDir || "dist"; const result = await handleMarkdownRequest(req.url, null, outDir); res.setHeader("Content-Type", "text/markdown; charset=utf-8"); res.setHeader("Cache-Control", "public, max-age=3600"); res.setHeader("X-Markdown-Source", result.source); res.setHeader("X-Markdown-Cached", result.cached.toString()); res.end(result.content); log(`Served ${req.url} from ${result.source} (cached: ${result.cached})`); } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`Error in preview server for ${req.url}: ${message}`); res.statusCode = 404; res.end(`HTML content not found for ${req.url}`); } }); } }; } //#endregion //#region src/index.ts var src_default = viteHtmlToMarkdownPlugin; //#endregion export { src_default as default, viteHtmlToMarkdownPlugin };