Парсим css
TRANSCRIPT
Парсим CSSРоман Дворнов
AvitoЕкатеринбург, сентябрь 2016
Руководитель фронтенда в Avito
Основной интерес – SPA
Open source:basis.js, CSSO, component-inspector, csstree и другие
За любую движуху, кроме голодовки ;)
Зачем парсить CSS?
Много CSS, пишут люди, много спек, разная поддержка браузерами, все
меняется и т.д.
4
Modern CSS is hard
Инструменты• Пре-/пост-процессинг • Форматирование • Валидация и линтирование • Подсказки • Генерация гайдбуков, тестов • Оптимизация и сжатие • …
5
Для всего этого нужно хорошо понимать структуру CSS
6
.rule { property: value;}
7
CSS в вакууме – все просто
👌
Мы все еще недостаточно знаем о CSS
8
9
Светлая сторонаВсе хорошо знают и часто используют
Знают очень опытные или копипаста
Темная сторонаТайное экспертное знание, может даже (пока) нигде не работаетНовое
CSS
Уже с нами
CSS не так прост• at-rules @media, @supports, @font-face, …
• необязательные кавычки url(…), [name=value], …
• сложные функции calc(), var(), attr(), …
• экранирование
• bad-url, bad-string…
• …
10
11
#\31 \ \+\ 2 { content: "hello\world"; background: url(p\(4\).jpg)}
Валидный ли CSS?
11
#\31 \ \+\ 2 { content: "hello\world"; background: url(p\(4\).jpg)}
Валидный ли CSS?
Да, совершенно валидный! Э – экранирование
12
#\31 \ \+\ 2 { content: "hello\world"; background: url(p\(4\).jpg);}
Экранирование
drafts.csswg.org/cssom/#the-css.escape()-method
В идентификаторах, именах классов, атрибутов и т.д.
можно использовать любые символы, если экранировать, даже придумали метод для
этого – CSS.escape()
CSS.escape('1 + 2') === '\31 \ \+\ 2'
13
#\31 \+2 { content: "hello\world"; background: url(p\(4\).jpg);}
Экранирование
www.w3.org/TR/CSS22/grammar.html#grammar
Можно экранировать перевод строки в строках, прям как в JavaScript ;)
14
#\31 \+2 { content: "hello\world"; background: url(p\(4\).jpg);}
Экранирование
www.w3.org/TR/CSS22/grammar.html#grammar
Можно экранировать специальные символы
в url()
15
ЭкранированиеБраузер поймет правильно
А вот парсер инспектора стилей в Chrome DevTools написан плохо
16
.simple { --color: green; border: 1px solid var(--color, red);}
CSS Custom Properties
www.w3.org/TR/css-variables/#using-variables
17
.complex { --bg: url('..') no-repeat, bottom 10px right url(..); background: var(--bg, bottom url('..'), left), red;}
CSS Custom Properties
Переменная может вставить любую часть значения, даже запятые, как и fallback
www.w3.org/TR/css-variables/#using-variables
18
@supports (display: flex) { .simple { display: flex; }}
@supports
19
@supports (display: flex) { .simple { display: flex; }}
@supports
Декларация
20
@supports (display: flex) { .simple { display: flex; }}
@supports
Декларация
Тоже декларация
21
@supports (background: top calc(25% + 2px) !important) { …}
@supports
Браузер может не верно ответить на вопрос, но синтаксис разрешает все,
что можно в декларации, даже !important
www.w3.org/TR/css3-conditional/#at-supports
Синтаксис CSS меняется• Расширяется
• Корректируется
• Добавляются новые конструкции
22
23
:not(a.foo) { ... }:not(a):not(b) { ... }
:not()Было можно указывать только простые селекторы
www.w3.org/TR/css3-selectors/#negation
24
:not(a, b) { ... } /* :not(a):not(b) */img:not(a *) { ... } /* изображение не внутри ссылки */
:not()CSS Selectors Level 4 разрешил список любых селекторов, уже поддерживается в Safari 9+
drafts.csswg.org/selectors-4/#negationtinyurl.com/Webkit-Complex-Selectors
25
:nth-child(1 of p img) { ... } /* первый <img> в <p> */:nth-last-child(2n + 1 of .foo, .bar) { ... }
:nth-child()/:nth-last-child()CSS Selectors Level 4 и тут разрешил список любых селекторов, тоже поддерживается в Safari 9+
tinyurl.com/Webkit-Complex-Selectorsdrafts.csswg.org/selectors-4/#the-nth-child-pseudo
26
.attr { width: attr(name %, var(--default-width, calc(50% + 2em))); +}
attr()CSS Values and Units Module Level 4 разрешает attr() в любом месте + тип или единица изменения + fallback значение
drafts.csswg.org/css-values/#attr-notationПока никем не поддерживается и в состоянии at risk
Самое «вкусное»• Баги браузеров
• Хаки _property, value !ie, \9, …
• Легаси префиксы, filter, expression(), …
27
28
_property: value; // IE6property: value !ie; // IE ≤ 7property: value\9; // IE 6-8
// Chrome ≤ 28 Safari ≤ 7.selector { (;property: value;); }.selector { [;property: value;]; }
// Chrome 22-28 Safari ≥ 7@media \\0 screen {}
browserhacks.com
Проблема разбора CSS является комплексной и имеет множество подводных камней
29
Написать собственный парсер CSS становится все сложнее
30
Инструментам нужно корректное и детальное описание структуры CSS
31
CSS парсеры
Парсеров CSS на JavaScript достаточно много
33
Частые проблемы• Заброшены и не развиваются
• Устарели (не поддерживают новое в CSS)
• Содержат ошибки
• Неудачная структура
• Медленные34
Плюсы PostCSS• Развивается и поддерживается
• Хорошо справляется с синтаксисом CSS и даже будущим + tolerant mode
• Сохраняет информацию о форматировании
• Удобное API для работы с AST
• Быстрый36
Основная проблема: селекторы и значения свойств остаются не разобранными
37
Это вынуждает разработчиков
• Использовать костыли
• Писать свои парсеры
• Использовать дополнительные парсеры:postcss-selector-parser postcss-value-parser
38
39
// !singlequotes|!doublequotes|!url()|pixelunitvar pxRegex = /"[^"]+"|'[^']+'|url\([^\)]+\)|(\d*\.?\d+)px/ig;
css.walkDecls(function (decl, i) { if (opts.propWhiteList.indexOf(decl.prop) === -1) return; … decl.value = decl.value.replace(pxRegex, function (m, $1) { if (!$1) return m; var pixels = parseFloat($1); if (pixels < minPixelValue) return m; return toFixed((pixels / rootValue), unitPrecision) + 'rem'; });});
Кто во что горазд
postcss-pxtorem
CSSTree
Основные цели• Правильный разбор с учетом семантики CSS
• Детальное, корректное и удобное AST
• Удобное API для работы с AST
• Сформировать«стандарт» для CSS AST на подобии ESTree
• Быстрый и экономный41
Немного истории
Чуть меньше года назад я стал мейнтейнером CSSO
(минификатор CSS)
43
github.com/css/csso
Проблемы• Не развивается с 2013
• Неудобный формат AST, местами странный
• Много ошибок
• Запутанная и сложная кодовая база
• Медленный, потребляет много памяти, GC45
46
[ "stylesheet", [ "atrules", [ "atkeyword", [ "ident", "import" ] ], [ "s", " " ], [ "string", "'path/to/file.css'" ] ]]
Было: AST – массив массивов
Парсер – последнее, что я собирался трогать, но…
он был полностью переписан (так получилось 😳)
47
48
{ "type": "StyleSheet", "rules": [{ "type": "Atrule", "name": "import", "expression": { "type": "AtruleExpression", "sequence": [ ... ] }, "block": null }]}
Стало: AST – дерево объектов
Скорость
CSSO – история ускорения (в том числе про парсер)
51
tinyurl.com/csso-speedup
После выступления разогнал парсер еще :)
52
* Вдохновленный общением с Вячеславом @mraleph Егоровым
Хотите чтобы ваш JavaScript работал так же быстро как Си, сделайте его похожим на Си
53
Немного из того, что сделано• Максимум string -> number • Массив объектов -> несколько TypedArray • Отказ от RegExp, toLowerCase(), … • array -> bi-directional lists • immutable objects • …
54
55
CSSTree: 13 msMensch: 31 msCSSOM: 36 msPostCSS: 38 msRework: 81 msPostCSS Full: 100 msGonzales: 175 msStylecow: 176 msGonzales PE: 214 msParserLib: 414 ms
bootstrap.css v3.3.7 (146Kb)
github.com/postcss/benchmark
Не детальное AST
Детальное AST
PostCSS Full = PostCSS
+ postcss-selector-parser + postcss-value-parser
56
CSSTree: 130 msPostCSS: 234 msCSSOM: 250 msMensch: 381 msRework: 507 msPostCSS Full: 571 msStylecow: 1156 msGonzales: 1202 msGonzales PE: 1646 msParserLib: 5509 ms
actiagent.ru (983Kb)
github.com/postcss/benchmark
Разбор AST
58
csstree.walk(function (node) { var property = this.declaration && this.declaration.property.name; if (opts.propWhiteList.indexOf(property) === -1) return;
if (node.type === 'Dimension' && node.unit === 'px') { var pixels = parseFloat(node.value); if (pixels >= minPixelValue) { node.value = toFixed((pixels / rootValue), unitPrecision); node.unit = 'rem'; } }});
Почему важно детальное ASTКак мог бы выглядеть postcss-pxtorem
59
background: top 100% url(..) no-repeat #008800;
Identifier Url Identifier HexPercentage
При обычном разборе мы знаем
60
background: top 100% url(..) no-repeat #008800;
background-position
background-image
background-repeat
background-color
Хотелось бы понимать больше
Как Вы понимаете за что отвечает та или иная часть
значения?
61
А браузер?
62
В спецификации
63
В спецификации
63
Грамматика описана в
CSS Values and Units Module
похоже на RegExp, только проще
64
drafts.csswg.org/css-values/#value-defs
Основная идея: Выдергивать из спецификаций синтаксис значений, разбирать
его и мапить на AST
65
Выдергивать не пришлось
Mozilla Template:CSSData
66
developer.mozilla.org/en-US/docs/Template:cssdata
Год шел к имплементации…
Запилил за несколько дней :)
67
Еще несколько дней ушло на исправление ошибок в синтаксисах,
актуализации и дополнении
68
github.com/csstree/csstree/blob/master/data/patch.json
Получившийся патч (181 синтаксис)
К сожалению, пока результат мапинга не тот, что ожидался…
69
Зато получилось немало интересного
😋
70
71
csstree.github.io/docs/syntax.html
Документация синтаксиса
72
csstree.github.io/docs/validator.html
Валидатор синтаксиса CSS значений
73
var csstree = require('css-tree');var syntax = csstree.syntax.defaultSyntax;var ast = csstree.parse('… your css …');
csstree.walkDeclarations(ast, function(node) { if (!syntax.match(node.property.name, node.value)) { console.log(syntax.lastMatchError); }});
Свой валидатор в 8 строк
74
var csstree = require('css-tree');var mySyntax = csstree.syntax.сreate({ generic: true, // чтобы добавились примитивы вроде <ident>, <number> etc types: { name: '<ident>', role: 'organizer | sponsor | speaker | attendee | volunteer', person: '<name> <role>' }, properties: { FrontTalks: '<person>#' }});
mySyntax.match('fronttalks', 'Oleg organizer, Roman speaker'); // okmySyntax.match('fronttalks', 'Roman'); // Mismatch
Cобственный синтаксис
css-values vs. csstree.syntax
76
с марта 2016
postcss, ES6 и т.д.
70% (254 из 361) свойств Mozilla Template:CSSData
с 4 сентября 2016 :)
csstree, ES5
100% свойств Mozilla Template:CSSData
+ 80 свойств + исправления
+ легаси + …
Кое что еще
• csstree-validator – npm пакет + консольная команда
• stylelint-csstree-validator – плагин для stylelint
• gulp-csstree – плагин для gulp
• SublimeLinter-contrib-csstree – плагин для Sublime Text
• vscode-csstree – плагин для VS Code
More is coming…77
(запилили вчера, не шутка ;)
Заключение
Подробное и правильное AST – проще код инструментов, работающие в большинстве
случаев
79
Планы: парсер• Исправить/улучшить парсинг/AST
• Возможность расширять/патчить основной синтаксис
• Возможность задавать уровень детализации
• Использовать заданный синтаксис значений при разборе (опционально)
• Оптимизировать/ускорить еще
• …80
Планы: синтаксис• Предоставить удобный формат сопоставления
• Оптимизировать/ускорить
• Транспиляция синтаксиса в код
• Интеграции, плагины…
• …
81
Уже есть положительный эффект• Найдены ошибки в Mozilla Template:CSSData
https://bugzilla.mozilla.org/show_bug.cgi?id=1300329
• Найдена ошибка в спекеhttps://github.com/w3c/csswg-drafts/issues/458
• Появляются вопросы к спекамhttps://github.com/w3c/csswg-drafts/issues/461
• Появляются инструменты для валидации синтаксисов, в том числе будущих
82