* }> * } */ public function parse(string $content): array { $lines = explode("\n", $content); $blocks = []; $i = 0; $len = \count($lines); while ($i < $len) { if (preg_match('/^\s*map\s+\$(\w+)\s+\$(\w+)\s*\{/', $lines[$i], $m)) { $block = [ 'source' => $m[1], 'target' => $m[2], 'startLine' => $i, 'endLine' => $i, 'entries' => [], ]; $openBrace = $i; $depth = 0; $inBlock = false; for ($j = $i; $j < $len; $j++) { $line = $lines[$j]; if (str_contains($line, '{')) { $depth++; $inBlock = true; } if (str_contains($line, '}')) { $depth--; if ($depth === 0 && $inBlock) { $block['endLine'] = $j; break; } } if ($inBlock && $j > $openBrace) { $trimmed = trim($line); if ($trimmed === '' || $trimmed === '}') { continue; } if (preg_match('/^default\s+(\S+)\s*;\s*(.*)$/', $trimmed, $defaultMatch)) { $block['default'] = trim($defaultMatch[1], '"\''); continue; } if (preg_match('/^(\S+)\s+(\S+)\s*;\s*(.*)$/', $trimmed, $entry)) { $hostname = trim($entry[1], '"\''); $value = trim($entry[2], '"\''); $comment = trim($entry[3] ?? ''); $block['entries'][] = [ 'hostname' => $hostname, 'value' => $value, 'line' => $j, 'comment' => $comment !== '' ? $comment : null, ]; } } } if ($block['endLine'] > $block['startLine']) { $blocks[] = $block; } $i = $block['endLine'] + 1; } else { $i++; } } return ['blocks' => $blocks]; } /** * Rebuild file content from parsed blocks, preserving non-map content and original map order/position. * * Each block must include startLine, endLine from {@see parse()} (GUI saves merge file metadata with edited entries). */ public function rebuild(string $originalContent, array $parsedBlocks): string { $lines = explode("\n", $originalContent); $len = \count($lines); /** @var array> $blockAtLine */ $blockAtLine = []; foreach ($parsedBlocks['blocks'] as $block) { if (!isset($block['startLine'], $block['endLine'])) { throw new \InvalidArgumentException( 'Each map block must include startLine and endLine (re-parse the file or use merged save data).' ); } $blockAtLine[(int) $block['startLine']] = $block; } ksort($blockAtLine, SORT_NUMERIC); $output = []; $i = 0; while ($i < $len) { if (isset($blockAtLine[$i])) { $block = $blockAtLine[$i]; foreach ($this->renderMapBlockLines($lines, $block) as $line) { $output[] = $line; } $i = (int) $block['endLine'] + 1; continue; } $output[] = $lines[$i]; $i++; } return implode("\n", $output); } /** * @param array $block * @return list */ private function renderMapBlockLines(array $fileLines, array $block): array { $start = (int) $block['startLine']; $indent = $this->detectIndent($fileLines, $start); $out = []; $out[] = sprintf('%smap $%s $%s {', $indent, $block['source'], $block['target']); $defaultValue = $block['default'] ?? "''"; $out[] = sprintf('%s default %s;', $indent, $defaultValue); foreach ($block['entries'] as $entry) { if (!\is_array($entry)) { continue; } $hostname = $this->escapeIfNeeded((string) ($entry['hostname'] ?? '')); $value = $this->escapeIfNeeded((string) ($entry['value'] ?? '')); $comment = !empty($entry['comment']) ? ' ' . (string) $entry['comment'] : ''; $out[] = sprintf('%s %s %s;%s', $indent, $hostname, $value, $comment); } $out[] = $indent . '}'; return $out; } private function detectIndent(array $lines, int $lineNum): string { if ($lineNum < \count($lines)) { $line = $lines[$lineNum]; if (preg_match('/^(\s*)/', $line, $m)) { return $m[1]; } } return ''; } private function escapeIfNeeded(string $value): string { if (preg_match('/^[a-zA-Z0-9._-]+$/', $value)) { return $value; } return '"' . addcslashes($value, '"\\') . '"'; } }