note-properties/test/slugifyWikilinkTarget.test.ts
saberzero1 3539cf1e6a
fix: slugify wikilinks in frontmatter to prevent %20 in hrefs
Wikilinks appearing in frontmatter property strings (e.g. 'related:
"[[My Note]]"') were rendered with their raw target passed
directly to resolveRelative, leaving the space intact. The browser
then percent-encoded it as %20 in the rendered href, producing
broken links like './My%20Note' that never resolve.

The root cause was in renderTextWithLinks (NoteProperties.tsx):
the matched wikilink target went straight into resolveRelative
without going through slugifyFilePath, so spaces, ampersands, and
other characters that should be slugified survived into the href.
extractLinksFromValue (transformer.ts) had the same issue: it
stored raw target strings in file.data.frontmatterLinks instead
of canonical slugs.

Adds a shared slugifyWikilinkTarget helper in util/path.ts that
mirrors the slug+anchor contract CrawlLinks uses for body-content
links, so frontmatter wikilinks and body wikilinks now produce
identical hrefs. Both NoteProperties.tsx and the transformer's
extractLinksFromValue now go through this helper.

Also replaces the plugin's local slugTag/slugifyFilePath/
getFileExtension helpers with the shared @quartz-community/utils
versions. This eliminates slugification drift between plugins and
ensures that note-properties honors the same case-insensitive
matching that the rest of the Quartz ecosystem uses.

BREAKING CHANGE: tag URLs and frontmatter-wikilink slugs are now
lowercased (inherited from utils' case-insensitive matching).
Tags #MyTag, #mytag, and #MYTAG collapse into a single tag page
tags/mytag.
2026-04-16 22:22:10 +02:00

42 lines
1.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { slugifyWikilinkTarget } from "../src/util/path";
describe("slugifyWikilinkTarget", () => {
it("converts spaces in the path to hyphens", () => {
expect(slugifyWikilinkTarget("My Note")).toBe("my-note");
});
it("lowercases case in the path (Obsidian-parity case-insensitive matching)", () => {
expect(slugifyWikilinkTarget("CamelCase")).toBe("camelcase");
});
it("strips the trailing .md extension", () => {
expect(slugifyWikilinkTarget("My Note.md")).toBe("my-note");
});
it("slugifies a nested path per segment", () => {
expect(slugifyWikilinkTarget("Folder Name/My Note")).toBe("folder-name/my-note");
});
it("preserves and slugifies the anchor after '#'", () => {
expect(slugifyWikilinkTarget("My Note#Some Section")).toBe("my-note#some-section");
});
it("returns the slugified anchor alone when path is empty", () => {
expect(slugifyWikilinkTarget("#Just An Anchor")).toBe("#just-an-anchor");
});
it("handles targets with no spaces unchanged", () => {
expect(slugifyWikilinkTarget("already-slugged")).toBe("already-slugged");
});
it("replaces '&' with '-and-'", () => {
expect(slugifyWikilinkTarget("Foo&Bar")).toBe("foo-and-bar");
});
it("never produces '%20' for a target that contains a space", () => {
const result = slugifyWikilinkTarget("My Note");
expect(result).not.toContain("%20");
expect(result).not.toContain(" ");
});
});