import { QuartzTransformerPlugin } from "../types" import { FullSlug, RelativeURL, SimpleSlug, TransformOptions, _stripSlashes, simplifySlug, splitAnchor, transformLink, } from "../../util/path" import path from "path" import { visit } from "unist-util-visit" import isAbsoluteUrl from "is-absolute-url" interface Options { /** How to resolve Markdown paths */ markdownLinkResolution: TransformOptions["strategy"] /** Strips folders from a link so that it looks nice */ prettyLinks: boolean openLinksInNewTab: boolean } const defaultOptions: Options = { markdownLinkResolution: "absolute", prettyLinks: true, openLinksInNewTab: false, } export const CrawlLinks: QuartzTransformerPlugin | undefined> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { name: "LinkProcessing", htmlPlugins(ctx) { return [ () => { return (tree, file) => { const curSlug = simplifySlug(file.data.slug!) const outgoing: Set = new Set() const transformOptions: TransformOptions = { strategy: opts.markdownLinkResolution, allSlugs: ctx.allSlugs, } visit(tree, "element", (node, _index, _parent) => { // rewrite all links if ( node.tagName === "a" && node.properties && typeof node.properties.href === "string" ) { let dest = node.properties.href as RelativeURL node.properties.className ??= [] node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal") // Check if the link has alias text if ( node.children.length === 1 && node.children[0].type === "text" && node.children[0].value !== dest ) { // Add the 'alias' class if the text content is not the same as the href node.properties.className.push("alias") } if (opts.openLinksInNewTab) { node.properties.target = "_blank" } // don't process external links or intra-document anchors const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#")) if (isInternal) { dest = node.properties.href = transformLink( file.data.slug!, dest, transformOptions, ) // url.resolve is considered legacy // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to const url = new URL(dest, `https://base.com/${curSlug}`) const canonicalDest = url.pathname let [destCanonical, _destAnchor] = splitAnchor(canonicalDest) if (destCanonical.endsWith("/")) { destCanonical += "index" } // need to decodeURIComponent here as WHATWG URL percent-encodes everything const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug const simple = simplifySlug(full) outgoing.add(simple) node.properties["data-slug"] = full } // rewrite link internals if prettylinks is on if ( opts.prettyLinks && isInternal && node.children.length === 1 && node.children[0].type === "text" && !node.children[0].value.startsWith("#") ) { node.children[0].value = path.basename(node.children[0].value) } } // transform all other resources that may use links if ( ["img", "video", "audio", "iframe"].includes(node.tagName) && node.properties && typeof node.properties.src === "string" ) { if (!isAbsoluteUrl(node.properties.src)) { let dest = node.properties.src as RelativeURL dest = node.properties.src = transformLink( file.data.slug!, dest, transformOptions, ) node.properties.src = dest } } }) file.data.links = [...outgoing] } }, ] }, } } declare module "vfile" { interface DataMap { links: SimpleSlug[] } }