/** * @file AutoclassCSS - Generator CSS skeleton {@link https://github.com/tenorok/autoclassCSS} * @copyright 2012–2013 Artem Kurbatov, tenorok.ru * @license MIT license * @version 0.0.4 */ (function(global) { /** * Конструктор * @constructor * @name Autoclasscss * @param {string} [html] HTML-разметка * @param {Object} [options] Опции */ function Autoclasscss(html, options) { // Если переданы только опции if(isObject(html)) { options = html; html = ''; } this.html = html || ''; this.params = {}; // Если переданы опции if(isObject(options)) { return setOptions.call(this, options); } // Устанавливаются стандартные опции return setOptions.call(this); } /** * Установить опции * @private * @param {Object} [customOptions] Опции или ничего для установления стандартных опций * @returns {this} */ function setOptions(customOptions) { var options = mergeOptions(customOptions); for(var option in options) { this[option].apply(this, getOptionAsArray(option, options[option])); } return this; } /** * Получить опцию в виде массива * Для передачи аргументов в apply * @private * @param {string} name Имя опции * @param {*} value Значение опции * @returns {Array} */ function getOptionAsArray(name, value) { if(isOptionParamCanBeArray(name, value)) { return [value]; } return isArray(value) ? value : [value]; } /** * Может ли опция принимать массив в качестве аргумента * Некоторым опциям надо передавать параметр в виде массива * @private * @param {string} name Имя опции * @param {*} value Значение опции * @returns {boolean} */ function isOptionParamCanBeArray(name, value) { return !!~['ignore', 'tag'].indexOf(name) && isArray(value); } /** * Объединить опции со стандартными опциями * @private * @param {Object} [customOptions] Опции * @returns {*} */ function mergeOptions(customOptions) { var options = getDefaultOptions(); if(!customOptions) return options; for(var option in customOptions) { options[option] = customOptions[option]; } return options; } /** * Получить стандартные опции * @private * @returns {Object} */ function getDefaultOptions() { return { brace: 'default', flat: false, ignore: false, indent: ['spaces', 4], inner: true, line: false, tag: false }; } /** * Продублировать строку * @private * @param {string} string Строка * @param {number} count Количество дублирований * @returns {string} */ function duplicateStr(string, count) { return new Array(count + 1).join(string); } function isString(string) { return typeof string === 'string'; } function isBoolean(bool) { return typeof bool === 'boolean'; } function isArray(array) { return array instanceof Array; } function isObject(object) { return object instanceof Object; } function isRegexp(regexp) { return regexp instanceof RegExp; } Autoclasscss.prototype = { /** * Настройка отступов * @memberof Autoclasscss# * @param {string} type Тип отступов, принимает одно из следующих значений: * "tabs" - табы * "spaces" - пробелы * @param {number} [count=1] Количество символов в одном отступе * @throws {Error} Неизвестный тип отступов * @returns {this} */ indent: function(type, count) { count = count || 1; var indents = { tabs: '\t', spaces: ' ' }, indentStr = indents[type]; if(!indentStr) { throw new Error('Unknown indent type: ' + type); } this.params.indent = duplicateStr(indentStr, count); return this; }, /** * Добавление игнорируемых классов * @memberof Autoclasscss# * @param {string|Array|boolean|RegExp} classes Класс, массив классов, регулярное выражение или false для отмены игнорирования * @returns {this} */ ignore: function(classes) { // Если false if(isBoolean(classes) && !classes) { this.params.ignore = []; return this; } if(isRegexp(classes)) { this.params.ignore = classes; return this; } // Если в ignore не массив, а регулярное выражение if(!isArray(this.params.ignore)) { // Сброс ignore в пустой массив, чтобы не было ошибок при добавлении this.params.ignore = []; } if(isString(classes)) { this.params.ignore.push(classes); return this; } if(isArray(classes)) { this.params.ignore = this.params.ignore.concat(classes); return this; } }, /** * Установление плоского или вложенного списка селекторов * @memberof Autoclasscss# * @param {boolean} state Плоский или не плоский список * @returns {this} */ flat: function(state) { this.params.flat = state; return this; }, /** * Добавлять или не добавлять отступы внутри фигурных скобок * @memberof Autoclasscss# * @param {boolean} state Добавлять или не добавлять * @returns {this} */ inner: function(state) { this.params.inner = state; return this; }, /** * Указывать тег в селекторе * @memberof Autoclasscss# * @param {boolean|string|Array} tag Значение опции можно передавать в разном виде, например: * true|false - указывать или не указывать все теги * 'div' - указывать тег div * ['ul', 'li'] - указывать теги ul и li * @returns {this} */ tag: function(tag) { this.params.tag = isString(tag) ? [tag] : tag; return this; }, /** * Способ отображения открывающей скобки * @memberof Autoclasscss# * @param {string} type Способ отображения, принимает одно из следующих значений: * "default" - через пробел после селектора * "newline" - на новой строке под селектором * @throws {Error} Неизвестный способ отображения * @returns {this} */ brace: function(type) { if(!~['default', 'newline'].indexOf(type)) { throw new Error('Unknown brace type: ' + type); } this.params.brace = type; return this; }, /** * Отбивать селекторы пустой строкой * @memberof Autoclasscss# * @param {boolean} state Отбивать или не отбивать * @param {number} [count=1] Количество строк для отбива * @returns {this} */ line: function(state, count) { this.params.line = state ? duplicateStr('\n', count || 1) : ''; return this; }, /** * Установить HTML-разметку * @memberof Autoclasscss# * @param {string} html HTML-разметка * @returns {this} */ set: function(html) { this.html = html; return this; }, /** * Получить CSS-каркас * @memberof Autoclasscss# * @returns {string} CSS-каркас */ get: function() { var that = this; /** * Колбек вызывается для каждого вхождения подстроки в строку * @private * @callback Autoclasscss~iterateSubstrCallback * @param {Object} match Информация о текущем вхождении */ /** * Проитерироваться по всем вхождениям подстроки в строку * @private * @param {string} string Исходная строка * @param {RegExp} regexp Регулярное выражения для поиска подстроки * @param {Autoclasscss~iterateSubstrCallback} callback Колбек будет вызван для каждого вхождения */ function iterateSubstr(string, regexp, callback) { var match; while((match = regexp.exec(string)) != null) { callback.call(this, match); } } /** * Получить информационный массив по всем открывающим тегам в HTML * @private * @param {string} html Исходный HTML * @returns {Array} */ function searchOpenTags(html) { var openTagsInfo = []; iterateSubstr(html, /<[-A-Za-z0-9_]+/g, function(openTag) { openTagsInfo.push({ type: 'tag-open', position: openTag.index, name: openTag[0].substr(1) }); }); return openTagsInfo; } /** * Получить информационный массив по всем закрывающим тегам в HTML * @private * @param {string} html Исходный HTML * @returns {Array} */ function searchCloseTags(html) { var closeTagsInfo = []; iterateSubstr(html, /<\//g, function(closeTag) { closeTagsInfo.push({ type: 'tag-close', position: closeTag.index }); }); return closeTagsInfo; } /** * Получить содержимое атрибута class * @private * @param {string} classAttr Вырванный из HTML кусок с атрибутом class * @returns {string} */ function getClassAttrContent(classAttr) { return classAttr.match(/('|")[\s*-A-Za-z0-9_\s*]+('|")/i)[0].replace(/\s*('|")\s*/g, ''); } /** * Колбек вызывается для каждого класса в атрибуте class * @private * @callback Autoclasscss~iterateClassesInAttrCallback * @param {string} cls Текущий класс * @param {number} pos Порядковый номер класса в атрибуте */ /** * Проитерироваться по классам в атрибуте class * @private * @param {string} classAttrContent Содержимое атрибута class * @param {Autoclasscss~iterateClassesInAttrCallback} callback Колбек будет вызван для каждого класса */ function iterateClassesInAttr(classAttrContent, callback) { // Если атрибут класса пустой if(!classAttrContent) return; classAttrContent.replace(/\s+/g, ' ').split(' ').forEach(function(cls, pos) { callback.call(this, cls, pos); }); } /** * Получить информационный массив по всем классам в HTML * @private * @param {string} html Исходный HTML * @returns {Array} */ function searchClasses(html) { var classesInfo = []; // Перебор всех атрибутов class в html iterateSubstr(html, /\s+class\s*=\s*('|")\s*[-A-Za-z0-9_\s*]+\s*('|")/g, function(classAttr) { iterateClassesInAttr(getClassAttrContent(classAttr[0]), function(cls, pos) { classesInfo.push({ type: 'class', position: classAttr.index + pos, // Для сохранения последовательности классов в атрибуте val: cls }); }); }); return classesInfo; } /** * Узнать является ли тег одиночным * @private * @param {string} tag Имя тега * @returns {boolean} */ function isSingleTag(tag) { return !!~[ '!doctype', 'area', 'base', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'wbr' ].indexOf(tag); } /** * Является ли класс игнорируемым * @private * @param {string} cls Имя класса * @returns {boolean} */ function isIgnoringClass(cls) { if(isArray(that.params.ignore)) { return ~that.params.ignore.indexOf(cls); } // Иначе в ignore регулярное выражение return that.params.ignore.test(cls); } /** * Получить массив тегов с их классами * @private * @param {Array} htmlStructureInfo Информационный массив по HTML-структуре * @returns {Array} */ function putClassesIntoTags(htmlStructureInfo) { var tags = []; htmlStructureInfo.forEach(function(element) { switch(element.type) { case 'tag-open': tags.push({ type: element.type, name: element.name, single: isSingleTag(element.name), classes: [] }); break; case 'class': isIgnoringClass(element.val) || tags[tags.length - 1].classes.push(element.val); break; case 'tag-close': tags.push({ type: element.type }); } }); return tags; } /** * Получить плоский массив классов с указанием их уровня вложенности * @private * @param {Array} tags Массив тегов с их классами * @returns {Array} */ function getClassLevels(tags) { var classes = [], tree = [], // Для контроля уровня вложенности exist = []; // Добавленные классы tags.forEach(function(tag) { if(tag.type === 'tag-open') { tree.push(tag); addClasses(tag.name, tag.classes, getTagsWithClassesCount()); tag.single && tree.pop(); } else { tree.pop(); } }); /** * Получить текущее количество тегов с классами * @private * @returns {number} */ function getTagsWithClassesCount() { var count = -1; tree.forEach(function(tag) { tag.classes.length > 0 && count++; }); return count; } /** * Добавить класс к выводу * @private * @param {string} tag Имя тега * @param {Array} tagClasses Массив классов тега * @param {number} level Уровень вложенности тега */ function addClasses(tag, tagClasses, level) { tagClasses.forEach(function(cls) { if(~exist.indexOf(cls)) return; exist.push(cls); classes.push({ tag: tag, name: cls, level: level }); }); } return classes; } /** * Нужно ли указывать тег в селекторе * @private * @param {string} tag Имя тега * @returns {boolean} */ function isOkTag(tag) { var paramsTag = that.params.tag; if(isBoolean(paramsTag)) return paramsTag; return !!~paramsTag.indexOf(tag); } /** * Получить открывающую скобку * @private * @param {string} indent Сформированный отступ до селектора * @returns {string} */ function getBrace(indent) { switch(that.params.brace) { case 'default': return ' {'; case 'newline': return '\n' + indent + '{'; } } /** * Сформировать CSS-каркас * @private * @param {Array} classes Плоский массив классов с указанием их уровня вложенности * @returns {string} */ function genCSSSkeleton(classes) { var css = []; classes.forEach(function(cls) { var paramsIndent = that.params.indent, indent = !that.params.flat ? duplicateStr(paramsIndent, cls.level) : '', innerIndent = that.params.inner ? '\n' + indent + paramsIndent + '\n' + indent : '', tag = isOkTag(cls.tag) ? cls.tag : ''; css.push(indent + tag + '.' + cls.name + getBrace(indent) + innerIndent + '}'); }); return css.join('\n' + that.params.line); } /** * Получить информационный массив по HTML-структуре * @private * @param {string} html Исходный HTML * @returns {Array} */ function getHtmlStructureInfo(html) { return searchOpenTags(html) .concat(searchCloseTags(html)) .concat(searchClasses(html)) .sort(function(a, b) { return a.position - b.position; }); } return genCSSSkeleton( getClassLevels( putClassesIntoTags( getHtmlStructureInfo(this.html) ) ) ); } }; global.Autoclasscss = Autoclasscss; })(this, undefined);