Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | 2235x 2x 140x 140x 7x 133x 133x 133x 140x 140x 140x 140x 133x 133x 133x 1212x 1206x 188x 188x 1018x 133x 256x 256x 256x 256x 7145x 7145x 7145x 276x 276x 138x 138x 138x 138x 138x 138x 138x 6869x 5534x 256x 166440x 166440x 166440x 166440x 130x 166440x 166440x 130x 130x 130x 130x 130x 130x 130x 130x 166440x 5534x 5534x 5534x 166440x 166440x 130x 130x 130x 166310x 166310x 5534x 130x 4897x 130x 1x 1x 1x 1x 1x 130x 130x 2063x 2063x 2063x 1933x 130x 130x 130x 130x 5167x 5167x 5167x 5036x 131x 131x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module Aggregator/Clean/RewriteLinks
* @description Rewrite repo-relative Markdown links/images to absolute
* GitHub URLs so the published HTML is portable.
*/
import { blobUrl as _blobUrl, rawUrl as _rawUrl } from '../infra/github-urls.js';
/**
* Build a GitHub blob URL for a repo-relative path.
*
* @param relPath - Repo-relative file path
* @returns Absolute `https://github.com/.../blob/main/...` URL
*/
export function githubBlobUrl(relPath: string): string {
return _blobUrl(relPath);
}
/**
* Build a `raw.githubusercontent.com` URL for a repo-relative path.
*
* @param relPath - Repo-relative file path
* @returns Absolute raw-content URL
*/
export function githubRawUrl(relPath: string): string {
return _rawUrl(relPath);
}
/**
* Resolve a relative link target against the artifact's directory and
* return an absolute GitHub URL.
*
* @param target - Original link target (e.g. `../templates/foo.md`)
* @param artifactRelPath - Repo-relative path of the artifact
* @param raw - When true, produce a raw.githubusercontent URL (for images)
* @returns Absolute URL (or the original target for anchors/absolute links)
*/
export function resolveLink(target: string, artifactRelPath: string, raw: boolean): string {
const lower = target.toLowerCase();
if (
/^[a-z][a-z0-9+.-]*:\/\//i.test(target) ||
target.startsWith('//') ||
target.startsWith('#') ||
lower.startsWith('mailto:') ||
lower.startsWith('tel:') ||
lower.startsWith('data:')
) {
return target;
}
const artifactDir = artifactRelPath.split('/').slice(0, -1).join('/');
const suffixMatch = /[#?].*$/.exec(target);
const suffix = suffixMatch ? suffixMatch[0] : '';
const bare = suffix ? target.slice(0, -suffix.length) : target;
const resolved = posixResolve(artifactDir, bare);
const url = raw ? githubRawUrl(resolved) : githubBlobUrl(resolved);
return `${url}${suffix}`;
}
/**
* POSIX path-resolve over `/`-separated strings. Mirrors `path.posix.resolve`
* on a virtual absolute root so we don't depend on `path` for pure string ops.
*
* @param baseDir - Directory portion of the base path (POSIX separators)
* @param rel - Relative path to resolve against `baseDir`
* @returns Resolved path with `.` / `..` segments collapsed
*/
function posixResolve(baseDir: string, rel: string): string {
const parts = `${baseDir}/${rel}`.split('/');
const stack: string[] = [];
for (const part of parts) {
if (part === '' || part === '.') continue;
if (part === '..') {
stack.pop();
continue;
}
stack.push(part);
}
return stack.join('/');
}
/**
* Rewrite `[text](relative.md)` and `` links to GitHub URLs.
*
* @param md - Markdown source (may contain fenced code blocks, left untouched)
* @param artifactRelPath - Repo-relative path of the artifact
* @returns Markdown with every non-fence-local link rewritten
*/
export function rewriteLinks(md: string, artifactRelPath: string): string {
const lines = md.split('\n');
let inFence = false;
let fenceMarker = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i] ?? '';
const fenceMatch = /^\s*(`{3,}|~{3,})/.exec(line);
if (fenceMatch?.[1]) {
const marker = fenceMatch[1];
if (!inFence) {
inFence = true;
fenceMarker = marker;
continue;
}
Eif (marker.charAt(0) === fenceMarker.charAt(0) && marker.length >= fenceMarker.length) {
inFence = false;
fenceMarker = '';
continue;
}
}
if (inFence) continue;
lines[i] = rewriteLinksInLine(line, artifactRelPath);
}
return lines.join('\n');
}
/**
* Attempt to parse a Markdown link starting at `line[index]`. Returns
* `null` when no valid `[text](target)` / `` link is present.
*
* @param line - Source line being scanned
* @param index - Zero-based index of the candidate `[` or `!`
* @param artifactRelPath - Repo-relative path of the source artifact
* @returns `{ replacement, nextIndex }` when a link was rewritten, else `null`
*/
function tryParseLinkAt(
line: string,
index: number,
artifactRelPath: string
): { replacement: string; nextIndex: number } | null {
const ch = line.charAt(index);
const isImage = ch === '!' && line.charAt(index + 1) === '[';
const isLink = ch === '[';
if (!isImage && !isLink) return null;
const start = isImage ? index + 1 : index;
const closeText = findMatchingBracket(line, start);
Iif (closeText === -1 || line.charAt(closeText + 1) !== '(') return null;
const openParen = closeText + 1;
const closeParen = findMatchingParen(line, openParen);
Iif (closeParen === -1) return null;
const label = line.slice(start, closeText + 1);
const rawTarget = line.slice(openParen + 1, closeParen).trim();
const { target, title } = splitTargetAndTitle(rawTarget);
const newTarget = resolveLink(target, artifactRelPath, isImage);
const replacement = (isImage ? '!' : '') + label + '(' + newTarget + title + ')';
return { replacement, nextIndex: closeParen + 1 };
}
/**
* Rewrite every `[text](target)` occurrence in a single non-fenced line.
* Uses a manual scanner instead of a global regex to avoid catastrophic
* backtracking on nested brackets.
*
* @param line - One line of Markdown, outside any fenced code block
* @param artifactRelPath - Repo-relative path of the source artifact
* @returns Line with every local `.md` target rewritten to a GitHub URL
*/
function rewriteLinksInLine(line: string, artifactRelPath: string): string {
let out = '';
let i = 0;
while (i < line.length) {
const parsed = tryParseLinkAt(line, i, artifactRelPath);
if (parsed) {
out += parsed.replacement;
i = parsed.nextIndex;
continue;
}
out += line.charAt(i);
i++;
}
return out;
}
/**
* Split a raw Markdown link target into its URL and optional `"title"`
* suffix. Uses a linear scanner instead of a regex to avoid catastrophic
* backtracking on pathological input.
*
* @param raw - Raw contents between the parentheses of a Markdown link
* @returns `{ target, title }` where `title` is `""` when absent, or the
* leading whitespace + `"..."` suffix when present
*/
function splitTargetAndTitle(raw: string): { target: string; title: string } {
let i = 0;
while (i < raw.length && !/\s/.test(raw.charAt(i))) i++;
if (i === raw.length) return { target: raw, title: '' };
const target = raw.slice(0, i);
const rest = raw.slice(i);
const trimmed = rest.trimStart();
Eif (
trimmed.length >= 2 &&
trimmed.charAt(0) === '"' &&
trimmed.charAt(trimmed.length - 1) === '"'
) {
return { target, title: rest };
}
return { target: raw, title: '' };
}
/**
* Index of the matching `]` for a `[` at position `start`, or `-1` if none.
*
* @param line - Line being scanned
* @param start - Zero-based index of the opening `[`
* @returns Zero-based index of the matching `]`, or `-1`
*/
function findMatchingBracket(line: string, start: number): number {
let depth = 0;
for (let i = start; i < line.length; i++) {
const ch = line.charAt(i);
Iif (ch === '\\') {
i++;
continue;
}
if (ch === '[') depth++;
else if (ch === ']') {
depth--;
Eif (depth === 0) return i;
}
}
return -1;
}
/**
* Index of the matching `)` for a `(` at position `start`, or `-1` if none.
*
* @param line - Line being scanned
* @param start - Zero-based index of the opening `(`
* @returns Zero-based index of the matching `)`, or `-1`
*/
function findMatchingParen(line: string, start: number): number {
let depth = 0;
for (let i = start; i < line.length; i++) {
const ch = line.charAt(i);
Iif (ch === '\\') {
i++;
continue;
}
if (ch === '(') depth++;
else if (ch === ')') {
depth--;
if (depth === 0) return i;
}
}
return -1;
}
|