Jacky Zhao
2023-11-14 74777118a7fd19e4a296706c2a4b5fdca546c4fa
feat: header and full-page transcludes (closes #557)
2 files modified
89 ■■■■ changed files
quartz/components/renderPage.tsx 81 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/ofm.ts 8 ●●●●● patch | view | raw | blame | history
quartz/components/renderPage.tsx
@@ -5,7 +5,7 @@
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
import { FullSlug, RelativeURL, joinSegments } from "../util/path"
import { visit } from "unist-util-visit"
import { Root, Element } from "hast"
import { Root, Element, ElementContent } from "hast"
interface RenderComponents {
  head: QuartzComponent
@@ -61,22 +61,81 @@
      const classNames = (node.properties?.className ?? []) as string[]
      if (classNames.includes("transclude")) {
        const inner = node.children[0] as Element
        const blockSlug = inner.properties?.["data-slug"] as FullSlug
        const blockRef = node.properties!.dataBlock as string
        const transcludeTarget = inner.properties?.["data-slug"] as FullSlug
        // TODO: avoid this expensive find operation and construct an index ahead of time
        let blockNode = componentData.allFiles.find((f) => f.slug === blockSlug)?.blocks?.[blockRef]
        if (blockNode) {
          if (blockNode.tagName === "li") {
            blockNode = {
              type: "element",
              tagName: "ul",
              children: [blockNode],
        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(1)
          let blockNode = page.blocks?.[blockRef]
          if (blockNode) {
            if (blockNode.tagName === "li") {
              blockNode = {
                type: "element",
                tagName: "ul",
                children: [blockNode],
              }
            }
            node.children = [
              blockNode,
              {
                type: "element",
                tagName: "a",
                properties: { href: inner.properties?.href, class: ["internal"] },
                children: [{ type: "text", value: `Link to original` }],
              },
            ]
          }
        } else if (blockRef?.startsWith("#") && page.htmlAst) {
          // header transclude
          blockRef = blockRef.slice(1)
          let startIdx = undefined
          let endIdx = undefined
          for (const [i, el] of page.htmlAst.children.entries()) {
            if (el.type === "element" && el.tagName.match(/h[1-6]/)) {
              if (endIdx) {
                break
              }
              if (startIdx) {
                endIdx = i
              } else if (el.properties?.id === blockRef) {
                startIdx = i
              }
            }
          }
          if (!startIdx) {
            return
          }
          node.children = [
            blockNode,
            ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]),
            {
              type: "element",
              tagName: "a",
              properties: { href: inner.properties?.href, class: ["internal"] },
              children: [{ type: "text", value: `Link to original` }],
            },
          ]
        } else if (page.htmlAst) {
          // page transclude
          node.children = [
            {
              type: "element",
              tagName: "h1",
              children: [
                { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` },
              ],
            },
            ...(page.htmlAst.children as ElementContent[]),
            {
              type: "element",
              tagName: "a",
quartz/plugins/transformers/ofm.ts
@@ -1,7 +1,7 @@
import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types"
import { Root, HTML, BlockContent, DefinitionContent, Code, Paragraph } from "mdast"
import { Element, Literal } from "hast"
import { Element, Literal, Root as HtmlRoot } from "hast"
import { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { slug as slugAnchor } from "github-slugger"
import rehypeRaw from "rehype-raw"
@@ -236,13 +236,13 @@
                    value: `<iframe src="${url}"></iframe>`,
                  }
                } else if (ext === "") {
                  const block = anchor.slice(1)
                  const block = anchor
                  return {
                    type: "html",
                    data: { hProperties: { transclude: true } },
                    value: `<blockquote class="transclude" data-url="${url}" data-block="${block}"><a href="${
                      url + anchor
                    }" class="transclude-inner">Transclude of block ${block}</a></blockquote>`,
                    }" class="transclude-inner">Transclude of ${url}${block}</a></blockquote>`,
                  }
                }
@@ -436,6 +436,7 @@
          const blockTagTypes = new Set(["blockquote"])
          return (tree, file) => {
            file.data.blocks = {}
            file.data.htmlAst = tree
            visit(tree, "element", (node, index, parent) => {
              if (blockTagTypes.has(node.tagName)) {
@@ -524,5 +525,6 @@
declare module "vfile" {
  interface DataMap {
    blocks: Record<string, Element>
    htmlAst: HtmlRoot
  }
}