* Resolve a relative URL to an absolute URL. * If repository info is available, resolve to provider's raw file URLs. * For markdown files (.md), use blob URLs so they render properly. * Otherwise, fall back to jsdelivr CDN (except for .md files which are left unchanged).
(url: string, packageName: string, repoInfo?: RepositoryInfo)
| 322 | * Otherwise, fall back to jsdelivr CDN (except for .md files which are left unchanged). |
| 323 | */ |
| 324 | function resolveUrl(url: string, packageName: string, repoInfo?: RepositoryInfo): string { |
| 325 | if (!url) return url |
| 326 | if (url.startsWith('#')) { |
| 327 | // Prefix anchor links to match heading IDs (avoids collision with page IDs) |
| 328 | // Normalize markdown-style heading fragments to the same slug format used |
| 329 | // for generated README heading IDs, but leave already-prefixed values as-is. |
| 330 | const fragment = url.slice(1) |
| 331 | if (!fragment) { |
| 332 | return '#' |
| 333 | } |
| 334 | if (fragment.startsWith(USER_CONTENT_PREFIX)) { |
| 335 | return `#${fragment}` |
| 336 | } |
| 337 | |
| 338 | const normalizedFragment = slugify(decodeHashFragment(fragment)) |
| 339 | return toUserContentHash(normalizedFragment || fragment) |
| 340 | } |
| 341 | // Absolute paths (e.g. /package/foo from a previous npmjs redirect) are already resolved |
| 342 | if (url.startsWith('/')) return url |
| 343 | if (hasProtocol(url, { acceptRelative: true })) { |
| 344 | try { |
| 345 | const parsed = new URL(url, 'https://example.com') |
| 346 | if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { |
| 347 | // Redirect npmjs urls to ourself |
| 348 | if (isNpmJsUrlThatCanBeRedirected(parsed)) { |
| 349 | return parsed.pathname + parsed.search + parsed.hash |
| 350 | } |
| 351 | return url |
| 352 | } |
| 353 | } catch { |
| 354 | // Invalid URL, fall through to resolve as relative |
| 355 | } |
| 356 | // return protocol-relative URLs (//example.com) as-is |
| 357 | if (url.startsWith('//')) { |
| 358 | return url |
| 359 | } |
| 360 | // for non-HTTP protocols (javascript:, data:, etc.), don't return, treat as relative |
| 361 | } |
| 362 | |
| 363 | // Check if this is a markdown file link |
| 364 | const isMarkdownFile = /\.md$/i.test(url.split('?')[0]?.split('#')[0] ?? '') |
| 365 | |
| 366 | // Use provider's URL base when repository info is available |
| 367 | // This handles assets that exist in the repo but not in the npm tarball |
| 368 | if (repoInfo?.rawBaseUrl) { |
| 369 | // Normalize the relative path (remove leading ./) |
| 370 | let relativePath = url.replace(/^\.\//, '') |
| 371 | |
| 372 | // If package is in a subdirectory, resolve relative paths from there |
| 373 | // e.g., for packages/ai with ./assets/hero.gif → packages/ai/assets/hero.gif |
| 374 | // but for ../../.github/assets/banner.jpg → resolve relative to subdirectory |
| 375 | if (repoInfo.directory) { |
| 376 | // Split directory into parts for relative path resolution |
| 377 | const dirParts = repoInfo.directory.split('/').filter(Boolean) |
| 378 | |
| 379 | // Handle ../ navigation |
| 380 | while (relativePath.startsWith('../')) { |
| 381 | relativePath = relativePath.slice(3) |
no test coverage detected