diff --git a/package.json b/package.json index d16243ec2..71c09df32 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.1.0", + "version": "4.1.1", "type": "module", "author": "jackyzha0 ", "license": "MIT", diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 3cb69ab51..ec5971431 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -5,7 +5,7 @@ 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" +import { Root, Element, ElementContent } from "hast" interface RenderComponents { head: QuartzComponent @@ -61,22 +61,81 @@ export function renderPage( 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", diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index b78a5e6e4..bf958afde 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -2,14 +2,18 @@ import { computePosition, flip, inline, shift } from "@floating-ui/dom" // from micromorph/src/utils.ts // https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5 -export function normalizeRelativeURLs(el: Element | Document, base: string | URL) { - const update = (el: Element, attr: string, base: string | URL) => { - el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname) +export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) { + const rebase = (el: Element, attr: string, newBase: string | URL) => { + const rebased = new URL(el.getAttribute(attr)!, newBase) + el.setAttribute(attr, rebased.pathname + rebased.hash) } - el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => update(item, "href", base)) - - el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => update(item, "src", base)) + el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => + rebase(item, "href", destination), + ) + el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => + rebase(item, "src", destination), + ) } const p = new DOMParser() diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index 4d2515032..473363d3c 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -109,6 +109,7 @@ function createRouter() { if (isSamePage(url) && url.hash) { const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) el?.scrollIntoView() + history.pushState({}, "", url) return } diff --git a/quartz/plugins/emitters/cname.ts b/quartz/plugins/emitters/cname.ts new file mode 100644 index 000000000..ffe2c6d12 --- /dev/null +++ b/quartz/plugins/emitters/cname.ts @@ -0,0 +1,29 @@ +import { FilePath, joinSegments } from "../../util/path" +import { QuartzEmitterPlugin } from "../types" +import fs from "fs" +import chalk from "chalk" + +export function extractDomainFromBaseUrl(baseUrl: string) { + const url = new URL(`https://${baseUrl}`) + return url.hostname +} + +export const CNAME: QuartzEmitterPlugin = () => ({ + name: "CNAME", + getQuartzComponents() { + return [] + }, + async emit({ argv, cfg }, _content, _resources, _emit): Promise { + if (!cfg.configuration.baseUrl) { + console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration")) + return [] + } + const path = joinSegments(argv.output, "CNAME") + const content = extractDomainFromBaseUrl(cfg.configuration.baseUrl) + if (!content) { + return [] + } + fs.writeFileSync(path, content) + return [path] as FilePath[] + }, +}) diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts index 99a2c54d5..bc378c47b 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -7,3 +7,4 @@ export { Assets } from "./assets" export { Static } from "./static" export { ComponentResources } from "./componentResources" export { NotFoundPage } from "./404" +export { CNAME } from "./cname" diff --git a/quartz/plugins/emitters/static.ts b/quartz/plugins/emitters/static.ts index 6f5d19d41..f0118e2e8 100644 --- a/quartz/plugins/emitters/static.ts +++ b/quartz/plugins/emitters/static.ts @@ -11,7 +11,10 @@ export const Static: QuartzEmitterPlugin = () => ({ async emit({ argv, cfg }, _content, _resources, _emit): Promise { const staticPath = joinSegments(QUARTZ, "static") const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns) - await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), { recursive: true }) + await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), { + recursive: true, + dereference: true, + }) return fps.map((fp) => joinSegments(argv.output, "static", fp)) as FilePath[] }, }) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 36cf73bee..628d06d73 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/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" @@ -110,7 +110,10 @@ function canonicalizeCallout(calloutName: string): keyof typeof callouts { // ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) // (|[^\[\]\|\#]+)? -> | then one or more non-special characters (alias) -const wikilinkRegex = new RegExp(/!?\[\[([^\[\]\|\#]+)?(#[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/, "g") +export const wikilinkRegex = 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 @@ -178,9 +181,9 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin src = src.replaceAll(wikilinkRegex, (value, ...capture) => { const [rawFp, rawHeader, rawAlias] = capture const fp = rawFp ?? "" - const anchor = rawHeader?.trim().slice(1) + const anchor = rawHeader?.trim().replace(/^#+/, "") const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : "" - const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" + const displayAlias = rawAlias ?? anchor ?? "" const embedDisplay = value.startsWith("!") ? "!" : "" return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]` }) @@ -236,13 +239,13 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin value: ``, } } else if (ext === "") { - const block = anchor.slice(1) + const block = anchor return { type: "html", data: { hProperties: { transclude: true } }, value: `
Transclude of block ${block}
`, + }" class="transclude-inner">Transclude of ${url}${block}`, } } @@ -477,6 +480,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin } } }) + + file.data.htmlAst = tree } }) } @@ -524,5 +529,6 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin declare module "vfile" { interface DataMap { blocks: Record + htmlAst: HtmlRoot } } diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts index 87c5802ff..d0781ec20 100644 --- a/quartz/plugins/transformers/toc.ts +++ b/quartz/plugins/transformers/toc.ts @@ -3,6 +3,7 @@ import { Root } from "mdast" import { visit } from "unist-util-visit" import { toString } from "mdast-util-to-string" import Slugger from "github-slugger" +import { wikilinkRegex } from "./ofm" export interface Options { maxDepth: 1 | 2 | 3 | 4 | 5 | 6 @@ -24,6 +25,7 @@ interface TocEntry { slug: string // this is just the anchor (#some-slug), not the canonical slug } +const regexMdLinks = new RegExp(/\[([^\[]+)\](\(.*\))/, "g") export const TableOfContents: QuartzTransformerPlugin | undefined> = ( userOpts, ) => { @@ -41,7 +43,16 @@ export const TableOfContents: QuartzTransformerPlugin | undefin let highestDepth: number = opts.maxDepth visit(tree, "heading", (node) => { if (node.depth <= opts.maxDepth) { - const text = toString(node) + let text = toString(node) + + // strip link formatting from toc entries + text = text.replace(wikilinkRegex, (_, rawFp, __, rawAlias) => { + const fp = rawFp?.trim() ?? "" + const alias = rawAlias?.slice(1).trim() + return alias ?? fp + }) + text = text.replace(regexMdLinks, "$1") + highestDepth = Math.min(highestDepth, node.depth) toc.push({ depth: node.depth,