* } */ public static function fromStored(mixed $storedData, ?string $legacyText): array { $normalizedStoredData = is_array($storedData) ? self::normalize($storedData) : self::empty(); if (self::hasContent($normalizedStoredData)) { return $normalizedStoredData; } return self::normalize($legacyText); } /** * @return array{ * summary: string|null, * items: array * } */ public static function normalize(mixed $value): array { if (is_array($value)) { return self::normalizeFromArray($value); } if (is_string($value) || $value === null) { return self::normalizeFromString($value); } return self::empty(); } /** * @param array{ * summary: string|null, * items: array * } $acceptanceCriteria */ public static function hasContent(array $acceptanceCriteria): bool { return $acceptanceCriteria['summary'] !== null || $acceptanceCriteria['items'] !== []; } /** * @param array{ * summary: string|null, * items: array * } $acceptanceCriteria * @return array{ * summary: string|null, * items: array * }|null */ public static function forStorage(array $acceptanceCriteria): ?array { $normalized = self::normalize($acceptanceCriteria); return self::hasContent($normalized) ? $normalized : null; } /** * @param array{ * summary: string|null, * items: array * } $acceptanceCriteria */ public static function toLegacyText(array $acceptanceCriteria): ?string { $normalized = self::normalize($acceptanceCriteria); $lines = []; if ($normalized['summary'] !== null) { $lines[] = $normalized['summary']; } if ($normalized['items'] !== []) { if ($normalized['summary'] !== null) { $lines[] = ''; } foreach ($normalized['items'] as $item) { $lines[] = sprintf( '%s- [%s] %s', str_repeat(' ', $item['indent']), $item['checked'] ? 'x' : ' ', $item['text'], ); } } $legacyText = trim(implode("\n", $lines)); return $legacyText === '' ? null : $legacyText; } /** * @return array{ * summary: string|null, * items: array * } */ private static function empty(): array { return [ 'summary' => null, 'items' => [], ]; } /** * @param array $value * @return array{ * summary: string|null, * items: array * } */ private static function normalizeFromArray(array $value): array { $summary = trim((string) Arr::get($value, 'summary', '')); $rawItems = Arr::get($value, 'items', []); if (! is_array($rawItems)) { $rawItems = []; } $items = []; foreach ($rawItems as $rawItem) { if (! is_array($rawItem)) { continue; } $text = trim((string) Arr::get($rawItem, 'text', '')); if ($text === '') { continue; } $rawChecked = Arr::get($rawItem, 'checked', false); $rawIndent = Arr::get($rawItem, 'indent', 0); $items[] = [ 'checked' => filter_var($rawChecked, FILTER_VALIDATE_BOOLEAN), 'indent' => self::clampIndent((int) $rawIndent), 'text' => $text, ]; } return [ 'summary' => $summary === '' ? null : $summary, 'items' => $items, ]; } /** * @return array{ * summary: string|null, * items: array * } */ private static function normalizeFromString(?string $value): array { $text = trim((string) $value); if ($text === '') { return self::empty(); } $lines = preg_split('/\R/u', $text); if (! is_array($lines)) { return [ 'summary' => $text, 'items' => [], ]; } $summary = null; $items = []; foreach ($lines as $line) { $trimmedLine = trim($line); if ($trimmedLine === '') { continue; } if ($summary === null) { $summary = $trimmedLine; continue; } $parsedItem = self::parseChecklistItem($line); if ($parsedItem === null) { continue; } $items[] = $parsedItem; } return [ 'summary' => $summary, 'items' => $items, ]; } /** * @return array{checked: bool, indent: int, text: string}|null */ private static function parseChecklistItem(string $line): ?array { $line = rtrim(str_replace("\t", ' ', $line)); if ($line === '') { return null; } if (! preg_match('/^(?\s*)(?.+)$/u', $line, $matches)) { return null; } $content = trim((string) Arr::get($matches, 'content', '')); if ($content === '') { return null; } $checked = false; if (preg_match('/^(?:[-*+]\s+)?\[(?[xX ])\]\s*(?.+)$/u', $content, $checkboxMatches)) { $checked = strtolower((string) Arr::get($checkboxMatches, 'state', '')) === 'x'; $content = trim((string) Arr::get($checkboxMatches, 'text', '')); } elseif (preg_match('/^[-*+]\s+(?.+)$/u', $content, $bulletMatches)) { $content = trim((string) Arr::get($bulletMatches, 'text', '')); } if ($content === '') { return null; } return [ 'checked' => $checked, 'indent' => self::indentLevel((string) Arr::get($matches, 'indent', '')), 'text' => $content, ]; } private static function indentLevel(string $leadingWhitespace): int { $normalizedWhitespace = str_replace("\t", ' ', $leadingWhitespace); return self::clampIndent(intdiv(strlen($normalizedWhitespace), 2)); } private static function clampIndent(int $indent): int { return max(0, min(self::MAX_INDENT_LEVEL, $indent)); } }