fix relative path resolution logic, add more path tests
This commit is contained in:
		
							parent
							
								
									6d9ffd6da5
								
							
						
					
					
						commit
						d6e73f221c
					
				| @ -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") | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| @ -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<T> = 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") | ||||
|  | ||||
| @ -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<Partial<Options> | 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<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 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | 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<Partial<Options> | 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 | ||||
|                 } | ||||
|               } | ||||
|  | ||||
| @ -11,7 +11,8 @@ html { | ||||
|   width: 100vw; | ||||
| } | ||||
| 
 | ||||
| body, section { | ||||
| body, | ||||
| section { | ||||
|   margin: 0; | ||||
|   max-width: 100%; | ||||
|   box-sizing: border-box; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user