From 9316ddf2f5ca7d0a08b06a9a6d548e9ad180fc4f Mon Sep 17 00:00:00 2001 From: Taha <62564400+mt190502@users.noreply.github.com> Date: Fri, 21 Mar 2025 19:58:10 +0300 Subject: [PATCH 01/12] fix(analytics): fix the load of the analytics scripts (#1865) * fix(analytics): fix the load of the analytics scripts * chore(lint): fix lint issues --- quartz/plugins/emitters/componentResources.ts | 133 ++++++++++-------- 1 file changed, 76 insertions(+), 57 deletions(-) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 5dd67e6aa..92794ef5e 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -88,89 +88,108 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso if (cfg.analytics?.provider === "google") { const tagId = cfg.analytics.tagId componentResources.afterDOMLoaded.push(` - const gtagScript = document.createElement("script") - gtagScript.src = "https://www.googletagmanager.com/gtag/js?id=${tagId}" - gtagScript.defer = true - document.head.appendChild(gtagScript) - - window.dataLayer = window.dataLayer || []; - function gtag() { dataLayer.push(arguments); } - gtag("js", new Date()); - gtag("config", "${tagId}", { send_page_view: false }); - - document.addEventListener("nav", () => { - gtag("event", "page_view", { - page_title: document.title, - page_location: location.href, + const gtagScript = document.createElement('script'); + gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id=${tagId}'; + gtagScript.defer = true; + gtagScript.onload = () => { + window.dataLayer = window.dataLayer || []; + function gtag() { + dataLayer.push(arguments); + } + gtag('js', new Date()); + gtag('config', '${tagId}', { send_page_view: false }); + gtag('event', 'page_view', { page_title: document.title, page_location: location.href }); + document.addEventListener('nav', () => { + gtag('event', 'page_view', { page_title: document.title, page_location: location.href }); }); - });`) + }; + + document.head.appendChild(gtagScript); + `) } else if (cfg.analytics?.provider === "plausible") { 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) + const plausibleScript = document.createElement('script'); + plausibleScript.src = '${plausibleHost}/js/script.manual.js'; + plausibleScript.setAttribute('data-domain', location.hostname); + plausibleScript.defer = true; + plausibleScript.onload = () => { + window.plausible = window.plausible || function () { (window.plausible.q = window.plausible.q || []).push(arguments); }; + plausible('pageview'); + document.addEventListener('nav', () => { + plausible('pageview'); + }); + }; - window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) } - - document.addEventListener("nav", () => { - plausible("pageview") - }) + document.head.appendChild(plausibleScript); `) } else if (cfg.analytics?.provider === "umami") { componentResources.afterDOMLoaded.push(` - const umamiScript = document.createElement("script") - umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js" - umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}") - umamiScript.setAttribute("data-auto-track", "false") - umamiScript.defer = true - document.head.appendChild(umamiScript) - - document.addEventListener("nav", () => { + const umamiScript = document.createElement("script"); + umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js"; + umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}"); + umamiScript.setAttribute("data-auto-track", "false"); + umamiScript.defer = true; + umamiScript.onload = () => { umami.track(); - }) + document.addEventListener("nav", () => { + umami.track(); + }); + }; + + document.head.appendChild(umamiScript); `) } else if (cfg.analytics?.provider === "goatcounter") { componentResources.afterDOMLoaded.push(` - const goatcounterScript = document.createElement("script") - goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}" - goatcounterScript.defer = true - goatcounterScript.setAttribute("data-goatcounter", - "https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count") - document.head.appendChild(goatcounterScript) + const goatcounterScript = document.createElement('script'); + goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}"; + goatcounterScript.defer = true; + goatcounterScript.setAttribute( + 'data-goatcounter', + "https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count" + ); + goatcounterScript.onload = () => { + window.goatcounter = { no_onload: true }; + goatcounter.count({ path: location.pathname }); + document.addEventListener('nav', () => { + goatcounter.count({ path: location.pathname }); + }); + }; - window.goatcounter = { no_onload: true } - document.addEventListener("nav", () => { - goatcounter.count({ path: location.pathname }) - }) + document.head.appendChild(goatcounterScript); `) } else if (cfg.analytics?.provider === "posthog") { componentResources.afterDOMLoaded.push(` - const posthogScript = document.createElement("script") + const posthogScript = document.createElement("script"); posthogScript.innerHTML= \`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n { + posthog.capture('$pageview', { path: location.pathname }); + + document.addEventListener('nav', () => { + posthog.capture('$pageview', { path: location.pathname }); + }); + }; - document.addEventListener("nav", () => { - posthog.capture('$pageview', { path: location.pathname }) - }) + document.head.appendChild(posthogScript); `) } else if (cfg.analytics?.provider === "tinylytics") { const siteId = cfg.analytics.siteId componentResources.afterDOMLoaded.push(` - const tinylyticsScript = document.createElement("script") - tinylyticsScript.src = "https://tinylytics.app/embed/${siteId}.js?spa" - tinylyticsScript.defer = true - document.head.appendChild(tinylyticsScript) - - document.addEventListener("nav", () => { - window.tinylytics.triggerUpdate() - }) + const tinylyticsScript = document.createElement('script'); + tinylyticsScript.src = 'https://tinylytics.app/embed/${siteId}.js?spa'; + tinylyticsScript.defer = true; + tinylyticsScript.onload = () => { + window.tinylytics.triggerUpdate(); + document.addEventListener('nav', () => { + window.tinylytics.triggerUpdate(); + }); + }; + + document.head.appendChild(tinylyticsScript); `) } else if (cfg.analytics?.provider === "cabin") { componentResources.afterDOMLoaded.push(` From 3ce6aa49bf25b26a4cb1bf18e9770271d132772d Mon Sep 17 00:00:00 2001 From: Karim <46734059+h-karim@users.noreply.github.com> Date: Fri, 21 Mar 2025 19:49:56 -0400 Subject: [PATCH 02/12] fix(ogImage): update socialImage path to include base URL if defined (#1858) * fix(ogImage): update socialImage path to include base URL if defined * feat(path): add function to check if a file path is absolute * fix(ogImage): handle absolute paths for user defined og image paths * docs(CustomOgImages): update socialImage property to accept full URLs * fix(ogImage): typo * fix(ogImage): improve user-defined OG image path handling * Update docs/plugins/CustomOgImages.md Co-authored-by: Jacky Zhao * Update quartz/plugins/emitters/ogImage.tsx Co-authored-by: Jacky Zhao * refactor(path): remove isAbsoluteFilePath function * fix(ogImage): update user-defined OG image path handling to support relative URLs * feat(ogImage): enhance user-defined OG image path handling with absolute URL support * refactor(ogImage): remove debug log for ogImagePath * feat(path): add isAbsoluteURL function and corresponding tests * refactor(path): remove unused URL import for isomorphic compatibility --------- Co-authored-by: Karim H Co-authored-by: Jacky Zhao --- docs/plugins/CustomOgImages.md | 2 +- quartz/plugins/emitters/ogImage.tsx | 12 +++++++++--- quartz/util/path.test.ts | 11 +++++++++++ quartz/util/path.ts | 10 ++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/plugins/CustomOgImages.md b/docs/plugins/CustomOgImages.md index 5d47c419c..4b5bf0391 100644 --- a/docs/plugins/CustomOgImages.md +++ b/docs/plugins/CustomOgImages.md @@ -62,7 +62,7 @@ The following properties can be used to customize your link previews: | `socialDescription` | `description` | Description to be used for preview. | | `socialImage` | `image`, `cover` | Link to preview image. | -The `socialImage` property should contain a link to an image relative to `quartz/static`. If you have a folder for all your images in `quartz/static/my-images`, an example for `socialImage` could be `"my-images/cover.png"`. +The `socialImage` property should contain a link to an image either relative to `quartz/static`, or a full URL. If you have a folder for all your images in `quartz/static/my-images`, an example for `socialImage` could be `"my-images/cover.png"`. Alternatively, you can use a fully qualified URL like `"https://example.com/cover.png"`. > [!info] Info > diff --git a/quartz/plugins/emitters/ogImage.tsx b/quartz/plugins/emitters/ogImage.tsx index 0b786955c..7fad8c530 100644 --- a/quartz/plugins/emitters/ogImage.tsx +++ b/quartz/plugins/emitters/ogImage.tsx @@ -1,7 +1,7 @@ import { QuartzEmitterPlugin } from "../types" import { i18n } from "../../i18n" import { unescapeHTML } from "../../util/escape" -import { FullSlug, getFileExtension, joinSegments, QUARTZ } from "../../util/path" +import { FullSlug, getFileExtension, isAbsoluteURL, joinSegments, QUARTZ } from "../../util/path" import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og" import sharp from "sharp" import satori, { SatoriOptions } from "satori" @@ -144,13 +144,19 @@ export const CustomOgImages: QuartzEmitterPlugin> = additionalHead: [ (pageData) => { const isRealFile = pageData.filePath !== undefined - const userDefinedOgImagePath = pageData.frontmatter?.socialImage + let userDefinedOgImagePath = pageData.frontmatter?.socialImage + + if (userDefinedOgImagePath) { + userDefinedOgImagePath = isAbsoluteURL(userDefinedOgImagePath) + ? userDefinedOgImagePath + : `https://${baseUrl}/static/${userDefinedOgImagePath}` + } + const generatedOgImagePath = isRealFile ? `https://${baseUrl}/${pageData.slug!}-og-image.webp` : undefined const defaultOgImagePath = `https://${baseUrl}/static/og-image.png` const ogImagePath = userDefinedOgImagePath ?? generatedOgImagePath ?? defaultOgImagePath - const ogImageMimeType = `image/${getFileExtension(ogImagePath) ?? "png"}` return ( <> diff --git a/quartz/util/path.test.ts b/quartz/util/path.test.ts index 29d845d95..9f94c68ec 100644 --- a/quartz/util/path.test.ts +++ b/quartz/util/path.test.ts @@ -38,6 +38,17 @@ describe("typeguards", () => { assert(!path.isRelativeURL("./abc/def.md")) }) + test("isAbsoluteURL", () => { + assert(path.isAbsoluteURL("https://example.com")) + assert(path.isAbsoluteURL("http://example.com")) + assert(path.isAbsoluteURL("ftp://example.com/a/b/c")) + assert(path.isAbsoluteURL("http://host/%25")) + assert(path.isAbsoluteURL("file://host/twoslashes?more//slashes")) + + assert(!path.isAbsoluteURL("example.com/abc/def")) + assert(!path.isAbsoluteURL("abc")) + }) + test("isFullSlug", () => { assert(path.isFullSlug("index")) assert(path.isFullSlug("abc/def")) diff --git a/quartz/util/path.ts b/quartz/util/path.ts index 0681fae72..b95770159 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -1,6 +1,7 @@ import { slug as slugAnchor } from "github-slugger" import type { Element as HastElement } from "hast" import { clone } from "./clone" + // this file must be isomorphic so it can't use node libs (e.g. path) export const QUARTZ = "quartz" @@ -39,6 +40,15 @@ export function isRelativeURL(s: string): s is RelativeURL { return validStart && validEnding && ![".md", ".html"].includes(getFileExtension(s) ?? "") } +export function isAbsoluteURL(s: string): boolean { + try { + new URL(s) + } catch { + return false + } + return true +} + export function getFullSlug(window: Window): FullSlug { const res = window.document.body.dataset.slug! as FullSlug return res From 457b77dd485f08616bd597492cdc26ed89ae92d3 Mon Sep 17 00:00:00 2001 From: Emile Bangma Date: Sat, 22 Mar 2025 03:59:43 +0100 Subject: [PATCH 03/12] fix(frontmatter): prevent slug duplication through frontmatter (#1860) * fix(frontmatter): prevent slug duplication through frontmatter * Simplify duplicate slug checks * Update quartz/plugins/transformers/frontmatter.ts Co-authored-by: Jacky Zhao * lint --------- Co-authored-by: Jacky Zhao --- quartz/plugins/transformers/frontmatter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index c04c52a24..ce574280f 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -114,6 +114,10 @@ export const FrontMatter: QuartzTransformerPlugin> = (userOpts) if (socialImage) data.socialImage = socialImage + // Remove duplicate slugs + const uniqueSlugs = [...new Set(allSlugs)] + allSlugs.splice(0, allSlugs.length, ...uniqueSlugs) + // fill in frontmatter file.data.frontmatter = data as QuartzPluginData["frontmatter"] } From 4e74d11b1aee8c0affa0b13ba7b174d175ca3244 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 17:24:43 -0700 Subject: [PATCH 04/12] fix: cleanup a href link construction, global shared trie, breadcrumbs use trie --- quartz/build.ts | 1 + quartz/components/Breadcrumbs.tsx | 90 ++++++----------------- quartz/components/TagList.tsx | 5 +- quartz/components/pages/FolderContent.tsx | 25 +------ quartz/components/pages/TagContent.tsx | 7 +- quartz/util/ctx.ts | 27 ++++++- quartz/util/fileTrie.test.ts | 82 +++++++++++++++++++++ quartz/util/fileTrie.ts | 18 +++++ 8 files changed, 159 insertions(+), 96 deletions(-) diff --git a/quartz/build.ts b/quartz/build.ts index 7cf440569..032063f49 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -21,6 +21,7 @@ import { getStaticResourcesFromPlugins } from "./plugins" import { randomIdNonSecure } from "./util/random" import { ChangeEvent } from "./plugins/types" import { minimatch } from "minimatch" +import { FileTrieNode } from "./util/fileTrie" type ContentMap = Map< FilePath, diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index d0faeabb8..5144a314d 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -1,8 +1,8 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import breadcrumbsStyle from "./styles/breadcrumbs.scss" -import { FullSlug, SimpleSlug, joinSegments, resolveRelative } from "../util/path" -import { QuartzPluginData } from "../plugins/vfile" +import { FullSlug, SimpleSlug, resolveRelative, simplifySlug } from "../util/path" import { classNames } from "../util/lang" +import { trieFromAllFiles } from "../util/ctx" type CrumbData = { displayName: string @@ -22,10 +22,6 @@ interface BreadcrumbOptions { * Whether to look up frontmatter title for folders (could cause performance problems with big vaults) */ resolveFrontmatterTitle: boolean - /** - * Whether to display breadcrumbs on root `index.md` - */ - hideOnRoot: boolean /** * Whether to display the current page in the breadcrumbs. */ @@ -36,7 +32,6 @@ const defaultOptions: BreadcrumbOptions = { spacerSymbol: "❯", rootName: "Home", resolveFrontmatterTitle: true, - hideOnRoot: true, showCurrentPage: true, } @@ -48,78 +43,37 @@ function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: Simpl } export default ((opts?: Partial) => { - // Merge options with defaults const options: BreadcrumbOptions = { ...defaultOptions, ...opts } - - // computed index of folder name to its associated file data - let folderIndex: Map | undefined - const Breadcrumbs: QuartzComponent = ({ fileData, allFiles, displayClass, + ctx, }: QuartzComponentProps) => { - // Hide crumbs on root if enabled - if (options.hideOnRoot && fileData.slug === "index") { - return <> + const trie = (ctx.trie ??= trieFromAllFiles(allFiles)) + const slugParts = fileData.slug!.split("/") + const pathNodes = trie.ancestryChain(slugParts) + + if (!pathNodes) { + return null } - // Format entry for root element - const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug) - const crumbs: CrumbData[] = [firstEntry] - - if (!folderIndex && options.resolveFrontmatterTitle) { - folderIndex = new Map() - // construct the index for the first time - for (const file of allFiles) { - const folderParts = file.slug?.split("/") - if (folderParts?.at(-1) === "index") { - folderIndex.set(folderParts.slice(0, -1).join("/"), file) - } - } - } - - // Split slug into hierarchy/parts - const slugParts = fileData.slug?.split("/") - if (slugParts) { - // is tag breadcrumb? - const isTagPath = slugParts[0] === "tags" - - // full path until current part - let currentPath = "" - - for (let i = 0; i < slugParts.length - 1; i++) { - let curPathSegment = slugParts[i] - - // Try to resolve frontmatter folder title - const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/")) - if (currentFile) { - const title = currentFile.frontmatter!.title - if (title !== "index") { - curPathSegment = title - } - } - - // Add current slug to full path - currentPath = joinSegments(currentPath, slugParts[i]) - const includeTrailingSlash = !isTagPath || i < slugParts.length - 1 - - // Format and add current crumb - const crumb = formatCrumb( - curPathSegment, - fileData.slug!, - (currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug, - ) - crumbs.push(crumb) + const crumbs: CrumbData[] = pathNodes.map((node, idx) => { + const crumb = formatCrumb(node.displayName, fileData.slug!, simplifySlug(node.slug)) + if (idx === 0) { + crumb.displayName = options.rootName } - // Add current file to crumb (can directly use frontmatter title) - if (options.showCurrentPage && slugParts.at(-1) !== "index") { - crumbs.push({ - displayName: fileData.frontmatter!.title, - path: "", - }) + // For last node (current page), set empty path + if (idx === pathNodes.length - 1) { + crumb.path = "" } + + return crumb + }) + + if (!options.showCurrentPage) { + crumbs.pop() } return ( diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx index 4a89fbd67..c73ed392a 100644 --- a/quartz/components/TagList.tsx +++ b/quartz/components/TagList.tsx @@ -1,15 +1,14 @@ -import { pathToRoot, slugTag } from "../util/path" +import { FullSlug, resolveRelative } from "../util/path" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { classNames } from "../util/lang" const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentProps) => { const tags = fileData.frontmatter?.tags - const baseDir = pathToRoot(fileData.slug!) if (tags && tags.length > 0) { return (
    {tags.map((tag) => { - const linkDest = baseDir + `/tags/${slugTag(tag)}` + const linkDest = resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug) return (
  • diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index 9621f4f1a..afd4f5d7e 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -8,7 +8,8 @@ import { i18n } from "../../i18n" import { QuartzPluginData } from "../../plugins/vfile" import { ComponentChildren } from "preact" import { concatenateResources } from "../../util/resources" -import { FileTrieNode } from "../../util/fileTrie" +import { trieFromAllFiles } from "../../util/ctx" + interface FolderContentOptions { /** * Whether to display number of folders @@ -25,31 +26,11 @@ const defaultOptions: FolderContentOptions = { export default ((opts?: Partial) => { const options: FolderContentOptions = { ...defaultOptions, ...opts } - let trie: FileTrieNode< - QuartzPluginData & { - slug: string - title: string - filePath: string - } - > const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { const { tree, fileData, allFiles, cfg } = props - if (!trie) { - trie = new FileTrieNode([]) - allFiles.forEach((file) => { - if (file.frontmatter) { - trie.add({ - ...file, - slug: file.slug!, - title: file.frontmatter.title, - filePath: file.filePath!, - }) - } - }) - } - + const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles)) const folder = trie.findNode(fileData.slug!.split("/")) if (!folder) { return null diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index 03051d3a8..5e81901df 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -1,7 +1,7 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" import style from "../styles/listPage.scss" import { PageList, SortFn } from "../PageList" -import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" +import { FullSlug, getAllSegmentPrefixes, resolveRelative, simplifySlug } from "../../util/path" import { QuartzPluginData } from "../../plugins/vfile" import { Root } from "hast" import { htmlToJsx } from "../../util/jsx" @@ -74,10 +74,13 @@ export default ((opts?: Partial) => { ? contentPage?.description : htmlToJsx(contentPage.filePath!, root) + const tagListingPage = `/tags/${tag}` as FullSlug + const href = resolveRelative(fileData.slug!, tagListingPage) + return (

    - + {tag}

    diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts index b3e7a37f5..80115ec27 100644 --- a/quartz/util/ctx.ts +++ b/quartz/util/ctx.ts @@ -1,4 +1,6 @@ import { QuartzConfig } from "../cfg" +import { QuartzPluginData } from "../plugins/vfile" +import { FileTrieNode } from "./fileTrie" import { FilePath, FullSlug } from "./path" export interface Argv { @@ -13,13 +15,36 @@ export interface Argv { concurrency?: number } +export type BuildTimeTrieData = QuartzPluginData & { + slug: string + title: string + filePath: string +} + export interface BuildCtx { buildId: string argv: Argv cfg: QuartzConfig allSlugs: FullSlug[] allFiles: FilePath[] + trie?: FileTrieNode incremental: boolean } -export type WorkerSerializableBuildCtx = Omit +export function trieFromAllFiles(allFiles: QuartzPluginData[]): FileTrieNode { + const trie = new FileTrieNode([]) + allFiles.forEach((file) => { + if (file.frontmatter) { + trie.add({ + ...file, + slug: file.slug!, + title: file.frontmatter.title, + filePath: file.filePath!, + }) + } + }) + + return trie +} + +export type WorkerSerializableBuildCtx = Omit diff --git a/quartz/util/fileTrie.test.ts b/quartz/util/fileTrie.test.ts index d66e14266..c747f349f 100644 --- a/quartz/util/fileTrie.test.ts +++ b/quartz/util/fileTrie.test.ts @@ -330,4 +330,86 @@ describe("FileTrie", () => { ) }) }) + + describe("pathToNode", () => { + test("should return root node for empty path", () => { + const data = { title: "Root", slug: "index", filePath: "index.md" } + trie.add(data) + const path = trie.ancestryChain([]) + assert.deepStrictEqual(path, [trie]) + }) + + test("should return root node for index path", () => { + const data = { title: "Root", slug: "index", filePath: "index.md" } + trie.add(data) + const path = trie.ancestryChain(["index"]) + assert.deepStrictEqual(path, [trie]) + }) + + test("should return path to first level node", () => { + const data = { title: "Test", slug: "test", filePath: "test.md" } + trie.add(data) + const path = trie.ancestryChain(["test"]) + assert.deepStrictEqual(path, [trie, trie.children[0]]) + }) + + test("should return path to nested node", () => { + const data = { + title: "Nested", + slug: "folder/subfolder/test", + filePath: "folder/subfolder/test.md", + } + trie.add(data) + const path = trie.ancestryChain(["folder", "subfolder", "test"]) + assert.deepStrictEqual(path, [ + trie, + trie.children[0], + trie.children[0].children[0], + trie.children[0].children[0].children[0], + ]) + }) + + test("should return undefined for non-existent path", () => { + const data = { title: "Test", slug: "test", filePath: "test.md" } + trie.add(data) + const path = trie.ancestryChain(["nonexistent"]) + assert.strictEqual(path, undefined) + }) + + test("should return file data for intermediate folders", () => { + const data1 = { + title: "Root", + slug: "index", + filePath: "index.md", + } + const data2 = { + title: "Test", + slug: "folder/subfolder/test", + filePath: "folder/subfolder/test.md", + } + const data3 = { + title: "Folder Index", + slug: "folder/index", + filePath: "folder/index.md", + } + + trie.add(data1) + trie.add(data2) + trie.add(data3) + const path = trie.ancestryChain(["folder", "subfolder"]) + assert.deepStrictEqual(path, [trie, trie.children[0], trie.children[0].children[0]]) + assert.strictEqual(path[1].data, data3) + }) + + test("should return path for partial path", () => { + const data = { + title: "Nested", + slug: "folder/subfolder/test", + filePath: "folder/subfolder/test.md", + } + trie.add(data) + const path = trie.ancestryChain(["folder"]) + assert.deepStrictEqual(path, [trie, trie.children[0]]) + }) + }) }) diff --git a/quartz/util/fileTrie.ts b/quartz/util/fileTrie.ts index e3dc2e7aa..9e6706f4a 100644 --- a/quartz/util/fileTrie.ts +++ b/quartz/util/fileTrie.ts @@ -97,6 +97,24 @@ export class FileTrieNode { return this.children.find((c) => c.slugSegment === path[0])?.findNode(path.slice(1)) } + ancestryChain(path: string[]): Array> | undefined { + if (path.length === 0 || (path.length === 1 && path[0] === "index")) { + return [this] + } + + const child = this.children.find((c) => c.slugSegment === path[0]) + if (!child) { + return undefined + } + + const childPath = child.ancestryChain(path.slice(1)) + if (!childPath) { + return undefined + } + + return [this, ...childPath] + } + /** * Filter trie nodes. Behaves similar to `Array.prototype.filter()`, but modifies tree in place */ From aaa5c8e8e40be33aec74c1cf0073ac081cb918fc Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 17:34:14 -0700 Subject: [PATCH 05/12] feat: conditional render component --- docs/layout-components.md | 22 ++++++++++++++++++++++ quartz.layout.ts | 5 ++++- quartz/build.ts | 1 - quartz/components/ConditionalRender.tsx | 22 ++++++++++++++++++++++ quartz/components/index.ts | 2 ++ 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 quartz/components/ConditionalRender.tsx diff --git a/docs/layout-components.md b/docs/layout-components.md index 0c148a3c9..339ace8ec 100644 --- a/docs/layout-components.md +++ b/docs/layout-components.md @@ -60,3 +60,25 @@ The `DesktopOnly` component is the counterpart to `MobileOnly`. It makes its chi ```typescript Component.DesktopOnly(Component.TableOfContents()) ``` + +## `ConditionalRender` Component + +The `ConditionalRender` component is a wrapper that conditionally renders its child component based on a provided condition function. This is useful for creating dynamic layouts where components should only appear under certain conditions. + +```typescript +type ConditionalRenderConfig = { + component: QuartzComponent + condition: (props: QuartzComponentProps) => boolean +} +``` + +### Example Usage + +```typescript +Component.ConditionalRender({ + component: Component.Search(), + condition: (props) => props.displayClass !== "fullpage", +}) +``` + +This example would only render the Search component when the page is not in fullpage mode. diff --git a/quartz.layout.ts b/quartz.layout.ts index 1c601a2b6..e5c338815 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -17,7 +17,10 @@ export const sharedPageComponents: SharedLayout = { // components for pages that display a single page (e.g. a single note) export const defaultContentPageLayout: PageLayout = { beforeBody: [ - Component.Breadcrumbs(), + Component.ConditionalRender({ + component: Component.Breadcrumbs(), + condition: (page) => page.fileData.slug !== "index", + }), Component.ArticleTitle(), Component.ContentMeta(), Component.TagList(), diff --git a/quartz/build.ts b/quartz/build.ts index 032063f49..7cf440569 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -21,7 +21,6 @@ import { getStaticResourcesFromPlugins } from "./plugins" import { randomIdNonSecure } from "./util/random" import { ChangeEvent } from "./plugins/types" import { minimatch } from "minimatch" -import { FileTrieNode } from "./util/fileTrie" type ContentMap = Map< FilePath, diff --git a/quartz/components/ConditionalRender.tsx b/quartz/components/ConditionalRender.tsx new file mode 100644 index 000000000..74a4db07f --- /dev/null +++ b/quartz/components/ConditionalRender.tsx @@ -0,0 +1,22 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +type ConditionalRenderConfig = { + component: QuartzComponent + condition: (props: QuartzComponentProps) => boolean +} + +export default ((config: ConditionalRenderConfig) => { + const ConditionalRender: QuartzComponent = (props: QuartzComponentProps) => { + if (config.condition(props)) { + return + } + + return null + } + + ConditionalRender.afterDOMLoaded = config.component.afterDOMLoaded + ConditionalRender.beforeDOMLoaded = config.component.beforeDOMLoaded + ConditionalRender.css = config.component.css + + return ConditionalRender +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 49a3cb6da..2b601cd1a 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -21,6 +21,7 @@ import RecentNotes from "./RecentNotes" import Breadcrumbs from "./Breadcrumbs" import Comments from "./Comments" import Flex from "./Flex" +import ConditionalRender from "./ConditionalRender" export { ArticleTitle, @@ -46,4 +47,5 @@ export { Breadcrumbs, Comments, Flex, + ConditionalRender, } From 3027eced6c2d8b767d56f0c9b09b5fda30135d2b Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 17:42:23 -0700 Subject: [PATCH 06/12] chore(test): add tests for resolveRelative --- quartz/util/path.test.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/quartz/util/path.test.ts b/quartz/util/path.test.ts index 9f94c68ec..ca498b6cb 100644 --- a/quartz/util/path.test.ts +++ b/quartz/util/path.test.ts @@ -1,7 +1,7 @@ import test, { describe } from "node:test" import * as path from "./path" import assert from "node:assert" -import { FullSlug, TransformOptions } from "./path" +import { FullSlug, TransformOptions, SimpleSlug } from "./path" describe("typeguards", () => { test("isSimpleSlug", () => { @@ -314,3 +314,32 @@ describe("link strategies", () => { }) }) }) + +describe("resolveRelative", () => { + test("from index", () => { + assert.strictEqual(path.resolveRelative("index" as FullSlug, "index" as FullSlug), "./") + assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc" as FullSlug), "./abc") + assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc/def" as FullSlug), "./abc/def") + assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc/def/ghi" as FullSlug), "./abc/def/ghi") + }) + + test("from nested page", () => { + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "index" as FullSlug), "../") + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "abc" as FullSlug), "../abc") + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "abc/def" as FullSlug), "../abc/def") + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "ghi/jkl" as FullSlug), "../ghi/jkl") + }) + + test("with index paths", () => { + assert.strictEqual(path.resolveRelative("abc/index" as FullSlug, "index" as FullSlug), "../") + assert.strictEqual(path.resolveRelative("abc/def/index" as FullSlug, "index" as FullSlug), "../../") + assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc/index" as FullSlug), "./abc/") + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "abc/index" as FullSlug), "../abc/") + }) + + test("with simple slugs", () => { + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "" as SimpleSlug), "../") + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "ghi" as SimpleSlug), "../ghi") + assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "ghi/" as SimpleSlug), "../ghi/") + }) +}) From 141f053b0dfbf6b4432396d1c741012c8d87ceec Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 17:43:47 -0700 Subject: [PATCH 07/12] chore: format path.test.ts --- quartz/util/path.test.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/quartz/util/path.test.ts b/quartz/util/path.test.ts index ca498b6cb..e85f1d0ad 100644 --- a/quartz/util/path.test.ts +++ b/quartz/util/path.test.ts @@ -319,22 +319,40 @@ describe("resolveRelative", () => { test("from index", () => { assert.strictEqual(path.resolveRelative("index" as FullSlug, "index" as FullSlug), "./") assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc" as FullSlug), "./abc") - assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc/def" as FullSlug), "./abc/def") - assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc/def/ghi" as FullSlug), "./abc/def/ghi") + assert.strictEqual( + path.resolveRelative("index" as FullSlug, "abc/def" as FullSlug), + "./abc/def", + ) + assert.strictEqual( + path.resolveRelative("index" as FullSlug, "abc/def/ghi" as FullSlug), + "./abc/def/ghi", + ) }) test("from nested page", () => { assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "index" as FullSlug), "../") assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "abc" as FullSlug), "../abc") - assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "abc/def" as FullSlug), "../abc/def") - assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "ghi/jkl" as FullSlug), "../ghi/jkl") + assert.strictEqual( + path.resolveRelative("abc/def" as FullSlug, "abc/def" as FullSlug), + "../abc/def", + ) + assert.strictEqual( + path.resolveRelative("abc/def" as FullSlug, "ghi/jkl" as FullSlug), + "../ghi/jkl", + ) }) test("with index paths", () => { assert.strictEqual(path.resolveRelative("abc/index" as FullSlug, "index" as FullSlug), "../") - assert.strictEqual(path.resolveRelative("abc/def/index" as FullSlug, "index" as FullSlug), "../../") + assert.strictEqual( + path.resolveRelative("abc/def/index" as FullSlug, "index" as FullSlug), + "../../", + ) assert.strictEqual(path.resolveRelative("index" as FullSlug, "abc/index" as FullSlug), "./abc/") - assert.strictEqual(path.resolveRelative("abc/def" as FullSlug, "abc/index" as FullSlug), "../abc/") + assert.strictEqual( + path.resolveRelative("abc/def" as FullSlug, "abc/index" as FullSlug), + "../abc/", + ) }) test("with simple slugs", () => { From 9d8d238912dec82ed8310eb67d0b2587f571b6db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:57:45 -0700 Subject: [PATCH 08/12] chore(deps): bump the production-dependencies group across 1 directory with 4 updates (#1867) Bumps the production-dependencies group with 4 updates in the / directory: [lightningcss](https://github.com/parcel-bundler/lightningcss), [pixi.js](https://github.com/pixijs/pixijs), [rehype-pretty-code](https://github.com/rehype-pretty/rehype-pretty-code/tree/HEAD/packages/core) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `lightningcss` from 1.29.2 to 1.29.3 - [Release notes](https://github.com/parcel-bundler/lightningcss/releases) - [Commits](https://github.com/parcel-bundler/lightningcss/compare/v1.29.2...v1.29.3) Updates `pixi.js` from 8.8.1 to 8.9.0 - [Release notes](https://github.com/pixijs/pixijs/releases) - [Commits](https://github.com/pixijs/pixijs/compare/v8.8.1...v8.9.0) Updates `rehype-pretty-code` from 0.14.0 to 0.14.1 - [Release notes](https://github.com/rehype-pretty/rehype-pretty-code/releases) - [Changelog](https://github.com/rehype-pretty/rehype-pretty-code/blob/master/packages/core/CHANGELOG.md) - [Commits](https://github.com/rehype-pretty/rehype-pretty-code/commits/rehype-pretty-code@0.14.1/packages/core) Updates `@types/node` from 22.13.10 to 22.13.11 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: lightningcss dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies - dependency-name: pixi.js dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-dependencies - dependency-name: rehype-pretty-code dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: production-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 115 +++++++++++++++++++++++----------------------- package.json | 8 ++-- 2 files changed, 62 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index db2e37391..241b45629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,13 +30,13 @@ "hast-util-to-string": "^3.0.1", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.29.2", + "lightningcss": "^1.29.3", "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", "minimatch": "^10.0.1", - "pixi.js": "^8.8.1", + "pixi.js": "^8.9.0", "preact": "^10.26.4", "preact-render-to-string": "^6.5.13", "pretty-bytes": "^6.1.1", @@ -46,7 +46,7 @@ "rehype-citation": "^2.2.2", "rehype-katex": "^7.0.1", "rehype-mathjax": "^7.1.0", - "rehype-pretty-code": "^0.14.0", + "rehype-pretty-code": "^0.14.1", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "remark": "^15.0.1", @@ -80,7 +80,7 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.13.10", + "@types/node": "^22.13.11", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", "@types/ws": "^8.18.0", @@ -1924,9 +1924,9 @@ } }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.13.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.11.tgz", + "integrity": "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==", "dev": true, "license": "MIT", "dependencies": { @@ -3994,9 +3994,9 @@ } }, "node_modules/lightningcss": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", - "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.3.tgz", + "integrity": "sha512-GlOJwTIP6TMIlrTFsxTerwC0W6OpQpCGuX1ECRLBUVRh6fpJH3xTqjCjRgQHTb4ZXexH9rtHou1Lf03GKzmhhQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -4009,22 +4009,22 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.29.2", - "lightningcss-darwin-x64": "1.29.2", - "lightningcss-freebsd-x64": "1.29.2", - "lightningcss-linux-arm-gnueabihf": "1.29.2", - "lightningcss-linux-arm64-gnu": "1.29.2", - "lightningcss-linux-arm64-musl": "1.29.2", - "lightningcss-linux-x64-gnu": "1.29.2", - "lightningcss-linux-x64-musl": "1.29.2", - "lightningcss-win32-arm64-msvc": "1.29.2", - "lightningcss-win32-x64-msvc": "1.29.2" + "lightningcss-darwin-arm64": "1.29.3", + "lightningcss-darwin-x64": "1.29.3", + "lightningcss-freebsd-x64": "1.29.3", + "lightningcss-linux-arm-gnueabihf": "1.29.3", + "lightningcss-linux-arm64-gnu": "1.29.3", + "lightningcss-linux-arm64-musl": "1.29.3", + "lightningcss-linux-x64-gnu": "1.29.3", + "lightningcss-linux-x64-musl": "1.29.3", + "lightningcss-win32-arm64-msvc": "1.29.3", + "lightningcss-win32-x64-msvc": "1.29.3" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.3.tgz", + "integrity": "sha512-fb7raKO3pXtlNbQbiMeEu8RbBVHnpyqAoxTyTRMEWFQWmscGC2wZxoHzZ+YKAepUuKT9uIW5vL2QbFivTgprZg==", "cpu": [ "arm64" ], @@ -4042,9 +4042,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.3.tgz", + "integrity": "sha512-KF2XZ4ZdmDGGtEYmx5wpzn6u8vg7AdBHaEOvDKu8GOs7xDL/vcU2vMKtTeNe1d4dogkDdi3B9zC77jkatWBwEQ==", "cpu": [ "x64" ], @@ -4062,9 +4062,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.3.tgz", + "integrity": "sha512-VUWeVf+V1UM54jv9M4wen9vMlIAyT69Krl9XjI8SsRxz4tdNV/7QEPlW6JASev/pYdiynUCW0pwaFquDRYdxMw==", "cpu": [ "x64" ], @@ -4082,9 +4082,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.3.tgz", + "integrity": "sha512-UhgZ/XVNfXQVEJrMIWeK1Laj8KbhjbIz7F4znUk7G4zeGw7TRoJxhb66uWrEsonn1+O45w//0i0Fu0wIovYdYg==", "cpu": [ "arm" ], @@ -4102,9 +4102,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.3.tgz", + "integrity": "sha512-Pqau7jtgJNmQ/esugfmAT1aCFy/Gxc92FOxI+3n+LbMHBheBnk41xHDhc0HeYlx9G0xP5tK4t0Koy3QGGNqypw==", "cpu": [ "arm64" ], @@ -4122,9 +4122,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.3.tgz", + "integrity": "sha512-dxakOk66pf7KLS7VRYFO7B8WOJLecE5OPL2YOk52eriFd/yeyxt2Km5H0BjLfElokIaR+qWi33gB8MQLrdAY3A==", "cpu": [ "arm64" ], @@ -4142,9 +4142,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.3.tgz", + "integrity": "sha512-ySZTNCpbfbK8rqpKJeJR2S0g/8UqqV3QnzcuWvpI60LWxnFN91nxpSSwCbzfOXkzKfar9j5eOuOplf+klKtINg==", "cpu": [ "x64" ], @@ -4162,9 +4162,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.3.tgz", + "integrity": "sha512-3pVZhIzW09nzi10usAXfIGTTSTYQ141dk88vGFNCgawIzayiIzZQxEcxVtIkdvlEq2YuFsL9Wcj/h61JHHzuFQ==", "cpu": [ "x64" ], @@ -4182,9 +4182,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.3.tgz", + "integrity": "sha512-VRnkAvtIkeWuoBJeGOTrZxsNp4HogXtcaaLm8agmbYtLDOhQdpgxW6NjZZjDXbvGF+eOehGulXZ3C1TiwHY4QQ==", "cpu": [ "arm64" ], @@ -4202,9 +4202,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "version": "1.29.3", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.3.tgz", + "integrity": "sha512-IszwRPu2cPnDQsZpd7/EAr0x2W7jkaWqQ1SwCVIZ/tSbZVXPLt6k8s6FkcyBjViCzvB5CW0We0QbbP7zp2aBjQ==", "cpu": [ "x64" ], @@ -5502,9 +5502,9 @@ } }, "node_modules/pixi.js": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.8.1.tgz", - "integrity": "sha512-Zmox3Vy52Kl6X/uxknKlxJWPVEFiP63nsX8soqB4butTkIOK3y7c9C204wcDfAgkwO1OlwYxscWtHv+ef4gqgA==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.9.0.tgz", + "integrity": "sha512-uhXZKbus1C4nHu2ZHDCHE7m9BSsGOAuR+rj31VPyN6O8L8TEFs/q5+/43sMBC89EjPahIhvYOSNtY9nnrrY7BA==", "license": "MIT", "dependencies": { "@pixi/colord": "^2.9.6", @@ -5762,9 +5762,10 @@ } }, "node_modules/rehype-pretty-code": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.14.0.tgz", - "integrity": "sha512-hBeKF/Wkkf3zyUS8lal9RCUuhypDWLQc+h9UrP9Pav25FUm/AQAVh4m5gdvJxh4Oz+U+xKvdsV01p1LdvsZTiQ==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.14.1.tgz", + "integrity": "sha512-IpG4OL0iYlbx78muVldsK86hdfNoht0z63AP7sekQNW2QOTmjxB7RbTO+rhIYNGRljgHxgVZoPwUl6bIC9SbjA==", + "license": "MIT", "dependencies": { "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.0", @@ -5777,7 +5778,7 @@ "node": ">=18" }, "peerDependencies": { - "shiki": "^1.3.0" + "shiki": "^1.0.0 || ^2.0.0 || ^3.0.0" } }, "node_modules/rehype-raw": { diff --git a/package.json b/package.json index 0b0b9d9cd..83202c521 100644 --- a/package.json +++ b/package.json @@ -56,13 +56,13 @@ "hast-util-to-string": "^3.0.1", "is-absolute-url": "^4.0.1", "js-yaml": "^4.1.0", - "lightningcss": "^1.29.2", + "lightningcss": "^1.29.3", "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", "minimatch": "^10.0.1", - "pixi.js": "^8.8.1", + "pixi.js": "^8.9.0", "preact": "^10.26.4", "preact-render-to-string": "^6.5.13", "pretty-bytes": "^6.1.1", @@ -72,7 +72,7 @@ "rehype-citation": "^2.2.2", "rehype-katex": "^7.0.1", "rehype-mathjax": "^7.1.0", - "rehype-pretty-code": "^0.14.0", + "rehype-pretty-code": "^0.14.1", "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "remark": "^15.0.1", @@ -103,7 +103,7 @@ "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.13.10", + "@types/node": "^22.13.11", "@types/pretty-time": "^1.1.5", "@types/source-map-support": "^0.5.10", "@types/ws": "^8.18.0", From 722b4321db97f284036f2ff6721c6d85d2ed93c5 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 18:03:52 -0700 Subject: [PATCH 09/12] docs: clarify transclusions --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index bd8c8969a..9f5ffec8c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ If you prefer instructions in a video format you can try following Nicole van de ## 🔧 Features -- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]], [[comments]] and [many more](./features/) right out of the box +- [[Obsidian compatibility]], [[full-text search]], [[graph view]], [[wikilinks|wikilinks, transclusions]], [[backlinks]], [[features/Latex|Latex]], [[syntax highlighting]], [[popover previews]], [[Docker Support]], [[i18n|internationalization]], [[comments]] and [many more](./features/) right out of the box - Hot-reload on configuration edits and incremental rebuilds for content edits - Simple JSX layouts and [[creating components|page components]] - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes From fe2e16d937e2b57fb2bb1d10dea47db7256a4a49 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 18:08:07 -0700 Subject: [PATCH 10/12] fix: disallow user-select in popover --- quartz/components/styles/popover.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index 38d612697..872045a2c 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -36,6 +36,8 @@ box-shadow: 6px 6px 36px 0 rgba(0, 0, 0, 0.25); overflow: auto; white-space: normal; + user-select: none; + cursor: default; } & > .popover-inner[data-content-type] { From c18e6cd5bb7e0de7e1a9a77927b48069f0bad4b9 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 20:38:06 -0700 Subject: [PATCH 11/12] fix(alias): resolve relative if alias is relative --- quartz/plugins/emitters/aliases.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts index 327cde85b..0eb4f3acc 100644 --- a/quartz/plugins/emitters/aliases.ts +++ b/quartz/plugins/emitters/aliases.ts @@ -1,14 +1,19 @@ -import { resolveRelative, simplifySlug } from "../../util/path" +import { isRelativeURL, resolveRelative, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import { write } from "./helpers" import { BuildCtx } from "../../util/ctx" import { VFile } from "vfile" +import path from "path" async function* processFile(ctx: BuildCtx, file: VFile) { const ogSlug = simplifySlug(file.data.slug!) - for (const slug of file.data.aliases ?? []) { - const redirUrl = resolveRelative(slug, file.data.slug!) + for (const aliasTarget of file.data.aliases ?? []) { + const aliasTargetSlug = isRelativeURL(aliasTarget) + ? path.normalize(path.join(ogSlug, "..", aliasTarget)) + : aliasTarget + + const redirUrl = resolveRelative(aliasTargetSlug, ogSlug) yield write({ ctx, content: ` @@ -23,7 +28,7 @@ async function* processFile(ctx: BuildCtx, file: VFile) { `, - slug, + slug: aliasTargetSlug, ext: ".html", }) } From 23b691f38cfd6672edb87cca3ea71bf266cb7db7 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 23 Mar 2025 20:43:01 -0700 Subject: [PATCH 12/12] fix: coerce fullslug --- quartz/plugins/emitters/aliases.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts index 0eb4f3acc..9cb9bd576 100644 --- a/quartz/plugins/emitters/aliases.ts +++ b/quartz/plugins/emitters/aliases.ts @@ -1,4 +1,4 @@ -import { isRelativeURL, resolveRelative, simplifySlug } from "../../util/path" +import { FullSlug, isRelativeURL, resolveRelative, simplifySlug } from "../../util/path" import { QuartzEmitterPlugin } from "../types" import { write } from "./helpers" import { BuildCtx } from "../../util/ctx" @@ -9,9 +9,11 @@ async function* processFile(ctx: BuildCtx, file: VFile) { const ogSlug = simplifySlug(file.data.slug!) for (const aliasTarget of file.data.aliases ?? []) { - const aliasTargetSlug = isRelativeURL(aliasTarget) - ? path.normalize(path.join(ogSlug, "..", aliasTarget)) - : aliasTarget + const aliasTargetSlug = ( + isRelativeURL(aliasTarget) + ? path.normalize(path.join(ogSlug, "..", aliasTarget)) + : aliasTarget + ) as FullSlug const redirUrl = resolveRelative(aliasTargetSlug, ogSlug) yield write({