Jacky Zhao
2023-08-13 d6e73f221c3e52ce6591cbd01621530e5f6fd703
fix relative path resolution logic, add more path tests
4 files modified
188 ■■■■ changed files
quartz/path.test.ts 105 ●●●●● patch | view | raw | blame | history
quartz/path.ts 40 ●●●●● patch | view | raw | blame | history
quartz/plugins/transformers/links.ts 40 ●●●● patch | view | raw | blame | history
quartz/styles/base.scss 3 ●●●● patch | view | raw | blame | history
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("slugifyFilePath", () => {
  test("slugifyFilePath", () => {
    asserts(
      [
        ["content/index.md", "content/index"],
@@ -154,7 +155,7 @@
    )
  })
  describe("transformInternalLink", () => {
  test("transformInternalLink", () => {
    asserts(
      [
        ["", "."],
@@ -178,7 +179,7 @@
    )
  })
  describe("pathToRoot", () => {
  test("pathToRoot", () => {
    asserts(
      [
        ["", "."],
@@ -191,3 +192,101 @@
    )
  })
})
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")
    })
  })
})
quartz/path.ts
@@ -42,6 +42,8 @@
//                                             └────────────┤ MD File ├─────┴─────────────────┘
//                                                          └─────────┘
export const QUARTZ = "quartz"
/// Utility type to simulate nominal types in TypeScript
type SlugLike<T> = string & { __brand: T }
@@ -194,7 +196,43 @@
  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")
quartz/plugins/transformers/links.ts
@@ -2,13 +2,12 @@
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 @@
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 @@
        () => {
          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<CanonicalSlug> = 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<CanonicalSlug> = new Set()
            visit(tree, "element", (node, _index, _parent) => {
              // rewrite all links
              if (
@@ -76,7 +54,7 @@
                // 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 @@
                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
                }
              }
quartz/styles/base.scss
@@ -11,7 +11,8 @@
  width: 100vw;
}
body, section {
body,
section {
  margin: 0;
  max-width: 100%;
  box-sizing: border-box;