From f581fd5894df043fabe5f0ee4577dbcea1c234dd Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 16 Dec 2024 00:47:24 -0600 Subject: [PATCH 01/10] feat(links.ts): custom identifiers for custom icons --- quartz/plugins/transformers/links.ts | 63 +++++++++++++++++++--------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 3e8dbdede5ba5..0d81c25accff1 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -12,7 +12,9 @@ import { import path from "path" import { visit } from "unist-util-visit" import isAbsoluteUrl from "is-absolute-url" -import { Root } from "hast" +import { ElementContent, Root } from "hast" + +type Sub = [RegExp, string | ElementContent] interface Options { /** How to resolve Markdown paths */ @@ -22,6 +24,7 @@ interface Options { openLinksInNewTab: boolean lazyLoad: boolean externalLinkIcon: boolean + substitutions?: Sub[] } const defaultOptions: Options = { @@ -55,32 +58,54 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) node.properties && typeof node.properties.href === "string" ) { - let dest = node.properties.href as RelativeURL + let href = node.properties.href + var dest = href as RelativeURL + var refIcon: string | ElementContent | null = null + var matched = false + opts.substitutions?.every(([regex, sub]) => { + let parts = href.match(regex) + if (parts != null) { + dest = parts.slice(1).join("") as RelativeURL + if (typeof sub == "object") { + refIcon = sub + } else { + refIcon = { type: "text", value: sub } + } + matched = true + return false // break equivalent + } + return true + }) + node.properties.href = dest const classes = (node.properties.className ?? []) as string[] const isExternal = isAbsoluteUrl(dest) classes.push(isExternal ? "external" : "internal") - if (isExternal && opts.externalLinkIcon) { - node.children.push({ - type: "element", - tagName: "svg", - properties: { - "aria-hidden": "true", - class: "external-icon", - style: "max-width:0.8em;max-height:0.8em", - viewBox: "0 0 512 512", - }, - children: [ - { + if ((isExternal && opts.externalLinkIcon) || matched) { + node.children.push( + refIcon != null + ? refIcon + : { type: "element", - tagName: "path", + tagName: "svg", properties: { - d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", + "aria-hidden": "true", + class: "external-icon", + style: "max-width:0.8em;max-height:0.8em", + viewBox: "0 0 512 512", }, - children: [], + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", + }, + children: [], + }, + ], }, - ], - }) + ) } // Check if the link has alias text From 663607d29622f52220cc46176f37da479e210d96 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 16 Dec 2024 01:05:56 -0600 Subject: [PATCH 02/10] lint: format --- quartz/plugins/transformers/links.ts | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 0d81c25accff1..e20af59f153f7 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -86,25 +86,25 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) refIcon != null ? refIcon : { - type: "element", - tagName: "svg", - properties: { - "aria-hidden": "true", - class: "external-icon", - style: "max-width:0.8em;max-height:0.8em", - viewBox: "0 0 512 512", - }, - children: [ - { - type: "element", - tagName: "path", - properties: { - d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", - }, - children: [], + type: "element", + tagName: "svg", + properties: { + "aria-hidden": "true", + class: "external-icon", + style: "max-width:0.8em;max-height:0.8em", + viewBox: "0 0 512 512", }, - ], - }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", + }, + children: [], + }, + ], + }, ) } From a0109486444ea3c270d3e2ec7a8e1c5d452d751f Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Tue, 17 Dec 2024 12:12:26 -0600 Subject: [PATCH 03/10] lint(links.ts): fix declarations --- quartz/plugins/transformers/links.ts | 46 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index e20af59f153f7..94ef0403146c4 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -58,12 +58,12 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) node.properties && typeof node.properties.href === "string" ) { - let href = node.properties.href - var dest = href as RelativeURL - var refIcon: string | ElementContent | null = null - var matched = false + const href = node.properties.href + let dest = href as RelativeURL + let refIcon: string | ElementContent | null = null + let matched = false opts.substitutions?.every(([regex, sub]) => { - let parts = href.match(regex) + const parts = href.match(regex) if (parts != null) { dest = parts.slice(1).join("") as RelativeURL if (typeof sub == "object") { @@ -86,25 +86,25 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) refIcon != null ? refIcon : { - type: "element", - tagName: "svg", - properties: { - "aria-hidden": "true", - class: "external-icon", - style: "max-width:0.8em;max-height:0.8em", - viewBox: "0 0 512 512", - }, - children: [ - { - type: "element", - tagName: "path", - properties: { - d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", - }, - children: [], - }, - ], + type: "element", + tagName: "svg", + properties: { + "aria-hidden": "true", + class: "external-icon", + style: "max-width:0.8em;max-height:0.8em", + viewBox: "0 0 512 512", }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", + }, + children: [], + }, + ], + }, ) } From 11db328af1aeac4ac5d1caa00d37361ce03dd277 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 23 Dec 2024 13:06:40 -0600 Subject: [PATCH 04/10] fix(links.ts): remove is-absolute-url --- package-lock.json | 12 ------------ package.json | 1 - quartz/plugins/transformers/links.ts | 6 +++--- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1933fa5b8ad0b..48933726549b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "hast-util-to-html": "^9.0.3", "hast-util-to-jsx-runtime": "^2.3.2", "hast-util-to-string": "^3.0.1", - "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", "lightningcss": "^1.28.1", "mdast-util-find-and-replace": "^3.0.1", @@ -4094,17 +4093,6 @@ "node": ">=12" } }, - "node_modules/is-absolute-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", - "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", diff --git a/package.json b/package.json index 11c1c91ea9542..b0247eb46d219 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "hast-util-to-html": "^9.0.3", "hast-util-to-jsx-runtime": "^2.3.2", "hast-util-to-string": "^3.0.1", - "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", "lightningcss": "^1.28.1", "mdast-util-find-and-replace": "^3.0.1", diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 94ef0403146c4..86a8803741fc0 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -11,7 +11,6 @@ import { } from "../../util/path" import path from "path" import { visit } from "unist-util-visit" -import isAbsoluteUrl from "is-absolute-url" import { ElementContent, Root } from "hast" type Sub = [RegExp, string | ElementContent] @@ -124,7 +123,7 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) } // don't process external links or intra-document anchors - const isInternal = !(isAbsoluteUrl(dest) || dest.startsWith("#")) + const isInternal = !(URL.canParse(dest) || dest.startsWith("#")) if (isInternal) { dest = node.properties.href = transformLink( file.data.slug!, @@ -170,8 +169,9 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) node.properties.loading = "lazy" } - if (!isAbsoluteUrl(node.properties.src)) { + if (!URL.canParse(node.properties.src)) { let dest = node.properties.src as RelativeURL + console.log(dest) dest = node.properties.src = transformLink( file.data.slug!, dest, From 9519492a38a9c1f20bca062456b5dd8577674006 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 23 Dec 2024 13:09:44 -0600 Subject: [PATCH 05/10] links.ts: move to newtype model --- quartz/plugins/transformers/links.ts | 112 ++++++++++++++++++++------- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 86a8803741fc0..772c9513587a0 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -13,7 +13,30 @@ import path from "path" import { visit } from "unist-util-visit" import { ElementContent, Root } from "hast" -type Sub = [RegExp, string | ElementContent] +type Sub = [RegExp, Appendable] +type Appendable = (Image | Emoji | Path) & Tagged +type Tagged = { type: "image" | "emoji" | "path" } +type Image = { src: string } +type Emoji = { text: string } +type Path = { + code: string + viewbox: string +} +export function Image(src: string | Image): Appendable { + if (typeof src == "object") { + return src as Image & { type: "image" } + } + return { src: src, type: "image" } +} +export function Emoji(text: string | Emoji): Appendable { + if (typeof text == "object") { + return text as Emoji & { type: "emoji" } + } + return { text: text, type: "emoji" } +} +export function Path(path: Path): Appendable { + return path as Path & { type: "path" } // This errors if path is uncast. what +} interface Options { /** How to resolve Markdown paths */ @@ -59,51 +82,85 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) ) { const href = node.properties.href let dest = href as RelativeURL - let refIcon: string | ElementContent | null = null + let refIcon: ElementContent | null = null let matched = false + // bfahrenfort: the 'every' lambda is like a foreach that allows continue/break opts.substitutions?.every(([regex, sub]) => { const parts = href.match(regex) - if (parts != null) { - dest = parts.slice(1).join("") as RelativeURL - if (typeof sub == "object") { - refIcon = sub - } else { - refIcon = { type: "text", value: sub } - } - matched = true - return false // break equivalent - } - return true - }) - node.properties.href = dest - const classes = (node.properties.className ?? []) as string[] - const isExternal = isAbsoluteUrl(dest) - classes.push(isExternal ? "external" : "internal") + if (parts == null) return true // continue - if ((isExternal && opts.externalLinkIcon) || matched) { - node.children.push( - refIcon != null - ? refIcon - : { + matched = true + dest = parts.slice(1).join("") as RelativeURL + switch (sub.type) { + case "image": + refIcon = { + type: "element", + tagName: "img", + properties: { + src: (sub as Image).src as FullSlug, + style: "max-width:1em;max-height:1em", + }, + children: [], + } + break + case "emoji": + refIcon = { type: "text", value: (sub as Emoji).text } + break + case "path": + let vector = sub as Path + refIcon = { type: "element", tagName: "svg", properties: { "aria-hidden": "true", class: "external-icon", - style: "max-width:0.8em;max-height:0.8em", - viewBox: "0 0 512 512", + style: "max-width:1em;max-height:1em", + viewBox: vector.viewbox, }, children: [ { type: "element", tagName: "path", properties: { - d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", + d: vector.code, }, children: [], }, ], - }, + } + break + } + return false // break + }) + node.properties.href = dest + const classes = (node.properties.className ?? []) as string[] + const isExternal = URL.canParse(dest) + classes.push(isExternal || matched ? "external" : "internal") + + if ((isExternal && opts.externalLinkIcon) || matched) { + node.children.push( + refIcon != null + ? refIcon + : { + type: "element", + tagName: "svg", + properties: { + "aria-hidden": "true", + class: "external-icon", + style: "max-width:0.8em;max-height:0.8em", + viewBox: "0 0 512 512", + }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z", + }, + children: [], + }, + ], + }, ) } @@ -177,6 +234,7 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) dest, transformOptions, ) + console.log(dest) node.properties.src = dest } } From c1bba16440046250d370efd3f6225ca34f65d839 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 23 Dec 2024 14:07:42 -0600 Subject: [PATCH 06/10] lint(links.ts): remove extraneous code --- quartz/plugins/transformers/links.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 772c9513587a0..8cdfb178c676c 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -97,7 +97,7 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) type: "element", tagName: "img", properties: { - src: (sub as Image).src as FullSlug, + src: (sub as Image).src, style: "max-width:1em;max-height:1em", }, children: [], From 473d05dab7ed9473283e462ed157535fbf1cc5d1 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 23 Dec 2024 14:08:23 -0600 Subject: [PATCH 07/10] lint(links.ts): debug statements --- quartz/plugins/transformers/links.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 8cdfb178c676c..cc85c6acd489b 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -228,13 +228,11 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) if (!URL.canParse(node.properties.src)) { let dest = node.properties.src as RelativeURL - console.log(dest) dest = node.properties.src = transformLink( file.data.slug!, dest, transformOptions, ) - console.log(dest) node.properties.src = dest } } From 936ce4ff07fa63bd913bb56538758d6f673e8e27 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 23 Dec 2024 14:34:08 -0600 Subject: [PATCH 08/10] fix(links.ts): styling --- quartz/plugins/transformers/links.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index cc85c6acd489b..7c54219b95978 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -98,7 +98,7 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) tagName: "img", properties: { src: (sub as Image).src, - style: "max-width:1em;max-height:1em", + style: "max-width:1em;max-height:1em;margin:0px", }, children: [], } @@ -137,6 +137,8 @@ export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) const isExternal = URL.canParse(dest) classes.push(isExternal || matched ? "external" : "internal") + // If the link matched a substitution, display the corresponding image afterwards; + // otherwise, if it's an external link, display the default external link icon if ((isExternal && opts.externalLinkIcon) || matched) { node.children.push( refIcon != null From ef2e56f029fd36c955a375e6566421b64906ade3 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 23 Dec 2024 14:34:55 -0600 Subject: [PATCH 09/10] links.ts: document link identifier image feature --- docs/plugins/CrawlLinks.md | 8 ++++++++ quartz.config.ts | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/plugins/CrawlLinks.md b/docs/plugins/CrawlLinks.md index 47b7bdd776133..c3134d505f690 100644 --- a/docs/plugins/CrawlLinks.md +++ b/docs/plugins/CrawlLinks.md @@ -19,6 +19,14 @@ 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. +- `substitutions`: default `[]`, a list of regex-image pairs. When you write a link's URL to match the regex, it will display the image after the link on your webpage. + - images may either be an `Image(url)`, `Emoji(text)`, or `Path({code: code, viewbox: viewbox})`. Examples: + - `Image("https://website.com/image.jpg")` + - `Image("/static/icon.png")` + - `Emoji("🪴")` + - `Path({code: "really long string like M320 0H288V64h32 82.7L201.4 265.4...", viewbox: "0 0 512 512"})` + - Example use: `substitutions: [ [/garden!(.+)/, Emoji("🪴")], ],` + - This would let you write links in Markdown like `[Someone's garden](garden!https://their-website.com)`, which would look like `Someone's Garden🪴` on the website. > [!warning] > Removing this plugin is _not_ recommended and will likely break the page. diff --git a/quartz.config.ts b/quartz.config.ts index dc339d987f1c2..fd33cd7e9e1d3 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -1,5 +1,6 @@ import { QuartzConfig } from "./quartz/cfg" import * as Plugin from "./quartz/plugins" +import { Image, Path, Emoji } from "./quartz/plugins/transformers/links" /** * Quartz 4.0 Configuration @@ -70,7 +71,12 @@ const config: QuartzConfig = { Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), Plugin.GitHubFlavoredMarkdown(), Plugin.TableOfContents(), - Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), + Plugin.CrawlLinks({ + markdownLinkResolution: "shortest", + // See https://quartz.jzhao.xyz/plugins/CrawlLinks + // Try uncommenting the below line and writing [Someone's Garden](garden!https://jzhao.xyz/) in markdown + // substitutions: [ [/garden!(.+)/, Emoji("🪴")], ], + }), Plugin.Description(), Plugin.Latex({ renderEngine: "katex" }), ], From 8c99c622aec52a79ad7008d9de01688fe1daa5f6 Mon Sep 17 00:00:00 2001 From: bfahrenfort Date: Mon, 23 Dec 2024 14:38:16 -0600 Subject: [PATCH 10/10] lint: format --- docs/plugins/CrawlLinks.md | 2 +- quartz.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/CrawlLinks.md b/docs/plugins/CrawlLinks.md index c3134d505f690..dafe39a20204b 100644 --- a/docs/plugins/CrawlLinks.md +++ b/docs/plugins/CrawlLinks.md @@ -25,7 +25,7 @@ This plugin accepts the following configuration options: - `Image("/static/icon.png")` - `Emoji("🪴")` - `Path({code: "really long string like M320 0H288V64h32 82.7L201.4 265.4...", viewbox: "0 0 512 512"})` - - Example use: `substitutions: [ [/garden!(.+)/, Emoji("🪴")], ],` + - Example use: `substitutions: [ [/garden!(.+)/, Emoji("🪴")], ],` - This would let you write links in Markdown like `[Someone's garden](garden!https://their-website.com)`, which would look like `Someone's Garden🪴` on the website. > [!warning] diff --git a/quartz.config.ts b/quartz.config.ts index fd33cd7e9e1d3..91aea4a8a014e 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -75,7 +75,7 @@ const config: QuartzConfig = { markdownLinkResolution: "shortest", // See https://quartz.jzhao.xyz/plugins/CrawlLinks // Try uncommenting the below line and writing [Someone's Garden](garden!https://jzhao.xyz/) in markdown - // substitutions: [ [/garden!(.+)/, Emoji("🪴")], ], + // substitutions: [[/garden!(.+)/, Emoji("🪴")]], }), Plugin.Description(), Plugin.Latex({ renderEngine: "katex" }),