Парсер CSS для .NET, написанный на C# (зачёркнуто) регулярных выражениях

в 8:20, , рубрики: .net, c++, css, Firefox, mozilla, мазохизм, ненормальное программирование, Программирование, метки: , , , , ,

Парсер CSS для .NET, написанный на C# (зачёркнуто) регулярных выражениях Понадобилось мне однажды распарсить CSS, чтобы вынуть @import, url(). Но для .NET были только разной степени кривоты поделки. Лучшей библиотекой была ExCSS, но она загибалась на таких тривиальных вещах, как медиа-запросы. Поэтому я решил заполнить пробел.

Были варианты: расковырять Chrome, расковырять Firefox, расковырять левую библиотеку. Нужно было гарантированное качество и регулярное обновление, поэтому последний вариант отпадал. В Chrome парсинг CSS и HTML генерировался на основе грамматик, и беглое изучение разнообразия инструментов для .NET повергло в уныние, что уж говорить о совместимости инструментов, поэтому Chrome отпал. Остался Firefox с вручную написанными парсерами.

И здесь начинается интересное: дабы сохранить хоть какую-то возможность обновлять код при выходе новых версий файрфокса, я решил не единожды переписать сорцы, а натравливать на плюсовые сорцы тонны регулярных выражений, и добиваться компилируемости плюсового кода только регулярками.

Я вижу косые взгляды. У кого-то уже тянется рука набирать 03…

Не спешите. Всё не так страшно. Конвертация страшного и ужасного nsCSSParser.cpp длиной в более, чем 10 000 строк, уложилась всего-то в небольшую простынку регулярок на 400 строк, то есть всего лишь 4% оригинального кода. Ну да, конвертация некоторых файлов в 10 строк выливалась в 30 строк кода, но не будем о грустном.

В общем, с грехом напополам потенциально самые часто обновляемые куски кода были конвертированы регулярками. Более-менее стабильные куски кода (типы значений и т.п.) и невменяемо-плюсовые куски кода (перегрузка оператора new для выделения дополнительной памяти, union'ы и т.п.) были переписаны вручную. Очень помогло, что стиль именования переменных в кода Мазилы очень строг, и по префиксу сразу можно понять, что перед тобой: аргумент, поле, константа, статическая переменная, член перечисления и т.п. Того же не скажешь про именование классов — там полное раздолбайство, которое даже в коде нелестно комментируется, — но, опять-таки, не будем о грустном.

Насколько удастся коварный план без особых проблем обновлять библиотеку с выходом новых версий файрфокса — покажет время.

Долго ли, коротко ли, в итоге получилось нечто, что делает вид, что работает.

Делает вид — это потому что я не представляю, как полноценно проверить работоспособность. Тесты в файрфоксе вызывают уныние своей джаваскрипт-в-аштээмэльностью. Других нормальных тестов я не нашёл. Twitter Bootstrap делает вид, что распарсивается (не падает, вручную просмотренные свойства похожи на правду). Я очень надеюсь, что библиотека хоть кому-то пригодится, и этот кто-то расскажет мне об ошибках, если таковые встретятся.

Что имеется

  • Поддерживаются все правила, свойства, значения и т.п., которые поддерживаются Mozilla Firefox. Специфические для ФФ расширения (-moz-*) в том числе.
  • Два режима совместимости: стандарты и IE6 глюки.
  • Все значения распарсиваются в сложные структуры. Краткое свойство background развернётся в несколько свойств, в том числе background-image, которое содержит список фоновых картинок, каждая из которых — URL или градиент; в последнем случае в градиенте будут содержаться отдельные точки и все параметры.
  • Обработка ошибок в соответствии со всеми спецификациями. Если что-то не распознает, то парсер просто пропустит непонятный кусок.
  • Детальное логирование ошибок. Все предупреждения про неверный синтаксис и свойства сваливаются в TraceSource "Alba.CsCss.CssParser" и кидаются событием.

Чего не имеется

  • Поддержка кидировок. В юникод нужно перекидывать самостоятельно.
  • Модификация и конвертация обратно в строку.
  • DOM CSS. Интерфейс с точки зрения стандартов кодирования C# весьма сомнителен, поэтому польза под вопросом.
  • Свойства с вендорными префиксами остальных браузеров (-webkit-, -ms-, -o-) игнорируются.
  • .NET 4.0 и ниже. От .NET 4.5 используется разве что IReadOnlyList, но пока возиться с версиями несколько лень.
  • Пакет NuGet. Сыровата пока библиотека.

Пример использования

// Распарсить CSS, указав URL файла (для логирования) и базовый URL (для резолва относительных урлов)
CssStyleSheet css = new CssLoader().ParseSheet("h1, h2 { color: #123; }",
    "http://example.com/sheet.css", "http://example.com/");
Console.WriteLine(css.SheetUri); // http://example.com/sheet.css
// Получить цвет (варианты равносильны)
Console.WriteLine(css.StyleRules.Single().Declaration.Color.Color.R); // 17
Console.WriteLine(css.Rules.OfType<CssStyleRule>.Single().Declaration
                               .GetValue(CssProperty.Color).Color.R); // 17
// Получить тег в первом селекторе
Console.WriteLine(css.StyleRules.Single().SelectorGroups.First().Selectors.Single().Tag);

Сборка проекта

  • Alba.CsCss — собственно библиотека. Зависимостей от других проектов не имеет. Если хотите использовать библитеку в своём солюшене, достаточно включить этот проект.
  • Alba.CsCss.Tests — «юнит»-тесты (попахивают скорее интеграционными). Количество такое, о котором в приличном обществе принято молчать.
  • Alba.Framework — персональный сборник велосипедов с квадратными колёсами фреймворк. Упрощает код в трансформациях T4. Для запуска оных нужно собрать Debug версию.
  • Alba.Framework.CodeGeneration — сборник T4 велосипедов. Собираться должен под админским аккаунтом, чтобы коварно пролезть в вашу систему и установить custom tool «AttachT4» (родственно T4 Toolbox, только без тонны хлама). Нужно, если хотите удобно работать с T4 в проекте.
  • Alba.Framework.Testing — тестировальные велосипеды. Используются в тестах.

Лицензия

Mozilla Public License. Помесь бульдога с носорогом BSD с GPL. Вирусная, как GPL, но заражает только отдельные файлы сорцов с кодом MPL. Всё остальное лицензию не волнует.

Как ощущения?

Незабываемые! Я почвствовал себя настоящим экспертом по регулярным выражениям.

Например, угадайте, что ищет этот код?

@"(?n)(?<!(switch ?(.*) ?{( *//.*)?|case .*:|default:|break;|return [^;]*;)s+)(?<s> +)(?<c>default|case .*):"

Правда случались и загвоздки. Например, я никак не могу переписать это выражение без использования «безоглядного» режима ((?> ... )):

.ReReplace(@"(?nx)
                (?<! , )                                # do not match if among arguments
                (?>                                      # no backtracking: disallow skipping openning bracket
                    (?<o> ( )?                          # opening bracket
                    (?<Var> aVariantMask | yVal | mask ) # A & B expression: A
                     &                                 # A & B expression: &
                    (?<Const> VARIANT_w+ | BG_w+ )     # A & B expression: B
                )
                (?<-o> ) )?                             # closing bracket
                (?!)? != 0)                           # do not match if comparison with 0 is already present
            (?(o)(?!))                                   # do not match if opening and closing brackets don't match",
            "((${Var} & ${Const}) != 0)")                // A & B -> ((A & B) != 0)

Здесь нужно преобразовать выражение (A & B) в ((A & B) != 0)), не добавляя лишние скобки и не добавляя проверку на ноль, если она уже есть.

Итоги

Библиотека написана. Будет ли она развиваться — зависит от того, будет ли она использоваться. Мне самому-то только малая часть нужна. Надеюсь на баг-репорты, а, возможно, даже пулл-реквесты, если есть смелые.

Автор: Athari

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js