@mdream/vite
Version:
Vite plugin for HTML to Markdown conversion with on-demand generation
182 lines (179 loc) • 6.35 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"],
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 };