mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
Add highlight tag parsing for changelog with video support
This commit is contained in:
110
packages/console/app/src/routes/changelog.json.ts
Normal file
110
packages/console/app/src/routes/changelog.json.ts
Normal 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,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user