From 660aae62e09ea1f5ec957149b78bfd01a85bd79f Mon Sep 17 00:00:00 2001 From: Odaimoko <934854676@qq.com> Date: Tue, 28 Nov 2023 15:05:18 +0800 Subject: [PATCH 1/9] docs: add Imk&Cc's homepage to showcase.md (#595) * add Imk&Cc's homepage to showcase.md * Update showcase.md * Update showcase.md --- docs/showcase.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/showcase.md b/docs/showcase.md index ed9df9f57..a5ed89bc9 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -20,5 +20,6 @@ Want to see what Quartz can do? Here are some cool community gardens: - [🧠🌳 Chad's Mind Garden](https://www.chadly.net/) - [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/) 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 0d314db1f8d6438e6e721e1c37c65fe84f193acf Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Wed, 29 Nov 2023 10:50:26 -0800 Subject: [PATCH 2/9] fix(style): overflow on toc --- quartz/components/styles/toc.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss index 3fac4432a..27ff62a40 100644 --- a/quartz/components/styles/toc.scss +++ b/quartz/components/styles/toc.scss @@ -30,6 +30,7 @@ button#toc { overflow: hidden; max-height: none; transition: max-height 0.5s ease; + position: relative; &.collapsed > .overflow::after { opacity: 0; From b5fec6c87f2060884425607c0c4de5eededbb30f Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Fri, 1 Dec 2023 09:00:47 -0800 Subject: [PATCH 3/9] feat: allow popovers on intrapage links (closes #243) --- quartz/components/scripts/popover.inline.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 371563ba9..08668ae8f 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -32,8 +32,6 @@ async function mouseEnterHandler( const hash = targetUrl.hash targetUrl.hash = "" targetUrl.search = "" - // prevent hover of the same page - if (thisUrl.toString() === targetUrl.toString()) return const contents = await fetch(`${targetUrl}`) .then((res) => res.text()) From 649090de1b7a8a8849074b8e1bfd938943e1b028 Mon Sep 17 00:00:00 2001 From: mancuoj <1337381768@qq.com> Date: Sat, 2 Dec 2023 14:59:02 +0800 Subject: [PATCH 4/9] docs: add deploy with netlify (#613) --- docs/hosting.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/hosting.md b/docs/hosting.md index a4ca1ea98..cf7064c8b 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -167,6 +167,17 @@ Using `docs.example.com` is an example of a subdomain. They're a simple way of c 4. Go to the Settings tab and then click Domains in the sidebar 5. Enter your subdomain into the field and press Add +## Netlify + +Like Vercel, you can also deploy the site generated by Quartz 4 via Netlify. + +1. Log in to the [Netlify dashboard](https://app.netlify.com/) and click "Add new site". +2. Select your Git provider and repository containing your Quartz project. +3. Under "Build command", enter `npx quartz build`. +4. Under "Publish directory", enter `public`. +5. Press Deploy. Once it's live, you'll have a `*.netlify.app` URL to view the page. +6. To add a custom domain, check "Domain management" in the left sidebar, just like with Vercel. + ## GitLab Pages You can configure GitLab CI to build and deploy a Quartz 4 project. From 82bd08d14a45f9b1c3d48000543562fa0d51f13a Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 2 Dec 2023 16:50:55 -0800 Subject: [PATCH 5/9] fix: transcludes and relative paths --- quartz/components/renderPage.tsx | 33 ++++++++++++----- quartz/components/scripts/graph.inline.ts | 28 +++++++------- quartz/plugins/transformers/links.ts | 12 +++--- quartz/plugins/transformers/ofm.ts | 3 +- quartz/util/path.ts | 45 +++++++++++++++++++---- 5 files changed, 85 insertions(+), 36 deletions(-) diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 5cb39d9ad..305f511fd 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -3,9 +3,10 @@ import { QuartzComponent, QuartzComponentProps } from "./types" import HeaderConstructor from "./Header" import BodyConstructor from "./Body" import { JSResourceToScriptElement, StaticResources } from "../util/resources" -import { FullSlug, RelativeURL, joinSegments } from "../util/path" +import { FullSlug, RelativeURL, joinSegments, normalizeHastElement } from "../util/path" import { visit } from "unist-util-visit" import { Root, Element, ElementContent } from "hast" +import { QuartzPluginData } from "../plugins/vfile" interface RenderComponents { head: QuartzComponent @@ -49,6 +50,18 @@ export function pageResources( } } +let pageIndex: Map | undefined = undefined +function getOrComputeFileIndex(allFiles: QuartzPluginData[]): Map { + if (!pageIndex) { + pageIndex = new Map() + for (const file of allFiles) { + pageIndex.set(file.slug!, file) + } + } + + return pageIndex +} + export function renderPage( slug: FullSlug, componentData: QuartzComponentProps, @@ -62,17 +75,15 @@ export function renderPage( if (classNames.includes("transclude")) { const inner = node.children[0] as Element const transcludeTarget = inner.properties?.["data-slug"] as FullSlug - - // TODO: avoid this expensive find operation and construct an index ahead of time - const page = componentData.allFiles.find((f) => f.slug === transcludeTarget) + const page = getOrComputeFileIndex(componentData.allFiles).get(transcludeTarget) if (!page) { return } let blockRef = node.properties?.dataBlock as string | undefined - if (blockRef?.startsWith("^")) { + if (blockRef?.startsWith("#^")) { // block transclude - blockRef = blockRef.slice(1) + blockRef = blockRef.slice("#^".length) let blockNode = page.blocks?.[blockRef] if (blockNode) { if (blockNode.tagName === "li") { @@ -84,7 +95,7 @@ export function renderPage( } node.children = [ - blockNode, + normalizeHastElement(blockNode, slug, transcludeTarget), { type: "element", tagName: "a", @@ -117,7 +128,9 @@ export function renderPage( } node.children = [ - ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]), + ...(page.htmlAst.children.slice(startIdx, endIdx) as ElementContent[]).map((child) => + normalizeHastElement(child as Element, slug, transcludeTarget), + ), { type: "element", tagName: "a", @@ -135,7 +148,9 @@ export function renderPage( { type: "text", value: page.frontmatter?.title ?? `Transclude of ${page.slug}` }, ], }, - ...(page.htmlAst.children as ElementContent[]), + ...(page.htmlAst.children as ElementContent[]).map((child) => + normalizeHastElement(child as Element, slug, transcludeTarget), + ), { type: "element", tagName: "a", diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index 1aff138f2..bddcfa4c6 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -1,4 +1,4 @@ -import type { ContentDetails } from "../../plugins/emitters/contentIndex" +import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex" import * as d3 from "d3" import { registerEscapeHandler, removeAllChildren } from "./util" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" @@ -46,20 +46,22 @@ async function renderGraph(container: string, fullSlug: FullSlug) { showTags, } = JSON.parse(graph.dataset["cfg"]!) - const data = await fetchData - + const data: Map = new Map( + Object.entries(await fetchData).map(([k, v]) => [ + simplifySlug(k as FullSlug), + v, + ]), + ) const links: LinkData[] = [] const tags: SimpleSlug[] = [] - const validLinks = new Set(Object.keys(data).map((slug) => simplifySlug(slug as FullSlug))) - - for (const [src, details] of Object.entries(data)) { - const source = simplifySlug(src as FullSlug) + const validLinks = new Set(data.keys()) + for (const [source, details] of data.entries()) { const outgoing = details.links ?? [] for (const dest of outgoing) { if (validLinks.has(dest)) { - links.push({ source, target: dest }) + links.push({ source: source, target: dest }) } } @@ -71,7 +73,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { tags.push(...localTags.filter((tag) => !tags.includes(tag))) for (const tag of localTags) { - links.push({ source, target: tag }) + links.push({ source: source, target: tag }) } } } @@ -93,17 +95,17 @@ async function renderGraph(container: string, fullSlug: FullSlug) { } } } else { - Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug))) + validLinks.forEach((id) => neighbourhood.add(id)) if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) } const graphData: { nodes: NodeData[]; links: LinkData[] } = { nodes: [...neighbourhood].map((url) => { - const text = url.startsWith("tags/") ? "#" + url.substring(5) : data[url]?.title ?? url + const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url return { id: url, text: text, - tags: data[url]?.tags ?? [], + tags: data.get(url)?.tags ?? [], } }), links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), @@ -200,7 +202,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { window.spaNavigate(new URL(targ, window.location.toString())) }) .on("mouseover", function (_, d) { - const neighbours: SimpleSlug[] = data[fullSlug].links ?? [] + const neighbours: SimpleSlug[] = data.get(slug)?.links ?? [] const neighbourNodes = d3 .selectAll(".node") .filter((d) => neighbours.includes(d.id)) diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index eec473c10..3072959df 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -81,14 +81,16 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = // WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to const url = new URL(dest, `https://base.com/${curSlug}`) const canonicalDest = url.pathname - const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) + let [destCanonical, _destAnchor] = splitAnchor(canonicalDest) + if (destCanonical.endsWith("/")) { + destCanonical += "index" + } // need to decodeURIComponent here as WHATWG URL percent-encodes everything - const simple = decodeURIComponent( - simplifySlug(destCanonical as FullSlug), - ) as SimpleSlug + const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug + const simple = simplifySlug(full) outgoing.add(simple) - node.properties["data-slug"] = simple + node.properties["data-slug"] = full } // rewrite link internals if prettylinks is on diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 2e47cedf5..4c6a6dbed 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -182,7 +182,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin const [rawFp, rawHeader, rawAlias] = capture const fp = rawFp ?? "" const anchor = rawHeader?.trim().replace(/^#+/, "") - const displayAnchor = anchor ? `#${slugAnchor(anchor)}` : "" + const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : "" + const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : "" const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" const embedDisplay = value.startsWith("!") ? "!" : "" return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]` diff --git a/quartz/util/path.ts b/quartz/util/path.ts index e450339fd..19aa09489 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -1,4 +1,5 @@ import { slug } from "github-slugger" +import type { ElementContent, Element as HastElement } from "hast" // this file must be isomorphic so it can't use node libs (e.g. path) export const QUARTZ = "quartz" @@ -65,7 +66,8 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug { } export function simplifySlug(fp: FullSlug): SimpleSlug { - return _stripSlashes(_trimSuffix(fp, "index"), true) as SimpleSlug + const res = _stripSlashes(_trimSuffix(fp, "index"), true) + return (res.length === 0 ? "/" : res) as SimpleSlug } export function transformInternalLink(link: string): RelativeURL { @@ -86,20 +88,47 @@ export function transformInternalLink(link: string): RelativeURL { // from micromorph/src/utils.ts // https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5 +const _rebaseHtmlElement = (el: Element, attr: string, newBase: string | URL) => { + const rebased = new URL(el.getAttribute(attr)!, newBase) + el.setAttribute(attr, rebased.pathname + rebased.hash) +} export function normalizeRelativeURLs(el: Element | Document, destination: string | URL) { - const rebase = (el: Element, attr: string, newBase: string | URL) => { - const rebased = new URL(el.getAttribute(attr)!, newBase) - el.setAttribute(attr, rebased.pathname + rebased.hash) - } - el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => - rebase(item, "href", destination), + _rebaseHtmlElement(item, "href", destination), ) el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => - rebase(item, "src", destination), + _rebaseHtmlElement(item, "src", destination), ) } +const _rebaseHastElement = ( + el: HastElement, + attr: string, + curBase: FullSlug, + newBase: FullSlug, +) => { + if (el.properties?.[attr]) { + if (!isRelativeURL(String(el.properties[attr]))) { + return + } + + const rel = joinSegments(resolveRelative(curBase, newBase), "..", el.properties[attr] as string) + el.properties[attr] = rel + } +} + +export function normalizeHastElement(el: HastElement, curBase: FullSlug, newBase: FullSlug) { + _rebaseHastElement(el, "src", curBase, newBase) + _rebaseHastElement(el, "href", curBase, newBase) + if (el.children) { + el.children = el.children.map((child) => + normalizeHastElement(child as HastElement, curBase, newBase), + ) + } + + return el +} + // resolve /a/b/c to ../.. export function pathToRoot(slug: FullSlug): RelativeURL { let rootPath = slug From 610b04406f635cfb3c2f958f61ce716de21b04f4 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 2 Dec 2023 16:52:44 -0800 Subject: [PATCH 6/9] fix: incorrect test --- quartz/util/path.test.ts | 2 +- quartz/util/path.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/util/path.test.ts b/quartz/util/path.test.ts index 8bbb58dc3..18edc9407 100644 --- a/quartz/util/path.test.ts +++ b/quartz/util/path.test.ts @@ -83,7 +83,7 @@ describe("transforms", () => { test("simplifySlug", () => { asserts( [ - ["index", ""], + ["index", "/"], ["abc", "abc"], ["abc/index", "abc/"], ["abc/def", "abc/def"], diff --git a/quartz/util/path.ts b/quartz/util/path.ts index 19aa09489..555a19154 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -1,5 +1,5 @@ import { slug } from "github-slugger" -import type { ElementContent, Element as HastElement } from "hast" +import type { Element as HastElement } from "hast" // this file must be isomorphic so it can't use node libs (e.g. path) export const QUARTZ = "quartz" @@ -25,7 +25,7 @@ export function isFullSlug(s: string): s is FullSlug { /** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */ export type SimpleSlug = SlugLike<"simple"> export function isSimpleSlug(s: string): s is SimpleSlug { - const validStart = !(s.startsWith(".") || s.startsWith("/")) + const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/"))) const validEnding = !(s.endsWith("/index") || s === "index") return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s) } From 54b4a5567c8c12d4d00a4f782f4a597a2433be73 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 2 Dec 2023 16:55:38 -0800 Subject: [PATCH 7/9] fix: fmt --- quartz/util/path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/util/path.ts b/quartz/util/path.ts index 555a19154..5cf54b803 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -1,5 +1,5 @@ import { slug } from "github-slugger" -import type { Element as HastElement } from "hast" +import type { Element as HastElement } from "hast" // this file must be isomorphic so it can't use node libs (e.g. path) export const QUARTZ = "quartz" From 0d8c025d6a591c158801887a1858d7ebdf7149e8 Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sat, 2 Dec 2023 17:00:06 -0800 Subject: [PATCH 8/9] deps: version bump --- package-lock.json | 7 ++++--- package.json | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a87907897..7c12e7146 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jackyzha0/quartz", - "version": "4.0.11", + "version": "4.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jackyzha0/quartz", - "version": "4.0.11", + "version": "4.1.2", "license": "MIT", "dependencies": { "@clack/prompts": "^0.6.3", @@ -85,7 +85,8 @@ "typescript": "^5.0.4" }, "engines": { - "node": ">=18.14" + "node": ">=18.14", + "npm": ">=9.3.1" } }, "node_modules/@clack/core": { diff --git a/package.json b/package.json index aa6324366..0a746dc3f 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.1", + "version": "4.1.2", "type": "module", "author": "jackyzha0 ", "license": "MIT", From 9c88d5967fee49d9e69b0e5dd22ca3bc44f9a12e Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Sun, 3 Dec 2023 09:22:16 -0800 Subject: [PATCH 9/9] fix: don't show popovers on heading anchors --- quartz/components/scripts/popover.inline.ts | 4 ++++ quartz/plugins/transformers/gfm.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 08668ae8f..4d51e2a6f 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -7,6 +7,10 @@ async function mouseEnterHandler( { clientX, clientY }: { clientX: number; clientY: number }, ) { const link = this + if (link.dataset.noPopover === "true") { + return + } + async function setPosition(popoverElement: HTMLElement) { const { x, y } = await computePosition(link, popoverElement, { middleware: [inline({ x: clientX, y: clientY }), shift(), flip()], diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts index 62624aac0..40c2205d3 100644 --- a/quartz/plugins/transformers/gfm.ts +++ b/quartz/plugins/transformers/gfm.ts @@ -31,6 +31,11 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin | rehypeAutolinkHeadings, { behavior: "append", + properties: { + ariaHidden: true, + tabIndex: -1, + "data-no-popover": true, + }, content: { type: "text", value: " §",