engines digest: require What You Get as first heading

This commit is contained in:
2026-08-06 19:03:16 +02:00
parent d82bb6f468
commit 23bbccd2b1
2 changed files with 56 additions and 34 deletions
@@ -1,59 +1,74 @@
import { describe, it, expect } from 'vitest'
import { getEngineDigest } from '../engines.store'
const WIKI_CONTENT = `
> WarpEngine is a **standalone**, mountable Rails engine that turns any Rails application into a retro software catalog.
// Generic markdown in the shape every engine-tagged wiki page must follow:
// intro paragraph, then "What You Get" as the first heading with bullets.
// Dummy text only — real content always comes from the wiki at runtime.
const SAMPLE_MARKDOWN = `
> Example Engine is a **sample** framework that turns markdown into landing cards.
# What You Get
- **Catalog domain**: \`Software\`, \`Release\` — all soft-deleted, with download tracking.
- **CI pipeline server**: Woodpecker [configuration extension](https://woodpecker-ci.org/docs) — the engine serves the full pipeline.
- **First feature**: some \`inline code\` — with a longer explanation.
- **Second feature**: a [link](https://example.org) inside the text.
- Plain bullet without a bold lead.
# Endpoints
# Later Section
- **Not picked up**: a list under a later heading.
`
describe('getEngineDigest', () => {
it('extracts the intro from the leading blockquote with inline markdown stripped', () => {
const digest = getEngineDigest(WIKI_CONTENT)
const digest = getEngineDigest(SAMPLE_MARKDOWN)
expect(digest.intro).toBe(
'WarpEngine is a standalone, mountable Rails engine that turns any Rails application into a retro software catalog.',
'Example Engine is a sample framework that turns markdown into landing cards.',
)
})
it('takes the heading above the first bullet list as the highlights title', () => {
expect(getEngineDigest(WIKI_CONTENT).highlightsTitle).toBe('What You Get')
it('takes the What You Get heading as the highlights title', () => {
expect(getEngineDigest(SAMPLE_MARKDOWN).highlightsTitle).toBe('What You Get')
})
it('parses bold-lead bullets into title and text', () => {
const [first, second] = getEngineDigest(WIKI_CONTENT).highlights
const [first, second] = getEngineDigest(SAMPLE_MARKDOWN).highlights
expect(first).toEqual({
title: 'Catalog domain',
text: 'Software, Release — all soft-deleted, with download tracking.',
title: 'First feature',
text: 'some inline code — with a longer explanation.',
})
expect(second).toEqual({
title: 'Second feature',
text: 'a link inside the text.',
})
expect(second.title).toBe('CI pipeline server')
expect(second.text).toBe('Woodpecker configuration extension — the engine serves the full pipeline.')
})
it('keeps bullets without a bold lead as plain text', () => {
const third = getEngineDigest(WIKI_CONTENT).highlights[2]
const third = getEngineDigest(SAMPLE_MARKDOWN).highlights[2]
expect(third).toEqual({ title: '', text: 'Plain bullet without a bold lead.' })
})
it('stops at the heading after the first list', () => {
const digest = getEngineDigest(WIKI_CONTENT)
expect(digest.highlights).toHaveLength(3)
it('stops at the heading after the What You Get section', () => {
expect(getEngineDigest(SAMPLE_MARKDOWN).highlights).toHaveLength(3)
})
it('matches the heading case-insensitively', () => {
const digest = getEngineDigest('# WHAT you get\n- **A**: b.')
expect(digest.highlights).toEqual([{ title: 'A', text: 'b.' }])
})
it('yields no highlights when the first heading is not What You Get', () => {
const digest = getEngineDigest('Intro line.\n\n# Features\n- **A**: b.\n\n# What You Get\n- **C**: d.')
expect(digest.intro).toBe('Intro line.')
expect(digest.highlights).toEqual([])
})
it('caps the highlights at six items', () => {
const many = '# Features\n' + Array.from({ length: 9 }, (_, i) => `- item ${i}`).join('\n')
const many = '# What You Get\n' + Array.from({ length: 9 }, (_, i) => `- item ${i}`).join('\n')
expect(getEngineDigest(many).highlights).toHaveLength(6)
})
it('uses the first paragraph as intro when there is no blockquote', () => {
const digest = getEngineDigest('Just a plain paragraph.\n\n---\n\n# Stuff\n- one')
const digest = getEngineDigest('Just a plain paragraph.\n\n---\n\n# What You Get\n- one')
expect(digest.intro).toBe('Just a plain paragraph.')
expect(digest.highlights).toEqual([{ title: '', text: 'one' }])
})
+21 -14
View File
@@ -9,14 +9,16 @@ export interface EngineHighlight {
text: string
}
// Landing-page digest of a wiki engine page: the intro paragraph plus the
// first bullet list (e.g. "What You Get") parsed into feature highlights.
// Landing-page digest of a wiki engine page. Convention: every engine-tagged
// page opens with an intro paragraph, then a "What You Get" heading as its
// first heading, with the feature bullets underneath.
export interface EngineDigest {
intro: string
highlightsTitle: string
highlights: EngineHighlight[]
}
const HIGHLIGHTS_HEADING = 'what you get'
const MAX_HIGHLIGHTS = 6
// Inline markdown (links, bold, code) stripped so the text reads as plain prose.
@@ -31,33 +33,38 @@ function stripInline(md: string): string {
export function getEngineDigest(content: string): EngineDigest {
const digest: EngineDigest = { intro: '', highlightsTitle: '', highlights: [] }
let heading = ''
let inHighlights = false
for (const raw of (content || '').split('\n')) {
const line = raw.trim()
if (!line || /^([-*_])\1{2,}$/.test(line)) continue
if (/^#{1,6}\s/.test(line)) {
// A heading after the list means the highlight section is over.
if (digest.highlights.length) break
heading = stripInline(line.replace(/^#{1,6}\s*/, ''))
const heading = line.match(/^#{1,6}\s+(.*)$/)
if (heading) {
// The section ends at the next heading; and if the page's first heading
// is not "What You Get", it doesn't follow the convention — no highlights.
if (inHighlights) break
const title = stripInline(heading[1])
if (title.toLowerCase() !== HIGHLIGHTS_HEADING) break
inHighlights = true
digest.highlightsTitle = title
continue
}
if (/^[-*]\s/.test(line)) {
const item = line.replace(/^[-*]\s+/, '')
const lead = item.match(/^\*\*([^*]+)\*\*\s*[:—–-]?\s*(.*)$/)
if (inHighlights) {
const bullet = line.match(/^[-*]\s+(.*)$/)
if (!bullet) continue
const lead = bullet[1].match(/^\*\*([^*]+)\*\*\s*[:—–-]?\s*(.*)$/)
digest.highlights.push(
lead
? { title: stripInline(lead[1]), text: stripInline(lead[2]) }
: { title: '', text: stripInline(item) },
: { title: '', text: stripInline(bullet[1]) },
)
if (!digest.highlightsTitle) digest.highlightsTitle = heading
continue
}
// First prose line (paragraph or blockquote) before any list is the intro.
if (!digest.intro && !digest.highlights.length) {
// First prose line (paragraph or blockquote) before the heading is the intro.
if (!digest.intro) {
digest.intro = stripInline(line.replace(/^>\s*/, ''))
}
}