Created
October 31, 2025 20:57
-
-
Save mthomason/b3ea7f93f4536076f518a53d34d3e55f to your computer and use it in GitHub Desktop.
Detect circular transclusions in quartz.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { render } from "preact-render-to-string" | |
| import { QuartzComponent, QuartzComponentProps } from "./types" | |
| import HeaderConstructor from "./Header" | |
| import BodyConstructor from "./Body" | |
| import { JSResourceToScriptElement, StaticResources } from "../util/resources" | |
| import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" | |
| import { clone } from "../util/clone" | |
| import { visit } from "unist-util-visit" | |
| import { Root, Element, ElementContent } from "hast" | |
| import { GlobalConfiguration } from "../cfg" | |
| import { i18n } from "../i18n" | |
| interface RenderComponents { | |
| head: QuartzComponent | |
| header: QuartzComponent[] | |
| beforeBody: QuartzComponent[] | |
| pageBody: QuartzComponent | |
| afterBody: QuartzComponent[] | |
| left: QuartzComponent[] | |
| right: QuartzComponent[] | |
| footer: QuartzComponent | |
| } | |
| const headerRegex = new RegExp(/h[1-6]/) | |
| export function pageResources( | |
| baseDir: FullSlug | RelativeURL, | |
| staticResources: StaticResources, | |
| ): StaticResources { | |
| const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json") | |
| const contentIndexScript = `const fetchData = fetch("${contentIndexPath}").then(data => data.json())` | |
| const resources: StaticResources = { | |
| css: [ | |
| { | |
| content: joinSegments(baseDir, "index.css"), | |
| }, | |
| ...staticResources.css, | |
| ], | |
| js: [ | |
| { | |
| src: joinSegments(baseDir, "prescript.js"), | |
| loadTime: "beforeDOMReady", | |
| contentType: "external", | |
| }, | |
| { | |
| loadTime: "beforeDOMReady", | |
| contentType: "inline", | |
| spaPreserve: true, | |
| script: contentIndexScript, | |
| }, | |
| ...staticResources.js, | |
| ], | |
| additionalHead: staticResources.additionalHead, | |
| } | |
| resources.js.push({ | |
| src: joinSegments(baseDir, "postscript.js"), | |
| loadTime: "afterDOMReady", | |
| moduleType: "module", | |
| contentType: "external", | |
| }) | |
| return resources | |
| } | |
| function renderTranscludes( | |
| root: Root, | |
| cfg: GlobalConfiguration, | |
| slug: FullSlug, | |
| componentData: QuartzComponentProps, | |
| visited: Set<FullSlug> | |
| ) { | |
| // process transcludes in componentData | |
| visit(root, "element", (node, _index, _parent) => { | |
| if (node.tagName === "blockquote") { | |
| const classNames = (node.properties?.className ?? []) as string[] | |
| if (classNames.includes("transclude")) { | |
| const inner = node.children[0] as Element | |
| const transcludeTarget = (inner.properties["data-slug"] ?? slug) as FullSlug | |
| if (visited.has(transcludeTarget)) { | |
| console.warn( | |
| `[quartz] Circular transclusion detected in \`${slug}\` attempting to transclude \`${transcludeTarget}\`. Skipping.`, | |
| ) | |
| node.children = [ | |
| { | |
| type: "element", | |
| tagName: "p", | |
| properties: { | |
| style: "color: var(--secondary);", | |
| }, | |
| children: [ | |
| { | |
| type: "text", | |
| value: `Circular transclusion detected: ${transcludeTarget}`, | |
| }, | |
| ], | |
| }, | |
| ] | |
| return | |
| } | |
| visited.add(transcludeTarget) | |
| const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) | |
| if (!page) { | |
| return | |
| } | |
| let blockRef = node.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], | |
| } | |
| } | |
| node.children = [ | |
| normalizeHastElement(blockNode, slug, transcludeTarget), | |
| { | |
| type: "element", | |
| tagName: "a", | |
| properties: { href: inner.properties?.href, class: ["internal", "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, el] of page.htmlAst.children.entries()) { | |
| // skip non-headers | |
| if (!(el.type === "element" && el.tagName.match(headerRegex))) continue | |
| const depth = Number(el.tagName.substring(1)) | |
| // lookin for our blockref | |
| if (startIdx === undefined || startDepth === undefined) { | |
| // skip until we find the blockref that matches | |
| if (el.properties?.id === blockRef) { | |
| startIdx = i | |
| startDepth = depth | |
| } | |
| } else if (depth <= startDepth) { | |
| // looking for new header that is same level or higher | |
| endIdx = i | |
| break | |
| } | |
| } | |
| if (startIdx === undefined) { | |
| return | |
| } | |
| node.children = [ | |
| ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) => | |
| normalizeHastElement(child as Element, slug, transcludeTarget), | |
| ), | |
| { | |
| type: "element", | |
| tagName: "a", | |
| properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, | |
| children: [ | |
| { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, | |
| ], | |
| }, | |
| ] | |
| } else if (page.htmlAst) { | |
| // page transclude | |
| node.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((child) => | |
| normalizeHastElement(child as Element, slug, transcludeTarget), | |
| ), | |
| { | |
| type: "element", | |
| tagName: "a", | |
| properties: { href: inner.properties?.href, class: ["internal", "transclude-src"] }, | |
| children: [ | |
| { type: "text", value: i18n(cfg.locale).components.transcludes.linkToOriginal }, | |
| ], | |
| }, | |
| ] | |
| } | |
| } | |
| } | |
| }) | |
| } | |
| export function renderPage( | |
| cfg: GlobalConfiguration, | |
| slug: FullSlug, | |
| componentData: QuartzComponentProps, | |
| components: RenderComponents, | |
| pageResources: StaticResources, | |
| ): 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) | |
| // 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, | |
| } = components | |
| const Header = HeaderConstructor() | |
| const Body = BodyConstructor() | |
| const LeftComponent = ( | |
| <div class="left sidebar"> | |
| {left.map((BodyComponent) => ( | |
| <BodyComponent {...componentData} /> | |
| ))} | |
| </div> | |
| ) | |
| const RightComponent = ( | |
| <div class="right sidebar"> | |
| {right.map((BodyComponent) => ( | |
| <BodyComponent {...componentData} /> | |
| ))} | |
| </div> | |
| ) | |
| const lang = componentData.fileData.frontmatter?.lang ?? cfg.locale?.split("-")[0] ?? "en" | |
| const direction = i18n(cfg.locale).direction ?? "ltr" | |
| const doc = ( | |
| <html lang={lang} dir={direction}> | |
| <Head {...componentData} /> | |
| <body data-slug={slug}> | |
| <div id="quartz-root" class="page"> | |
| <Body {...componentData}> | |
| {LeftComponent} | |
| <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> | |
| {RightComponent} | |
| <Footer {...componentData} /> | |
| </Body> | |
| </div> | |
| </body> | |
| {pageResources.js | |
| .filter((resource) => resource.loadTime === "afterDOMReady") | |
| .map((res) => JSResourceToScriptElement(res))} | |
| </html> | |
| ) | |
| return "<!DOCTYPE html>\n" + render(doc) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment