diff options
| author | Ryan Vogel <[email protected]> | 2026-01-25 20:56:37 -0500 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2026-01-25 21:26:43 -0500 |
| commit | ab3268896d2387a1dba36df2881a10a2ebf8dce6 (patch) | |
| tree | 9ecd839882fdb8aaebcd9cbff48e3b44c33aab5c /packages/console | |
| parent | 3d23d2df716bf44c8a26b445ee188cbee94c023b (diff) | |
| download | opencode-ab3268896d2387a1dba36df2881a10a2ebf8dce6.tar.gz opencode-ab3268896d2387a1dba36df2881a10a2ebf8dce6.zip | |
Add highlight tag parsing for changelog with video support
Diffstat (limited to 'packages/console')
| -rw-r--r-- | packages/console/app/src/routes/changelog.json.ts | 110 | ||||
| -rw-r--r-- | packages/console/app/src/routes/changelog/index.css | 35 | ||||
| -rw-r--r-- | packages/console/app/src/routes/changelog/index.tsx | 83 |
3 files changed, 227 insertions, 1 deletions
diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts new file mode 100644 index 000000000..b668229f8 --- /dev/null +++ b/packages/console/app/src/routes/changelog.json.ts @@ -0,0 +1,110 @@ +type Release = { + tag_name: string + name: string + body: string + published_at: string + html_url: string +} + +type Highlight = { + source: string + title: string + description: string + shortDescription?: string + image?: { + src: string + width: string + height: string + } + video?: string +} + +function parseHighlights(body: string): Highlight[] { + const highlights: Highlight[] = [] + const regex = /<highlight\s+source="([^"]+)">([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/<h2>([^<]+)<\/h2>/) + const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/) + const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="([^"]*)"\s+src="([^"]+)"/) + // Match standalone GitHub asset URLs (videos) + const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m) + + if (titleMatch) { + highlights.push({ + source, + title: titleMatch[1], + description: pMatch?.[2] || "", + shortDescription: pMatch?.[1], + image: imgMatch + ? { + width: imgMatch[1], + height: imgMatch[2], + src: imgMatch[4], + } + : undefined, + video: videoMatch?.[1], + }) + } + } + + return highlights +} + +function parseMarkdown(body: string) { + const lines = body.split("\n") + const sections: { title: string; items: string[] }[] = [] + let current: { title: string; items: string[] } | null = null + let skip = false + + for (const line of lines) { + if (line.startsWith("## ")) { + if (current) sections.push(current) + const title = line.slice(3).trim() + current = { title, items: [] } + skip = false + } else if (line.startsWith("**Thank you")) { + skip = true + } else if (line.startsWith("- ") && !skip) { + current?.items.push(line.slice(2).trim()) + } + } + if (current) sections.push(current) + + const highlights = parseHighlights(body) + + return { sections, highlights } +} + +export async function GET() { + const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenCode-Console", + }, + }) + + if (!response.ok) { + return { releases: [] } + } + + const releases = (await response.json()) as Release[] + + return { + releases: releases.map((release) => { + const parsed = parseMarkdown(release.body || "") + return { + tag: release.tag_name, + name: release.name, + date: release.published_at, + url: release.html_url, + highlights: parsed.highlights, + sections: parsed.sections, + } + }), + } +} diff --git a/packages/console/app/src/routes/changelog/index.css b/packages/console/app/src/routes/changelog/index.css index a445c7447..a06fb0055 100644 --- a/packages/console/app/src/routes/changelog/index.css +++ b/packages/console/app/src/routes/changelog/index.css @@ -465,6 +465,41 @@ } } } + + [data-component="highlights"] { + display: flex; + flex-direction: column; + gap: 2rem; + margin-bottom: 1.5rem; + } + + [data-component="highlight"] { + h4 { + font-size: 14px; + font-weight: 600; + color: var(--color-text-strong); + margin-bottom: 8px; + } + + p[data-slot="title"] { + font-weight: 500; + color: var(--color-text-strong); + margin-bottom: 4px; + } + + p { + color: var(--color-text); + line-height: 1.5; + margin-bottom: 12px; + } + + img, + video { + max-width: 100%; + height: auto; + border-radius: 4px; + } + } } a { diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index c1b931fe3..34fd5f83b 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -40,6 +40,59 @@ function formatDate(dateString: string) { }) } +type Highlight = { + source: string + title: string + description: string + shortDescription?: string + image?: { + src: string + width: string + height: string + } + video?: string +} + +function parseHighlights(body: string): Highlight[] { + const highlights: Highlight[] = [] + const regex = /<highlight\s+source="([^"]+)">([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/<h2>([^<]+)<\/h2>/) + const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/) + const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="([^"]*)"\s+src="([^"]+)"/) + // Match standalone GitHub asset URLs (videos) + const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m) + + if (titleMatch) { + highlights.push({ + source, + title: titleMatch[1], + description: pMatch?.[2] || "", + shortDescription: pMatch?.[1], + image: imgMatch + ? { + width: imgMatch[1], + height: imgMatch[2], + src: imgMatch[4], + } + : undefined, + video: videoMatch?.[1], + }) + } + } + + return highlights +} + +function toTitleCase(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() +} + function parseMarkdown(body: string) { const lines = body.split("\n") const sections: { title: string; items: string[] }[] = [] @@ -60,7 +113,9 @@ function parseMarkdown(body: string) { } if (current) sections.push(current) - return { sections } + const highlights = parseHighlights(body) + + return { sections, highlights } } function ReleaseItem(props: { item: string }) { @@ -87,6 +142,27 @@ function ReleaseItem(props: { item: string }) { ) } +function HighlightCard(props: { highlight: Highlight }) { + return ( + <div data-component="highlight"> + <h4>{props.highlight.source}</h4> + <p data-slot="title">{props.highlight.title}</p> + <p>{props.highlight.description}</p> + <Show when={props.highlight.video}> + <video src={props.highlight.video} controls autoplay loop muted playsinline /> + </Show> + <Show when={props.highlight.image && !props.highlight.video}> + <img + src={props.highlight.image!.src} + alt={props.highlight.title} + width={props.highlight.image!.width} + height={props.highlight.image!.height} + /> + </Show> + </div> + ) +} + export default function Changelog() { const releases = createAsync(() => getReleases()) @@ -120,6 +196,11 @@ export default function Changelog() { <time dateTime={release.published_at}>{formatDate(release.published_at)}</time> </header> <div data-slot="content"> + <Show when={parsed().highlights.length > 0}> + <div data-component="highlights"> + <For each={parsed().highlights}>{(highlight) => <HighlightCard highlight={highlight} />}</For> + </div> + </Show> <For each={parsed().sections}> {(section) => ( <div data-component="section"> |
