Skip to content

Commit 8cd4074

Browse files
RYGRITghostdevv
andauthored
feat: display yaml frontmatter as a table (#2166)
Co-authored-by: Willow (GHOST) <git@willow.sh>
1 parent 291c22d commit 8cd4074

File tree

1 file changed

+37
-4
lines changed

1 file changed

+37
-4
lines changed

server/utils/readme.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
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'
25
import sanitizeHtml from 'sanitize-html'
36
import { hasProtocol } from 'ufo'
4-
import type { ReadmeResponse, TocItem } from '#shared/types/readme'
57
import { convertBlobOrFileToRawUrl, type RepositoryInfo } from '#shared/utils/git-providers'
68
import { decodeHtmlEntities, stripHtmlTags } from '#shared/utils/html'
79
import { convertToEmoji } from '#shared/utils/emoji'
810
import { toProxiedImageUrl } from '#server/utils/image-proxy'
911

1012
import { highlightCodeSync } from './shiki'
13+
import { escapeHtml } from './docs/text'
1114

1215
/**
1316
* Playground provider configuration
@@ -438,13 +441,43 @@ function calculateSemanticDepth(depth: number, lastSemanticLevel: number) {
438441
return Math.min(depth + 2, maxAllowed)
439442
}
440443

444+
/**
445+
* Render YAML frontmatter as a GitHub-style key-value table.
446+
*/
447+
function renderFrontmatterTable(data: Record<string, unknown>): string {
448+
const entries = Object.entries(data)
449+
if (entries.length === 0) return ''
450+
451+
const rows = entries
452+
.map(([key, value]) => {
453+
const displayValue =
454+
typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '')
455+
return `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(displayValue)}</td></tr>`
456+
})
457+
.join('\n')
458+
return `<table><thead><tr><th>Key</th><th>Value</th></tr></thead><tbody>\n${rows}\n</tbody></table>\n`
459+
}
460+
441461
export async function renderReadmeHtml(
442462
content: string,
443463
packageName: string,
444464
repoInfo?: RepositoryInfo,
445465
): Promise<ReadmeResponse> {
446466
if (!content) return { html: '', playgroundLinks: [], toc: [] }
447467

468+
// Parse and strip YAML frontmatter, render as table if present
469+
let markdownBody = content
470+
let frontmatterHtml = ''
471+
try {
472+
const { data, content: body } = matter(content)
473+
if (data && Object.keys(data).length > 0) {
474+
frontmatterHtml = renderFrontmatterTable(data)
475+
markdownBody = body
476+
}
477+
} catch {
478+
// If frontmatter parsing fails, render the full content as-is
479+
}
480+
448481
const shiki = await getShikiHighlighter()
449482
const renderer = new marked.Renderer()
450483

@@ -615,8 +648,8 @@ ${html}
615648
// Strip trailing whitespace (tabs/spaces) from code block closing fences.
616649
// While marky-markdown handles these gracefully, marked fails to recognize
617650
// the end of a code block if the closing fences are followed by unexpected whitespaces.
618-
const normalizedContent = content.replace(/^( {0,3}(?:`{3,}|~{3,}))\s*$/gm, '$1')
619-
const rawHtml = marked.parse(normalizedContent) as string
651+
const normalizedContent = markdownBody.replace(/^( {0,3}(?:`{3,}|~{3,}))\s*$/gm, '$1')
652+
const rawHtml = frontmatterHtml + (marked.parse(normalizedContent) as string)
620653

621654
const sanitized = sanitizeHtml(rawHtml, {
622655
allowedTags: ALLOWED_TAGS,

0 commit comments

Comments
 (0)