fix relative path resolution logic, add more path tests
This commit is contained in:
parent
118712fddb
commit
eb3981d514
@ -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…
Reference in New Issue
Block a user