Jacky Zhao
2023-09-13 cce389c81d262d1d2a2bd8140c879efd68e3c6dd
feat: note transclusion (#475)

* basic transclude

* feat: note transclusion
8 files modified
105 ■■■■ changed files
docs/features/wikilinks.md 4 ●●● patch | view | raw | blame | history
docs/index.md 2 ●●● patch | view | raw | blame | history
package-lock.json 4 ●●●● patch | view | raw | blame | history
package.json 2 ●●● patch | view | raw | blame | history
quartz/components/renderPage.tsx 36 ●●●●● patch | view | raw | blame | history
quartz/plugins/transformers/links.ts 1 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/ofm.ts 50 ●●●● patch | view | raw | blame | history
quartz/styles/base.scss 6 ●●●●● patch | view | raw | blame | history
docs/features/wikilinks.md
@@ -13,6 +13,4 @@
- `[[Path to file]]`: produces a link to `Path to file.md` (or `Path-to-file.md`) with the text `Path to file`
- `[[Path to file | Here's the title override]]`: produces a link to `Path to file.md` with the text `Here's the title override`
- `[[Path to file#Anchor]]`: produces a link to the anchor `Anchor` in the file `Path to file.md`
> [!warning]
> Currently, Quartz does not support block references or note embed syntax.
- `[[Path to file#^block-ref]]`: produces a link to the specific block `block-ref` in the file `Path to file.md`
docs/index.md
@@ -30,7 +30,7 @@
## ðŸ”§ Features
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box
- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box
- Hot-reload for both configuration and content
- Simple JSX layouts and [[creating components|page components]]
- [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes
package-lock.json
@@ -1,12 +1,12 @@
{
  "name": "@jackyzha0/quartz",
  "version": "4.0.10",
  "version": "4.0.11",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "@jackyzha0/quartz",
      "version": "4.0.10",
      "version": "4.0.11",
      "license": "MIT",
      "dependencies": {
        "@clack/prompts": "^0.6.3",
package.json
@@ -2,7 +2,7 @@
  "name": "@jackyzha0/quartz",
  "description": "🌱 publish your digital garden and notes as a website",
  "private": true,
  "version": "4.0.10",
  "version": "4.0.11",
  "type": "module",
  "author": "jackyzha0 <j.zhao2k19@gmail.com>",
  "license": "MIT",
quartz/components/renderPage.tsx
@@ -4,6 +4,8 @@
import BodyConstructor from "./Body"
import { JSResourceToScriptElement, StaticResources } from "../util/resources"
import { FullSlug, RelativeURL, joinSegments } from "../util/path"
import { visit } from "unist-util-visit"
import { Root, Element } from "hast"
interface RenderComponents {
  head: QuartzComponent
@@ -53,6 +55,40 @@
  components: RenderComponents,
  pageResources: StaticResources,
): string {
  // process transcludes in componentData
  visit(componentData.tree as 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 blockSlug = inner.properties?.["data-slug"] as FullSlug
        const blockRef = node.properties!.dataBlock as string
        // 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],
            }
          }
          node.children = [
            blockNode,
            {
              type: "element",
              tagName: "a",
              properties: { href: inner.properties?.href, class: ["internal"] },
              children: [{ type: "text", value: `Link to original` }],
            },
          ]
        }
      }
    }
  })
  const {
    head: Head,
    header,
quartz/plugins/transformers/links.ts
@@ -72,6 +72,7 @@
                    simplifySlug(destCanonical as FullSlug),
                  ) as SimpleSlug
                  outgoing.add(simple)
                  node.properties["data-slug"] = simple
                }
                // rewrite link internals if prettylinks is on
quartz/plugins/transformers/ofm.ts
@@ -135,6 +135,7 @@
    const hast = toHast(ast, { allowDangerousHtml: true })!
    return toHtml(hast, { allowDangerousHtml: true })
  }
  const findAndReplace = opts.enableInHtmlEmbed
    ? (tree: Root, regex: RegExp, replace?: Replace | null | undefined) => {
        if (replace) {
@@ -238,8 +239,16 @@
                    value: `<iframe src="${url}"></iframe>`,
                  }
                } else if (ext === "") {
                  // TODO: note embed
                  const block = anchor.slice(1)
                  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>`,
                }
                }
                // otherwise, fall through to regular link
              }
@@ -422,17 +431,23 @@
      if (opts.parseBlockReferences) {
        plugins.push(() => {
          const inlineTagTypes = new Set(["p", "li"])
          const blockTagTypes = new Set(["blockquote"])
          return (tree, file) => {
            file.data.blocks = {}
            const validTagTypes = new Set(["blockquote", "p", "li"])
            visit(tree, "element", (node, _index, _parent) => {
              if (validTagTypes.has(node.tagName)) {
                const last = node.children.at(-1) as Literal
                if (last.value && typeof last.value === "string") {
                  const matches = last.value.match(blockReferenceRegex)
            visit(tree, "element", (node, index, parent) => {
              if (blockTagTypes.has(node.tagName)) {
                const nextChild = parent?.children.at(index! + 2) as Element
                if (nextChild && nextChild.tagName === "p") {
                  const text = nextChild.children.at(0) as Literal
                  if (text && text.value && text.type === "text") {
                    const matches = text.value.match(blockReferenceRegex)
                  if (matches && matches.length >= 1) {
                    last.value = last.value.slice(0, -matches[0].length)
                      parent!.children.splice(index! + 2, 1)
                    const block = matches[0].slice(1)
                      if (!Object.keys(file.data.blocks!).includes(block)) {
                    node.properties = {
                      ...node.properties,
                      id: block,
@@ -441,6 +456,25 @@
                  }
                }
              }
                }
              } else if (inlineTagTypes.has(node.tagName)) {
                const last = node.children.at(-1) as Literal
                if (last && last.value && typeof last.value === "string") {
                  const matches = last.value.match(blockReferenceRegex)
                  if (matches && matches.length >= 1) {
                    last.value = last.value.slice(0, -matches[0].length)
                    const block = matches[0].slice(1)
                    if (!Object.keys(file.data.blocks!).includes(block)) {
                      node.properties = {
                        ...node.properties,
                        id: block,
                      }
                      file.data.blocks![block] = node
                    }
                  }
                }
              }
            })
          }
        })
quartz/styles/base.scss
@@ -470,3 +470,9 @@
    background: linear-gradient(transparent 0px, var(--light));
  }
}
.transclude {
  ul {
    padding-left: 1rem;
  }
}