Add highlight tag parsing for changelog with video support

This commit is contained in:
Ryan Vogel
2026-01-25 20:56:37 -05:00
committed by Dax Raad
parent 3d23d2df71
commit ab3268896d
3 changed files with 227 additions and 1 deletions

View File

@@ -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,
}
}),
}
}

View File

@@ -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 {

View File

@@ -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">