The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: transcludes and relative paths

+85 -36
+24 -9
quartz/components/renderPage.tsx
··· 3 3 import HeaderConstructor from "./Header" 4 4 import BodyConstructor from "./Body" 5 5 import { JSResourceToScriptElement, StaticResources } from "../util/resources" 6 - import { FullSlug, RelativeURL, joinSegments } from "../util/path" 6 + import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" 7 7 import { visit } from "unist-util-visit" 8 8 import { Root, Element, ElementContent } from "hast" 9 + import { QuartzPluginData } from "../plugins/vfile" 9 10 10 11 interface RenderComponents { 11 12 head: QuartzComponent ··· 49 50 } 50 51 } 51 52 53 + let pageIndex: Map<FullSlug, QuartzPluginData> | undefined = undefined 54 + function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map<FullSlug, QuartzPluginData> { 55 + if (!pageIndex) { 56 + pageIndex = new Map() 57 + for (const file of allFiles) { 58 + pageIndex.set(file.slug!, file) 59 + } 60 + } 61 + 62 + return pageIndex 63 + } 64 + 52 65 export function renderPage( 53 66 slug: FullSlug, 54 67 componentData: QuartzComponentProps, ··· 62 75 if (classNames.includes("transclude")) { 63 76 const inner = node.children[0] as Element 64 77 const transcludeTarget = inner.properties?.["data-slug"] as FullSlug 65 - 66 - // TODO: avoid this expensive find operation and construct an index ahead of time 67 - const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) 78 + const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget) 68 79 if (!page) { 69 80 return 70 81 } 71 82 72 83 let blockRef = node.properties?.dataBlock as string | undefined 73 - if (blockRef?.startsWith("^")) { 84 + if (blockRef?.startsWith("#^")) { 74 85 // block transclude 75 - blockRef = blockRef.slice(1) 86 + blockRef = blockRef.slice("#^".length) 76 87 let blockNode = page.blocks?.[blockRef] 77 88 if (blockNode) { 78 89 if (blockNode.tagName === "li") { ··· 84 95 } 85 96 86 97 node.children = [ 87 - blockNode, 98 + normalizeHastElement(blockNode, slug, transcludeTarget), 88 99 { 89 100 type: "element", 90 101 tagName: "a", ··· 117 128 } 118 129 119 130 node.children = [ 120 - ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]), 131 + ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) => 132 + normalizeHastElement(child as Element, slug, transcludeTarget), 133 + ), 121 134 { 122 135 type: "element", 123 136 tagName: "a", ··· 135 148 { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` }, 136 149 ], 137 150 }, 138 - ...(page.htmlAst.children as ElementContent[]), 151 + ...(page.htmlAst.children as ElementContent[]).map((child) => 152 + normalizeHastElement(child as Element, slug, transcludeTarget), 153 + ), 139 154 { 140 155 type: "element", 141 156 tagName: "a",
+15 -13
quartz/components/scripts/graph.inline.ts
··· 1 - import type { ContentDetails } from "../../plugins/emitters/contentIndex" 1 + import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex" 2 2 import * as d3 from "d3" 3 3 import { registerEscapeHandler, removeAllChildren } from "./util" 4 4 import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" ··· 46 46 showTags, 47 47 } = JSON.parse(graph.dataset["cfg"]!) 48 48 49 - const data = await fetchData 50 - 49 + const data: Map<SimpleSlug, ContentDetails> = new Map( 50 + Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [ 51 + simplifySlug(k as FullSlug), 52 + v, 53 + ]), 54 + ) 51 55 const links: LinkData[] = [] 52 56 const tags: SimpleSlug[] = [] 53 57 54 - const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug))) 55 - 56 - for (const [src, details] of Object.entries<ContentDetails>(data)) { 57 - const source = simplifySlug(src as FullSlug) 58 + const validLinks = new Set(data.keys()) 59 + for (const [source, details] of data.entries()) { 58 60 const outgoing = details.links ?? [] 59 61 60 62 for (const dest of outgoing) { 61 63 if (validLinks.has(dest)) { 62 - links.push({ source, target: dest }) 64 + links.push({ source: source, target: dest }) 63 65 } 64 66 } 65 67 ··· 71 73 tags.push(...localTags.filter((tag) => !tags.includes(tag))) 72 74 73 75 for (const tag of localTags) { 74 - links.push({ source, target: tag }) 76 + links.push({ source: source, target: tag }) 75 77 } 76 78 } 77 79 } ··· 93 95 } 94 96 } 95 97 } else { 96 - Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug))) 98 + validLinks.forEach((id) => neighbourhood.add(id)) 97 99 if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) 98 100 } 99 101 100 102 const graphData: { nodes: NodeData[]; links: LinkData[] } = { 101 103 nodes: [...neighbourhood].map((url) => { 102 - const text = url.startsWith("tags/") ? "#" + url.substring(5) : data[url]?.title ?? url 104 + const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url 103 105 return { 104 106 id: url, 105 107 text: text, 106 - tags: data[url]?.tags ?? [], 108 + tags: data.get(url)?.tags ?? [], 107 109 } 108 110 }), 109 111 links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), ··· 200 202 window.spaNavigate(new URL(targ, window.location.toString())) 201 203 }) 202 204 .on("mouseover", function (_, d) { 203 - const neighbours: SimpleSlug[] = data[fullSlug].links ?? [] 205 + const neighbours: SimpleSlug[] = data.get(slug)?.links ?? [] 204 206 const neighbourNodes = d3 205 207 .selectAll<HTMLElement, NodeData>(".node") 206 208 .filter((d) => neighbours.includes(d.id))
+7 -5
quartz/plugins/transformers/links.ts
··· 81 81 // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to 82 82 const url = new URL(dest, `https://base.com/${curSlug}`) 83 83 const canonicalDest = url.pathname 84 - const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) 84 + let [destCanonical, _destAnchor] = splitAnchor(canonicalDest) 85 + if (destCanonical.endsWith("/")) { 86 + destCanonical += "index" 87 + } 85 88 86 89 // need to decodeURIComponent here as WHATWG URL percent-encodes everything 87 - const simple = decodeURIComponent( 88 - simplifySlug(destCanonical as FullSlug), 89 - ) as SimpleSlug 90 + const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug 91 + const simple = simplifySlug(full) 90 92 outgoing.add(simple) 91 - node.properties["data-slug"] = simple 93 + node.properties["data-slug"] = full 92 94 } 93 95 94 96 // rewrite link internals if prettylinks is on
+2 -1
quartz/plugins/transformers/ofm.ts
··· 182 182 const [rawFp, rawHeader, rawAlias] = capture 183 183 const fp = rawFp ?? "" 184 184 const anchor = rawHeader?.trim().replace(/^#+/, "") 185 - const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : "" 185 + const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : "" 186 + const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : "" 186 187 const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" 187 188 const embedDisplay = value.startsWith("!") ? "!" : "" 188 189 return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`
+37 -8
quartz/util/path.ts
··· 1 1 import { slug } from "github-slugger" 2 + import type { ElementContent, Element as HastElement } from "hast" 2 3 // this file must be isomorphic so it can't use node libs (e.g. path) 3 4 4 5 export const QUARTZ = "quartz" ··· 65 66 } 66 67 67 68 export function simplifySlug(fp: FullSlug): SimpleSlug { 68 - return _stripSlashes(_trimSuffix(fp, "index"), true) as SimpleSlug 69 + const res = _stripSlashes(_trimSuffix(fp, "index"), true) 70 + return (res.length === 0 ? "/" : res) as SimpleSlug 69 71 } 70 72 71 73 export function transformInternalLink(link: string): RelativeURL { ··· 86 88 87 89 // from micromorph/src/utils.ts 88 90 // https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5 91 + const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => { 92 + const rebased = new URL(el.getAttribute(attr)!, newBase) 93 + el.setAttribute(attr, rebased.pathname + rebased.hash) 94 + } 89 95 export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) { 90 - const rebase = (el: Element, attr: string, newBase: string | URL) => { 91 - const rebased = new URL(el.getAttribute(attr)!, newBase) 92 - el.setAttribute(attr, rebased.pathname + rebased.hash) 93 - } 94 - 95 96 el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => 96 - rebase(item, "href", destination), 97 + _rebaseHtmlElement(item, "href", destination), 97 98 ) 98 99 el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => 99 - rebase(item, "src", destination), 100 + _rebaseHtmlElement(item, "src", destination), 100 101 ) 102 + } 103 + 104 + const _rebaseHastElement = ( 105 + el: HastElement, 106 + attr: string, 107 + curBase: FullSlug, 108 + newBase: FullSlug, 109 + ) => { 110 + if (el.properties?.[attr]) { 111 + if (!isRelativeURL(String(el.properties[attr]))) { 112 + return 113 + } 114 + 115 + const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string) 116 + el.properties[attr] = rel 117 + } 118 + } 119 + 120 + export function normalizeHastElement(el: HastElement, curBase: FullSlug, newBase: FullSlug) { 121 + _rebaseHastElement(el, "src", curBase, newBase) 122 + _rebaseHastElement(el, "href", curBase, newBase) 123 + if (el.children) { 124 + el.children = el.children.map((child) => 125 + normalizeHastElement(child as HastElement, curBase, newBase), 126 + ) 127 + } 128 + 129 + return el 101 130 } 102 131 103 132 // resolve /a/b/c to ../..