pathfinder2e-notes/quartz/plugins/transformers/frontmatterrefs.ts
2026-01-11 21:55:09 +01:00

126 lines
3.5 KiB
TypeScript

import { QuartzTransformerPlugin } from "../types"
import { visit } from "unist-util-visit"
import { QuartzPluginData } from "../vfile"
import { unified } from "unified"
import remarkParse from "remark-parse"
import { Parent } from "unist"
export interface Options {
prefix: string
}
const defaultOptions: Options = {
prefix: "=this.",
}
// Convert Markdown hard-break syntax (two spaces + newline) into <br>
function preprocessHardBreaks(str: string): string {
return str.replace(/ {2,}\r?\n/g, "<br>\n")
}
// Parse a string as inline Markdown
function parseInlineMarkdown(str: string) {
const preprocessed = preprocessHardBreaks(str)
const tree = unified().use(remarkParse).parse(preprocessed) as Parent
// Inline context: unwrap a single paragraph
if (tree.children.length === 1 && tree.children[0].type === "paragraph") {
return (tree.children[0] as Parent).children
}
return tree.children
}
// Extract leading and trailing whitespace from a text node
function splitWhitespace(value: string) {
const match = value.match(/^(\s*)([\s\S]*?)(\s*)$/)
return {
leading: match?.[1] ?? "",
core: match?.[2] ?? value,
trailing: match?.[3] ?? "",
}
}
// Reattach whitespace around parsed nodes
function reattachWhitespace(
nodes: any[],
leading: string,
trailing: string,
) {
const result = [...nodes]
if (leading) {
result.unshift({ type: "text", value: leading })
}
if (trailing) {
result.push({ type: "text", value: trailing })
}
return result
}
export const ReplaceDataview: QuartzTransformerPlugin<Partial<Options>> = (
userOpts,
) => {
const opts = { ...defaultOptions, ...userOpts }
const pattern = new RegExp(`${opts.prefix.replace(".", "\\.")}(\\w+)`, "g")
function resolve(
value: string,
frontmatter: QuartzPluginData["frontmatter"],
): string {
return value.replace(pattern, (_, key) => {
const v = frontmatter[key as keyof typeof frontmatter]
return v != null ? String(v) : ""
})
}
return {
name: "ReplaceDataview",
markdownPlugins() {
return [
() => {
return (tree, file) => {
const frontmatter =
(file.data.frontmatter as QuartzPluginData["frontmatter"]) ?? {}
// Replace in normal text nodes
visit(tree, "text", (node, index, parent) => {
if (!parent || index == null) return
if (typeof node.value !== "string") return
const resolved = resolve(node.value, frontmatter)
if (resolved === node.value) return
const { leading, core, trailing } = splitWhitespace(node.value)
const parsed = parseInlineMarkdown(core)
const newNodes = reattachWhitespace(parsed, leading, trailing)
parent.children.splice(index, 1, ...newNodes)
})
// Replace inline code (`=this.prop`) and unwrap it
visit(tree, "inlineCode", (node, index, parent) => {
if (!parent || index == null) return
if (typeof node.value !== "string") return
const match = node.value.match(
new RegExp(`^${opts.prefix.replace(".", "\\.")}(\\w+)$`),
)
if (!match) return
const key = match[1]
const value = frontmatter[key as keyof typeof frontmatter]
if (value == null) return
const parsed = parseInlineMarkdown(String(value))
parent.children.splice(index, 1, ...parsed)
})
}
},
]
},
}
}