Initial import

This commit is contained in:
Admin
2026-07-11 17:08:37 +00:00
commit 03e5f3452b
303 changed files with 43365 additions and 0 deletions
@@ -0,0 +1,7 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
const Body: QuartzComponent = ({ children }: QuartzComponentProps) => {
return <div id="quartz-body">{children}</div>
}
export default (() => Body) satisfies QuartzComponentConstructor
@@ -0,0 +1,22 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
type ConditionalRenderConfig = {
component: QuartzComponent
condition: (props: QuartzComponentProps) => boolean
}
export default ((config: ConditionalRenderConfig) => {
const ConditionalRender: QuartzComponent = (props: QuartzComponentProps) => {
if (config.condition(props)) {
return <config.component {...props} />
}
return null
}
ConditionalRender.afterDOMLoaded = config.component.afterDOMLoaded
ConditionalRender.beforeDOMLoaded = config.component.beforeDOMLoaded
ConditionalRender.css = config.component.css
return ConditionalRender
}) satisfies QuartzComponentConstructor<ConditionalRenderConfig>
@@ -0,0 +1,30 @@
import { ValidLocale } from "../i18n"
import { QuartzPluginData } from "../plugins/vfile"
interface Props {
date: Date
locale?: ValidLocale
}
export type ValidDateType = keyof Required<QuartzPluginData>["dates"]
export function getDate(data: QuartzPluginData): Date | undefined {
if (!data.defaultDateType) {
throw new Error(
`Field 'defaultDateType' was not set. Ensure the CreatedModifiedDate plugin is configured with a 'defaultDateType' option. See https://quartz.jzhao.xyz/plugins/CreatedModifiedDate for more details.`,
)
}
return data.dates?.[data.defaultDateType]
}
export function formatDate(d: Date, locale: ValidLocale = "en-US"): string {
return d.toLocaleDateString(locale, {
year: "numeric",
month: "short",
day: "2-digit",
})
}
export function Date({ date, locale }: Props) {
return <time datetime={date.toISOString()}>{formatDate(date, locale)}</time>
}
@@ -0,0 +1,18 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component: QuartzComponent) => {
const Component = component
const DesktopOnly: QuartzComponent = (props: QuartzComponentProps) => {
return (
<div class="desktop-only">
<Component {...props} />
</div>
)
}
DesktopOnly.displayName = component.displayName
DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded
DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded
DesktopOnly.css = component?.css
return DesktopOnly
}) satisfies QuartzComponentConstructor<QuartzComponent>
@@ -0,0 +1,59 @@
import { concatenateResources } from "../util/resources"
import { classNames } from "../util/lang"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
type FlexConfig = {
components: {
Component: QuartzComponent
grow?: boolean
shrink?: boolean
basis?: string
order?: number
align?: "start" | "end" | "center" | "stretch"
justify?: "start" | "end" | "center" | "between" | "around"
}[]
direction?: "row" | "row-reverse" | "column" | "column-reverse"
wrap?: "nowrap" | "wrap" | "wrap-reverse"
gap?: string
}
export default ((config: FlexConfig) => {
const Flex: QuartzComponent = (props: QuartzComponentProps) => {
const direction = config.direction ?? "row"
const wrap = config.wrap ?? "nowrap"
const gap = config.gap ?? "1rem"
return (
<div
class={classNames(props.displayClass, "flex-component")}
style={`flex-direction: ${direction}; flex-wrap: ${wrap}; gap: ${gap};`}
>
{config.components.map((c) => {
const grow = c.grow ? 1 : 0
const shrink = (c.shrink ?? true) ? 1 : 0
const basis = c.basis ?? "auto"
const order = c.order ?? 0
const align = c.align ?? "center"
const justify = c.justify ?? "center"
return (
<div
style={`flex-grow: ${grow}; flex-shrink: ${shrink}; flex-basis: ${basis}; order: ${order}; align-self: ${align}; justify-self: ${justify};`}
>
<c.Component {...props} />
</div>
)
})}
</div>
)
}
Flex.afterDOMLoaded = concatenateResources(
...config.components.map((c) => c.Component.afterDOMLoaded),
)
Flex.beforeDOMLoaded = concatenateResources(
...config.components.map((c) => c.Component.beforeDOMLoaded),
)
Flex.css = concatenateResources(...config.components.map((c) => c.Component.css))
return Flex
}) satisfies QuartzComponentConstructor<FlexConfig>
@@ -0,0 +1,114 @@
import { i18n } from "../i18n"
import { FullSlug, getFileExtension, joinSegments, pathToRoot } from "../util/path"
import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/resources"
import { googleFontHref, googleFontSubsetHref } from "../util/theme"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { unescapeHTML } from "../util/escape"
import { CustomOgImagesEmitterName } from "../../.quartz/plugins"
export default (() => {
const Head: QuartzComponent = ({
cfg,
fileData,
externalResources,
ctx,
}: QuartzComponentProps) => {
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 { css, js, additionalHead } = externalResources
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const path = url.pathname as FullSlug
const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!)
const iconPath = joinSegments(baseDir, "static/icon.png")
// Url of current page
const socialUrl =
fileData.slug === "404" ? url.toString() : joinSegments(url.toString(), fileData.slug!)
const usesCustomOgImage = ctx.cfg.plugins.emitters.some(
(e) => e.name === CustomOgImagesEmitterName,
)
const ogImageDefaultPath = `https://${cfg.baseUrl}/static/og-image.png`
const coreStylesheet = css[0]?.content
const coreScript = js.find(
(r) => r.loadTime === "beforeDOMReady" && r.contentType === "external",
)
return (
<head>
<title>{title}</title>
<meta charSet="utf-8" />
{coreStylesheet && <link rel="preload" href={coreStylesheet} as="style" />}
{coreScript && coreScript.contentType === "external" && (
<link rel="preload" href={coreScript.src} as="script" />
)}
{cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link rel="stylesheet" href={googleFontHref(cfg.theme)} />
{cfg.theme.typography.title && (
<link rel="stylesheet" href={googleFontSubsetHref(cfg.theme, cfg.pageTitle)} />
)}
</>
)}
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossOrigin="anonymous" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="og:site_name" content={cfg.pageTitle}></meta>
<meta property="og:title" content={title} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta property="og:description" content={description} />
<meta property="og:image:alt" content={description} />
{!usesCustomOgImage && (
<>
<meta property="og:image" content={ogImageDefaultPath} />
<meta property="og:image:url" content={ogImageDefaultPath} />
<meta name="twitter:image" content={ogImageDefaultPath} />
<meta
property="og:image:type"
content={`image/${getFileExtension(ogImageDefaultPath) ?? "png"}`}
/>
</>
)}
{cfg.baseUrl && (
<>
<meta property="twitter:domain" content={cfg.baseUrl}></meta>
<meta property="og:url" content={socialUrl}></meta>
<meta property="twitter:url" content={socialUrl}></meta>
</>
)}
<link rel="icon" href={iconPath} />
<meta name="description" content={description} />
<meta name="generator" content="Quartz" />
{css.map((resource) => CSSResourceToStyleElement(resource, true))}
{js
.filter((resource) => resource.loadTime === "beforeDOMReady")
.map((res) => JSResourceToScriptElement(res, true))}
{additionalHead.map((resource) => {
if (typeof resource === "function") {
return resource(fileData)
} else {
return resource
}
})}
</head>
)
}
return Head
}) satisfies QuartzComponentConstructor
@@ -0,0 +1,22 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
const Header: QuartzComponent = ({ children }: QuartzComponentProps) => {
return children.length > 0 ? <header>{children}</header> : null
}
Header.css = `
header {
display: flex;
flex-direction: row;
align-items: center;
margin: 2rem 0;
gap: 1.5rem;
}
header h1 {
margin: 0;
flex: auto;
}
`
export default (() => Header) satisfies QuartzComponentConstructor
@@ -0,0 +1,18 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export default ((component: QuartzComponent) => {
const Component = component
const MobileOnly: QuartzComponent = (props: QuartzComponentProps) => {
return (
<div class="mobile-only">
<Component {...props} />
</div>
)
}
MobileOnly.displayName = component.displayName
MobileOnly.afterDOMLoaded = component?.afterDOMLoaded
MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded
MobileOnly.css = component?.css
return MobileOnly
}) satisfies QuartzComponentConstructor<QuartzComponent>
@@ -0,0 +1,114 @@
import { FullSlug, isFolderPath, resolveRelative } from "../util/path"
import { QuartzPluginData } from "../plugins/vfile"
import { Date, getDate } from "./Date"
import { QuartzComponent, QuartzComponentProps } from "./types"
export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number
export function byDateAndAlphabetical(): SortFn {
return (f1, f2) => {
// Sort by date/alphabetical
if (f1.dates && f2.dates) {
// sort descending
return getDate(f2)!.getTime() - getDate(f1)!.getTime()
} else if (f1.dates && !f2.dates) {
// prioritize files with dates
return -1
} else if (!f1.dates && f2.dates) {
return 1
}
// otherwise, sort lexographically by title
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
return f1Title.localeCompare(f2Title)
}
}
export function byDateAndAlphabeticalFolderFirst(): SortFn {
return (f1, f2) => {
// Sort folders first
const f1IsFolder = isFolderPath(f1.slug ?? "")
const f2IsFolder = isFolderPath(f2.slug ?? "")
if (f1IsFolder && !f2IsFolder) return -1
if (!f1IsFolder && f2IsFolder) return 1
// If both are folders or both are files, sort by date/alphabetical
if (f1.dates && f2.dates) {
// sort descending
return getDate(f2)!.getTime() - getDate(f1)!.getTime()
} else if (f1.dates && !f2.dates) {
// prioritize files with dates
return -1
} else if (!f1.dates && f2.dates) {
return 1
}
// otherwise, sort lexographically by title
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
return f1Title.localeCompare(f2Title)
}
}
type Props = {
limit?: number
sort?: SortFn
} & QuartzComponentProps
export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort }: Props) => {
const sorter = sort ?? byDateAndAlphabeticalFolderFirst()
let list = allFiles.sort(sorter)
if (limit) {
list = list.slice(0, limit)
}
return (
<ul class="section-ul">
{list.map((page) => {
const title = page.frontmatter?.title
const tags = page.frontmatter?.tags ?? []
return (
<li class="section-li">
<div class="section">
<p class="meta">{page.dates && <Date date={getDate(page)!} locale={cfg.locale} />}</p>
<div class="desc">
<h3>
<a
href={resolveRelative(fileData.slug!, page.slug!)}
class="internal internal-link"
>
{title}
</a>
</h3>
</div>
<ul class="tags">
{tags.map((tag) => (
<li>
<a
class="internal tag-link"
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
>
{tag}
</a>
</li>
))}
</ul>
</div>
</li>
)
})}
</ul>
)
}
PageList.css = `
.section h3 {
margin: 0;
}
.section > .tags {
margin: 0;
}
`
@@ -0,0 +1,8 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { classNames } from "../util/lang"
function Spacer({ displayClass }: QuartzComponentProps) {
return <div class={classNames(displayClass, "spacer")}></div>
}
export default (() => Spacer) satisfies QuartzComponentConstructor
@@ -0,0 +1,23 @@
import { componentRegistry } from "./registry"
import { QuartzComponent, QuartzComponentConstructor } from "./types"
export function External<Options extends object | undefined>(
name: string,
options?: Options,
): QuartzComponent {
const registered = componentRegistry.get(name)
if (!registered) {
throw new Error(
`External component "${name}" not found. ` +
`Make sure the plugin is installed and components are loaded before layouts are evaluated.`,
)
}
const { component } = registered
if (typeof component === "function") {
return (component as QuartzComponentConstructor<Options>)(options as Options)
}
return component as QuartzComponent
}
@@ -0,0 +1,61 @@
import { PageFrame, PageFrameProps } from "./types"
import HeaderConstructor from "../Header"
const Header = HeaderConstructor()
/**
* The default page frame — three-column layout with left sidebar, center
* content (header + body + afterBody), and right sidebar, followed by a footer.
*
* This is the original Quartz layout, extracted from renderPage.tsx.
*/
export const DefaultFrame: PageFrame = {
name: "default",
render({
componentData,
header,
beforeBody,
pageBody: Content,
afterBody,
left,
right,
footer: Footer,
}: PageFrameProps) {
return (
<>
<div class="left sidebar">
{left.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
<div class="center">
<div class="page-header">
<Header {...componentData}>
{header.map((HeaderComponent) => (
<HeaderComponent {...componentData} />
))}
</Header>
<div class="popover-hint">
{beforeBody.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
</div>
<Content {...componentData} />
<hr />
<div class="page-footer">
{afterBody.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
</div>
<div class="right sidebar">
{right.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
<Footer {...componentData} />
</>
)
},
}
@@ -0,0 +1,51 @@
import { PageFrame, PageFrameProps } from "./types"
import HeaderConstructor from "../Header"
const Header = HeaderConstructor()
/**
* Full-width page frame — no sidebars. The center content area spans the
* full width of the page. Header, beforeBody, body, afterBody, and footer
* are all rendered in a single column.
*
* Useful for page types like Canvas, presentations, or dashboards that
* need maximum horizontal space.
*/
export const FullWidthFrame: PageFrame = {
name: "full-width",
render({
componentData,
header,
beforeBody,
pageBody: Content,
afterBody,
footer: Footer,
}: PageFrameProps) {
return (
<>
<div class="center full-width">
<div class="page-header">
<Header {...componentData}>
{header.map((HeaderComponent) => (
<HeaderComponent {...componentData} />
))}
</Header>
<div class="popover-hint">
{beforeBody.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
</div>
<Content {...componentData} />
<hr />
<div class="page-footer">
{afterBody.map((BodyComponent) => (
<BodyComponent {...componentData} />
))}
</div>
</div>
<Footer {...componentData} />
</>
)
},
}
@@ -0,0 +1,23 @@
import { PageFrame, PageFrameProps } from "./types"
/**
* Minimal page frame — no sidebars, no header/footer chrome. Only the
* page body is rendered with a thin wrapper, plus the footer for legal/link
* obligations.
*
* Useful for immersive page types like full-screen canvases, kiosks,
* or custom landing pages that want complete control of the viewport.
*/
export const MinimalFrame: PageFrame = {
name: "minimal",
render({ componentData, pageBody: Content, footer: Footer }: PageFrameProps) {
return (
<>
<div class="center minimal">
<Content {...componentData} />
</div>
<Footer {...componentData} />
</>
)
},
}
@@ -0,0 +1,52 @@
import { PageFrame } from "./types"
import { DefaultFrame } from "./DefaultFrame"
import { FullWidthFrame } from "./FullWidthFrame"
import { MinimalFrame } from "./MinimalFrame"
import { frameRegistry } from "./registry"
export type { PageFrame, PageFrameProps } from "./types"
export { DefaultFrame } from "./DefaultFrame"
export { FullWidthFrame } from "./FullWidthFrame"
export { MinimalFrame } from "./MinimalFrame"
export { frameRegistry } from "./registry"
export type { RegisteredFrame } from "./registry"
/**
* Registry of built-in page frames. Page types can reference these by name
* via their `frame` property, and YAML config can override via
* `layout.byPageType.<name>.template`.
*
* The "default" frame reproduces the original three-column Quartz layout.
*/
const builtinFrames: Record<string, PageFrame> = {
default: DefaultFrame,
"full-width": FullWidthFrame,
minimal: MinimalFrame,
}
/**
* Resolve a frame by name. Checks plugin-registered frames first,
* then built-in frames, then falls back to DefaultFrame.
*/
export function resolveFrame(name: string | undefined): PageFrame {
if (!name || name === "default") {
return DefaultFrame
}
// Check plugin-registered frames first
const registered = frameRegistry.get(name)
if (registered) {
return registered.frame
}
// Fall back to built-in frames
const frame = builtinFrames[name]
if (!frame) {
const allFrameNames = [...Object.keys(builtinFrames), ...[...frameRegistry.getAll().keys()]]
console.warn(
`Unknown page frame "${name}", falling back to "default". Available frames: ${allFrameNames.join(", ")}`,
)
return DefaultFrame
}
return frame
}
@@ -0,0 +1,34 @@
import { PageFrame } from "./types"
export interface RegisteredFrame {
frame: PageFrame
source: string
}
class FrameRegistry {
private frames = new Map<string, RegisteredFrame>()
register(name: string, frame: PageFrame, source: string): void {
const existing = this.frames.get(name)
if (existing && existing.source !== source) {
console.warn(
`Page frame "${name}" from ${source} is overwriting frame from ${existing.source}`,
)
}
this.frames.set(name, { frame, source })
}
get(name: string): RegisteredFrame | undefined {
return this.frames.get(name)
}
getAll(): Map<string, RegisteredFrame> {
return new Map(this.frames)
}
has(name: string): boolean {
return this.frames.has(name)
}
}
export const frameRegistry = new FrameRegistry()
@@ -0,0 +1,43 @@
import { JSX } from "preact"
import { QuartzComponent, QuartzComponentProps } from "../types"
/**
* Props passed to a PageFrame's render function.
* Contains the resolved layout components and the shared component data.
*/
export interface PageFrameProps {
/** Component data shared across all components on the page */
componentData: QuartzComponentProps
/** The Head component (rendered in <head>) — NOT used by frames, included for completeness */
head: QuartzComponent
/** Header slot components (rendered inside <header>) */
header: QuartzComponent[]
/** Components rendered before the page body */
beforeBody: QuartzComponent[]
/** The page body component (Content) */
pageBody: QuartzComponent
/** Components rendered after the page body */
afterBody: QuartzComponent[]
/** Left sidebar components */
left: QuartzComponent[]
/** Right sidebar components */
right: QuartzComponent[]
/** Footer component */
footer: QuartzComponent
}
/**
* A PageFrame defines the inner HTML structure of a page inside the
* `<div id="quartz-root">` shell. Different frames can produce completely
* different layouts (e.g. with/without sidebars, horizontal scroll, etc.)
* while the outer shell (html, head, body, quartz-root) remains stable
* for SPA navigation.
*/
export interface PageFrame {
/** Unique name for this frame (e.g. "default", "full-width", "minimal") */
name: string
/** Render the inner page structure. Returns a JSX tree to be placed inside Body > #quartz-body. */
render: (props: PageFrameProps) => JSX.Element
/** Optional CSS string to include when this frame is active */
css?: string
}
@@ -0,0 +1,14 @@
import NotFound from "./pages/404"
import Head from "./Head"
import Spacer from "./Spacer"
import DesktopOnly from "./DesktopOnly"
import MobileOnly from "./MobileOnly"
import Flex from "./Flex"
import ConditionalRender from "./ConditionalRender"
export { componentRegistry, defineComponent } from "./registry"
export { External } from "./external"
export type { ComponentManifest, RegisteredComponent } from "./registry"
export type { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
export { Head, Spacer, DesktopOnly, MobileOnly, NotFound, Flex, ConditionalRender }
@@ -0,0 +1,54 @@
import { i18n } from "../../i18n"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
const NotFound: QuartzComponent = ({ cfg, ctx }: QuartzComponentProps) => {
const url = new URL(`https://${cfg.baseUrl ?? "example.com"}`)
const baseDir = ctx.argv.serve ? "/" : url.pathname
return (
<article class="popover-hint">
<h1>404</h1>
<p>{i18n(cfg.locale).pages.error.notFound}</p>
<a href={baseDir}>{i18n(cfg.locale).pages.error.home}</a>
<script
dangerouslySetInnerHTML={{
__html: `
if (typeof fetchData !== "undefined") {
fetchData.then(function(index) {
var basePath = document.body.dataset.basepath || "";
if (basePath.length > 1 && basePath.endsWith("/")) {
basePath = basePath.slice(0, -1);
}
var pathname = window.location.pathname;
var hasBasePrefix = basePath.length > 1 && pathname.startsWith(basePath);
if (hasBasePrefix) {
pathname = pathname.slice(basePath.length);
}
if (pathname.startsWith("/")) {
pathname = pathname.slice(1);
}
if (pathname.endsWith("/")) {
pathname = pathname.slice(0, -1);
}
if (pathname.endsWith(".html")) {
pathname = pathname.slice(0, -5);
}
if (pathname.endsWith("/index")) {
pathname = pathname.slice(0, -6);
}
var lowered = pathname.toLowerCase();
if (lowered !== pathname && index[lowered] != null) {
var prefix = hasBasePrefix ? basePath : "";
var target = prefix + (prefix.endsWith("/") ? "" : "/") + lowered;
window.location.replace(target);
}
});
}
`,
}}
/>
</article>
)
}
export default (() => NotFound) satisfies QuartzComponentConstructor
@@ -0,0 +1,132 @@
import { QuartzComponent, QuartzComponentConstructor } from "./types"
export interface ComponentManifest {
name: string
displayName: string
description: string
version: string
quartzVersion?: string
author?: string
homepage?: string
}
export interface RegisteredComponent {
component: QuartzComponent | QuartzComponentConstructor
source: string
manifest?: ComponentManifest
}
class ComponentRegistry {
private components = new Map<string, RegisteredComponent>()
private instanceCache = new Map<string, QuartzComponent>()
private optionOverrides = new Map<string, Record<string, unknown>>()
register(
name: string,
component: QuartzComponent | QuartzComponentConstructor,
source: string,
manifest?: ComponentManifest,
): void {
const existing = this.components.get(name)
if (existing && existing.source !== source) {
console.warn(`Component "${name}" is being overwritten by ${source}`)
}
this.components.set(name, { component, source, manifest })
}
get(name: string): RegisteredComponent | undefined {
return this.components.get(name)
}
getAll(): Map<string, RegisteredComponent> {
return new Map(this.components)
}
/** Store option overrides for a plugin, keyed by plugin directory name. */
setOptionOverrides(pluginName: string, opts?: Record<string, unknown>): void {
if (!opts || Object.keys(opts).length === 0) return
this.optionOverrides.set(pluginName, { ...this.optionOverrides.get(pluginName), ...opts })
this.instanceCache.clear()
}
getOptionOverrides(pluginName: string): Record<string, unknown> | undefined {
return this.optionOverrides.get(pluginName)
}
/**
* Instantiate a component constructor with options, returning a cached instance
* if the same constructor was already called with equivalent options.
* This prevents duplicate afterDOMLoaded scripts when the same component
* appears in multiple page-type layouts.
*/
instantiate(
constructor: QuartzComponentConstructor<any>,
options?: Record<string, unknown>,
): QuartzComponent {
const optsKey = options !== undefined ? JSON.stringify(options) : ""
// Use constructor identity + serialized options as cache key
// We store constructor name as a hint but rely on a unique id for identity
const ctorId =
(constructor as unknown as { __cacheId?: string }).__cacheId ??
((constructor as unknown as { __cacheId: string }).__cacheId =
`ctor_${this.instanceCache.size}`)
const cacheKey = `${ctorId}:${optsKey}`
const cached = this.instanceCache.get(cacheKey)
if (cached) return cached
const instance = constructor(options)
this.instanceCache.set(cacheKey, instance)
return instance
}
getAllComponents(): QuartzComponent[] {
// Deduplicate by component reference (same constructor may be registered under multiple keys)
const seen = new Set<QuartzComponent | QuartzComponentConstructor>()
const results: QuartzComponent[] = []
for (const r of this.components.values()) {
if (seen.has(r.component)) continue
seen.add(r.component)
try {
let instance: QuartzComponent
if (typeof r.component === "function") {
// Check if this constructor was already instantiated (with any options).
// Re-instantiating with `undefined` when options were provided would create
// a duplicate instance with separate afterDOMLoaded scripts.
const existing = this.findCachedInstance(r.component as QuartzComponentConstructor)
instance =
existing ?? this.instantiate(r.component as QuartzComponentConstructor, undefined)
} else {
instance = r.component as QuartzComponent
}
if (instance) {
results.push(instance)
}
} catch {
// Skip components that fail to instantiate
}
}
return results
}
private findCachedInstance(
constructor: QuartzComponentConstructor<any>,
): QuartzComponent | undefined {
const ctorId = (constructor as unknown as { __cacheId?: string }).__cacheId
if (!ctorId) return undefined
for (const [key, instance] of this.instanceCache) {
if (key.startsWith(`${ctorId}:`)) return instance
}
return undefined
}
}
export const componentRegistry = new ComponentRegistry()
export function defineComponent<Options extends object | undefined = undefined>(
factory: QuartzComponentConstructor<Options>,
manifest: ComponentManifest,
): QuartzComponentConstructor<Options> {
;(factory as any).__quartzComponent = { manifest }
return factory
}
@@ -0,0 +1,327 @@
import test, { describe } from "node:test"
import assert from "node:assert"
import { renderTranscludes, pageResources } from "./renderPage"
import { Root, Element } from "hast"
import { FullSlug } from "../util/path"
import { GlobalConfiguration } from "../cfg"
import { QuartzComponentProps } from "./types"
import { StaticResources } from "../util/resources"
function makeTranscludeBlockquote(targetSlug: string, block?: string): Element {
return {
type: "element",
tagName: "blockquote",
properties: {
className: ["transclude"],
...(block ? { dataBlock: block } : {}),
},
children: [
{
type: "element",
tagName: "a",
properties: {
href: `./${targetSlug}`,
"data-slug": targetSlug,
className: ["transclude-inner"],
},
children: [{ type: "text", value: `Transclude of ${targetSlug}` }],
},
],
}
}
function makePageData(slug: string, htmlAst: Root, extra?: Record<string, unknown>) {
return {
slug: slug as FullSlug,
htmlAst,
frontmatter: { title: slug, tags: [] },
...extra,
} as unknown as QuartzComponentProps["allFiles"][number]
}
const cfg = { locale: "en-US" } as GlobalConfiguration
function makeComponentData(
allFiles: QuartzComponentProps["allFiles"],
): Pick<QuartzComponentProps, "allFiles" | "cfg"> {
return { allFiles, cfg } as unknown as QuartzComponentProps
}
describe("renderTranscludes", () => {
test("resolves a single page transclusion", () => {
const root: Root = {
type: "root",
children: [makeTranscludeBlockquote("target")],
}
const targetHtml: Root = {
type: "root",
children: [
{
type: "element",
tagName: "p",
properties: {},
children: [{ type: "text", value: "Target content" }],
},
],
}
const allFiles = [makePageData("target", targetHtml)]
const visited = new Set<FullSlug>(["current" as FullSlug])
renderTranscludes(
root,
cfg,
"current" as FullSlug,
makeComponentData(allFiles) as QuartzComponentProps,
visited,
)
const bq = root.children[0] as Element
const texts = JSON.stringify(bq.children)
assert.ok(texts.includes("Target content"), "transcluded content should be inlined")
})
test("allows the same page to be embedded twice as siblings", () => {
const root: Root = {
type: "root",
children: [makeTranscludeBlockquote("target"), makeTranscludeBlockquote("target")],
}
const targetHtml: Root = {
type: "root",
children: [
{
type: "element",
tagName: "p",
properties: {},
children: [{ type: "text", value: "Duplicated content" }],
},
],
}
const allFiles = [makePageData("target", targetHtml)]
const visited = new Set<FullSlug>(["current" as FullSlug])
renderTranscludes(
root,
cfg,
"current" as FullSlug,
makeComponentData(allFiles) as QuartzComponentProps,
visited,
)
const first = root.children[0] as Element
const second = root.children[1] as Element
const firstText = JSON.stringify(first.children)
const secondText = JSON.stringify(second.children)
assert.ok(firstText.includes("Duplicated content"), "first embed should resolve")
assert.ok(
secondText.includes("Duplicated content"),
"second embed should resolve, not be rejected as circular",
)
assert.ok(!secondText.includes("Circular transclusion"), "should not show circular warning")
})
test("allows different sections of the same page to be embedded", () => {
const root: Root = {
type: "root",
children: [
makeTranscludeBlockquote("target", "#intro"),
makeTranscludeBlockquote("target", "#details"),
],
}
const targetHtml: Root = {
type: "root",
children: [
{
type: "element",
tagName: "h2",
properties: { id: "intro" },
children: [{ type: "text", value: "Intro" }],
},
{
type: "element",
tagName: "p",
properties: {},
children: [{ type: "text", value: "Intro text" }],
},
{
type: "element",
tagName: "h2",
properties: { id: "details" },
children: [{ type: "text", value: "Details" }],
},
{
type: "element",
tagName: "p",
properties: {},
children: [{ type: "text", value: "Details text" }],
},
],
}
const allFiles = [makePageData("target", targetHtml)]
const visited = new Set<FullSlug>(["current" as FullSlug])
renderTranscludes(
root,
cfg,
"current" as FullSlug,
makeComponentData(allFiles) as QuartzComponentProps,
visited,
)
const first = root.children[0] as Element
const second = root.children[1] as Element
const firstText = JSON.stringify(first.children)
const secondText = JSON.stringify(second.children)
assert.ok(firstText.includes("Intro text"), "first header section should resolve")
assert.ok(
!firstText.includes("Details text"),
"first section should not include second section",
)
assert.ok(secondText.includes("Details text"), "second header section should resolve")
assert.ok(!secondText.includes("Circular transclusion"), "should not show circular warning")
})
test("detects actual circular transclusion (A -> B -> A)", () => {
// Page A embeds B, and B's htmlAst contains a transclusion of A
const bTranscludesA = makeTranscludeBlockquote("pageA")
const pageB_htmlAst: Root = {
type: "root",
children: [
{
type: "element",
tagName: "p",
properties: {},
children: [{ type: "text", value: "Page B content" }],
},
bTranscludesA,
],
}
const pageA_htmlAst: Root = {
type: "root",
children: [
{
type: "element",
tagName: "p",
properties: {},
children: [{ type: "text", value: "Page A content" }],
},
],
}
const root: Root = {
type: "root",
children: [makeTranscludeBlockquote("pageB")],
}
const allFiles = [makePageData("pageA", pageA_htmlAst), makePageData("pageB", pageB_htmlAst)]
const visited = new Set<FullSlug>(["pageA" as FullSlug])
renderTranscludes(
root,
cfg,
"pageA" as FullSlug,
makeComponentData(allFiles) as QuartzComponentProps,
visited,
)
const bq = root.children[0] as Element
const fullText = JSON.stringify(bq.children)
assert.ok(fullText.includes("Page B content"), "page B content should be inlined")
assert.ok(fullText.includes("Circular transclusion"), "circular A->B->A should be detected")
assert.ok(!fullText.includes("Page A content"), "page A should not be re-inlined inside B")
})
test("self-referencing transclusion is blocked", () => {
const root: Root = {
type: "root",
children: [makeTranscludeBlockquote("self")],
}
const selfHtml: Root = {
type: "root",
children: [
{
type: "element",
tagName: "p",
properties: {},
children: [{ type: "text", value: "Self content" }],
},
],
}
const allFiles = [makePageData("self", selfHtml)]
const visited = new Set<FullSlug>(["self" as FullSlug])
renderTranscludes(
root,
cfg,
"self" as FullSlug,
makeComponentData(allFiles) as QuartzComponentProps,
visited,
)
const bq = root.children[0] as Element
const text = JSON.stringify(bq.children)
assert.ok(text.includes("Circular transclusion"), "self-reference should be blocked")
})
})
describe("pageResources", () => {
const emptyResources: StaticResources = {
css: [],
js: [],
additionalHead: [],
}
test("uses baseDir prefix for resource paths in production mode", () => {
const result = pageResources("/quartz" as FullSlug, emptyResources)
assert.ok(
result.css[0].content.startsWith("/quartz/"),
`expected css path to start with /quartz/, got: ${result.css[0].content}`,
)
const externalJs = result.js.find((j) => j.contentType === "external" && "src" in j)
assert.ok(externalJs && "src" in externalJs)
assert.ok(
externalJs.src.startsWith("/quartz/"),
`expected js src to start with /quartz/, got: ${externalJs.src}`,
)
})
test("omits subpath prefix when baseDir is empty (serve mode)", () => {
const result = pageResources("." as FullSlug, emptyResources)
for (const css of result.css) {
assert.ok(
!css.content.includes("/quartz/"),
`css path should not contain /quartz/, got: ${css.content}`,
)
}
for (const js of result.js) {
if (js.contentType === "external" && "src" in js) {
assert.ok(
!js.src.includes("/quartz/"),
`js src should not contain /quartz/, got: ${js.src}`,
)
}
}
})
test("contentIndex path reflects baseDir", () => {
const withPrefix = pageResources("/quartz" as FullSlug, emptyResources)
const inlineJs = withPrefix.js.find((j) => j.contentType === "inline" && "script" in j)
assert.ok(inlineJs && "script" in inlineJs)
assert.ok(
inlineJs.script.includes("/quartz/static/contentIndex.json"),
`expected contentIndex fetch to include /quartz/ prefix, got: ${inlineJs.script}`,
)
const withoutPrefix = pageResources("." as FullSlug, emptyResources)
const inlineJsServe = withoutPrefix.js.find((j) => j.contentType === "inline" && "script" in j)
assert.ok(inlineJsServe && "script" in inlineJsServe)
assert.ok(
!inlineJsServe.script.includes("/quartz/static/contentIndex.json"),
`expected contentIndex fetch without /quartz/ prefix in serve mode, got: ${inlineJsServe.script}`,
)
})
})
@@ -0,0 +1,376 @@
import { render } from "preact-render-to-string"
import { QuartzComponent, QuartzComponentProps } from "./types"
import BodyConstructor from "./Body"
import {
CSSResource,
JSResource,
JSResourceToScriptElement,
StaticResources,
} from "../util/resources"
import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path"
import { clone } from "../util/clone"
import { Root, Element, ElementContent } from "hast"
import { GlobalConfiguration } from "../cfg"
import { i18n } from "../i18n"
import { styleText } from "util"
import { resolveFrame } from "./frames"
import type { TreeTransform } from "../plugins/types"
import type { BuildCtx } from "../util/ctx"
interface RenderComponents {
head: QuartzComponent
header: QuartzComponent[]
beforeBody: QuartzComponent[]
pageBody: QuartzComponent
afterBody: QuartzComponent[]
left: QuartzComponent[]
right: QuartzComponent[]
footer: QuartzComponent
frame?: string
}
const headerRegex = new RegExp(/h[1-6]/)
export function pageResources(
baseDir: FullSlug | RelativeURL,
staticResources: StaticResources,
ctx?: BuildCtx,
): StaticResources {
const hashedNames = ctx?.hashedResourceNames
const cssFile = hashedNames?.["index.css"] ?? "index.css"
const prescriptFile = hashedNames?.["prescript.js"] ?? "prescript.js"
const postscriptFile = hashedNames?.["postscript.js"] ?? "postscript.js"
const componentCssResources: CSSResource[] = []
if (ctx?.componentCssMap) {
const seen = new Set<string>()
for (const filename of ctx.componentCssMap.values()) {
if (seen.has(filename)) continue
seen.add(filename)
componentCssResources.push({ content: joinSegments(baseDir, filename) })
}
}
const extracted = ctx?.extractedInlineResources
const resolvedCss: CSSResource[] = staticResources.css.map((resource) => {
if (!(resource.inline ?? false) || !extracted) return resource
const filename = extracted.get(resource.content)
if (!filename) return resource
return { content: joinSegments(baseDir, filename) }
})
const resolvedJs: JSResource[] = staticResources.js.map((resource) => {
if (resource.contentType !== "inline" || !extracted) return resource
const filename = extracted.get(resource.script)
if (!filename) return resource
return {
src: joinSegments(baseDir, filename),
loadTime: resource.loadTime,
contentType: "external" as const,
moduleType: resource.moduleType,
spaPreserve: resource.spaPreserve,
}
})
const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())`
const resources: StaticResources = {
css: [
{
content: joinSegments(baseDir, cssFile),
},
...componentCssResources,
...resolvedCss,
],
js: [
{
src: joinSegments(baseDir, prescriptFile),
loadTime: "beforeDOMReady",
contentType: "external",
},
{
loadTime: "beforeDOMReady",
contentType: "inline",
spaPreserve: true,
script: contentIndexScript,
},
...resolvedJs,
],
additionalHead: staticResources.additionalHead,
}
resources.js.push({
src: joinSegments(baseDir, postscriptFile),
loadTime: "afterDOMReady",
moduleType: "module",
contentType: "external",
})
return resources
}
/** @internal Exported for testing only. */
export function renderTranscludes(
root: Root,
cfg: GlobalConfiguration,
slug: FullSlug,
componentData: QuartzComponentProps,
visited: Set<FullSlug>,
) {
// Walk the tree manually instead of using visit() so we can track the
// ancestor chain for cycle detection. visit() runs the callback before
// descending into replaced children, so a Set-based guard there falsely
// rejects sibling transclusions of the same target.
function walk(node: Element | Root) {
const children = (node as Root).children ?? []
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child?.type !== "element") continue
const el = child as Element
if (el.tagName !== "blockquote") {
walk(el)
continue
}
const classNames = (el.properties?.className ?? []) as string[]
if (!classNames.includes("transclude")) {
walk(el)
continue
}
const inner = el.children[0] as Element
const transcludeTarget = (inner.properties["data-slug"] ?? slug) as FullSlug
if (visited.has(transcludeTarget)) {
console.warn(
styleText(
"yellow",
`Warning: Skipping circular transclusion: ${slug} -> ${transcludeTarget}`,
),
)
el.children = [
{
type: "element",
tagName: "p",
properties: { style: "color: var(--secondary);" },
children: [
{
type: "text",
value: `Circular transclusion detected: ${transcludeTarget}`,
},
],
},
]
continue
}
visited.add(transcludeTarget)
let page = componentData.allFiles.find((f) => f.slug === transcludeTarget)
if (!page) {
const dotIdx = transcludeTarget.lastIndexOf(".")
const slashIdx = transcludeTarget.lastIndexOf("/")
if (dotIdx > slashIdx + 1) {
const stripped = transcludeTarget.slice(0, dotIdx) as FullSlug
page = componentData.allFiles.findLast((f) => f.slug === stripped)
}
}
if (!page) {
visited.delete(transcludeTarget)
continue
}
let blockRef = el.properties.dataBlock as string | undefined
if (blockRef?.startsWith("#^")) {
// block transclude
blockRef = blockRef.slice("#^".length)
let blockNode = page.blocks?.[blockRef]
if (blockNode) {
if (blockNode.tagName === "li") {
blockNode = {
type: "element",
tagName: "ul",
properties: {},
children: [blockNode],
}
}
el.children = [
normalizeHastElement(blockNode, slug, transcludeTarget),
{
type: "element",
tagName: "a",
properties: {
href: inner.properties?.href,
class: ["internal", "internal-link", "transclude-src"],
},
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
}
} else if (blockRef?.startsWith("#") && page.htmlAst) {
// header transclude
blockRef = blockRef.slice(1)
let startIdx = undefined
let startDepth = undefined
let endIdx = undefined
for (const [i, htmlEl] of page.htmlAst.children.entries()) {
if (!(htmlEl.type === "element" && htmlEl.tagName.match(headerRegex))) continue
const depth = Number(htmlEl.tagName.substring(1))
if (startIdx === undefined || startDepth === undefined) {
if (htmlEl.properties?.id === blockRef) {
startIdx = i
startDepth = depth
}
} else if (depth <= startDepth) {
endIdx = i
break
}
}
if (startIdx === undefined) {
visited.delete(transcludeTarget)
continue
}
el.children = [
...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((c) =>
normalizeHastElement(c as Element, slug, transcludeTarget),
),
{
type: "element",
tagName: "a",
properties: {
href: inner.properties?.href,
class: ["internal", "internal-link", "transclude-src"],
},
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
} else if (page.htmlAst) {
// page transclude
el.children = [
{
type: "element",
tagName: "h1",
properties: {},
children: [
{
type: "text",
value:
page.frontmatter?.title ??
i18n(cfg.locale).components.transcludes.transcludeOf({
targetSlug: page.slug!,
}),
},
],
},
...(page.htmlAst.children as ElementContent[]).map((c) =>
normalizeHastElement(c as Element, slug, transcludeTarget),
),
{
type: "element",
tagName: "a",
properties: {
href: inner.properties?.href,
class: ["internal", "internal-link", "transclude-src"],
},
children: [
{ type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal },
],
},
]
}
// Recurse into the replaced children to resolve nested transclusions,
// then remove from visited so sibling embeds of the same target work.
walk(el)
visited.delete(transcludeTarget)
}
}
walk(root)
}
export function renderPage(
cfg: GlobalConfiguration,
slug: FullSlug,
componentData: QuartzComponentProps,
components: RenderComponents,
pageResources: StaticResources,
treeTransforms?: TreeTransform[],
): string {
// make a deep copy of the tree so we don't remove the transclusion references
// for the file cached in contentMap in build.ts
const root = clone(componentData.tree) as Root
const visited = new Set<FullSlug>([slug])
renderTranscludes(root, cfg, slug, componentData, visited)
// Run plugin-provided tree transforms (e.g. resolving inline bases codeblocks)
if (treeTransforms) {
for (const transform of treeTransforms) {
transform(root, slug, componentData)
}
}
// set componentData.tree to the edited html that has transclusions rendered
componentData.tree = root
const {
head: Head,
header,
beforeBody,
pageBody: Content,
afterBody,
left,
right,
footer: Footer,
frame: frameName,
} = components
const Body = BodyConstructor()
const frame = resolveFrame(frameName)
const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en"
const direction = i18n(cfg.locale).direction ?? "ltr"
// During local dev (--serve), the dev server serves from root without the
// baseUrl subpath, so basePath must be empty to avoid broken links.
const basePath =
componentData.ctx.argv.serve || !cfg.baseUrl
? ""
: new URL(`https://${cfg.baseUrl}`).pathname.replace(/\/$/, "")
const doc = (
<html lang={lang} dir={direction}>
<Head {...componentData} />
<body data-slug={slug} data-basepath={basePath}>
{frame.css && <style dangerouslySetInnerHTML={{ __html: frame.css }} />}
<div id="quartz-root" class="page" data-frame={frame.name}>
<Body {...componentData}>
{[
frame.render({
componentData,
head: Head,
header,
beforeBody,
pageBody: Content,
afterBody,
left,
right,
footer: Footer,
}),
]}
</Body>
</div>
</body>
{pageResources.js
.filter((resource) => resource.loadTime === "afterDOMReady")
.map((res) => JSResourceToScriptElement(res, true))}
</html>
)
return "<!DOCTYPE html>\n" + render(doc)
}
@@ -0,0 +1,141 @@
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
import { normalizeRelativeURLs } from "../../util/path"
import { fetchCanonical } from "./util"
const p = new DOMParser()
let activeAnchor: HTMLAnchorElement | null = null
async function mouseEnterHandler(
this: HTMLAnchorElement,
{ clientX, clientY }: { clientX: number; clientY: number },
) {
const link = (activeAnchor = this)
if (link.dataset.noPopover === "true") {
return
}
async function setPosition(popoverElement: HTMLElement) {
const { x, y } = await computePosition(link, popoverElement, {
strategy: "fixed",
middleware: [inline({ x: clientX, y: clientY }), shift(), flip()],
})
Object.assign(popoverElement.style, {
transform: `translate(${x.toFixed()}px, ${y.toFixed()}px)`,
})
}
function showPopover(popoverElement: HTMLElement) {
clearActivePopover()
popoverElement.classList.add("active-popover")
setPosition(popoverElement as HTMLElement)
if (hash !== "") {
const inner = popoverElement.querySelector(".popover-inner") as HTMLElement | null
if (inner) {
const targetAnchor = `#popover-internal-${hash.slice(1)}`
const heading = inner.querySelector(targetAnchor) as HTMLElement | null
if (heading) {
// leave ~12px of buffer when scrolling to a heading
inner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
}
}
}
}
const targetUrl = new URL(link.href)
const hash = decodeURIComponent(targetUrl.hash)
targetUrl.hash = ""
targetUrl.search = ""
const popoverId = `popover-${link.pathname}`
const prevPopoverElement = document.getElementById(popoverId)
// dont refetch if there's already a popover
if (!!document.getElementById(popoverId)) {
showPopover(prevPopoverElement as HTMLElement)
return
}
const response = await fetchCanonical(targetUrl).catch((err) => {
console.error(err)
})
if (!response) return
const rawContentType = response.headers.get("Content-Type")
if (!rawContentType) return
const [contentType] = rawContentType.split(";")
const [contentTypeCategory, typeInfo] = contentType.split("/")
const popoverElement = document.createElement("div")
popoverElement.id = popoverId
popoverElement.classList.add("popover")
const popoverInner = document.createElement("div")
popoverInner.classList.add("popover-inner")
popoverInner.dataset.contentType = contentType ?? undefined
popoverElement.appendChild(popoverInner)
switch (contentTypeCategory) {
case "image":
const img = document.createElement("img")
img.src = targetUrl.toString()
img.alt = targetUrl.pathname
popoverInner.appendChild(img)
break
case "application":
switch (typeInfo) {
case "pdf":
const pdf = document.createElement("iframe")
pdf.src = targetUrl.toString()
popoverInner.appendChild(pdf)
break
default:
break
}
break
default:
const contents = await response.text()
const html = p.parseFromString(contents, "text/html")
normalizeRelativeURLs(html, targetUrl)
// prepend all IDs inside popovers to prevent duplicates
html.querySelectorAll("[id]").forEach((el) => {
const targetID = `popover-internal-${el.id}`
el.id = targetID
})
const elts = [...html.getElementsByClassName("popover-hint")]
if (elts.length === 0) return
elts.forEach((elt) => popoverInner.appendChild(elt))
}
if (!!document.getElementById(popoverId)) {
return
}
document.body.appendChild(popoverElement)
if (activeAnchor !== this) {
return
}
showPopover(popoverElement)
}
function clearActivePopover() {
activeAnchor = null
const allPopoverElements = document.querySelectorAll(".popover")
allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover"))
}
function setupPopovers() {
const links = [...document.querySelectorAll("a.internal")] as HTMLAnchorElement[]
for (const link of links) {
link.addEventListener("mouseenter", mouseEnterHandler)
link.addEventListener("mouseleave", clearActivePopover)
window.addCleanup(() => {
link.removeEventListener("mouseenter", mouseEnterHandler)
link.removeEventListener("mouseleave", clearActivePopover)
})
}
}
document.addEventListener("nav", setupPopovers)
document.addEventListener("render", setupPopovers)
@@ -0,0 +1,185 @@
import test, { describe } from "node:test"
import assert from "node:assert"
type ScrollArg = { top: number; behavior?: ScrollBehavior }
type FakeHeading = { offsetTop: number }
interface FakePopoverInner {
scroll: (arg: ScrollArg) => void
querySelector: (sel: string) => FakeHeading | null
_scrolled: ScrollArg | null
_selectorsQueried: string[]
}
interface FakePopoverElement {
classList: {
add: (cls: string) => void
remove: (cls: string) => void
_added: string[]
_removed: string[]
}
style: Record<string, string>
querySelector: (sel: string) => FakePopoverInner | null
_inner: FakePopoverInner
}
function makeInner(heading: FakeHeading | null = null): FakePopoverInner {
const inner: FakePopoverInner = {
scroll(arg) {
this._scrolled = arg
},
querySelector(sel) {
this._selectorsQueried.push(sel)
return heading
},
_scrolled: null,
_selectorsQueried: [],
}
return inner
}
function makePopoverElement(inner: FakePopoverInner): FakePopoverElement {
const added: string[] = []
const removed: string[] = []
return {
classList: {
add(cls) {
added.push(cls)
},
remove(cls) {
removed.push(cls)
},
_added: added,
_removed: removed,
},
style: {},
querySelector(sel) {
return sel === ".popover-inner" ? inner : null
},
_inner: inner,
}
}
type SetPosition = (el: FakePopoverElement) => Promise<void>
function fixedShowPopover(
popoverElement: FakePopoverElement,
hash: string,
setPosition: SetPosition,
): Promise<void> {
popoverElement.classList.add("active-popover")
const positionResult = setPosition(popoverElement)
if (hash !== "") {
const inner = popoverElement.querySelector(".popover-inner")
if (inner) {
const targetAnchor = `#popover-internal-${hash.slice(1)}`
const heading = inner.querySelector(targetAnchor)
if (heading) {
inner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
}
}
}
return positionResult
}
describe("showPopover on cache-hit with hash", () => {
test("does not reference any lexical popoverInner from an outer scope", async () => {
const heading: FakeHeading = { offsetTop: 200 }
const inner = makeInner(heading)
const popoverElement = makePopoverElement(inner)
await fixedShowPopover(popoverElement, "#plugins", async () => {})
assert.ok(
popoverElement.classList._added.includes("active-popover"),
"active-popover class must be applied",
)
assert.deepStrictEqual(
inner._scrolled,
{ top: 200 - 12, behavior: "instant" },
"scroll must target heading.offsetTop - 12",
)
assert.deepStrictEqual(inner._selectorsQueried, ["#popover-internal-plugins"])
})
test("skips scroll when hash is empty", async () => {
const inner = makeInner({ offsetTop: 123 })
const popoverElement = makePopoverElement(inner)
await fixedShowPopover(popoverElement, "", async () => {})
assert.strictEqual(inner._scrolled, null)
assert.deepStrictEqual(inner._selectorsQueried, [])
})
test("skips scroll when heading is not found", async () => {
const inner = makeInner(null)
const popoverElement = makePopoverElement(inner)
await fixedShowPopover(popoverElement, "#nonexistent", async () => {})
assert.strictEqual(inner._scrolled, null)
assert.deepStrictEqual(inner._selectorsQueried, ["#popover-internal-nonexistent"])
})
test("decodes percent-encoded fragments when building the selector", async () => {
const heading: FakeHeading = { offsetTop: 50 }
const inner = makeInner(heading)
const popoverElement = makePopoverElement(inner)
await fixedShowPopover(popoverElement, "#a-b", async () => {})
assert.deepStrictEqual(inner._selectorsQueried, ["#popover-internal-a-b"])
})
})
describe("buggy showPopover (lexical-capture pattern) regression guard", () => {
test("accessing a capture-before-declaration variable throws ReferenceError (TDZ simulation)", () => {
function simulateBuggyMouseEnter(hash: string) {
function buggyShowPopover(popoverElement: FakePopoverElement) {
popoverElement.classList.add("active-popover")
if (hash !== "") {
const targetAnchor = `#popover-internal-${hash.slice(1)}`
const heading = popoverInner.querySelector(targetAnchor)
if (heading) {
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" })
}
}
}
const cachedInner = makeInner({ offsetTop: 999 })
const cachedElement = makePopoverElement(cachedInner)
buggyShowPopover(cachedElement)
const popoverInner = makeInner(null)
return popoverInner
}
assert.throws(() => simulateBuggyMouseEnter("#plugins"), {
name: "ReferenceError",
})
})
test("same pattern does NOT throw when hash is empty (explains why first link without fragment works)", () => {
function simulateBuggyMouseEnter(hash: string) {
function buggyShowPopover(popoverElement: FakePopoverElement) {
popoverElement.classList.add("active-popover")
if (hash !== "") {
const _unused = popoverInner.querySelector("x")
void _unused
}
}
const cachedInner = makeInner(null)
const cachedElement = makePopoverElement(cachedInner)
buggyShowPopover(cachedElement)
const popoverInner = makeInner(null)
return popoverInner
}
assert.doesNotThrow(() => simulateBuggyMouseEnter(""))
})
})
@@ -0,0 +1,163 @@
import test, { describe } from "node:test"
import assert from "node:assert"
// Inline the encoder function from search.inline.ts for testing
const encoder = (str: string): string[] => {
const tokens: string[] = []
let bufferStart = -1
let bufferEnd = -1
const lower = str.toLowerCase()
let i = 0
for (const char of lower) {
const code = char.codePointAt(0)!
const isCJK =
(code >= 0x3040 && code <= 0x309f) ||
(code >= 0x30a0 && code <= 0x30ff) ||
(code >= 0x4e00 && code <= 0x9fff) ||
(code >= 0xac00 && code <= 0xd7af) ||
(code >= 0x20000 && code <= 0x2a6df)
const isWhitespace = code === 32 || code === 9 || code === 10 || code === 13
if (isCJK) {
if (bufferStart !== -1) {
tokens.push(lower.slice(bufferStart, bufferEnd))
bufferStart = -1
}
tokens.push(char)
} else if (isWhitespace) {
if (bufferStart !== -1) {
tokens.push(lower.slice(bufferStart, bufferEnd))
bufferStart = -1
}
} else {
if (bufferStart === -1) bufferStart = i
bufferEnd = i + char.length
}
i += char.length
}
if (bufferStart !== -1) {
tokens.push(lower.slice(bufferStart))
}
return tokens
}
describe("search encoder", () => {
describe("English text", () => {
test("should tokenize simple English words", () => {
const result = encoder("hello world")
assert.deepStrictEqual(result, ["hello", "world"])
})
test("should handle multiple spaces", () => {
const result = encoder("hello world")
assert.deepStrictEqual(result, ["hello", "world"])
})
test("should handle tabs and newlines", () => {
const result = encoder("hello\tworld\ntest")
assert.deepStrictEqual(result, ["hello", "world", "test"])
})
test("should lowercase all text", () => {
const result = encoder("Hello WORLD Test")
assert.deepStrictEqual(result, ["hello", "world", "test"])
})
})
describe("CJK text", () => {
test("should tokenize Japanese Hiragana character by character", () => {
const result = encoder("こんにちは")
assert.deepStrictEqual(result, ["こ", "ん", "に", "ち", "は"])
})
test("should tokenize Japanese Katakana character by character", () => {
const result = encoder("コントロール")
assert.deepStrictEqual(result, ["コ", "ン", "ト", "ロ", "ー", "ル"])
})
test("should tokenize Japanese Kanji character by character", () => {
const result = encoder("日本語")
assert.deepStrictEqual(result, ["日", "本", "語"])
})
test("should tokenize Korean Hangul character by character", () => {
const result = encoder("안녕하세요")
assert.deepStrictEqual(result, ["안", "녕", "하", "세", "요"])
})
test("should tokenize Chinese characters character by character", () => {
const result = encoder("你好世界")
assert.deepStrictEqual(result, ["你", "好", "世", "界"])
})
test("should handle mixed Hiragana/Katakana/Kanji", () => {
const result = encoder("て以来")
assert.deepStrictEqual(result, ["て", "以", "来"])
})
})
describe("Mixed CJK and English", () => {
test("should handle Japanese with English words", () => {
const result = encoder("hello 世界")
assert.deepStrictEqual(result, ["hello", "世", "界"])
})
test("should handle English with Japanese words", () => {
const result = encoder("世界 hello world")
assert.deepStrictEqual(result, ["世", "界", "hello", "world"])
})
test("should handle complex mixed content", () => {
const result = encoder("これはtest文章です")
assert.deepStrictEqual(result, ["こ", "れ", "は", "test", "文", "章", "で", "す"])
})
test("should handle mixed Korean and English", () => {
const result = encoder("hello 안녕 world")
assert.deepStrictEqual(result, ["hello", "안", "녕", "world"])
})
test("should handle mixed Chinese and English", () => {
const result = encoder("你好 world")
assert.deepStrictEqual(result, ["你", "好", "world"])
})
})
describe("Edge cases", () => {
test("should handle empty string", () => {
const result = encoder("")
assert.deepStrictEqual(result, [])
})
test("should handle only whitespace", () => {
const result = encoder(" \t\n ")
assert.deepStrictEqual(result, [])
})
test("should handle single character", () => {
const result = encoder("a")
assert.deepStrictEqual(result, ["a"])
})
test("should handle single CJK character", () => {
const result = encoder("あ")
assert.deepStrictEqual(result, ["あ"])
})
test("should handle CJK with trailing whitespace", () => {
const result = encoder("日本語 ")
assert.deepStrictEqual(result, ["日", "本", "語"])
})
test("should handle English with trailing whitespace", () => {
const result = encoder("hello ")
assert.deepStrictEqual(result, ["hello"])
})
})
})
@@ -0,0 +1,221 @@
import micromorph from "micromorph"
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
import { fetchCanonical } from "./util"
// adapted from `micromorph`
// https://github.com/natemoo-re/micromorph
const NODE_TYPE_ELEMENT = 1
let announcer = document.createElement("route-announcer")
const isElement = (target: EventTarget | null): target is Element =>
(target as Node)?.nodeType === NODE_TYPE_ELEMENT
const isLocalUrl = (href: string) => {
try {
const url = new URL(href)
if (window.location.origin === url.origin) {
return true
}
} catch (e) {}
return false
}
const isSamePage = (url: URL): boolean => {
const sameOrigin = url.origin === window.location.origin
const samePath = url.pathname === window.location.pathname
return sameOrigin && samePath
}
const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined => {
if (!isElement(target)) return
if (target.attributes.getNamedItem("target")?.value === "_blank") return
const a = target.closest("a")
if (!a) return
if ("routerIgnore" in a.dataset) return
const { href } = a
if (!isLocalUrl(href)) return
return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined }
}
function notifyNav(url: FullSlug) {
const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } })
document.dispatchEvent(event)
}
const cleanupFns: Set<(...args: any[]) => void> = new Set()
window.addCleanup = (fn) => cleanupFns.add(fn)
function startLoading() {
document.querySelector(".navigation-progress")?.remove()
const loadingBar = document.createElement("div")
loadingBar.className = "navigation-progress"
loadingBar.style.width = "0"
document.body.prepend(loadingBar)
setTimeout(() => {
loadingBar.style.width = "80%"
}, 100)
}
function stopLoading() {
const loadingBar = document.querySelector(".navigation-progress")
if (loadingBar) {
loadingBar.remove()
}
}
let isNavigating = false
let p: DOMParser
async function _navigate(url: URL, isBack: boolean = false) {
isNavigating = true
startLoading()
p = p || new DOMParser()
const contents = await fetchCanonical(url)
.then((res) => {
const contentType = res.headers.get("content-type")
if (contentType?.startsWith("text/html")) {
return res.text()
} else {
window.location.assign(url)
}
})
.catch(() => {
window.location.assign(url)
})
if (!contents) return
// notify about to nav
const event: CustomEventMap["prenav"] = new CustomEvent("prenav", { detail: {} })
document.dispatchEvent(event)
// cleanup old
cleanupFns.forEach((fn) => fn())
cleanupFns.clear()
const html = p.parseFromString(contents, "text/html")
normalizeRelativeURLs(html, url)
let title = html.querySelector("title")?.textContent
if (title) {
document.title = title
} else {
const h1 = document.querySelector("h1")
title = h1?.innerText ?? h1?.textContent ?? url.pathname
}
if (announcer.textContent !== title) {
announcer.textContent = title
}
announcer.dataset.persist = ""
html.body.appendChild(announcer)
document.querySelector(".navigation-progress")?.remove()
micromorph(document.body, html.body)
// scroll into place and add history
if (!isBack) {
if (url.hash) {
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
el?.scrollIntoView()
} else {
window.scrollTo({ top: 0 })
}
}
// now, patch head, re-executing scripts
const elementsToRemove = document.head.querySelectorAll(":not([data-persist])")
elementsToRemove.forEach((el) => el.remove())
const elementsToAdd = html.head.querySelectorAll(":not([data-persist])")
elementsToAdd.forEach((el) => document.head.appendChild(el))
// delay setting the url until now
// at this point everything is loaded so changing the url should resolve to the correct addresses
if (!isBack) {
history.pushState({}, "", url)
}
notifyNav(getFullSlug(window))
delete announcer.dataset.persist
}
async function navigate(url: URL, isBack: boolean = false) {
if (isNavigating) return
isNavigating = true
try {
await _navigate(url, isBack)
} catch (e) {
console.error(e)
window.location.assign(url)
} finally {
stopLoading()
isNavigating = false
}
}
window.spaNavigate = navigate
function createRouter() {
if (typeof window !== "undefined") {
window.addEventListener("click", async (event) => {
const { url } = getOpts(event) ?? {}
// dont hijack behaviour, just let browser act normally
if (!url || event.ctrlKey || event.metaKey) return
event.preventDefault()
if (isSamePage(url) && url.hash) {
const el = document.getElementById(decodeURIComponent(url.hash.substring(1)))
el?.scrollIntoView()
history.pushState({}, "", url)
return
}
navigate(url, false)
})
window.addEventListener("popstate", (event) => {
const { url } = getOpts(event) ?? {}
if (window.location.hash && window.location.pathname === url?.pathname) return
navigate(new URL(window.location.toString()), true)
return
})
}
return new (class Router {
go(pathname: RelativeURL) {
const url = new URL(pathname, window.location.toString())
return navigate(url, false)
}
back() {
return window.history.back()
}
forward() {
return window.history.forward()
}
})()
}
createRouter()
notifyNav(getFullSlug(window))
if (!customElements.get("route-announcer")) {
const attrs = {
"aria-live": "assertive",
"aria-atomic": "true",
style:
"position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px",
}
customElements.define(
"route-announcer",
class RouteAnnouncer extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
for (const [key, value] of Object.entries(attrs)) {
this.setAttribute(key, value)
}
}
},
)
}
@@ -0,0 +1,46 @@
export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) {
if (!outsideContainer) return
function click(this: HTMLElement, e: HTMLElementEventMap["click"]) {
if (e.target !== this) return
e.preventDefault()
e.stopPropagation()
cb()
}
function esc(e: HTMLElementEventMap["keydown"]) {
if (!e.key.startsWith("Esc")) return
e.preventDefault()
cb()
}
outsideContainer?.addEventListener("click", click)
window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
document.addEventListener("keydown", esc)
window.addCleanup(() => document.removeEventListener("keydown", esc))
}
export function removeAllChildren(node: HTMLElement) {
while (node.firstChild) {
node.removeChild(node.firstChild)
}
}
// AliasRedirect emits HTML redirects which also have the link[rel="canonical"]
// containing the URL it's redirecting to.
// Extracting it here with regex is _probably_ faster than parsing the entire HTML
// with a DOMParser effectively twice (here and later in the SPA code), even if
// way less robust - we only care about our own generated redirects after all.
const canonicalRegex = /<link rel="canonical" href="([^"]*)">/
export async function fetchCanonical(url: URL): Promise<Response> {
const res = await fetch(`${url}`)
if (!res.headers.get("content-type")?.startsWith("text/html")) {
return res
}
// reading the body can only be done once, so we need to clone the response
// to allow the caller to read it if it's was not a redirect
const text = await res.clone().text()
const [_, redirect] = text.match(canonicalRegex) ?? []
return redirect ? fetch(`${new URL(redirect, url)}`) : res
}
@@ -0,0 +1,89 @@
@use "../../styles/variables.scss" as *;
@keyframes dropin {
0% {
opacity: 0;
visibility: hidden;
}
1% {
opacity: 0;
}
100% {
opacity: 1;
visibility: visible;
}
}
.popover {
z-index: 999;
position: fixed;
overflow: visible;
padding: 1rem;
left: 0;
top: 0;
will-change: transform;
& > .popover-inner {
position: relative;
width: 30rem;
max-height: 20rem;
padding: 0 1rem 1rem 1rem;
font-weight: initial;
font-style: initial;
line-height: normal;
font-size: initial;
font-family: var(--bodyFont);
border: 1px solid var(--lightgray);
background-color: var(--light);
border-radius: 5px;
box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25);
overflow: auto;
overscroll-behavior: contain;
white-space: normal;
user-select: none;
cursor: default;
}
& > .popover-inner[data-content-type] {
&[data-content-type*="pdf"],
&[data-content-type*="image"] {
padding: 0;
max-height: 100%;
}
&[data-content-type*="image"] {
img {
margin: 0;
border-radius: 0;
display: block;
}
}
&[data-content-type*="pdf"] {
iframe {
width: 100%;
}
}
}
h1 {
font-size: 1.5rem;
}
visibility: hidden;
opacity: 0;
transition:
opacity 0.3s ease,
visibility 0.3s ease;
@media all and ($mobile) {
display: none !important;
}
}
.active-popover,
.popover:hover {
animation: dropin 0.3s ease;
animation-fill-mode: forwards;
animation-delay: 0.2s;
}
@@ -0,0 +1,30 @@
import { JSX } from "preact"
import { StaticResources, StringResource } from "../util/resources"
import { QuartzPluginData } from "../plugins/vfile"
import { GlobalConfiguration } from "../cfg"
import { Node } from "hast"
import { BuildCtx } from "../util/ctx"
export type QuartzComponentProps = {
ctx: BuildCtx
externalResources: StaticResources
fileData: QuartzPluginData
cfg: GlobalConfiguration
children: (QuartzComponent | JSX.Element)[]
tree: Node
allFiles: QuartzPluginData[]
displayClass?: "mobile-only" | "desktop-only"
} & JSX.IntrinsicAttributes & {
[key: string]: any
}
export type QuartzComponent = ((props: QuartzComponentProps) => any) & {
displayName?: string
css?: StringResource
beforeDOMLoaded?: StringResource
afterDOMLoaded?: StringResource
}
export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
opts: Options,
) => QuartzComponent