import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { StyleConfig, HtmlDocumentMeta } from "./types.js";
import { DEFAULT_STYLE } from "./constants.js";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const CODE_THEMES_DIR = path.resolve(SCRIPT_DIR, "code-themes");
export function buildCss(baseCss: string, themeCss: string, style: StyleConfig = DEFAULT_STYLE): string {
const variables = `
:root {
--md-primary-color: ${style.primaryColor};
--md-font-family: ${style.fontFamily};
--md-font-size: ${style.fontSize};
--foreground: ${style.foreground};
--blockquote-background: ${style.blockquoteBackground};
--md-accent-color: ${style.accentColor};
--md-container-bg: ${style.containerBg};
}
body {
margin: 0;
padding: 24px;
background: #ffffff;
}
#output {
max-width: 860px;
margin: 0 auto;
}
`.trim();
return [variables, baseCss, themeCss].join("\n\n");
}
export function loadCodeThemeCss(themeName: string): string {
const filePath = path.join(CODE_THEMES_DIR, `${themeName}.min.css`);
try {
return fs.readFileSync(filePath, "utf-8");
} catch {
console.error(`Code theme CSS not found: ${filePath}`);
return "";
}
}
export function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string, codeThemeCss?: string): string {
const lines = [
"",
"",
"
",
' ',
' ',
` ${meta.title}`,
];
if (meta.author) {
lines.push(` `);
}
if (meta.description) {
lines.push(` `);
}
lines.push(` `);
if (codeThemeCss) {
lines.push(` `);
}
lines.push(
"",
"",
' ',
html,
"
",
"",
""
);
return lines.join("\n");
}
export async function inlineCss(html: string): Promise {
try {
const { default: juice } = await import("juice");
return juice(html, {
inlinePseudoElements: true,
preserveImportant: true,
resolveCSSVariables: false,
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`Missing dependency "juice" for CSS inlining. Install it first (e.g. "bun add juice" or "npm add juice"). Original error: ${detail}`
);
}
}
export function normalizeCssText(cssText: string, style: StyleConfig = DEFAULT_STYLE): string {
return cssText
.replace(/var\(--md-primary-color\)/g, style.primaryColor)
.replace(/var\(--md-font-family\)/g, style.fontFamily)
.replace(/var\(--md-font-size\)/g, style.fontSize)
.replace(/var\(--blockquote-background\)/g, style.blockquoteBackground)
.replace(/var\(--md-accent-color\)/g, style.accentColor)
.replace(/var\(--md-container-bg\)/g, style.containerBg)
.replace(/hsl\(var\(--foreground\)\)/g, "#3f3f3f")
.replace(/--md-primary-color:\s*[^;"']+;?/g, "")
.replace(/--md-font-family:\s*[^;"']+;?/g, "")
.replace(/--md-font-size:\s*[^;"']+;?/g, "")
.replace(/--blockquote-background:\s*[^;"']+;?/g, "")
.replace(/--md-accent-color:\s*[^;"']+;?/g, "")
.replace(/--md-container-bg:\s*[^;"']+;?/g, "")
.replace(/--foreground:\s*[^;"']+;?/g, "");
}
export function normalizeInlineCss(html: string, style: StyleConfig = DEFAULT_STYLE): string {
let output = html;
output = output.replace(
/`
);
output = output.replace(
/style="([^"]*)"/gi,
(_match, cssText: string) => `style="${normalizeCssText(cssText, style)}"`
);
output = output.replace(
/style='([^']*)'/gi,
(_match, cssText: string) => `style='${normalizeCssText(cssText, style)}'`
);
return output;
}
export function modifyHtmlStructure(htmlString: string): string {
let output = htmlString;
const pattern =
/]*)>([\s\S]*?)(|)<\/li>/i;
while (pattern.test(output)) {
output = output.replace(pattern, "- $2
$3");
}
return output;
}
export function removeFirstHeading(html: string): string {
return html.replace(/]*>[\s\S]*?<\/h[12]>/, "");
}