diff --git a/CHANGELOG.md b/CHANGELOG.md index fc6d2826..a758f662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ -## 0.2.14 (2014-12-10) +## 0.2.15 (2014-12-17) -### Fixes: +### Critical and major changes +* Find variable declarations from every file. Use styleVariables to filter selected files (#[344](https://github.com/SC5/sc5-styleguide/pull/344)) +* Feature: Custom KSS parameter for wrapper markup (#[338](https://github.com/SC5/sc5-styleguide/pull/338)) + **The syntax for declaring a component wrapper has been changed. It is not compartible anymore. When updating change + you wrapper components according to [documentation](https://github.com/SC5/sc5-styleguide#wrapper-markup).** -* Fix test directive when running gulp dev (#[335](https://github.com/SC5/sc5-styleguide/pull/335)) -* Fix: Include demo-gulpfile.js to NPM package. Fixes demo (#[339](https://github.com/SC5/sc5-styleguide/pull/339)) -* Fix typo in demo gulp file (#[336](https://github.com/SC5/sc5-styleguide/pull/336)) +### Bug fixes +* Allow empty single-line comments (#[345](https://github.com/SC5/sc5-styleguide/pull/345)) +* Fixes failed styleguide generation when section modifier has no markup (#[343](https://github.com/SC5/sc5-styleguide/pull/343)) +* Fix: Do not detect @imports as variables (#[342](https://github.com/SC5/sc5-styleguide/pull/342)) -### Improvements: - -* Remove unused scoped styles from processing flow (#[337](https://github.com/SC5/sc5-styleguide/pull/337)) \ No newline at end of file +### Internal changes +* Instruction how to deal with branches (#[346](https://github.com/SC5/sc5-styleguide/pull/346)) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index ff1ad599..07b2b137 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,5 +1,9 @@ # Development instructions +### Branches +The project is developed in `dev` branch. New feature or fix should come with a pull request from a fork. You can make a +pull request from either your `dev` branch or from a feature branch. + ### Running development server and watches The project contains a small demo stylesheet that can be used to develop the UI. @@ -35,13 +39,14 @@ To be able to check during development, please 1. Check that all the needed pull requests are merged 1. Make sure that your clone fetched all the tags which exist in the SC5 repo -1. Rebase your `master` branch against SC5 -1. Create `release/x.y.z` branch with the number of upcoming version and switch to it +1. Rebase your `dev` branch against SC5 +1. Create `release/x.y.z` branch from `dev` with the number of upcoming version and switch to it 1. Increment the package number in `package.json` 1. Run `gulp publish` 1. Check the `CHANGELOG.md` file. You can remove not needed items or rename them. 1. Commit changes -1. Make a pull request from your feature branch +1. Make a pull request from your feature branch into `dev` +1. Once it is merged, make a pull request from `dev` to `master` 1. Once your pull request is merged, rebase your `master` against SC5 again 1. Run `npm publish` 1. Create a versioning tag in GitHub. Insert the `CHANGELOG.md` content as a description of this versioning tag. diff --git a/README.md b/README.md index 084df89e..f02a54f5 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Port in which the server will run Automatically generate style guide on file change. `--watch` does not run server. Combile with `--server` if you want to run server -Config JSON file could contain following settings +Config JSON file could look like following: { title: "My Style guide", @@ -101,7 +101,6 @@ For more specific documentation. See [Build options](#build-options) section. title: "My Styleguide", server: true, rootPath: outputPath, - styleVariables: '', overviewPath: "", sass: { // Options passed to gulp-sass @@ -142,7 +141,6 @@ Then you are able to use the same gulp task inside you `Gruntfile`: title: "My Styleguide", server: true, rootPath: outputPath, - styleVariables: '', overviewPath: "", sass: { // Options passed to gulp-sass @@ -231,7 +229,7 @@ This allows Angular to deal with the routing. However, the static files should b **styleVariables** (string, optional) -Path to the file containing SASS variables that can be used as modifiers in the KSS notation. +By default variable definitions are searched from every file passed in gulp.src. styleVariables parameter could be used to filter from which files variables are loaded. **filesConfig** (array, optional) **(Experimental feature)** @@ -265,36 +263,36 @@ Sometimes your component examples need a wrapper. For example: * your component is not visible with white background; * your comnponent needs a container with a predefined height. -You can cover such cases by adding a wrapper to a component markup. The wrapper should go after the example in -markup: +You can cover such cases by adding a wrapper to a component markup. The wrapper should be defined as a custom parmater +in the KSS documentation block: ``` // markup: //
  • // Item //
  • -// +// +// sg-wrapper: // -// ``` -Here a piece of markup between `` and `` tags is a wrapper. The `` +The `` inside shows where to place an example. Wrappers can be used for fixes like this: ``` // markup: -//
    This is a white compoennt
    -// +//
    This is a white component
    +// +// sg-wrapper: //
    -// +// //
    -//
    ``` The modifiers get the same wrapper as their parent section. @@ -307,9 +305,10 @@ following KSS markup // // markup: //
    -// +// +// sg-wrapper: //
    -// +// //
    // // Styleguide 1.0 @@ -320,9 +319,10 @@ following KSS markup // // markup: // -// +// +// sg-wrapper: //
    -// +// //
    // // Styleguide 1.1 diff --git a/lib/app/js/services/Styleguide.js b/lib/app/js/services/Styleguide.js index 8866add0..9138c50a 100644 --- a/lib/app/js/services/Styleguide.js +++ b/lib/app/js/services/Styleguide.js @@ -13,6 +13,7 @@ angular.module('sgApp') this.sections = {}; this.config = {}; + this.variables = {}; this.status = { hasError: false, error: {} @@ -24,6 +25,7 @@ angular.module('sgApp') url: 'styleguide.json' }).success(function(response) { _this.config.data = response.config; + _this.variables.data = response.variables; _this.sections.data = response.sections; }); }; diff --git a/lib/app/js/services/Variables.js b/lib/app/js/services/Variables.js index 1e968c2d..55291414 100644 --- a/lib/app/js/services/Variables.js +++ b/lib/app/js/services/Variables.js @@ -16,25 +16,29 @@ _this.refreshDirtyStates(); }, true); - this.getLocalVarByName = function(name) { + this.variableMatches = function(var1, var2) { + return var1.name === var2.name && var1.file === var2.file; + }; + + this.getLocalVar = function(variable) { for (var i = this.variables.length - 1; i >= 0; i--) { - if (this.variables[i].name === name) { + if (this.variableMatches(this.variables[i], variable)) { return this.variables[i]; } } }; - this.getLocalIndexByName = function(name) { + this.getLocalIndex = function(variable) { for (var i = this.variables.length - 1; i >= 0; i--) { - if (this.variables[i].name === name) { + if (this.variableMatches(this.variables[i], variable)) { return i; } } }; - this.getServerVarByName = function(name) { + this.getServerVar = function(variable) { for (var i = serverData.length - 1; i >= 0; i--) { - if (serverData[i].name === name) { + if (this.variableMatches(serverData[i], variable)) { return serverData[i]; } } @@ -44,7 +48,7 @@ var _this = this; // Mark variables that differ from the server version as dirty angular.forEach(_this.variables, function(variable) { - var serverVar = _this.getServerVarByName(variable.name); + var serverVar = _this.getServerVar(variable); if (serverVar && serverVar.value !== variable.value && !variable.dirty) { variable.dirty = true; } else if (serverVar && serverVar.value === variable.value && variable.dirty) { @@ -59,28 +63,30 @@ } else { for (var i = 0; i < serverData.length; i++) { var oldIndex; - if (this.variables[i] && this.variables[i].name !== serverData[i].name) { - if (!this.getServerVarByName(this.variables[i].name)) { + if (this.variables[i] && !this.variableMatches(this.variables[i], serverData[i])) { + if (!this.getServerVar(this.variables[i])) { // This variable does not exists anymore on the server. Remove it this.variables.splice(i, 1); - } else if (this.getLocalVarByName(serverData[i].name) && !this.getLocalVarByName(serverData[i].name).dirty) { + } else if (this.getLocalVar(serverData[i]) && !this.getLocalVar(serverData[i]).dirty) { // The variable already exists but in another position // It is not changed so we can just remove it - oldIndex = this.getLocalIndexByName(serverData[i].name); + oldIndex = this.getLocalIndex(serverData[i]); this.variables.splice(oldIndex, 1); - this.variables.splice(i, 0, {name: serverData[i].name, value: serverData[i].value}); - } else if (this.getLocalVarByName(serverData[i].name)) { + this.variables.splice(i, 0, angular.copy(serverData[i])); + } else if (this.getLocalVar(serverData[i])) { // The variable already exists but in another position // It is changed so we need to keep the old values - oldIndex = this.getLocalIndexByName(serverData[i].name); + oldIndex = this.getLocalIndex(serverData[i]); var oldValue = this.variables[oldIndex].value; this.variables.splice(oldIndex, 1); - this.variables.splice(i, 0, {name: serverData[i].name, value: oldValue}); + var newObject = angular.copy(serverData[i]); + newObject.value = oldValue; + this.variables.splice(i, 0, newObject); } else { // The variable does not exists anywhere else. Just add it - this.variables.splice(i, 0, {name: serverData[i].name, value: serverData[i].value}); + this.variables.splice(i, 0, angular.copy(serverData[i])); } - } else if (this.variables[i] && this.variables[i].name === serverData[i].name) { + } else if (this.variables[i] && this.variableMatches(this.variables[i], serverData[i])) { // Variable exists already locally // Update value if variable does not have any local changes if (!this.variables[i].dirty) { @@ -88,7 +94,7 @@ } } else if (!this.variables[i]) { // Add new local variable - this.variables.push({name: serverData[i].name, value: serverData[i].value}); + this.variables.push(angular.copy(serverData[i])); } } } @@ -98,7 +104,7 @@ var _this = this; // Reset every key to corresponding server value angular.forEach(this.variables, function(variable) { - var serverVar = _this.getServerVarByName(variable.name); + var serverVar = _this.getServerVar(variable); if (serverVar) { variable.value = serverVar.value; } @@ -117,29 +123,9 @@ return this; }; - this.sync = function(method) { - var validMethods = ['load', 'save']; - - // Parameter validation - if (validMethods.indexOf(method) === -1) { - throw 'No valid method provided. Available methods: ' + validMethods.join(', '); - } - - // Load variables from server or localStorage - if (method === 'load') { - this.socket.emit('request variables from server'); - } - - // Save variables to server - if (method === 'save') { - this.socket.emit('variables to server', this.variables); - } - }; - this.saveVariables = function() { - var _this = this; if (this.socket) { - _this.sync('save'); + this.socket.emit('variables to server', this.variables); } else { throw 'Socket not available.'; } @@ -152,14 +138,10 @@ // Update new server data when it is available $rootScope.$watch(function() { - return Styleguide.config.data; + return Styleguide.variables.data; }, function(newValue) { if (newValue) { - if (newValue.settings) { - serverData = newValue.settings; - } else { - serverData = []; - } + serverData = newValue; _this.refreshValues(); _this.refreshDirtyStates(); } diff --git a/lib/app/sass/app.scss b/lib/app/sass/app.scss index 25375a99..e34c6aac 100644 --- a/lib/app/sass/app.scss +++ b/lib/app/sass/app.scss @@ -482,13 +482,13 @@ $mobile: new-breakpoint(max-width 480px); //
  • // Item //
  • -// +// +// sg-wrapper: // -// // // Styleguide 3.3.1 diff --git a/lib/modules/io.js b/lib/modules/io.js index 112c412a..63f0d8d0 100644 --- a/lib/modules/io.js +++ b/lib/modules/io.js @@ -1,12 +1,11 @@ var fs = require('fs'), path = require('path'), + Q = require('q'), parser = require('./variable-parser'); module.exports = function(ioServer, options) { - var loadVariables, - saveVariables, - io = ioServer, + var io = ioServer, compileError = false; if (options.styleVariables && !fs.existsSync(options.styleVariables)) { @@ -14,35 +13,49 @@ module.exports = function(ioServer, options) { return; } - loadVariables = function(socket) { - return function() { - var values, syntax = path.extname(options.styleVariables).substring(1); - fs.readFile(options.styleVariables, {encoding: 'utf8'}, function(err, data) { - if (err) { - return console.error(err); - } - values = parser.parseVariables(data, syntax); - socket.emit('variables from server', values); - console.log('EVENT: variables from server'); + function groupVariablesByFilename(variables) { + return variables.reduce(function(prev, curr) { + var filePath = curr.file; + if (!prev[filePath]) { + prev[filePath] = []; + } + prev[filePath].push(curr); + return prev; + }, {}); + } + + function saveVariables(variables) { + return Q.Promise(function(resolve) { + + // First group variables by file name + var groupedVariables = groupVariablesByFilename(variables), + filePromises = []; + + // Go trough every file and update variables defined in that file + Object.keys(groupedVariables).forEach(function(filePath) { + filePromises.push(Q.promise(function(resolveFile) { + var fileVariables = groupedVariables[filePath], + syntax = path.extname(filePath).substring(1); + + // Read original file contents to be updated + fs.readFile(filePath, {encoding: 'utf8'}, function(err, originalData) { + // Update variables and store results back to the original file + var data = parser.setVariables(originalData, syntax, fileVariables); + fs.writeFile(filePath, data, function(err) { + if (err) { + console.error(err); + } + resolveFile(); + }); + }); + })); }); - }; - }; - saveVariables = function(socket) { - return function(variables) { - var syntax = path.extname(options.styleVariables).substring(1); - fs.readFile(options.styleVariables, {encoding: 'utf8'}, function(err, originalData) { - var data = parser.setVariables(originalData, syntax, variables); - fs.writeFile(options.styleVariables, data, function(err, data) { - if (err) { - return console.error(err); - } - socket.emit('variables saved to server', data); - console.log('EVENT: variables saved to server'); - }); + Q.all(filePromises).then(function() { + resolve(); }); - }; - }; + }); + } function emitProgressStart() { io.sockets.emit('styleguide progress start'); @@ -66,8 +79,12 @@ module.exports = function(ioServer, options) { io.on('connection', function(socket) { console.log('Socket connection established (id:', socket.conn.id + ')'); - socket.on('request variables from server', loadVariables(socket)); - socket.on('variables to server', saveVariables(socket)); + socket.on('variables to server', function(variables) { + saveVariables(variables).then(function() { + socket.emit('variables saved to server'); + console.log('EVENT: variables saved to server'); + }); + }); if (compileError) { emitCompileError(); } else { @@ -76,6 +93,8 @@ module.exports = function(ioServer, options) { }); return { + saveVariables: saveVariables, + groupVariablesByFilename: groupVariablesByFilename, emitProgressStart: emitProgressStart, emitCompileError: emitCompileError, emitCompileSuccess: emitCompileSuccess diff --git a/lib/modules/kss-additional-params.js b/lib/modules/kss-additional-params.js new file mode 100644 index 00000000..ff38b662 --- /dev/null +++ b/lib/modules/kss-additional-params.js @@ -0,0 +1,37 @@ +'use strict'; + +function trimLinebreaks(str) { + // Remove leading and trailing linebreaks + if (!str) { + return str; + } + return str.replace(/^[\r\n]+|[\r\n]+$/g, ''); +} + +module.exports = { + + /* Parses additional KSS params for the styleguide */ + get: function(source) { + + // Remove comment markup from comments + var comment = source.split('//').join('').split('/*').join('').split('*/').join(''), + additionalKssParams = {}; + + comment = trimLinebreaks(comment); + + comment.split('\n\n').forEach(function(markUpBlock) { + + var varName = markUpBlock.match(/([^:^\n]*):[\s\S]*\n/); + if (varName && varName[1]) { + varName = varName[1].trim(); + } + if (varName && varName.substring(0, 3) === 'sg-') { + additionalKssParams[varName] = markUpBlock.substring(markUpBlock.indexOf('\n') + 1); + } + + }); + + return additionalKssParams; + } + +}; diff --git a/lib/modules/kss-parser.js b/lib/modules/kss-parser.js index eaf8af1a..f8a31558 100644 --- a/lib/modules/kss-parser.js +++ b/lib/modules/kss-parser.js @@ -5,6 +5,8 @@ var kss = require('kss'), Q = require('q'), gutil = require('gulp-util'), kssSplitter = require('./kss-splitter'), + kssAdditionalParams = require('./kss-additional-params'), + _ = require('lodash'), sanitizeHtml = require('sanitize-html'); // Parses kss.KssSection to JSON @@ -47,8 +49,10 @@ function sanitize(string) { return sanitizeHtml(string, {allowedTags: [], allowedAttributes: []}); } -function processBlock(block, options, json) { +function processBlock(block, options) { return Q.Promise(function(resolve, reject) { + + // Parse with original KSS library kss.parse(block.kss, options, function(err, styleguide) { var section, blockStyles; @@ -65,19 +69,21 @@ function processBlock(block, options, json) { } blockStyles = trimLinebreaks(block.code); + // Add extra parameters + section[0] = _.assign(section[0], kssAdditionalParams.get(block.kss)); + // Add related CSS to section if (blockStyles && blockStyles !== '') { section[0].css = blockStyles; } - json.sections = json.sections.concat(section); } - resolve(); + resolve(section); } }); }); } -function processFile(contents, syntax, options, json) { +function processFile(contents, syntax, options) { return Q.Promise(function(resolve, reject) { var blockPromises = [], blocks; @@ -86,12 +92,25 @@ function processFile(contents, syntax, options, json) { // Process every block in the current file blocks.forEach(function(block) { - blockPromises.push(processBlock(block, options, json)); + blockPromises.push(processBlock(block, options)); }); } catch (err) { reject(err); } - Q.all(blockPromises).then(resolve); + Q.all(blockPromises).then(function(results) { + resolve(results.reduce(function(memo, result) { + var blockResult = result.valueOf(); + if (blockResult && blockResult.length > 0) { + // Map syntax to every block. This is later used when parsing used variables + // Finally add sections to array + return memo.concat(blockResult.map(function(currentBlock) { + currentBlock.syntax = syntax; + return currentBlock; + })); + } + return memo; + }, [])); + }); }); } @@ -124,25 +143,31 @@ function bySectionReference(x, y) { module.exports = { // Parse node-kss object ( {'file.path': 'file.contents.toString('utf8'}' ) - parseKSS: function(files, options) { + parseKssSections: function(files, options) { return Q.Promise(function(resolve, reject) { - var json = { - sections: [] - }, - filePromises = [], - fileKeys = Object.keys(files); + var filePromises = [], + sections = []; - fileKeys.forEach(function(filePath) { + // Process every file + Object.keys(files).forEach(function(filePath) { var contents = files[filePath], syntax = path.extname(filePath).substring(1); - filePromises.push(processFile(contents, syntax, options, json)); + filePromises.push(processFile(contents, syntax, options)); }); + // All files are processed + Q.all(filePromises).then(function(results) { + // Combine sections from every file to a single array + results.map(function(result) { + var fileSections = result.valueOf(); + if (fileSections && fileSections.length > 0) { + sections = sections.concat(fileSections); + } + }); - Q.all(filePromises).then(function() { - // All files are processed. Sort results and call main promise + // Sort sections by reference number and call main promise try { - json.sections.sort(bySectionReference); - resolve(json); + sections.sort(bySectionReference); + resolve(sections); } catch (err) { reject(err); } diff --git a/lib/modules/variable-parser.js b/lib/modules/variable-parser.js index 9cb4dff1..e887041f 100644 --- a/lib/modules/variable-parser.js +++ b/lib/modules/variable-parser.js @@ -1,5 +1,7 @@ var gonzales = require('gonzales-pe'), gonzo = require('gonzales-ast'), + path = require('path'), + Q = require('q'), _ = require('lodash'); function astToSrc(ast, syntax) { @@ -10,7 +12,7 @@ function astToSrc(ast, syntax) { } // Parse Style variables to object -module.exports.parseVariables = function(string, syntax) { +function parseVariableDeclarations(string, syntax) { syntax = syntax || 'scss'; var out = [], @@ -40,27 +42,31 @@ module.exports.parseVariables = function(string, syntax) { var varName = nodes[1][1][1], varVal = ''; - /* Grabs all the listed values - * Fix then https://github.com/tonyganch/gonzales-pe/issues/17 is fixed */ - nodes.forEach(function(element) { - if (element === 'atrules' || element[0] === 'atkeyword') { - return; - } - if (element[0] === 'operator' && element[1] === ':') { - return; - } - varVal += astToSrc(element, syntax); // Syntax is always less as this visitor is only for LESS - }); + // Skip at-keywords that do not decrade variable (Ex. @imports) + if (nodes[2][0] === 'operator' && nodes[2][1] === ':') { + /* Grabs all the listed values + * Fix then https://github.com/tonyganch/gonzales-pe/issues/17 is fixed */ + nodes.forEach(function(element) { + if (element === 'atrules' || element[0] === 'atkeyword') { + return; + } + if (element[0] === 'operator' && element[1] === ':' + ) { + return; + } + varVal += astToSrc(element, syntax); // Syntax is always less as this visitor is only for LESS + }); - out.push({name: varName, value: varVal.trim()}); + out.push({name: varName, value: varVal.trim()}); + } } }]); return out; -}; +} // Parse Style variables to object -module.exports.findVariables = function(string, syntax) { +function findVariables(string, syntax) { syntax = syntax || 'scss'; var out = [], @@ -94,14 +100,57 @@ module.exports.findVariables = function(string, syntax) { } }]); return out; -}; +} + +function byFileName(a, b) { + if (a.file < b.file) { + return -1; + } + if (a.file > b.file) { + return 1; + } + return 0; +} + +function parseVariableDeclarationsFromFiles(files) { + return Q.Promise(function(resolve) { + var variables = [], + filePromises = []; + + Object.keys(files).forEach(function(filePath) { + var contents = files[filePath], + syntax = path.extname(filePath).substring(1); + filePromises.push(Q.promise(function(resolve) { + var fileVariables = parseVariableDeclarations(contents, syntax); + + // Map correct file name to every variable + fileVariables.map(function(variable) { + variable.file = filePath; + return variable; + }); + + variables = variables.concat(fileVariables); + resolve(); + })); + }); + + Q.all(filePromises).then(function() { + // All files are processed. Sort variables by file name + variables.sort(byFileName); + // Call main promise + resolve(variables); + }); + }); +} // Modifies string so that variables passed in object are updated -module.exports.setVariables = function(string, syntax, variables) { +function setVariables(string, syntax, variables) { var ast, result, changeVariable; changeVariable = function(ast, key, value) { - return gonzo.traverse(ast, [{ + + var visitors = { + sass: { // Visitor for SASS and SCSS syntaxes test: function(name, nodes) { return name === 'declaration' && nodes[1][0] === 'variable' && nodes[1][1].indexOf(key) !== -1; @@ -111,7 +160,7 @@ module.exports.setVariables = function(string, syntax, variables) { return node; } }, - { + less: { // Visitor for LESS syntax test: function(name) { return name === 'atrules'; @@ -144,7 +193,10 @@ module.exports.setVariables = function(string, syntax, variables) { return node; } } - ]); + }; + visitors.scss = visitors.sass; + + return gonzo.traverse(ast, [visitors[syntax]]); }; ast = gonzales.srcToAST({ @@ -161,9 +213,9 @@ module.exports.setVariables = function(string, syntax, variables) { syntax: syntax }); return result; -}; +} -module.exports.findModifierVariables = function(modifiers) { +function findModifierVariables(modifiers) { var out = []; if (modifiers) { modifiers.forEach(function(modifier) { @@ -173,4 +225,12 @@ module.exports.findModifierVariables = function(modifiers) { }); } return out; +} + +module.exports = { + parseVariableDeclarations: parseVariableDeclarations, + parseVariableDeclarationsFromFiles: parseVariableDeclarationsFromFiles, + setVariables: setVariables, + findVariables: findVariables, + findModifierVariables: findModifierVariables }; diff --git a/lib/modules/wrapper-markup.js b/lib/modules/wrapper-markup.js index c70f505e..a3a7466c 100644 --- a/lib/modules/wrapper-markup.js +++ b/lib/modules/wrapper-markup.js @@ -1,16 +1,3 @@ -/* Calculates a warpper for a section */ -function ownWrapper(section) { - var re, match, - wrapper = ''; - - re = /^([\s\S]*)([\s\S]*)<\/sg\:wrapper>([\s\S]*)$/; - match = re.exec(section.markup); - if (match) { - wrapper = match[2]; - } - return wrapper; -} - function buildReferenceDictionary(sections) { var prevSections = [], index = {}, @@ -52,36 +39,28 @@ function buildReferenceDictionary(sections) { module.exports.generateSectionWrapperMarkup = function(sections) { /* Calculate own wrapper */ var wrapper, + wrapperKeyword = '', index = buildReferenceDictionary(sections); return sections.map(function(section) { - wrapper = ownWrapper(section); + wrapper = section['sg-wrapper'] || wrapperKeyword; /* Wrap a wrapper with a parent wrapper */ if (section.parentReference) { var parentWrapper = index[section.parentReference].wrapper; - wrapper = parentWrapper.replace('', wrapper); + wrapper = parentWrapper.replace(wrapperKeyword, wrapper); } section.wrapper = wrapper; - if (section.markup) { - /* Clean markup */ - if (section.markup.indexOf('') !== -1) { - section.markup = section.markup.substring(0, section.markup.indexOf('', section.markup); + section.wrappedMarkup = section.wrapper.replace(wrapperKeyword, section.markup); } /* Wrap modifiers */ section.modifiers.forEach(function(modifier) { - /* Clean modifier markup */ - if (modifier.markup.indexOf('') !== -1) { - modifier.markup = modifier.markup.substring(0, modifier.markup.indexOf('', modifier.markup); + modifier.wrappedMarkup = section.wrapper.replace(wrapperKeyword, modifier.markup); }); /* Clean nested sections */ diff --git a/lib/styleguide.js b/lib/styleguide.js index ef655588..f3402756 100644 --- a/lib/styleguide.js +++ b/lib/styleguide.js @@ -10,18 +10,17 @@ 'use strict'; var through = require('through2'), - fs = require('fs'), gulp = require('gulp'), rename = require('gulp-rename'), markdown = require(__dirname + '/modules/markdown'), atRules = require(__dirname + '/modules/at-rules'), mustache = require('gulp-mustache'), variableParser = require(__dirname + '/modules/variable-parser'), - parseKSS = require(__dirname + '/modules/kss-parser').parseKSS, + kssParser = require(__dirname + '/modules/kss-parser'), pseudoSelectors = require(__dirname + '/modules/pseudo-selectors'), preprocess = require(__dirname + '/modules/preprocess'), wrapperMarkup = require(__dirname + '/modules/wrapper-markup'), - path = require('path'), + minimatch = require('minimatch'), sgServer = require(__dirname + '/server'), File = require('vinyl'), Q = require('q'), @@ -39,7 +38,7 @@ function sanitizeOptions(opt) { extraHead: (typeof opt.extraHead === 'object') ? opt.extraHead.join('\n') : opt.extraHead, appRoot: opt.appRoot || '', commonClass: opt.commonClass || '', - styleVariables: opt.styleVariables || '', + styleVariables: opt.styleVariables || false, server: opt.server || false, port: opt.port, rootPath: opt.rootPath, @@ -84,24 +83,29 @@ function removeConfigOutputPath(json) { } function appendUsedVariablesToEachBlock(opt, styleguide) { - if (opt.styleVariables) { - var syntax = path.extname(opt.styleVariables).substring(1); - // Parse variables from the defined file - styleguide.config.settings = variableParser.parseVariables(fs.readFileSync(opt.styleVariables, 'utf-8'), syntax); - // Go trough every styleguide style block and find used variables - styleguide.sections.forEach(function(section) { - if (section.css) { - section.variables = variableParser.findVariables(section.css, syntax); + // Go trough every styleguide style block and find used variables + styleguide.sections.forEach(function(section) { + if (section.css) { + section.variables = variableParser.findVariables(section.css, section.syntax); + } + variableParser.findModifierVariables(section.modifiers).forEach(function(varName) { + if (!section.variables || section.variables.indexOf(varName) === -1) { + section.variables = section.variables || []; + section.variables.push(varName); } - variableParser.findModifierVariables(section.modifiers).forEach(function(varName) { - if (!section.variables || section.variables.indexOf(varName) === -1) { - section.variables = section.variables || []; - section.variables.push(varName); - } - }); - return section; }); - } + return section; + }); +} + +function filterFiles(files, filter) { + var filtered = {}; + Object.keys(files).forEach(function(filePath) { + if (minimatch(filePath, filter)) { + filtered[filePath] = files[filePath]; + } + }); + return filtered; } module.exports = function(options) { @@ -113,7 +117,6 @@ module.exports = function(options) { }; function bufferFileContents(file, enc, done) { - if (file.isNull()) { return; } @@ -134,9 +137,22 @@ module.exports = function(options) { // A stream through which each file will pass return through(throughOpts, bufferFileContents, function(callback) { - var _this = this; + var _this = this, + // Styleguide object to be built + styleguide = {}, + // Parse KSS sections + parseKSSPromise = kssParser.parseKssSections(filesBuffer, opt.kssOpt), + // Filter variable files + // File paths are full absolute paths so we need to add wildcard prefix + // Also empty wildcard should return all files + variableFiles = opt.styleVariables ? filterFiles(filesBuffer, '**/' + opt.styleVariables) : filesBuffer, + // Parse variable decarations from files + parseVariablesPromise = variableParser.parseVariableDeclarationsFromFiles(variableFiles); + + Q.all([parseKSSPromise, parseVariablesPromise]).spread(function(sections, variables) { + styleguide.sections = sections; + styleguide.variables = variables; - parseKSS(filesBuffer, opt.kssOpt).then(function(styleguide) { function pushAllFiles() { return through.obj(function(file, enc, cb) { _this.push(file); diff --git a/package.json b/package.json index 8549be89..0dc39b9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sc5-styleguide", - "version": "0.2.14", + "version": "0.2.15", "description": "Styleguide generator is a handy little tool that helps you generate good looking styleguides from stylesheets using KSS notation.", "bin": { "styleguide": "./bin/styleguide" @@ -35,7 +35,7 @@ "debug": "~2.1.0", "event-stream": "^3.1.7", "express": "^4.10.4", - "gonzales-ast": "0.0.5", + "gonzales-ast": "0.0.6", "gonzales-pe": "^3.0.0-12", "gulp": "^3.8.10", "gulp-concat": "^2.4.2", @@ -50,10 +50,11 @@ "kss": "^1.2.0", "less": "^2.1.1", "lodash": "^2.4.1", + "minimatch": "^2.0.1", "morgan": "^1.5.0", "node-bourbon": "^1.2.3", - "node.extend": "^1.1.3", "node-neat": "^1.3.0", + "node.extend": "^1.1.3", "q": "^1.1.2", "run-sequence": "^1.0.2", "sanitize-html": "^1.4.3", @@ -91,7 +92,6 @@ "karma-phantomjs-launcher": "^0.1.4", "karma-sinon-chai": "^0.2.0", "main-bower-files": "^2.4.1", - "minimatch": "^2.0.1", "multiline": "^1.0.1", "proxyquire": "^1.1.0", "requirefrom": "^0.2.0", diff --git a/test/angular/unit/services/Variables.test.js b/test/angular/unit/services/Variables.test.js index a996b038..a7479ba8 100644 --- a/test/angular/unit/services/Variables.test.js +++ b/test/angular/unit/services/Variables.test.js @@ -19,13 +19,11 @@ describe('Service: Variables', function() { }; styleguideMock = { - config: { - data: { - settings: [ - {name: 'setting1', value: 'value1'}, - {name: 'setting2', value: 'value2'} - ] - } + variables: { + data: [ + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'} + ] } }; $provide.value('Styleguide', styleguideMock); @@ -43,39 +41,45 @@ describe('Service: Variables', function() { it('should get default values from Styleguide service', function() { rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'value1'}, - {name: 'setting2', value: 'value2'} + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'} ]); }); - it('should set empty array as server data if variables do not exists', function() { + it('should set empty array as server data no vairables are found', function() { rootScope.$digest(); - styleguideMock.config.data = {}; + styleguideMock.variables.data = []; rootScope.$digest(); expect(Variables.variables).to.eql([]); }); - it('should set empty array as server data no vairables are found', function() { + it('should not mark server side changes as dirty', function() { rootScope.$digest(); - styleguideMock.config.data = { - settings: [] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting1', value: 'new value1'}, + {file: 'file', name: 'setting2', value: 'new value2'} + ]; rootScope.$digest(); - expect(Variables.variables).to.eql([]); + expect(Variables.variables).to.eql([ + {file: 'file', name: 'setting1', value: 'new value1'}, + {file: 'file', name: 'setting2', value: 'new value2'} + ]); }); - it('should not mark server side changes as dirty', function() { + it('should allow same named variables from different files', function() { rootScope.$digest(); - styleguideMock.config.data = { - settings: [ - {name: 'setting1', value: 'new value1'}, - {name: 'setting2', value: 'new value2'} - ] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file2', name: 'setting1', value: 'value1'}, + {file: 'file2', name: 'setting2', value: 'value2'} + ]; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'new value1'}, - {name: 'setting2', value: 'new value2'} + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file2', name: 'setting1', value: 'value1'}, + {file: 'file2', name: 'setting2', value: 'value2'} ]); }); @@ -84,8 +88,25 @@ describe('Service: Variables', function() { Variables.variables[0].value = 'changed'; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'changed', dirty: true}, - {name: 'setting2', value: 'value2'} + {file: 'file', name: 'setting1', value: 'changed', dirty: true}, + {file: 'file', name: 'setting2', value: 'value2'} + ]); + }); + + it('should not mark variable as dirty if it is from different file', function() { + rootScope.$digest(); + styleguideMock.variables.data = [ + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file2', name: 'setting1', value: 'value1'} + ]; + rootScope.$digest(); + Variables.variables[2].value = 'changed'; + rootScope.$digest(); + expect(Variables.variables).to.eql([ + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file2', name: 'setting1', value: 'changed', dirty: true} ]); }); @@ -96,8 +117,8 @@ describe('Service: Variables', function() { Variables.variables[0].value = 'value1'; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'value1'}, - {name: 'setting2', value: 'value2'} + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'} ]); }); @@ -108,8 +129,8 @@ describe('Service: Variables', function() { Variables.variables[0].value = 'value1'; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'value1'}, - {name: 'setting2', value: 'value2'} + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'} ]); }); @@ -117,29 +138,25 @@ describe('Service: Variables', function() { rootScope.$digest(); Variables.variables[1].value = 'changed2'; rootScope.$digest(); - styleguideMock.config.data = { - settings: [ - {name: 'setting1', value: 'new value1'}, - {name: 'setting2', value: 'new value2'} - ] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting1', value: 'new value1'}, + {file: 'file', name: 'setting2', value: 'new value2'} + ]; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'new value1'}, - {name: 'setting2', value: 'changed2', dirty: true} + {file: 'file', name: 'setting1', value: 'new value1'}, + {file: 'file', name: 'setting2', value: 'changed2', dirty: true} ]); }); it('should remove local values that does not exist on server side', function() { rootScope.$digest(); - styleguideMock.config.data = { - settings: [ - {name: 'setting2', value: 'value2'} - ] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting2', value: 'value2'} + ]; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting2', value: 'value2'} + {file: 'file', name: 'setting2', value: 'value2'} ]); }); @@ -150,61 +167,55 @@ describe('Service: Variables', function() { Variables.resetLocal(); rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'value1'}, - {name: 'setting2', value: 'value2'} + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'} ]); }); it('should allow new server side variables at the end of the data', function() { rootScope.$digest(); - styleguideMock.config.data = { - settings: [ - {name: 'setting1', value: 'value1'}, - {name: 'setting2', value: 'value2'}, - {name: 'setting3', value: 'value3'} - ] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file', name: 'setting3', value: 'value3'} + ]; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'value1'}, - {name: 'setting2', value: 'value2'}, - {name: 'setting3', value: 'value3'} + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file', name: 'setting3', value: 'value3'} ]); }); it('should allow new server side variables between existing variables', function() { rootScope.$digest(); - styleguideMock.config.data = { - settings: [ - {name: 'setting1', value: 'value1'}, - {name: 'setting3', value: 'value3'}, - {name: 'setting2', value: 'value2'} - ] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting3', value: 'value3'}, + {file: 'file', name: 'setting2', value: 'value2'} + ]; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting1', value: 'value1'}, - {name: 'setting3', value: 'value3'}, - {name: 'setting2', value: 'value2'} + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting3', value: 'value3'}, + {file: 'file', name: 'setting2', value: 'value2'} ]); }); it('should handle properly mixed index changes and new variables', function() { rootScope.$digest(); - styleguideMock.config.data = { - settings: [ - {name: 'setting3', value: 'value3'}, - {name: 'setting4', value: 'value4'}, - {name: 'setting2', value: 'value2'}, - {name: 'setting1', value: 'value1'} - ] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting3', value: 'value3'}, + {file: 'file', name: 'setting4', value: 'value4'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file', name: 'setting1', value: 'value1'} + ]; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting3', value: 'value3'}, - {name: 'setting4', value: 'value4'}, - {name: 'setting2', value: 'value2'}, - {name: 'setting1', value: 'value1'} + {file: 'file', name: 'setting3', value: 'value3'}, + {file: 'file', name: 'setting4', value: 'value4'}, + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file', name: 'setting1', value: 'value1'} ]); }); @@ -213,18 +224,16 @@ describe('Service: Variables', function() { Variables.variables[0].value = 'new value1'; Variables.variables[1].value = 'new value2'; rootScope.$digest(); - styleguideMock.config.data = { - settings: [ - {name: 'setting2', value: 'value2'}, - {name: 'setting1', value: 'value1'}, - {name: 'setting3', value: 'value3'} - ] - }; + styleguideMock.variables.data = [ + {file: 'file', name: 'setting2', value: 'value2'}, + {file: 'file', name: 'setting1', value: 'value1'}, + {file: 'file', name: 'setting3', value: 'value3'} + ]; rootScope.$digest(); expect(Variables.variables).to.eql([ - {name: 'setting2', value: 'new value2', dirty: true}, - {name: 'setting1', value: 'new value1', dirty: true}, - {name: 'setting3', value: 'value3'} + {file: 'file', name: 'setting2', value: 'new value2', dirty: true}, + {file: 'file', name: 'setting1', value: 'new value1', dirty: true}, + {file: 'file', name: 'setting3', value: 'value3'} ]); }); diff --git a/test/integration/structure.test.js b/test/integration/structure.test.js index 9033bb33..f8f6b209 100644 --- a/test/integration/structure.test.js +++ b/test/integration/structure.test.js @@ -3,17 +3,17 @@ var gulp = require('gulp'), expect = chai.expect, through = require('through2'), styleguide = require('requirefrom')('lib')('styleguide'), - defaultSource = './test/projects/scss-project/source/**/*.scss', + defaultSource = 'test/projects/scss-project/source/**/*.scss', defaultConfig = { title: 'Test Styleguide', - overviewPath: './test/projects/scss-project/source/test_overview.md', + overviewPath: 'test/projects/scss-project/source/test_overview.md', appRoot: '/my-styleguide-book', extraHead: [ '', '' ], commonClass: ['custom-class-1', 'custom-class-2'], - styleVariables: './test/projects/scss-project/source/styles/_styleguide_variables.scss', + styleVariables: 'test/projects/scss-project/source/styles/_styleguide_variables.scss', sass: { // Options passed to gulp-sass in preprocess.js }, @@ -216,13 +216,16 @@ function sharedStyleguideJSON() { expect(this.jsonData.config.commonClass).to.eql(['custom-class-1', 'custom-class-2']); }); - it('should contain all style variables from defined file', function() { - var sassData = [ - {name: 'color-red', value: '#ff0000'}, - {name: 'color-green', value: '#00ff00'}, - {name: 'color-blue', value: '#0000ff'} - ]; - expect(this.jsonData.config.settings).to.eql(sassData); + it('should contain all style variable names from defined file', function() { + expect(this.jsonData.variables[0].name).to.eql('color-red'); + expect(this.jsonData.variables[1].name).to.eql('color-green'); + expect(this.jsonData.variables[2].name).to.eql('color-blue'); + }); + + it('should contain all style variable values from defined file', function() { + expect(this.jsonData.variables[0].value).to.eql('#ff0000'); + expect(this.jsonData.variables[1].value).to.eql('#00ff00'); + expect(this.jsonData.variables[2].value).to.eql('#0000ff'); }); it('should not reveal outputPath', function() { @@ -278,7 +281,7 @@ describe('styleguide.css for SCSS project', function() { config; config = defaultConfig; - config.styleVariables = './test/projects/scss-project/source/styles/_styleguide_variables.scss'; + config.styleVariables = 'test/projects/scss-project/source/styles/_styleguide_variables.scss'; source = './test/projects/scss-project/source/**/*.scss'; styleguideStream(source, config).pipe( through.obj({objectMode: true}, collector(files), function(callback) { @@ -300,7 +303,7 @@ describe('styleguide.css for LESS project', function() { config; config = defaultConfig; - config.styleVariables = './test/projects/less-project/source/styles/_styleguide_variables.less'; + config.styleVariables = 'test/projects/less-project/source/styles/_styleguide_variables.less'; source = './test/projects/less-project/source/**/*.less'; styleguideStream(source, config).pipe( through.obj({objectMode: true}, collector(files), function(callback) { @@ -322,8 +325,8 @@ describe('styleguide.json for SCSS project', function() { config; config = defaultConfig; - config.styleVariables = './test/projects/scss-project/source/styles/_styleguide_variables.scss'; - source = './test/projects/scss-project/source/**/*.scss'; + config.styleVariables = 'test/projects/scss-project/source/styles/_styleguide_variables.scss'; + source = 'test/projects/scss-project/source/**/*.scss'; styleguideStream(source, config).pipe( through.obj({objectMode: true}, collector(files), function(callback) { _this.jsonData = JSON.parse(findFile(files, 'styleguide.json').contents); @@ -333,6 +336,13 @@ describe('styleguide.json for SCSS project', function() { ); }); + it('should contain filenames where variables are defined', function() { + var path = 'test/projects/scss-project/source/styles/_styleguide_variables.scss'; + expect(this.jsonData.variables[0].file).to.contain(path); + expect(this.jsonData.variables[1].file).to.contain(path); + expect(this.jsonData.variables[2].file).to.contain(path); + }); + sharedStyleguideJSON(); }); @@ -344,8 +354,8 @@ describe('styleguide.json for LESS project', function() { config; config = defaultConfig; - config.styleVariables = './test/projects/less-project/source/styles/_styleguide_variables.less'; - source = './test/projects/less-project/source/**/*.less'; + config.styleVariables = 'test/projects/less-project/source/styles/_styleguide_variables.less'; + source = 'test/projects/less-project/source/**/*.less'; styleguideStream(source, config).pipe( through.obj({objectMode: true}, collector(files), function(callback) { _this.jsonData = JSON.parse(findFile(files, 'styleguide.json').contents); @@ -355,5 +365,12 @@ describe('styleguide.json for LESS project', function() { ); }); + it('should contain filenames where variables are defined', function() { + var path = 'test/projects/less-project/source/styles/_styleguide_variables.less'; + expect(this.jsonData.variables[0].file).to.contain(path); + expect(this.jsonData.variables[1].file).to.contain(path); + expect(this.jsonData.variables[2].file).to.contain(path); + }); + sharedStyleguideJSON(); }); diff --git a/test/projects/less-project/source/styles/style.less b/test/projects/less-project/source/styles/style.less index 77d117a3..0a3d95c3 100644 --- a/test/projects/less-project/source/styles/style.less +++ b/test/projects/less-project/source/styles/style.less @@ -1,5 +1,7 @@ @import "_styleguide_variables"; +@test-variable: 10px; + // First section // // Markup: diff --git a/test/projects/scss-project/source/styles/style.scss b/test/projects/scss-project/source/styles/style.scss index 96993691..2c2f9c9b 100644 --- a/test/projects/scss-project/source/styles/style.scss +++ b/test/projects/scss-project/source/styles/style.scss @@ -1,5 +1,7 @@ @import "styleguide_variables"; +$test-variable: 10px; + // First section // // Markup: diff --git a/test/unit/modules/io.test.js b/test/unit/modules/io.test.js index 02e1e396..b11b2638 100644 --- a/test/unit/modules/io.test.js +++ b/test/unit/modules/io.test.js @@ -79,7 +79,6 @@ describe('module io', function() { }); describe('socket connection listener', function() { - var listener; beforeEach(function() { @@ -91,11 +90,6 @@ describe('module io', function() { expect(listener).to.be.a('function'); }); - it('registers listener on "request variables from server" event', function() { - listener.call(undefined, socket); - expect(socket.on).to.have.been.calledWith('request variables from server'); - }); - it('registers listener on "variables to server" event', function() { listener.call(undefined, socket); expect(socket.on).to.have.been.calledWith('variables to server'); @@ -114,122 +108,77 @@ describe('module io', function() { listener.call(undefined, socket); expect(socket.emit).to.have.been.calledWith('styleguide compile error').and.calledWith('styleguide progress end'); }); - }); - describe('loading variables', function() { - - var listener, - readCallback; - - beforeEach(function() { - sinon.spy(console, 'error'); - opt.styleVariables = 'test/vars.scss'; - listener = getSocketListener('request variables from server'); - fs.readFile = sinon.spy(); - - listener.call(); - readCallback = fs.readFile.lastCall.args[2]; - }); - - afterEach(function() { - console.error.restore(); - }); - - it('reads file defined in options.styleVariables', function() { - expect(fs.readFile).to.have.been.calledWith(opt.styleVariables, { encoding: 'utf8' }); - }); - - it('passes file contents to variable parser.parseVariables', function() { - var fileData = '$foo: 10px;'; - readCallback.call(undefined, undefined, fileData); - expect(parser.parseVariables).to.have.been.calledWith(fileData, 'scss'); - }); - - it('emits variable data with socket event "variables from server"', function() { - var data = [{ name: 'foo', value: '10px' }]; - parser.parseVariables = sinon.stub().returns(data); - readCallback.call(); - expect(socket.emit).to.have.been.calledWith('variables from server', data); - }); - - it('only logs error to console if reading variables file fails', function() { - socket.emit.reset(); - readCallback.call(undefined, 'read fail'); - expect(console.error).to.have.been.calledWith('read fail'); - expect(parser.parseVariables).not.to.have.been.called; - expect(socket.emit).not.to.have.been.called; - }); - - }); - - describe('saving variables', function() { - - var listener, - readCallback, - writeCallback, - fileData = '$foo: 10px;', - newVariables = [{ name: 'foo', value: '16px' }], - newData = '$foo: 16px;'; + describe('save variables', function() { + var newVariables = [ + { + file: 'path/first_file.less', + name: 'myvar1', + value: 'myvalue1' + }, + { + file: 'path/second_file.scss', + name: 'myvar3', + value: 'myvalue3' + }, + { + file: 'path/first_file.less', + name: 'myvar2', + value: 'myvalue2' + } + ]; - beforeEach(function() { - sinon.spy(console, 'error'); + beforeEach(function(done) { opt.styleVariables = 'test/vars.scss'; - listener = getSocketListener('variables to server'); - fs.readFile = sinon.spy(); - fs.writeFile = sinon.spy(); - - listener.call(undefined, newVariables); - readCallback = fs.readFile.lastCall.args[2]; - }); - afterEach(function() { - console.error.restore(); + // Stub the file system module + sinon.stub(fs, 'readFile'); + sinon.stub(fs, 'writeFile'); + + fs.readFile + .withArgs('path/first_file.less', sinon.match.any, sinon.match.func) + .callsArgWith(2, null, 'First file content'); + + fs.readFile + .withArgs('path/second_file.scss', sinon.match.any, sinon.match.func) + .callsArgWith(2, null, 'Second file content'); + + fs.writeFile + .withArgs('path/first_file.less', sinon.match.any, sinon.match.func) + .callsArgWith(2, null, 'Changed first file content'); + + fs.writeFile + .withArgs('path/second_file.scss', sinon.match.any, sinon.match.func) + .callsArgWith(2, null, 'Changed cecond file content'); + + io.saveVariables(newVariables).then(function() { + done(); + }); + }); + + it('should call set variables with the original file contents and variables from that file', function() { + var firstFileVars = [{ + file: 'path/first_file.less', + name: 'myvar1', + value: 'myvalue1' + }, + { + file: 'path/first_file.less', + name: 'myvar2', + value: 'myvalue2' + }], + secondFileVars = [{ + file: 'path/second_file.scss', + name: 'myvar3', + value: 'myvalue3' + }]; + expect(parser.setVariables).to.have.been.calledWith('First file content', 'less', firstFileVars); + expect(parser.setVariables).to.have.been.calledWith('Second file content', 'scss', secondFileVars); }); - - it('reads file defined in options.styleVariables', function() { - expect(fs.readFile).to.have.been.calledWith(opt.styleVariables, { encoding: 'utf8' }); - }); - - it('passes original file contents to variable parser.setVariables', function() { - - readCallback.call(undefined, undefined, fileData); - expect(parser.setVariables).to.have.been.calledWith(fileData, 'scss'); - }); - - it('passes new variables to variable parser.setVariables', function() { - readCallback.call(undefined, undefined, fileData); - expect(parser.setVariables).to.have.been.calledWith(fileData, 'scss', newVariables); - }); - - it('writes data returned from parser.setVariables() to options.styleVariables file', function() { - parser.setVariables = sinon.stub().returns(newData); - readCallback(); - expect(fs.writeFile).to.have.been.calledWith(opt.styleVariables, newData); - }); - - it('emits new variable data with socket event "variables saved to server"', function() { - readCallback(); - writeCallback = fs.writeFile.lastCall.args[2]; - - writeCallback.call(undefined, undefined, newData); - expect(socket.emit).to.have.been.calledWith('variables saved to server', newData); - }); - - it('only logs error to console if writing new variables data fails', function() { - socket.emit.reset(); - readCallback.call(); - writeCallback = fs.writeFile.lastCall.args[2]; - - writeCallback.call(undefined, 'write fail'); - expect(console.error).to.have.been.calledWith('write fail'); - expect(socket.emit).not.to.have.been.called; - }); - }); function setUp() { - socket = { conn: { id: '123' }, emit: sinon.spy(), @@ -255,15 +204,4 @@ describe('module io', function() { io = ioModule(server, opt); } - - function getSocketListener(event) { - var connectionListener = server.on.lastCall.args[1]; - connectionListener.call(undefined, socket); - for (var i = 0; i < socket.on.callCount; i += 1) { - if (socket.on.getCall(i).args[0] === event) { - return socket.on.getCall(i).args[1]; - } - } - } - }); diff --git a/test/unit/modules/kss-additional-params.test.js b/test/unit/modules/kss-additional-params.test.js new file mode 100644 index 00000000..f77e1493 --- /dev/null +++ b/test/unit/modules/kss-additional-params.test.js @@ -0,0 +1,110 @@ +//jscs:disable disallowTrailingWhitespace +//jscs:disable disallowMultipleLineBreaks +var requireModule = require('requirefrom')('lib/modules'), + chai = require('chai'), + expect = chai.expect, + multiline = require('multiline'), + kssAdditionalParams = requireModule('kss-additional-params'); + +describe('Parsing KSS additional params', function() { + + it('Should parse from singleline-commented block', function() { + var str = multiline(function() { + /* +// sg-param: +// Value + */ + }), + result = { 'sg-param': ' Value' }, + params = kssAdditionalParams.get(str); + + expect(params).eql(result); + }); + + it('Should parse from multiline-commented block', function() { + + var str = '' + + '/*\n' + + ' sg-param:\n' + + ' Value' + + '*/', + result = { 'sg-param': ' Value' }, + params = kssAdditionalParams.get(str); + + expect(params).eql(result); + }); + + it('Should parse multiple params', function() { + var str = multiline(function() { + /* +// sg-param1: +// Value +// +// sg-param2: +// Value +// +// sg-param3: +// Value + */ + }), + result = { + 'sg-param1':' Value', + 'sg-param2':' Value', + 'sg-param3':' Value' + }, + params = kssAdditionalParams.get(str); + + expect(params).eql(result); + }); + + it('Should gulp different space combinations', function() { + var str = multiline(function() { + /* +// sg-param1 : +// Value +// +//sg-param2: +// Value +// +// sg-param3: +// Value + */ + }), + result = { + 'sg-param1':' Value', + 'sg-param2':' Value', + 'sg-param3':' Value' + }, + params = kssAdditionalParams.get(str); + + expect(params).eql(result); + }); + + it('Should ignore extra comments', function() { + var str = multiline(function() { + /* +// Something here +// +// sg-param1 : +// Value +// +//sg-param2: +// Value +// +// sg-empty-param: +// +// sg-param3: +// Value + */ + }), + result = { + 'sg-param1':' Value', + 'sg-param2':' Value', + 'sg-param3':' Value' + }, + params = kssAdditionalParams.get(str); + + expect(params).eql(result); + }); + +}); diff --git a/test/unit/modules/kss-parser.test.js b/test/unit/modules/kss-parser.test.js index 0ff9f346..c97fc17b 100644 --- a/test/unit/modules/kss-parser.test.js +++ b/test/unit/modules/kss-parser.test.js @@ -6,27 +6,48 @@ var requireModule = require('requirefrom')('lib/modules'), describe('KSS parser', function() { - function parse() { - var files = {}; - Array.prototype.slice.call(arguments).forEach(function(file, idx) { - files['file' + idx] = file; - }); - return parser.parseKSS(files, {}); + function parse(files) { + return parser.parseKssSections(files, {}); } function expectOrder(expected) { - return function(styleguide) { - var order = styleguide.sections.map(function(section) { + return function(sections) { + var order = sections.map(function(section) { return section.reference; }); expect(order).to.eql(expected); }; } - var file1, file2, file3, file4; + it('parses sections from multiple files', function(done) { + var files = { + 'file1.less': multiline(function() { + /* + // Styleguide 1.0 + */ + }), + 'file2.scss': multiline(function() { + /* + // Styleguide 2.0 + + // Styleguide 2.1 + + // Styleguide 2.2 + */ + }), + 'file3.scss': multiline(function() { + /* + // Styleguide 3.0 + */ + }) + }; + parse(files).then(function(sections) { + expect(sections.length).to.eql(5); + }).then(done).catch(done); + }); - beforeEach(function() { - file1 = multiline(function() { + it('sorts sections numerically according to first level', function(done) { + var file = {'file1.less': multiline(function() { /* // Styleguide 10 @@ -34,8 +55,13 @@ describe('KSS parser', function() { // Styleguide 1 */ - }); - file2 = multiline(function() { + })}, + order = ['1', '2', '10']; + parse(file).then(expectOrder(order)).then(done).catch(done); + }); + + it('sorts sub-sections numerically according to second level', function(done) { + var file = {'file2.less': multiline(function() { /* // Styleguide 2.10 @@ -45,86 +71,102 @@ describe('KSS parser', function() { // Styleguide 2.1 */ - }); - file3 = multiline(function() { + })}, + order = ['2.1', '2.2', '2.10', '3']; + parse(file).then(expectOrder(order)).then(done).catch(done); + }); + + it('sorts sub-sub-sections numerically according to third level', function(done) { + var file = {'file3.less': multiline(function() { /* - // Styleguide 3.1.10 + // Styleguide 3.1.10 - // Styleguide 3.2 + // Styleguide 3.2 - // Styleguide 4 + // Styleguide 4 - // Styleguide 3.1.2 + // Styleguide 3.1.2 - // Styleguide 3.1.1 + // Styleguide 3.1.1 */ - }); - file4 = multiline(function() { - /* - // Styleguide 1.2.3.4.10 - - // Styleguide 1.2.3.4.5.6.7 - - // Styleguide 1.2.4.19 - - // Styleguide 1.2.4.2 - */ - }); - }); - - it('parses sections from multiple files', function(done) { - parse(file1, file2, file3).then(function(styleguide) { - expect(styleguide.sections.length).to.eql(12); - }).then(done).catch(done); + })}, + order = ['3.1.1', '3.1.2', '3.1.10', '3.2', '4']; + parse(file).then(expectOrder(order)).then(done).catch(done); }); - it('sorts sections numerically according to first level', function(done) { - var order = ['1', '2', '10']; - parse(file1).then(expectOrder(order)).then(done).catch(done); - }); + it('sorts arbitrarily deep sub-sections correctly', function(done) { + var file = {'file4.less': multiline(function() { + /* + // Styleguide 1.2.3.4.10 - it('sorts sub-sections numerically according to second level', function(done) { - var order = ['2.1', '2.2', '2.10', '3']; - parse(file2).then(expectOrder(order)).then(done).catch(done); - }); + // Styleguide 1.2.3.4.5.6.7 - it('sorts sub-sub-sections numerically according to third level', function(done) { - var order = ['3.1.1', '3.1.2', '3.1.10', '3.2', '4']; - parse(file3).then(expectOrder(order)).then(done).catch(done); - }); + // Styleguide 1.2.4.19 - it('sorts arbitrarily deep sub-sections correctly', function(done) { - var order = ['1.2.3.4.5.6.7', '1.2.3.4.10', '1.2.4.2', '1.2.4.19']; - parse(file4).then(expectOrder(order)).then(done).catch(done); + // Styleguide 1.2.4.2 + */ + })}, + order = ['1.2.3.4.5.6.7', '1.2.3.4.10', '1.2.4.2', '1.2.4.19']; + parse(file).then(expectOrder(order)).then(done).catch(done); }); it('parses section reference 1.0 as 1', function(done) { - var file = multiline(function() { - /* - // Styleguide 2.0 - - // Styleguide 1.0 - */ - }); + var file = { + 'file1.less': multiline(function() { + /* + // Styleguide 2.0 + + // Styleguide 1.0 + */ + }) + }; parse(file).then(expectOrder(['1', '2'])).then(done).catch(done); }); + it('should store correct syntax', function(done) { + var files = { + 'file1.less': multiline(function() { + /* + // Styleguide 1.0 + */ + }), + 'file2.scss': multiline(function() { + /* + // Styleguide 2.0 + */ + }) + }; + parse(files).then(function(sections) { + expect(sections[0].syntax).to.eql('less'); + expect(sections[1].syntax).to.eql('scss'); + }).then(done).catch(done); + }); + it('ignores trailing dot when parsing section reference', function(done) { - parse('//Styleguide 1.2.').then(expectOrder(['1.2'])).then(done).catch(done); + var file = { + 'file1.less': multiline(function() { + /* + // Styleguide 1.2. + */ + }) + }; + parse(file).then(expectOrder(['1.2'])).then(done).catch(done); }); it('rejects with error if two sections are defined with same reference number', function(done) { - var file = multiline(function() { - /* - // Foo - // - // Styleguide 1.1 - - // Bar - // - // Styleguide 1.1 - */ - }); + var file = { + 'file1.less': multiline(function() { + /* + // Foo + // + // Styleguide 1.1 + + // Bar + // + // Styleguide 1.1 + */ + }) + }; parse(file).done(function() { done(Error('expected promise to reject')); }, function(err) { diff --git a/test/unit/modules/variable-parser.test.js b/test/unit/modules/variable-parser.test.js index f844f7ce..a5df1e24 100644 --- a/test/unit/modules/variable-parser.test.js +++ b/test/unit/modules/variable-parser.test.js @@ -5,6 +5,46 @@ var requireModule = require('requirefrom')('lib/modules'), parser = requireModule('variable-parser'); describe('Parser', function() { + + describe('finding variable declarations from files', function() { + var files; + + beforeEach(function() { + files = { + 'ccc.scss': multiline(function() { + /* + $var4: value1; + */ + }), + 'aaa.scss': multiline(function() { + /* + $var2: value2; + $var1: value1; + */ + }), + 'bbb.scss': multiline(function() { + /* + $var3: value3; + */ + }) + }; + }); + + it('should sort variables by filename', function(done) { + parser.parseVariableDeclarationsFromFiles(files).then(function(variables) { + expect(variables[0].file === 'aaa.scss'); + expect(variables[variables.length - 1].file === 'ccc.scss'); + }).then(done).catch(done); + }); + + it('should keep variable sort the same inside a single file', function(done) { + parser.parseVariableDeclarationsFromFiles(files).then(function(variables) { + expect(variables[1].name === 'var2'); + expect(variables[2].name === 'var2'); + }).then(done).catch(done); + }); + }); + describe('variable finding', function() { describe('SCSS syntax', function() { it('should return all used variables', function() { @@ -146,7 +186,7 @@ describe('Parser', function() { {name: 'mypadding', value: '3px'}, {name: 'myfont', value: '"Helvetica Neue", Helvetica, Arial, sans-serif'} ]; - expect(parser.parseVariables(str)).eql(result); + expect(parser.parseVariableDeclarations(str)).eql(result); }); it('should parse variables from file with containing comments and intended lines', function() { @@ -163,7 +203,7 @@ describe('Parser', function() { {name: 'mypadding', value: '3px'}, {name: 'myfont', value: '"Helvetica Neue", Helvetica, Arial, sans-serif'} ]; - expect(parser.parseVariables(str)).eql(result); + expect(parser.parseVariableDeclarations(str)).eql(result); }); it('should parse variables correct when there are multiple variables in a single line', function() { @@ -173,7 +213,7 @@ describe('Parser', function() { {name: 'color2', value: '#00ff00'}, {name: 'color3', value: '#0000ff'} ]; - expect(parser.parseVariables(str)).eql(result); + expect(parser.parseVariableDeclarations(str)).eql(result); }); it('should not take commented variables', function() { @@ -189,7 +229,17 @@ describe('Parser', function() { {name: 'color1', value: '#ff0000'}, {name: 'color3', value: '#0000ff'} ]; - expect(parser.parseVariables(str)).eql(result); + expect(parser.parseVariableDeclarations(str)).eql(result); + }); + + it('should not detect @import as variable', function() { + var str = multiline(function() { + /* + @import 'file'; + */ + }), + result = []; + expect(parser.parseVariableDeclarations(str)).eql(result); }); }); @@ -207,7 +257,7 @@ describe('Parser', function() { {name: 'mypadding', value: '3px'}, {name: 'myfont', value: '"Helvetica Neue", Helvetica, Arial, sans-serif'} ]; - expect(parser.parseVariables(str, 'less')).eql(result); + expect(parser.parseVariableDeclarations(str, 'less')).eql(result); }); it('should parse variables from file with containing comments and intended lines', function() { @@ -224,7 +274,7 @@ describe('Parser', function() { {name: 'mypadding', value: '3px'}, {name: 'myfont', value: '"Helvetica Neue", Helvetica, Arial, sans-serif'} ]; - expect(parser.parseVariables(str, 'less')).eql(result); + expect(parser.parseVariableDeclarations(str, 'less')).eql(result); }); it('should parse variables correct when there are multiple variables in a single line', function() { @@ -234,7 +284,7 @@ describe('Parser', function() { {name: 'color2', value: '#00ff00'}, {name: 'color3', value: '#0000ff'} ]; - expect(parser.parseVariables(str, 'less')).eql(result); + expect(parser.parseVariableDeclarations(str, 'less')).eql(result); }); it('should not take commented variables', function() { @@ -250,7 +300,29 @@ describe('Parser', function() { {name: 'color1', value: '#ff0000'}, {name: 'color3', value: '#0000ff'} ]; - expect(parser.parseVariables(str, 'less')).eql(result); + expect(parser.parseVariableDeclarations(str, 'less')).eql(result); + }); + + it('should not detect @import as a variable', function() { + var str = multiline(function() { + /* + @import 'file'; + */ + }), + result = []; + expect(parser.parseVariableDeclarations(str, 'less')).eql(result); + }); + + it('should accept variables named @import', function() { + var str = multiline(function() { + /* + @import: 3px; + */ + }), + result = [ + {name: 'import', value: '3px'} + ]; + expect(parser.parseVariableDeclarations(str, 'less')).eql(result); }); }); }); @@ -301,6 +373,7 @@ describe('Parser', function() { changed = parser.setVariables(str, 'scss', variables); expect(changed).eql(result); }); + it('should preserve indents', function() { var str = multiline(function() { /* @@ -322,6 +395,27 @@ describe('Parser', function() { changed = parser.setVariables(str, 'scss', variables); expect(changed).eql(result); }); + it('should preserve inline comments', function() { + var str = multiline(function() { + /* +$mycolor: #00ff00; +// +$mypadding: 3px; + */ + }), + variables = [ + {name: 'mypadding', value: '0'} + ], + result = multiline(function() { + /* +$mycolor: #00ff00; +// +$mypadding: 0; + */ + }), + changed = parser.setVariables(str, 'scss', variables); + expect(changed).eql(result); + }); it('should preserve comments', function() { var str = '' + '$mycolor: #00ff00;\n' + @@ -338,6 +432,7 @@ describe('Parser', function() { expect(changed).eql(result); }); }); + describe('LESS syntax', function() { it('should change single value variable', function() { var str = multiline(function() { @@ -406,7 +501,7 @@ describe('Parser', function() { changed = parser.setVariables(str, 'less', variables); expect(changed).eql(result); }); - it('should preserve comments', function() { + it('should preserve multiline comments', function() { var str = '' + '@mycolor: #00ff00;\n' + '/* Comment */\n' + diff --git a/test/unit/modules/wrapper-markup.test.js b/test/unit/modules/wrapper-markup.test.js index af247128..244923d6 100644 --- a/test/unit/modules/wrapper-markup.test.js +++ b/test/unit/modules/wrapper-markup.test.js @@ -12,40 +12,62 @@ describe('KSS wrapper markup generator', function() { }; beforeEach(function() { - var markup = []; - markup[0] = multiline(function() { + var section = []; + section[0] = {}, + section[0].markup =multiline(function() { /* */ }), - markup[1] = multiline(function() { + section[1] = {}, + section[1].markup = multiline(function() { /*

    Content inside outer wrapper

    - +*/ + }), + section[1]['sg-wrapper'] = multiline(function() { +/* - + - */ }), - markup[2] = multiline(function() { + section[2] = {}, + section[2].markup = multiline(function() { /*

    Content inside outer wrapper

    */ }), - markup[3] = multiline(function() { + section[3] = {}, + section[3].markup = multiline(function() { /*

    Content inside inner and outer wrapper

    - +*/ + }), + section[3]['sg-wrapper'] = multiline(function() { +/* - + - */ }), - markup[4] = multiline(function() { + section[4] = {}, + section[4].markup = multiline(function() { /*

    Second level content

    +*/ + }), + section[5] = {}, + section[5].markup = multiline(function() { +/* + +*/ + }), + section[5]['sg-wrapper'] = multiline(function() { +/* + + + */ }); @@ -55,31 +77,47 @@ describe('KSS wrapper markup generator', function() { description: '', reference: '1.0', modifiers: [], - markup: markup[0] + markup: section[0].markup }, { header: 'Define outer wrapper', description: '', reference: '1.1', modifiers: [], - markup: markup[1] + markup: section[1].markup, + 'sg-wrapper': section[1]['sg-wrapper'] }, { header: 'Content inside outer wrapper', description: '', reference: '1.1.1', modifiers: [], - markup: markup[2] + markup: section[2].markup }, { header: 'Content inside inner and outer wrapper', description: '', reference: '1.1.2', modifiers: [], - markup: markup[3] + markup: section[3].markup, + 'sg-wrapper': section[3]['sg-wrapper'] }, { header: 'Multiple inherited wrapper', description: '', reference: '1.1.2.1', modifiers: [], - markup: markup[4] + markup: section[4].markup + }, { + header: 'Button', + description: '', + reference: '2', + modifiers: [ + { + id: 1, + name: 'modifier', + description: '', + markup: '' + } + ], + markup: section[5].markup, + 'sg-wrapper': section[5]['sg-wrapper'] }] }; json.sections = wrapperMarkup.generateSectionWrapperMarkup(json.sections); @@ -109,4 +147,10 @@ describe('KSS wrapper markup generator', function() { var wrappedMarkup = '

    Second level content

    '; expect(removeLinebreaks(json.sections[4].wrappedMarkup)).eql(wrappedMarkup); }); + + it('should work for modifiers', function() { + var wrappedMarkup = ''; + expect(removeLinebreaks(json.sections[5].modifiers[0].wrappedMarkup)).eql(wrappedMarkup); + }); + });