diff --git a/quartz/path.test.ts b/quartz/path.test.ts index 480493ef..d86bca5f 100644 --- a/quartz/path.test.ts +++ b/quartz/path.test.ts @@ -1,6 +1,7 @@ import test, { describe } from "node:test" import * as path from "./path" import assert from "node:assert" +import { CanonicalSlug, ServerSlug, TransformOptions } from "./path" describe("typeguards", () => { test("isClientSlug", () => { @@ -137,7 +138,7 @@ describe("transforms", () => { ) }) - describe("slugifyFilePath", () => { + test("slugifyFilePath", () => { asserts( [ ["content/index.md", "content/index"], @@ -154,7 +155,7 @@ describe("transforms", () => { ) }) - describe("transformInternalLink", () => { + test("transformInternalLink", () => { asserts( [ ["", "."], @@ -178,7 +179,7 @@ describe("transforms", () => { ) }) - describe("pathToRoot", () => { + test("pathToRoot", () => { asserts( [ ["", "."], @@ -191,3 +192,101 @@ describe("transforms", () => { ) }) }) + +describe("link strategies", () => { + const allSlugs = ["a/b/c", "a/b/d", "a/b/index", "e/f", "e/g/h", "index"] as ServerSlug[] + + describe("absolute", () => { + const opts: TransformOptions = { + strategy: "absolute", + allSlugs, + } + + 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, "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#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") + }) + + 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), "../..") + }) + + test("from index", () => { + const cur = "" as CanonicalSlug + 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") + }) + }) + + describe("shortest", () => { + const opts: TransformOptions = { + strategy: "shortest", + allSlugs, + } + + test("from a/b/c", () => { + 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), "../../..") + }) + + 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), "../..") + }) + + 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), ".") + }) + }) + + describe("relative", () => { + const opts: TransformOptions = { + strategy: "relative", + allSlugs, + } + + test("from a/b/c", () => { + 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") + }) + + test("from a/b/index", () => { + const cur = "a/b" as CanonicalSlug + 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, "c", opts), "./c") + }) + + test("from index", () => { + const cur = "" as CanonicalSlug + assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "./e/g/h") + assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b") + }) + }) +}) diff --git a/quartz/path.ts b/quartz/path.ts index e410771a..e5bd0d6d 100644 --- a/quartz/path.ts +++ b/quartz/path.ts @@ -42,6 +42,8 @@ import { slug } from "github-slugger" // └────────────┤ MD File ├─────┴─────────────────┘ // └─────────┘ +export const QUARTZ = "quartz" + /// Utility type to simulate nominal types in TypeScript type SlugLike = string & { __brand: T } @@ -194,7 +196,43 @@ export function getAllSegmentPrefixes(tags: string): string[] { return results } -export const QUARTZ = "quartz" +export interface TransformOptions { + strategy: "absolute" | "relative" | "shortest" + allSlugs: ServerSlug[] +} + +export function transformLink( + src: CanonicalSlug, + target: string, + opts: TransformOptions, +): RelativeURL { + let targetSlug: string = transformInternalLink(target) + + if (opts.strategy === "relative") { + return _addRelativeToStart(targetSlug) as RelativeURL + } else { + targetSlug = _stripSlashes(targetSlug.slice(".".length)) + let [targetCanonical, targetAnchor] = splitAnchor(targetSlug) + + if (opts.strategy === "shortest") { + // if the file name is unique, then it's just the filename + const matchingFileNames = opts.allSlugs.filter((slug) => { + const parts = slug.split("/") + const fileName = parts.at(-1) + return targetCanonical === fileName + }) + + // only match, just use it + if (matchingFileNames.length === 1) { + const targetSlug = canonicalizeServer(matchingFileNames[0]) + return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL + } + } + + // if it's not unique, then it's the absolute path from the vault root + return joinSegments(pathToRoot(src), targetSlug) as RelativeURL + } +} function _canonicalize(fp: string): string { fp = _trimSuffix(fp, "index") diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index 5b15725a..10b527c9 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -2,13 +2,12 @@ import { QuartzTransformerPlugin } from "../types" import { CanonicalSlug, RelativeURL, + TransformOptions, _stripSlashes, canonicalizeServer, joinSegments, - pathToRoot, - resolveRelative, splitAnchor, - transformInternalLink, + transformLink, } from "../../path" import path from "path" import { visit } from "unist-util-visit" @@ -16,7 +15,7 @@ import isAbsoluteUrl from "is-absolute-url" interface Options { /** How to resolve Markdown paths */ - markdownLinkResolution: "absolute" | "relative" | "shortest" + markdownLinkResolution: TransformOptions["strategy"] /** Strips folders from a link so that it looks nice */ prettyLinks: boolean } @@ -35,34 +34,13 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = () => { return (tree, file) => { const curSlug = canonicalizeServer(file.data.slug!) - const transformLink = (target: string): RelativeURL => { - const targetSlug = _stripSlashes(transformInternalLink(target).slice(".".length)) - let [targetCanonical, targetAnchor] = splitAnchor(targetSlug) - if (opts.markdownLinkResolution === "relative") { - return targetSlug as RelativeURL - } else if (opts.markdownLinkResolution === "shortest") { - // if the file name is unique, then it's just the filename - const matchingFileNames = ctx.allSlugs.filter((slug) => { - const parts = slug.split(path.posix.sep) - const fileName = parts.at(-1) - return targetCanonical === fileName - }) + const outgoing: Set = new Set() - // only match, just use it - if (matchingFileNames.length === 1) { - const targetSlug = canonicalizeServer(matchingFileNames[0]) - return (resolveRelative(curSlug, targetSlug) + targetAnchor) as RelativeURL - } - - // if it's not unique, then it's the absolute path from the vault root - // (fall-through case) - } - - // treat as absolute - return joinSegments(pathToRoot(curSlug), targetSlug) as RelativeURL + const transformOptions: TransformOptions = { + strategy: opts.markdownLinkResolution, + allSlugs: ctx.allSlugs, } - const outgoing: Set = new Set() visit(tree, "element", (node, _index, _parent) => { // rewrite all links if ( @@ -76,7 +54,7 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = // don't process external links or intra-document anchors if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { - dest = node.properties.href = transformLink(dest) + dest = node.properties.href = transformLink(curSlug, dest, transformOptions) const canonicalDest = path.posix.normalize(joinSegments(curSlug, dest)) const [destCanonical, _destAnchor] = splitAnchor(canonicalDest) outgoing.add(destCanonical as CanonicalSlug) @@ -102,7 +80,7 @@ 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(dest) + dest = node.properties.src = transformLink(curSlug, dest, transformOptions) node.properties.src = dest + ext } } diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index fc23b3a0..b5e6b273 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -11,7 +11,8 @@ html { width: 100vw; } -body, section { +body, +section { margin: 0; max-width: 100%; box-sizing: border-box;