fix relative path resolution logic, add more path tests
This commit is contained in:
		
							parent
							
								
									6d9ffd6da5
								
							
						
					
					
						commit
						d6e73f221c
					
				
					 4 changed files with 152 additions and 36 deletions
				
			
		| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import test, { describe } from "node:test"
 | 
					import test, { describe } from "node:test"
 | 
				
			||||||
import * as path from "./path"
 | 
					import * as path from "./path"
 | 
				
			||||||
import assert from "node:assert"
 | 
					import assert from "node:assert"
 | 
				
			||||||
 | 
					import { CanonicalSlug, ServerSlug, TransformOptions } from "./path"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("typeguards", () => {
 | 
					describe("typeguards", () => {
 | 
				
			||||||
  test("isClientSlug", () => {
 | 
					  test("isClientSlug", () => {
 | 
				
			||||||
| 
						 | 
					@ -137,7 +138,7 @@ describe("transforms", () => {
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe("slugifyFilePath", () => {
 | 
					  test("slugifyFilePath", () => {
 | 
				
			||||||
    asserts(
 | 
					    asserts(
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        ["content/index.md", "content/index"],
 | 
					        ["content/index.md", "content/index"],
 | 
				
			||||||
| 
						 | 
					@ -154,7 +155,7 @@ describe("transforms", () => {
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe("transformInternalLink", () => {
 | 
					  test("transformInternalLink", () => {
 | 
				
			||||||
    asserts(
 | 
					    asserts(
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        ["", "."],
 | 
					        ["", "."],
 | 
				
			||||||
| 
						 | 
					@ -178,7 +179,7 @@ describe("transforms", () => {
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe("pathToRoot", () => {
 | 
					  test("pathToRoot", () => {
 | 
				
			||||||
    asserts(
 | 
					    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")
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,8 @@ import { slug } from "github-slugger"
 | 
				
			||||||
//                                             └────────────┤ MD File ├─────┴─────────────────┘
 | 
					//                                             └────────────┤ MD File ├─────┴─────────────────┘
 | 
				
			||||||
//                                                          └─────────┘
 | 
					//                                                          └─────────┘
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const QUARTZ = "quartz"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Utility type to simulate nominal types in TypeScript
 | 
					/// Utility type to simulate nominal types in TypeScript
 | 
				
			||||||
type SlugLike<T> = string & { __brand: T }
 | 
					type SlugLike<T> = string & { __brand: T }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -194,7 +196,43 @@ export function getAllSegmentPrefixes(tags: string): string[] {
 | 
				
			||||||
  return results
 | 
					  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 {
 | 
					function _canonicalize(fp: string): string {
 | 
				
			||||||
  fp = _trimSuffix(fp, "index")
 | 
					  fp = _trimSuffix(fp, "index")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,12 @@ import { QuartzTransformerPlugin } from "../types"
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  CanonicalSlug,
 | 
					  CanonicalSlug,
 | 
				
			||||||
  RelativeURL,
 | 
					  RelativeURL,
 | 
				
			||||||
 | 
					  TransformOptions,
 | 
				
			||||||
  _stripSlashes,
 | 
					  _stripSlashes,
 | 
				
			||||||
  canonicalizeServer,
 | 
					  canonicalizeServer,
 | 
				
			||||||
  joinSegments,
 | 
					  joinSegments,
 | 
				
			||||||
  pathToRoot,
 | 
					 | 
				
			||||||
  resolveRelative,
 | 
					 | 
				
			||||||
  splitAnchor,
 | 
					  splitAnchor,
 | 
				
			||||||
  transformInternalLink,
 | 
					  transformLink,
 | 
				
			||||||
} from "../../path"
 | 
					} from "../../path"
 | 
				
			||||||
import path from "path"
 | 
					import path from "path"
 | 
				
			||||||
import { visit } from "unist-util-visit"
 | 
					import { visit } from "unist-util-visit"
 | 
				
			||||||
| 
						 | 
					@ -16,7 +15,7 @@ import isAbsoluteUrl from "is-absolute-url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Options {
 | 
					interface Options {
 | 
				
			||||||
  /** How to resolve Markdown paths */
 | 
					  /** How to resolve Markdown paths */
 | 
				
			||||||
  markdownLinkResolution: "absolute" | "relative" | "shortest"
 | 
					  markdownLinkResolution: TransformOptions["strategy"]
 | 
				
			||||||
  /** Strips folders from a link so that it looks nice */
 | 
					  /** Strips folders from a link so that it looks nice */
 | 
				
			||||||
  prettyLinks: boolean
 | 
					  prettyLinks: boolean
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -35,34 +34,13 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
				
			||||||
        () => {
 | 
					        () => {
 | 
				
			||||||
          return (tree, file) => {
 | 
					          return (tree, file) => {
 | 
				
			||||||
            const curSlug = canonicalizeServer(file.data.slug!)
 | 
					            const curSlug = canonicalizeServer(file.data.slug!)
 | 
				
			||||||
            const transformLink = (target: string): RelativeURL => {
 | 
					            const outgoing: Set<CanonicalSlug> = new Set()
 | 
				
			||||||
              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
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // only match, just use it
 | 
					            const transformOptions: TransformOptions = {
 | 
				
			||||||
                if (matchingFileNames.length === 1) {
 | 
					              strategy: opts.markdownLinkResolution,
 | 
				
			||||||
                  const targetSlug = canonicalizeServer(matchingFileNames[0])
 | 
					              allSlugs: ctx.allSlugs,
 | 
				
			||||||
                  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 outgoing: Set<CanonicalSlug> = new Set()
 | 
					 | 
				
			||||||
            visit(tree, "element", (node, _index, _parent) => {
 | 
					            visit(tree, "element", (node, _index, _parent) => {
 | 
				
			||||||
              // rewrite all links
 | 
					              // rewrite all links
 | 
				
			||||||
              if (
 | 
					              if (
 | 
				
			||||||
| 
						 | 
					@ -76,7 +54,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // don't process external links or intra-document anchors
 | 
					                // don't process external links or intra-document anchors
 | 
				
			||||||
                if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
 | 
					                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 canonicalDest = path.posix.normalize(joinSegments(curSlug, dest))
 | 
				
			||||||
                  const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
 | 
					                  const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
 | 
				
			||||||
                  outgoing.add(destCanonical as CanonicalSlug)
 | 
					                  outgoing.add(destCanonical as CanonicalSlug)
 | 
				
			||||||
| 
						 | 
					@ -102,7 +80,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 | 
				
			||||||
                if (!isAbsoluteUrl(node.properties.src)) {
 | 
					                if (!isAbsoluteUrl(node.properties.src)) {
 | 
				
			||||||
                  let dest = node.properties.src as RelativeURL
 | 
					                  let dest = node.properties.src as RelativeURL
 | 
				
			||||||
                  const ext = path.extname(node.properties.src)
 | 
					                  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
 | 
					                  node.properties.src = dest + ext
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,8 @@ html {
 | 
				
			||||||
  width: 100vw;
 | 
					  width: 100vw;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
body, section {
 | 
					body,
 | 
				
			||||||
 | 
					section {
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
  max-width: 100%;
 | 
					  max-width: 100%;
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue