diff --git a/plug-api/lib/ref.test.ts b/plug-api/lib/ref.test.ts
index 535f15db..93f67fb4 100644
--- a/plug-api/lib/ref.test.ts
+++ b/plug-api/lib/ref.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from "vitest";
+import { describe, expect, test } from "vitest";
import {
decodePageURI,
encodePageURI,
@@ -117,6 +117,50 @@ test("isValidPath() and isValidName()", () => {
expect(!isValidName("hello>there")).toBeTruthy();
});
+describe("anchor refs", () => {
+ test("parseToRef bare anchor", () => {
+ expect(parseToRef("$pete")).toEqual({
+ path: "",
+ details: { type: "anchor", name: "pete" },
+ });
+ });
+
+ test("parseToRef page-qualified anchor", () => {
+ expect(parseToRef("Some Page$pete")).toEqual({
+ path: "Some Page.md",
+ details: { type: "anchor", name: "pete" },
+ });
+ });
+
+ test("parseToRef accepts allowed character classes", () => {
+ for (const name of ["toc1", "tasks/7", "a:b", "a-b", "_foo"]) {
+ const ref = parseToRef("$" + name);
+ expect(ref?.details).toEqual({ type: "anchor", name });
+ }
+ });
+
+ test("parseToRef rejects digit-leading anchors", () => {
+ // "$100" should NOT be parsed as an anchor; falling through to
+ // the path branch should fail because the path would be "$100"
+ // which is not a valid path either.
+ expect(parseToRef("$100")).toBeNull();
+ });
+
+ test("encodeRef round-trips bare anchor", () => {
+ expect(encodeRef({ path: "", details: { type: "anchor", name: "pete" } }))
+ .toBe("$pete");
+ });
+
+ test("encodeRef round-trips page-qualified anchor", () => {
+ expect(
+ encodeRef({
+ path: "Some Page.md",
+ details: { type: "anchor", name: "pete" },
+ }),
+ ).toBe("Some Page$pete");
+ });
+});
+
test("Page URI encoding", () => {
expect(encodePageURI("foo")).toEqual("foo");
expect(encodePageURI("folder/foo")).toEqual("folder/foo");
diff --git a/plug-api/lib/ref.ts b/plug-api/lib/ref.ts
index edbf33c5..672e7af0 100644
--- a/plug-api/lib/ref.ts
+++ b/plug-api/lib/ref.ts
@@ -23,7 +23,8 @@ export type Ref = {
details?:
| { type: "position"; pos: number }
| { type: "linecolumn"; line: number; column: number }
- | { type: "header"; header: string };
+ | { type: "header"; header: string }
+ | { type: "anchor"; name: string };
};
/**
@@ -96,7 +97,7 @@ export function isValidPath(path: string): path is Path {
* TO THE INNER WORKINGS OF SILVERBULLET AND CHANGES COULD INTRODUCE MAJOR BUGS
*/
const refRegex =
- /^(?\^)?(?(?!.*\.[a-zA-Z0-9]+\.md$)(?!\/?(\.|\^))(?!.*(?:\/|^)\.{1,2}(?:\/|$)|.*\/{2})(?!.*(?:\]\]|\[\[))[^@#|<>]*)(@(?\d+)|@[Ll](?\d+)(?:[Cc](?\d+))?|#\s*(?.*))?$/;
+ /^(?\^)?(?(?!.*\.[a-zA-Z0-9]+\.md$)(?!\/?(\.|\^))(?!.*(?:\/|^)\.{1,2}(?:\/|$)|.*\/{2})(?!.*(?:\]\]|\[\[))[^@#|<>$]*)(@(?\d+)|@[Ll](?\d+)(?:[Cc](?\d+))?|#\s*(?.*)|\$(?[A-Za-z_][A-Za-z0-9_/:-]*))?$/;
/**
* Parses a reference string into a ref object.
@@ -132,6 +133,8 @@ export function parseToRef(stringRef: string): Ref | null {
type: "header",
header: groups.header,
};
+ } else if (groups.anchor !== undefined) {
+ ref.details = { type: "anchor", name: groups.anchor };
}
return ref;
@@ -158,6 +161,8 @@ export function encodeRef(ref: Ref): string {
stringRef += `@${ref.details.pos}`;
} else if (ref.details?.type === "header") {
stringRef += `#${ref.details.header}`;
+ } else if (ref.details?.type === "anchor") {
+ stringRef += `$${ref.details.name}`;
}
return stringRef;
@@ -191,6 +196,8 @@ export function getOffsetFromRef(
case "header": {
return getOffsetFromHeader(parseTree, ref.details.header);
}
+ case "anchor":
+ return -1;
}
}