From 0a6e9c3f86250f124190119b18c5fe55ad468ee4 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 13 Sep 2023 11:28:53 -0700 Subject: [PATCH] feat: note transclusion (#475) * basic transclude * feat: note transclusion --- docs/features/wikilinks.md | 4 +-- docs/index.md | 2 +- package-lock.json | 4 +-- package.json | 2 +- quartz/components/renderPage.tsx | 36 +++++++++++++++++++ quartz/plugins/transformers/links.ts | 1 + quartz/plugins/transformers/ofm.ts | 52 +++++++++++++++++++++++----- quartz/styles/base.scss | 6 ++++ 8 files changed, 91 insertions(+), 16 deletions(-) diff --git a/docs/features/wikilinks.md b/docs/features/wikilinks.md index 704a0d0c..50bbb1bb 100644 --- a/docs/features/wikilinks.md +++ b/docs/features/wikilinks.md @@ -13,6 +13,4 @@ This is enabled as a part of [[Obsidian compatibility]] and can be configured an - `[[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` diff --git a/docs/index.md b/docs/index.md index e5b9dfef..05de2bae 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ This will guide you through initializing your Quartz with content. Once you've d ## 🔧 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 diff --git a/package-lock.json b/package-lock.json index a19d81c1..a8790789 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index 95c57cd8..0a2085ce 100644 --- a/package.json +++ b/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 ", "license": "MIT", diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 25297f28..451813b5 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -4,6 +4,8 @@ import HeaderConstructor from "./Header" 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 @@ export function renderPage( 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, diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 02ced158..e050e00a 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -72,6 +72,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = simplifySlug(destCanonical as FullSlug), ) as SimpleSlug outgoing.add(simple) + node.properties["data-slug"] = simple } // rewrite link internals if prettylinks is on diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 811d659e..8306f40d 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -135,6 +135,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin 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 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin value: ``, } } else if (ext === "") { - // TODO: note embed + const block = anchor.slice(1) + return { + type: "html", + data: { hProperties: { transclude: true } }, + value: `
Transclude of block ${block}
`, + } } + // otherwise, fall through to regular link } @@ -422,22 +431,47 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin 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)) { + + 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) { + 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, + } + file.data.blocks![block] = node + } + } + } + } + } else if (inlineTagTypes.has(node.tagName)) { const last = node.children.at(-1) as Literal - if (last.value && typeof last.value === "string") { + 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) - node.properties = { - ...node.properties, - id: block, + + if (!Object.keys(file.data.blocks!).includes(block)) { + node.properties = { + ...node.properties, + id: block, + } + file.data.blocks![block] = node } - file.data.blocks![block] = node } } } diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 34def878..92c0f84d 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -470,3 +470,9 @@ ol.overflow { background: linear-gradient(transparent 0px, var(--light)); } } + +.transclude { + ul { + padding-left: 1rem; + } +}