'use strict'; const node_path = require('node:path'); const posix = require('node:path/posix'); const colorette = require('colorette'); const defu = require('defu'); const fs = require('fs-extra'); const glob = require('glob'); const GrayMatter = require('gray-matter'); const RehypeMeta = require('rehype-meta'); const RehypeParse = require('rehype-parse'); const RehypeStringify = require('rehype-stringify'); const unified = require('unified'); const unistUtilVisit = require('unist-util-visit'); const node_url = require('node:url'); const node_buffer = require('node:buffer'); const promises = require('node:fs/promises'); const node_module = require('node:module'); const resvgWasm = require('@resvg/resvg-wasm'); const regexCreator = require('emoji-regex'); const ora = require('ora'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const fs__default = /*#__PURE__*/_interopDefaultCompat(fs); const GrayMatter__default = /*#__PURE__*/_interopDefaultCompat(GrayMatter); const RehypeMeta__default = /*#__PURE__*/_interopDefaultCompat(RehypeMeta); const RehypeParse__default = /*#__PURE__*/_interopDefaultCompat(RehypeParse); const RehypeStringify__default = /*#__PURE__*/_interopDefaultCompat(RehypeStringify); const regexCreator__default = /*#__PURE__*/_interopDefaultCompat(regexCreator); const ora__default = /*#__PURE__*/_interopDefaultCompat(ora); const logModulePrefix = `${colorette.cyan(`@nolebase/vitepress-plugin-og-image`)}${colorette.gray(":")}`; async function tryToLocateTemplateSVGFile(siteConfig, configTemplateSvgPath) { if (configTemplateSvgPath != null) return node_path.resolve(siteConfig.srcDir, configTemplateSvgPath); const templateSvgPathUnderPublicDir = node_path.resolve(siteConfig.srcDir, "public", "og-template.svg"); if (await fs__default.pathExists(templateSvgPathUnderPublicDir)) return templateSvgPathUnderPublicDir; const __dirname = node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('vitepress/index.cjs', document.baseURI).href)))); const templateSvgPathUnderRootDir = node_path.resolve(__dirname, "assets", "og-template.svg"); if (await fs__default.pathExists(templateSvgPathUnderRootDir)) return templateSvgPathUnderRootDir; } async function tryToLocateFontFile(siteConfig) { const fontPathUnderPublicDir = node_path.resolve(siteConfig.srcDir, "public", "SourceHanSansSC.otf"); if (await fs__default.pathExists(fontPathUnderPublicDir)) return fontPathUnderPublicDir; const __dirname = node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('vitepress/index.cjs', document.baseURI).href)))); const fontPathUnderRootDir = node_path.resolve(__dirname, "assets", "SourceHanSansSC.otf"); if (await fs__default.pathExists(fontPathUnderRootDir)) return fontPathUnderRootDir; } async function applyCategoryText(pageItem, categoryOptions) { if (typeof categoryOptions?.byCustomGetter !== "undefined") { const gotTextMaybePromise = categoryOptions.byCustomGetter({ ...pageItem }); if (typeof gotTextMaybePromise !== "undefined") { if (gotTextMaybePromise instanceof Promise) return await gotTextMaybePromise; if (gotTextMaybePromise) return gotTextMaybePromise; } } if (typeof categoryOptions?.byPathPrefix !== "undefined") { for (const { prefix, text } of categoryOptions.byPathPrefix) { if (pageItem.normalizedSourceFilePath.startsWith(prefix)) { if (!text) { console.warn( `${logModulePrefix} ${colorette.yellow("[WARN]")} empty text for prefix ${prefix} when processing ${pageItem.sourceFilePath} with categoryOptions.byPathPrefix, will ignore...` ); return; } return text; } if (pageItem.normalizedSourceFilePath.startsWith(`/${prefix}`)) { if (!text) { console.warn( `${logModulePrefix} ${colorette.yellow("[WARN]")} empty text for prefix ${prefix} when processing ${pageItem.sourceFilePath} with categoryOptions.byPathPrefix, will ignore...` ); return; } return text; } } console.warn( `${logModulePrefix} ${colorette.yellow("[WARN]")} no path prefix matched for ${pageItem.sourceFilePath} with categoryOptions.byPathPrefix, will ignore...` ); return; } if (typeof categoryOptions?.byLevel !== "undefined") { const level = Number.parseInt(String(categoryOptions?.byLevel ?? 0)); if (Number.isNaN(level)) { console.warn( `${logModulePrefix} ${colorette.yellow("[ERROR]")} byLevel must be a number, but got ${categoryOptions.byLevel} instead when processing ${pageItem.sourceFilePath} with categoryOptions.byLevel, will ignore...` ); return; } const dirs = pageItem.sourceFilePath.split(node_path.sep); if (dirs.length > level) return dirs[level]; console.warn(`${logModulePrefix} ${colorette.red(`[ERROR] byLevel is out of range for ${pageItem.sourceFilePath} with categoryOptions.byLevel.`)} will ignore...`); } } async function applyCategoryTextWithFallback(pageItem, categoryOptions) { const customText = await applyCategoryText(pageItem, categoryOptions); if (customText) return customText; const fallbackWithFrontmatter = typeof categoryOptions?.fallbackWithFrontmatter === "undefined" ? true : categoryOptions.fallbackWithFrontmatter; if (fallbackWithFrontmatter && "category" in pageItem.frontmatter && pageItem.frontmatter.category && typeof pageItem.frontmatter.category === "string") { return pageItem.frontmatter.category ?? ""; } console.warn(`${logModulePrefix} ${colorette.yellow("[WARN]")} no category text found for ${pageItem.sourceFilePath} with categoryOptions ${JSON.stringify(categoryOptions)}.}`); return "Un-categorized"; } const emojiRegex = regexCreator__default(); function removeEmoji(str) { return str.replace(emojiRegex, ""); } const escapeMap = { "<": "<", ">": ">", "'": "'", '"': """, "&": "&" }; function escape(content, ignore) { ignore = (ignore || "").replace(/[^&"<>']/g, ""); const pattern = `([&"<>'])`.replace(new RegExp(`[${ignore}]`, "g"), ""); return content.replace(new RegExp(pattern, "g"), (_, item) => { return escapeMap[item]; }); } const imageBuffers = /* @__PURE__ */ new Map(); function templateSVG(siteName, siteDescription, title, category, ogTemplate, maxCharactersPerLine) { maxCharactersPerLine ?? (maxCharactersPerLine = 17); const lines = removeEmoji(title).trim().replaceAll("\r\n", "\n").split("\n").map((line) => line.trim()); for (let i = 0; i < lines.length; i++) { const val = lines[i].trim(); if (val.length > maxCharactersPerLine) { let breakPoint = val.lastIndexOf(" ", maxCharactersPerLine); if (breakPoint < 0) { for (let j = Math.min(val.length - 1, maxCharactersPerLine); j > 0; j--) { if (val[j] === val[j].toUpperCase()) { breakPoint = j; break; } } } if (breakPoint < 0) breakPoint = maxCharactersPerLine; lines[i] = val.slice(0, breakPoint); lines[i + 1] = `${val.slice(lines[i].length)}${lines[i + 1] || ""}`; } lines[i] = lines[i].trim(); } const categoryStr = category ? removeEmoji(category).trim() : ""; const data = { siteName, siteDescription, category: categoryStr, line1: lines[0] || "", line2: lines[1] || "", line3: `${lines[2] || ""}${lines[3] ? "..." : ""}` }; return ogTemplate.replace(/\{\{([^}]+)\}\}/g, (_, name) => { if (!name || typeof name !== "string" || !(name in data)) return ""; const nameKeyOf = name; return escape(data[nameKeyOf]); }); } let resvgInit = false; async function initSVGRenderer() { try { if (!resvgInit) { const wasm = promises.readFile(node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('vitepress/index.cjs', document.baseURI).href))).resolve("@resvg/resvg-wasm/index_bg.wasm")); await resvgWasm.initWasm(wasm); resvgInit = true; } } catch (err) { throw new Error(`Failed to init resvg wasm due to ${err}`); } } let fontBuffer; async function initFontBuffer(options) { if (!options?.fontPath) return; if (fontBuffer) return fontBuffer; try { fontBuffer = await promises.readFile(options.fontPath); } catch (err) { throw new Error(`Failed to read font file due to ${err}`); } return fontBuffer; } async function renderSVG(svgContent, fontBuffer2, imageUrlResolver, additionalFontBuffers, resultImageWidth) { try { const resvg = new resvgWasm.Resvg( svgContent, { fitTo: { mode: "width", value: resultImageWidth ?? 1200 }, font: { fontBuffers: fontBuffer2 ? [fontBuffer2, ...additionalFontBuffers ?? []] : additionalFontBuffers ?? [], // Load system fonts might cost more time loadSystemFonts: false } } ); try { const resolvedImages = await Promise.all( resvg.imagesToResolve().map(async (url) => { return { url, buffer: await resolveImageUrlWithCache(url, imageUrlResolver) }; }) ); for (const { url, buffer } of resolvedImages) resvg.resolveImage(url, buffer); const res = resvg.render(); return { png: res.asPng(), width: res.width, height: res.height }; } catch (err) { throw new Error(`Failed to render open graph images on path due to ${err}`); } } catch (err) { throw new Error(`Failed to initiate Resvg instance to render open graph images due to ${err}`); } } function resolveImageUrlWithCache(url, imageUrlResolver) { if (imageBuffers.has(url)) return imageBuffers.get(url); const result = resolveImageUrl(url, imageUrlResolver); imageBuffers.set(url, result); return result; } async function resolveImageUrl(url, imageUrlResolver) { if (imageUrlResolver != null) { const res2 = await imageUrlResolver(url); if (res2 != null) return res2; } const res = await fetch(url); const buffer = await res.arrayBuffer(); return node_buffer.Buffer.from(buffer); } const okMark = colorette.green("\u2713"); const failMark = colorette.red("\u2716"); async function task(taskName, task2) { const startsAt = Date.now(); const moduleNamePrefix = colorette.cyan("@nolebase/vitepress-plugin-og-image"); const grayPrefix = colorette.gray(":"); const spinnerPrefix = `${moduleNamePrefix}${grayPrefix}`; const spinner = ora__default({ discardStdin: false }); spinner.start(`${spinnerPrefix} ${taskName}...`); let result; try { result = await task2(); } catch (e) { spinner.stopAndPersist({ symbol: failMark }); throw e; } const elapsed = Date.now() - startsAt; const suffixText = `${colorette.gray(`(${elapsed}ms)`)} ${result}` ?? ""; spinner.stopAndPersist({ symbol: okMark, suffixText }); } function renderTaskResultsSummary(results, siteConfig) { const successCount = results.filter((item) => item.status === "success"); const skippedCount = results.filter((item) => item.status === "skipped"); const erroredCount = results.filter((item) => item.status === "errored"); const stats = `${colorette.green(`${successCount.length} generated`)}, ${colorette.yellow(`${skippedCount.length} skipped`)}, ${colorette.red(`${erroredCount.length} errored`)}`; const skippedList = ` - ${colorette.yellow("Following files were skipped")}: ${skippedCount.map((item) => { return colorette.gray(` - ${node_path.relative(siteConfig.root, item.filePath)}: ${item.reason}`); }).join("\n")}`; const erroredList = ` - ${colorette.red("Following files encountered errors")} ${erroredCount.map((item) => { return colorette.gray(` - ${node_path.relative(siteConfig.root, item.filePath)}: ${item.reason}`); }).join("\n")}`; const overallResults = [stats]; if (skippedCount.length > 0) overallResults.push(skippedList); if (erroredCount.length > 0) overallResults.push(erroredList); return overallResults.join("\n\n"); } function getLocales(siteData) { const locales = []; locales.push(siteData.lang ?? "root"); if (Object.keys(siteData.locales).length === 0) return locales; for (const locale in siteData.locales) { if (locale !== siteData.lang) locales.push(locale); } return locales; } function getTitleWithLocales(siteData, locale) { if (Object.keys(siteData.locales).length > 0) { const title = siteData.locales[locale]?.title; if (title) return title; if (siteData.locales.root.title) return siteData.locales.root.title; return siteData.title; } return siteData.title; } function getDescriptionWithLocales(siteData, locale) { if (Object.keys(siteData.locales).length > 0) { const description = siteData.locales[locale]?.description; if (description) return description; if (siteData.locales.root.description) return siteData.locales.root.description; return siteData.description; } return siteData.description; } function getSidebar(siteData, themeConfig) { const locales = getLocales(siteData); if (locales.length === 0) { return { defaultLocale: siteData.lang, locales: locales || [], sidebar: { [siteData.lang]: flattenThemeConfigSidebar(themeConfig.sidebar) || [] } }; } const sidebar = { defaultLocale: siteData.lang, locales, sidebar: {} }; for (const locale of locales) { let themeConfigSidebar = []; if (typeof siteData.locales[locale]?.themeConfig?.sidebar !== "undefined") themeConfigSidebar = siteData.locales[locale]?.themeConfig?.sidebar || []; else if (typeof siteData.themeConfig?.sidebar !== "undefined") themeConfigSidebar = siteData.themeConfig?.sidebar || []; else if (typeof themeConfig.sidebar !== "undefined") themeConfigSidebar = themeConfig.sidebar; else themeConfigSidebar = []; sidebar.sidebar[locale] = flattenThemeConfigSidebar(themeConfigSidebar) || []; } return sidebar; } function flattenThemeConfigSidebar(sidebar) { if (!sidebar) return []; if (Array.isArray(sidebar)) return sidebar; return Object.keys(sidebar).reduce((prev, curr) => { const items = sidebar[curr]; return prev.concat(items); }, []); } function flattenSidebar(sidebar, base) { return sidebar.reduce((prev, curr) => { if (curr.items) { return prev.concat( flattenSidebar( curr.items.map((item) => addBaseToItem(item, curr.base ?? base)), curr.base ?? base ).concat( curr.link == null ? [] : [{ ...curr, items: void 0, link: curr.link != null ? (curr.base ?? "") + curr.link : curr.link }] ) ); } return prev.concat(curr); }, []); } function addBaseToItem(item, base) { if (base == null || base === "") return item; return { ...item, link: item.link != null ? base + item.link : item.link }; } async function renderSVGAndRewriteHTML(siteConfig, siteTitle, siteDescription, page, file, ogImageTemplateSvg, ogImageTemplateSvgPath, domain, imageUrlResolver, additionalFontBuffers, resultImageWidth, maxCharactersPerLine, overrideExistingMetaTags) { const fileName = node_path.basename(file, ".html"); const ogImageFilePathBaseName = `og-${fileName}.png`; const ogImageFilePathFullName = `${node_path.dirname(file)}/${ogImageFilePathBaseName}`; const html = await fs__default.readFile(file, "utf-8"); const parsedHtml = unified.unified().use(RehypeParse__default, { fragment: true }).parse(html); let hasOgImage = false; unistUtilVisit.visit(parsedHtml, "element", (node) => { if (node.tagName === "meta" && (node.properties?.name === "og:image" || node.properties?.name === "twitter:image")) hasOgImage = node.properties.name; else return true; }); if (hasOgImage && !overrideExistingMetaTags) { return { filePath: file, status: "skipped", reason: `already has ${hasOgImage} meta tag` }; } const templatedOgImageSvg = templateSVG( siteTitle, siteDescription, page.title, page.category ?? "", ogImageTemplateSvg, maxCharactersPerLine ); let width; let height; try { const res = await renderSVGAndSavePNG( templatedOgImageSvg, ogImageFilePathFullName, ogImageTemplateSvgPath, node_path.relative(siteConfig.srcDir, file), { fontPath: await tryToLocateFontFile(siteConfig), imageUrlResolver, additionalFontBuffers, resultImageWidth } ); width = res.width; height = res.height; } catch (err) { return { filePath: file, status: "errored", reason: String(err) }; } const result = await unified.unified().use(RehypeParse__default).use(RehypeMeta__default, { og: true, twitter: true, image: { url: `${domain}/${node_path.relative(siteConfig.outDir, ogImageFilePathFullName).split(node_path.sep).map((item) => encodeURIComponent(item)).join("/")}`, width, height } }).use(RehypeStringify__default).process(html); try { await fs__default.writeFile(file, String(result), "utf-8"); } catch (err) { console.error( `${logModulePrefix} `, `${colorette.red("[ERROR] \u2717")} failed to write transformed HTML on path [${node_path.relative(siteConfig.srcDir, file)}] due to ${err}`, ` ${colorette.red(err.message)} ${colorette.gray(String(err.stack))}` ); return { filePath: file, status: "errored", reason: String(err) }; } return { filePath: file, status: "success" }; } async function renderSVGAndSavePNG(svgContent, saveAs, forSvgSource, forFile, options) { try { const { png: pngBuffer, width, height } = await renderSVG(svgContent, await initFontBuffer(options), options.imageUrlResolver, options.additionalFontBuffers, options.resultImageWidth); try { await fs__default.writeFile(saveAs, pngBuffer, "binary"); } catch (err) { console.error( `${logModulePrefix} `, `${colorette.red("[ERROR] \u2717")} open graph image rendered successfully, but failed to write generated open graph image on path [${saveAs}] due to ${err}`, ` ${colorette.red(err.message)} ${colorette.gray(String(err.stack))}` ); throw err; } return { width, height }; } catch (err) { console.error( `${logModulePrefix} `, `${colorette.red("[ERROR] \u2717")} failed to generate open graph image as ${colorette.green(`[${saveAs}]`)} with ${colorette.green(`[${forSvgSource}]`)} due to ${colorette.red(String(err))}`, `skipped open graph image generation for ${colorette.green(`[${forFile}]`)}`, ` SVG Content: ${svgContent}`, ` Detailed stack information bellow: ${colorette.red(err.message)} ${colorette.gray(String(err.stack))}` ); throw err; } } function buildEndGenerateOpenGraphImages(options) { options = defu.defu(options, { resultImageWidth: 1200, maxCharactersPerLine: 17, overrideExistingMetaTags: true }); return async (siteConfig) => { await initSVGRenderer(); const ogImageTemplateSvgPath = await tryToLocateTemplateSVGFile(siteConfig, options.templateSvgPath); await task("rendering open graph images", async () => { const themeConfig = siteConfig.site.themeConfig; const sidebar = getSidebar(siteConfig.site, themeConfig); let pages = []; for (const locale of sidebar.locales) { const flattenedSidebar = flattenSidebar(sidebar.sidebar[locale]); const items = []; for (const item of flattenedSidebar) { const relativeLink = item.link ?? ""; const sourceFilePath = relativeLink.endsWith("/") ? `${relativeLink}index.md` : relativeLink.endsWith(".md") ? relativeLink : `${relativeLink}.md`; const sourceFileContent = fs__default.readFileSync(`${node_path.join(siteConfig.srcDir, sourceFilePath)}`, "utf-8"); const { data } = GrayMatter__default(sourceFileContent); const res = { ...item, title: item.text ?? item.title ?? "Untitled", category: "", locale, frontmatter: data, sourceFilePath, normalizedSourceFilePath: sourceFilePath.split(node_path.sep).join(posix.sep) }; res.category = await applyCategoryTextWithFallback(res, options.category); items.push(res); } pages = pages.concat(items); } const files = await glob.glob(`${siteConfig.outDir}/**/*.html`, { nodir: true }); if (!ogImageTemplateSvgPath) { return `${colorette.green(`${0} generated`)}, ${colorette.yellow(`${files.length} (all) skipped`)}, ${colorette.red(`${0} errored`)}. - ${colorette.red("Failed to locate")} og-template.svg ${colorette.red("under public or plugin directory")}, did you forget to put it? will skip open graph image generation.`; } const ogImageTemplateSvg = fs__default.readFileSync(ogImageTemplateSvgPath, "utf-8"); const generatedForFiles = await Promise.all(files.map(async (file) => { const relativePath = node_path.relative(siteConfig.outDir, file); const link = `/${relativePath.slice(0, relativePath.lastIndexOf(".")).replaceAll(node_path.sep, "/")}`.split("/index")[0]; const page = pages.find((item) => { let itemLink = item.link; if (itemLink?.endsWith(".md")) itemLink = itemLink.slice(0, -".md".length); if (itemLink === link) return true; if (itemLink === `${link}/`) return true; return false; }); if (!page) { return { filePath: file, status: "skipped", reason: "correspond Markdown page not found in sidebar" }; } const siteTitle = getTitleWithLocales(siteConfig.site, page.locale); const siteDescription = getDescriptionWithLocales(siteConfig.site, page.locale); return await renderSVGAndRewriteHTML( siteConfig, siteTitle, siteDescription, page, file, ogImageTemplateSvg, ogImageTemplateSvgPath, options.baseUrl, options.svgImageUrlResolver, options.svgFontBuffers, options.resultImageWidth, options.maxCharactersPerLine, options.overrideExistingMetaTags ); })); return renderTaskResultsSummary(generatedForFiles, siteConfig); }); }; } exports.buildEndGenerateOpenGraphImages = buildEndGenerateOpenGraphImages; //# sourceMappingURL=index.cjs.map