import type { DraftFaqItem, DraftSummary } from "@pipeline/shared"; export type DraftPreviewBlock = | { type: "heading"; level: number; text: string } | { type: "paragraph"; text: string; links: DraftPreviewLink[] } | { type: "table"; headers: string[]; rows: string[][] } | { type: "image"; alt: string; src: string } | { type: "faq"; items: DraftFaqItem[] }; export type DraftPreviewLink = { label: string; href: string; }; export type DraftVersionComparison = { leftVersion: number; rightVersion: number; titleChanged: boolean; metaTitleChanged: boolean; metaDescriptionChanged: boolean; bodyChanged: boolean; changedLineCount: number; }; export function selectDraftById( drafts: readonly DraftSummary[], draftId: string | null, ): DraftSummary | null { if (!draftId) { return drafts[0] ?? null; } return drafts.find((draft) => draft.id === draftId) ?? null; } export function compareDraftVersions( left: DraftSummary, right: DraftSummary, ): DraftVersionComparison { const leftBodyMarkdown = left.body_markdown ?? ""; const rightBodyMarkdown = right.body_markdown ?? ""; const leftLines = normalizedLines(leftBodyMarkdown); const rightLines = normalizedLines(rightBodyMarkdown); return { leftVersion: left.version, rightVersion: right.version, titleChanged: left.title !== right.title, metaTitleChanged: (left.meta_title ?? "") !== (right.meta_title ?? ""), metaDescriptionChanged: (left.meta_description ?? "") !== (right.meta_description ?? ""), bodyChanged: leftBodyMarkdown !== rightBodyMarkdown, changedLineCount: changedLineCount(leftLines, rightLines), }; } export function parseDraftPreview( markdown: string, faqItems: readonly DraftFaqItem[], ): DraftPreviewBlock[] { const lines = markdown.split(/\r?\n/); const blocks: DraftPreviewBlock[] = []; let index = 0; while (index < lines.length) { const line = lines[index].trim(); if (!line) { index += 1; continue; } if (line.startsWith("#")) { const match = line.match(/^(#{1,6})\s+(.+)$/); if (match) { blocks.push({ type: "heading", level: match[1].length, text: match[2].trim(), }); index += 1; continue; } } if (line.startsWith("![") && line.includes("](") && line.endsWith(")")) { const match = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); if (match) { blocks.push({ type: "image", alt: match[1], src: match[2], }); index += 1; continue; } } if (line.startsWith("|") && index + 1 < lines.length && lines[index + 1].includes("|")) { const tableLines: string[] = []; while (index < lines.length && lines[index].trim().startsWith("|")) { tableLines.push(lines[index].trim()); index += 1; } if (tableLines.length >= 2) { const headers = parseTableRow(tableLines[0]); const rows = tableLines.slice(2).map(parseTableRow); blocks.push({ type: "table", headers, rows, }); } continue; } const paragraphLines: string[] = []; while (index < lines.length && lines[index].trim()) { paragraphLines.push(lines[index].trim()); index += 1; } const paragraphText = paragraphLines.join(" "); blocks.push({ type: "paragraph", text: paragraphText, links: extractLinks(paragraphText), }); } if (faqItems.length > 0) { blocks.push({ type: "faq", items: [...faqItems], }); } return blocks; } function normalizedLines(value: string): string[] { return value .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); } function changedLineCount(left: string[], right: string[]): number { const limit = Math.max(left.length, right.length); let changes = 0; for (let index = 0; index < limit; index += 1) { if ((left[index] ?? "") !== (right[index] ?? "")) { changes += 1; } } return changes; } function parseTableRow(row: string): string[] { return row .slice(1, row.endsWith("|") ? -1 : row.length) .split("|") .map((cell) => cell.trim()); } function extractLinks(text: string): DraftPreviewLink[] { const links: DraftPreviewLink[] = []; const pattern = /\[([^\]]+)\]\(([^)]+)\)/g; let match: RegExpExecArray | null = pattern.exec(text); while (match) { links.push({ label: match[1], href: match[2] }); match = pattern.exec(text); } return links; }