Bicycle/tools/sort_html_attrs.php
Egor Isaev b65603b4d0 dev
2026-06-23 10:44:18 +03:00

245 lines
6.9 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env php
<?php
/**
* @package Bicycle
* @author Egor Isaev
* @description sort_html_attrs.php
* @copyright (c) 23/09/2025
*
*
* Сортирует атрибуты ТОЛЬКО внутри HTML-тегов.
* Не меняет табы/пробелы/переносы/PHP-блоки.
*
* Порядок: id → class → href → type → name → data-* → aria-* → role → остальные (в исходном порядке).
*
* Использование:
* php tools/sort_html_attrs.php path/to/file.html [--inplace]
*/
if ($argc < 2) {
fwrite(STDERR, "Usage: php sort_html_attrs.php file.[html] [--inplace]\n");
exit(1);
}
$file_path = $argv[1];
$inplace = in_array('--inplace', $argv, true);
$code = @file_get_contents($file_path);
if ($code === false) {
fwrite(STDERR, "Cannot read: $file_path\n");
exit(1);
}
function TokenizeAttributes(string $s): array
{
$tokens = [];
$i = 0;
$n = strlen($s);
while ($i < $n) {
// ведущий разделитель (сохраняем как есть)
$sep_start = $i;
while ($i < $n && preg_match('/\s/u', $s[$i])) {
$i++;
}
$sep = substr($s, $sep_start, $i - $sep_start);
if ($i >= $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;