From 2e9896c893602dfab6a1b564800c6354b802cece Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Dec 2023 09:52:17 -0800 Subject: [PATCH 01/17] fix: deep clone before relativizing urls in transclude (closes #640) --- package-lock.json | 6 ++++++ package.json | 1 + quartz/util/path.ts | 7 ++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 0bc416311..dd88ff7da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "remark-smartypants": "^2.0.0", + "rfdc": "^1.3.0", "rimraf": "^5.0.5", "serve-handler": "^6.1.5", "shikiji": "^0.9.9", @@ -5161,6 +5162,11 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "node_modules/rimraf": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", diff --git a/package.json b/package.json index 252467e14..70df83305 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "remark-smartypants": "^2.0.0", + "rfdc": "^1.3.0", "rimraf": "^5.0.5", "serve-handler": "^6.1.5", "shikiji": "^0.9.9", diff --git a/quartz/util/path.ts b/quartz/util/path.ts index 92cfabe49..d3997069b 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -1,5 +1,9 @@ import { slug as slugAnchor } from "github-slugger" import type { Element as HastElement } from "hast" +import rfdc from "rfdc" + +const clone = rfdc() + // this file must be isomorphic so it can't use node libs (e.g. path) export const QUARTZ = "quartz" @@ -121,7 +125,8 @@ const _rebaseHastElement = ( } } -export function normalizeHastElement(el: HastElement, curBase: FullSlug, newBase: FullSlug) { +export function normalizeHastElement(rawEl: HastElement, curBase: FullSlug, newBase: FullSlug) { + const el = clone(rawEl) // clone so we dont modify the original page _rebaseHastElement(el, "src", curBase, newBase) _rebaseHastElement(el, "href", curBase, newBase) if (el.children) { From 8fe37cc5e526a48bf14178207d16a6b7d9fbf536 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Dec 2023 10:05:00 -0800 Subject: [PATCH 02/17] docs: update issue template --- .github/ISSUE_TEMPLATE/bug_report.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7f1576df4..9ac527d4a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -20,12 +20,19 @@ Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. -**Screenshots** +**Screenshots and Source** If applicable, add screenshots to help explain your problem. +You can help speed up fixing the problem by either + +1. providing a simple reproduction +2. linking to your Quartz repository where the problem can be observed + **Desktop (please complete the following information):** -- Device: [e.g. iPhone6] +- Quartz Version: [e.g. v4.1.2] +- `node` Version: [e.g. v18.16] +- `npm` version: [e.g. v10.1.0] - OS: [e.g. iOS] - Browser [e.g. chrome, safari] From be76da9e95b6744f4934280cb85504371ccc1244 Mon Sep 17 00:00:00 2001 From: migueltorrescosta Date: Wed, 20 Dec 2023 20:09:48 +0000 Subject: [PATCH 03/17] docs: Add CollapsedWave to showcase.md (#643) Thank you so much for a beautiful setup --- docs/showcase.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/showcase.md b/docs/showcase.md index 1e2ef56ae..2cd56b306 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -21,5 +21,6 @@ Want to see what Quartz can do? Here are some cool community gardens: - [Pedro MC Fernandes's Topo da Mente](https://www.pmcf.xyz/topo-da-mente/) - [Mau Camargo's Notkesto](https://notes.camargomau.com/) - [Caicai's Novels](https://imoko.cc/blog/caicai/) +- [🌊 Collapsed Wave](https://collapsedwave.com/) If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)! From 63bf1e14b5d3ef310e560b8257975b72a37ac614 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 20 Dec 2023 19:55:20 -0800 Subject: [PATCH 04/17] style: remove relative from base pre --- quartz/styles/base.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index 607749aa1..af9c6f7d4 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -332,7 +332,6 @@ pre { border-radius: 5px; overflow-x: auto; border: 1px solid var(--lightgray); - position: relative; &:has(> code.mermaid) { border: none; From 504b44716240bb3fb9a077a1acaa3dc1059e2c1e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 27 Dec 2023 16:44:14 -0800 Subject: [PATCH 05/17] fix: use slugs instead of title as basis for explorer (#652) * use slugs instead of title as basis for explorer * fix folder persist state, better default behaviour * use relative path instead of full path as full path is affected by -d * dont use title in breadcrumb if it's just index lol --- quartz/components/Breadcrumbs.tsx | 8 +- quartz/components/Explorer.tsx | 63 +++++----- quartz/components/ExplorerNode.tsx | 114 +++++++++++-------- quartz/components/scripts/explorer.inline.ts | 7 +- quartz/plugins/index.ts | 1 + quartz/processors/parse.ts | 5 +- quartz/util/path.ts | 2 +- 7 files changed, 110 insertions(+), 90 deletions(-) diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index 8998c4064..a0b8cf564 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -68,8 +68,9 @@ export default ((opts?: Partial) => { // construct the index for the first time for (const file of allFiles) { if (file.slug?.endsWith("index")) { - const folderParts = file.filePath?.split("/") + const folderParts = file.slug?.split("/") if (folderParts) { + // 2nd last to exclude the /index const folderName = folderParts[folderParts?.length - 2] folderIndex.set(folderName, file) } @@ -88,7 +89,10 @@ export default ((opts?: Partial) => { // Try to resolve frontmatter folder title const currentFile = folderIndex?.get(curPathSegment) if (currentFile) { - curPathSegment = currentFile.frontmatter!.title + const title = currentFile.frontmatter!.title + if (title !== "index") { + curPathSegment = title + } } // Add current slug to full path diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index 95eac4304..e3ed9b1cc 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -12,6 +12,9 @@ const defaultOptions = { folderClickBehavior: "collapse", folderDefaultState: "collapsed", useSavedState: true, + mapFn: (node) => { + return node + }, sortFn: (a, b) => { // Sort order: folders first, then files. Sort folders and files alphabetically if ((!a.file && !b.file) || (a.file && b.file)) { @@ -22,6 +25,7 @@ const defaultOptions = { sensitivity: "base", }) } + if (a.file && !b.file) { return 1 } else { @@ -41,46 +45,34 @@ export default ((userOpts?: Partial) => { let jsonTree: string function constructFileTree(allFiles: QuartzPluginData[]) { - if (!fileTree) { - // Construct tree from allFiles - fileTree = new FileNode("") - allFiles.forEach((file) => fileTree.add(file, 1)) + if (fileTree) { + return + } - /** - * Keys of this object must match corresponding function name of `FileNode`, - * while values must be the argument that will be passed to the function. - * - * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options) - */ - const functions = { - map: opts.mapFn, - sort: opts.sortFn, - filter: opts.filterFn, - } + // Construct tree from allFiles + fileTree = new FileNode("") + allFiles.forEach((file) => fileTree.add(file)) - // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) - if (opts.order) { - // Order is important, use loop with index instead of order.map() - for (let i = 0; i < opts.order.length; i++) { - const functionName = opts.order[i] - if (functions[functionName]) { - // for every entry in order, call matching function in FileNode and pass matching argument - // e.g. i = 0; functionName = "filter" - // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn) - - // @ts-ignore - // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning - fileTree[functionName].call(fileTree, functions[functionName]) - } + // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) + if (opts.order) { + // Order is important, use loop with index instead of order.map() + for (let i = 0; i < opts.order.length; i++) { + const functionName = opts.order[i] + if (functionName === "map") { + fileTree.map(opts.mapFn) + } else if (functionName === "sort") { + fileTree.sort(opts.sortFn) + } else if (functionName === "filter") { + fileTree.filter(opts.filterFn) } } - - // Get all folders of tree. Initialize with collapsed state - const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") - - // Stringify to pass json tree as data attribute ([data-tree]) - jsonTree = JSON.stringify(folders) } + + // Get all folders of tree. Initialize with collapsed state + const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") + + // Stringify to pass json tree as data attribute ([data-tree]) + jsonTree = JSON.stringify(folders) } function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { @@ -120,6 +112,7 @@ export default ((userOpts?: Partial) => { ) } + Explorer.css = explorerStyle Explorer.afterDOMLoaded = script return Explorer diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index e5ceb0bf3..118f25b62 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -1,6 +1,13 @@ // @ts-ignore import { QuartzPluginData } from "../plugins/vfile" -import { resolveRelative } from "../util/path" +import { + joinSegments, + resolveRelative, + clone, + simplifySlug, + SimpleSlug, + FilePath, +} from "../util/path" type OrderEntries = "sort" | "filter" | "map" @@ -10,9 +17,9 @@ export interface Options { folderClickBehavior: "collapse" | "link" useSavedState: boolean sortFn: (a: FileNode, b: FileNode) => number - filterFn?: (node: FileNode) => boolean - mapFn?: (node: FileNode) => void - order?: OrderEntries[] + filterFn: (node: FileNode) => boolean + mapFn: (node: FileNode) => void + order: OrderEntries[] } type DataWrapper = { @@ -25,59 +32,74 @@ export type FolderState = { collapsed: boolean } +function getPathSegment(fp: FilePath | undefined, idx: number): string | undefined { + if (!fp) { + return undefined + } + + return fp.split("/").at(idx) +} + // Structure to add all files into a tree export class FileNode { - children: FileNode[] - name: string + children: Array + name: string // this is the slug segment displayName: string file: QuartzPluginData | null depth: number - constructor(name: string, file?: QuartzPluginData, depth?: number) { + constructor(slugSegment: string, displayName?: string, file?: QuartzPluginData, depth?: number) { this.children = [] - this.name = name - this.displayName = name - this.file = file ? structuredClone(file) : null + this.name = slugSegment + this.displayName = displayName ?? file?.frontmatter?.title ?? slugSegment + this.file = file ? clone(file) : null this.depth = depth ?? 0 } - private insert(file: DataWrapper) { - if (file.path.length === 1) { - if (file.path[0] !== "index.md") { - this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) - } else { - const title = file.file.frontmatter?.title - if (title && title !== "index" && file.path[0] === "index.md") { + private insert(fileData: DataWrapper) { + if (fileData.path.length === 0) { + return + } + + const nextSegment = fileData.path[0] + + // base case, insert here + if (fileData.path.length === 1) { + if (nextSegment === "") { + // index case (we are the root and we just found index.md), set our data appropriately + const title = fileData.file.frontmatter?.title + if (title && title !== "index") { this.displayName = title } - } - } else { - const next = file.path[0] - file.path = file.path.splice(1) - for (const child of this.children) { - if (child.name === next) { - child.insert(file) - return - } + } else { + // direct child + this.children.push(new FileNode(nextSegment, undefined, fileData.file, this.depth + 1)) } - const newChild = new FileNode(next, undefined, this.depth + 1) - newChild.insert(file) - this.children.push(newChild) + return } + + // find the right child to insert into + fileData.path = fileData.path.splice(1) + const child = this.children.find((c) => c.name === nextSegment) + if (child) { + child.insert(fileData) + return + } + + const newChild = new FileNode( + nextSegment, + getPathSegment(fileData.file.relativePath, this.depth), + undefined, + this.depth + 1, + ) + newChild.insert(fileData) + this.children.push(newChild) } // Add new file to tree - add(file: QuartzPluginData, splice: number = 0) { - this.insert({ file, path: file.filePath!.split("/").splice(splice) }) - } - - // Print tree structure (for debugging) - print(depth: number = 0) { - let folderChar = "" - if (!this.file) folderChar = "|" - console.log("-".repeat(depth), folderChar, this.name, this.depth) - this.children.forEach((e) => e.print(depth + 1)) + add(file: QuartzPluginData) { + this.insert({ file: file, path: simplifySlug(file.slug!).split("/") }) } /** @@ -95,7 +117,6 @@ export class FileNode { */ map(mapFn: (node: FileNode) => void) { mapFn(this) - this.children.forEach((child) => child.map(mapFn)) } @@ -110,16 +131,16 @@ export class FileNode { const traverse = (node: FileNode, currentPath: string) => { if (!node.file) { - const folderPath = currentPath + (currentPath ? "/" : "") + node.name + const folderPath = joinSegments(currentPath, node.name) if (folderPath !== "") { folderPaths.push({ path: folderPath, collapsed }) } + node.children.forEach((child) => traverse(child, folderPath)) } } traverse(this, "") - return folderPaths } @@ -147,10 +168,9 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro const isDefaultOpen = opts.folderDefaultState === "open" // Calculate current folderPath - let pathOld = fullPath ? fullPath : "" let folderPath = "" if (node.name !== "") { - folderPath = `${pathOld}/${node.name}` + folderPath = joinSegments(fullPath ?? "", node.name) } return ( @@ -185,7 +205,11 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro {/* render tag if folderBehavior is "link", otherwise render )} @@ -241,8 +241,8 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro ))} - + )} - + ) } diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index 28e9f9bb2..ff046a665 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -106,7 +106,7 @@ svg { align-items: center; font-family: var(--headerFont); - & p { + & span { font-size: 0.95rem; display: inline-block; color: var(--secondary); From e1b6a0014cabf07ddc7e7931912aece471959224 Mon Sep 17 00:00:00 2001 From: Sidney <85735034+sidney-eliot@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:04:15 +0100 Subject: [PATCH 07/17] docs: add explorer example for advanced `sortFn` (#564) * Added doc example to explorer sortFn * Prettier fixed formatting * Let Prettier fix the formatting of the entire markdown file * Updated example * Added extra commentary and fixed example * Update docs/features/explorer.md * doc fixes * docs: remove leftover TODO * docs: move example to `advanced` --------- Co-authored-by: Sidney <85735034+Epicrex@users.noreply.github.com> Co-authored-by: Jacky Zhao Co-authored-by: Ben Schlegel --- docs/features/explorer.md | 115 +++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/docs/features/explorer.md b/docs/features/explorer.md index fd656a888..f4d54faaf 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -179,6 +179,34 @@ Component.Explorer({ ## Advanced examples +> [!tip] +> When writing more complicated functions, the `layout` file can start to look very cramped. +> You can fix this by defining your functions in another file. +> +> ```ts title="functions.ts" +> import { Options } from "./quartz/components/ExplorerNode" +> export const mapFn: Options["mapFn"] = (node) => { +> // implement your function here +> } +> export const filterFn: Options["filterFn"] = (node) => { +> // implement your function here +> } +> export const sortFn: Options["sortFn"] = (a, b) => { +> // implement your function here +> } +> ``` +> +> You can then import them like this: +> +> ```ts title="quartz.layout.ts" +> import { mapFn, filterFn, sortFn } from "./functions.ts" +> Component.Explorer({ +> mapFn: mapFn, +> filterFn: filterFn, +> sortFn: sortFn, +> }) +> ``` + ### Add emoji prefix To add emoji prefixes (📁 for folders, 📄 for files), you could use a map function like this: @@ -216,30 +244,63 @@ Notice how we customized the `order` array here. This is done because the defaul To fix this, we just changed around the order and apply the `sort` function before changing the display names in the `map` function. -> [!tip] -> When writing more complicated functions, the `layout` file can start to look very cramped. -> You can fix this by defining your functions in another file. -> -> ```ts title="functions.ts" -> import { Options } from "./quartz/components/ExplorerNode" -> export const mapFn: Options["mapFn"] = (node) => { -> // implement your function here -> } -> export const filterFn: Options["filterFn"] = (node) => { -> // implement your function here -> } -> export const sortFn: Options["sortFn"] = (a, b) => { -> // implement your function here -> } -> ``` -> -> You can then import them like this: -> -> ```ts title="quartz.layout.ts" -> import { mapFn, filterFn, sortFn } from "./functions.ts" -> Component.Explorer({ -> mapFn: mapFn, -> filterFn: filterFn, -> sortFn: sortFn, -> }) -> ``` +### Use `sort` with pre-defined sort order + +Here's another example where a map containing file/folder names (as slugs) is used to define the sort order of the explorer in quartz. All files/folders that aren't listed inside of `nameOrderMap` will appear at the top of that folders hierarchy level. + +It's also worth mentioning, that the smaller the number set in `nameOrderMap`, the higher up the entry will be in the explorer. Incrementing every folder/file by 100, makes ordering files in their folders a lot easier. Lastly, this example still allows you to use a `mapFn` or frontmatter titles to change display names, as it uses slugs for `nameOrderMap` (which is unaffected by display name changes). + +```ts title="quartz.layout.ts" +Component.Explorer({ + sortFn: (a, b) => { + const nameOrderMap: Record = { + "poetry-folder": 100, + "essay-folder": 200, + "research-paper-file": 201, + "dinosaur-fossils-file": 300, + "other-folder": 400, + } + + let orderA = 0 + let orderB = 0 + + if (a.file && a.file.slug) { + orderA = nameOrderMap[a.file.slug] || 0 + } else if (a.name) { + orderA = nameOrderMap[a.name] || 0 + } + + if (b.file && b.file.slug) { + orderB = nameOrderMap[b.file.slug] || 0 + } else if (b.name) { + orderB = nameOrderMap[b.name] || 0 + } + + return orderA - orderB + }, +}) +``` + +For reference, this is how the quartz explorer window would look like with that example: + +``` +📖 Poetry Folder +📑 Essay Folder + ⚗ïļ Research Paper File +ðŸĶī Dinosaur Fossils File +ðŸ”Ū Other Folder +``` + +And this is how the file structure would look like: + +``` +index.md +poetry-folder + index.md +essay-folder + index.md + research-paper-file.md +dinosaur-fossils-file.md +other-folder + index.md +``` From dafc9f318e29ab444c12e84564b38b5318a13d78 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 28 Dec 2023 08:02:04 -0800 Subject: [PATCH 08/17] feat: minify js scripts (closes #655) (#657) --- quartz/plugins/emitters/componentResources.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 884db4dd1..b51d09191 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -14,6 +14,7 @@ import { StaticResources } from "../../util/resources" import { QuartzComponent } from "../../components/types" import { googleFontHref, joinStyles } from "../../util/theme" import { Features, transform } from "lightningcss" +import { transform as transpile } from "esbuild" type ComponentResources = { css: string[] @@ -56,9 +57,16 @@ function getComponentResources(ctx: BuildCtx): ComponentResources { } } -function joinScripts(scripts: string[]): string { +async function joinScripts(scripts: string[]): Promise { // wrap with iife to prevent scope collision - return scripts.map((script) => `(function () {${script}})();`).join("\n") + const script = scripts.map((script) => `(function () {${script}})();`).join("\n") + + // minify with esbuild + const res = await transpile(script, { + minify: true, + }) + + return res.code } function addGlobalPageResources( @@ -165,8 +173,11 @@ export const ComponentResources: QuartzEmitterPlugin = (opts?: Partial< addGlobalPageResources(ctx, resources, componentResources) const stylesheet = joinStyles(ctx.cfg.configuration.theme, ...componentResources.css, styles) - const prescript = joinScripts(componentResources.beforeDOMLoaded) - const postscript = joinScripts(componentResources.afterDOMLoaded) + const [prescript, postscript] = await Promise.all([ + joinScripts(componentResources.beforeDOMLoaded), + joinScripts(componentResources.afterDOMLoaded), + ]) + const fps = await Promise.all([ emit({ slug: "index" as FullSlug, From 359484c139c074e60f188616b3f6435cde46c42e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 28 Dec 2023 08:48:14 -0800 Subject: [PATCH 09/17] fix: more robust tags parsing --- quartz/plugins/transformers/frontmatter.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index d50217ba6..26a665d8f 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -49,11 +49,19 @@ export const FrontMatter: QuartzTransformerPlugin | undefined> data.title = file.stem ?? "Untitled" } - if (data.tags && !Array.isArray(data.tags)) { + if (data.tags) { + // coerce to array + if (!Array.isArray(data.tags)) { + data.tags = data.tags + .toString() + .split(oneLineTagDelim) + .map((tag: string) => tag.trim()) + } + + // remove all non-string tags data.tags = data.tags - .toString() - .split(oneLineTagDelim) - .map((tag: string) => tag.trim()) + .filter((tag: unknown) => typeof tag === "string" || typeof tag === "number") + .map((tag: string | number) => tag.toString()) } // slug them all!! From 68f53352e715861b155bd11baffe9f6e3032ff1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Thu, 28 Dec 2023 17:49:35 +0100 Subject: [PATCH 10/17] feat: Self-hosted Plausible support (#656) * Self-hosted Plausible support * Remove leftover import --- package-lock.json | 9 --------- package.json | 1 - quartz/cfg.ts | 1 + quartz/components/scripts/plausible.inline.ts | 3 --- quartz/plugins/emitters/componentResources.ts | 17 ++++++++++++++--- 5 files changed, 15 insertions(+), 16 deletions(-) delete mode 100644 quartz/components/scripts/plausible.inline.ts diff --git a/package-lock.json b/package-lock.json index dd88ff7da..ecf6a2a75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "mdast-util-to-hast": "^13.0.2", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "plausible-tracker": "^0.3.8", "preact": "^10.19.3", "preact-render-to-string": "^6.3.1", "pretty-bytes": "^6.1.1", @@ -4451,14 +4450,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/plausible-tracker": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz", - "integrity": "sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==", - "engines": { - "node": ">=10" - } - }, "node_modules/preact": { "version": "10.19.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", diff --git a/package.json b/package.json index 70df83305..b99471d76 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "mdast-util-to-hast": "^13.0.2", "mdast-util-to-string": "^4.0.0", "micromorph": "^0.4.5", - "plausible-tracker": "^0.3.8", "preact": "^10.19.3", "preact-render-to-string": "^6.3.1", "pretty-bytes": "^6.1.1", diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 8371b5e2b..7f0f206e1 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -7,6 +7,7 @@ export type Analytics = | null | { provider: "plausible" + host?: string } | { provider: "google" diff --git a/quartz/components/scripts/plausible.inline.ts b/quartz/components/scripts/plausible.inline.ts deleted file mode 100644 index 704f5d5fe..000000000 --- a/quartz/components/scripts/plausible.inline.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Plausible from "plausible-tracker" -const { trackPageview } = Plausible() -document.addEventListener("nav", () => trackPageview()) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index b51d09191..e8a81bc0b 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -4,8 +4,6 @@ import { QuartzEmitterPlugin } from "../types" // @ts-ignore import spaRouterScript from "../../components/scripts/spa.inline" // @ts-ignore -import plausibleScript from "../../components/scripts/plausible.inline" -// @ts-ignore import popoverScript from "../../components/scripts/popover.inline" import styles from "../../styles/custom.scss" import popoverStyle from "../../components/styles/popover.scss" @@ -103,7 +101,20 @@ function addGlobalPageResources( }); });`) } else if (cfg.analytics?.provider === "plausible") { - componentResources.afterDOMLoaded.push(plausibleScript) + const plausibleHost = cfg.analytics.host ?? "https://plausible.io" + componentResources.afterDOMLoaded.push(` + const plausibleScript = document.createElement("script") + plausibleScript.src = "${plausibleHost}/js/script.manual.js" + plausibleScript.setAttribute("data-domain", location.hostname) + plausibleScript.defer = true + document.head.appendChild(plausibleScript) + + window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) } + + document.addEventListener("nav", () => { + plausible("pageview") + }) + `) } else if (cfg.analytics?.provider === "umami") { componentResources.afterDOMLoaded.push(` const umamiScript = document.createElement("script") From e277ed5c307cd263b0f07dfece45946c3fe195d9 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 28 Dec 2023 08:54:04 -0800 Subject: [PATCH 11/17] fix: use joinSegment instead of joining via slash in sitemap (closes #658) --- quartz/plugins/emitters/contentIndex.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index c5170c64a..bc4c6c325 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -2,7 +2,7 @@ import { Root } from "hast" import { GlobalConfiguration } from "../../cfg" import { getDate } from "../../components/Date" import { escapeHTML } from "../../util/escape" -import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path" +import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import { toHtml } from "hast-util-to-html" import path from "path" @@ -37,7 +37,7 @@ const defaultOptions: Options = { function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { const base = cfg.baseUrl ?? "" const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` - https://${base}/${encodeURI(slug)} + https://${joinSegments(base, encodeURI(slug))} ${content.date?.toISOString()} ` const urls = Array.from(idx) @@ -52,8 +52,8 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex, limit?: nu const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => ` ${escapeHTML(content.title)} - ${root}/${encodeURI(slug)} - ${root}/${encodeURI(slug)} + ${joinSegments(root, encodeURI(slug))} + ${joinSegments(root, encodeURI(slug))} ${content.richContent ?? content.description} ${content.date?.toUTCString()} ` From 4b6c7aeffe6028f68af7704d7928584a46d3bb1f Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 28 Dec 2023 13:56:20 -0800 Subject: [PATCH 12/17] feat: lazyLoading specifier in link transformer --- quartz/plugins/transformers/links.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 3072959df..50d2d1a0a 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -12,6 +12,7 @@ import { import path from "path" import { visit } from "unist-util-visit" import isAbsoluteUrl from "is-absolute-url" +import { Root } from "hast" interface Options { /** How to resolve Markdown paths */ @@ -19,12 +20,14 @@ interface Options { /** Strips folders from a link so that it looks nice */ prettyLinks: boolean openLinksInNewTab: boolean + lazyLoad: boolean } const defaultOptions: Options = { markdownLinkResolution: "absolute", prettyLinks: true, openLinksInNewTab: false, + lazyLoad: false, } export const CrawlLinks: QuartzTransformerPlugin | undefined> = (userOpts) => { @@ -34,7 +37,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = htmlPlugins(ctx) { return [ () => { - return (tree, file) => { + return (tree: Root, file) => { const curSlug = simplifySlug(file.data.slug!) const outgoing: Set = new Set() @@ -51,8 +54,8 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = typeof node.properties.href === "string" ) { let dest = node.properties.href as RelativeURL - node.properties.className ??= [] - node.properties.className.push(isAbsoluteUrl(dest) ? "external" : "internal") + const classes = (node.properties.className ?? []) as string[] + classes.push(isAbsoluteUrl(dest) ? "external" : "internal") // Check if the link has alias text if ( @@ -61,8 +64,9 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = node.children[0].value !== dest ) { // Add the 'alias' class if the text content is not the same as the href - node.properties.className.push("alias") + classes.push("alias") } + node.properties.className = classes if (opts.openLinksInNewTab) { node.properties.target = "_blank" @@ -111,6 +115,10 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = node.properties && typeof node.properties.src === "string" ) { + if (opts.lazyLoad) { + node.properties.loading = "lazy" + } + if (!isAbsoluteUrl(node.properties.src)) { let dest = node.properties.src as RelativeURL dest = node.properties.src = transformLink( From e758cbe1ee7a2bb128cbd95fd6d3cdcd34623800 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 28 Dec 2023 14:00:15 -0800 Subject: [PATCH 13/17] pkg: bump version to 4.1.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ecf6a2a75..73531b4b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.1.3", + "version": "4.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.1.3", + "version": "4.1.4", "license": "MIT", "dependencies": { "@clack/prompts": "^0.7.0", diff --git a/package.json b/package.json index b99471d76..bb79280d8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@jackyzha0/quartz", "description": "ðŸŒą publish your digital garden and notes as a website", "private": true, - "version": "4.1.3", + "version": "4.1.4", "type": "module", "author": "jackyzha0 ", "license": "MIT", From 40cfccdc77e1734e12537359a70e71b74d88ec72 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 28 Dec 2023 15:07:59 -0800 Subject: [PATCH 14/17] style: relative back on pre --- quartz/styles/base.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index af9c6f7d4..607749aa1 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -332,6 +332,7 @@ pre { border-radius: 5px; overflow-x: auto; border: 1px solid var(--lightgray); + position: relative; &:has(> code.mermaid) { border: none; From e603d7396b2eae90d1edb055e32fef33a5e77028 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 1 Jan 2024 08:58:25 -0800 Subject: [PATCH 15/17] fix: parse emoji tags in body (closes #659) --- quartz/plugins/transformers/ofm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index f8a83930a..5aa09c5bb 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -125,7 +125,7 @@ const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm") // #(...) -> capturing group, tag itself must start with # // (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores // (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/" -const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\d])+(?:\/[-_\p{L}\d]+)*)/, "gu") +const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\p{Emoji}\d])+(?:\/[-_\p{L}\p{Emoji}\d]+)*)/, "gu") const blockReferenceRegex = new RegExp(/\^([A-Za-z0-9]+)$/, "g") export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( From 002bbc37b140d177276cd655049e7de2072fa537 Mon Sep 17 00:00:00 2001 From: Jimmy He <417267+hhe@users.noreply.github.com> Date: Mon, 1 Jan 2024 14:14:37 -0800 Subject: [PATCH 16/17] fix: Continue setup even if a file to delete is not found (#663) * Continue setup even if a file to delete is not found For various reasons, `.gitkeep` may be deleted already. (In my case, even though I followed the [Getting Started](https://quartz.jzhao.xyz) instructions exactly, my first run resulted in an `fatal: 'upstream' does not appear to be a git repository`) If we try to delete `.gitkeep` again and don't ignore `ENOENT`, then the whole setup fails. * Use fs.existsSync --- quartz/cli/handlers.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quartz/cli/handlers.js b/quartz/cli/handlers.js index 586881af9..37762a4fb 100644 --- a/quartz/cli/handlers.js +++ b/quartz/cli/handlers.js @@ -113,7 +113,10 @@ export async function handleCreate(argv) { } } - await fs.promises.unlink(path.join(contentFolder, ".gitkeep")) + const gitkeepPath = path.join(contentFolder, ".gitkeep") + if (fs.existsSync(gitkeepPath)) { + await fs.promises.unlink(gitkeepPath) + } if (setupStrategy === "copy" || setupStrategy === "symlink") { let originalFolder = sourceDirectory From b33f13ccaf4ec14a94ee0ee467dda04cf4981d00 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Mon, 1 Jan 2024 14:20:27 -0800 Subject: [PATCH 17/17] fix: dont show last page if folder --- quartz.layout.ts | 2 +- quartz/components/Breadcrumbs.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz.layout.ts b/quartz.layout.ts index 8b6edd8f8..4e8a85ff4 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -37,7 +37,7 @@ export const defaultContentPageLayout: PageLayout = { // components for pages that display lists of pages (e.g. tags or folders) export const defaultListPageLayout: PageLayout = { - beforeBody: [Component.ArticleTitle()], + beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle()], left: [ Component.PageTitle(), Component.MobileOnly(Component.Spacer()), diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index a0b8cf564..175f6f39d 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -104,7 +104,7 @@ export default ((opts?: Partial) => { } // Add current file to crumb (can directly use frontmatter title) - if (options.showCurrentPage) { + if (options.showCurrentPage && slugParts.at(-1) === "") { crumbs.push({ displayName: fileData.frontmatter!.title, path: "",