- lib/product-doc-parser.ts: parseProductDocMd(md) → {ok, frontmatter, body}
| {ok:false, errors[]} met line-info bij YAML-fouten. Pattern gespiegeld
uit lib/idea-plan-parser.ts.
- lib/product-doc-frontmatter.ts: setProductDocFrontmatterFields(md, patch)
laat de server `last_updated` server-side normaliseren (P2-review-fix
uit docs/recommendations/product-docs-storage-system-review-2026-05-16).
Gebruikt yaml.parseDocument om field-ordering best-effort te behouden.
- todayIsoDate() helper voor 'yyyy-mm-dd' string.
- __tests__: 19 nieuwe tests groen — parse-success/fail-paden, en
expliciete P2-coverage (vervangen + toevoegen last_updated, behoud
overige velden + body).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
// Parser voor de product_doc markdown. Format: yaml-frontmatter (gevalideerd
|
|
// via productDocFrontmatterSchema) + markdown-body. Synchroon — geen LLM.
|
|
//
|
|
// Wordt door alle Product Doc server-actions (create/update) gebruikt om
|
|
// frontmatter te valideren vóór save. Bij parse-fout: 422 met line-info.
|
|
// Pattern gespiegeld uit lib/idea-plan-parser.ts.
|
|
|
|
import { parse as parseYaml, YAMLParseError } from 'yaml'
|
|
|
|
import {
|
|
productDocFrontmatterSchema,
|
|
type ProductDocFrontmatter,
|
|
} from '@/lib/schemas/product-doc'
|
|
|
|
export type ProductDocParseError = {
|
|
line?: number
|
|
message: string
|
|
hint?: string
|
|
}
|
|
|
|
export type ProductDocParseResult =
|
|
| { ok: true; frontmatter: ProductDocFrontmatter; body: string }
|
|
| { ok: false; errors: ProductDocParseError[] }
|
|
|
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
|
|
|
|
export function parseProductDocMd(md: string): ProductDocParseResult {
|
|
const match = md.match(FRONTMATTER_RE)
|
|
if (!match) {
|
|
return {
|
|
ok: false,
|
|
errors: [
|
|
{
|
|
line: 1,
|
|
message:
|
|
'Doc mist yaml-frontmatter. Eerste regel moet `---` zijn, gevolgd door de frontmatter en een afsluitende `---`.',
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
const [, frontmatterRaw, body] = match
|
|
|
|
let parsed: unknown
|
|
try {
|
|
parsed = parseYaml(frontmatterRaw)
|
|
} catch (err) {
|
|
if (err instanceof YAMLParseError) {
|
|
const yamlLine = err.linePos?.[0]?.line
|
|
// +1 voor de openende `---`-regel (frontmatterRaw start op regel 2)
|
|
const fileLine = yamlLine != null ? yamlLine + 1 : undefined
|
|
return {
|
|
ok: false,
|
|
errors: [{ line: fileLine, message: err.message }],
|
|
}
|
|
}
|
|
return {
|
|
ok: false,
|
|
errors: [{ message: err instanceof Error ? err.message : String(err) }],
|
|
}
|
|
}
|
|
|
|
const validation = productDocFrontmatterSchema.safeParse(parsed)
|
|
if (!validation.success) {
|
|
return {
|
|
ok: false,
|
|
errors: validation.error.issues.map((iss) => ({
|
|
message: `${iss.path.join('.') || '<root>'}: ${iss.message}`,
|
|
})),
|
|
}
|
|
}
|
|
|
|
return { ok: true, frontmatter: validation.data, body: body.trimStart() }
|
|
}
|