#!/usr/bin/env php = $n) { // хвостовой сепаратор if ($sep !== '') { $tokens[] = ['sep' => $sep, 'attr' => '', 'name' => null]; } break; } // имя атрибута $name_start = $i; while ($i < $n && preg_match('/[^\s=>\/]/u', $s[$i])) { $i++; } $raw_name = substr($s, $name_start, $i - $name_start); if ($raw_name === '') { // мусорный символ — сохранить как часть sep $sep .= $s[$i]; $i++; $tokens[] = ['sep' => $sep, 'attr' => '', 'name' => null]; continue; } $name_lc = strtolower($raw_name); // пробелы до '=' $probe = $i; while ($probe < $n && preg_match('/\s/u', $s[$probe])) { $probe++; } $attr_text = $raw_name; $i = $probe; if ($i < $n && $s[$i] === '=') { // '=' и пробелы после $attr_text .= substr($s, $probe, $i - $probe + 1); $i++; $sp = $i; while ($i < $n && preg_match('/\s/u', $s[$i])) { $i++; } $attr_text .= substr($s, $sp, $i - $sp); // значение if ($i < $n && ($s[$i] === '"' || $s[$i] === "'")) { $q = $s[$i]; $attr_text .= $q; $i++; while ($i < $n) { $ch = $s[$i]; $attr_text .= $ch; $i++; if ($ch === $q) { break; } } } else { $v0 = $i; while ($i < $n && !preg_match('/[\s>]/u', $s[$i])) { $i++; } $attr_text .= substr($s, $v0, $i - $v0); } } $tokens[] = ['sep' => $sep, 'attr' => $attr_text, 'name' => $name_lc]; } return $tokens; } function SortTokens(array $tokens): array { $prio_exact = ['id', 'class', 'href', 'type', 'name']; $b = ['exact' => [], 'data' => [], 'aria' => [], 'role' => [], 'rest' => [], 'tail' => []]; foreach ($tokens as $t) { if ($t['name'] === null) { $b['tail'][] = $t; continue; } $n = $t['name']; if (in_array($n, $prio_exact, true)) { $b['exact'][] = $t; } elseif (str_starts_with($n, 'data-')) { $b['data'][] = $t; } elseif (str_starts_with($n, 'aria-')) { $b['aria'][] = $t; } elseif ($n === 'role') { $b['role'][] = $t; } else { $b['rest'][] = $t; } } // exact в порядке prio_exact $exact_sorted = []; foreach ($prio_exact as $want) { foreach ($b['exact'] as $t) { if ($t['name'] === $want) { $exact_sorted[] = $t; } } } $ordered = array_merge($exact_sorted, $b['data'], $b['aria'], $b['role'], $b['rest']); foreach ($b['tail'] as $t) { $ordered[] = $t; } // хвостовые сепараторы return $ordered; } function ReorderTagAttributes(string $head): string { // имя тега + остальное if (!preg_match('/^([a-zA-Z][\w:-]*)(.*)$/s', $head, $m)) { return $head; } $tag = $m[1]; $rest = $m[2]; if ($rest === '') { return $head; } $tokens = TokenizeAttributes($rest); if (!$tokens) { return $head; } $ordered = SortTokens($tokens); // собрать обратно без изменения пробелов: имя + (sep+attr)… $out = $tag; foreach ($ordered as $t) { $out .= $t['sep'] . $t['attr']; } return $out; } function Process(string $code): string { $out = ''; $i = 0; $n = strlen($code); while ($i < $n) { // PHP-блок: копируем как есть if ($i + 1 < $n && $code[$i] === '<' && $code[$i + 1] === '?') { $start = $i; $i += 2; while ($i + 1 < $n && !($code[$i] === '?' && $code[$i + 1] === '>')) { $i++; } $i = min($i + 2, $n); $out .= substr($code, $start, $i - $start); continue; } // Тег? if ($code[$i] === '<') { $tag_start = $i; $i++; // комментарии/doctype/закрывающий — копируем if ($i < $n && ($code[$i] === '!' || $code[$i] === '/')) { while ($i < $n && $code[$i] !== '>') { $i++; } if ($i < $n) { $i++; } $out .= substr($code, $tag_start, $i - $tag_start); continue; } // обычный тег до '>' $h0 = $i; while ($i < $n && $code[$i] !== '>') { $i++; } if ($i >= $n) { $out .= substr($code, $tag_start); break; } $head = substr($code, $h0, $i - $h0); $i++; // пропустить '>' // переставить атрибуты только в head $new_head = ReorderTagAttributes($head); $out .= '<' . $new_head . '>'; continue; } // просто текст $out .= $code[$i]; $i++; } return $out; } $out = Process($code); if ($inplace) { if (@file_put_contents($file_path, $out) === false) { fwrite(STDERR, "Cannot write: $file_path\n"); exit(1); } exit(0); } echo $out;