fix relative path resolution logic, add more path tests
| | |
| | | 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", () => { |
| | |
| | | ) |
| | | }) |
| | | |
| | | describe("slugifyFilePath", () => { |
| | | test("slugifyFilePath", () => { |
| | | asserts( |
| | | [ |
| | | ["content/index.md", "content/index"], |
| | |
| | | ) |
| | | }) |
| | | |
| | | describe("transformInternalLink", () => { |
| | | test("transformInternalLink", () => { |
| | | asserts( |
| | | [ |
| | | ["", "."], |
| | |
| | | ) |
| | | }) |
| | | |
| | | describe("pathToRoot", () => { |
| | | test("pathToRoot", () => { |
| | | asserts( |
| | | [ |
| | | ["", "."], |
| | |
| | | ) |
| | | }) |
| | | }) |
| | | |
| | | 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") |
| | | }) |
| | | }) |
| | | }) |
| | |
| | | // └────────────┤ MD File ├─────┴─────────────────┘ |
| | | // └─────────┘ |
| | | |
| | | export const QUARTZ = "quartz" |
| | | |
| | | /// Utility type to simulate nominal types in TypeScript |
| | | type SlugLike<T> = string & { __brand: T } |
| | | |
| | |
| | | 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") |
| | |
| | | import { |
| | | CanonicalSlug, |
| | | RelativeURL, |
| | | TransformOptions, |
| | | _stripSlashes, |
| | | canonicalizeServer, |
| | | joinSegments, |
| | | pathToRoot, |
| | | resolveRelative, |
| | | splitAnchor, |
| | | transformInternalLink, |
| | | transformLink, |
| | | } from "../../path" |
| | | import path from "path" |
| | | import { visit } from "unist-util-visit" |
| | |
| | | |
| | | 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 |
| | | } |
| | |
| | | () => { |
| | | 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 |
| | | }) |
| | | |
| | | // 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 outgoing: Set<CanonicalSlug> = new Set() |
| | | |
| | | const transformOptions: TransformOptions = { |
| | | strategy: opts.markdownLinkResolution, |
| | | allSlugs: ctx.allSlugs, |
| | | } |
| | | |
| | | visit(tree, "element", (node, _index, _parent) => { |
| | | // rewrite all links |
| | | if ( |
| | |
| | | |
| | | // 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) |
| | |
| | | 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 |
| | | } |
| | | } |
| | |
| | | width: 100vw; |
| | | } |
| | | |
| | | body, section { |
| | | body, |
| | | section { |
| | | margin: 0; |
| | | max-width: 100%; |
| | | box-sizing: border-box; |