processors.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import * as sass from "sass";
  2. import {
  3. firstFound,
  4. stripFileExtension,
  5. getCleanPath,
  6. getHref
  7. } from "./util.js";
  8. import fs from "fs/promises";
  9. import handlebars from "handlebars";
  10. import { marked } from "marked";
  11. import markedCodePreview from "marked-code-preview";
  12. import matter from "gray-matter";
  13. import { optimize } from "svgo";
  14. import sharp from "sharp";
  15. import path from "path";
  16. import { minify } from "html-minifier-terser";
  17. import favicons from "favicons";
  18. const imageSizes = [
  19. "640w",
  20. "768w",
  21. "1024w",
  22. "1366w",
  23. "1600w",
  24. "1920w",
  25. "2560w"
  26. ];
  27. const templateCache = new Map();
  28. function createMarkdownRenderer(meta) {
  29. return marked
  30. .use({ gfm: true })
  31. .use(markedCodePreview)
  32. .use({
  33. renderer: {
  34. image({ href, title, text }) {
  35. const hrefWithoutExt = stripFileExtension(href);
  36. const attrs = [`alt="${text}"`];
  37. const foundSrcSet = meta.resources.images.find(({ detail }) => {
  38. return detail.imageRef === href;
  39. });
  40. if (foundSrcSet) {
  41. const srcSetString = foundSrcSet.detail.srcSet
  42. .map(src => src.join(" "))
  43. .join(", ");
  44. const defaultSrc = foundSrcSet.detail.srcSet[0][0];
  45. attrs.push(`src="${defaultSrc}"`);
  46. attrs.push(`srcset="${srcSetString}"`);
  47. attrs.push(`sizes="(min-width: 800px) 40vw, 100vw"`);
  48. attrs.push(
  49. `style="aspect-ratio: ${foundSrcSet.detail.aspectRatio}"`
  50. );
  51. } else {
  52. attrs.push(`src="${href}"`);
  53. }
  54. if (title) {
  55. attrs.push(`title="${title}"`);
  56. }
  57. return `<img ${attrs.join(" ")} >`;
  58. }
  59. }
  60. });
  61. }
  62. export async function renderMarkdownWithTemplate(
  63. filePath,
  64. meta,
  65. fileOutputDir,
  66. fileOutputPath
  67. ) {
  68. const content = await fs.readFile(filePath, "utf8");
  69. const { data, content: markdown } = matter(content);
  70. const templateName = data.template || meta.opts.defaultTemplate;
  71. const href = getHref(fileOutputPath, meta);
  72. if (!templateCache.has(templateName)) {
  73. const templatePath = firstFound(
  74. meta.opts.templateDirs,
  75. `${templateName}.hbs`
  76. );
  77. if (!templatePath) throw new Error(`Template not found: ${templateName}`);
  78. const templateContent = await fs.readFile(templatePath, "utf8");
  79. templateCache.set(templateName, handlebars.compile(templateContent));
  80. }
  81. const template = templateCache.get(templateName);
  82. const renderer = createMarkdownRenderer(meta);
  83. const html = template({
  84. ...data,
  85. ...meta,
  86. href,
  87. content: renderer(markdown)
  88. });
  89. console.log("====>", data, meta, href);
  90. const minifiedHtml = await minify(html, {
  91. collapseWhitespace: true,
  92. removeComments: true,
  93. removeRedundantAttributes: true,
  94. removeEmptyAttributes: true,
  95. minifyCSS: true,
  96. minifyJS: true
  97. });
  98. return {
  99. detail: { ...data, href },
  100. result: minifiedHtml
  101. };
  102. }
  103. export async function compileSass(filePath) {
  104. const result = await sass.compileAsync(filePath, { style: "compressed" });
  105. return { result: result.css.toString() };
  106. }
  107. export async function optimiseSvg(filePath) {
  108. const svgString = await fs.readFile(filePath, "utf8");
  109. const result = optimize(svgString, {
  110. plugins: ["preset-default"]
  111. });
  112. return { result: result.data };
  113. }
  114. export async function copy(filePath) {
  115. const fileContent = await fs.readFile(filePath, "utf8");
  116. return { result: fileContent };
  117. }
  118. export async function optimiseImage(filePath, meta, fileOutputDir) {
  119. const sourceExtension = path.extname(filePath);
  120. const outputExtension = ".webp";
  121. const base = path.basename(filePath, sourceExtension);
  122. const original = sharp(filePath);
  123. const metadata = await original.metadata();
  124. const { width, height } = metadata;
  125. if (!width || !height) {
  126. throw new Error("Could not determine image dimensions");
  127. }
  128. const aspectRatio = width / height;
  129. const srcSet = await Promise.all(
  130. imageSizes.map(async size => {
  131. const sizeNum = parseInt(size.replace("w", ""), 10);
  132. const outputFile = path.join(
  133. fileOutputDir,
  134. `${base}-${sizeNum}${outputExtension}`
  135. );
  136. await original
  137. .clone()
  138. .resize(sizeNum)
  139. .webp({ quality: 80 })
  140. .toFile(outputFile);
  141. return [getCleanPath(outputFile, meta), size];
  142. })
  143. );
  144. const imageRef = getCleanPath(path.join(filePath), meta);
  145. return {
  146. result: srcSet.map(src => src[0]),
  147. detail: { imageRef, srcSet, aspectRatio },
  148. written: true
  149. };
  150. }
  151. export async function generateFavicons(filePath, meta, fileOutputDir) {
  152. // Configuration for favicons package
  153. const configuration = {
  154. path: getCleanPath(fileOutputDir, meta), // Path for overriding default icons path
  155. appName: meta.opts.site?.name || "Website",
  156. appShortName: meta.opts.site?.shortName || "Site",
  157. appDescription: meta.opts.site?.description || "",
  158. developerName: meta.opts.site?.author || "",
  159. developerURL: meta.opts.site?.url || "",
  160. dir: "auto",
  161. lang: meta.opts.site?.language | "en-US",
  162. background: meta.opts.site?.backgroundColor || "#ffffff",
  163. theme_color: meta.opts.site?.themeColor || "#ffffff",
  164. appleStatusBarStyle: "black-translucent",
  165. display: "standalone",
  166. orientation: "any",
  167. scope: "/",
  168. start_url: "/",
  169. version: "1.0",
  170. logging: false,
  171. pixel_art: false,
  172. loadManifestWithCredentials: false,
  173. manifestMaskable: false,
  174. icons: {
  175. android: true,
  176. appleIcon: true,
  177. appleStartup: true,
  178. favicons: true,
  179. windows: true,
  180. yandex: true
  181. }
  182. };
  183. try {
  184. const response = await favicons(filePath, configuration);
  185. // Write all generated images to disk
  186. await Promise.all(
  187. response.images.map(async image => {
  188. const outputPath = path.join(fileOutputDir, image.name);
  189. await fs.writeFile(outputPath, image.contents);
  190. })
  191. );
  192. // Write all generated files (manifests, etc.) to disk
  193. await Promise.all(
  194. response.files.map(async file => {
  195. const outputPath = path.join(fileOutputDir, file.name);
  196. await fs.writeFile(outputPath, file.contents);
  197. })
  198. );
  199. // Combine HTML meta tags
  200. const htmlMeta = response.html.join("\n ");
  201. return {
  202. detail: { htmlMeta },
  203. result: [
  204. ...response.images.map(img =>
  205. getCleanPath(path.join(fileOutputDir, img.name), meta)
  206. ),
  207. ...response.files.map(file =>
  208. getCleanPath(path.join(fileOutputDir, file.name), meta)
  209. )
  210. ],
  211. written: true
  212. };
  213. } catch (error) {
  214. throw new Error(`Failed to generate favicons: ${error.message}`);
  215. }
  216. }