Помимо регулярных выражений: улучшение анализа значений CSS в Chrome DevTools

Philip Pfaffe
Эргюн Эрдогмус
Ergün Erdogmus

Вы заметили, что свойства CSS на вкладке « Стили » в Chrome DevTools в последнее время стали выглядеть более усовершенствованными? Эти обновления, выпущенные между Chrome 121 и 128, являются результатом значительного улучшения способа анализа и представления значений CSS. В этой статье мы познакомим вас с техническими деталями этого преобразования — перехода от системы сопоставления регулярных выражений к более надежному синтаксическому анализатору.

Давайте сравним текущий DevTools с предыдущей версией:

Вверху: это последняя версия Chrome, внизу: Chrome 121.

Большая разница, правда? Вот краткий обзор ключевых улучшений:

  • color-mix . Удобный предварительный просмотр, визуально представляющий два аргумента цвета в функции color-mix .
  • pink . Интерактивный предварительный просмотр цвета именованного цвета pink . Нажмите на него, чтобы открыть палитру цветов для легкой настройки.
  • var(--undefined, [fallback value]) . Улучшена обработка неопределенных переменных: неопределенная переменная выделена серым цветом , а активное резервное значение (в данном случае цвет HSL) отображается с интерактивным предварительным просмотром цвета.
  • hsl(…) : еще один интерактивный предварительный просмотр цвета для функции цвета hsl , обеспечивающий быстрый доступ к палитре цветов.
  • 177deg : интерактивные часы угла , которые позволяют интерактивно перетаскивать и изменять значение угла.
  • var(--saturation, …) : кликабельная ссылка на определение пользовательского свойства, позволяющая легко перейти к соответствующему объявлению.

Разница поразительная. Чтобы добиться этого, нам пришлось научить DevTools понимать значения свойств CSS намного лучше, чем раньше.

Разве эти превью уже не были доступны?

Хотя эти значки предварительного просмотра могут показаться знакомыми, они не всегда отображаются последовательно, особенно в сложном синтаксисе CSS, как в примере выше. Даже в тех случаях, когда они работали, часто требовались значительные усилия, чтобы заставить их работать правильно.

Причина в том, что система анализа значений органично развивается с первых дней DevTools. Однако он не смог справиться с недавними удивительными новыми возможностями, которые мы получили от CSS, и соответствующим увеличением сложности языка. Чтобы идти в ногу с эволюцией, системе потребовалась полная переработка, и мы именно это и сделали!

Как обрабатываются значения свойств CSS

В DevTools процесс рендеринга и оформления объявлений свойств на вкладке «Стили» разделен на два отдельных этапа:

  1. Структурный анализ. На этом начальном этапе объявление свойства анализируется для определения его основных компонентов и их связей. Например, в объявлении border: 1px solid red он будет распознавать 1px как длину, solid как строку и red как цвет.
  2. Рендеринг. На основе структурного анализа на этапе рендеринга эти компоненты преобразуются в HTML-представление. Это обогащает отображаемый текст свойств интерактивными элементами и визуальными подсказками. Например, значение red цвета отображается с помощью кликабельного значка цвета, при нажатии на который открывается палитра цветов для легкой модификации.

Регулярные выражения

Раньше мы полагались на регулярные выражения (регулярные выражения) для анализа значений свойств для структурного анализа. Мы поддерживали список регулярных выражений, соответствующих битам значений свойств, которые мы считали украшением. Например, были выражения, которые соответствовали цветам CSS, длинам, углам, более сложным подвыражениям, таким как вызовы функций var и т. д. Мы просматривали текст слева направо для анализа значений, постоянно ища первое выражение из списка, соответствующее следующему фрагменту текста.

Хотя большую часть времени это работало нормально, количество случаев, когда это не работало, продолжало расти. За прошедшие годы мы получили большое количество отчетов об ошибках, в которых сопоставление было не совсем правильным. Когда мы их исправили — некоторые исправления были простыми, другие — весьма сложными, — нам пришлось переосмыслить наш подход, чтобы избежать технического долга. Давайте рассмотрим некоторые вопросы!

Соответствие color-mix()

Регулярное выражение, которое мы использовали для функции color-mix() было следующим:

/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g

Что соответствует его синтаксису:

color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})

Попробуйте запустить следующий пример, чтобы визуализировать совпадения.

const re = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;

// it works - simpler example
const simpler = re.exec('color-mix(in srgb, pink, hsl(127deg 100% 50%))');
console.table(simpler.groups);

re.exec('');

// it doesn't work - complex example
const complex = re.exec('color-mix(in srgb, pink, var(--undefined, hsl(127deg var(--saturation, 100%) 50%)))');
console.table(complex.groups);

Результат сопоставления для функции смешивания цветов.

Более простой пример работает нормально. Однако в более сложном примере соответствие <firstColor> равно hsl(177deg var(--saturation и <secondColor> соответствует 100%) 50%)) , что совершенно бессмысленно.

Мы знали, что это проблема. В конце концов, CSS как формальный язык не является обычным , поэтому мы уже включили специальную обработку для работы с более сложными аргументами функций, такими как функции var . Однако, как вы можете видеть на первом скриншоте , это работало не во всех случаях.

Соответствие tan()

Одна из наиболее забавных ошибок , о которой сообщалось , касалась тригонометрической функции tan() . Регулярное выражение, которое мы использовали для сопоставления цветов, включало подвыражение \b[a-zA-Z]+\b(?!-) для сопоставления именованных цветов, например ключевого слова red . Затем мы проверили, действительно ли совпавшая часть является именованным цветом, и угадайте, что: tan тоже является именованным цветом! Итак, мы ошибочно интерпретировали выражения tan() как цвета.

Соответствие var()

Давайте посмотрим на другой пример, функции var() с резервной копией, содержащей другие ссылки var() : var(--non-existent, var(--margin-vertical)) .

Наше регулярное выражение для var() с радостью соответствует этому значению. За исключением того, что сопоставление прекращается при первой закрывающей скобке. Таким образом, приведенный выше текст сопоставляется как var(--non-existent, var(--margin-vertical) . Это хрестоматийное ограничение сопоставления регулярных выражений. Языки, требующие совпадающих круглых скобок, принципиально не являются регулярными.

Переход на CSS-парсер

Когда анализ текста с использованием регулярных выражений перестает работать (поскольку анализируемый язык не является регулярным), есть канонический следующий шаг: использовать синтаксический анализатор для грамматики более высокого типа. Для CSS это означает синтаксический анализатор контекстно-свободных языков. Фактически, такая система парсера уже существовала в кодовой базе DevTools: CodeMirror's Lezer , которая является основой, например, для подсветки синтаксиса в CodeMirror, редакторе, который вы найдете на панели «Источники» . Синтаксический анализатор CSS Лезера позволял нам создавать (неабстрактные) синтаксические деревья для правил CSS и был готов к использованию. Победа.

Синтаксическое дерево для значения свойства `hsl(177deg var(--saturation, 100%) 50%)`. Это упрощенная версия результата, полученного парсером Lezer, без учета чисто синтаксических узлов для запятых и круглых скобок.

За исключением того, что сразу после установки мы обнаружили, что невозможен переход от сопоставления на основе регулярных выражений к сопоставлению на основе синтаксического анализатора: эти два подхода работают в противоположных направлениях. При сопоставлении частей значений с регулярными выражениями DevTools сканировал входные данные слева направо, неоднократно пытаясь найти самое раннее совпадение из упорядоченного списка шаблонов. В синтаксическом дереве сопоставление начинается снизу вверх, например, сначала анализируются аргументы вызова, прежде чем пытаться сопоставить вызов функции. Думайте об этом как о вычислении арифметического выражения, где вы сначала рассматриваете выражения в скобках, затем мультипликативные операторы, а затем аддитивные операторы. В этом случае сопоставление на основе регулярных выражений соответствует вычислению арифметического выражения слева направо. Нам действительно не хотелось переписывать всю систему сопоставления с нуля: существовало 15 различных пар сопоставителей и рендереров с тысячами строк кода, что делало маловероятным, что мы сможем выпустить ее за один этап.

Поэтому мы нашли решение, которое позволило нам вносить поэтапные изменения, о которых мы более подробно опишем ниже. Короче говоря, мы сохранили двухэтапный подход, но на первом этапе мы пытаемся сопоставить подвыражения снизу вверх (таким образом разрывая поток регулярных выражений), а на втором этапе мы выполняем рендеринг сверху вниз. На обоих этапах мы могли использовать существующие средства сопоставления и рендеринга на основе регулярных выражений практически без изменений и, таким образом, могли переносить их один за другим.

Этап 1. Сопоставление снизу вверх.

Первая фаза более или менее точно и исключительно выполняет то, что написано на обложке. Мы проходим по дереву снизу вверх и пытаемся сопоставить подвыражения в каждом узле синтаксического дерева, который мы посещаем. Чтобы сопоставить определенное подвыражение, средство сопоставления может использовать регулярное выражение, как это делалось в существующей системе. Начиная с версии 128, в некоторых случаях мы все еще делаем это, например, для сопоставления длин . Альтернативно, сопоставитель может проанализировать структуру поддерева, корнем которого является текущий узел. Это позволяет ему одновременно обнаруживать синтаксические ошибки и записывать структурную информацию.

Рассмотрим пример синтаксического дерева выше:

Этап 1: Сопоставление снизу вверх в синтаксическом дереве.

Для этого дерева наши сопоставители будут применяться в следующем порядке:

  1. hsl( 177deg var(--saturation, 100%) 50%) : Сначала мы обнаруживаем первый аргумент вызова функции hsl — угол оттенка. Мы сопоставляем его с помощью средства сопоставления углов, чтобы можно было украсить значение угла значком угла.
  2. hsl(177deg var(--saturation, 100%) 50%) : Во-вторых, мы обнаруживаем вызов функции var с помощью средства сопоставления var. Для таких вызовов мы в основном хотим сделать две вещи:
    • Найдите объявление переменной и вычислите ее значение, а затем добавьте ссылку и всплывающее окно к имени переменной для подключения к ним соответственно.
    • Украсьте вызов цветным значком, если вычисленное значение является цветом. На самом деле есть и третья вещь, но об этом мы поговорим позже.
  3. hsl(177deg var(--saturation, 100%) 50%) : Наконец, мы сопоставляем выражение вызова для функции hsl , чтобы можно было украсить его цветным значком.

Помимо поиска подвыражений, которые мы хотели бы украсить, на самом деле есть вторая функция, которую мы запускаем как часть процесса сопоставления. Обратите внимание, что на шаге №2 мы сказали, что ищем вычисленное значение имени переменной. Фактически, мы делаем еще один шаг вперед и распространяем результаты вверх по дереву. И не только для переменной, но и для резервного значения! Гарантируется, что при посещении узла функции var его дочерние элементы были посещены заранее, поэтому мы уже знаем результаты любых функций var , которые могут появиться в резервном значении. Поэтому мы можем легко и дешево заменять функции var их результатами на лету, что позволяет нам тривиально отвечать на вопросы типа «Является ли результат этого вызова var цветом?», как мы это делали на шаге №2.

Этап 2. Рендеринг сверху вниз.

На втором этапе мы меняем направление. Взяв результаты сопоставления из этапа 1, мы преобразуем дерево в HTML, просматривая его сверху вниз. Для каждого посещенного узла мы проверяем, соответствует ли он, и если да, вызываем соответствующий рендерер сопоставителя. Мы избегаем необходимости специальной обработки узлов, содержащих только текст (например, NumberLiteral «50%)», включая средство сопоставления и средство визуализации по умолчанию для текстовых узлов. Средства визуализации просто выводят узлы HTML, которые, собранные вместе, создают представление значения свойства, включая его украшения.

Этап 2: Рендеринг сверху вниз по синтаксическому дереву.

Для примера дерева приведен порядок, в котором отображается значение свойства:

  1. Посетите вызов функции hsl . Он совпал, поэтому вызовите функцию рендеринга функции цвета. Он делает две вещи:
    • Вычисляет фактическое значение цвета, используя механизм оперативной подстановки для любых аргументов var , а затем рисует значок цвета.
    • Рекурсивно отображает дочерние элементы CallExpression . Это автоматически обеспечивает отображение имени функции, круглых скобок и запятых, которые представляют собой просто текст.
  2. Посетите первый аргумент вызова hsl . Он совпал, поэтому вызовите средство рендеринга угла, которое рисует значок угла и текст угла.
  3. Посетите второй аргумент — вызов var . Он совпал, поэтому вызовите var renderer , который выведет следующее:
    • Текст var( в начале.
    • Имя переменной и украшает ее либо ссылкой на определение переменной, либо серым цветом текста, чтобы указать, что она не определена. Также к переменной добавляется всплывающее окно для отображения информации о ее значении.
    • Запятая, а затем рекурсивно отображает резервное значение.
    • Закрывающая скобка.
  4. Посетите последний аргумент вызова hsl . Он не совпал, поэтому просто выведите его текстовое содержимое.

Заметили ли вы, что в этом алгоритме рендеринг полностью контролирует процесс рендеринга дочерних элементов соответствующего узла? Рекурсивный рендеринг дочерних элементов является упреждающим. Этот трюк позволил осуществить поэтапный переход от рендеринга на основе регулярных выражений к рендерингу на основе синтаксического дерева. Для узлов, сопоставленных с устаревшим средством сопоставления регулярных выражений, соответствующий модуль визуализации можно использовать в его исходной форме. С точки зрения синтаксического дерева, он будет отвечать за рендеринг всего поддерева, а его результат (узел HTML) можно будет без проблем подключить к окружающему процессу рендеринга. Это дало нам возможность портировать средства сопоставления и средства визуализации парами и заменять их по одному.

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

Как вы можете видеть в приведенном выше примере, мы используем этот механизм и для других пар значков, например, для color-mix() и его двух цветовых каналов или функций var , которые возвращают цвет из его резервного варианта.

Влияние на производительность

Углубляясь в эту проблему с целью повышения надежности и устранения давних проблем, мы ожидали некоторого снижения производительности, учитывая, что мы начали использовать полноценный парсер. Для проверки этого мы создали тест, который отображает около 3,5 тыс. объявлений свойств, и профилировали версии на основе регулярных выражений и парсера с 6-кратным регулированием на машине M1.

Как мы и ожидали, в этом случае подход, основанный на синтаксическом анализе, оказался на 27% медленнее, чем подход, основанный на регулярных выражениях. Для рендеринга подхода на основе регулярных выражений потребовалось 11 секунд, а для рендеринга подхода на основе парсера — 15 секунд.

Учитывая преимущества, которые мы получаем от нового подхода, мы решили двигаться дальше.

Благодарности

Мы выражаем глубочайшую благодарность Софии Емельяновой и Джеселин Йен за неоценимую помощь в редактировании этого поста!

Загрузите предварительный просмотр каналов

Рассмотрите возможность использования Chrome Canary , Dev или Beta в качестве браузера для разработки по умолчанию. Эти каналы предварительного просмотра дают вам доступ к новейшим функциям DevTools, тестируют передовые API-интерфейсы веб-платформы и находят проблемы на вашем сайте раньше, чем это сделают ваши пользователи!

Связь с командой Chrome DevTools

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