publish(task 3): frontmatter-parser mit yaml + body-split

parseFrontmatter(md) trennt yaml-frontmatter vom markdown-body via
regex, parst yaml mit @std/yaml. Frontmatter-interface enthält
nostr-publish-konvention: optionale images-liste mit file/role/alt/
license/authors plus Author-interface (name/url/orcid). 4 tests grün.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jörg Lohrer 2026-04-18 05:22:41 +02:00
parent 1e4359aab6
commit 178016f0f4
3 changed files with 91 additions and 0 deletions

View File

@ -0,0 +1,47 @@
import { parse as parseYaml } from '@std/yaml'
export interface Author {
name: string
url?: string
orcid?: string
[key: string]: unknown
}
export interface ImageEntry {
file: string
role?: 'cover'
alt: string
caption?: string
license: string | 'UNKNOWN'
authors: Author[] | 'UNKNOWN'
source_url?: string
modifications?: string
[key: string]: unknown
}
export interface Frontmatter {
title: string
slug: string
date: Date
description?: string
image?: string
cover?: { image?: string; alt?: string; caption?: string }
tags?: string[]
draft?: boolean
license?: string
authors?: Author[]
images?: ImageEntry[]
[key: string]: unknown
}
export function parseFrontmatter(md: string): { fm: Frontmatter; body: string } {
const match = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/)
if (!match) {
throw new Error('Frontmatter: no leading --- / --- block found')
}
const fm = parseYaml(match[1]) as Frontmatter
if (!fm || typeof fm !== 'object') {
throw new Error('Frontmatter: YAML did not produce an object')
}
return { fm, body: match[2] }
}

17
publish/tests/fixtures/sample-post.md vendored Normal file
View File

@ -0,0 +1,17 @@
---
layout: post
title: "Sample Title"
slug: "sample-slug"
description: "A short summary"
image: cover.png
cover:
image: cover.png
alt: "Alt text"
date: 2024-01-15
tags: ["Foo", "Bar"]
draft: false
---
Body content here.
![pic](image1.jpg)

View File

@ -0,0 +1,27 @@
import { assertEquals, assertThrows } from '@std/assert'
import { parseFrontmatter } from '../src/core/frontmatter.ts'
Deno.test('parseFrontmatter: zerlegt Frontmatter und Body', async () => {
const md = await Deno.readTextFile('./tests/fixtures/sample-post.md')
const { fm, body } = parseFrontmatter(md)
assertEquals(fm.title, 'Sample Title')
assertEquals(fm.slug, 'sample-slug')
assertEquals(fm.date instanceof Date, true)
assertEquals(fm.tags, ['Foo', 'Bar'])
assertEquals(fm.cover?.image, 'cover.png')
assertEquals(body.trim().startsWith('Body content here.'), true)
})
Deno.test('parseFrontmatter: wirft bei fehlendem Frontmatter', () => {
assertThrows(() => parseFrontmatter('no frontmatter here'), Error, 'Frontmatter')
})
Deno.test('parseFrontmatter: wirft bei unvollständigem Frontmatter', () => {
assertThrows(() => parseFrontmatter('---\ntitle: x\n'), Error, 'Frontmatter')
})
Deno.test('parseFrontmatter: erhält Leerzeichen in String-Werten', () => {
const md = '---\ntitle: "Hello World"\nslug: "h-w"\ndate: 2024-01-01\n---\n\nbody'
const { fm } = parseFrontmatter(md)
assertEquals(fm.title, 'Hello World')
})