diff --git a/package.json b/package.json
index e708fb1..01d6d2b 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"scripts": {
"prepare": "tsc",
"build": "node --enable-source-maps build/node/cli.js",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "node --test --enable-source-maps --experimental-test-coverage"
},
"dependencies": {
"antihtml": "^0.3.0"
diff --git a/tsconfig.json b/tsconfig.json
index 3c721be..2e679cd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"files": ["cli.ts"],
- "include": ["content"],
+ "include": ["content", "utils"],
"compilerOptions": {
"outDir": "build/node",
"rootDir": ".",
diff --git a/utils/resolve-refs.test.tsx b/utils/resolve-refs.test.tsx
new file mode 100644
index 0000000..9b29330
--- /dev/null
+++ b/utils/resolve-refs.test.tsx
@@ -0,0 +1,51 @@
+import assert from "node:assert/strict";
+import { suite, test } from "node:test";
+import { resolveRefs } from "./resolve-refs.js";
+import type { Element } from "antihtml";
+
+
+suite("function resolveRefs", () => {
+ test("root to root relative href", () => {
+ const el = resolveRefs(Link, "/");
+ assert.equal((el as Element).attributes.get("href"), "page.html");
+ });
+
+ test("root to subdir relative href", () => {
+ const el = resolveRefs(Link, "/");
+ assert.equal((el as Element).attributes.get("href"), "dir/page.html");
+ });
+
+ test("subdir to root relative href", () => {
+ const el = resolveRefs(Link, "/subdir");
+ assert.equal((el as Element).attributes.get("href"), "../page.html");
+ });
+
+ test("subdir to subdir relative href", () => {
+ const el = resolveRefs(Link, "/subdir");
+ assert.equal((el as Element).attributes.get("href"), "../alt/page.html");
+ });
+
+ test("nested element", () => {
+ const el = resolveRefs(
, "/");
+ assert.equal((el.childNodes[1] as Element).attributes.get("href"), "page.html");
+ });
+
+ test("returns element if no changes", () => {
+ const el = Content with emphasis
;
+ const resEl = resolveRefs(el, "/");
+ assert.equal(el, resEl);
+ });
+
+ test("returns new element if changed", () => {
+ const el = ;
+ const resEl = resolveRefs(el, "/");
+ assert.notEqual(el, resEl);
+ });
+
+ test("does not modify input", () => {
+ const elFn = () => ;
+ const el = elFn();
+ resolveRefs(el, "/");
+ assert.deepEqual(el, elFn());
+ });
+});
diff --git a/utils/resolve-refs.ts b/utils/resolve-refs.ts
new file mode 100644
index 0000000..d3ea932
--- /dev/null
+++ b/utils/resolve-refs.ts
@@ -0,0 +1,52 @@
+import * as posix from "node:path/posix";
+import { Node, Element } from "antihtml";
+
+function shallowCopyElement(element: Element) {
+ const copy = new Element(element.name);
+ copy.attributes = new Map(element.attributes);
+ copy.childNodes = element.childNodes;
+ return copy;
+}
+
+/**
+ Resolves absolute href attributes in a and link elements the Node tree into relative references from the given directory.
+
+ @param node Node tree to transform
+ @param dir Absolute path to directory to resolve references from.
+ @returns new node tree with href attributes transformed, or the original node if no transformations took place.
+*/
+export function resolveRefs(node: Node, dir: string) {
+ if (!(node instanceof Element)) {
+ return node;
+ }
+ let resolvedNode = node;
+ const name = node.name;
+ if (
+ (name === "link" || name === "a")
+ && node.attributes.has("href")
+ ) {
+ const original = node.attributes.get("href")!
+ /* node:coverage ignore next 3 */
+ if (!original.startsWith("/")) {
+ console.log(`Warning: found relative href to ${original}`);
+ } else {
+ const ref = posix.relative(dir, original);
+ resolvedNode = shallowCopyElement(node);
+ resolvedNode.attributes.set("href", ref);
+ }
+ }
+ const resolvedChildren: Node[] = [];
+ let modifiedChildren = false;
+ for (const child of resolvedNode.childNodes) {
+ const resolvedChild = resolveRefs(child, dir);
+ if (child !== resolvedChild) {
+ modifiedChildren = true;
+ }
+ resolvedChildren.push(resolvedChild);
+ }
+ if (modifiedChildren) {
+ resolvedNode = shallowCopyElement(node);
+ resolvedNode.childNodes = resolvedChildren;
+ }
+ return resolvedNode;
+}