Compare commits

..

No commits in common. "de727b4686f20ef589b34c0a9e069265cf2728d8" and "f301eca9a72953aee5a65e55dfeaaa9c4b1b2516" have entirely different histories.

7 changed files with 65 additions and 91 deletions

10
package-lock.json generated
View File

@ -75,6 +75,7 @@
"quartz": "quartz/bootstrap-cli.mjs" "quartz": "quartz/bootstrap-cli.mjs"
}, },
"devDependencies": { "devDependencies": {
"@types/cli-spinner": "^0.2.3",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
@ -1584,6 +1585,15 @@
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/cli-spinner": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@types/cli-spinner/-/cli-spinner-0.2.3.tgz",
"integrity": "sha512-TMO6mWltW0lCu1de8DMRq9+59OP/tEjghS+rs8ZEQ2EgYP5yV3bGw0tS14TMyJGqFaoVChNvhkVzv9RC1UgX+w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/css-font-loading-module": { "node_modules/@types/css-font-loading-module": {
"version": "0.0.12", "version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",

View File

@ -98,6 +98,7 @@
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {
"@types/cli-spinner": "^0.2.3",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View File

@ -87,7 +87,6 @@ const config: QuartzConfig = {
Plugin.Assets(), Plugin.Assets(),
Plugin.Static(), Plugin.Static(),
Plugin.NotFoundPage(), Plugin.NotFoundPage(),
Plugin.CustomOgImages(),
], ],
}, },
} }

View File

@ -3,6 +3,7 @@ import { i18n } from "../../i18n"
import { unescapeHTML } from "../../util/escape" import { unescapeHTML } from "../../util/escape"
import { FullSlug, getFileExtension } from "../../util/path" import { FullSlug, getFileExtension } from "../../util/path"
import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og" import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og"
import { getFontSpecificationName } from "../../util/theme"
import sharp from "sharp" import sharp from "sharp"
import satori from "satori" import satori from "satori"
import { loadEmoji, getIconCode } from "../../util/emoji" import { loadEmoji, getIconCode } from "../../util/emoji"

View File

@ -4,7 +4,6 @@ import { ProcessedContent } from "../plugins/vfile"
import { QuartzLogger } from "../util/log" import { QuartzLogger } from "../util/log"
import { trace } from "../util/trace" import { trace } from "../util/trace"
import { BuildCtx } from "../util/ctx" import { BuildCtx } from "../util/ctx"
import chalk from "chalk"
export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) { export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
const { argv, cfg } = ctx const { argv, cfg } = ctx
@ -25,18 +24,14 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
emittedFiles++ emittedFiles++
if (ctx.argv.verbose) { if (ctx.argv.verbose) {
console.log(`[emit:${emitter.name}] ${file}`) console.log(`[emit:${emitter.name}] ${file}`)
} else {
log.updateText(`Emitting output files: ${chalk.gray(file)}`)
} }
} }
} else { } else {
// Array case // Array case
emittedFiles += emitted.length emittedFiles += emitted.length
for (const file of emitted) { if (ctx.argv.verbose) {
if (ctx.argv.verbose) { for (const file of emitted) {
console.log(`[emit:${emitter.name}] ${file}`) console.log(`[emit:${emitter.name}] ${file}`)
} else {
log.updateText(`Emitting output files: ${chalk.gray(file)}`)
} }
} }
} }

View File

@ -1,43 +1,26 @@
import readline from "readline" import { Spinner } from "cli-spinner"
export class QuartzLogger { export class QuartzLogger {
verbose: boolean verbose: boolean
private spinnerInterval: NodeJS.Timeout | undefined spinner: Spinner | undefined
private spinnerText: string = ""
private spinnerIndex: number = 0
private readonly spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
constructor(verbose: boolean) { constructor(verbose: boolean) {
this.verbose = verbose this.verbose = verbose
} }
start(text: string) { start(text: string) {
this.spinnerText = text
if (this.verbose) { if (this.verbose) {
console.log(text) console.log(text)
} else { } else {
this.spinnerIndex = 0 this.spinner = new Spinner(`%s ${text}`)
this.spinnerInterval = setInterval(() => { this.spinner.setSpinnerString(18)
readline.clearLine(process.stdout, 0) this.spinner.start()
readline.cursorTo(process.stdout, 0)
process.stdout.write(`${this.spinnerChars[this.spinnerIndex]} ${this.spinnerText}`)
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length
}, 100)
} }
} }
updateText(text: string) {
this.spinnerText = text
}
end(text?: string) { end(text?: string) {
if (!this.verbose && this.spinnerInterval) { if (!this.verbose) {
clearInterval(this.spinnerInterval) this.spinner!.stop(true)
this.spinnerInterval = undefined
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0)
} }
if (text) { if (text) {
console.log(text) console.log(text)
} }

View File

@ -1,11 +1,8 @@
import { promises as fs } from "fs"
import { FontWeight, SatoriOptions } from "satori/wasm" import { FontWeight, SatoriOptions } from "satori/wasm"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { JSXInternal } from "preact/src/jsx" import { JSXInternal } from "preact/src/jsx"
import { FontSpecification, ThemeKey } from "./theme" import { FontSpecification, ThemeKey } from "./theme"
import path from "path"
import { QUARTZ } from "./path"
const defaultHeaderWeight = [700] const defaultHeaderWeight = [700]
const defaultBodyWeight = [400] const defaultBodyWeight = [400]
@ -51,55 +48,48 @@ export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: Fo
return fonts return fonts
} }
// Cache for memoizing font data
const fontCache = new Map<string, Promise<ArrayBuffer>>()
/** /**
* Get the `.ttf` file of a google font * Get the `.ttf` file of a google font
* @param fontName name of google font * @param fontName name of google font
* @param weight what font weight to fetch font * @param weight what font weight to fetch font
* @returns `.ttf` file of google font * @returns `.ttf` file of google font
*/ */
export async function fetchTtf( export async function fetchTtf(fontName: string, weight: FontWeight): Promise<ArrayBuffer> {
fontName: string, const cacheKey = `${fontName}-${weight}`
weight: FontWeight, if (fontCache.has(cacheKey)) {
): Promise<Buffer<ArrayBufferLike>> { return fontCache.get(cacheKey)!
const cacheKey = `${fontName.replaceAll(" ", "-")}-${weight}`
const cacheDir = path.join(QUARTZ, ".quartz-cache", "fonts")
const cachePath = path.join(cacheDir, cacheKey)
// Check if font exists in cache
try {
await fs.access(cachePath)
return fs.readFile(cachePath)
} catch (error) {
// ignore errors and fetch font
} }
// Get css file from google fonts // If not in cache, fetch and store the promise
const cssResponse = await fetch( const fontPromise = (async () => {
`https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`, try {
) // Get css file from google fonts
const css = await cssResponse.text() const cssResponse = await fetch(
`https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`,
)
const css = await cssResponse.text()
// Extract .ttf url from css file // Extract .ttf url from css file
const urlRegex = /url\((https:\/\/fonts.gstatic.com\/s\/.*?.ttf)\)/g const urlRegex = /url\((https:\/\/fonts.gstatic.com\/s\/.*?.ttf)\)/g
const match = urlRegex.exec(css) const match = urlRegex.exec(css)
if (!match) { if (!match) {
throw new Error("Could not fetch font") throw new Error("Could not fetch font")
} }
// fontData is an ArrayBuffer containing the .ttf file data // fontData is an ArrayBuffer containing the .ttf file data (get match[1] due to google fonts response format, always contains link twice, but second entry is the "raw" link)
const fontResponse = await fetch(match[1]) const fontResponse = await fetch(match[1])
const fontData = Buffer.from(await fontResponse.arrayBuffer()) return await fontResponse.arrayBuffer()
} catch (error) {
throw new Error(`Error fetching font: ${error}`)
}
})()
try { fontCache.set(cacheKey, fontPromise)
await fs.mkdir(cacheDir, { recursive: true }) return fontPromise
await fs.writeFile(cachePath, fontData)
} catch (error) {
console.warn(`Failed to cache font: ${error}`)
// Continue even if caching fails
}
return fontData
} }
export type SocialImageOptions = { export type SocialImageOptions = {
@ -171,7 +161,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
title: string, title: string,
description: string, description: string,
fonts: SatoriOptions["fonts"], fonts: SatoriOptions["fonts"],
fileData: QuartzPluginData, _fileData: QuartzPluginData,
) => { ) => {
const fontBreakPoint = 22 const fontBreakPoint = 22
const useSmallerFont = title.length > fontBreakPoint const useSmallerFont = title.length > fontBreakPoint
@ -187,8 +177,8 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
height: "100%", height: "100%",
width: "100%", width: "100%",
backgroundColor: cfg.theme.colors[colorScheme].light, backgroundColor: cfg.theme.colors[colorScheme].light,
gap: "1rem", gap: "2rem",
padding: "3rem 3rem", padding: "1.5rem 5rem",
}} }}
> >
<div <div
@ -197,36 +187,31 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
alignItems: "center", alignItems: "center",
width: "100%", width: "100%",
flexDirection: "row", flexDirection: "row",
gap: "2rem", gap: "2.5rem",
}} }}
> >
<div <img src={iconPath} width={135} height={135} />
style={{
display: "flex",
border: "1px solid red",
}}
>
<img src={iconPath} width={135} height={135} />
</div>
<div <div
style={{ style={{
display: "flex", display: "flex",
color: cfg.theme.colors[colorScheme].dark, color: cfg.theme.colors[colorScheme].dark,
maxWidth: "80%", fontSize: useSmallerFont ? 70 : 82,
fontFamily: fonts[0].name,
maxWidth: "70%",
overflow: "hidden",
textOverflow: "ellipsis",
}} }}
> >
<h1 <p
style={{ style={{
margin: 0, margin: 0,
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
whiteSpace: "nowrap", whiteSpace: "nowrap",
fontSize: useSmallerFont ? 64 : 72,
fontFamily: fonts[0].name,
}} }}
> >
{title} {title}
</h1> </p>
</div> </div>
</div> </div>
<div <div
@ -236,7 +221,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
fontSize: 44, fontSize: 44,
fontFamily: fonts[1].name, fontFamily: fonts[1].name,
maxWidth: "100%", maxWidth: "100%",
maxHeight: "60%", maxHeight: "40%",
overflow: "hidden", overflow: "hidden",
}} }}
> >
@ -245,7 +230,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
margin: 0, margin: 0,
display: "-webkit-box", display: "-webkit-box",
WebkitBoxOrient: "vertical", WebkitBoxOrient: "vertical",
WebkitLineClamp: 5, WebkitLineClamp: 3,
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
}} }}