diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 5cb39d9a..305f511f 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -3,9 +3,10 @@ import { QuartzComponent, QuartzComponentProps } from "./types" import HeaderConstructor from "./Header" import BodyConstructor from "./Body" import { JSResourceToScriptElement, StaticResources } from "../util/resources" -import { FullSlug, RelativeURL, joinSegments } from "../util/path" +import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" import { visit } from "unist-util-visit" import { Root, Element, ElementContent } from "hast" +import { QuartzPluginData } from "../plugins/vfile" interface RenderComponents { head: QuartzComponent @@ -49,6 +50,18 @@ export function pageResources( } } +let pageIndex: Map | undefined = undefined +function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map { + if (!pageIndex) { + pageIndex = new Map() + for (const file of allFiles) { + pageIndex.set(file.slug!, file) + } + } + + return pageIndex +} + export function renderPage( slug: FullSlug, componentData: QuartzComponentProps, @@ -62,17 +75,15 @@ export function renderPage( if (classNames.includes("transclude")) { const inner = node.children[0] as Element const transcludeTarget = inner.properties?.["data-slug"] as FullSlug - - // TODO: avoid this expensive find operation and construct an index ahead of time - const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) + const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget) if (!page) { return } let blockRef = node.properties?.dataBlock as string | undefined - if (blockRef?.startsWith("^")) { + if (blockRef?.startsWith("#^")) { // block transclude - blockRef = blockRef.slice(1) + blockRef = blockRef.slice("#^".length) let blockNode = page.blocks?.[blockRef] if (blockNode) { if (blockNode.tagName === "li") { @@ -84,7 +95,7 @@ export function renderPage( } node.children = [ - blockNode, + normalizeHastElement(blockNode, slug, transcludeTarget), { type: "element", tagName: "a", @@ -117,7 +128,9 @@ export function renderPage( } node.children = [ - ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]), + ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) => + normalizeHastElement(child as Element, slug, transcludeTarget), + ), { type: "element", tagName: "a", @@ -135,7 +148,9 @@ export function renderPage( { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` }, ], }, - ...(page.htmlAst.children as ElementContent[]), + ...(page.htmlAst.children as ElementContent[]).map((child) => + normalizeHastElement(child as Element, slug, transcludeTarget), + ), { type: "element", tagName: "a", diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index 1aff138f..bddcfa4c 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -1,4 +1,4 @@ -import type { ContentDetails } from "../../plugins/emitters/contentIndex" +import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex" import * as d3 from "d3" import { registerEscapeHandler, removeAllChildren } from "./util" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" @@ -46,20 +46,22 @@ async function renderGraph(container: string, fullSlug: FullSlug) { showTags, } = JSON.parse(graph.dataset["cfg"]!) - const data = await fetchData - + const data: Map = new Map( + Object.entries(await fetchData).map(([k, v]) => [ + simplifySlug(k as FullSlug), + v, + ]), + ) const links: LinkData[] = [] const tags: SimpleSlug[] = [] - const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug))) - - for (const [src, details] of Object.entries(data)) { - const source = simplifySlug(src as FullSlug) + const validLinks = new Set(data.keys()) + for (const [source, details] of data.entries()) { const outgoing = details.links ?? [] for (const dest of outgoing) { if (validLinks.has(dest)) { - links.push({ source, target: dest }) + links.push({ source: source, target: dest }) } } @@ -71,7 +73,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { tags.push(...localTags.filter((tag) => !tags.includes(tag))) for (const tag of localTags) { - links.push({ source, target: tag }) + links.push({ source: source, target: tag }) } } } @@ -93,17 +95,17 @@ async function renderGraph(container: string, fullSlug: FullSlug) { } } } else { - Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug))) + validLinks.forEach((id) => neighbourhood.add(id)) if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) } const graphData: { nodes: NodeData[]; links: LinkData[] } = { nodes: [...neighbourhood].map((url) => { - const text = url.startsWith("tags/") ? "#" + url.substring(5) : data[url]?.title ?? url + const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url return { id: url, text: text, - tags: data[url]?.tags ?? [], + tags: data.get(url)?.tags ?? [], } }), links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), @@ -200,7 +202,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { window.spaNavigate(new URL(targ, window.location.toString())) }) .on("mouseover", function (_, d) { - const neighbours: SimpleSlug[] = data[fullSlug].links ?? [] + const neighbours: SimpleSlug[] = data.get(slug)?.links ?? [] const neighbourNodes = d3 .selectAll(".node") .filter((d) => neighbours.includes(d.id)) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index eec473c1..3072959d 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -81,14 +81,16 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to const url = new URL(dest, `https://base.com/${curSlug}`) const canonicalDest = url.pathname - const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) + let [destCanonical, _destAnchor] = splitAnchor(canonicalDest) + if (destCanonical.endsWith("/")) { + destCanonical += "index" + } // need to decodeURIComponent here as WHATWG URL percent-encodes everything - const simple = decodeURIComponent( - simplifySlug(destCanonical as FullSlug), - ) as SimpleSlug + const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug + const simple = simplifySlug(full) outgoing.add(simple) - node.properties["data-slug"] = simple + node.properties["data-slug"] = full } // rewrite link internals if prettylinks is on diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 2e47cedf..4c6a6dbe 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -182,7 +182,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin const [rawFp, rawHeader, rawAlias] = capture const fp = rawFp ?? "" const anchor = rawHeader?.trim().replace(/^#+/, "") - const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : "" + const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : "" + const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : "" const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" const embedDisplay = value.startsWith("!") ? "!" : "" return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]` diff --git a/quartz/util/path.ts b/quartz/util/path.ts index e450339f..19aa0948 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -1,4 +1,5 @@ import { slug } from "github-slugger" +import type { ElementContent, Element as HastElement } from "hast" // this file must be isomorphic so it can't use node libs (e.g. path) export const QUARTZ = "quartz" @@ -65,7 +66,8 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { } export function simplifySlug(fp: FullSlug): SimpleSlug { - return _stripSlashes(_trimSuffix(fp, "index"), true) as SimpleSlug + const res = _stripSlashes(_trimSuffix(fp, "index"), true) + return (res.length === 0 ? "/" : res) as SimpleSlug } export function transformInternalLink(link: string): RelativeURL { @@ -86,20 +88,47 @@ export function transformInternalLink(link: string): RelativeURL { // from micromorph/src/utils.ts // https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5 +const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => { + const rebased = new URL(el.getAttribute(attr)!, newBase) + el.setAttribute(attr, rebased.pathname + rebased.hash) +} 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) => - rebase(item, "href", destination), + _rebaseHtmlElement(item, "href", destination), ) el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => - rebase(item, "src", destination), + _rebaseHtmlElement(item, "src", destination), ) } +const _rebaseHastElement = ( + el: HastElement, + attr: string, + curBase: FullSlug, + newBase: FullSlug, +) => { + if (el.properties?.[attr]) { + if (!isRelativeURL(String(el.properties[attr]))) { + return + } + + const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string) + el.properties[attr] = rel + } +} + +export function normalizeHastElement(el: HastElement, curBase: FullSlug, newBase: FullSlug) { + _rebaseHastElement(el, "src", curBase, newBase) + _rebaseHastElement(el, "href", curBase, newBase) + if (el.children) { + el.children = el.children.map((child) => + normalizeHastElement(child as HastElement, curBase, newBase), + ) + } + + return el +} + // resolve /a/b/c to ../.. export function pathToRoot(slug: FullSlug): RelativeURL { let rootPath = slug