import * as sass from "sass"; import { firstFound, stripFileExtension, getCleanPath, getHref } from "./util.js"; import fs from "fs/promises"; import handlebars from "handlebars"; import { marked } from "marked"; import markedCodePreview from "marked-code-preview"; import matter from "gray-matter"; import { optimize } from "svgo"; import sharp from "sharp"; import path from "path"; import { minify } from "html-minifier-terser"; import favicons from "favicons"; const imageSizes = [ "640w", "768w", "1024w", "1366w", "1600w", "1920w", "2560w" ]; const templateCache = new Map(); function createMarkdownRenderer(meta) { return marked .use({ gfm: true }) .use(markedCodePreview) .use({ renderer: { image({ href, title, text }) { const hrefWithoutExt = stripFileExtension(href); const attrs = [`alt="${text}"`]; const foundSrcSet = meta.resources.images.find(({ detail }) => { return detail.imageRef === href; }); if (foundSrcSet) { const srcSetString = foundSrcSet.detail.srcSet .map(src => src.join(" ")) .join(", "); const defaultSrc = foundSrcSet.detail.srcSet[0][0]; attrs.push(`src="${defaultSrc}"`); attrs.push(`srcset="${srcSetString}"`); attrs.push(`sizes="(min-width: 800px) 40vw, 100vw"`); attrs.push( `style="aspect-ratio: ${foundSrcSet.detail.aspectRatio}"` ); } else { attrs.push(`src="${href}"`); } if (title) { attrs.push(`title="${title}"`); } return ``; } } }); } export async function renderMarkdownWithTemplate( filePath, meta, fileOutputDir, fileOutputPath ) { const content = await fs.readFile(filePath, "utf8"); const { data, content: markdown } = matter(content); const templateName = data.template || meta.opts.defaultTemplate; const href = getHref(fileOutputPath, meta); if (!templateCache.has(templateName)) { const templatePath = firstFound( meta.opts.templateDirs, `${templateName}.hbs` ); if (!templatePath) throw new Error(`Template not found: ${templateName}`); const templateContent = await fs.readFile(templatePath, "utf8"); templateCache.set(templateName, handlebars.compile(templateContent)); } const template = templateCache.get(templateName); const renderer = createMarkdownRenderer(meta); const html = template({ ...data, ...meta, href, content: renderer(markdown) }); console.log("====>", data, meta, href); const minifiedHtml = await minify(html, { collapseWhitespace: true, removeComments: true, removeRedundantAttributes: true, removeEmptyAttributes: true, minifyCSS: true, minifyJS: true }); return { detail: { ...data, href }, result: minifiedHtml }; } export async function compileSass(filePath) { const result = await sass.compileAsync(filePath, { style: "compressed" }); return { result: result.css.toString() }; } export async function optimiseSvg(filePath) { const svgString = await fs.readFile(filePath, "utf8"); const result = optimize(svgString, { plugins: ["preset-default"] }); return { result: result.data }; } export async function copy(filePath) { const fileContent = await fs.readFile(filePath, "utf8"); return { result: fileContent }; } export async function optimiseImage(filePath, meta, fileOutputDir) { const sourceExtension = path.extname(filePath); const outputExtension = ".webp"; const base = path.basename(filePath, sourceExtension); const original = sharp(filePath); const metadata = await original.metadata(); const { width, height } = metadata; if (!width || !height) { throw new Error("Could not determine image dimensions"); } const aspectRatio = width / height; const srcSet = await Promise.all( imageSizes.map(async size => { const sizeNum = parseInt(size.replace("w", ""), 10); const outputFile = path.join( fileOutputDir, `${base}-${sizeNum}${outputExtension}` ); await original .clone() .resize(sizeNum) .webp({ quality: 80 }) .toFile(outputFile); return [getCleanPath(outputFile, meta), size]; }) ); const imageRef = getCleanPath(path.join(filePath), meta); return { result: srcSet.map(src => src[0]), detail: { imageRef, srcSet, aspectRatio }, written: true }; } export async function generateFavicons(filePath, meta, fileOutputDir) { // Configuration for favicons package const configuration = { path: getCleanPath(fileOutputDir, meta), // Path for overriding default icons path appName: meta.opts.site?.name || "Website", appShortName: meta.opts.site?.shortName || "Site", appDescription: meta.opts.site?.description || "", developerName: meta.opts.site?.author || "", developerURL: meta.opts.site?.url || "", dir: "auto", lang: meta.opts.site?.language | "en-US", background: meta.opts.site?.backgroundColor || "#ffffff", theme_color: meta.opts.site?.themeColor || "#ffffff", appleStatusBarStyle: "black-translucent", display: "standalone", orientation: "any", scope: "/", start_url: "/", version: "1.0", logging: false, pixel_art: false, loadManifestWithCredentials: false, manifestMaskable: false, icons: { android: true, appleIcon: true, appleStartup: true, favicons: true, windows: true, yandex: true } }; try { const response = await favicons(filePath, configuration); // Write all generated images to disk await Promise.all( response.images.map(async image => { const outputPath = path.join(fileOutputDir, image.name); await fs.writeFile(outputPath, image.contents); }) ); // Write all generated files (manifests, etc.) to disk await Promise.all( response.files.map(async file => { const outputPath = path.join(fileOutputDir, file.name); await fs.writeFile(outputPath, file.contents); }) ); // Combine HTML meta tags const htmlMeta = response.html.join("\n "); return { detail: { htmlMeta }, result: [ ...response.images.map(img => getCleanPath(path.join(fileOutputDir, img.name), meta) ), ...response.files.map(file => getCleanPath(path.join(fileOutputDir, file.name), meta) ) ], written: true }; } catch (error) { throw new Error(`Failed to generate favicons: ${error.message}`); } }