Сортировка правил в CSS по БЭМ
В своем проекте я использую методологию БЭМ совместно с gulp. Таск для css выглядит следующим образом:
gulp.task('css', function(done) {
gulp.src(['src/assets/css/global/*.css', 'src/assets/css/blocks/*.css'])
.pipe(plumber({errorHandler: errorNotify}))
.pipe(sourcemaps.init())
.pipe(concat('style.css'))
.pipe(minifyCss())
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('prod/assets'))
.on('end', function() {
gulp.series('reload')();
done();
});
});
У меня несколько упрощенная иерархия файлов: всего один файл на блок, все его элементы, модификаторы, и модификаторы элементов. Все работает до тех пор, пока я не начинаю использовать миксы.
Предположим, у меня есть некоторый блок "search" со свойством "border-color: white", и элемент "header__search" со свойством "border-color: black" в соответствующих файлах "header.css" и "search.css". Естественно, в такой ситуации должно отдаваться предпочтение элементу "header__search". Но на выходе содержимое "header.css" оказывается перед содержимым "search.css", и получается, что у элемента с обоими классами белая граница вместо черной.
Решение я вижу следующим образом: на выходе разместить правила в порядке: "все блоки - все элементы - все модификаторы". Вопрос: как это сделать? В смысле, есть ли какие-либо готовые решения для такого? Соответствующих плагинов я не нашел.
Ответы (1 шт):
Готовых решений я так и не нашел, поэтому написал свое.
По сути, нужно осуществить поиск правил по регулярному выражению, удалить их и вставить в соответствующее место в файле. Порядок должен быть следующий: "блок - модификатор блока - элемент - модификатор элемента". Есть еще один важный момент: сортировка будет проходить по последнему классу в селекторе. Например, селектор ".block_mod .block__element path" будет считаться элементом, а селектор ".block__element + .block" - блоком. Для начала, разберемся с регулярными выражениями.
Выражение поиска любых комбинаций селектора (или же @-правила) и набора свойств. Первая и вторая группа - селектор и набор свойств соответственно. Между ними может быть сколько угодно пробелов и переносов строк:
/([^{}]+?)\s*{([\s\S]*?)}/g
Выражение поиска блока. Первая группа - все, что находится до искомого нами блока. Вторая группа - сам блок. Утверждение в конце означает, что после искомого блока не должно быть объявлено других классов, а также блок действительно является блоком, а не модификатором:
/(.*)\.([a-zA-Z0-9-]+)(?!.*?[._])/
Выражение поиска элемента. Тут все аналогично выражению для блока, за исключением разделения двумя подчеркиваниями:
/(.*)\.([a-zA-Z0-9-]+__[a-zA-Z0-9-]+)(?!.*?[._])/
Выражение поиска модификатора блока. Первая группа - аналогично блоку. Вторая группа - это возможный элемент (ведь модификатор может быть как у блока, так и у элемента). Третья группа - аналогично второй у элемента. Утверждение также аналогично утверждению у блока, только теперь нет проверки на модификатор:
/(.*)\.([a-zA-Z0-9-]+_[a-zA-Z0-9-]+)(?!.*?\.)/
Выражение поиска модификатора элемента. Аналогично выражению для модификатора элемента, за исключением разделения двумя подчеркиваниями:
/(.*)\.([a-zA-Z0-9-]+__[a-zA-Z0-9-]+_[a-zA-Z0-9-]+)(?!.*?\.)/
Еще есть @-правило, но для них не нужно регулярное выражение, достаточно только проверить, есть ли там "@":
selector.search('@') !== -1
Что касается плагинов, то тут нужен только плагин, который позволит читать и писать файлы. Можно вообще использовать fs, но я же предпочел gulp-insert.
Теперь нужно написать функцию, которая принимает содержимое css-файла в виде строки, и возвращает отсортированный результат. Собственно, вот код данной функции:
function cssBemSort(contents) {
// Здесь будут храниться части файла: блоки, элементы, и модификаторы соответственно
let blocks = elements = modificators = '';
// Ищем все комбинации селектора и набора свойств
contents = contents.replace(/([^{}]+?)\s*{([\s\S]*?)}/g, function(match, selector, props) {
// Если селектор вовсе и не селектор, а @-правило, рекурсивно обрабатываем его и возвращаем на место
// Если это правило, содержащее другие правила, вроде @media, правила внутри будут отсортированы
// Если же это что-то другое, то ничего страшного не произойдет - правила просто не будут найдены, и будет возвращена исходная строка
if (selector.search('@') !== -1) {
const sortedProps = cssBemSort(properties);
return `${selector}{${sortedProps}}`;
}
// Добавляем совпадение к блокам
else if (/.*\.[a-zA-Z0-9-]+(?!.*?[._])/.test(selector)) {
blocks += match;
}
// Добавляем совпадение к элементам
else if (/.*\.[a-zA-Z0-9-]+__[a-zA-Z0-9-]+(?!.*?[._])/.test(selector)) {
elements += match;
}
// Добавляем совпадение к модификаторам
else if (/.*\.([a-zA-Z0-9-]+__)?[a-zA-Z0-9-]+_[a-zA-Z0-9-]+(?!.*?\.)/.test(selector)) {
modificators += match;
}
// Если селектор не соответствует ничему из вышеперечисленного, ничего не трогаем
else return match;
// Вырезаем данную часть из исходного файла
return '';
});
// Возвращаем результат в отсортированом порядке
return contents + blocks + elements + modificators;
}
Осталось только дополнить таск:
gulp.task('css', function(done) {
gulp.src(['src/assets/css/global/*.css', 'src/assets/css/blocks/*.css'])
.pipe(plumber({errorHandler: errorNotify}))
.pipe(sourcemaps.init())
.pipe(concat('style.css'))
.pipe(minifyCss())
// Вызываем метод transform у плагина insert, который принимает callback
// Важно вызвать его после concat, иначе отсортируется каждый файл по отдельности
.pipe(insert.transform(cssBemSort))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('prod/assets'))
.on('end', function() {
gulp.series('reload')();
done();
});
});
Готово. К сожалению, я рядовой фронтендер, и не обладаю достаточными знаниями node.js, чтобы написать это в виде отдельного gulp-плагина. Так что будет неплохо, если кто-нибудь сделает такой плагин, если это вообще имеет смысл.