import * as sass from "sass" import { firstFound, generateRandomId, getCleanPath, getHref, slugifyString, writeFile, } 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 templateCache = new Map() function createMarkdownRenderer(meta) { return marked .use({ gfm: true }) .use(markedCodePreview) .use({ renderer: { image({ href, title, text }) { const attrs = [`alt="${text}"`] const foundSrcSet = meta.resources.images[slugifyString(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 `` }, }, }) } async function findTemplatePath(templateDirs, templateName) { const templatePath = await firstFound( templateDirs, `${templateName}.hbs`, ) if (!templatePath) throw new Error(`Template not found: ${templateName}`) return templatePath } async function getTemplate(templatePath) { if (!templateCache.has(templatePath)) { const templateContent = await fs.readFile(templatePath, "utf8") templateCache.set(templatePath, { path: templatePath, renderer: handlebars.compile(templateContent), }) } return templateCache.get(templatePath) } export async function renderTemplate({ config, meta, }) { const templatePath = config.filePath || await findTemplatePath( config.templateDirs, config.template, ) const fileOutputPath = config.fileOutputPath const href = getHref(fileOutputPath, meta) const template = await getTemplate(templatePath) const html = template.renderer({ ...meta, href, ...config, }) if (config.writeOut) { const minifiedHtml = await minify(html, { collapseWhitespace: true, removeComments: true, removeRedundantAttributes: true, removeEmptyAttributes: true, minifyCSS: true, minifyJS: true, }) await writeFile(fileOutputPath, minifiedHtml) return { detail: { html }, deps: { paths: [template.path], }, paths: [fileOutputPath], ref: slugifyString(href), } } return { detail: { html }, deps: { paths: [template.path], }, ref: slugifyString(href), } } export async function renderMarkdownToHtml({ config, meta, }) { const filePath = config.filePath const fileOutputPath = config.fileOutputPath const content = await fs.readFile(filePath, "utf8") const { data, content: markdown } = matter(content) const href = getHref(fileOutputPath, meta) const renderer = createMarkdownRenderer(meta) const html = renderer(markdown) return { detail: { ...data, href, content: html, fileOutputPath }, ref: slugifyString(filePath), } } export async function renderMarkdownWithTemplate({ config, meta, }) { const filePath = config.filePath const fileOutputPath = config.fileOutputPath const content = await fs.readFile(filePath, "utf8") const { data, content: markdown } = matter(content) const templateName = data.template || config.defaultTemplate const href = getHref(fileOutputPath, meta) if (!templateCache.has(templateName)) { const templatePath = await firstFound( config.templateDirs, `${templateName}.hbs`, ) if (!templatePath) throw new Error(`Template not found: ${templateName}`) const templateContent = await fs.readFile(templatePath, "utf8") templateCache.set(templateName, { path: templatePath, renderer: handlebars.compile(templateContent), }) } const template = templateCache.get(templateName) const renderer = createMarkdownRenderer(meta) const html = template.renderer({ ...data, ...meta, href, content: renderer(markdown), }) const minifiedHtml = await minify(html, { collapseWhitespace: true, removeComments: true, removeRedundantAttributes: true, removeEmptyAttributes: true, minifyCSS: true, minifyJS: true, }) await writeFile(fileOutputPath, minifiedHtml) return { detail: { ...data, href }, paths: [fileOutputPath], deps: { paths: [template.path], }, ref: slugifyString(fileOutputPath), } } export async function compileSass({ config, meta }) { const filePath = config.filePath const fileOutputPath = config.fileOutputPath const result = await sass.compileAsync(filePath, { style: "compressed" }) await writeFile(fileOutputPath, result.css) return { paths: [fileOutputPath], ref: slugifyString(fileOutputPath), detail: { href: fileOutputPath.replace(meta.opts.outDir, ""), }, deps: { paths: [...result.loadedUrls.map(item => item.pathname)], }, } } export async function optimiseSvg({ config }) { const filePath = config.filePath const fileOutputPath = config.fileOutputPath const svgString = await fs.readFile(filePath, "utf8") const result = optimize(svgString, { plugins: ["preset-default"], }) await writeFile(fileOutputPath, result.data) return { paths: [fileOutputPath], ref: slugifyString(fileOutputPath), } } export async function copy({ config }) { const filePath = config.filePath const fileOutputPath = config.fileOutputPath await fs.mkdir(config.fileOutputDir, { recursive: true }) await fs.copyFile(filePath, fileOutputPath) return { paths: [fileOutputPath], ref: slugifyString(fileOutputPath), } } export async function imageToWebP({ meta, config }) { const filePath = config.filePath const fileOutputDir = config.fileOutputDir const sourceExtension = path.extname(filePath) const outputExtension = config.outputFileExtension const base = path.basename(filePath, sourceExtension) await fs.mkdir(fileOutputDir, { recursive: true }) 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 name = config.uniqueFilenames ? base : `${base}-${generateRandomId()}` const srcSet = await Promise.all( config.imageSizes.map(async size => { const sizeNum = parseInt(size.replace("w", ""), 10) const outputFile = path.join( fileOutputDir, `${name}-${sizeNum}${outputExtension}`, ) await original .clone() .resize(sizeNum) .webp({ quality: config.quality }) .toFile(outputFile) return [getCleanPath(outputFile, meta), size] }), ) const imageRef = slugifyString(getCleanPath(path.join(filePath), meta)) return { paths: srcSet.map(src => src[0]), detail: { srcSet, aspectRatio }, ref: imageRef, } } export async function generateFavicons({ meta, config }) { const filePath = config.filePath const fileOutputDir = config.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 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 writeFile(outputPath, file.contents) }), ) // Combine HTML meta tags const htmlMeta = response.html.join("\n ") return { detail: { htmlMeta, }, paths: [ ...response.images.map(img => getCleanPath(path.join(fileOutputDir, img.name), meta), ), ...response.files.map(file => getCleanPath(path.join(fileOutputDir, file.name), meta), ), ], ref: "metatags", } } catch (error) { throw new Error(`Failed to generate favicons: ${error.message}`) } }