|
1 | | -import { marked, type Tokens } from 'marked' |
| 1 | +import type { ReadmeResponse, TocItem } from '#shared/types/readme' |
| 2 | +import type { Tokens } from 'marked' |
| 3 | +import matter from 'gray-matter' |
| 4 | +import { marked } from 'marked' |
2 | 5 | import sanitizeHtml from 'sanitize-html' |
3 | 6 | import { hasProtocol } from 'ufo' |
4 | | -import type { ReadmeResponse, TocItem } from '#shared/types/readme' |
5 | 7 | import { convertBlobOrFileToRawUrl, type RepositoryInfo } from '#shared/utils/git-providers' |
6 | 8 | import { decodeHtmlEntities, stripHtmlTags } from '#shared/utils/html' |
7 | 9 | import { convertToEmoji } from '#shared/utils/emoji' |
8 | 10 | import { toProxiedImageUrl } from '#server/utils/image-proxy' |
9 | 11 |
|
10 | 12 | import { highlightCodeSync } from './shiki' |
| 13 | +import { escapeHtml } from './docs/text' |
11 | 14 |
|
12 | 15 | /** |
13 | 16 | * Playground provider configuration |
@@ -403,13 +406,43 @@ function calculateSemanticDepth(depth: number, lastSemanticLevel: number) { |
403 | 406 | return Math.min(depth + 2, maxAllowed) |
404 | 407 | } |
405 | 408 |
|
| 409 | +/** |
| 410 | + * Render YAML frontmatter as a GitHub-style key-value table. |
| 411 | + */ |
| 412 | +function renderFrontmatterTable(data: Record<string, unknown>): string { |
| 413 | + const entries = Object.entries(data) |
| 414 | + if (entries.length === 0) return '' |
| 415 | + |
| 416 | + const rows = entries |
| 417 | + .map(([key, value]) => { |
| 418 | + const displayValue = |
| 419 | + typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '') |
| 420 | + return `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(displayValue)}</td></tr>` |
| 421 | + }) |
| 422 | + .join('\n') |
| 423 | + return `<table><thead><tr><th>Key</th><th>Value</th></tr></thead><tbody>\n${rows}\n</tbody></table>\n` |
| 424 | +} |
| 425 | + |
406 | 426 | export async function renderReadmeHtml( |
407 | 427 | content: string, |
408 | 428 | packageName: string, |
409 | 429 | repoInfo?: RepositoryInfo, |
410 | 430 | ): Promise<ReadmeResponse> { |
411 | 431 | if (!content) return { html: '', playgroundLinks: [], toc: [] } |
412 | 432 |
|
| 433 | + // Parse and strip YAML frontmatter, render as table if present |
| 434 | + let markdownBody = content |
| 435 | + let frontmatterHtml = '' |
| 436 | + try { |
| 437 | + const { data, content: body } = matter(content) |
| 438 | + if (data && Object.keys(data).length > 0) { |
| 439 | + frontmatterHtml = renderFrontmatterTable(data) |
| 440 | + markdownBody = body |
| 441 | + } |
| 442 | + } catch { |
| 443 | + // If frontmatter parsing fails, render the full content as-is |
| 444 | + } |
| 445 | + |
413 | 446 | const shiki = await getShikiHighlighter() |
414 | 447 | const renderer = new marked.Renderer() |
415 | 448 |
|
@@ -514,7 +547,7 @@ ${html} |
514 | 547 |
|
515 | 548 | marked.setOptions({ renderer }) |
516 | 549 |
|
517 | | - const rawHtml = marked.parse(content) as string |
| 550 | + const rawHtml = frontmatterHtml + (marked.parse(markdownBody) as string) |
518 | 551 |
|
519 | 552 | const sanitized = sanitizeHtml(rawHtml, { |
520 | 553 | allowedTags: ALLOWED_TAGS, |
|
0 commit comments