Add resolveRefs transform

Provide a transformation function that maps absolute references to
resources into relative references based on the location of a page.
This makes it possible to use the same links across multiple pages
in the hierarchy that works when loaded as files from the filesystem.
This commit is contained in:
Hornwitser 2025-01-22 08:58:48 +01:00
parent 7aa937a7e9
commit 17f8693eae
4 changed files with 105 additions and 2 deletions

View file

@ -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"

View file

@ -1,6 +1,6 @@
{
"files": ["cli.ts"],
"include": ["content"],
"include": ["content", "utils"],
"compilerOptions": {
"outDir": "build/node",
"rootDir": ".",

View file

@ -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(<a href="/page.html">Link</a>, "/");
assert.equal((el as Element).attributes.get("href"), "page.html");
});
test("root to subdir relative href", () => {
const el = resolveRefs(<a href="/dir/page.html">Link</a>, "/");
assert.equal((el as Element).attributes.get("href"), "dir/page.html");
});
test("subdir to root relative href", () => {
const el = resolveRefs(<a href="/page.html">Link</a>, "/subdir");
assert.equal((el as Element).attributes.get("href"), "../page.html");
});
test("subdir to subdir relative href", () => {
const el = resolveRefs(<a href="/alt/page.html">Link</a>, "/subdir");
assert.equal((el as Element).attributes.get("href"), "../alt/page.html");
});
test("nested element", () => {
const el = resolveRefs(<div>Content with <a href="/page.html">Link</a></div>, "/");
assert.equal((el.childNodes[1] as Element).attributes.get("href"), "page.html");
});
test("returns element if no changes", () => {
const el = <div>Content with <em>emphasis</em></div>;
const resEl = resolveRefs(el, "/");
assert.equal(el, resEl);
});
test("returns new element if changed", () => {
const el = <div>Content with <a href="/page.html">Link</a></div>;
const resEl = resolveRefs(el, "/");
assert.notEqual(el, resEl);
});
test("does not modify input", () => {
const elFn = () => <div>Content with <a href="/page.html">Link</a></div>;
const el = elFn();
resolveRefs(el, "/");
assert.deepEqual(el, elFn());
});
});

52
utils/resolve-refs.ts Normal file
View file

@ -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;
}