[Перевод] DRY CSS: Как использовать каждое объявление только один раз

f0be29bf794b63d2d10d88e498b59903.png


Использование DRY в CSS — это способ максимально избегать повторения в таблицах стилей. Этот подход не панацея, но он достаточно эффективен и является одним из основных методов оптимизации. Поскольку я использовал и изучал его почти 10 лет, в этой статье хочу поделиться своим опытом и знаниями.

А если вам будет интересна тема оптимизации CSS, то я рассказал об основах в моей небольшой книге «CSS Optimization Basics».

Основные шаги


Прежде чем мы углубимся в подробности, как «высушить» таблицы с помощью DRY, обратите внимание, что этот метод нетривиально автоматизировать из-за каскадности. Но, не смотря на это, я постараюсь ничего не упустить.

  1. Пишите CSS обычным и естественным образом.
  2. Определитесь с DRY-границами, что вы будете оптимизировать: раздел (функционально разделенные CSS-стили), файл, компонент или @media-запросы. Я работаю на уровне файл/@media, то есть обычно «сушу» всё в максимально возможном объеме.
  3. Убедитесь, что формат кода является консистентным, так как border:0;, border: 0; и border: none; означают одно и тоже, но это значительно усложняет поиск дубликатов.
  4. Найдите повторяющиеся объявления:
    1. Для новых стилей: после первоначальной инициализации.
    2. Для новых функций и исправлений: после завершения соответствующих действий.
    3. Совет: если для изменений в файле недостаточно подсказки системы версионирования, просто временно сделайте отступ для измененных объявлений для последующий проверки на уникальность.
  5. Разрешите повторяющиеся объявления:
    1. Проверьте каждое объявление (в новых таблицах стилей) или каждое изменённое объявление на предмет повторений в заданном диапазоне (если дедупликация ограничена отдельными разделами, сузьте поиск этими разделами).
    2. Для каждого повторного объявления (переход к действиям):
      • Определите, какое правило в таблице стилей должно быть первым (для этого вы должны определить, какой путь вы выбираете для сортировки селекторов).
      • Если первое правило содержит дополнительные объявления, которые еще не были проверены, то скопируйте всё правило и вставьте его после оригинала. Сохраните обнаруженный дубликат в первом правиле и удалите другие объявления, и сделайте наоборот во втором правиле, чтобы оно было похоже на старое правило, только без объявления, которое будет выполняться более одного раза.
      • Скопируйте селекторы других правил, которые содержат соответствующее объявление, в правило, идущее первым.
      • Обязательно удалите повторяющиеся объявления, селекторы которых были только что скопированы в таблицу стилей, и удалите правило, если оно состоит только из перемещенного дублирующего объявления.
      • (Повторите шаги).
    3. Убедитесь, что правила, обрабатывающие ранее повторяющиеся объявления, содержат селекторы в правильном порядке.
    4. Убедитесь, что правила, обрабатывающие ранее повторяющиеся объявления, имеют правильное расположение.


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

Особые случаи


Существует два сценария, требующих особого внимания:

  1. Разделение файлов: отдельные CSS-файлы могут быть полезны, особенно при повторной сборке в эксплуатацию, но когда дело доходит до «сушки» объявлений, они создают «жесткий» барьер: требуется много усилий для поиска и удаления повторяющихся объявлений. Если мы работаем с небольшой или средней кодовой базой на CSS, то может быть разумно перейти на одну таблицу стилей. Но когда мы имеем дело со сложными таблицами, то некоторое повторение допустимо.
  2. Строгость подхода или отступление от него: если мы строго избегаем повторений (то есть полностью хотим удалить дубликаты), мы всё равно будем иногда сталкиваться с исключениями. Эти исключения, кроме структурно-зависимых, таких как @media-запросы, файловые границы, или когда последовательность (каскад) является важной, будут вызывать проблемы с поддержкой. Селекторные хаки также являются исключением, поскольку некоторые селекторы работают таким образом, что фактически не позволяют пользовательскому агенту корректно интерпретировать соответствующее правило. В таких случаях мы не можем избежать совпадений, потому что при объединении соответствующих селекторов мы можем повлиять на их работу и результат будет некорректный.


Примеры


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

Оптимизация. Пример первый


Этот пример взят с сайта www.engadget.com, где мы обнаружили 92% повторений, случайный раздел, но по-крайней мере отсортированный в алфавитном порядке. Мы предполагаем, что порядок селекторов нас устраивает, и не будем менять и комментировать имена классов и т. д.

Код
.arrow-left {
  border-color: transparent;
  border-style: solid;
  border-width: 10px 10px 10px 0;
  height: 0;
  width: 0;
}

.arrow-down {
  border-bottom: 10px solid transparent;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-top: 10px solid #2b2d32;
  bottom: 0;
  height: 0;
  left: 20px;
  width: 0;
}

.faq-list .faq-item-title {
  cursor: pointer;
}

.faq-list .faq-item-title:after {
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid #111;
  content: '';
  display: inline-block;
  float: right;
  height: 0;
  opacity: .5;
  vertical-align: top;
  width: 0;
}

#contact input:focus,
#contact textarea:focus {
  border: 1px solid #3398db;
}

.flickity-slider>.table {
  table-layout: fixed;
}

::selection {
  background: #9b59b6;
  color: #fff;
}

.videoWrapper {
  height: 0;
  padding-bottom: 56.25%;
  position: relative;
}

.videoWrapper iframe {
  height: 100%;
  left: 0;
  position: absolute;
  top: 0;
  width: 100%;
}

.i-rail-followus__socials {
  display: table;
}

.i-rail-followus__tw {
  vertical-align: sub;
}


Когда мы смотрим на эту таблицу, то сразу хотим быстро удалить повторяющиеся объявления. Но поскольку мы находимся в самом начале «сушки», не будем отходить от алгоритма и просто спускаемся по таблице сверху вниз, просматривая каждое объявление. Первым является border-color: transparent;. Оно где-нибудь еще используется? Нет, тогда идём к следующему — border-style: solid;. Оно уникальное. Тоже самое с border-width: 10px 10px 10px 0;. Затем height: 0;. Не уникальное.

Соответственно, наша работа начинается с height: 0;. Заметим, что это объявление используется в трех различных правилах: .arrow-down, .faq-list .faq-item-title:after и .videoWrapper. Поскольку у нас нет правила, которое включает в себя именно эти четыре селектора, мы копируем их, чтобы сформировать новое правило непосредственно перед тем, в котором мы нашли первое вхождение height: 0;( .arrow-left). Другими словам, мы пишем новое правило только для height: 0;.

.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after,
.videoWrapper {
  height: 0;
}


Теперь убираем height: 0; из всех остальных правил. Поскольку ни одно из них не состояло исключительно из этого объявления, мы не можем удалить их целиком (пока). Обычно я делаю это за один шаг: если нахожу «бесспорный» дубликат, то создаю новое правило, ищу больше вхождений, удаляю объявление, которое нужно переместить, и копирую селектор (ы), который нужно вставить. С опытом всё это становится проще.

Продолжим с правилом .arrow-left, с объявлением, следующим после height: 0; — width: 0. Дубликат? Да. И это хорошо, потому что когда мы проверяем, где еще используется width: 0;, то видим, что оно почти такое же, но не идентичное селекторам, использующим height: 0;. То есть мы начнем с нового правила для width: 0;, убедившись, что удалили все предыдущие вхождения:

.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after {
  width: 0;
}


Это правило идет после созданного для height: 0; и перед .arrow-letf;, которое мы только что проверили и оптимизировали. Я считаю, что результирующий порядок полезен, потому что предпочитаю, чтобы правила располагались в порядке важности и воздействия. А поскольку мы объединяли правила, то сделали их более эффективными.

Давайте проработаем правило .arrow-down;. В нём нет повторений, хотя его можно описать элегантнее: border: 10px solid transparent; border-top-color: #2b2d32;.

Продолжим с .faq-list .faq-item-title. Тут нет дубликатов. На самом деле этот фрагмент таблицы стилей был довольно простым, поэтому мы больше не находим дополнительных совпадений.

Код
.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after,
.videoWrapper {
  height: 0;
}

.arrow-left,
.arrow-down,
.faq-list .faq-item-title:after {
  width: 0;
}

.arrow-left {
  border-color: transparent;
  border-style: solid;
  border-width: 10px 10px 10px 0;
}

.arrow-down {
  border-bottom: 10px solid transparent;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-top: 10px solid #2b2d32;
  bottom: 0;
  left: 20px;
}

.faq-list .faq-item-title {
  cursor: pointer;
}

.faq-list .faq-item-title:after {
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid #111;
  content: '';
  display: inline-block;
  float: right;
  opacity: .5;
  vertical-align: top;
}

#contact input:focus,
#contact textarea:focus {
  border: 1px solid #3398db;
}

.flickity-slider>.table {
  table-layout: fixed;
}

::selection {
  background: #9b59b6;
  color: #fff;
}

.videoWrapper {
  padding-bottom: 56.25%;
  position: relative;
}

.videoWrapper iframe {
  height: 100%;
  left: 0;
  position: absolute;
  top: 0;
  width: 100%;
}

.i-rail-followus__socials {
  display: table;
}

.i-rail-followus__tw {
  vertical-align: sub;
}


Оптимизация. Пример второй


Для второго примера возьмём eBay. На этот раз мы тоже хотим просто переформатировать (отступы, сортировка объявлений по алфавиту), но снова сталкиваемся с практиками, которых следует избегать (особенно c именами классов или отсутствием резервных шрифтов, но для шрифтов вроде Arial это не является проблемой).

Код
.sh-GifCont {
  color: #999;
  font-family: Verdana !important;
  font-size: 10px;
  font-weight: normal;
  padding: 0 10px 4px 10px;
}

.sh-GetFastImg {
  background-image: url('http: //ir.ebaystatic.com/pictures/aw/pics/de/viewitem/spr4VI.png');
  background-position: 0 -178px;
  background-repeat: no-repeat;
  float: left;
  height: 16px;
  margin-right: 4px;
  width: 56px;
}

.sh-float-l {
  float: left;
}

.sh-FrZip {
  padding: 10px 0 0 15px;
  width: 12%;
}

.sh-FrDelLoc {
  padding: 10px 15px 0 10px;
  width: 10%;
}

.sh-FrCnt {
  padding-left: 10px;
  padding-right: 0;
  text-align: left;
}

.sh-FrZipCnt {
  padding: 0 0 0 15px;
}

.sh-FrDelLocCnt {
  padding: 0;
}

.sh-FrBtn {
  padding: 5px 15px 10px 8px;
}

.sh-FrDelSite {
  padding: 6px 0 0;
}

.vi-frs-sh-FrSlctr {
  display: inline;
  padding: 6px 15px 0 10px;
}

.sh-FrZipDiv {
  display: inline;
  padding: 6px 15px 0 0;
}

.sh-FrTxt {
  color: #333;
  font-family: Arial;
  font-size: 12px;
  font-weight: normal;
  padding-left: 15px;
}

.sh-FrLrnMore {
  display: inline;
  padding: 10px 10px 10px 15px;
}

.sh-FrQuote {
  display: inline;
}

.sh-FrLnk {
  margin-top: 5px;
}

.sh-Tbl {
  padding: 10px;
}

.sh-TblCnt {
  color: #333;
  font-family: Arial;
  font-size: 12px;
  font-weight: normal;
  padding-left: 15px;
}

.sh-TblHdr {
  color: #5d5d5d;
  font-family: Verdana;
  font-size: 12px;
  font-weight: normal;
  padding-left: 15px;
}

.sh-Info {
  color: #999;
  font-family: Arial;
  font-size: 11px;
  font-weight: normal;
}

.sh-FrSbTxt {
  color: #999;
  font-family: arial;
  font-size: 11px;
  font-weight: normal;
  padding-left: 15px;
}

.sh-FreightHdr {
  color: #333;
  font-family: Verdana !important;
  font-size: 10px;
  font-weight: normal;
  padding: 5px 0 5px 23px;
}

.sh-Freight-Hdr {
  color: #333;
  font-family: Verdana !important;
  font-size: 10px;
  font-weight: normal;
  padding-left: 13px;
}

.sh-Cnt {
  color: #5d5d5d;
  font-family: Arial;
  font-size: small;
  font-weight: normal;
  padding-left: 13px;
}

.vi-frs-sh-TxtCnt {
  color: #333;
  font-family: Arial;
  font-size: 12px;
  font-weight: normal;
}

.sh-BtnTxt {
  color: #333;
  font-family: Verdana,Tahoma,Arial;
  font-size: 12px;
  font-weight: normal;
  height: 24px;
  margin: 0;
  padding: 0 3px;
  position: relative;
  text-decoration: none;
  top: 0;
}

.sh-bubble-position {
  float: left;
  padding-top: 5px;
}

.sh-del-lrge b {
  font-size: 15px;
}

.sh-gspFirstLine {
  color: #333;
  font-family: Arial;
  font-size: 15px;
  padding: 25px 10px 5px 0;
}

.sh-gspSecondLine {
  color: #777;
  font-family: Arial;
  font-size: 12px;
  padding: 0 10px 15px 0;
}


Первый шаг: color: #999; был использован более одного раза? Да. Итак, мы сначала создаем собственное правило:

.sh-GifCont {
  color: #999;
}


Следует добавить селекторы для двух других вхождений:

.sh-GifCont,
.sh-Info,
.sh-FrSbTxt {
  color: #999;
}


Здесь нам также помогает предположение, что таблица стилей уже использует тот порядок выбора, который нам нужен, и что смена правил не вызовет проблем. Полезно иметь четкое представление о порядке выбора и о решении каскадных проблем, не перемещая затронутые правила или не объединяя их.

font-family: Verdana !important; — это объявление используется трижды. Затем font-size: 10px; — обратите внимание, важно избегать повторения селекторов: это объявление используется теми же селекторами, что мы сгруппировали для «Verdana».

.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
  font-family: Verdana !important;
}

.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
  font-size: 10px;
}


Резюмируем правила:

.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
  font-family: Verdana !important;
  font-size: 10px;}


Таким образом мы проработаем всю таблицу стилей, пока не получим:

Код
.sh-GifCont,
.sh-Info,
.sh-FrSbTxt {
  color: #999;
}

.sh-GifCont,
.sh-FreightHdr,
.sh-Freight-Hdr {
  font-family: verdana !important;
  font-size: 10px;
}

.sh-GifCont,
.sh-FrTxt,
.sh-TblCnt,
.sh-TblHdr,
.sh-Info,
.sh-FrSbTxt,
.sh-FreightHdr,
.sh-Freight-Hdr,
.sh-Cnt,
.vi-frs-sh-TxtCnt,
.sh-BtnTxt {
  font-weight: normal;
}

.sh-GifCont {
  padding: 0 10px 4px 10px;
}

.sh-GetFastImg,
.sh-float-l,
.sh-bubble-position {
  float: left;
}

.sh-GetFastImg {
  background-image: url('http: //ir.ebaystatic.com/pictures/aw/pics/de/viewitem/spr4VI.png');
  background-position: 0 -178px;
  background-repeat: no-repeat;
  height: 16px;
  margin-right: 4px;
  width: 56px;
}

.sh-FrZip {
  padding: 10px 0 0 15px;
  width: 12%;
}

.sh-FrDelLoc {
  padding: 10px 15px 0 10px;
  width: 10%;
}

.sh-FrCnt {
  padding-left: 10px;
  padding-right: 0;
  text-align: left;
}

.sh-FrZipCnt {
  padding: 0 0 0 15px;
}

.sh-FrDelLocCnt {
  padding: 0;
}

.sh-FrBtn {
  padding: 5px 15px 10px 8px;
}

.sh-FrDelSite {
  padding: 6px 0 0;
}

.vi-frs-sh-FrSlctr,
.sh-FrZipDiv,
.sh-FrLrnMore,
.sh-FrQuote {
  display: inline;
}

.vi-frs-sh-FrSlctr {
  padding: 6px 15px 0 10px;
}

.sh-FrZipDiv {
  padding: 6px 15px 0 0;
}

.sh-FrTxt,
.sh-TblCnt,
.sh-FreightHdr,
.sh-Freight-Hdr,
.vi-frs-sh-TxtCnt,
.sh-BtnTxt,
.sh-gspFirstLine {
  color: #333;
}

.sh-FrTxt,
.sh-TblCnt,
.sh-Info,
.sh-FrSbTxt,
.sh-Cnt,
.vi-frs-sh-TxtCnt,
.sh-gspFirstLine,
.sh-gspSecondLine {
  font-family: arial;
}

.sh-FrTxt,
.sh-TblCnt,
.sh-TblHdr,
.vi-frs-sh-TxtCnt,
.sh-BtnTxt,
.sh-gspSecondLine {
  font-size: 12px;
}

.sh-FrTxt,
.sh-TblCnt,
.sh-TblHdr,
.sh-FrSbTxt {
  padding-left: 15px;
}

.sh-FrLrnMore {
  padding: 10px 10px 10px 15px;
}

.sh-FrLnk {
  margin-top: 5px;
}

.sh-Tbl {
  padding: 10px;
}

.sh-TblHdr,
.sh-Cnt {
  color: #5d5d5d;
}

.sh-TblHdr {
  font-family: verdana;
}

.sh-Info,
.sh-FrSbTxt {
  font-size: 11px;
}

.sh-FreightHdr {
  padding: 5px 0 5px 23px;
}

.sh-Freight-Hdr,
.sh-Cnt {
  padding-left: 13px;
}

.sh-Cnt {
  font-size: small;
}

.sh-BtnTxt {
  font-family: verdana,tahoma,arial;
  height: 24px;
  margin: 0;
  padding: 0 3px;
  position: relative;
  text-decoration: none;
  top: 0;
}

.sh-bubble-position {
  padding-top: 5px;
}

.sh-del-lrge b,
.sh-gspFirstLine {
  font-size: 15px;
}

.sh-gspFirstLine {
  padding: 25px 10px 5px 0;
}

.sh-gspSecondLine {
  color: #777;
  padding: 0 10px 15px 0;
}


Редактирование


То, что мы рассмотрели выше, может показаться большой сложной работой. Но всё, что для этого нужно, это воля, практика и понимание, что сложная и утомительная работа возникает только тогда, когда вы выполняете ее с полностью несортированными, неоптимизированными таблицами стилей. Как только мы проясним наши стандарты написания кода и шаги по оптимизации, обновлять и поддерживать таблицы стилей становится довольно просто. Достаточно будет перепроверить свои правки.

Давайте также рассмотрим это очень кратко в третьем примере, отрывке из Code Responsible.

h1,
h2 {
  color: #000;
  font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif;
  font-weight: 400;
  line-height: 1.13;
}

h1 {
  font-size: 1.86em;
  margin: 0 0 .53em;
}

h2 {
  counter-increment: counter;
  font-size: 1.5em;
  margin: 1em 0 0;
}


Здесь мы можем легко изменить правило: предположим, что для h1 нужен другой margin, к примеру, margin: 1em 0 0;. Многие сразу поймут, что мы можем и должны сделать. А чтобы показать один из возможных способов, с более сложной таблицей стилей я поступил бы так.

Первое, что нужно сделать, это внести изменение, отметить его (мой редактор — обычно IntelliJ IDEA — показывает изменения и упрощает их поиск, но мне всё равно нравится временно отступать от новых или измененных объявлений) и протестировать:

h1,
h2 {
  color: #000;
  font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif;
  font-weight: 400;
  line-height: 1.13;
}

h1 {
  font-size: 1.86em;
   margin: 1em 0 0;
}

h2 {
  counter-increment: counter;
  font-size: 1.5em;
  margin: 1em 0 0;
}


Во-вторых, после успешного тестирования я бы проверил все эти измененные строки, не нужно ли их оптимизировать еще раз. Здесь мы обнаружим, что поля для h1 и h2 идентичны.

Следуя способу разбивки на отдельные шаги, создадим правило для h1 и h2. Но у них уже есть собственное правило, поэтому объединим их в одно. Результат:

h1,
h2 {
  color: #000;
  font-family: futurastd-book, futura, 'droid sans', 'helvetica neue', helvetica, sans-serif;
  font-weight: 400;
  line-height: 1.13;
  margin: 1em 0 0;
}

h1 {
  font-size: 1.86em;
}

h2 {
  counter-increment: counter;
  font-size: 1.5em;
}


Этим я хотел показать, что поддерживать таблицы гораздо легче, чем оптимизировать их.

Подсказки


Хочу поделиться несколькими советами (поскольку я обновляю статьи на meiert.com, я могу со временем изменить эти советы).

  1. Поиск без учета регистра. Вполне возможно, что различие регистра является не случайным (некоторые области таблицы стилей, такие как сгенерированный контент или URL-адреса, могут быть чувствительны к регистру). eBay является хорошим примером: независимо от отсутствующих резервных шрифтов, некоторые из используемых объявлений шрифтов пишутся с заглавной буквы по-разному. Например, мы находим font-family: Arial; и font-family: arial;. Их следует объединить, а значит версия с корректировкой на повторение использует только одно объявление и только в одной нотации (нижний регистр).
  2. Закрывайте каждое (предварительное) объявление точкой с запятой. Это упрощает просто копирование и перемещение объявлений, а также избавляет нас от множества ложных срабатываний (а их будет несколько). Тег !important является прекрасным примером: если мы ищем «незакрытые» выражения, то border: 0 обязательно совпадёт с border: 0 !important, и может появиться бесконечное количество других действительно разных объявлений. Это усложнит нашу работу и повысит вероятность ошибок.
  3. Используйте стандартный порядок выбора. Мы не только ограничиваем энтропию таблицы стилей, но и, поскольку новые правила «автоматически» попадают в четко определенные места, получаем естественную линию защиты от повторения селектора. И мы не хотим, чтобы в статьях, подобных этой, селекторы оставались «сухими». Подумайте, можете ли вы использовать мой дизайн упорядочивания селекторов, тем самым помогая сообществу веб-разработчиков стандартизировать его, или создать свой собственный.


Требования к инструментам


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

  1. Редакторы могут помочь нам избежать повторяющихся объявлений, выделяя их. Лично я думаю, что небольшой символ »!» в конце строки, настраиваемый в каждом редакторе, был бы замечательным решением, как и возможность игнорировать или отключать уведомления для определенных строк. Это значительно упростило бы работу по оптимизации: не только для отслеживания избыточности, но и для получения представления о том, насколько проблематичен тот или иной случай. некоторых случаях таблицы стилей на 90% состоят из повторений.
  2. Я уверен, у вас остались вопросы;, но надеюсь, что их осталось меньше, чем раньше, когда мы в течение многих лет пренебрегали этой оптимизацией. С моей точки зрения, DRY CSS позволил бы нам более трезво взглянуть на переменные и другие функции, которые попали в спецификации CSS.


Эти вопросы, конечно, следует принимать во внимание, и я надеюсь, что вы поможете нам всем поработать над ними. Пример с Яндексом показал, что отсутствие повторяющихся объявлений — не панацея. Это помогает сделать CSS компактнее и управляемее, но всё же есть крайние случаи, когда он усложняется. Нам будет полезно изучить эти моменты.

Наконец, я считаю, что избегание повторяющихся объявлений является важным способом работы с CSS. Фактически, я не вижу никакого другого метода оптимизации, помимо создания ваших личных рекомендаций по коду, потому что без систематического подхода к на предотвращению повторения вся наша работа просто увеличивает энтропию, которая не помогает писать качественный код. Но именно к нему мы стремимся как профессионалы. Давайте уделим больше внимания качеству кода и парадигме DRY CSS, и посмотрим, что мы можем автоматизировать, не забывая о своем мастерстве.

© Habrahabr.ru