incremental all the things

This commit is contained in:
Jacky Zhao 2025-03-15 22:00:37 -07:00
parent f528d6139e
commit 7681a86815
24 changed files with 377 additions and 242 deletions

View File

@ -1,7 +1,5 @@
---
title: Authoring Content
aliases:
- test/author 1
---
All of the content in your Quartz should go in the `/content` folder. The content for the home page of your Quartz lives in `content/index.md`. If you've [[index#🪴 Get Started|setup Quartz]] already, this folder should already be initialized. Any Markdown in this folder will get processed by Quartz.

View File

@ -9,7 +9,7 @@ import { parseMarkdown } from "./processors/parse"
import { filterContent } from "./processors/filter"
import { emitContent } from "./processors/emit"
import cfg from "../quartz.config"
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path"
import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
import chokidar from "chokidar"
import { ProcessedContent } from "./plugins/vfile"
import { Argv, BuildCtx } from "./util/ctx"
@ -173,7 +173,6 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
const perf = new PerfTimer()
perf.addEvent("rebuild")
console.log(chalk.yellow("Detected change, rebuilding..."))
console.log(changes)
// update changesSinceLastBuild
for (const change of changes) {
@ -181,21 +180,17 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
}
const staticResources = getStaticResourcesFromPlugins(ctx)
const processedFiles = await Promise.all(
Object.entries(changesSinceLastBuild)
.filter(([fp, type]) => type !== "delete" && path.extname(fp) === ".md")
.map(async ([fp, _type]) => {
for (const [fp, type] of Object.entries(changesSinceLastBuild)) {
if (type === "delete" || path.extname(fp) !== ".md") continue
const fullPath = joinSegments(argv.directory, toPosixPath(fp)) as FilePath
const parsed = await parseMarkdown(ctx, [fullPath])
parsed.forEach((content) =>
for (const content of parsed) {
contentMap.set(content[1].data.relativePath!, {
type: "markdown",
content,
}),
)
return parsed
}),
).then((results) => results.flat())
})
}
}
// update state using changesSinceLastBuild
// we do this weird play of add => compute change events => remove
@ -236,16 +231,19 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
// update allFiles and then allSlugs with the consistent view of content map
ctx.allFiles = Array.from(contentMap.keys())
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
const processedFiles = Array.from(contentMap.values())
.filter((file) => file.type === "markdown")
.map((file) => file.content)
let emittedFiles = 0
for (const emitter of cfg.plugins.emitters) {
// Try to use partialEmit if available, otherwise assume the output is static
const emitFn = emitter.partialEmit
if (!emitFn) {
const emitFn = emitter.partialEmit ?? emitter.emit
const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents)
if (emitted === null) {
continue
}
const emitted = await emitFn(ctx, processedFiles, staticResources, changeEvents)
if (Symbol.asyncIterator in emitted) {
// Async generator case
for await (const file of emitted) {

View File

@ -2,7 +2,6 @@ import { ValidDateType } from "./components/Date"
import { QuartzComponent } from "./components/types"
import { ValidLocale } from "./i18n"
import { PluginTypes } from "./plugins/types"
import { SocialImageOptions } from "./util/og"
import { Theme } from "./util/theme"
export type Analytics =

View File

@ -456,6 +456,7 @@ export async function handleBuild(argv) {
const paths = await globby([
"**/*.ts",
"quartz/cli/*.js",
"quartz/static/**/*",
"**/*.tsx",
"**/*.scss",
"package.json",

View File

@ -9,7 +9,6 @@ import { visit } from "unist-util-visit"
import { Root, Element, ElementContent } from "hast"
import { GlobalConfiguration } from "../cfg"
import { i18n } from "../i18n"
import { QuartzPluginData } from "../plugins/vfile"
interface RenderComponents {
head: QuartzComponent
@ -25,7 +24,6 @@ interface RenderComponents {
const headerRegex = new RegExp(/h[1-6]/)
export function pageResources(
baseDir: FullSlug | RelativeURL,
fileData: QuartzPluginData,
staticResources: StaticResources,
): StaticResources {
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")

View File

@ -10,7 +10,7 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
}
document.addEventListener("nav", () => {
const switchTheme = (e: Event) => {
const switchTheme = () => {
const newTheme =
document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark"
document.documentElement.setAttribute("saved-theme", newTheme)

View File

@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types"
import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage"
import { FullPageLayout } from "../../cfg"
import { FilePath, FullSlug } from "../../util/path"
import { FullSlug } from "../../util/path"
import { sharedPageComponents } from "../../../quartz.layout"
import { NotFound } from "../../components"
import { defaultProcessedContent } from "../vfile"
@ -40,7 +40,7 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
description: notFound,
frontmatter: { title: notFound, tags: [] },
})
const externalResources = pageResources(path, vfile.data, resources)
const externalResources = pageResources(path, resources)
const componentData: QuartzComponentProps = {
ctx,
fileData: vfile.data,
@ -58,5 +58,6 @@ export const NotFoundPage: QuartzEmitterPlugin = () => {
ext: ".html",
})
},
async *partialEmit() {},
}
}

View File

@ -4,7 +4,7 @@ import { write } from "./helpers"
import { BuildCtx } from "../../util/ctx"
import { VFile } from "vfile"
async function *processFile(ctx: BuildCtx, file: VFile) {
async function* processFile(ctx: BuildCtx, file: VFile) {
const ogSlug = simplifySlug(file.data.slug!)
for (const slug of file.data.aliases ?? []) {

View File

@ -41,12 +41,12 @@ export const Assets: QuartzEmitterPlugin = () => {
if (changeEvent.type === "add" || changeEvent.type === "change") {
yield copyFile(ctx.argv, changeEvent.path)
} else if (changeEvent.type === 'delete') {
} else if (changeEvent.type === "delete") {
const name = slugifyFilePath(changeEvent.path)
const dest = joinSegments(ctx.argv.output, name) as FilePath
await fs.promises.unlink(dest)
}
}
}
},
}
}

View File

@ -23,4 +23,5 @@ export const CNAME: QuartzEmitterPlugin = () => ({
await fs.promises.writeFile(path, content)
return [path] as FilePath[]
},
async *partialEmit() {},
})

View File

@ -1,4 +1,4 @@
import { FilePath, FullSlug, joinSegments } from "../../util/path"
import { FullSlug, joinSegments } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
// @ts-ignore
@ -293,5 +293,6 @@ export const ComponentResources: QuartzEmitterPlugin = () => {
content: postscript,
})
},
async *partialEmit() {},
}
}

View File

@ -1,21 +1,50 @@
import path from "path"
import { visit } from "unist-util-visit"
import { Root } from "hast"
import { VFile } from "vfile"
import { QuartzEmitterPlugin } from "../types"
import { QuartzComponentProps } from "../../components/types"
import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage"
import { FullPageLayout } from "../../cfg"
import { Argv } from "../../util/ctx"
import { FilePath, isRelativeURL, joinSegments, pathToRoot } from "../../util/path"
import { pathToRoot } from "../../util/path"
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { Content } from "../../components"
import chalk from "chalk"
import { write } from "./helpers"
import { BuildCtx } from "../../util/ctx"
import { Node } from "unist"
import { StaticResources } from "../../util/resources"
import { QuartzPluginData } from "../vfile"
async function processContent(
ctx: BuildCtx,
tree: Node,
fileData: QuartzPluginData,
allFiles: QuartzPluginData[],
opts: FullPageLayout,
resources: StaticResources,
) {
const slug = fileData.slug!
const cfg = ctx.cfg.configuration
const externalResources = pageResources(pathToRoot(slug), resources)
const componentData: QuartzComponentProps = {
ctx,
fileData,
externalResources,
cfg,
children: [],
tree,
allFiles,
}
const content = renderPage(cfg, slug, componentData, opts, externalResources)
return write({
ctx,
content,
slug,
ext: ".html",
})
}
// TODO check for transclusions in partial rebuild
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
const opts: FullPageLayout = {
...sharedPageComponents,
@ -45,38 +74,18 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
]
},
async *emit(ctx, content, resources) {
const cfg = ctx.cfg.configuration
const allFiles = content.map((c) => c[1].data)
let containsIndex = false
for (const [tree, file] of content) {
const slug = file.data.slug!
if (slug === "index") {
containsIndex = true
}
if (file.data.slug?.endsWith("/index")) {
continue
}
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
const componentData: QuartzComponentProps = {
ctx,
fileData: file.data,
externalResources,
cfg,
children: [],
tree,
allFiles,
}
const content = renderPage(cfg, slug, componentData, opts, externalResources)
yield write({
ctx,
content,
slug,
ext: ".html",
})
// only process home page, non-tag pages, and non-index pages
if (slug.endsWith("/index") || slug.startsWith("tags/")) continue
yield processContent(ctx, tree, file.data, allFiles, opts, resources)
}
if (!containsIndex) {
@ -87,5 +96,25 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
)
}
},
async *partialEmit(ctx, content, resources, changeEvents) {
const allFiles = content.map((c) => c[1].data)
// find all slugs that changed or were added
const changedSlugs = new Set<string>()
for (const changeEvent of changeEvents) {
if (!changeEvent.file) continue
if (changeEvent.type === "add" || changeEvent.type === "change") {
changedSlugs.add(changeEvent.file.data.slug!)
}
}
for (const [tree, file] of content) {
const slug = file.data.slug!
if (!changedSlugs.has(slug)) continue
if (slug.endsWith("/index") || slug.startsWith("tags/")) continue
yield processContent(ctx, tree, file.data, allFiles, opts, resources)
}
},
}
}

View File

@ -7,7 +7,6 @@ import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../
import { FullPageLayout } from "../../cfg"
import path from "path"
import {
FilePath,
FullSlug,
SimpleSlug,
stripSlashes,
@ -18,12 +17,89 @@ import {
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { FolderContent } from "../../components"
import { write } from "./helpers"
import { i18n } from "../../i18n"
import { i18n, TRANSLATIONS } from "../../i18n"
import { BuildCtx } from "../../util/ctx"
import { StaticResources } from "../../util/resources"
interface FolderPageOptions extends FullPageLayout {
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
}
async function* processFolderInfo(
ctx: BuildCtx,
folderInfo: Record<SimpleSlug, ProcessedContent>,
allFiles: QuartzPluginData[],
opts: FullPageLayout,
resources: StaticResources,
) {
for (const [folder, folderContent] of Object.entries(folderInfo) as [
SimpleSlug,
ProcessedContent,
][]) {
const slug = joinSegments(folder, "index") as FullSlug
const [tree, file] = folderContent
const cfg = ctx.cfg.configuration
const externalResources = pageResources(pathToRoot(slug), resources)
const componentData: QuartzComponentProps = {
ctx,
fileData: file.data,
externalResources,
cfg,
children: [],
tree,
allFiles,
}
const content = renderPage(cfg, slug, componentData, opts, externalResources)
yield write({
ctx,
content,
slug,
ext: ".html",
})
}
}
function computeFolderInfo(
folders: Set<SimpleSlug>,
content: ProcessedContent[],
locale: keyof typeof TRANSLATIONS,
): Record<SimpleSlug, ProcessedContent> {
// Create default folder descriptions
const folderInfo: Record<SimpleSlug, ProcessedContent> = Object.fromEntries(
[...folders].map((folder) => [
folder,
defaultProcessedContent({
slug: joinSegments(folder, "index") as FullSlug,
frontmatter: {
title: `${i18n(locale).pages.folderContent.folder}: ${folder}`,
tags: [],
},
}),
]),
)
// Update with actual content if available
for (const [tree, file] of content) {
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
if (folders.has(slug)) {
folderInfo[slug] = [tree, file]
}
}
return folderInfo
}
function _getFolders(slug: FullSlug): SimpleSlug[] {
var folderName = path.dirname(slug ?? "") as SimpleSlug
const parentFolderNames = [folderName]
while (folderName !== ".") {
folderName = path.dirname(folderName ?? "") as SimpleSlug
parentFolderNames.push(folderName)
}
return parentFolderNames
}
export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (userOpts) => {
const opts: FullPageLayout = {
...sharedPageComponents,
@ -66,59 +142,29 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
}),
)
const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
[...folders].map((folder) => [
folder,
defaultProcessedContent({
slug: joinSegments(folder, "index") as FullSlug,
frontmatter: {
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`,
tags: [],
const folderInfo = computeFolderInfo(folders, content, cfg.locale)
yield* processFolderInfo(ctx, folderInfo, allFiles, opts, resources)
},
}),
]),
async *partialEmit(ctx, content, resources, changeEvents) {
const allFiles = content.map((c) => c[1].data)
const cfg = ctx.cfg.configuration
// Find all folders that need to be updated based on changed files
const affectedFolders: Set<SimpleSlug> = new Set()
for (const changeEvent of changeEvents) {
if (!changeEvent.file) continue
const slug = changeEvent.file.data.slug!
const folders = _getFolders(slug).filter(
(folderName) => folderName !== "." && folderName !== "tags",
)
for (const [tree, file] of content) {
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
if (folders.has(slug)) {
folderDescriptions[slug] = [tree, file]
}
folders.forEach((folder) => affectedFolders.add(folder))
}
for (const folder of folders) {
const slug = joinSegments(folder, "index") as FullSlug
const [tree, file] = folderDescriptions[folder]
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
const componentData: QuartzComponentProps = {
ctx,
fileData: file.data,
externalResources,
cfg,
children: [],
tree,
allFiles,
}
const content = renderPage(cfg, slug, componentData, opts, externalResources)
yield write({
ctx,
content,
slug,
ext: ".html",
})
// If there are affected folders, rebuild their pages
if (affectedFolders.size > 0) {
const folderInfo = computeFolderInfo(affectedFolders, content, cfg.locale)
yield* processFolderInfo(ctx, folderInfo, allFiles, opts, resources)
}
},
}
}
function _getFolders(slug: FullSlug): SimpleSlug[] {
var folderName = path.dirname(slug ?? "") as SimpleSlug
const parentFolderNames = [folderName]
while (folderName !== ".") {
folderName = path.dirname(folderName ?? "") as SimpleSlug
parentFolderNames.push(folderName)
}
return parentFolderNames
}

View File

@ -11,8 +11,6 @@ type WriteOptions = {
content: string | Buffer | Readable
}
type DeleteOptions = Omit<WriteOptions, "content">
export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise<FilePath> => {
const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath
const dir = path.dirname(pathToPage)

View File

@ -4,10 +4,12 @@ import { unescapeHTML } from "../../util/escape"
import { FullSlug, getFileExtension } from "../../util/path"
import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og"
import sharp from "sharp"
import satori from "satori"
import satori, { SatoriOptions } from "satori"
import { loadEmoji, getIconCode } from "../../util/emoji"
import { Readable } from "stream"
import { write } from "./helpers"
import { BuildCtx } from "../../util/ctx"
import { QuartzPluginData } from "../vfile"
const defaultOptions: SocialImageOptions = {
colorScheme: "lightMode",
@ -42,6 +44,41 @@ async function generateSocialImage(
return sharp(Buffer.from(svg)).webp({ quality: 40 })
}
async function processOgImage(
ctx: BuildCtx,
fileData: QuartzPluginData,
fonts: SatoriOptions["fonts"],
fullOptions: SocialImageOptions,
) {
const cfg = ctx.cfg.configuration
const slug = fileData.slug!
const titleSuffix = cfg.pageTitleSuffix ?? ""
const title =
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
const description =
fileData.frontmatter?.socialDescription ??
fileData.frontmatter?.description ??
unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description)
const stream = await generateSocialImage(
{
title,
description,
fonts,
cfg,
fileData,
},
fullOptions,
)
return write({
ctx,
content: stream,
slug: `${slug}-og-image` as FullSlug,
ext: ".webp",
})
}
export const CustomOgImagesEmitterName = "CustomOgImages"
export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> = (userOpts) => {
const fullOptions = { ...defaultOptions, ...userOpts }
@ -58,39 +95,23 @@ export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> =
const fonts = await getSatoriFonts(headerFont, bodyFont)
for (const [_tree, vfile] of content) {
// if this file defines socialImage, we can skip
if (vfile.data.frontmatter?.socialImage !== undefined) {
continue
if (vfile.data.frontmatter?.socialImage !== undefined) continue
yield processOgImage(ctx, vfile.data, fonts, fullOptions)
}
const slug = vfile.data.slug!
const titleSuffix = cfg.pageTitleSuffix ?? ""
const title =
(vfile.data.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
const description =
vfile.data.frontmatter?.socialDescription ??
vfile.data.frontmatter?.description ??
unescapeHTML(
vfile.data.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description,
)
const stream = await generateSocialImage(
{
title,
description,
fonts,
cfg,
fileData: vfile.data,
},
fullOptions,
)
async *partialEmit(ctx, _content, _resources, changeEvents) {
const cfg = ctx.cfg.configuration
const headerFont = cfg.theme.typography.header
const bodyFont = cfg.theme.typography.body
const fonts = await getSatoriFonts(headerFont, bodyFont)
yield write({
ctx,
content: stream,
slug: `${slug}-og-image` as FullSlug,
ext: ".webp",
})
// find all slugs that changed or were added
for (const changeEvent of changeEvents) {
if (!changeEvent.file) continue
if (changeEvent.file.data.frontmatter?.socialImage !== undefined) continue
if (changeEvent.type === "add" || changeEvent.type === "change") {
yield processOgImage(ctx, changeEvent.file.data, fonts, fullOptions)
}
}
},
externalResources: (ctx) => {

View File

@ -19,4 +19,5 @@ export const Static: QuartzEmitterPlugin = () => ({
yield dest
}
},
async *partialEmit() {},
})

View File

@ -5,22 +5,94 @@ import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage"
import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile"
import { FullPageLayout } from "../../cfg"
import {
FilePath,
FullSlug,
getAllSegmentPrefixes,
joinSegments,
pathToRoot,
} from "../../util/path"
import { FullSlug, getAllSegmentPrefixes, joinSegments, pathToRoot } from "../../util/path"
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { TagContent } from "../../components"
import { write } from "./helpers"
import { i18n } from "../../i18n"
import { i18n, TRANSLATIONS } from "../../i18n"
import { BuildCtx } from "../../util/ctx"
import { StaticResources } from "../../util/resources"
interface TagPageOptions extends FullPageLayout {
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
}
function computeTagInfo(
allFiles: QuartzPluginData[],
content: ProcessedContent[],
locale: keyof typeof TRANSLATIONS,
): [Set<string>, Record<string, ProcessedContent>] {
const tags: Set<string> = new Set(
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
)
// add base tag
tags.add("index")
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
[...tags].map((tag) => {
const title =
tag === "index"
? i18n(locale).pages.tagContent.tagIndex
: `${i18n(locale).pages.tagContent.tag}: ${tag}`
return [
tag,
defaultProcessedContent({
slug: joinSegments("tags", tag) as FullSlug,
frontmatter: { title, tags: [] },
}),
]
}),
)
// Update with actual content if available
for (const [tree, file] of content) {
const slug = file.data.slug!
if (slug.startsWith("tags/")) {
const tag = slug.slice("tags/".length)
if (tags.has(tag)) {
tagDescriptions[tag] = [tree, file]
if (file.data.frontmatter?.title === tag) {
file.data.frontmatter.title = `${i18n(locale).pages.tagContent.tag}: ${tag}`
}
}
}
}
return [tags, tagDescriptions]
}
async function processTagPage(
ctx: BuildCtx,
tag: string,
tagContent: ProcessedContent,
allFiles: QuartzPluginData[],
opts: FullPageLayout,
resources: StaticResources,
) {
const slug = joinSegments("tags", tag) as FullSlug
const [tree, file] = tagContent
const cfg = ctx.cfg.configuration
const externalResources = pageResources(pathToRoot(slug), resources)
const componentData: QuartzComponentProps = {
ctx,
fileData: file.data,
externalResources,
cfg,
children: [],
tree,
allFiles,
}
const content = renderPage(cfg, slug, componentData, opts, externalResources)
return write({
ctx,
content,
slug: file.data.slug!,
ext: ".html",
})
}
export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts) => {
const opts: FullPageLayout = {
...sharedPageComponents,
@ -52,64 +124,46 @@ export const TagPage: QuartzEmitterPlugin<Partial<TagPageOptions>> = (userOpts)
async *emit(ctx, content, resources) {
const allFiles = content.map((c) => c[1].data)
const cfg = ctx.cfg.configuration
const tags: Set<string> = new Set(
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
)
// add base tag
tags.add("index")
const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
[...tags].map((tag) => {
const title =
tag === "index"
? i18n(cfg.locale).pages.tagContent.tagIndex
: `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}`
return [
tag,
defaultProcessedContent({
slug: joinSegments("tags", tag) as FullSlug,
frontmatter: { title, tags: [] },
}),
]
}),
)
for (const [tree, file] of content) {
const slug = file.data.slug!
if (slug.startsWith("tags/")) {
const tag = slug.slice("tags/".length)
if (tags.has(tag)) {
tagDescriptions[tag] = [tree, file]
if (file.data.frontmatter?.title === tag) {
file.data.frontmatter.title = `${i18n(cfg.locale).pages.tagContent.tag}: ${tag}`
}
}
}
}
const [tags, tagDescriptions] = computeTagInfo(allFiles, content, cfg.locale)
for (const tag of tags) {
const slug = joinSegments("tags", tag) as FullSlug
const [tree, file] = tagDescriptions[tag]
const externalResources = pageResources(pathToRoot(slug), file.data, resources)
const componentData: QuartzComponentProps = {
ctx,
fileData: file.data,
externalResources,
cfg,
children: [],
tree,
allFiles,
yield processTagPage(ctx, tag, tagDescriptions[tag], allFiles, opts, resources)
}
},
async *partialEmit(ctx, content, resources, changeEvents) {
const allFiles = content.map((c) => c[1].data)
const cfg = ctx.cfg.configuration
// Find all tags that need to be updated based on changed files
const affectedTags: Set<string> = new Set()
for (const changeEvent of changeEvents) {
if (!changeEvent.file) continue
const slug = changeEvent.file.data.slug!
// If it's a tag page itself that changed
if (slug.startsWith("tags/")) {
const tag = slug.slice("tags/".length)
affectedTags.add(tag)
}
const content = renderPage(cfg, slug, componentData, opts, externalResources)
yield write({
ctx,
content,
slug: file.data.slug!,
ext: ".html",
})
// If a file with tags changed, we need to update those tag pages
const fileTags = changeEvent.file.data.frontmatter?.tags ?? []
fileTags.flatMap(getAllSegmentPrefixes).forEach((tag) => affectedTags.add(tag))
// Always update the index tag page if any file changes
affectedTags.add("index")
}
// If there are affected tags, rebuild their pages
if (affectedTags.size > 0) {
// We still need to compute all tags because tag pages show all tags
const [_tags, tagDescriptions] = computeTagInfo(allFiles, content, cfg.locale)
for (const tag of affectedTags) {
if (tagDescriptions[tag]) {
yield processTagPage(ctx, tag, tagDescriptions[tag], allFiles, opts, resources)
}
}
}
},
}

View File

@ -3,7 +3,7 @@ import remarkFrontmatter from "remark-frontmatter"
import { QuartzTransformerPlugin } from "../types"
import yaml from "js-yaml"
import toml from "toml"
import { FilePath, FullSlug, getFileExtension, joinSegments, slugifyFilePath, slugTag, trimSuffix } from "../../util/path"
import { FilePath, FullSlug, getFileExtension, slugifyFilePath, slugTag } from "../../util/path"
import { QuartzPluginData } from "../vfile"
import { i18n } from "../../i18n"

View File

@ -54,7 +54,7 @@ export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>>
textTransform(_ctx, src) {
if (opts.wikilinks) {
src = src.toString()
src = src.replaceAll(relrefRegex, (value, ...capture) => {
src = src.replaceAll(relrefRegex, (_value, ...capture) => {
const [text, link] = capture
return `[${text}](${link})`
})
@ -62,7 +62,7 @@ export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>>
if (opts.removePredefinedAnchor) {
src = src.toString()
src = src.replaceAll(predefinedHeadingIdRegex, (value, ...capture) => {
src = src.replaceAll(predefinedHeadingIdRegex, (_value, ...capture) => {
const [headingText] = capture
return headingText
})
@ -70,7 +70,7 @@ export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>>
if (opts.removeHugoShortcode) {
src = src.toString()
src = src.replaceAll(hugoShortcodeRegex, (value, ...capture) => {
src = src.replaceAll(hugoShortcodeRegex, (_value, ...capture) => {
const [scContent] = capture
return scContent
})
@ -78,7 +78,7 @@ export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>>
if (opts.replaceFigureWithMdImg) {
src = src.toString()
src = src.replaceAll(figureTagRegex, (value, ...capture) => {
src = src.replaceAll(figureTagRegex, (_value, ...capture) => {
const [src] = capture
return `![](${src})`
})
@ -86,11 +86,11 @@ export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin<Partial<Options>>
if (opts.replaceOrgLatex) {
src = src.toString()
src = src.replaceAll(inlineLatexRegex, (value, ...capture) => {
src = src.replaceAll(inlineLatexRegex, (_value, ...capture) => {
const [eqn] = capture
return `$${eqn}$`
})
src = src.replaceAll(blockLatexRegex, (value, ...capture) => {
src = src.replaceAll(blockLatexRegex, (_value, ...capture) => {
const [eqn] = capture
return `$$${eqn}$$`
})

View File

@ -1,10 +1,8 @@
import { QuartzTransformerPlugin } from "../types"
import { PluggableList } from "unified"
import { SKIP, visit } from "unist-util-visit"
import { visit } from "unist-util-visit"
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { Root, Html, Paragraph, Text, Link, Parent } from "mdast"
import { Node } from "unist"
import { VFile } from "vfile"
import { BuildVisitor } from "unist-util-visit"
export interface Options {
@ -34,21 +32,10 @@ const defaultOptions: Options = {
const orRegex = new RegExp(/{{or:(.*?)}}/, "g")
const TODORegex = new RegExp(/{{.*?\bTODO\b.*?}}/, "g")
const DONERegex = new RegExp(/{{.*?\bDONE\b.*?}}/, "g")
const videoRegex = new RegExp(/{{.*?\[\[video\]\].*?\:(.*?)}}/, "g")
const youtubeRegex = new RegExp(
/{{.*?\[\[video\]\].*?(https?:\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?)}}/,
"g",
)
// const multimediaRegex = new RegExp(/{{.*?\b(video|audio)\b.*?\:(.*?)}}/, "g")
const audioRegex = new RegExp(/{{.*?\[\[audio\]\].*?\:(.*?)}}/, "g")
const pdfRegex = new RegExp(/{{.*?\[\[pdf\]\].*?\:(.*?)}}/, "g")
const blockquoteRegex = new RegExp(/(\[\[>\]\])\s*(.*)/, "g")
const roamHighlightRegex = new RegExp(/\^\^(.+)\^\^/, "g")
const roamItalicRegex = new RegExp(/__(.+)__/, "g")
const tableRegex = new RegExp(/- {{.*?\btable\b.*?}}/, "g") /* TODO */
const attributeRegex = new RegExp(/\b\w+(?:\s+\w+)*::/, "g") /* TODO */
function isSpecialEmbed(node: Paragraph): boolean {
if (node.children.length !== 2) return false
@ -135,7 +122,7 @@ export const RoamFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | un
const plugins: PluggableList = []
plugins.push(() => {
return (tree: Root, file: VFile) => {
return (tree: Root) => {
const replacements: [RegExp, ReplaceFunction][] = []
// Handle special embeds (audio, video, PDF)

View File

@ -44,17 +44,17 @@ export type QuartzEmitterPlugin<Options extends OptionType = undefined> = (
) => QuartzEmitterPluginInstance
export type QuartzEmitterPluginInstance = {
name: string
emit(
emit: (
ctx: BuildCtx,
content: ProcessedContent[],
resources: StaticResources,
): Promise<FilePath[]> | AsyncGenerator<FilePath>
partialEmit?(
) => Promise<FilePath[]> | AsyncGenerator<FilePath>
partialEmit?: (
ctx: BuildCtx,
content: ProcessedContent[],
resources: StaticResources,
changeEvents: ChangeEvent[],
): Promise<FilePath[]> | AsyncGenerator<FilePath>
) => Promise<FilePath[]> | AsyncGenerator<FilePath> | null
/**
* Returns the components (if any) that are used in rendering the page.
* This helps Quartz optimize the page by only including necessary resources

View File

@ -7,7 +7,7 @@ import { Root as HTMLRoot } from "hast"
import { MarkdownContent, ProcessedContent } from "../plugins/vfile"
import { PerfTimer } from "../util/perf"
import { read } from "to-vfile"
import { FilePath, FullSlug, QUARTZ, slugifyFilePath } from "../util/path"
import { FilePath, QUARTZ, slugifyFilePath } from "../util/path"
import path from "path"
import workerpool, { Promise as WorkerPromise } from "workerpool"
import { QuartzLogger } from "../util/log"

View File

@ -1,8 +1,8 @@
import sourceMapSupport from "source-map-support"
sourceMapSupport.install(options)
import cfg from "../quartz.config"
import { Argv, BuildCtx, WorkerSerializableBuildCtx } from "./util/ctx"
import { FilePath, FullSlug } from "./util/path"
import { BuildCtx, WorkerSerializableBuildCtx } from "./util/ctx"
import { FilePath } from "./util/path"
import {
createFileParser,
createHtmlProcessor,

View File

@ -11,6 +11,8 @@
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"jsxImportSource": "preact"