Всем привет!
Продолжаем цикл статей по разработке браузерного движка.
Да, лучше поздно, чем никогда. Да, перерыв был большой.
В конце статьи я опишу, как поживает проект lexbor
, что с ним происходит.
В этой статье я постараюсь раскрыть особенности парсинга Cascading Style Sheets (CSS). Расскажу, как вывернуть «ежа» наизнанку и как тестировать полученный результат.
В CSS спецификациях всё разжевано, ну, или почти всё, тут я расскажу, как всё устроено, куда смотреть и с чего начать.
Эта статья больше обзорная, здесь не будет мелких подробностей реализации, скорее, общие сведения и основные алгоритмы. За мельчайшими подробностями прошу в код на GitHub.
И конечно, как это обычно бывает, мы замахнёмся на звание самого быстрого парсера CSS.
Куда смотреть?
Есть два источника CSS спецификаций:
- https://www.w3.org/TR/ — консорциум, который «все» интернет-спецификации держит.
- https://drafts.csswg.org/ — все черновики (Drafts) последних спецификаций. Это тоже W3 только с другого бока.
Мы с вами будем работать с drafts.csswg.org.
Там всё лаконично, в каждом модуле есть ссылки на все версии модуля – от черновика до рекомендованного к использованию.
Как устроено?
CSS устроен в виде модулей: Syntax, Namespaces, Selectors, CSSOM, Values и так далее. Полный список можно найти на csswg.org.
Каждый модуль, точнее, определённая его версия, имеет статус: Working Draft, Candidate Recommendation, Recommendation и так далее, все этапы можно посмотреть на w3.org.
Говоря народным языком, в каждом модуле есть пометка «рекомендован к использованию», только вот начали писать – и неясно, что из этого выйдет, может, вообще удалим этот модуль, и всё в этом духе, только более лаконично.
Нас же интересует только Editor’s Draft и Working Draft с оглядкой на Recommendation. Если честно, то W3 – те ещё тормоза, и пока у них до Recommendation дойдет (миллиарды звезд сгорят в космосе), то уже устареет всё. То есть мы будем относиться к CSS стандартам, как к живым (living), так же, как устроен HTML стандарт.
Приступим к основам
Из всех модулей CSS есть основные, без которых никак, это БАЗА.
Syntax
Модуль синтаксиса является основой. В нём описывается структура CSS, принципы построения токенов и дальнейший их парсинг.
Values
Модуль описывает, как устроена грамматика. Во всех модулях мы будем сталкиваться с грамматиками и основными типами CSS.
К примеру, для свойства width
грамматика имеет вид:
width = auto | <length-percentage [0,∞]> | min-content | max-content | fit-content(<length-percentage [0,∞]>)
Помимо этого, в модуле описываются основные типы и математические функции:
<length>
, <angle>
, <time>
, min()
, max()
и так далее.
Рассмотрим, как раскладывается тип <length-percentage>
:
<length-percentage> = <length> | <percentage-token>
<length> = <dimension-token> | <number-token [0,0]>
Все типы, заканчивающиеся на -token
, являются токенами, полученными из токенизатора, то есть несут в себе разного рода данные.
В рамках данной статьи мы не будем рассматривать каждый тип. Вы всё найдете в указываемых мной спецификациях. Я описываю общую картину.
CSSOM
У HTML есть DOM, у CSS есть CSSOM. Объектная модель, дерево. Я бы не сказал, что это основа, просто представление CSS Style Sheets в виде набора объектов в дереве. Иначе говоря, даёт возможность управлять «миром» через JavaScript.
Всё это основы, чтобы не пугаться остальных модулей и понимать, чего в них написано. Ну, и чтобы понимать, с чем неравный бой мы будем вести дальше.
Syntax
: Tokenizer
Собственно, как и везде, токенизатор принимает поток данных и разбивает его на токены.
Рассмотрим пример, возьмём CSS:
div {width: 10px !important}
Токенизатор создаст токены:
"div" — <ident-token>
" " — <whitespace-token>
"{" — <left-curly-bracket-token>
"width" — <ident-token>
":" — <colon-token>
" " — <whitespace-token>
"10px" — <dimension-token>
" " — <whitespace-token>
"!" — <delim-token>
"important" — <ident-token>
"}" — <right-curly-bracket-token>
Все виды токенов и алгоритм их создания вы найдете в спецификации. Тут ничего уникального.
Syntax
: Parsing
Парсинг накапливает токены в определённые структуры для передачи на дальнейшую обработку в различные модули CSS.
Определённая структура – это:
- Stylesheet (ещё недавно называлась List of Rules)
- At-Rule
- Qualified Rule
- Block’s contents
- Declaration
- Component value
- Simple block
- Function
Для примера чуть подробнее рассмотрим Qualified Rule
.
Опять возьмём наш пример:
div {width: 10px !important}
Qualified Rule
может иметь прелюдию
(Prelude
) и правила
(Rules
).
Здесь:
div
— это прелюдияPrelude
.{width: 10px !important}
— это список правилRules
.
В данном случае:
Prelude
содержит в себеComponent value
.Rules
содержит в себеLists of Declarations
.
width: 10px !important
— это декларация. Когда их много, то это список деклараций (Lists of Declarations
).
Соответственно, декларация содержит в себе:
width
— имяname
10px
— значениеvalue
!important
—important
флаг.
Обычные пользователи описали бы это так:
div
— CSS Selectors{width: 10px !important}
— список CSS свойств.
Собственно, ничего сложного. Остальные структуры устроены примерно так же.
Например, Stylesheet
содержит в себе Rules
, а именно: At-Rule
и Qualified Rule
в любом количестве.
Как мы уже поняли, созданные структуры со списками токенов должны кем-то в дальнейшем обрабатываться.
Этот кто-то – другие CSS модули.
К примеру, модулю CSS Selectors передаются данные из Qualified Rule
Prelude
.
Модулю Media Queries на парсинг передаётся вся At-Rule
структура. Каждому своё!
Теория закончена, начинаем жизнь
Давайте возьмём немало известный bootstrap.css
, вес которого ≈180KB
.
Из этого файла токенизатор создаст ≈51700
токенов без учёта комментариев. Количество не маленькое.
А теперь давайте представим, что мы первым циклом по ним проходим, когда формируем структуру в синтаксисе, вторым и последующими – разбираем по модулям.
Конечно, последующие циклы будут по меньшему количеству токенов, будут отброшены {
, }
, [
, ]
и какие-то ещё, в зависимости от ситуации.
Вот тут мы и начинаем задумываться с вами над вопросом: а как оптимизировать?!
Вариантов реализовать оптимизированный CSS парсер немало, рассмотрим основные.
SAX Style
Мы можем установить обратные вызовы (callbacks
) на все стадии CSS Syntax структур и передавать туда токены.
Выглядеть это будет примерно так:
div {width: 10px !important}
"div" — callback_qualified_rule_prelude(<ident-token>)
" " — callback_qualified_rule_prelude(<whitespace-token>)
— callback_qualified_rule_prelude(<end-token>)
"{" — skip <left-curly-bracket-token>
"width" — callback_declaration_name(<ident-token>)
":" — skip <colon-token>
" " — skip <whitespace-token>
"10px" — callback_declaration_value(<dimension-token>)
" " — skip <whitespace-token>
"!" — skip <delim-token>
"important" — skip <ident-token>
- callback_declaration_important(true)
"}" — skip <right-curly-bracket-token>
Как видим из небольшого примера, обратных вызовов будет много. Следить за всем этим зоопарком будет сложно.
В данном варианте очень многое перекладывается на пользователя.
Также немалой проблемой является отсутствие возможности посмотреть следующий токен. Вот тут кто-то может поспорить и сказать: «Я же могу позвать токенизатор и спросить у него следующий токен, не нарушая общего алгоритма». Ответ будет простой: нельзя так делать, мы не будем знать, относится следующий токен к нашей, текущей, стадии или нет. Соответственно, мы пытаемся взять на себя двойную работу – ещё раз анализировать структуру CSS.
Плюсы:
- Фиксированная память для работы токенизатора и парсера.
- Парсер будет заниматься исключительно отслеживанием структуры и вызовом обратных вызовов.
- Легко реализовать поддержку парсинга частями (chunks).
Минусы:
- Сложность в поддержке для пользователя. Многое перекладывается на пользователя.
- Невозможность получить следующий токен.
Безумная реализация — самая быстрая
А давайте каждый модуль сам будет следить за структурой CSS!
Вообще всё переложим на пользователя, пусть разбирается, ему виднее.
Если пользователь начал разбор CSS Selectors, то, помимо самой грамматики селекторов, он должен отслеживать вложенность по токенам: {
, }
, (
, )
, function(
.
Как-то так это будет выглядеть:
div {width: 10px !important}
"div {" — Selectors parse
"width: 10px !important}" — Declarations parse
Парсинг селекторов будет происходить следующим образом:
- Разбираем по грамматике селекторов.
- Проверяем каждый токен. Является ли токен
{
при текущей глубине вложенности. - Если токен
{
, то переключаем на следующую стадию парсинга.
Звучит просто, но на деле всё сложнее:
- Знание о стадии для переключения придётся передавать каждому модулю, они об этом ничего не знают.
- Надо ли съедать токен
{
или}
перед передачей на следующую стадию? - Необходимо следить за глубиной вложенности. Нельзя просто передать управление следующему модулю при встрече, к примеру,
}
. А если было(})
? Всегда необходимо точно понимать, как описывается структура CSS в текущий момент. Это очень проблематично и в разы усложняет разработку, а уж отладку точно, если что-то пойдёт не так. - Часто, если возникла ошибка парсинга, то возникла она не там, где остановился парсер или допустил ошибку. Скорее всего, ошибка происходит значительно раньше: какой-то модуль неверно подсчитал вложенность или захватил лишний токен. В общем, поддержка этого варианта очень сложна.
Плюсы:
- Полный контроль над токенизатором. Токены можно получать и смотреть наперёд.
- Скорость, самый быстрый вариант. Отсутствие каких-либо обратных вызовов, парсинг напрямую.
Минусы:
- Сложная разработка и поддержка.
Изначально я «игрался» с данным подходом к парсингу CSS.
Идея была такая:
- Не писать всё вручную, а создать генератор Си кода по грамматикам.
- Научить генератор кода следить за глобальной CSS структурой.
То есть отдаём генератору кода грамматику модуля Selectors, а он генерирует Си код парсинга с учётом глобальной CSS структуры, так сказать, подмешивает в грамматику Selectors понимание структуры.
Я достиг неплохих результатов в этом подходе, но решил завязать с этим направлением. Создание такого генератора кода – задача очень большая и непростая, но интересная. Одна стадия оптимизации чего стоит.
Мною было решено вернуться к этому в тот момент, когда проект lexbor
будет масштабно использоваться и реально потребуется сильный буст в скорости.
Ёж наизнанку
Мы уже определились, что для нас очень важна возможность управлять токенами, получать их самостоятельно.
Когда мы говорим про парсинг каких-либо данных, то всегда представляем чёткую последовательность:
- Токенизатор создает токены.
- Парсер их обрабатывает.
Всё логично.
А что, если нам заставить токенизатор следить за глобальной структурой CSS?
То есть при запросе токена у токенизатора он будет где-то внутри отслеживать CSS структуру.
Такой некий парсинг наизнанку.
Данный подход реализован в моём проекте lexbor
.
Принцип такой:
Мы задаем обратные вызовы для разных стадий парсинга CSS структуры.
На каждой стадии обратный вызов зовётся только один раз. Если началась прелюдия, то зовётся обратный вызов начала. Не на каждый токен, а только на начало.
Хорошо, но а как же отслеживать структуру в наших обратных вызовах?
Мы создадим несколько функций, прокси-функции для функций токенизатора. Функцию получения токена lxb_css_syntax_parser_token()
и функцию потребления токена lxb_css_syntax_parser_consume()
. Пользователь будет обращаться не к токенизатору напрямую, а к нашим прокси-функциям.
Алгоритм работы функции lxb_css_syntax_parser_token()
прост:
- Получаем токен из токенизатора.
- Анализируем токен в структуре CSS.
- Возвращаем пользователю токен как он есть, если он принадлежит к текущей стадии парсинга, иначе возвращаем токен окончания данных
LXB_CSS_SYNTAX_TOKEN__TERMINATED
.
При передаче пользователю токена окончания данных LXB_CSS_SYNTAX_TOKEN__TERMINATED
анализатор CSS структуры переходит в стадию ожидания решения от пользователя. Решений может быть несколько:
lxb_css_parser_success()
— всё прошло успешно, данные, которые мы ожидали.lxb_css_parser_failed()
— не ожидаемые нами данные.lxb_css_parser_memory_fail()
— случилась ошибка выделения памяти, заканчиваем парсинг.lxb_css_parser_stop()
— просто останавливаем дальнейший парсинг.
Иначе говоря, если пользователь находится в какой-либо стадии парсинга, например, в стадии Prelude
у Qualified Rule
, то он из неё не выйдет, и анализатор структуры CSS не переключится дальше, пока пользователь не вернёт success
или failed
.
При этом функции принятия решений можно вызвать сразу. Если вызвать lxb_css_parser_success()
, а в текущей стадии будут токены, не относящиеся к LXB_CSS_SYNTAX_TOKEN_WHITESPACE
, то текущая стадия автоматически перейдёт в failed
. Это удобно тем, что нам не нужно проверять все токены, пока не придёт LXB_CSS_SYNTAX_TOKEN__TERMINATED
. Если мы уверены, что в текущей стадии всё распарсили, то возвращаем lxb_css_parser_success()
, а если уж там будут ещё какие токены, то всё автоматически разрешится.
Итоговый алгоритм выглядит примерно так:
1. Пользователь желает парсить `Qualified Rule`. Выставляет callback-и на начало `Prelude`, на начало блока `Rules` и на окончание парсинга `Qualified Rule`.
2. Запускает парсинг.
3. Начало цикла.
3.1. Мы вызываем функцию получения токена `lxb_css_syntax_parser_token()`.
3.1.1. Получаем токен от токенизатора.
3.1.2. Анализируем в структуре CSS.
3.1.3. Парсеру выставляется пользовательский callback в зависимости от структуры CSS.
3.2. Мы вызываем установленный callback с передачей ему полученного токена.
3.3. Пользователь в callback-е выгребает все токены до `LXB_CSS_SYNTAX_TOKEN__TERMINATED`, используя функцию `lxb_css_syntax_parser_token()`.
3.4. Пользователь вернул нам управление, переходим к пункту 3.1.
Это очень упрощённая картина мира. Внутри функции lxb_css_syntax_parser_token()
есть фазы — это переключение между структурами CSS Qualified Rule
, At-Rule
и так далее + разные системные фазы. Также там есть свой стек, CSS структура рекурсивна, а мы не любим рекурсии.
Плюсы:
- Полное управление токенизатором.
- Скорость: всё происходит на лету.
- Безопасность для пользователя. Полностью соблюдается структура CSS.
- Простота разработки пользовательских парсеров.
Минусы:
- Я не нашел минусов, по моему суждению, это самый сбалансированный подход парсинга CSS.
Парсинг – это хорошо, как тестировать будем?
Чтобы понять масштаб проблемы, разберём, как устроен синтаксис грамматик. Значения в грамматиках могут иметь комбинаторы и мультипликаторы.
Комбинаторы
Прямой порядок:
<my> = a b c
<my>
может содержать следующие значения:
<my> = a b c
Одно значение из перечисленных:
<my> = a | b | c
<my>
может содержать следующие значения:
<my> = a
<my> = b
<my> = c
Одно или все значения из перечисленных в любом порядке:
<my> = a || b || c
<my>
может содержать следующие значения:
<my> = a
<my> = a b
<my> = a c
<my> = a b c
<my> = a c b
<my> = b
<my> = b a
<my> = b c
<my> = b a c
<my> = b c a
<my> = c
<my> = c a
<my> = c b
<my> = c a b
<my> = c b a
Все значения из перечисленных в любом порядке:
<my> = a && b && c
<my>
может содержать следующие значения:
<my> = a b c
<my> = a c b
<my> = b a c
<my> = b c a
<my> = c a b
<my> = c b a
Значения можно группировать:
<my> = [ [a | b | c] && x ] r t v
Мультипликаторы
Кто знаком с regex, тот поймет сразу.
Ноль или бесконечное число раз:
<my> = a*
<my>
может содержать следующие значения:
<my> = a
<my> = a a a a a a a a a a a a a
<my> =
Один или бесконечное число раз:
<my> = a+
<my>
может содержать следующие значения:
<my> = a
<my> = a a a a a a a a a a a a a
Может присутствовать, а может нет:
<my> = a?
<my>
может содержать следующие значения:
<my> = a
<my> =
Может присутствовать от A
до B
раз, период:
<my> = a{1,4}
<my>
может содержать следующие значения:
<my> = a
<my> = a a
<my> = a a a
<my> = a a a a
Один или бесконечное число раз, разделённые запятой:
<my> = a#
<my>
может содержать следующие значения:
<my> = a
<my> = a, a
<my> = a, a, a
<my> = a, a, a, a
Обязательно одно значение должно быть:
<my> = [a? | b? | c?]!
В данном примере внутри группы допускается отсутствие значения, но восклицательный знак !
требует хотя бы одно, иначе ошибка.
Мультипликаторы могут комбинироваться:
<my> = a#{1,5}
Значения a
через запятую, от одного до пяти.
Это всё, что надо знать про грамматику, самое важное.
Теперь взглянем на синтаксис грамматики цвета:
<color> = <absolute-color-base> | currentcolor | <system-color>
<absolute-color-base> = <hex-color> | <absolute-color-function> | <named-color> | transparent
<absolute-color-function> = <rgb()> | <rgba()> |
<hsl()> | <hsla()> | <hwb()> |
<lab()> | <lch()> | <oklab()> | <oklch()> |
<color()>
<rgb()> = [ <legacy-rgb-syntax> | <modern-rgb-syntax> ]
<rgba()> = [ <legacy-rgba-syntax> | <modern-rgba-syntax> ]
<legacy-rgb-syntax> = rgb( <percentage>#{3} , <alpha-value>? ) |
rgb( <number>#{3} , <alpha-value>? )
<legacy-rgba-syntax> = rgba( <percentage>#{3} , <alpha-value>? ) |
rgba( <number>#{3} , <alpha-value>? )
<modern-rgb-syntax> = rgb( [ <number> | <percentage> | none]{3}
[ / [<alpha-value> | none] ]?)
<modern-rgba-syntax> = rgba([ <number> | <percentage> | none]{3}
[ / [<alpha-value> | none] ]?)
... там дальше много ещё...
С похмелья не разберёшься.
При этом парсер под это писать несложно, даже просто. Но вот писать все варианты тестов под это очень затруднительно.
А если представить, сколько разнообразных CSS деклараций, то совсем руки опускаются.
Логичная мысль — написать генератор тестов по грамматикам!
Это оказалось проще сказать, чем сделать. Не то чтобы прям рокетсайнс, но и человек с мороза призадумается.
Основные проблемы, с которыми я столкнулся:
Комбинаторные бомбы.
К примеру, если взять такую грамматику:
<text-decoration-line> = none | [ underline || overline || line-through || blink ]
<text-decoration-style> = solid | double | dotted | dashed | wavy
<text-decoration-color> = <color>
<text-decoration> = <text-decoration-line> || <text-decoration-style> || <text-decoration-color>
Формирование тестов для <text-decoration>
уйдёт в "бесконечность".
Все значения <color>
должны быть в разных вариантах соединены с <text-decoration-line>
и <text-decoration-style>
, а это очень-очень много.
Пришлось придумать ограничитель вариантов для группы — /1
. Косая черта и значение, сколько вариантов будет взято из группы.
В итоге <text-decoration>
был преобразован в:
<text-decoration> = <text-decoration-line> || <text-decoration-style> || <text-decoration-color>/1
При этом <color>
как свойство полностью формирует все свои тесты и проходит их отдельно.
Грамматики опускают пробелы (LXB_CSS_SYNTAX_TOKEN_WHITESPACE
).
Обычно ниже под грамматиками пишут, что такие-то и такие значения не должны иметь пробелы между собой.
Нас так не устраивает, нам необходимо это учесть непосредственно в грамматике.
Появился модификатор ^WS
(Without Spaces):
<frequency> = <number-token> <frequency-units>^WS
<frequency-units> = Hz | kHz
Перед <frequency-units>
пробелы недопустимы.
Порядок парсинга.
Порядок входных данных может быть произвольным, но после парсинга значения обретают свои позиции в структурах и при сериализации значения имеют точный порядок.
Для примера:
<x> = a && b && c
Тесты сформируются следующие:
<x> = a b c
<x> = a c b
<x> = b a c
<x> = b c a
<x> = c a b
<x> = c b a
Все эти тесты валидные, но наш результат после парсинга всегда будет <x> = a b c
. Возник вопрос: как сравнивать с остальными?
Внутреннее чувство подсказывало, что задача резко усложнилась, но чувство отваги (не слабоумия) подталкивало сделать всё с разбегу.
И как вы понимаете, с разбегу ворваться и реализовать не удалось. Пришлось думать!
Давайте посмотрим на такой пример:
<x> = a && [x || y || [f && g && h]] && c
Стало ясно, что результат формирования тестов для каждой группы должен возвращать не просто тест, но и правильный ответ на этот тест.
Это слегка стало проблемой. Реализация усложнилась.
Были перепробованы разные решения, я бы даже сказал, костыли, чтобы не усложнять код. Но все они приносили с собой кучу исключений и на 10% нерабочий код.
Казалось бы, назначить уникальный индекс по возрастанию, в зависимости от того, где находится значение, а в конце, когда тест сформирован, отсортировать индексы и получить результат для парсера. Вот вам и тест, и какой результат вернет парсер.
Тоже нет. У каждого значения есть свои Комбинаторы и Мультипликаторы, которые могут расставлять разные значения между значениями.
К примеру, где-то может быть пробел, где-то нет, где-то запятые между значениями. Если итоговый результат просто отсортировать, то получится каша.
В итоге самое надёжное решение только одно – формировать тест и результат отдельно. Иначе говоря, формирование результата будет проходить все те же стадии, что и формирование теста.
Накладно, конечно, но ладно, у нас же не реалтайм, мы можем и подождать.
В итоге получился замечательный инструмент, который генерирует тесты по грамматикам.
Сейчас тесты для CSS Declaration (свойства) генерируются за 1
секунду, и количество их 82911
. На диске занимают около 20MB
в json формате.
Согласитесь, писать вручную столько тестов – занятие времязатратное.
Данный подход позволил выявить немало проблем с парсингом свойств, думаю, ошибок 10 я поймал. Зато теперь есть 100% уверенность, что валидный CSS будет разобран верно.
И тут же возникает актуальный вопрос: а как тестировать не валидный CSS?
На данном этапе я активно использую Clang
фазер. Но, конечно, необходимо реализовать неправильные/сломанные тесты, используя грамматику. Чтобы из неправильных тестов не получался ложный правильный результат.
Вот так выглядит текущий список громматик для деклараций в проекте lexbor
.
Вообще, говоря о тестировании
Написание тестов для кода занимает столько же времени, сколько и его разработка, а то и больше.
Сейчас проект lexbor
проходит постоянное тестирование на примерно 30 операционных системах с включёнными asan
, msan
, UB
, memory leak
(где это возможно).
Также постоянно работают Clang
фазеры. Используется Cppcheck
.
Кстати, сегодня комманда PVS‑Studio порадовала, отсыпали годовой ключ на их статический анализатор кода. Буду активно использовать для тестирования проекта.
Каков статус проекта lexbor
?
Несмотря на долгий перерыв, а он был действительно долгим, проект активно развивается. Реализовано уже многое, сейчас идет реализация layout/renderer tree.
То есть скоро проект будет создавать окна, отрисовывать глифы, рисовать block, inline и так далее.
Вообще, кода столько, что статей на 5 хватит, если воду не лить, а то и больше.
Перерыв в разработке был связан с работой в NGINX, там я с головой ушёл в разработку JS движка NJS. Что только на пользу для проекта lexbor
в плане знаний.
К счастью, ко мне присоединился очень хороший человек (глыба английского и технической документации), помогает с документацией к проекту, взял на себя весь английский язык и вообще всё, что будет связано с документацией к проекту.
Где бенчмарки?
А их не будет. Точнее, сейчас не будет.
На данный момент проект lexbor
поддерживает только Selectors и Declarations (больше 70 свойст) для полного парсинга. Точнее не так, полностью синтаксис, а уже в нём Selectors и Declarations. Мне этого достаточно для дальнейшей разработки движка.
Как только появятся @media
, переменные и прочее, сразу же померимся «длиной органа» с другими.
Всё не поддерживаемое CSS парсером формируется в значение CUSTOM
. То есть в итоговом дереве все данные будут присутствовать, просто иметь структуру *_custom_t
.
Намекну лишь, что lightningcss, написанный на Rust, который позиционируется как «An extremely fast CSS parser», меня совсем не удивил.
Ссылки
Первая статья: HTML.
Ссылка на проект: lexbor.
Скромные примеры CSS: CSS Examples.
Примеры использования с HTML: HTML + CSS.