From bb5c136296e5de90d702abbc1d13f835b6d6caa4 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Fri, 17 Apr 2026 02:24:33 +0200 Subject: [PATCH] feat(path): add normalizeHastElement for cross-slug HAST rebasing Moved from Quartz core (quartz/util/path.ts) into the shared utils package so that plugins performing cross-slug HAST embedding can re-use the same rebase helper as Quartz's internal transclude expansion, rather than reimplementing or skipping the rebase. The primary consumer outside of Quartz core is canvas-page, whose buildEmbeddedContent() pulls vfile.data.htmlAst from arbitrary target pages and serializes it verbatim into canvas pages - producing broken relative hrefs that survive accidentally on root deployments but fail under non-trivial basePath configurations. Exposing the helper here lets canvas-page (and any future embedding plugin) rebase correctly without duplicating the logic. Implementation details: - Deep-clones the input via structuredClone (Node >=22 builtin) so callers don't need to bring their own clone function - Absolute URLs (http://, https://, /root-relative) pass through untouched; only relative URLs are rebased - Recurses into children so nested anchors inside a rebased subtree are also normalized Backed by 10 new tests covering same-slug, cross-slug (both directions), nested depth, anchor preservation, absolute URL passthrough, root-relative passthrough, src-attribute rebasing, elements without href/src, nested children recursion, and input-immutability. --- dist/index.d.ts | 2 +- dist/index.js.map | 2 +- dist/path.d.ts | 20 ++++++++ dist/path.js | 19 ++++++++ dist/path.js.map | 2 +- src/path.ts | 46 ++++++++++++++++++ test/path.test.ts | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 206 insertions(+), 3 deletions(-) diff --git a/dist/index.d.ts b/dist/index.d.ts index 732da01..be34902 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -37,5 +37,5 @@ export { formatDate } from "./date.js"; export { getIconCode } from "./emoji.js"; export { byDateAndAlphabetical, getDate } from "./sort.js"; export { FilePath, FullSlug } from "@quartz-community/types"; -import "hast-util-to-jsx-runtime"; import "hast"; +import "hast-util-to-jsx-runtime"; diff --git a/dist/index.js.map b/dist/index.js.map index d4b8f0d..b5967d9 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/path.ts","../src/dom.ts","../src/lang.ts","../src/escape.ts","../src/jsx.tsx","../src/date.ts","../src/emoji.ts","../src/sort.ts"],"names":["window","slugAnchor","jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { slug as slugAnchor } from \"github-slugger\";\nimport type { FilePath, FullSlug } from \"@quartz-community/types\";\n\nexport type { FilePath, FullSlug };\n\n/** No '/index' ending, no file extension, can have trailing slash for folders. */\nexport type SimpleSlug = string & { _brand: \"SimpleSlug\" };\n\n/** Starts with './' or '../', used for navigation. */\nexport type RelativeURL = string & { _brand: \"RelativeURL\" };\n\nexport interface TransformOptions {\n strategy: \"absolute\" | \"relative\" | \"shortest\";\n allSlugs: FullSlug[];\n}\n\nexport function isFilePath(s: string): s is FilePath {\n const validStart = !s.startsWith(\".\");\n return validStart && _hasFileExtension(s);\n}\n\nexport function isFullSlug(s: string): s is FullSlug {\n const validStart = !(s.startsWith(\".\") || s.startsWith(\"/\"));\n const validEnding = !s.endsWith(\"/\");\n return validStart && validEnding && !_containsForbiddenCharacters(s);\n}\n\nexport function isSimpleSlug(s: string): s is SimpleSlug {\n const validStart = !(s.startsWith(\".\") || (s.length > 1 && s.startsWith(\"/\")));\n const validEnding = !endsWith(s, \"index\");\n return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s);\n}\n\nexport function isRelativeURL(s: string): s is RelativeURL {\n const validStart = /^\\.{1,2}/.test(s);\n const validEnding = !endsWith(s, \"index\");\n return validStart && validEnding && ![\".md\", \".html\"].includes(getFileExtension(s) ?? \"\");\n}\n\nexport function isAbsoluteURL(s: string): boolean {\n try {\n new URL(s);\n } catch {\n return false;\n }\n return true;\n}\n\nexport function simplifySlug(fp: FullSlug | string): SimpleSlug {\n const res = stripSlashes(trimSuffix(fp, \"index\"), true);\n return (res.length === 0 ? \"/\" : res) as SimpleSlug;\n}\n\nexport function getFullSlug(window: Window): FullSlug {\n const res = window.document.body.dataset.slug! as FullSlug;\n return res;\n}\n\nexport function getFullSlugFromUrl(): FullSlug {\n let rawSlug = window.location.pathname;\n if (rawSlug.endsWith(\"/\")) rawSlug = rawSlug.slice(0, -1);\n if (rawSlug.startsWith(\"/\")) rawSlug = rawSlug.slice(1);\n return rawSlug as FullSlug;\n}\n\nexport function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {\n fp = stripSlashes(fp) as FilePath;\n const ext = getFileExtension(fp);\n const withoutFileExt = fp.replace(new RegExp(ext + \"$\"), \"\");\n const finalExt = excludeExt || [\".md\", \".html\", undefined].includes(ext) ? \"\" : ext;\n\n let slug = _sluggify(withoutFileExt);\n\n if (endsWith(slug, \"_index\")) {\n slug = slug.replace(/_index$/, \"index\");\n }\n\n // Obsidian \"Folder Notes\" convention (insideFolder storage): a file named the\n // same as its parent folder is treated as the folder's landing page. Rewrite\n // `folder/folder` → `folder/index` so every downstream consumer (trie, link\n // resolver, simplifySlug, isFolderPath) sees the canonical Quartz slug shape\n // and Just Works. A top-level single-segment slug is NOT rewritten — that\n // represents Obsidian's `parentFolder` storage mode, which Quartz already\n // routes as a regular top-level page.\n const segments = slug.split(\"/\");\n if (segments.length >= 2 && segments[segments.length - 1] === segments[segments.length - 2]) {\n segments[segments.length - 1] = \"index\";\n slug = segments.join(\"/\");\n }\n\n return (slug + (finalExt ?? \"\")) as FullSlug;\n}\n\nexport function joinSegments(...args: string[]): string {\n if (args.length === 0) {\n return \"\";\n }\n\n let joined = args\n .filter((segment) => segment !== \"\" && segment !== \"/\")\n .map((segment) => stripSlashes(segment))\n .join(\"/\");\n\n const first = args[0];\n const last = args[args.length - 1];\n\n if (first?.startsWith(\"/\")) {\n joined = \"/\" + joined;\n }\n\n if (last?.endsWith(\"/\")) {\n joined = joined + \"/\";\n }\n\n return joined;\n}\n\nexport function resolvePath(to: string): string {\n if (to.startsWith(\"/\")) return to;\n return \"/\" + to;\n}\n\n/** Read the base path injected at build time via `data-basepath` on ``.\n * Returns `\"\"` for root deployments, e.g. `\"/repository\"` for subdirectory. */\nexport function getBasePath(): string {\n if (typeof document === \"undefined\") return \"\";\n return document.body?.dataset?.basepath ?? \"\";\n}\n\n/** Resolve a slug to an absolute URL path, prepending the site's base path.\n * e.g. `resolveBasePath(\"features/Callouts\")` → `\"/repository/features/Callouts\"` */\nexport function resolveBasePath(to: string, basePath?: string): string {\n const base = basePath ?? getBasePath();\n const slug = to.startsWith(\"/\") ? to : \"/\" + to;\n return base + slug;\n}\n\nexport function endsWith(s: string, suffix: string): boolean {\n return s === suffix || s.endsWith(\"/\" + suffix);\n}\n\nexport function trimSuffix(s: string, suffix: string): string {\n if (endsWith(s, suffix)) {\n s = s.slice(0, -suffix.length);\n }\n return s;\n}\n\nexport function stripSlashes(s: string, onlyStripPrefix?: boolean): string {\n if (s.startsWith(\"/\")) {\n s = s.substring(1);\n }\n\n if (!onlyStripPrefix && s.endsWith(\"/\")) {\n s = s.slice(0, -1);\n }\n\n return s;\n}\n\nexport function getFileExtension(s: string): string | undefined {\n return s.match(/\\.[A-Za-z0-9]+$/)?.[0];\n}\n\nexport function isFolderPath(fplike: string): boolean {\n return (\n fplike.endsWith(\"/\") ||\n endsWith(fplike, \"index\") ||\n endsWith(fplike, \"index.md\") ||\n endsWith(fplike, \"index.html\")\n );\n}\n\nexport function getAllSegmentPrefixes(path: string): string[] {\n const segments = path.split(\"/\");\n const results: string[] = [];\n for (let i = 0; i < segments.length; i++) {\n results.push(segments.slice(0, i + 1).join(\"/\"));\n }\n return results;\n}\n\nexport function pathToRoot(slug: FullSlug): RelativeURL {\n let rootPath = slug\n .split(\"/\")\n .filter((x) => x !== \"\")\n .slice(0, -1)\n .map((_) => \"..\")\n .join(\"/\");\n\n if (rootPath.length === 0) {\n rootPath = \".\";\n }\n\n return rootPath as RelativeURL;\n}\n\nexport function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL {\n const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL;\n return res;\n}\n\nexport function splitAnchor(link: string): [string, string] {\n const [fp, anchor] = link.split(\"#\", 2);\n if (fp!.endsWith(\".pdf\")) {\n return [fp!, anchor === undefined ? \"\" : `#${anchor}`];\n }\n const slugged = anchor === undefined ? \"\" : \"#\" + slugAnchor(anchor);\n return [fp!, slugged];\n}\n\nexport function slugTag(tag: string): string {\n return tag\n .split(\"/\")\n .map((tagSegment) => _sluggify(tagSegment))\n .join(\"/\");\n}\n\nexport function transformInternalLink(link: string): RelativeURL {\n const [fplike, anchor] = splitAnchor(decodeURI(link));\n\n const folderPath = isFolderPath(fplike);\n const segments = fplike.split(\"/\").filter((x) => x.length > 0);\n const prefix = segments.filter(_isRelativeSegment).join(\"/\");\n const fp = segments.filter((seg) => !_isRelativeSegment(seg) && seg !== \"\").join(\"/\");\n\n const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath));\n const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug));\n const trail = folderPath ? \"/\" : \"\";\n const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL;\n return res;\n}\n\nexport function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL {\n const targetSlug = transformInternalLink(target);\n\n if (opts.strategy === \"relative\") {\n return targetSlug as RelativeURL;\n } else {\n // When src is a folder page (e.g. \"Compendium/Spells\" backed by index.md),\n // the browser serves it at \"/Compendium/Spells/\" with a trailing slash.\n // pathToRoot needs the \"/index\" suffix to compute the correct depth.\n const effectiveSrc =\n !endsWith(src, \"index\") && opts.allSlugs.includes(`${src}/index` as FullSlug)\n ? (`${src}/index` as FullSlug)\n : src;\n\n const folderTail = isFolderPath(targetSlug) ? \"/\" : \"\";\n const canonicalSlug = stripSlashes(targetSlug.slice(\".\".length));\n const [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug);\n\n if (opts.strategy === \"shortest\") {\n const isMultiSegment = targetCanonical.includes(\"/\");\n const isFolderTarget = isFolderPath(targetSlug);\n const matchingFileNames = opts.allSlugs.filter((slug) => {\n if (isMultiSegment) {\n // Multi-segment partial path: match by suffix\n if (slug === targetCanonical || slug.endsWith(\"/\" + targetCanonical)) {\n return true;\n }\n // Folder targets have \"/index\" stripped — also match the \"/index\" variant\n if (isFolderTarget) {\n const withIndex = targetCanonical + \"/index\";\n return slug === withIndex || slug.endsWith(\"/\" + withIndex);\n }\n return false;\n }\n const parts = slug.split(\"/\");\n const fileName = parts.at(-1);\n return targetCanonical === fileName;\n });\n\n if (matchingFileNames.length === 1) {\n const matchedSlug = matchingFileNames[0]!;\n return (resolveRelative(effectiveSrc, matchedSlug) + targetAnchor) as RelativeURL;\n }\n }\n\n return (joinSegments(pathToRoot(effectiveSrc), canonicalSlug) + folderTail) as RelativeURL;\n }\n}\n\n/**\n * Slugify a file path for use as an href.\n * Replaces whitespace→hyphens, &→-and-, %→-percent, removes ? and #, and\n * lowercases the result so that link matching is case-insensitive (matching\n * Obsidian's link-resolution semantics). Operates per segment so directory\n * separators are preserved.\n */\nexport function slugifyPath(s: string): string {\n return s\n .split(\"/\")\n .map((segment) =>\n segment\n .replace(/\\s/g, \"-\")\n .replace(/&/g, \"-and-\")\n .replace(/%/g, \"-percent\")\n .replace(/\\?/g, \"\")\n .replace(/#/g, \"\")\n .toLowerCase(),\n )\n .join(\"/\")\n .replace(/\\/$/, \"\");\n}\n\n/** @internal */\nfunction _sluggify(s: string): string {\n return slugifyPath(s);\n}\n\nfunction _containsForbiddenCharacters(s: string): boolean {\n return s.includes(\" \") || s.includes(\"#\") || s.includes(\"?\") || s.includes(\"&\");\n}\n\nfunction _hasFileExtension(s: string): boolean {\n return getFileExtension(s) !== undefined;\n}\n\nfunction _isRelativeSegment(s: string): boolean {\n return /^\\.{0,2}$/.test(s);\n}\n\nfunction _addRelativeToStart(s: string): string {\n if (s === \"\") {\n s = \".\";\n }\n\n if (!s.startsWith(\".\")) {\n s = joinSegments(\".\", s);\n }\n\n return s;\n}\n","export function removeAllChildren(el: HTMLElement): void {\n while (el.firstChild) {\n el.removeChild(el.firstChild);\n }\n}\n\nexport function registerEscapeHandler(\n outsideContainer: HTMLElement | null,\n onEscape: () => void,\n): () => void {\n if (!outsideContainer) return () => {};\n\n const onClick = (e: MouseEvent) => {\n if (!outsideContainer.classList.contains(\"active\")) {\n return;\n }\n if (e.target === outsideContainer) {\n e.preventDefault();\n e.stopPropagation();\n onEscape();\n }\n };\n\n const onKeydown = (e: KeyboardEvent) => {\n if (!outsideContainer.classList.contains(\"active\")) {\n return;\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n onEscape();\n }\n };\n\n outsideContainer.addEventListener(\"click\", onClick);\n document.addEventListener(\"keydown\", onKeydown);\n\n return () => {\n outsideContainer.removeEventListener(\"click\", onClick);\n document.removeEventListener(\"keydown\", onKeydown);\n };\n}\n\nexport function normalizeRelativeURLs(html: Document, baseUrl: string): void {\n const elements = html.querySelectorAll(\"[src], [href]\");\n for (const el of Array.from(elements)) {\n const attr = el.hasAttribute(\"href\") ? \"href\" : \"src\";\n const val = el.getAttribute(attr);\n if (!val) continue;\n if (\n val.startsWith(\"http://\") ||\n val.startsWith(\"https://\") ||\n val.startsWith(\"mailto:\") ||\n val.startsWith(\"tel:\") ||\n val.startsWith(\"#\") ||\n val.startsWith(\"/\") ||\n val.startsWith(\"data:\")\n ) {\n continue;\n }\n try {\n const normalized = new URL(val, baseUrl).toString();\n el.setAttribute(attr, normalized);\n } catch {\n continue;\n }\n }\n}\n","export function capitalize(s: string): string {\n return s.substring(0, 1).toUpperCase() + s.substring(1);\n}\n\nexport function classNames(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n","/**\n * HTML escape utilities for safe string rendering.\n */\n\n/**\n * Escapes HTML special characters in a string to prevent XSS.\n * @param unsafe - The string to escape\n * @returns The escaped string safe for HTML rendering\n *\n * @example\n * escapeHTML('') // '<script>alert("xss")</script>'\n */\nexport function escapeHTML(unsafe: string): string {\n return unsafe\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll('\"', \""\")\n .replaceAll(\"'\", \"'\");\n}\n\n/**\n * Unescapes HTML entities back to their original characters.\n * @param html - The HTML-escaped string\n * @returns The unescaped string\n *\n * @example\n * unescapeHTML('<div>') // '
'\n */\nexport function unescapeHTML(html: string): string {\n return html\n .replaceAll(\"&\", \"&\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll(\""\", '\"')\n .replaceAll(\"'\", \"'\");\n}\n","import type { Components, Jsx } from \"hast-util-to-jsx-runtime\";\nimport { toJsxRuntime } from \"hast-util-to-jsx-runtime\";\nimport type { Root, Node } from \"hast\";\nimport { Fragment, jsx, jsxs } from \"preact/jsx-runtime\";\nimport { h } from \"preact\";\n\n/**\n * Extract text content from JSX children.\n *\n * `toJsxRuntime` passes the inner text of `