From 896ead5f07838760687a40715b40970d124f0a66 Mon Sep 17 00:00:00 2001 From: yuxuanj Date: Mon, 26 Jan 2026 15:35:20 -0800 Subject: [PATCH 1/2] link rewrite test --- test/rewrite-links.test.ts | 301 +++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 test/rewrite-links.test.ts diff --git a/test/rewrite-links.test.ts b/test/rewrite-links.test.ts new file mode 100644 index 0000000..fee8504 --- /dev/null +++ b/test/rewrite-links.test.ts @@ -0,0 +1,301 @@ +/** + * Test cases based on: + * https://developer-stage.adobe.com/github-actions-test/test/test-url + */ + +import { Helix } from '@adobe/helix-universal'; +import { resolve } from '../src/steps/rewrite-links.js'; +import rewriteLinks from '../src/steps/rewrite-links.js'; +import { DEFAULT_CONTEXT } from './util.js'; + +describe('rewrite-links', () => { + let ctx: Helix.UniversalContext; + + beforeEach(() => { + // Context simulates being at /github-actions-test/test/test-url.md + ctx = DEFAULT_CONTEXT({ + attributes: { + content: { + root: '/src/pages/test', + path: '/src/pages/test/test-url.md', + pathprefix: '/github-actions-test/test', + owner: 'AdobeDocs', + repo: 'github-actions-test', + branch: 'main', + localMode: false, + origin: 'https://raw.githubusercontent.com', + }, + }, + }); + }); + + describe('resolve() function', () => { + describe('External links - should not be rewritten', () => { + it('should not rewrite http:// links', () => { + const result = resolve(ctx, 'http://example.com', 'a'); + expect(result).to.equal('http://example.com'); + }); + + it('should not rewrite https:// links', () => { + const result = resolve(ctx, 'https://example.com/path', 'a'); + expect(result).to.equal('https://example.com/path'); + }); + + it('should not rewrite mailto: links', () => { + const result = resolve(ctx, 'mailto:test@example.com', 'a'); + expect(result).to.equal('mailto:test@example.com'); + }); + }); + + it('Testing path relative link relative to current directory', () => { + const result = resolve(ctx, 'path-test/index.md', 'a'); + expect(result).to.equal('/github-actions-test/test/test/path-test/index.md'); + }); + + it('Root relative link', () => { + const result = resolve(ctx, '/api/index.md', 'a'); + expect(result).to.equal('/github-actions-test/test/api/index.md'); + }); + + it('path relative link that goes to parent of current directory', () => { + const result = resolve(ctx, '../support/index.md', 'a'); + expect(result).to.equal('/github-actions-test/test/support/index.md'); + }); + + it('path explicit relative link to current directory', () => { + const result = resolve(ctx, './path-test/index.md', 'a'); + expect(result).to.equal('/github-actions-test/test/test/path-test/index.md'); + }); + + it('path relative link to a file without a trailing slash', () => { + const result = resolve(ctx, 'path-test/pathname', 'a'); + expect(result).to.equal('/path-test/pathname'); + }); + + it('path relative link to a file with a trailing slash', () => { + const result = resolve(ctx, 'path-test/pathname/', 'a'); + expect(result).to.equal('/path-test/pathname'); + }); + + it('path relative link to a file with a trailing slash but no index.md should fail', () => { + const result = resolve(ctx, 'path-test/pathname/', 'a'); + // Without index.md, this should not convert to index.md + expect(result).to.equal('/path-test/pathname'); + }); + + it('path relative link to a file without a trailing slash and no .md should fail', () => { + const result = resolve(ctx, 'path-test/pathname', 'a'); + // Without .md extension, this should not be treated as markdown + expect(result).to.equal('/path-test/pathname'); + }); + + it('path relative link to a file with a trailing slash goes to index.md', () => { + const result = resolve(ctx, 'path-test/', 'a'); + expect(result).to.equal('/path-test'); + }); + + it('path relative link to a file without a trailing slash goes to index.md', () => { + const result = resolve(ctx, 'path-test', 'a'); + expect(result).to.equal('/path-test'); + }); + + it('testing anchor link on the current page', () => { + const result = resolve(ctx, '#path-relative-link-that-goes-to-parent-of-current-directory', 'a'); + expect(result).to.equal('#path-relative-link-that-goes-to-parent-of-current-directory'); + }); + + it('testing anchor link on another page', () => { + const result = resolve(ctx, 'anchor-link-in-table#request-object', 'a'); + expect(result).to.equal('/anchor-link-in-table#request-object'); + }); + + it('testing anchor link on another page with md in the link', () => { + const result = resolve(ctx, 'anchor-link-in-table.md#request-object', 'a'); + expect(result).to.equal('/github-actions-test/test/test/anchor-link-in-table.md#request-object'); + }); + + it('external link with no http and https should fail', () => { + const result = resolve(ctx, 'www.google.com', 'a'); + // www.google.com is not recognized as external, so it gets rewritten as a relative path + expect(result).to.equal('/www.google.com'); + }); + + describe('Links that already have pathprefix', () => { + it('should not rewrite links that already have pathprefix', () => { + const result = resolve(ctx, '/github-actions-test/test/some-page', 'a'); + expect(result).to.equal('/github-actions-test/test/some-page'); + }); + }); + + describe('Image handling', () => { + it('should generate GitHub raw URL for images (remote mode)', () => { + const result = resolve(ctx, './images/screenshot.png', 'img'); + expect(result).to.include('raw.githubusercontent.com'); + expect(result).to.include('AdobeDocs'); + expect(result).to.include('github-actions-test'); + }); + + it('should generate local URL for images (local mode)', () => { + const localCtx = DEFAULT_CONTEXT({ + attributes: { + content: { + root: '/src/pages/test', + path: '/src/pages/test/test-page.md', + pathprefix: '/github-actions-test/test', + owner: 'AdobeDocs', + repo: 'github-actions-test', + branch: 'main', + localMode: true, + origin: 'http://127.0.0.1:3003', + }, + }, + }); + + const result = resolve(localCtx, './images/screenshot.png', 'img'); + expect(result).to.include('127.0.0.1:3003'); + }); + }); + }); + + describe('rewriteLinks() function - HAST transformation', () => { + it('should rewrite href in anchor elements and remove .md extension', () => { + ctx.attributes.content.hast = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'a', + properties: { href: './other-page.md' }, + children: [{ type: 'text', value: 'Link' }], + }, + ], + }; + + rewriteLinks(ctx); + + const anchor = ctx.attributes.content.hast.children[0]; + expect(anchor.properties.href).to.not.include('.md'); + expect(anchor.properties.href).to.include('/github-actions-test/test'); + }); + + it('should rewrite src in img elements to GitHub raw URL', () => { + ctx.attributes.content.hast = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'img', + properties: { src: './images/photo.png' }, + children: [], + }, + ], + }; + + rewriteLinks(ctx); + + const img = ctx.attributes.content.hast.children[0]; + expect(img.properties.src).to.include('raw.githubusercontent.com'); + }); + + it('should handle index.md links by removing index.md', () => { + ctx.attributes.content.hast = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'a', + properties: { href: './folder/index.md' }, + children: [{ type: 'text', value: 'Folder' }], + }, + ], + }; + + rewriteLinks(ctx); + + const anchor = ctx.attributes.content.hast.children[0]; + expect(anchor.properties.href).to.not.include('index.md'); + }); + + it('should handle .md# links by removing .md but keeping anchor', () => { + ctx.attributes.content.hast = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'a', + properties: { href: './page.md#section' }, + children: [{ type: 'text', value: 'Section Link' }], + }, + ], + }; + + rewriteLinks(ctx); + + const anchor = ctx.attributes.content.hast.children[0]; + expect(anchor.properties.href).to.include('#section'); + expect(anchor.properties.href).to.not.include('.md'); + }); + + it('should not modify external links', () => { + ctx.attributes.content.hast = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'a', + properties: { href: 'https://adobe.com' }, + children: [{ type: 'text', value: 'Adobe' }], + }, + ], + }; + + rewriteLinks(ctx); + + const anchor = ctx.attributes.content.hast.children[0]; + expect(anchor.properties.href).to.equal('https://adobe.com'); + }); + + it('should handle multiple links in document', () => { + ctx.attributes.content.hast = { + type: 'root', + children: [ + { + type: 'element', + tagName: 'div', + properties: {}, + children: [ + { + type: 'element', + tagName: 'a', + properties: { href: './page1.md' }, + children: [{ type: 'text', value: 'Page 1' }], + }, + { + type: 'element', + tagName: 'a', + properties: { href: 'https://external.com' }, + children: [{ type: 'text', value: 'External' }], + }, + { + type: 'element', + tagName: 'img', + properties: { src: './image.png' }, + children: [], + }, + ], + }, + ], + }; + + rewriteLinks(ctx); + + const div = ctx.attributes.content.hast.children[0]; + const [link1, link2, img] = div.children; + + expect(link1.properties.href).to.include('/github-actions-test/test'); + expect(link1.properties.href).to.not.include('.md'); + expect(link2.properties.href).to.equal('https://external.com'); + expect(img.properties.src).to.include('raw.githubusercontent.com'); + }); + }); +}); From d32950d77687e91e028b9ed26a6349b85030948c Mon Sep 17 00:00:00 2001 From: yuxuanj Date: Fri, 6 Feb 2026 23:13:52 -0800 Subject: [PATCH 2/2] fixed no url update on anchars in local dev --- src/steps/to-hast.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/steps/to-hast.ts b/src/steps/to-hast.ts index c7b006f..c78d9a1 100644 --- a/src/steps/to-hast.ts +++ b/src/steps/to-hast.ts @@ -19,12 +19,56 @@ import { Helix } from '@adobe/helix-universal'; import { toHast as mdast2hast, defaultHandlers, State } from 'mdast-util-to-hast'; import { raw } from 'hast-util-raw'; import { mdast2hastGridTablesHandler, TYPE_TABLE } from '@adobe/mdast-util-gridtables'; +import { toString } from 'hast-util-to-string'; + +/** + * Converts text to a URL-friendly slug for use as heading IDs. + * @param text - The heading text to convert + * @returns A lowercase, hyphenated slug + */ +function textToSlug(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') // Remove special characters + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen + .replace(/^-+/, '') // Remove leading hyphens + .replace(/-+$/, ''); // Remove trailing hyphens +} + +/** + * Creates a custom heading handler that generates IDs for anchor links. + * @param depth - The heading level (1-6) + * @returns A handler function for the heading + */ +function createHeadingHandler(depth: number) { + return (state: State, node: any) => { + const children = state.all(node); + const result = { + type: 'element' as const, + tagName: `h${depth}`, + properties: {} as Record, + children, + }; + + // Generate ID from heading text content + const textContent = toString(result); + if (textContent) { + result.properties.id = textToSlug(textContent); + } + + return result; + }; +} export default function toHast(ctx: Helix.UniversalContext) { const { content } = ctx.attributes; content.hast = mdast2hast(content.mdast, { handlers: { ...defaultHandlers, + heading: (state: State, node: any) => { + return createHeadingHandler(node.depth)(state, node); + }, section: (state: State, node: any) => { const n = { ...node }; const children = state.all(n);