$i['item_type'] === 'collection'); if ($nested) { $lines[] = "collections:"; foreach ($nested as $item) { $lines[] = " - " . $item['item_slug']; } } return implode("\n", $lines) . "\n"; } function yaml_quote(string $s): string { if ($s === '') return '""'; if (preg_match('/[:#{}&*!|>"\'\[\]]/', $s) || preg_match('/^\s/', $s)) { $s = str_replace('"', '\\"', $s); return '"' . $s . '"'; } return $s; } function yaml_multiline(string $s, int $indent = 0): string { if ($s === '') return '""'; $pad = str_repeat(' ', $indent); // Use folded scalar for readability $lines = explode("\n", trim($s)); if (count($lines) <= 1 && strlen($s) < 60) { return yaml_quote($s); } $out = ">-"; foreach ($lines as $line) { $out .= "\n" . $pad . " " . $line; } return $out; } function yaml_array(array $arr): string { if (empty($arr)) return '[]'; return '[' . implode(', ', array_map('yaml_quote', $arr)) . ']'; } /** * Parse a YAML protocol file into a database-ready array. * Uses yaml_parse if available, otherwise a simple parser for the protocol format. */ function parse_protocol_yaml(string $yaml_text): ?array { if (function_exists('yaml_parse')) { // Harden against PHP object injection via !php/object and !php/const tags. // The callbacks must be valid callables; null them out to neutralize deserialization. $neutralize = fn() => null; $data = yaml_parse($yaml_text, 0, $ndocs, [ '!php/object' => $neutralize, '!php/const' => $neutralize, ]); if (!is_array($data)) return null; } else { $data = simple_yaml_parse($yaml_text); if (!is_array($data)) return null; } $steps = []; foreach (($data['steps'] ?? []) as $step) { $steps[] = [ 'headline' => $step['headline'] ?? '', 'description' => $step['description'] ?? '', ]; } return [ 'slug' => $data['id'] ?? slugify($data['title'] ?? 'untitled'), 'title' => $data['title'] ?? '', 'description' => $data['description'] ?? '', 'source' => $data['source'] ?? '', 'source_url' => $data['source_url'] ?? '', 'tags' => json_encode($data['tags'] ?? []), 'forked_from' => $data['forked_from'] ?? null, 'image' => $data['image'] ?? null, 'steps' => json_encode($steps), 'outcome' => $data['outcome'] ?? '', 'practice' => $data['practice'] ?? '', ]; } /** * Minimal YAML parser for simple protocol files. * Handles: top-level key: value, nested lists with - headline:, folded scalars * This is NOT a full YAML parser — it handles the protocol format only. */ function simple_yaml_parse(string $text): ?array { $result = []; $lines = explode("\n", $text); $current_key = null; $in_steps = false; $in_folded = false; $folded_buffer = ''; $folded_key = null; $folded_indent = 0; $flush_folded = function() use (&$result, &$folded_buffer, &$folded_key, &$in_folded) { if ($in_folded && $folded_key !== null) { $result[$folded_key] = trim($folded_buffer); $folded_buffer = ''; $in_folded = false; $folded_key = null; } }; foreach ($lines as $line) { // Skip comments and empty lines if (preg_match('/^\s*#/', $line) || trim($line) === '') continue; // Folded scalar continuation if ($in_folded) { if (preg_match('/^(\s{' . ($folded_indent + 2) . ',})/', $line, $m)) { $folded_buffer .= ' ' . trim($line); continue; } else { $flush_folded(); } } // Steps list items if (preg_match('/^(\s+) - headline:\s*(.*)$/', $line, $m)) { $in_steps = true; $result['steps'][] = ['headline' => trim($m[2], '"'), 'description' => '']; $current_key = 'description'; continue; } if (preg_match('/^(\s+) description:\s*(.*)$/', $line, $m)) { $val = trim($m[2], '"'); if (str_starts_with($val, '>-') || str_starts_with($val, '|-')) { $in_folded = true; $folded_key = null; // Will set on last step $folded_buffer = ''; $folded_indent = strlen($m[1]); } else { if (!empty($result['steps'])) { $result['steps'][count($result['steps']) - 1]['description'] = $val; } } continue; } // Top-level key: value if (preg_match('/^([a-z_]+):\s*(.*)$/', $line, $m)) { $flush_folded(); $key = $m[1]; $val = trim($m[2]); if ($val === '' ) { $current_key = $key; if ($key === 'steps') { $result['steps'] = []; $in_steps = true; } continue; } // Parse inline array [a, b, c] if (str_starts_with($val, '[') && str_ends_with($val, ']')) { $inner = substr($val, 1, -1); $result[$key] = array_map(fn($s) => trim($s, ' "'), array_filter(explode(',', $inner), fn($s) => trim($s) !== '')); continue; } $result[$key] = trim($val, '"'); continue; } } $flush_folded(); return $result; }