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 | 169242x 2x 10634x 10634x 3413x 7221x 7221x 7221x 10634x 10634x 10634x 10634x 7221x 7221x 7221x 60525x 59811x 10291x 10291x 49520x 7221x 40811x 40811x 40811x 40811x 5698868x 5698868x 5698868x 59484x 59484x 29742x 29742x 29742x 29742x 29742x 29742x 29742x 5639384x 5243879x 40811x 348670203x 348670203x 348670203x 348670203x 23730x 348670203x 348670203x 10624x 10624x 10624x 10624x 10624x 10624x 10624x 10624x 348670203x 5243879x 5243879x 5243879x 348670203x 348670203x 10624x 10624x 10624x 348659579x 348659579x 5243879x 10624x 446003x 10624x 1x 1x 1x 1x 1x 23730x 23730x 563916x 563916x 563916x 540004x 23884x 23884x 28x 10624x 10624x 467261x 467261x 467261x 456604x 10657x 10657x | // 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);
if (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--;
if (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;
}
|