diff --git a/quartz.config.ts b/quartz.config.ts index b3db3d60d..31dd5a495 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -72,6 +72,7 @@ const config: QuartzConfig = { Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), Plugin.Description(), Plugin.Latex({ renderEngine: "katex" }), + Plugin.LucideIcons(), ], filters: [Plugin.RemoveDrafts()], emitters: [ diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts index 8e2cd844f..a6173cca7 100644 --- a/quartz/plugins/transformers/index.ts +++ b/quartz/plugins/transformers/index.ts @@ -11,3 +11,4 @@ export { SyntaxHighlighting } from "./syntax" export { TableOfContents } from "./toc" export { HardLineBreaks } from "./linebreaks" export { RoamFlavoredMarkdown } from "./roam" +export { LucideIcons } from "./lucide" diff --git a/quartz/plugins/transformers/lucide.ts b/quartz/plugins/transformers/lucide.ts new file mode 100644 index 000000000..fa2781bc7 --- /dev/null +++ b/quartz/plugins/transformers/lucide.ts @@ -0,0 +1,93 @@ +import { Element, Literal, Root } from "hast" +import { visit } from "unist-util-visit" +import { QuartzTransformerPlugin } from "../types" + +function rehypeLucideIcons(verbose: boolean = false) { + return (tree: Root) => { + visit(tree, "text", (node: Literal, index: number | undefined, parent: any) => { + if ( + typeof node.value === "string" && + typeof index === "number" && + parent && + parent.children && + Array.isArray(parent.children) + ) { + // Replace the first match of a Lucide icon tag in this node. + // Once the node is updated with the icon element, it will trigger a + // new visit call to this node and recursively replace all icon tags. + const originalText = node.value + const lucidePattern = /:luc_([a-z_]+):/ + const lucideTag = originalText.match(lucidePattern) + + if (lucideTag) { + const [iconTag, iconName] = lucideTag + const tagStart = lucideTag.index ?? 0 + + let replacedText: Array = [] + + if (tagStart > 0) { + replacedText.push({ + type: "text", + value: originalText.substring(0, tagStart), + }) + } + + const lucideIconElement: Element = { + type: "element", + tagName: "i", + properties: { + class: `lucide lucide-${iconName}`, + "data-lucide": iconName, + "aria-hidden": "true", + }, + children: [], + } + replacedText.push(lucideIconElement) + + if (verbose) { + console.log( + `[LucideIcons] Replaced markdown :luc_${iconName}: with HTML Lucide icon "${iconName}"`, + ) + } + + const remainingText = originalText.substring(tagStart + iconTag.length) + if (remainingText) { + replacedText.push({ + type: "text", + value: remainingText, + }) + } + + parent.children.splice(index, 1, ...replacedText) + } + } + }) + } +} + +export const LucideIcons: QuartzTransformerPlugin = () => { + return { + name: "LucideIcons", + htmlPlugins(ctx) { + return [() => rehypeLucideIcons(ctx?.argv?.verbose || false)] + }, + externalResources() { + return { + js: [ + { + src: "https://unpkg.com/lucide@latest", + loadTime: "afterDOMReady", + contentType: "external", + }, + { + script: "lucide.createIcons();", + loadTime: "afterDOMReady", + contentType: "inline", + }, + ], + } + }, + } +} + +export default LucideIcons diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 14e6ae674..dda895972 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -639,3 +639,9 @@ iframe.pdf { transition: width 0.2s ease; z-index: 9999; } + +.lucide { + /* render Lucide icons proportional to font size */ + width: 1em; + height: 1em; +}