fix relative path resolution logic, add more path tests

This commit is contained in:
Jacky Zhao 2023-08-12 21:16:34 -07:00
parent 118712fddb
commit eb3981d514
4 changed files with 152 additions and 36 deletions

View File

@ -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")
})
})
})

View File

@ -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")

View File

@ -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
} }
} }

View File

@ -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;