import Handlebars from 'handlebars'; /** One numbered clause of a dynamic article, with optional nested bullets. */ export interface RenderedClause { text: string; bullets: string[]; } /** A dynamic article ready for the Handlebars template. */ export interface RenderedArticle { number: number; title: string; /** Set (instead of clauses) when the body is a single plain paragraph. */ paragraph?: string; clauses: RenderedClause[]; } /** * Parse a template article body into clauses. Format: one clause per line; * lines prefixed with "- " become bullets nested under the preceding clause. * A body that reduces to a single clause without bullets renders as a plain * paragraph rather than a numbered list of one. */ export function parseArticleBody(body: string): Pick { const lines = (body ?? '') .split('\n') .map((line) => line.trim()) .filter((line) => line.length > 0); const clauses: RenderedClause[] = []; for (const line of lines) { if (line.startsWith('- ')) { const bullet = line.slice(2).trim(); if (clauses.length === 0) { clauses.push({ text: bullet, bullets: [] }); } else { clauses[clauses.length - 1].bullets.push(bullet); } } else { clauses.push({ text: line, bullets: [] }); } } if (clauses.length === 1 && clauses[0].bullets.length === 0) { return { paragraph: clauses[0].text, clauses: [] }; } return { clauses }; } /** * Interpolate Handlebars placeholders ({{client.companyName}}, {{contractDate}}, * …) inside admin-authored template text against the contract view model. * Malformed placeholders must never break document generation — fall back to * the raw text. */ export function interpolateTemplateText(text: string, context: unknown): string { if (!text || !text.includes('{{')) return text ?? ''; try { return Handlebars.compile(text)(context); } catch { return text; } }