Устаревшие селекторы в SCSS

Я люблю инструменты типа JSDoc, SassDoc и Docco, однако, иногда хочется интерактивности

Проблема

Допустим, у вас есть следующий код

.message {
font-weight: bold;
    &__red {
color: red;
}
}

и в какой то момент вы решили использовать селектор message__error вместо message__red, и чтобы ничего не сломать теперь код выглядит примерно так

.message {
font-weight: bold;
    //obsolete
//please use `message__error` instead
&__red {
color: red;
}
    &__error {
color: red;
}
}

Сгенерировав документацию мы увидим, что селектор message__red является устаревшим.

Я хочу чтобы устаревшие селекторы записывались в отдельный файл, а при загрузке страницы, в консоль, выводились элементы с этими селекторами

test.html:225 obsolete selector ".lead" found on element <p class=​"lead">​…​</p>​, use ".cover-lead" instead

Парсинг SCSS файлов

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

Синтаксическое дерево

Нам не нужно разбивать исходный код на мелкие детали. Достаточно получить @mixin, @include и селекторы. Т.е. можно обойтись 4 нодами: Mixin, Include и Ruleset.

var Node = function(extras) {
extras = extras || {};
    this.obsolete = extras.comment ? extras.comment.indexOf('obsolete') !== -1 : false;
};
var Mixin = exports.Mixin = function(name, extras) {
Node.call(this, extras);
};
Mixin.prototype = new Node;
Mixin.prototype.constructor = Mixin;

var Include = exports.Include = function(name, extras) {
Node.call(this, extras);
};
Include.prototype = new Node;
Include.prototype.constructor = Include;

var Ruleset = exports.Ruleset = function(selectors, extras) {
Node.call(this, extras);
};
Ruleset.prototype = new Node;
Ruleset.prototype.constructor = Ruleset;

Парсинг

SCSS селекторы могут быть записаны множеством способов

[attr=value]
[attr]
tag
.class
#id
#{$element-selector}

Мы поделим их на четыре группы: [attr], [attr=value], class_or_id_or_tag и class_or_id_or_tag{variable}, а используя парслеты мы сможем легко их распарсить

function selector() {
this.consumeIf(':comment');
    return this.consume(function() {
var t;
return [
this.consume(':identifier').lexeme,
this.consume('{').lexeme,
this.consume(':identifier').lexeme,
this.consume('}').lexeme,
(t = this.consumeIf(':identifier')) ? t.lexeme : ''
].join('');
}, function() {
return [
this.consume('[').lexeme,
this.consume(':identifier').lexeme,
this.consume(':operator').lexeme,
this.consume(parslets.lValue).lexeme,
this.consume(']').lexeme
].join('');
}, function() {
return [
this.consume('[').lexeme,
this.consume(':identifier').lexeme,
this.consume(']').lexeme
].join('');
}, function() {
return this.consume(':identifier').lexeme;
});
}

Получение списка устаревших селекторов

В базовом классе Node есть свойство obsolete, опираясь на которое мы можем отфильтровать полученные узлы, и взять только нужные

function obsoleteNodes(t) {
return new parslets.TokenWrapper(t).consume(parslets.search(scss.content)).filter(function(node) {
return node.obsolete;
});
}

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

Ruleset.prototype.selectors = function() {
var retVal = [];
    for (var i = this._selectors.length - 1; i >= 0; i--) {
var topSel = this._selectors[i];
        retVal.push(topSel);
        for (var j = this.body.length - 1; j >= 0; j--) {
var child = this.body[j];
            if (child instanceof Ruleset) {
var childSels = child.selectors();
                for (var k = childSels.length - 1; k >= 0; k--) {
if (childSels[k].charAt(0) == '&') {
retVal.push(topSel + childSels[k].substr(2));
} else {
retVal.push(topSel + ' ' + childSels[k]);
}
}
}
}
}
    return retVal;
};

Интерполяция и селекторы

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

selector-#{$value} {
property: value;
}

В общем случае, нужно хранить все переменные и их значения, затем проверять каждый селектор на наличие в нем шаблона #{value} и производить замену.

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

В результате нужно найти все миксины и их вызовы. Затем в селекторах внутри миксинов заменить #{value} на значения соответствующий аргументов.

//получаем список всех инклудов сгруппированных по имени
// {
// 'include_name': [
// ....
// ]
//}
var includes = includeNodes(tokens);

/*список всех нод*/list.forEach(function(node) {
if (node instanceof ast.Mixin) {
if (includes[node.name]) {
var mixin = node;
            for (var i = includes[node.name].length - 1; i >= 0; i--) {
var include = includes[node.name][i];
                blackList.push.apply(blackList, mixin.selectors.map(function(sel) {
for (var i = include.arguments.length - 1; i >= 0; i--) {
sel = sel.replace('#{' + mixin.arguments[i].name + '}', include.arguments[i]);
}
return sel;
}));
}
}
}
});

Исходный код можно найти на гитхабе — https://github.com/pkorzh/theprowl

A single golf clap? Or a long standing ovation?

By clapping more or less, you can signal to us which stories really stand out.