excerpt` line, then the body. This reads that * layout (tolerating a missing excerpt, text before the title, or a fenced answer) and writes it back for a refine * request. It also fills a template's `{field}` placeholders. * * @since 0.1.0 */ final class StudioDocument { /** * @since 0.1.0 * @return array{title: string, excerpt: string, body: string} */ public static function parse(string $markdown): array { $text = trim(str_replace(["\r\n", "\r"], "\n", $markdown)); if (preg_match('/^```(?:markdown|md)?\n(.*)\n```$/s', $text, $m)) { $text = trim($m[1]); } $lines = explode("\n", $text); $title = ''; foreach ($lines as $i => $line) { if (preg_match('/^#\s+(.+?)\s*#*\s*$/', $line, $m)) { $title = trim($m[1]); $lines = array_slice($lines, $i + 1); break; } } while ($lines !== [] && trim($lines[0]) === '') { array_shift($lines); } $excerpt = []; while ($lines !== [] && str_starts_with(ltrim($lines[0]), '>')) { $excerpt[] = trim((string) preg_replace('/^\s*>\s?/', '', (string) array_shift($lines))); } return ['title' => $title, 'excerpt' => trim(implode(' ', $excerpt)), 'body' => trim(implode("\n", $lines))]; } /** * @since 0.1.0 * @param array{title: string, excerpt: string, body: string} $document */ public static function compose(array $document): string { return trim('# ' . $document['title'] . "\n\n" . ($document['excerpt'] !== '' ? '> ' . $document['excerpt'] . "\n\n" : '') . $document['body']); } /** * Fill `{field}` placeholders; a field left empty reads "not specified". * * @since 0.1.0 * @param array $values */ public static function fill(string $instructions, array $values): string { return (string) preg_replace_callback('/\{([a-z][a-z0-9_]{0,30})\}/', static function (array $m) use ($values): string { $value = trim($values[$m[1]] ?? ''); return $value !== '' ? $value : 'not specified'; }, $instructions); } }