From 1e02f75a1b1b5c61c81a73b5c7f9c08d367cb706 Mon Sep 17 00:00:00 2001 From: Steve Carlson Date: Fri, 27 Feb 2015 16:08:28 -0800 Subject: [PATCH] Integrated AutoAtom functionality --- .gitignore | 1 + bin/atomizer | 58 +++++-- examples/basic-config.js | 24 +++ examples/html/sample-0.html | 1 + examples/html/sample-1.html | 1 + package.json | 8 +- src/atomizer.js | 333 ++++++++++++++++++++++++++++++++++-- 7 files changed, 396 insertions(+), 30 deletions(-) create mode 100644 examples/basic-config.js create mode 100644 examples/html/sample-0.html create mode 100644 examples/html/sample-1.html diff --git a/.gitignore b/.gitignore index c563380e..a4bbb555 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ artifacts/ npm-debug.log dist/ +*.log diff --git a/bin/atomizer b/bin/atomizer index 9e94dc76..be463fb7 100755 --- a/bin/atomizer +++ b/bin/atomizer @@ -14,16 +14,30 @@ var path = require('path'); var fs = require('fs'); var chalk = require('chalk'); var atomizer = require('../src/atomizer'); -var configObjs = []; +var _ = require('lodash'); +var content = ''; +var config; +var parsedConfig = {}; var params = require('minimist')(process.argv.slice(2)); if (process.argv.slice(2).length === 0 || params.help) { - var usage = ['usage: ', process.title, '[-o|--outfile=] [--help] configfile ...'].join(' '); + var usage = ['usage: ', process.title, ' -c|--config= [-o|--outfile=] [--help] [--verbose] [ ...]'].join(' '); console.log(usage); return; } +function handleMergeArrays (a, b) { + if (_.isArray(a) && _.isArray(b)) { + a.forEach(function(item){ + if(_.findIndex(b, item) === -1) { + b.push(item); + } + }); + return b; + } +} + // TODO var options = { require: [] @@ -35,19 +49,39 @@ if (options.require.length > 0) { }); } -var srcFiles = params._ || []; -srcFiles = srcFiles.filter(function(filepath) { - if (!fs.existsSync(filepath)) { - console.warn('Configuration file ' + chalk.cyan(filepath) + ' not found.'); +// Static config should contain the general 'config' options, along with any +// statically defined configuration. It is required. +var configFile = params.c || params.config; +if (configFile) { + if (!fs.existsSync(configFile)) { + throw new Error('Configuration file ' + chalk.cyan(configFile) + ' not found.'); return false; - } else { - configObjs.push(require(path.resolve(filepath))); - return true; } -}); + config = require(path.resolve(configFile)); +} else { + throw new Error('Configuration file not provided.'); + return false; +} + +// Generate config from parsed src files +var parseFiles = params._ || []; +if (parseFiles.length) { + var classNamesObj = {}; + parseFiles.forEach(function (filepath) { + console.warn('Parsing file ' + chalk.cyan(filepath) + ' for Atomic CSS classes'); + var fileContents = fs.readFileSync(path.resolve(filepath), {encoding: 'utf-8'}); + atomizer.parse(fileContents, classNamesObj); + }); + parsedConfig = atomizer.getConfig(classNamesObj, config, !!params.verbose); +} + +// Merge the static config with the generated config +config = _.merge(parsedConfig, config, handleMergeArrays); -var content = atomizer(configObjs, options); +// Create the CSS +content = atomizer.createCSS(config, options); +// Output the CSS var outfile = params.o || params.outfile; if (outfile) { fs.mkdir(path.dirname(outfile), function (err) { @@ -58,5 +92,5 @@ if (outfile) { }); }); } else { - process.stdout.write(content); + process.stdout.write("\n" + content); } \ No newline at end of file diff --git a/examples/basic-config.js b/examples/basic-config.js new file mode 100644 index 00000000..2e4c29ae --- /dev/null +++ b/examples/basic-config.js @@ -0,0 +1,24 @@ +module.exports = { + 'config': { + 'namespace': '#atomic', + 'start': 'left', + 'end': 'right', + 'breakPoints': { + 'sm': '767px', + 'md': '992px', + 'lg': '1200px' + } + }, + + // pattern + 'display': { + 'b': true + }, + + // pattern + 'border-top': { + custom: [ + {suffix: '1', values: ['1px solid #ccc']} + ] + } +}; \ No newline at end of file diff --git a/examples/html/sample-0.html b/examples/html/sample-0.html new file mode 100644 index 00000000..b100eeff --- /dev/null +++ b/examples/html/sample-0.html @@ -0,0 +1 @@ +
Test
\ No newline at end of file diff --git a/examples/html/sample-1.html b/examples/html/sample-1.html new file mode 100644 index 00000000..34ed8247 --- /dev/null +++ b/examples/html/sample-1.html @@ -0,0 +1 @@ +
Test
\ No newline at end of file diff --git a/package.json b/package.json index e1996eae..552a1365 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "atomizer", - "version": "0.2.5", + "version": "1.0.0-alpha.1", "description": "A tool for creating Atomic CSS, a collection of single purpose styling units for maximum reuse", "main": "./lib/atomic.js", "contributors": [ @@ -30,12 +30,12 @@ "object-assign": "^2.0.0", "absurd": "~0.3.34", "minimist": "~1.1.0", - "chalk": "~0.5.1", - "lodash": "~3.2.0" + "chalk": "~1.0.0", + "lodash": "~3.3.0" }, "devDependencies": { "mocha": "~2.1.0", - "chai": "~2.0.0", + "chai": "~2.1.0", "sinon": "~1.12.2", "sinon-chai": "~2.7.0", "istanbul": "~0.3.5", diff --git a/src/atomizer.js b/src/atomizer.js index 45fbe8f7..c20dfd1f 100644 --- a/src/atomizer.js +++ b/src/atomizer.js @@ -12,8 +12,313 @@ var AtomicBuilder = require('./lib/AtomicBuilder.js'); var objectAssign = require('object-assign'); var rules = require('./rules.js'); -module.exports = function (configObjs, options) { +var atomicRegex; +var verbose = false; +/** + * helper function to handle merging array of objects + * @param {mixed} a Data of the first merge param + * @param {mixed} b Data of the second merge param + * @return {mixed} The merged object + */ +function handleMergeArrays (a, b) { + if (_.isArray(a) && _.isArray(b)) { + a.forEach(function(item){ + if(_.findIndex(b, item) === -1) { + b.push(item); + } + }); + return b; + } +} + +/** + * Get the unit of a length. + * @param {string} value The length to be parsed. + * @return {string|false} The unit of the string or false if length is not a number. + */ +function getUnit(value) { + if (isNaN(parseFloat(value, 10))) { + return false; + } + return value.replace(/^[\d\.\s]+/, ''); +} + +/** + * Tells wether a value is a length unit or not + * @param {string} value The value to be tested. + * @return {Boolean} + */ +function isLength(value) { + return parseInt(value, 10) === 0 || (/^-?(?:\d+)?\.?\b\d+[a-z]+$/.test(value) && ['em', 'ex', 'ch', 'rem', 'vh', 'vw', 'vmin', 'vmax', 'px', 'mm', 'cm', 'in', 'pt', 'pc'].indexOf(getUnit(value)) >= 0); +} + +/** + * Tells wether a value is a percentage or not + * @param {string} value The value to be tested. + * @return {Boolean} + */ +function isPercentage(value) { + return /^-?(?:\d+)?\.?\b\d+%$/.test(value); +} + +/** + * Tells wether a value is a hex value or not + * @param {string} value The value to be tested. + * @return {Boolean} + */ +function isHex(value) { + return /^[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(value); +} + +/** + * Tells wether a value is an integer or not + * @param {string} value The value to be tested. + * @return {Boolean} + */ +function isInteger(value) { + value = parseInt(value, 10); + return !isNaN(value) && (value % 1) === 0; +} + +/** + * Tells wether a value is a float or not + * @param {string} value The value to be tested. + * @return {Boolean} + */ +function isFloat(value) { + value = parseFloat(value, 10); + return (!isNaN(value) && value.toString().indexOf('.') !== -1); +} + +/** + * helper function to handle merging array of objects + * @param {mixed} a Data of the first merge param + * @param {mixed} b Data of the second merge param + * @return {mixed} The merged object + */ +function handleMergeArrays (a, b) { + if (_.isArray(a) && _.isArray(b)) { + a.forEach(function(item){ + if(_.findIndex(b, item) === -1) { + b.push(item); + } + }); + return b; + } +} + +/** + * Escapes special regular expression characters + * @param {string} str The regexp string. + * @return {string} The escaped regexp string. + */ +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); +} + +/** + * Returns a regular expression with atomic classes based on the rules from atomizer. + * Making it as a function for better separation (code style). + * @return {RegExp} The regular expression containing the atomic classes. + */ +function getAtomicRegex(rules) { + var regexes = []; + + rules.forEach(function (pattern) { + var prefix = pattern.prefix || ''; + prefix = prefix.replace('.', ''); + + if (pattern.rules) { + pattern.rules.forEach(function (rule) { + regexes.push(escapeRegExp(prefix + rule.suffix) + '\\b'); + }); + } else { + // custom-only patterns with no rules + } + if (pattern.prefix) { + regexes.push('\\b' + escapeRegExp(prefix) + '(?:(?:neg)?[0-9]+(?:\.[0-9]+)?(?:[a-zA-Z%]+)?|[0-9a-f]{3}(?:[0-9a-f]{3})?|[a-zA-Z0-9%\.]+\\b)'); + } + }); + return new RegExp('(' + regexes.join('|') + ')', 'g'); +} + +/** + * Get an atomic config rule given an atomic class name + * @param {string} className An atomic class name + * @param {object} currentConfig The current config. + * @param {array} warnings An array of warnings generated while processing custom config rules + * @return {object} The config rule for the given class name. + */ +function getConfigRule(className, currentConfig, warnings) { + var sepIndex = className.indexOf('-') + 1; + var prefix = '.' + className.substring(0, sepIndex); + var suffix = className.substring(sepIndex); + var configRule = {}; + var value; + + // iterate rules to find the pattern that the className belongs to + rules.some(function (pattern) { + var patternRulesLength = 0; + + // filter to the prefix we're looking for + if (pattern.prefix === prefix) { + // set the id in the config rule + configRule[pattern.id] = {}; + + // if the pattern has rules, let's find the suffix + if (pattern.rules) { + patternRulesLength = pattern.rules.length; + pattern.rules.some(function (rule, index) { + // found the suffix, place it in the config + if (rule.suffix === suffix) { + configRule[pattern.id][suffix] = true; + return true; + } + // it's a custom suffix + else if (patternRulesLength === index + 1) { + configRule = handleCustomConfigRule(configRule, className, pattern, suffix, currentConfig, warnings); + return true; + } + }); + } + // no pattern.rules, then it's a custom suffix + else { + configRule = handleCustomConfigRule(configRule, className, pattern, suffix, currentConfig, warnings); + } + return true; + } + }); + + return configRule; +} + +/** + * Used by getConfigRule to handle custom config rules + * @param {object} configRule The config rule object being built. + * @param {string} className The class name of the custom class to be evaluated. + * @param {object} pattern The pattern that matches this class in atomic css. + * @param {string} suffix The suffix of the class. + * @param {object} currentConfig An existing config to merge with + * @param {array} warnings An array of warnings generated while processing custom config rules + * @return {object} The custom config rule. + */ +function handleCustomConfigRule(configRule, className, pattern, suffix, currentConfig, warnings) { + var value; + + if (pattern.allowSuffixToValue && ( + isPercentage(suffix) || + isLength(suffix) || + isHex(suffix) || + isInteger(suffix) || + isFloat(suffix))) { + + if (!configRule[pattern.id].custom) { + configRule[pattern.id].custom = []; + } + + value = isHex(suffix) ? '#' + suffix : suffix; + configRule[pattern.id].custom.push({ + suffix: suffix, + values: [value] + }); + if (verbose) { + console.warn('Found `' + className + '`, config has been added.'); + } + } else { + if (!currentConfig[pattern.id] || !currentConfig[pattern.id].custom) { + warnMissingClassInConfig(className, pattern.id, suffix, warnings); + return false; + } + + if (currentConfig[pattern.id].custom.every(function (custom) { + return custom.suffix !== suffix; + })) { + warnMissingClassInConfig(className, pattern.id, suffix, warnings); + }; + } + + return configRule; +} + + +/** + * Used to log warning messages about missing classes in the config + * @param {string} className The missing class name. + * @param {string} id The id of the pattern. + * @param {string} suffix The suffix of the class. + * @param {array} warnings An array of warnings to be displayed to the user + * @void + */ +function warnMissingClassInConfig(className, patternId, suffix, warnings) { + warnings.push([ + 'Warning: Class `' + className + '` is ambiguous, and must be manually added to your config file:', + '\'' + patternId + '\'' + ':' + '{', + ' custom: [', + ' {suffix: \'' + suffix + '\', values: [\'YOUR-CUSTOM-VALUE\']}', + ' ]', + '}' + ].join("\n")); +} + +/** + * Look for atomic class names in text and add to class names object. + * @param {string} src The text to be parsed. + * @param {object} classNamesObj The classNames object. + * @return {array} An array of class names. + */ +var parse = function (src, classNamesObj) { + if (!atomicRegex) { + atomicRegex = getAtomicRegex(rules); + } + + var match = atomicRegex.exec(src); + while (match !== null) { + classNamesObj[match[0]] = classNamesObj[match[0]] ? classNamesObj[match[0]] + 1 : 1; + match = atomicRegex.exec(src); + } + + return classNamesObj; +}; + +/** + * Get config object given an array of atomic class names. + * @param {object} classNamesObj The object of atomic class names. + * @param {object} currentConfig The current config. + * @param {boolean} verboseLogging Verbose logging (default = false) + * @return {object} The atomic config object. + */ +var getConfig = function (classNamesObj, currentConfig, verboseLogging) { + var config = {}, + warnings = [], + className; + + verbose = !!verboseLogging; + + for (className in classNamesObj) { + if (classNamesObj.hasOwnProperty(className)) { + config = _.merge(config, getConfigRule(className, currentConfig, warnings), handleMergeArrays); + } + } + + // Now that we've processed all the configuration, notify the user + // if any custom classnames were found that were too ambiguous to + // have their config auto-generated. + if (warnings.length) { + warnings.forEach(function (w) { + console.warn(w); + }); + } + + return config; +} + +/** + * createCSS() + * + * Converts configuration JSON into CSS + */ +var createCSS = function (config, options) { var content; options = objectAssign({}, { @@ -26,14 +331,10 @@ module.exports = function (configObjs, options) { banner: '' }, options); - if (!configObjs) { + if (!config) { throw new Error('No configuration provided.'); } - if (!_.isArray(configObjs)) { - configObjs = [configObjs]; - } - var api = Absurd(); api.morph(options.morph); @@ -42,14 +343,13 @@ module.exports = function (configObjs, options) { api.import(options.require); } - configObjs.forEach(function (config) { - var atomicBuilder = new AtomicBuilder(rules, config); - var build = atomicBuilder.getBuild(); - if (!_.size(build)) { - throw new Error('Failed to generate CSS. The `build` object is empty.'); - } - api.add(build); - }); + var atomicBuilder = new AtomicBuilder(rules, config); + var build = atomicBuilder.getBuild(); + if (!_.size(build)) { + throw new Error('Failed to generate CSS. The `build` object is empty.'); + } + + api.add(build); api.compile(function(err, result) { /* istanbul ignore if else */ @@ -62,3 +362,8 @@ module.exports = function (configObjs, options) { return content; }; +module.exports = { + createCSS: createCSS, + parse: parse, + getConfig: getConfig +};