159 lines
5.8 KiB
PHP
159 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, '"\\') . '"';
|
|
}
|
|
}
|