import type { AcceptanceCriteriaItemModel, AcceptanceCriteriaModel, } from '@/types/models/acceptance-criteria-model'; const MAX_INDENT_LEVEL = 8; export type AcceptanceCriteriaEditorState = { checklist: string; summary: string; }; export const emptyAcceptanceCriteria = (): AcceptanceCriteriaModel => ({ summary: null, items: [], }); export const normalizeAcceptanceCriteria = ( value: AcceptanceCriteriaModel | null | undefined, ): AcceptanceCriteriaModel => { if (value === null || value === undefined) { return emptyAcceptanceCriteria(); } const summary = value.summary?.trim() ?? null; const items = Array.isArray(value.items) ? value.items .map((item) => ({ checked: item.checked === true, indent: clampIndent(item.indent), text: item.text.trim(), })) .filter((item) => item.text !== '') : []; return { summary: summary === '' ? null : summary, items, }; }; export const parseChecklistLines = ( checklistInput: string, ): AcceptanceCriteriaItemModel[] => { if (checklistInput.trim() === '') { return []; } return checklistInput .split(/\r?\n/u) .map((line) => parseChecklistLine(line)) .filter( (parsedLine): parsedLine is AcceptanceCriteriaItemModel => parsedLine !== null, ); }; export const formatChecklistLines = ( items: AcceptanceCriteriaItemModel[], ): string => { return items .map((item) => { const indentation = ' '.repeat(clampIndent(item.indent)); const marker = item.checked ? '[x]' : '[ ]'; return `${indentation}${marker} ${item.text}`; }) .join('\n'); }; export const buildAcceptanceCriteria = ( summaryInput: string, checklistInput: string, ): AcceptanceCriteriaModel => { const summary = summaryInput.trim(); return normalizeAcceptanceCriteria({ summary: summary === '' ? null : summary, items: parseChecklistLines(checklistInput), }); }; export const acceptanceCriteriaToEditorState = ( value: AcceptanceCriteriaModel | null | undefined, ): AcceptanceCriteriaEditorState => { const normalized = normalizeAcceptanceCriteria(value); return { checklist: formatChecklistLines(normalized.items), summary: normalized.summary ?? '', }; }; const parseChecklistLine = ( line: string, ): AcceptanceCriteriaItemModel | null => { const normalizedLine = line.replaceAll('\t', ' ').trimEnd(); if (normalizedLine.trim() === '') { return null; } const match = /^(?\s*)(?.+)$/u.exec(normalizedLine); if (match === null || match.groups === undefined) { return null; } const indentLevel = clampIndent( Math.floor(match.groups.indent.length / 2), ); let content = match.groups.content.trim(); let checked = false; const checkboxMatch = /^(?:[-*+]\s+)?\[(?[xX ])\]\s*(?.+)$/u.exec(content); if (checkboxMatch?.groups !== undefined) { checked = checkboxMatch.groups.state.toLowerCase() === 'x'; content = checkboxMatch.groups.text.trim(); } else { const bulletMatch = /^[-*+]\s+(?.+)$/u.exec(content); if (bulletMatch?.groups !== undefined) { content = bulletMatch.groups.text.trim(); } } if (content === '') { return null; } return { checked, indent: indentLevel, text: content, }; }; const clampIndent = (indent: number): number => { if (!Number.isFinite(indent)) { return 0; } return Math.max(0, Math.min(MAX_INDENT_LEVEL, Math.trunc(indent))); };