mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-05-18 22:44:14 +02:00
Compare commits
11 Commits
7c5ea35aa0
...
3b325acb46
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3b325acb46 | ||
![]() |
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.
|
||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -27,7 +27,6 @@
|
||||
"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",
|
||||
@ -3848,17 +3847,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",
|
||||
|
@ -53,7 +53,6 @@
|
||||
"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",
|
||||
|
@ -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