@mdream/vite
Version:
Vite plugin for HTML to Markdown conversion with on-demand generation
180 lines (177 loc) • 6.64 kB
JavaScript
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 };