From 17f8693eaed6b3cbfd1e401977a33e58f967713c Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 22 Jan 2025 08:58:48 +0100 Subject: [PATCH] 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. --- package.json | 2 +- tsconfig.json | 2 +- utils/resolve-refs.test.tsx | 51 ++++++++++++++++++++++++++++++++++++ utils/resolve-refs.ts | 52 +++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 utils/resolve-refs.test.tsx create mode 100644 utils/resolve-refs.ts 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(
Content with Link
, "/"); + 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 =
Content with Link
; + const resEl = resolveRefs(el, "/"); + assert.notEqual(el, resEl); + }); + + test("does not modify input", () => { + const elFn = () =>
Content with Link
; + 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; +}