У меня есть проект php, который использует grunt для компиляции sass-файлов в css. Мне было интересно, если есть способ сделать переименование класса CSS похож на Google Таблицы стилей закрытия. Так что на самом деле это вопрос из двух частей:
Насколько я знаю, в sass в настоящее время нет такой функции, если только ее нельзя добавить в качестве расширения. Однако с помощью grunt я мог сначала скомпилировать файлы sass, а затем запустить другую задачу, которая переименовывает класс и выводит файл карты. Технически я мог бы использовать для этого таблицы стилей закрытия, но я ищу что-то более легкое, не требующее установки другой зависимости.
Теперь я мог бы просто вставить что-то вроде этого для каждого класса CSS: <?php echo getclassname("some-class-name") ?>
который будет ссылаться на файл карты, сгенерированный выше, чтобы получить правильное имя класса. Но это кажется утомительным. Есть ли лучший способ сделать это?
Сначала скомпилируйте sass, а затем пропустите его через пользовательское задание, чтобы переименовать классы.
Я использую CSS модуль узла для разбора CSS.
Давайте начнем с рассмотрения пользовательского задания.
Отказ от ответственности: я написал этот код быстро, поэтому он, вероятно, не готов к работе.
var fs = require( 'fs' ),
rename = require( './rename.js' );
// Register the rename_css task.
grunt.registerMultiTask('rename_css', 'Shorten css class names', function () {
var options = this.options(); // Pass all options directly to css.parse
this.files.forEach(function ( file ) {
var renamed = rename.rename(
fs.readFileSync( file.src[ 0 ], 'utf8' ), options );
fs.writeFileSync( file.dest, renamed.text );
fs.writeFileSync( file.map, JSON.stringify( renamed.map, null, 2 ) );
});
});
Конфигурация для этой задачи будет выглядеть примерно так:
grunt.initConfig({
rename_css: {
options: { compress: true }, // Minify the output css.
main: {
src: "style.css",
dest: "style.min.css",
map: "map.json"}
}
});
Файл rename.js слишком длинный, чтобы показать все это здесь, но вы можете увидеть весь файл на GitHub. Вот основная функция:
function rename( s, options /* passed directly to css.parse */ ) {
/**
* Give the css classes short names like a-b instead of some-class
*
* Returns an object in the form {text: `newCss`, map: `partsMap`} whare text is
* the newly generated css and partsMap is a map in the {oldPart: newPart}.
*/
var
ast = css.parse( s, options ),
countMap = walkPass1( ast.stylesheet ), // Walk the first pass.
sortedCounts = [],
map = {}, // Final map.
part,
// List of charictor positions for the short class names.
// Each number corresponds to a charictor in the `chars` string.
charPosSet = [ 0 ];
// Unpack the count map.
for ( part in countMap ) {
sortedCounts.push({
name: part,
count: countMap[ part ],
replacment: undefined
});
}
// Sort based on the number of counts.
// That way we can give the most used classes the smallest names.
sortedCounts.sort(function( a, b ) { return b.count - a.count });
// Generate the small class names.
sortedCounts.forEach(function ( part ) {
var
s = '',
i = charPosSet.length;
// Build up the replacment name.
charPosSet.forEach(function ( pos ) {
s += chars[ pos ];
});
while ( i-- ) {
charPosSet[ i ]++;
// If the current char pos is greater then the lenght of `chars`
// Then we set it to zero.
if ( charPosSet[ i ] == chars.length ) {
charPosSet[ i ] = 0;
if ( i == 0 ) { // Time to add another digit.
charPosSet.push( 0 ); // The next digit will start at zero.
}
} else {
// Everything is in bounds so break the loop.
break;
}
}
part.replacment = s;
});
// Now we pack a basic map in the form of old -> new.
sortedCounts.forEach(function ( part ) {
map[ part.name ] = part.replacment;
});
// Walk the tree a second time actually renameing the classes.
walkPass2( ast.stylesheet, map );
return {
text: css.stringify( ast, options ), // Rebuild the css.
map: map
};
}
Это выглядит сложным, но вот разбивка того, что он делает:
Стоит отметить, что эта функция даст более часто используемым классам более короткие имена, что
результат в немного меньших файлах CSS.
Это можно сделать с помощью выходного буфера. Это может выглядеть примерно так (вверху страницы перед рутом html
тег):
<?php
define(DEV_MODE, false);
function build_class( $name, $map ) {
$parts = [];
foreach ( explode( '-', $name ) as $part ) {
$newPart = array_key_exists( $part, $map )? $map[ $part ] : $part;
array_push( $parts, $newPart );
}
return implode( '-', $parts );
}
function class_rename ( $content ) {
$string = file_get_contents( 'map.json' );
$classMap = json_decode( $string, true );
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false; // Remove unnesesary whitespace.
@$doc->loadHTML( $content );
foreach ( $doc->getElementsByTagName( '*' ) as $elem ) {
$classStr = $elem->getAttribute( 'class' );
if ( ! empty( $classStr ) ) { // No need setting empty classess all over the place.
$classes = []; // This is ware we put all the renamed classes.
foreach ( explode( ' ', $classStr ) as $class ) {
array_push( $classes, build_class( $class, $classMap ) );
}
$elem->setAttribute( 'class', implode( ' ', $classes ) );
}
}
return $doc->saveHTML();
}
if (!DEV_MODE)
ob_start( 'class_rename' );
?>
Хотя это и не является частью первоначального вопроса, решение довольно интересное и не совсем тривиальное, поэтому я решил включить его.
Прежде всего зарегистрируйте другое задание:
var fnPattern = /(jQuery|\$|find|__)\s*\(\s*(["'])((?:\\.|(?!\2).)*)\2\s*\)/g;
grunt.registerMultiTask('rename_js', 'Use short css class names.', function () {
this.files.forEach(function ( file ) {
var
content = fs.readFileSync( file.src[ 0 ], 'utf8' ),
map = JSON.parse( fs.readFileSync( file.map ) ),
output = content.replace( fnPattern, function ( match, fn, delimiter, str ) {
var classes, i;
if ( fn == '__' ) {
classes = str.split( ' ' );
i = classes.length;
while ( i-- ) {
classes[ i ] = rename.getClassName( classes[i], map );
}
// We can safly assume that that the classes string won't contain any quotes.
return '"' + classes.join( ' ' ) + '"';
} else { // Must be a jQuery function.
return match.replace( str, rename.getClassSelector( str, map ) );
}
});
// Wrap the output in a function so that the `__` function can get removed by an optimizer.
fs.writeFileSync( file.dest, '!(function(window, undefined) {\n' + output + '\n})(window);' );
});
});
Файл JavaScript может выглядеть примерно так:
function __( s ) {
return s;
}
window.main = function () {
var elems = document.getElementsByClassName(__('some-class-name')),
i = elems.length;
while ( i-- ) {
elems[ i ].className += __(' some-other-class-name');
}
}
Важной частью является __
объявление функции. Во время разработки эта функция ничего не будет делать, но когда мы создаем
приложение, эта функция будет заменена скомпилированной строкой класса. Используемое выражение регулярного выражения найдет все вхождения
__
а также функции jQuery (jQuery
, $
а также jQuery.find
). Затем он создает три группы: имя функции,
разделитель (либо "
или же '
) и внутренняя строка. Вот диаграмма, чтобы помочь лучше понять, что происходит:
(?:jQuery|\$|find)\s*\(\s*(["'])((?:\\.|(?!\1).)*)\1\s*\)
Если имя функции __
тогда мы заменим его так же, как мы сделали для php. Если нет, то это
вероятно, селектор, поэтому мы пытаемся заменить класс селектора.
(Обратите внимание, что это не обрабатывает ввод html-текста в функцию jQuery.)
Вы можете получить полный пример Вот
Других решений пока нет …