From 0c199975f2d469ecdfd7efcf2ddd16ffa1dc492b Mon Sep 17 00:00:00 2001 From: Jacky Zhao Date: Thu, 17 Aug 2023 00:55:28 -0700 Subject: [PATCH] various path fixes for links to extensions, fix relative paths in links --- content/advanced/making plugins.md | 2 +- content/configuration.md | 2 +- content/features/upcoming features.md | 2 + content/hosting.md | 2 +- .../{dns-records.png => dns records.png} | Bin .../{quartz-layout.png => quartz layout.png} | Bin ...line.png => quartz transform pipeline.png} | Bin content/layout.md | 2 +- quartz/bootstrap-cli.mjs | 20 ++++--- quartz/components/scripts/spa.inline.ts | 3 +- quartz/plugins/emitters/assets.ts | 2 +- quartz/plugins/transformers/lastmod.ts | 1 + quartz/plugins/transformers/links.ts | 3 +- quartz/plugins/transformers/ofm.ts | 2 +- quartz/util/path.test.ts | 55 ++++++++++++------ quartz/util/path.ts | 33 ++++++----- 16 files changed, 77 insertions(+), 52 deletions(-) rename content/images/{dns-records.png => dns records.png} (100%) rename content/images/{quartz-layout.png => quartz layout.png} (100%) rename content/images/{quartz-transform-pipeline.png => quartz transform pipeline.png} (100%) diff --git a/content/advanced/making plugins.md b/content/advanced/making plugins.md index 2de32008..1f1616f4 100644 --- a/content/advanced/making plugins.md +++ b/content/advanced/making plugins.md @@ -7,7 +7,7 @@ title: Making your own plugins Quartz's plugins are a series of transformations over content. This is illustrated in the diagram of the processing pipeline below: -![[quartz-transform-pipeline.png]] +![[quartz transform pipeline.png]] All plugins are defined as a function that takes in a single parameter for options `type OptionType = object | undefined` and return an object that corresponds to the type of plugin it is. diff --git a/content/configuration.md b/content/configuration.md index 182a87b2..763a27a9 100644 --- a/content/configuration.md +++ b/content/configuration.md @@ -50,7 +50,7 @@ This part of the configuration concerns anything that can affect the whole site. You can think of Quartz plugins as a series of transformations over content. -![[quartz-transform-pipeline.png]] +![[quartz transform pipeline.png]] ```ts plugins: { diff --git a/content/features/upcoming features.md b/content/features/upcoming features.md index 8d6c657b..bbf962bc 100644 --- a/content/features/upcoming features.md +++ b/content/features/upcoming features.md @@ -4,6 +4,8 @@ draft: true ## todo +- static icon path (in head) never gets updated + - do we update relative links on spa? - back button with anchors / popovers + spa is broken - debounce cfg rebuild on large repos - investigate content rebuild triggering multiple times even when debounced, causing an esbuild deadlock diff --git a/content/hosting.md b/content/hosting.md index 11dff182..1e900a5b 100644 --- a/content/hosting.md +++ b/content/hosting.md @@ -100,7 +100,7 @@ Here's how to add a custom domain to your GitHub pages deployment. - `185.199.111.153` - If you are using a subdomain, navigate to your DNS provider and create a `CNAME` record that points your subdomain to the default domain for your site. For example, if you want to use the subdomain `quartz.example.com` for your user site, create a `CNAME` record that points `quartz.example.com` to `.github.io`. -![[dns-records.png]]_The above shows a screenshot of Google Domains configured for both `jzhao.xyz` (an apex domain) and `quartz.jzhao.xyz` (a subdomain)._ +![[dns records.png]]_The above shows a screenshot of Google Domains configured for both `jzhao.xyz` (an apex domain) and `quartz.jzhao.xyz` (a subdomain)._ See the [GitHub documentation](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site#configuring-a-subdomain) for more detail about how to setup your own custom domain with GitHub Pages. diff --git a/content/images/dns-records.png b/content/images/dns records.png similarity index 100% rename from content/images/dns-records.png rename to content/images/dns records.png diff --git a/content/images/quartz-layout.png b/content/images/quartz layout.png similarity index 100% rename from content/images/quartz-layout.png rename to content/images/quartz layout.png diff --git a/content/images/quartz-transform-pipeline.png b/content/images/quartz transform pipeline.png similarity index 100% rename from content/images/quartz-transform-pipeline.png rename to content/images/quartz transform pipeline.png diff --git a/content/layout.md b/content/layout.md index 80171007..8a74b911 100644 --- a/content/layout.md +++ b/content/layout.md @@ -20,7 +20,7 @@ export interface FullPageLayout { These correspond to following parts of the page: -![[quartz-layout.png|800]] +![[quartz layout.png|800]] > [!note] > There are two additional layout fields that are _not_ shown in the above diagram. diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index 077e31b1..8551e764 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -388,7 +388,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. await build(clientRefresh) const server = http.createServer(async (req, res) => { - const serve = async (fp) => { + const serve = async () => { await serveHandler(req, res, { public: argv.output, directoryListing: false, @@ -400,11 +400,11 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. } const redirect = (newFp) => { - res.writeHead(301, { + res.writeHead(302, { Location: newFp, }) - console.log(chalk.yellow("[301]") + chalk.grey(` ${req.url} -> ${newFp}`)) - return res.end() + console.log(chalk.yellow("[302]") + chalk.grey(` ${req.url} -> ${newFp}`)) + res.end() } let fp = req.url?.split("?")[0] ?? "/" @@ -415,7 +415,8 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. // does /trailing/index.html exist? if so, serve it const indexFp = path.posix.join(fp, "index.html") if (fs.existsSync(path.posix.join(argv.output, indexFp))) { - return serve(indexFp) + req.url = fp + return serve() } // does /trailing.html exist? if so, redirect to /trailing @@ -424,7 +425,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. base += ".html" } if (fs.existsSync(path.posix.join(argv.output, base))) { - return redirect(base) + return redirect(fp.slice(0, -1)) } } else { // /regular @@ -434,7 +435,8 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. base += ".html" } if (fs.existsSync(path.posix.join(argv.output, base))) { - return serve(base) + req.url = fp + return serve() } // does /regular/index.html exist? if so, redirect to /regular/ @@ -444,7 +446,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. } } - return serve(fp) + return serve() }) server.listen(argv.port) console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`)) @@ -458,7 +460,7 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started. await build(clientRefresh) }) } else { - await build(() => {}) + await build(() => { }) ctx.dispose() } }) diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index 7e450b76..c28da28c 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -17,7 +17,7 @@ const isLocalUrl = (href: string) => { } return true } - } catch (e) {} + } catch (e) { } return false } @@ -50,6 +50,7 @@ async function navigate(url: URL, isBack: boolean = false) { history.pushState({}, "", url) window.scrollTo({ top: 0 }) } + const html = p.parseFromString(contents, "text/html") let title = html.querySelector("title")?.textContent if (title) { diff --git a/quartz/plugins/emitters/assets.ts b/quartz/plugins/emitters/assets.ts index 44bb7185..edc22d9e 100644 --- a/quartz/plugins/emitters/assets.ts +++ b/quartz/plugins/emitters/assets.ts @@ -18,7 +18,7 @@ export const Assets: QuartzEmitterPlugin = () => { for (const fp of fps) { const ext = path.extname(fp) const src = joinSegments(argv.directory, fp) as FilePath - const name = (slugifyFilePath(fp as FilePath) + ext) as FilePath + const name = (slugifyFilePath(fp as FilePath, true) + ext) as FilePath const dest = joinSegments(assetsPath, name) as FilePath const dir = path.dirname(dest) as FilePath diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts index 15dd89e3..507b5852 100644 --- a/quartz/plugins/transformers/lastmod.ts +++ b/quartz/plugins/transformers/lastmod.ts @@ -36,6 +36,7 @@ export const CreatedModifiedDate: QuartzTransformerPlugin | und } else if (source === "frontmatter" && file.data.frontmatter) { created ||= file.data.frontmatter.date modified ||= file.data.frontmatter.lastmod + modified ||= file.data.frontmatter.updated modified ||= file.data.frontmatter["last-modified"] published ||= file.data.frontmatter.publishDate } else if (source === "git") { diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index a2607196..d867039d 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -79,9 +79,8 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = ) { if (!isAbsoluteUrl(node.properties.src)) { let dest = node.properties.src as RelativeURL - const ext = path.extname(node.properties.src) dest = node.properties.src = transformLink(curSlug, dest, transformOptions) - node.properties.src = dest + ext + node.properties.src = dest } } }) diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 97054b1f..b324daca 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -196,7 +196,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin // embed cases if (value.startsWith("!")) { const ext: string = path.extname(fp).toLowerCase() - const url = slugifyFilePath(fp as FilePath) + ext + const url = slugifyFilePath(fp as FilePath) if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg"].includes(ext)) { const dims = alias ?? "" let [width, height] = dims.split("x", 2) diff --git a/quartz/util/path.test.ts b/quartz/util/path.test.ts index 5655585d..ddbb0ee9 100644 --- a/quartz/util/path.test.ts +++ b/quartz/util/path.test.ts @@ -49,6 +49,7 @@ describe("typeguards", () => { assert(path.isRelativeURL("./abc/def#an-anchor")) assert(path.isRelativeURL("./abc/def?query=1#an-anchor")) assert(path.isRelativeURL("../abc/def")) + assert(path.isRelativeURL("./abc/def.pdf")) assert(!path.isRelativeURL("abc")) assert(!path.isRelativeURL("/abc/def")) @@ -60,12 +61,12 @@ describe("typeguards", () => { test("isServerSlug", () => { assert(path.isServerSlug("index")) assert(path.isServerSlug("abc/def")) + assert(path.isServerSlug("html.energy")) + assert(path.isServerSlug("test.pdf")) assert(!path.isServerSlug(".")) assert(!path.isServerSlug("./abc/def")) assert(!path.isServerSlug("../abc/def")) - assert(!path.isServerSlug("index.html")) - assert(!path.isServerSlug("abc/def.html")) assert(!path.isServerSlug("abc/def#anchor")) assert(!path.isServerSlug("abc/def?query=1")) assert(!path.isServerSlug("note with spaces")) @@ -140,11 +141,12 @@ describe("transforms", () => { asserts( [ ["content/index.md", "content/index"], + ["content/index.html", "content/index"], ["content/_index.md", "content/index"], ["/content/index.md", "content/index"], - ["content/cool.png", "content/cool"], + ["content/cool.png", "content/cool.png"], ["index.md", "index"], - ["test.mp4", "test"], + ["test.mp4", "test.mp4"], ["note with spaces.md", "note-with-spaces"], ], path.slugifyFilePath, @@ -160,10 +162,13 @@ describe("transforms", () => { [".", "."], ["./", "./"], ["./index", "./"], + ["./index#abc", "./#abc"], ["./index.html", "./"], ["./index.md", "./"], + ["./index.css", "./index.css"], ["content", "./content"], ["content/test.md", "./content/test"], + ["content/test.pdf", "./content/test.pdf"], ["./content/test.md", "./content/test"], ["../content/test.md", "../content/test"], ["tags/", "./tags/"], @@ -193,7 +198,7 @@ describe("transforms", () => { }) describe("link strategies", () => { - const allSlugs = ["a/b/c", "a/b/d", "a/b/index", "e/f", "e/g/h", "index"] as ServerSlug[] + const allSlugs = ["a/b/c", "a/b/d", "a/b/index", "e/f", "e/g/h", "index", "a/test.png"] as ServerSlug[] describe("absolute", () => { const opts: TransformOptions = { @@ -204,27 +209,29 @@ describe("link strategies", () => { test("from a/b/c", () => { const cur = "a/b/c" as CanonicalSlug assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../../a/b/d") - assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b/") assert.strictEqual(path.transformLink(cur, "e/f", opts), "../../../e/f") assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "../../../e/g/h") - assert.strictEqual(path.transformLink(cur, "index", opts), "../../..") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../../") + assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../../index.png") assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../../#abc") assert.strictEqual(path.transformLink(cur, "tag/test", opts), "../../../tag/test") assert.strictEqual(path.transformLink(cur, "a/b/c#test", opts), "../../../a/b/c#test") + assert.strictEqual(path.transformLink(cur, "a/test.png", opts), "../../../a/test.png") }) test("from a/b/index", () => { const cur = "a/b" as CanonicalSlug assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d") assert.strictEqual(path.transformLink(cur, "a/b", opts), "../../a/b") - assert.strictEqual(path.transformLink(cur, "index", opts), "../..") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../") }) test("from index", () => { const cur = "" as CanonicalSlug - assert.strictEqual(path.transformLink(cur, "index", opts), ".") + assert.strictEqual(path.transformLink(cur, "index", opts), "./") assert.strictEqual(path.transformLink(cur, "a/b/c", opts), "./a/b/c") - assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/") }) }) @@ -238,24 +245,29 @@ describe("link strategies", () => { const cur = "a/b/c" as CanonicalSlug assert.strictEqual(path.transformLink(cur, "d", opts), "../../../a/b/d") assert.strictEqual(path.transformLink(cur, "h", opts), "../../../e/g/h") - assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b") - assert.strictEqual(path.transformLink(cur, "index", opts), "../../..") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b/") + assert.strictEqual(path.transformLink(cur, "a/b/index.png", opts), "../../../a/b/index.png") + assert.strictEqual(path.transformLink(cur, "a/b/index#abc", opts), "../../../a/b/#abc") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../../") + assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../../index.png") + assert.strictEqual(path.transformLink(cur, "test.png", opts), "../../../a/test.png") + assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../../#abc") }) test("from a/b/index", () => { const cur = "a/b" as CanonicalSlug assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d") assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h") - assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b") - assert.strictEqual(path.transformLink(cur, "index", opts), "../..") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/") + assert.strictEqual(path.transformLink(cur, "index", opts), "../../") }) test("from index", () => { const cur = "" as CanonicalSlug assert.strictEqual(path.transformLink(cur, "d", opts), "./a/b/d") assert.strictEqual(path.transformLink(cur, "h", opts), "./e/g/h") - assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b") - assert.strictEqual(path.transformLink(cur, "index", opts), ".") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/") + assert.strictEqual(path.transformLink(cur, "index", opts), "./") }) }) @@ -269,9 +281,14 @@ describe("link strategies", () => { const cur = "a/b/c" as CanonicalSlug assert.strictEqual(path.transformLink(cur, "d", opts), "./d") assert.strictEqual(path.transformLink(cur, "index", opts), "./") - assert.strictEqual(path.transformLink(cur, "../../index", opts), "../../") - assert.strictEqual(path.transformLink(cur, "../../", opts), "../../") - assert.strictEqual(path.transformLink(cur, "../../e/g/h", opts), "../../e/g/h") + assert.strictEqual(path.transformLink(cur, "../../../index", opts), "../../../") + assert.strictEqual(path.transformLink(cur, "../../../index.png", opts), "../../../index.png") + assert.strictEqual(path.transformLink(cur, "../../../index#abc", opts), "../../../#abc") + assert.strictEqual(path.transformLink(cur, "../../../", opts), "../../../") + assert.strictEqual(path.transformLink(cur, "../../../a/test.png", opts), "../../../a/test.png") + assert.strictEqual(path.transformLink(cur, "../../../e/g/h", opts), "../../../e/g/h") + assert.strictEqual(path.transformLink(cur, "../../../e/g/h", opts), "../../../e/g/h") + assert.strictEqual(path.transformLink(cur, "../../../e/g/h#abc", opts), "../../../e/g/h#abc") }) test("from a/b/index", () => { diff --git a/quartz/util/path.ts b/quartz/util/path.ts index c1016af5..c949474f 100644 --- a/quartz/util/path.ts +++ b/quartz/util/path.ts @@ -72,7 +72,7 @@ export type RelativeURL = SlugLike<"relative"> export function isRelativeURL(s: string): s is RelativeURL { const validStart = /^\.{1,2}/.test(s) const validEnding = !(s.endsWith("/index") || s === "index") - return validStart && validEnding && !_hasFileExtension(s) + return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "") } /** A server side slug. This is what Quartz uses to emit files so uses index suffixes */ @@ -80,7 +80,7 @@ export type ServerSlug = SlugLike<"server"> export function isServerSlug(s: string): s is ServerSlug { const validStart = !(s.startsWith(".") || s.startsWith("/")) const validEnding = !s.endsWith("/") - return validStart && validEnding && !_containsForbiddenCharacters(s) && !_hasFileExtension(s) + return validStart && validEnding && !_containsForbiddenCharacters(s) } /** The real file path to a file on disk */ @@ -114,9 +114,14 @@ export function canonicalizeServer(slug: ServerSlug): CanonicalSlug { return res } -export function slugifyFilePath(fp: FilePath): ServerSlug { +export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): ServerSlug { fp = _stripSlashes(fp) as FilePath - const withoutFileExt = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "") + let ext = _getFileExtension(fp) + const withoutFileExt = fp.replace(new RegExp(ext + "$"), "") + if (excludeExt || [".md", ".html", undefined].includes(ext)) { + ext = "" + } + let slug = withoutFileExt .split("/") .map((segment) => segment.replace(/\s/g, "-")) // slugify all segments @@ -128,7 +133,7 @@ export function slugifyFilePath(fp: FilePath): ServerSlug { slug = slug.replace(/_index$/, "index") } - return slug as ServerSlug + return slug + ext as ServerSlug } export function transformInternalLink(link: string): RelativeURL { @@ -139,19 +144,16 @@ export function transformInternalLink(link: string): RelativeURL { fplike.endsWith("index.md") || fplike.endsWith("index.html") || fplike.endsWith("/") + let segments = fplike.split("/").filter((x) => x.length > 0) let prefix = segments.filter(_isRelativeSegment).join("/") let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/") - // implicit markdown - if (!_hasFileExtension(fp)) { - fp += ".md" - } - - fp = canonicalizeServer(slugifyFilePath(fp as FilePath)) + // manually add ext here as we want to not strip 'index' if it has an extension + fp = canonicalizeServer(slugifyFilePath(fp as FilePath) as ServerSlug) const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp)) const trail = folderPath ? "/" : "" - const res = (_addRelativeToStart(joined) + anchor + trail) as RelativeURL + const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL return res } @@ -217,8 +219,9 @@ export function transformLink( if (opts.strategy === "relative") { return _addRelativeToStart(targetSlug) as RelativeURL } else { - targetSlug = _stripSlashes(targetSlug.slice(".".length)) - let [targetCanonical, targetAnchor] = splitAnchor(targetSlug) + const folderTail = targetSlug.endsWith("/") ? "/" : "" + const canonicalSlug = _stripSlashes(targetSlug.slice(".".length)) + let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug) if (opts.strategy === "shortest") { // if the file name is unique, then it's just the filename @@ -236,7 +239,7 @@ export function transformLink( } // if it's not unique, then it's the absolute path from the vault root - return joinSegments(pathToRoot(src), targetSlug) as RelativeURL + return joinSegments(pathToRoot(src), canonicalSlug) + folderTail as RelativeURL } }