Files
MageSail/Magesail/Model/NginxMapParser.php
T
2026-03-21 03:01:08 -05:00

160 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace MageSail\Magesail\Model;
/**
* Parses NGINX map blocks into structured data for GUI editing.
*/
class NginxMapParser
{
/**
* @return array{blocks: array<int, array{source: string, target: string, startLine: int, endLine: int, entries: array<int, array{hostname: string, value: string, line: int, comment?: string}>}>}
*/
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<int, array<string, mixed>> $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<string, mixed> $block
* @return list<string>
*/
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, '"\\') . '"';
}
}