Skip to content

Instantly share code, notes, and snippets.

@screeny05
Created January 3, 2022 20:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save screeny05/151bbb1d379c1b699ede3920241ac4d7 to your computer and use it in GitHub Desktop.
Save screeny05/151bbb1d379c1b699ede3920241ac4d7 to your computer and use it in GitHub Desktop.
(function bemLinter(){
const namespaces = ['o', 'c', 'u', 's', 't', 'is', 'has'];
const suffixes = ['xs', 's', 'ms', 'sm', 'md', 'lg', 'l', 'xl', 'print'];
const SEVERITY_ERROR = 'error';
const SEVERITY_WARNING = 'warn';
const SEVERITY_INFO = 'info';
const ERR_TYPE_STRAY_ELEMENT = 'stray-element';
const ERR_TYPE_MISSING_NAMESPACE = 'missing-namespace';
const ERR_TYPE_INVALID_SUFFIX = 'invalid-suffix';
const ERR_TYPE_CAMELCASE = 'camelcase';
const ERR_TYPE_MISSING_NON_MODIFIED = 'missing-non-modified';
/** cls has to have a namespace [deactivated - only for itcss] */
const namespaceChecker = (node, cls) => {
const regex = new RegExp(`^_?(${namespaces.join('|')})-.`);
return {
success: regex.test(cls),
message: `${cls} needs to start with a namespace. possible namespaces are: ${namespaces.join(', ')}`,
severity: SEVERITY_ERROR,
type: ERR_TYPE_MISSING_NAMESPACE
};
};
/** cls either needs no suffix, or a valid one */
const mediaSuffixChecker = (node, cls) => {
const regex = /@(.*?)$/;
const match = cls.match(regex);
if(!match){
return { success: true };
}
return {
success: suffixes.indexOf(match[1]) !== -1,
message: `${cls} has an invalid suffix of '${match[1]}'. possible suffixes are: ${suffixes.join(', ')}`,
severity: SEVERITY_ERROR,
type: ERR_TYPE_INVALID_SUFFIX
}
};
/** cls has to be all lowercase */
const camelcaseChecker = (node, cls) => {
return {
success: cls.toLowerCase() === cls,
message: `${cls} needs to be all-lowercase. no camleCase allowed`,
severity: SEVERITY_ERROR,
type: ERR_TYPE_CAMELCASE
}
};
/** modifiers can't stand alone */
const nonModifiedChecker = (node, cls) => {
const regex = /^(.*?)--/;
const match = cls.match(regex);
if(!match){
return { success: true };
}
return {
success: node.classList.contains(match[1]),
message: `the modifier ${cls} doesn't have a matching block or element on the same element ${match[1]}`,
severity: SEVERITY_ERROR,
type: ERR_TYPE_MISSING_NON_MODIFIED
};
};
/** element has to be inside a block */
const strayElementChecker = (node, cls) => {
const regex = /^(.*?)__/;
const match = cls.match(regex);
if(!match){
return { success: true };
}
while(node.parentElement){
node = node.parentElement;
if(node.classList.contains(match[1])){
return { success: true };
}
}
return {
success: false,
message: `the element ${cls} is not contained within the parent-block ${match[1]}`,
severity: SEVERITY_ERROR,
type: ERR_TYPE_STRAY_ELEMENT
};
};
/** prints messages as collapsed console-entries */
const printMessages = messages => {
let lastType = null;
messages
.sort((a, b) => a.type < b.type ? -1 : a.type > b.type ? 1 : 0)
.forEach(msg => {
if(msg.type !== lastType){
console.groupEnd();
console.groupCollapsed(msg.type);
lastType = msg.type;
}
console[msg.severity](msg.message, msg.node);
});
console.groupEnd();
};
const checkers = [
// namespaceChecker
mediaSuffixChecker,
camelcaseChecker,
nonModifiedChecker,
strayElementChecker
];
const messages = [];
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
while(walker.nextNode()){
const node = walker.currentNode;
if(node.classList.length === 0){
continue;
}
node.classList.forEach(cls => {
checkers.forEach(checker => {
const message = checker(node, cls);
if(message.success){
return;
}
messages.push({
node,
message: message.message,
severity: message.severity,
type: message.type
});
});
});
}
printMessages(messages);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment