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:
parent
1e4359aab6
commit
178016f0f4
|
|
@ -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] }
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
@ -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')
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue