Initial commit

This commit is contained in:
Pax
2026-07-07 20:48:21 -04:00
committed by GitHub
commit ceb2d21bbf
292 changed files with 42584 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import { QuartzPageTypePlugin } from "../types"
import { match } from "./matchers"
import { NotFound } from "../../components"
import { defaultProcessedContent } from "../vfile"
import { i18n } from "../../i18n"
import { FullSlug } from "../../util/path"
export const NotFoundPageType: QuartzPageTypePlugin = () => ({
name: "404",
priority: -1,
match: match.none(),
generate({ cfg }) {
const notFound = i18n(cfg.locale).pages.error.title
const slug = "404" as FullSlug
const [, vfile] = defaultProcessedContent({
slug,
text: notFound,
description: notFound,
frontmatter: { title: notFound, tags: [] },
})
return [
{
slug,
title: notFound,
data: vfile.data,
},
]
},
layout: "404",
frame: "minimal",
body: NotFound,
})

View File

@@ -0,0 +1,339 @@
import { QuartzEmitterPlugin, QuartzPageTypePluginInstance, TreeTransform } from "../types"
import { QuartzComponent, QuartzComponentProps } from "../../components/types"
import { pageResources, renderPage } from "../../components/renderPage"
import { FullPageLayout } from "../../cfg"
import { FilePath, FullSlug, pathToRoot } from "../../util/path"
import { ProcessedContent, defaultProcessedContent } from "../vfile"
import { write } from "../emitters/helpers"
import { BuildCtx, trieFromAllFiles } from "../../util/ctx"
import { StaticResources } from "../../util/resources"
import { render } from "preact-render-to-string"
import { fromHtml } from "hast-util-from-html"
import { Root as HtmlRoot } from "hast"
function getPageTypes(ctx: BuildCtx): QuartzPageTypePluginInstance[] {
return (ctx.cfg.plugins.pageTypes ?? []) as unknown as QuartzPageTypePluginInstance[]
}
function resolveLayout(
pageType: QuartzPageTypePluginInstance,
sharedDefaults: Partial<FullPageLayout>,
byPageType: Record<string, Partial<FullPageLayout>>,
): FullPageLayout {
const overrides = byPageType[pageType.layout] ?? {}
// Frame priority: config override > page type declaration > default
const frame = overrides.frame ?? pageType.frame ?? "default"
return {
head: overrides.head ?? sharedDefaults.head!,
header: overrides.header ?? sharedDefaults.header ?? [],
beforeBody: overrides.beforeBody ?? sharedDefaults.beforeBody ?? [],
pageBody: pageType.body(undefined),
afterBody: overrides.afterBody ?? sharedDefaults.afterBody ?? [],
left: overrides.left ?? sharedDefaults.left ?? [],
right: overrides.right ?? sharedDefaults.right ?? [],
footer: overrides.footer ?? sharedDefaults.footer!,
frame,
}
}
function collectComponents(
pageTypes: QuartzPageTypePluginInstance[],
sharedDefaults: Partial<FullPageLayout>,
byPageType: Record<string, Partial<FullPageLayout>>,
): QuartzComponent[] {
const seen = new Set<QuartzComponent>()
for (const pt of pageTypes) {
const layout = resolveLayout(pt, sharedDefaults, byPageType)
const all = [
layout.head,
...layout.header,
...layout.beforeBody,
layout.pageBody,
...layout.afterBody,
...layout.left,
...layout.right,
layout.footer,
]
for (const c of all) {
if (c) seen.add(c)
}
}
return [...seen]
}
interface DispatcherOptions {
defaults: Partial<FullPageLayout>
byPageType: Record<string, Partial<FullPageLayout>>
}
async function emitPage(
ctx: BuildCtx,
slug: FullSlug,
tree: ProcessedContent[0],
fileData: ProcessedContent[1]["data"],
allFiles: ProcessedContent[1]["data"][],
layout: FullPageLayout,
resources: StaticResources,
treeTransforms?: TreeTransform[],
) {
const cfg = ctx.cfg.configuration
// For the 404 page, use an absolute base path so assets resolve correctly
// when the hosting provider serves 404.html from any URL depth.
// During local dev (--serve), the dev server strips baseDir itself and
// serves files from root, so the 404 page must use "/" to avoid requesting
// assets under a path prefix that the dev server doesn't serve.
const baseDir =
slug === "404"
? ((ctx.argv.serve
? "/"
: new URL(`https://${cfg.baseUrl ?? "example.com"}`).pathname) as FullSlug)
: pathToRoot(slug)
const externalResources = pageResources(baseDir, resources, ctx)
const componentData: QuartzComponentProps = {
ctx,
fileData,
externalResources,
cfg,
children: [],
tree,
allFiles,
}
return write({
ctx,
content: renderPage(cfg, slug, componentData, layout, externalResources, treeTransforms),
slug,
ext: ".html",
})
}
/**
* Render each virtual page's Body component to HTML and parse it to a hast tree,
* populating both the ProcessedContent tree and vfile.data.htmlAst so that
* transclusion (e.g. ![[file.canvas]]) can inline the virtual page's content.
*/
function populateVirtualPageHtmlAst(
virtualEntries: Array<{
tree: ProcessedContent[0]
vfile: ProcessedContent[1]
layout: FullPageLayout
vpSlug: FullSlug
}>,
ctx: BuildCtx,
allFiles: ProcessedContent[1]["data"][],
resources: StaticResources,
) {
const cfg = ctx.cfg.configuration
for (const ve of virtualEntries) {
const BodyComponent = ve.layout.pageBody
const externalResources = pageResources(pathToRoot(ve.vpSlug), resources, ctx)
const componentData: QuartzComponentProps = {
ctx,
fileData: ve.vfile.data,
externalResources,
cfg,
children: [],
tree: ve.tree,
allFiles,
}
try {
const htmlString = render(BodyComponent(componentData))
const htmlAst = fromHtml(htmlString, { fragment: true }) as HtmlRoot
ve.vfile.data.htmlAst = htmlAst
} catch {
// Body rendering failed — leave htmlAst empty so transclusion falls
// back to the default title-only display.
}
}
}
export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>> = (userOpts) => {
const defaults = userOpts?.defaults ?? {}
const byPageType = userOpts?.byPageType ?? {}
return {
name: "PageTypeDispatcher",
getQuartzComponents(ctx) {
const pageTypes = getPageTypes(ctx)
return collectComponents(pageTypes, defaults, byPageType)
},
async *emit(ctx, content, resources) {
const pageTypes = [...getPageTypes(ctx)].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
const cfg = ctx.cfg.configuration
const allFiles = content.map((c) => c[1].data)
// Collect tree transforms from all page type plugins
const treeTransforms: TreeTransform[] = pageTypes.flatMap(
(pt) => pt.treeTransforms?.(ctx) ?? [],
)
// Ensure trie is available for components that need folder hierarchy (e.g. FolderContent)
ctx.trie ??= trieFromAllFiles(allFiles)
// Phase 1: Generate all virtual pages first so their data is available in allFiles
// for transclude resolution in renderPage (e.g. ![[file.canvas]], ![[file.base]])
const virtualEntries: Array<{
tree: ProcessedContent[0]
vfile: ProcessedContent[1]
layout: FullPageLayout
vpSlug: FullSlug
}> = []
for (const pt of pageTypes) {
if (!pt.generate) continue
const virtualPages = pt.generate({ content, cfg, ctx })
const layout = resolveLayout(pt, defaults, byPageType)
for (const vp of virtualPages) {
const vpSlug = vp.slug as FullSlug
const vpRelativePath = (vpSlug + ".md") as FilePath
const [tree, vfile] = defaultProcessedContent({
slug: vpSlug,
relativePath: vpRelativePath,
frontmatter: { title: vp.title, tags: [] },
...vp.data,
})
if (vpSlug !== "404") {
ctx.virtualPages.push([tree, vfile])
}
virtualEntries.push({ tree, vfile, layout, vpSlug })
}
}
// Merge virtual page data into allFiles before populating htmlAst so that
// Body components rendered during populateVirtualPageHtmlAst can resolve
// cross-virtual-page embeds (e.g. a .base file embedded in a .canvas file).
// The vfile.data objects are shared by reference, so htmlAst set on earlier
// entries becomes visible to later entries in the same pass.
const allFilesWithVirtual = [...allFiles, ...virtualEntries.map((ve) => ve.vfile.data)]
// Render Body components to populate htmlAst for transclusion
populateVirtualPageHtmlAst(virtualEntries, ctx, allFilesWithVirtual, resources)
// Phase 2: Emit regular pages (with virtual page data available for transclusion)
for (const [tree, file] of content) {
const slug = file.data.slug!
const fileData = file.data
for (const pt of pageTypes) {
if (pt.match({ slug, fileData, cfg })) {
const layout = resolveLayout(pt, defaults, byPageType)
yield emitPage(
ctx,
slug,
tree,
fileData,
allFilesWithVirtual,
layout,
resources,
treeTransforms,
)
break
}
}
}
// Phase 3: Emit virtual pages
for (const ve of virtualEntries) {
yield emitPage(
ctx,
ve.vpSlug,
ve.tree,
ve.vfile.data,
allFilesWithVirtual,
ve.layout,
resources,
treeTransforms,
)
}
},
async *partialEmit(ctx, content, resources, changeEvents) {
const pageTypes = [...getPageTypes(ctx)].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
const cfg = ctx.cfg.configuration
const allFiles = content.map((c) => c[1].data)
// Collect tree transforms from all page type plugins
const treeTransforms: TreeTransform[] = pageTypes.flatMap(
(pt) => pt.treeTransforms?.(ctx) ?? [],
)
// Rebuild trie on partial emit to reflect file changes
ctx.trie = trieFromAllFiles(allFiles)
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!)
}
}
// Phase 1: Generate all virtual pages first so their data is available in allFiles
const virtualEntries: Array<{
tree: ProcessedContent[0]
vfile: ProcessedContent[1]
layout: FullPageLayout
vpSlug: FullSlug
}> = []
for (const pt of pageTypes) {
if (!pt.generate) continue
const virtualPages = pt.generate({ content, cfg, ctx })
const layout = resolveLayout(pt, defaults, byPageType)
for (const vp of virtualPages) {
const vpSlug = vp.slug as FullSlug
const vpRelativePath = (vpSlug + ".md") as FilePath
const [tree, vfile] = defaultProcessedContent({
slug: vpSlug,
relativePath: vpRelativePath,
frontmatter: { title: vp.title, tags: [] },
...vp.data,
})
if (vpSlug !== "404") {
ctx.virtualPages.push([tree, vfile])
}
virtualEntries.push({ tree, vfile, layout, vpSlug })
}
}
const allFilesWithVirtual = [...allFiles, ...virtualEntries.map((ve) => ve.vfile.data)]
// Render Body components to populate htmlAst for transclusion
populateVirtualPageHtmlAst(virtualEntries, ctx, allFilesWithVirtual, resources)
// Phase 2: Emit changed regular pages
for (const [tree, file] of content) {
const slug = file.data.slug!
if (!changedSlugs.has(slug)) continue
const fileData = file.data
for (const pt of pageTypes) {
if (pt.match({ slug, fileData, cfg })) {
const layout = resolveLayout(pt, defaults, byPageType)
yield emitPage(
ctx,
slug,
tree,
fileData,
allFilesWithVirtual,
layout,
resources,
treeTransforms,
)
break
}
}
}
// Phase 3: Emit virtual pages
for (const ve of virtualEntries) {
yield emitPage(
ctx,
ve.vpSlug,
ve.tree,
ve.vfile.data,
allFilesWithVirtual,
ve.layout,
resources,
treeTransforms,
)
}
},
}
}

View File

@@ -0,0 +1,3 @@
export { match } from "./matchers"
export { NotFoundPageType } from "./404"
export { PageTypeDispatcher } from "./dispatcher"

View File

@@ -0,0 +1,39 @@
import { PageMatcher } from "../types"
export const match = {
ext: (extension: string): PageMatcher => {
const normalized = extension.startsWith(".") ? extension : `.${extension}`
return ({ slug }) => slug.endsWith(normalized) || !slug.includes(".")
},
slugPrefix: (prefix: string): PageMatcher => {
return ({ slug }) => slug.startsWith(prefix)
},
frontmatter: (key: string, predicate: (value: unknown) => boolean): PageMatcher => {
return ({ fileData }) => {
const fm = fileData.frontmatter as Record<string, unknown> | undefined
return fm ? predicate(fm[key]) : false
}
},
and: (...matchers: PageMatcher[]): PageMatcher => {
return (args) => matchers.every((m) => m(args))
},
or: (...matchers: PageMatcher[]): PageMatcher => {
return (args) => matchers.some((m) => m(args))
},
not: (matcher: PageMatcher): PageMatcher => {
return (args) => !matcher(args)
},
all: (): PageMatcher => {
return () => true
},
none: (): PageMatcher => {
return () => false
},
}