mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
feat: add PageType plugin infrastructure (Phase D Step 4)
This commit is contained in:
parent
f8a682ab45
commit
68f3c3fadd
@ -56,13 +56,14 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
|||||||
const output = argv.output
|
const output = argv.output
|
||||||
|
|
||||||
const pluginCount = Object.values(cfg.plugins).flat().length
|
const pluginCount = Object.values(cfg.plugins).flat().length
|
||||||
const pluginNames = (key: "transformers" | "filters" | "emitters") =>
|
const pluginNames = (key: "transformers" | "filters" | "emitters" | "pageTypes") =>
|
||||||
cfg.plugins[key].map((plugin) => plugin.name)
|
(cfg.plugins[key] ?? []).map((plugin) => plugin.name)
|
||||||
if (argv.verbose) {
|
if (argv.verbose) {
|
||||||
console.log(`Loaded ${pluginCount} plugins`)
|
console.log(`Loaded ${pluginCount} plugins`)
|
||||||
console.log(` Transformers: ${pluginNames("transformers").join(", ")}`)
|
console.log(` Transformers: ${pluginNames("transformers").join(", ")}`)
|
||||||
console.log(` Filters: ${pluginNames("filters").join(", ")}`)
|
console.log(` Filters: ${pluginNames("filters").join(", ")}`)
|
||||||
console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
|
console.log(` Emitters: ${pluginNames("emitters").join(", ")}`)
|
||||||
|
console.log(` PageTypes: ${pluginNames("pageTypes").join(", ")}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const release = await mut.acquire()
|
const release = await mut.acquire()
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
QuartzTransformerPluginInstance,
|
QuartzTransformerPluginInstance,
|
||||||
QuartzFilterPluginInstance,
|
QuartzFilterPluginInstance,
|
||||||
QuartzEmitterPluginInstance,
|
QuartzEmitterPluginInstance,
|
||||||
|
QuartzPageTypePluginInstance,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { LoadedPlugin } from "./loader/types"
|
import { LoadedPlugin } from "./loader/types"
|
||||||
|
|
||||||
@ -9,6 +10,7 @@ export interface PluginConfiguration {
|
|||||||
transformers: (QuartzTransformerPluginInstance | LoadedPlugin)[]
|
transformers: (QuartzTransformerPluginInstance | LoadedPlugin)[]
|
||||||
filters: (QuartzFilterPluginInstance | LoadedPlugin)[]
|
filters: (QuartzFilterPluginInstance | LoadedPlugin)[]
|
||||||
emitters: (QuartzEmitterPluginInstance | LoadedPlugin)[]
|
emitters: (QuartzEmitterPluginInstance | LoadedPlugin)[]
|
||||||
|
pageTypes?: (QuartzPageTypePluginInstance | LoadedPlugin)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLoadedPlugin(plugin: unknown): plugin is LoadedPlugin {
|
export function isLoadedPlugin(plugin: unknown): plugin is LoadedPlugin {
|
||||||
@ -27,13 +29,22 @@ export function getPluginInstance<T extends object | undefined>(
|
|||||||
| QuartzTransformerPluginInstance
|
| QuartzTransformerPluginInstance
|
||||||
| QuartzFilterPluginInstance
|
| QuartzFilterPluginInstance
|
||||||
| QuartzEmitterPluginInstance
|
| QuartzEmitterPluginInstance
|
||||||
|
| QuartzPageTypePluginInstance
|
||||||
| LoadedPlugin,
|
| LoadedPlugin,
|
||||||
options?: T,
|
options?: T,
|
||||||
): QuartzTransformerPluginInstance | QuartzFilterPluginInstance | QuartzEmitterPluginInstance {
|
):
|
||||||
|
| QuartzTransformerPluginInstance
|
||||||
|
| QuartzFilterPluginInstance
|
||||||
|
| QuartzEmitterPluginInstance
|
||||||
|
| QuartzPageTypePluginInstance {
|
||||||
if (isLoadedPlugin(plugin)) {
|
if (isLoadedPlugin(plugin)) {
|
||||||
const factory = plugin.plugin as (
|
const factory = plugin.plugin as (
|
||||||
opts?: T,
|
opts?: T,
|
||||||
) => QuartzTransformerPluginInstance | QuartzFilterPluginInstance | QuartzEmitterPluginInstance
|
) =>
|
||||||
|
| QuartzTransformerPluginInstance
|
||||||
|
| QuartzFilterPluginInstance
|
||||||
|
| QuartzEmitterPluginInstance
|
||||||
|
| QuartzPageTypePluginInstance
|
||||||
return factory(options)
|
return factory(options)
|
||||||
}
|
}
|
||||||
return plugin
|
return plugin
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export * from "./filters"
|
|||||||
export * from "./emitters"
|
export * from "./emitters"
|
||||||
export * from "./types"
|
export * from "./types"
|
||||||
export * from "./config"
|
export * from "./config"
|
||||||
|
export * as PageTypes from "./pageTypes"
|
||||||
export * as PluginLoader from "./loader"
|
export * as PluginLoader from "./loader"
|
||||||
|
|
||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
|
|||||||
@ -7,7 +7,12 @@ import {
|
|||||||
PluginResolutionOptions,
|
PluginResolutionOptions,
|
||||||
PluginSpecifier,
|
PluginSpecifier,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { QuartzTransformerPlugin, QuartzFilterPlugin, QuartzEmitterPlugin } from "../types"
|
import {
|
||||||
|
QuartzTransformerPlugin,
|
||||||
|
QuartzFilterPlugin,
|
||||||
|
QuartzEmitterPlugin,
|
||||||
|
QuartzPageTypePlugin,
|
||||||
|
} from "../types"
|
||||||
import { parsePluginSource, installPlugin, getPluginEntryPoint } from "./gitLoader"
|
import { parsePluginSource, installPlugin, getPluginEntryPoint } from "./gitLoader"
|
||||||
|
|
||||||
const MINIMUM_QUARTZ_VERSION = "4.5.0"
|
const MINIMUM_QUARTZ_VERSION = "4.5.0"
|
||||||
@ -51,7 +56,9 @@ async function tryImportPlugin(packageName: string): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectPluginType(module: unknown): "transformer" | "filter" | "emitter" | null {
|
function detectPluginType(
|
||||||
|
module: unknown,
|
||||||
|
): "transformer" | "filter" | "emitter" | "pageType" | null {
|
||||||
if (!module || typeof module !== "object") return null
|
if (!module || typeof module !== "object") return null
|
||||||
|
|
||||||
const mod = module as Record<string, unknown>
|
const mod = module as Record<string, unknown>
|
||||||
@ -60,6 +67,8 @@ function detectPluginType(module: unknown): "transformer" | "filter" | "emitter"
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasPageTypeProps = ["match", "body", "layout"].every((key) => key in mod)
|
||||||
|
|
||||||
const hasTransformerProps = ["textTransform", "markdownPlugins", "htmlPlugins"].some(
|
const hasTransformerProps = ["textTransform", "markdownPlugins", "htmlPlugins"].some(
|
||||||
(key) => key in mod && (typeof mod[key] === "function" || mod[key] === undefined),
|
(key) => key in mod && (typeof mod[key] === "function" || mod[key] === undefined),
|
||||||
)
|
)
|
||||||
@ -70,6 +79,7 @@ function detectPluginType(module: unknown): "transformer" | "filter" | "emitter"
|
|||||||
|
|
||||||
const hasEmitterProps = ["emit"].some((key) => key in mod && typeof mod[key] === "function")
|
const hasEmitterProps = ["emit"].some((key) => key in mod && typeof mod[key] === "function")
|
||||||
|
|
||||||
|
if (hasPageTypeProps) return "pageType"
|
||||||
if (hasEmitterProps) return "emitter"
|
if (hasEmitterProps) return "emitter"
|
||||||
if (hasFilterProps) return "filter"
|
if (hasFilterProps) return "filter"
|
||||||
if (hasTransformerProps) return "transformer"
|
if (hasTransformerProps) return "transformer"
|
||||||
@ -79,8 +89,13 @@ function detectPluginType(module: unknown): "transformer" | "filter" | "emitter"
|
|||||||
|
|
||||||
function extractPluginFactory(
|
function extractPluginFactory(
|
||||||
module: unknown,
|
module: unknown,
|
||||||
type: "transformer" | "filter" | "emitter",
|
type: "transformer" | "filter" | "emitter" | "pageType",
|
||||||
): QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin | null {
|
):
|
||||||
|
| QuartzTransformerPlugin
|
||||||
|
| QuartzFilterPlugin
|
||||||
|
| QuartzEmitterPlugin
|
||||||
|
| QuartzPageTypePlugin
|
||||||
|
| null {
|
||||||
if (!module || typeof module !== "object") return null
|
if (!module || typeof module !== "object") return null
|
||||||
|
|
||||||
const mod = module as Record<string, unknown>
|
const mod = module as Record<string, unknown>
|
||||||
@ -88,7 +103,11 @@ function extractPluginFactory(
|
|||||||
const factory = mod.default ?? mod[type] ?? mod.plugin ?? null
|
const factory = mod.default ?? mod[type] ?? mod.plugin ?? null
|
||||||
|
|
||||||
if (typeof factory === "function") {
|
if (typeof factory === "function") {
|
||||||
return factory as QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin
|
return factory as
|
||||||
|
| QuartzTransformerPlugin
|
||||||
|
| QuartzFilterPlugin
|
||||||
|
| QuartzEmitterPlugin
|
||||||
|
| QuartzPageTypePlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@ -374,7 +393,7 @@ export async function resolvePlugins(
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
styleText("cyan", `External plugins loaded:`) +
|
styleText("cyan", `External plugins loaded:`) +
|
||||||
` ${byType.transformer ?? 0} transformers, ${byType.filter ?? 0} filters, ${byType.emitter ?? 0} emitters`,
|
` ${byType.transformer ?? 0} transformers, ${byType.filter ?? 0} filters, ${byType.emitter ?? 0} emitters, ${byType.pageType ?? 0} pageTypes`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import { QuartzTransformerPlugin, QuartzFilterPlugin, QuartzEmitterPlugin } from "../types"
|
import {
|
||||||
|
QuartzTransformerPlugin,
|
||||||
|
QuartzFilterPlugin,
|
||||||
|
QuartzEmitterPlugin,
|
||||||
|
QuartzPageTypePlugin,
|
||||||
|
} from "../types"
|
||||||
import { BuildCtx } from "../../util/ctx"
|
import { BuildCtx } from "../../util/ctx"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,7 +30,7 @@ export interface PluginManifest {
|
|||||||
author?: string
|
author?: string
|
||||||
homepage?: string
|
homepage?: string
|
||||||
keywords?: string[]
|
keywords?: string[]
|
||||||
category?: "transformer" | "filter" | "emitter"
|
category?: "transformer" | "filter" | "emitter" | "pageType"
|
||||||
quartzVersion?: string
|
quartzVersion?: string
|
||||||
configSchema?: object
|
configSchema?: object
|
||||||
/** Components provided by this plugin */
|
/** Components provided by this plugin */
|
||||||
@ -36,9 +41,9 @@ export interface PluginManifest {
|
|||||||
* Loaded plugin with metadata
|
* Loaded plugin with metadata
|
||||||
*/
|
*/
|
||||||
export interface LoadedPlugin {
|
export interface LoadedPlugin {
|
||||||
plugin: QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin
|
plugin: QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin | QuartzPageTypePlugin
|
||||||
manifest: PluginManifest
|
manifest: PluginManifest
|
||||||
type: "transformer" | "filter" | "emitter"
|
type: "transformer" | "filter" | "emitter" | "pageType"
|
||||||
source: string
|
source: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
quartz/plugins/pageTypes/404.ts
Normal file
32
quartz/plugins/pageTypes/404.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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",
|
||||||
|
body: NotFound,
|
||||||
|
})
|
||||||
182
quartz/plugins/pageTypes/dispatcher.ts
Normal file
182
quartz/plugins/pageTypes/dispatcher.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { QuartzEmitterPlugin, QuartzPageTypePluginInstance } from "../types"
|
||||||
|
import { QuartzComponent, QuartzComponentProps } from "../../components/types"
|
||||||
|
import { pageResources, renderPage } from "../../components/renderPage"
|
||||||
|
import { FullPageLayout } from "../../cfg"
|
||||||
|
import { FullSlug, pathToRoot } from "../../util/path"
|
||||||
|
import { ProcessedContent, defaultProcessedContent } from "../vfile"
|
||||||
|
import { write } from "../emitters/helpers"
|
||||||
|
import { BuildCtx } from "../../util/ctx"
|
||||||
|
import { StaticResources } from "../../util/resources"
|
||||||
|
|
||||||
|
function resolveLayout(
|
||||||
|
pageType: QuartzPageTypePluginInstance,
|
||||||
|
sharedDefaults: Partial<FullPageLayout>,
|
||||||
|
byPageType: Record<string, Partial<FullPageLayout>>,
|
||||||
|
): FullPageLayout {
|
||||||
|
const overrides = byPageType[pageType.layout] ?? {}
|
||||||
|
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!,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
const cfg = ctx.cfg.configuration
|
||||||
|
const externalResources = pageResources(pathToRoot(slug), resources)
|
||||||
|
const componentData: QuartzComponentProps = {
|
||||||
|
ctx,
|
||||||
|
fileData,
|
||||||
|
externalResources,
|
||||||
|
cfg,
|
||||||
|
children: [],
|
||||||
|
tree,
|
||||||
|
allFiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
return write({
|
||||||
|
ctx,
|
||||||
|
content: renderPage(cfg, slug, componentData, layout, externalResources),
|
||||||
|
slug,
|
||||||
|
ext: ".html",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>> = (userOpts) => {
|
||||||
|
const defaults = userOpts?.defaults ?? {}
|
||||||
|
const byPageType = userOpts?.byPageType ?? {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "PageTypeDispatcher",
|
||||||
|
getQuartzComponents(ctx) {
|
||||||
|
const pageTypes = ctx.cfg.plugins.pageTypes ?? []
|
||||||
|
return collectComponents(pageTypes, defaults, byPageType)
|
||||||
|
},
|
||||||
|
async *emit(ctx, content, resources) {
|
||||||
|
const pageTypes = [...(ctx.cfg.plugins.pageTypes ?? [])].sort(
|
||||||
|
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
|
||||||
|
)
|
||||||
|
const cfg = ctx.cfg.configuration
|
||||||
|
const allFiles = content.map((c) => c[1].data)
|
||||||
|
|
||||||
|
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, allFiles, layout, resources)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [tree, vfile] = defaultProcessedContent({
|
||||||
|
slug: vp.slug,
|
||||||
|
frontmatter: { title: vp.title, tags: [] },
|
||||||
|
...vp.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
yield emitPage(ctx, vp.slug, tree, vfile.data, allFiles, layout, resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async *partialEmit(ctx, content, resources, changeEvents) {
|
||||||
|
const pageTypes = [...(ctx.cfg.plugins.pageTypes ?? [])].sort(
|
||||||
|
(a, b) => (b.priority ?? 0) - (a.priority ?? 0),
|
||||||
|
)
|
||||||
|
const cfg = ctx.cfg.configuration
|
||||||
|
const allFiles = content.map((c) => c[1].data)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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, allFiles, layout, resources)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [tree, vfile] = defaultProcessedContent({
|
||||||
|
slug: vp.slug,
|
||||||
|
frontmatter: { title: vp.title, tags: [] },
|
||||||
|
...vp.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
yield emitPage(ctx, vp.slug, tree, vfile.data, allFiles, layout, resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
3
quartz/plugins/pageTypes/index.ts
Normal file
3
quartz/plugins/pageTypes/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { match } from "./matchers"
|
||||||
|
export { NotFoundPageType } from "./404"
|
||||||
|
export { PageTypeDispatcher } from "./dispatcher"
|
||||||
39
quartz/plugins/pageTypes/matchers.ts
Normal file
39
quartz/plugins/pageTypes/matchers.ts
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,15 +1,17 @@
|
|||||||
import { PluggableList } from "unified"
|
import { PluggableList } from "unified"
|
||||||
import { StaticResources } from "../util/resources"
|
import { StaticResources } from "../util/resources"
|
||||||
import { ProcessedContent } from "./vfile"
|
import { ProcessedContent, QuartzPluginData } from "./vfile"
|
||||||
import { QuartzComponent } from "../components/types"
|
import { QuartzComponent, QuartzComponentConstructor } from "../components/types"
|
||||||
import { FilePath } from "../util/path"
|
import { FilePath, FullSlug } from "../util/path"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { VFile } from "vfile"
|
import { VFile } from "vfile"
|
||||||
|
|
||||||
export interface PluginTypes {
|
export interface PluginTypes {
|
||||||
transformers: QuartzTransformerPluginInstance[]
|
transformers: QuartzTransformerPluginInstance[]
|
||||||
filters: QuartzFilterPluginInstance[]
|
filters: QuartzFilterPluginInstance[]
|
||||||
emitters: QuartzEmitterPluginInstance[]
|
emitters: QuartzEmitterPluginInstance[]
|
||||||
|
pageTypes?: QuartzPageTypePluginInstance[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionType = object | undefined
|
type OptionType = object | undefined
|
||||||
@ -63,3 +65,67 @@ export type QuartzEmitterPluginInstance = {
|
|||||||
getQuartzComponents?: (ctx: BuildCtx) => QuartzComponent[]
|
getQuartzComponents?: (ctx: BuildCtx) => QuartzComponent[]
|
||||||
externalResources?: ExternalResourcesFn
|
externalResources?: ExternalResourcesFn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PageType Plugin Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher function: determines if a source file belongs to a page type.
|
||||||
|
* Returns true if the page type should own this file.
|
||||||
|
*/
|
||||||
|
export type PageMatcher = (args: {
|
||||||
|
slug: FullSlug
|
||||||
|
fileData: QuartzPluginData
|
||||||
|
cfg: GlobalConfiguration
|
||||||
|
}) => boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual page descriptor for page types that generate pages
|
||||||
|
* from aggregated data (e.g., tag indexes, folder listings).
|
||||||
|
*/
|
||||||
|
export interface VirtualPage {
|
||||||
|
slug: FullSlug
|
||||||
|
title: string
|
||||||
|
data: Partial<QuartzPluginData>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generator function: produces virtual pages from all processed content.
|
||||||
|
* Used by page types that don't match source files but instead create
|
||||||
|
* synthetic pages (e.g., one page per tag, one page per folder).
|
||||||
|
*/
|
||||||
|
export type PageGenerator = (args: {
|
||||||
|
content: ProcessedContent[]
|
||||||
|
cfg: GlobalConfiguration
|
||||||
|
ctx: BuildCtx
|
||||||
|
}) => VirtualPage[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function that creates a PageType plugin instance.
|
||||||
|
*/
|
||||||
|
export type QuartzPageTypePlugin<Options extends OptionType = undefined> = (
|
||||||
|
opts?: Options,
|
||||||
|
) => QuartzPageTypePluginInstance
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A PageType plugin instance.
|
||||||
|
*
|
||||||
|
* PageTypes are a declarative abstraction over page-rendering emitters.
|
||||||
|
* Each PageType declares which files it owns (via `match`), optionally
|
||||||
|
* generates virtual pages (via `generate`), and provides a body component
|
||||||
|
* and layout reference for rendering.
|
||||||
|
*/
|
||||||
|
export type QuartzPageTypePluginInstance = {
|
||||||
|
name: string
|
||||||
|
/** Higher priority wins when multiple page types match the same file. Default: 0. */
|
||||||
|
priority?: number
|
||||||
|
/** Determines which source files this page type owns. */
|
||||||
|
match: PageMatcher
|
||||||
|
/** Produces virtual pages from aggregated content data. */
|
||||||
|
generate?: PageGenerator
|
||||||
|
/** Layout key — references a key in `layout.byPageType`. */
|
||||||
|
layout: string
|
||||||
|
/** The body component constructor for this page type. */
|
||||||
|
body: QuartzComponentConstructor
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user