Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontmatter): Add an option to index frontmatter wikilinks #1673

Open
wants to merge 1 commit into
base: v4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/plugins/CrawlLinks.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This plugin accepts the following configuration options:
- `openLinksInNewTab`: If `true`, configures external links to open in a new tab. Defaults to `false`.
- `lazyLoad`: If `true`, adds lazy loading to resource elements (`img`, `video`, etc.) to improve page load performance. Defaults to `false`.
- `externalLinkIcon`: Adds an icon next to external links when `true` (default) to visually distinguishing them from internal links.
- `indexFrontmatterWikilinks`: If `true`, parses Obsidian-style wikilinks in the frontmatter and adds them to the graph (including things like backlinks) as if they were part of the note content. Defaults to `false`.

> [!warning]
> Removing this plugin is _not_ recommended and will likely break the page.
Expand Down
53 changes: 42 additions & 11 deletions quartz/plugins/transformers/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import path from "path"
import { visit } from "unist-util-visit"
import isAbsoluteUrl from "is-absolute-url"
import { Root } from "hast"
import { wikilinkRegex } from "./ofm"

interface Options {
/** How to resolve Markdown paths */
Expand All @@ -22,6 +23,7 @@ interface Options {
openLinksInNewTab: boolean
lazyLoad: boolean
externalLinkIcon: boolean
indexFrontmatterWikilinks: boolean
}

const defaultOptions: Options = {
Expand All @@ -30,6 +32,20 @@ const defaultOptions: Options = {
openLinksInNewTab: false,
lazyLoad: false,
externalLinkIcon: true,
indexFrontmatterWikilinks: false,
}

function getFullInternalLink(dest: RelativeURL, fileSlug: SimpleSlug): FullSlug {
// url.resolve is considered legacy
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
const url = new URL(dest, "https://base.com/" + stripSlashes(fileSlug, true))
const canonicalDest = url.pathname
let [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
if (destCanonical.endsWith("/")) {
destCanonical += "index"
}
// need to decodeURIComponent here as WHATWG URL percent-encodes everything
return decodeURIComponent(stripSlashes(destCanonical, true)) as FullSlug
}

export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
Expand Down Expand Up @@ -107,17 +123,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
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/" + stripSlashes(curSlug, true))
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 full = getFullInternalLink(dest, curSlug)
const simple = simplifySlug(full)
outgoing.add(simple)
node.properties["data-slug"] = full
Expand Down Expand Up @@ -157,6 +163,31 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
}
})

if (opts.indexFrontmatterWikilinks) {
const strings = Object.values(file.data.frontmatter ?? {})
.flatMap((vs) => (Array.isArray(vs) ? vs : [vs]))
.filter((v) => typeof v === "string")

for (const string of strings) {
// the regex is /g so we have to do this to get the captures
// exec doesn't work because it's stateful and so returns null every other time (very bad)
// we do all of that to reuse the wikilinkRegex from ofm
const [captures] = [...string.matchAll(wikilinkRegex)]
if (!captures || captures[0] != string || string.startsWith("!")) {
// not matched, or didn't match the whole string, or is the embed syntax for some reason,
// which doesn't make sense to support in frontmatter
continue
}
const [_, rawFp, rawHeader] = captures
const fp = rawFp?.trim() ?? ""
const anchor = rawHeader?.trim() ?? ""
const dest = transformLink(file.data.slug!, fp + anchor, transformOptions)
const full = getFullInternalLink(dest, curSlug)
const simple = simplifySlug(full)
outgoing.add(simple)
}
}

file.data.links = [...outgoing]
}
},
Expand Down
Loading