( content: string, packageName: string, repoInfo?: RepositoryInfo, )
| 473 | } |
| 474 | |
| 475 | export async function renderReadmeHtml( |
| 476 | content: string, |
| 477 | packageName: string, |
| 478 | repoInfo?: RepositoryInfo, |
| 479 | ): Promise<ReadmeResponse> { |
| 480 | if (!content) return { html: '', playgroundLinks: [], toc: [] } |
| 481 | |
| 482 | // Parse and strip YAML frontmatter, render as table if present |
| 483 | let markdownBody = content |
| 484 | let frontmatterHtml = '' |
| 485 | try { |
| 486 | const { data, content: body } = matter(content) |
| 487 | if (data && Object.keys(data).length > 0) { |
| 488 | frontmatterHtml = renderFrontmatterTable(data) |
| 489 | markdownBody = body |
| 490 | } |
| 491 | } catch { |
| 492 | // If frontmatter parsing fails, render the full content as-is |
| 493 | } |
| 494 | |
| 495 | const shiki = await getShikiHighlighter() |
| 496 | const renderer = new marked.Renderer() |
| 497 | |
| 498 | // Collect playground links during parsing |
| 499 | const collectedLinks: PlaygroundLink[] = [] |
| 500 | const seenUrls = new Set<string>() |
| 501 | |
| 502 | // Collect table of contents items during parsing |
| 503 | const toc: TocItem[] = [] |
| 504 | |
| 505 | // Track used heading slugs to handle duplicates (GitHub-style: foo, foo-1, foo-2) |
| 506 | const usedSlugs = new Map<string, number>() |
| 507 | |
| 508 | // Track heading hierarchy to ensure sequential order for accessibility |
| 509 | // Page h1 = package name, h2 = "Readme" section heading |
| 510 | // So README starts at h3, and we ensure no levels are skipped |
| 511 | // Visual styling preserved via data-level attribute (original depth) |
| 512 | let lastSemanticLevel = 2 // Start after h2 (the "Readme" section heading) |
| 513 | |
| 514 | // Shared heading processing for both markdown and HTML headings |
| 515 | function processHeading( |
| 516 | depth: number, |
| 517 | displayHtml: string, |
| 518 | plainText: string, |
| 519 | slugSource: string, |
| 520 | preservedAttrs = '', |
| 521 | ) { |
| 522 | const semanticLevel = calculateSemanticDepth(depth, lastSemanticLevel) |
| 523 | lastSemanticLevel = semanticLevel |
| 524 | |
| 525 | let slug = slugify(slugSource) |
| 526 | if (!slug) slug = 'heading' |
| 527 | |
| 528 | const count = usedSlugs.get(slug) ?? 0 |
| 529 | usedSlugs.set(slug, count + 1) |
| 530 | const uniqueSlug = count === 0 ? slug : `${slug}-${count}` |
| 531 | const id = toUserContentId(uniqueSlug) |
| 532 |
no test coverage detected