| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- 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 `<img ${attrs.join(" ")} >`
- },
- },
- })
- }
- 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}`)
- }
- }
|