mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-05-18 06:24:22 +02:00
Compare commits
12 Commits
3b325acb46
...
7c5ea35aa0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7c5ea35aa0 | ||
![]() |
7be47742a6 | ||
![]() |
8c99c622ae | ||
![]() |
ef2e56f029 | ||
![]() |
936ce4ff07 | ||
![]() |
473d05dab7 | ||
![]() |
c1bba16440 | ||
![]() |
9519492a38 | ||
![]() |
11db328af1 | ||
![]() |
a010948644 | ||
![]() |
663607d296 | ||
![]() |
f581fd5894 |
@ -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.
|
||||
|
39
package-lock.json
generated
39
package-lock.json
generated
@ -27,14 +27,13 @@
|
||||
"hast-util-to-html": "^9.0.4",
|
||||
"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.29.1",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"mdast-util-to-hast": "^13.2.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromorph": "^0.4.5",
|
||||
"pixi.js": "^8.6.6",
|
||||
"pixi.js": "^8.7.3",
|
||||
"preact": "^10.25.4",
|
||||
"preact-render-to-string": "^6.5.13",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
@ -79,10 +78,10 @@
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/pretty-time": "^1.1.5",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@types/ws": "^8.5.14",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"esbuild": "^0.24.2",
|
||||
"prettier": "^3.4.2",
|
||||
@ -1914,10 +1913,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz",
|
||||
"integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==",
|
||||
"version": "22.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz",
|
||||
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
@ -1943,10 +1943,11 @@
|
||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz",
|
||||
"integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@ -3848,17 +3849,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",
|
||||
@ -5583,9 +5573,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pixi.js": {
|
||||
"version": "8.6.6",
|
||||
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.6.6.tgz",
|
||||
"integrity": "sha512-o5pw7G2yuIrnBx0G4npBlmFp+XGNcapI/Ufs62rRj/4XKxc1Zo74YJr/BtEXcXTraTKd+pQvYOLvnfxRjxBMvQ==",
|
||||
"version": "8.7.3",
|
||||
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.7.3.tgz",
|
||||
"integrity": "sha512-wfWlhJYnGx1s4f2yoouevQjaeacbJ12LTkJGa+n9AIYNIjOnmJylBtZ2mARX7iFk3mr2xv0wuo//XPe2hk5OBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pixi/colord": "^2.9.6",
|
||||
"@types/css-font-loading-module": "^0.0.12",
|
||||
|
@ -53,14 +53,13 @@
|
||||
"hast-util-to-html": "^9.0.4",
|
||||
"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.29.1",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"mdast-util-to-hast": "^13.2.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromorph": "^0.4.5",
|
||||
"pixi.js": "^8.6.6",
|
||||
"pixi.js": "^8.7.3",
|
||||
"preact": "^10.25.4",
|
||||
"preact-render-to-string": "^6.5.13",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
@ -102,10 +101,10 @@
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/pretty-time": "^1.1.5",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@types/ws": "^8.5.14",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"esbuild": "^0.24.2",
|
||||
"prettier": "^3.4.2",
|
||||
|
@ -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" }),
|
||||
],
|
||||
|
@ -11,8 +11,32 @@ import {
|
||||
} from "../../util/path"
|
||||
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, 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 */
|
||||
@ -22,6 +46,7 @@ interface Options {
|
||||
openLinksInNewTab: boolean
|
||||
lazyLoad: boolean
|
||||
externalLinkIcon: boolean
|
||||
substitutions?: Sub[]
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
@ -55,32 +80,90 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
node.properties &&
|
||||
typeof node.properties.href === "string"
|
||||
) {
|
||||
let dest = node.properties.href as RelativeURL
|
||||
const classes = (node.properties.className ?? []) as string[]
|
||||
const isExternal = isAbsoluteUrl(dest)
|
||||
classes.push(isExternal ? "external" : "internal")
|
||||
const href = node.properties.href
|
||||
let dest = href as RelativeURL
|
||||
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) return true // continue
|
||||
|
||||
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: [
|
||||
{
|
||||
matched = true
|
||||
dest = parts.slice(1).join("") as RelativeURL
|
||||
switch (sub.type) {
|
||||
case "image":
|
||||
refIcon = {
|
||||
type: "element",
|
||||
tagName: "path",
|
||||
tagName: "img",
|
||||
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",
|
||||
src: (sub as Image).src,
|
||||
style: "max-width:1em;max-height:1em;margin:0px",
|
||||
},
|
||||
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:1em;max-height:1em",
|
||||
viewBox: vector.viewbox,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "path",
|
||||
properties: {
|
||||
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 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
|
||||
? 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: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the link has alias text
|
||||
@ -99,7 +182,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (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!,
|
||||
@ -145,7 +228,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
||||
node.properties.loading = "lazy"
|
||||
}
|
||||
|
||||
if (!isAbsoluteUrl(node.properties.src)) {
|
||||
if (!URL.canParse(node.properties.src)) {
|
||||
let dest = node.properties.src as RelativeURL
|
||||
dest = node.properties.src = transformLink(
|
||||
file.data.slug!,
|
||||
|
Loading…
x
Reference in New Issue
Block a user