From cce389c81d262d1d2a2bd8140c879efd68e3c6dd Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed, 13 Sep 2023 18:28:53 +0000
Subject: [PATCH] feat: note transclusion (#475)
---
quartz/plugins/transformers/ofm.ts | 109 ++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 92 insertions(+), 17 deletions(-)
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index b324dac..8306f40 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -1,6 +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 { Replace, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
import { slug as slugAnchor } from "github-slugger"
import rehypeRaw from "rehype-raw"
@@ -9,7 +10,7 @@
import { JSResource } from "../../util/resources"
// @ts-ignore
import calloutScript from "../../components/scripts/callout.inline.ts"
-import { FilePath, canonicalizeServer, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
+import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
import { toHast } from "mdast-util-to-hast"
import { toHtml } from "hast-util-to-html"
import { PhrasingContent } from "mdast-util-find-and-replace/lib"
@@ -21,6 +22,7 @@
callouts: boolean
mermaid: boolean
parseTags: boolean
+ parseBlockReferences: boolean
enableInHtmlEmbed: boolean
}
@@ -31,6 +33,7 @@
callouts: true,
mermaid: true,
parseTags: true,
+ parseBlockReferences: true,
enableInHtmlEmbed: false,
}
@@ -69,6 +72,8 @@
const calloutMapping: Record<string, keyof typeof callouts> = {
note: "note",
abstract: "abstract",
+ summary: "abstract",
+ tldr: "abstract",
info: "info",
todo: "todo",
tip: "tip",
@@ -96,7 +101,7 @@
function canonicalizeCallout(calloutName: string): keyof typeof callouts {
let callout = calloutName.toLowerCase() as keyof typeof calloutMapping
- return calloutMapping[callout] ?? calloutName
+ return calloutMapping[callout] ?? "note"
}
const capitalize = (s: string): string => {
@@ -109,14 +114,17 @@
// (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link)
// (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias)
const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)?(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g")
-const highlightRegex = new RegExp(/==(.+)==/, "g")
+const highlightRegex = new RegExp(/==([^=]+)==/, "g")
const commentRegex = new RegExp(/%%(.+)%%/, "g")
// from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts
const calloutRegex = new RegExp(/^\[\!(\w+)\]([+-]?)/)
const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
-// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line
-// #(\w+) -> tag itself is # followed by a string of alpha-numeric characters
-const tagRegex = new RegExp(/(?:^| )#([\w-_\/]+)/, "g")
+// (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line
+// #(...) -> capturing group, tag itself must start with #
+// (?:[-_\p{L}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters, hyphens and/or underscores
+// (?:\/[-_\p{L}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
+const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu")
+const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g")
export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
userOpts,
@@ -127,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) {
@@ -230,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
}
@@ -320,7 +337,7 @@
const titleHtml: HTML = {
type: "html",
- value: `<div
+ value: `<div
class="callout-title"
>
<div class="callout-icon">${callouts[calloutType]}</div>
@@ -381,16 +398,16 @@
if (opts.parseTags) {
plugins.push(() => {
return (tree: Root, file) => {
- const slug = canonicalizeServer(file.data.slug!)
- const base = pathToRoot(slug)
- findAndReplace(tree, tagRegex, (value: string, tag: string) => {
- if (file.data.frontmatter) {
+ const base = pathToRoot(file.data.slug!)
+ findAndReplace(tree, tagRegex, (_value: string, tag: string) => {
+ tag = slugTag(tag)
+ if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) {
file.data.frontmatter.tags.push(tag)
}
return {
type: "link",
- url: base + `/tags/${slugTag(tag)}`,
+ url: base + `/tags/${tag}`,
data: {
hProperties: {
className: ["tag-link"],
@@ -399,7 +416,7 @@
children: [
{
type: "text",
- value,
+ value: `#${tag}`,
},
],
}
@@ -407,11 +424,63 @@
}
})
}
-
return plugins
},
htmlPlugins() {
- return [rehypeRaw]
+ const plugins = [rehypeRaw]
+
+ if (opts.parseBlockReferences) {
+ plugins.push(() => {
+ const inlineTagTypes = new Set(["p", "li"])
+ const blockTagTypes = new Set(["blockquote"])
+ return (tree, file) => {
+ file.data.blocks = {}
+
+ 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 && 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
+ }
+ }
+ }
+ }
+ })
+ }
+ })
+ }
+
+ return plugins
},
externalResources() {
const js: JSResource[] = []
@@ -429,7 +498,7 @@
script: `
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark'
- mermaid.initialize({
+ mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
theme: darkMode ? 'dark' : 'default'
@@ -450,3 +519,9 @@
},
}
}
+
+declare module "vfile" {
+ interface DataMap {
+ blocks: Record<string, Element>
+ }
+}
--
Gitblit v1.10.0