├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── package.json ├── tasks └── patternprimer.js └── test ├── fixtures ├── global.css └── patterns │ └── headline-1.html └── patternprimer_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log 16 | 17 | test/fixtures/output -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "globals": { 15 | "jQuery": true 16 | } 17 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_script: 5 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | // time grunt for measuring 3 | require('time-grunt')(grunt); 4 | // load all grunt tasks matching the `grunt-*` pattern 5 | require('load-grunt-tasks')(grunt); 6 | 7 | // project config 8 | grunt.initConfig({ 9 | 10 | // clean 11 | clean: { 12 | test: ['test/fixtures/output'] 13 | }, 14 | 15 | // hinting 16 | jshint: { 17 | files: ['tasks/*.js', 'test/*.js'], 18 | options: { 19 | jshintrc: '.jshintrc' 20 | } 21 | }, 22 | 23 | // unit tests 24 | nodeunit: { 25 | tests: ['test/*_test.js'], 26 | } 27 | 28 | }); 29 | 30 | // default - does everything 31 | grunt.registerTask('default', ['clean:test', 'jshint', 'nodeunit']); 32 | grunt.registerTask('test', ['clean:test', 'jshint', 'nodeunit']); 33 | } -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 asciidisco 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-patternprimer v0.1.1 [![Build Status](https://travis-ci.org/asciidisco/grunt-patternprimer.png?branch=master)](https://travis-ci.org/asciidisco/grunt-patternprimer) [![devDependency Status](https://david-dm.org/asciidisco/grunt-patternprimer/dev-status.png?theme=shields.io)](https://david-dm.org/asciidisco/grunt-patternprimer#info=devDependencies) 2 | 3 | > Grunt enabled port of [adactios](https://github.com/adactio) [Pattern-Primer](https://github.com/adactio/Pattern-Primer) 4 | 5 | ## What?! 6 | As stated in the original docs: 7 | > Create little snippets of markup and save them to the "patterns folder." The pattern primer will generate a list of all the patterns in that folder. You will see the pattern rendered as HTML. You will also get the source displayed in a textarea. 8 | 9 | Check also the related [Blog Post](http://adactio.com/journal/5028/) & [example](http://patternprimer.adactio.com/) from Jeremy. 10 | 11 | ## Getting Started 12 | This plugin requires Grunt `~0.4.0` 13 | 14 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 15 | 16 | ```shell 17 | npm install grunt-patternprimer --save-dev 18 | ``` 19 | 20 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 21 | 22 | ```js 23 | grunt.loadNpmTasks('grunt-patternprimer'); 24 | ``` 25 | 26 | ## Patternprimer task 27 | _Run this task with the `grunt patternprimer` command._ 28 | 29 | Task targets, files and options may be specified according to the grunt [Configuring tasks](http://gruntjs.com/configuring-tasks) guide. 30 | 31 | ### Options 32 | 33 | #### wwwroot 34 | Type: `String` 35 | Default: `public` 36 | 37 | This is the Place all your HTML extracts (the pattern files) live it is relative to the `` folder of your project. 38 | 39 | #### css 40 | Type: `Array` 41 | Default: `['global.css']` 42 | 43 | Array with all the css files you that should be loaded in the parttern primer. 44 | 45 | Note: You can also specify remote ressources like `http://my.domain.com/style.css`. 46 | They will be downloaded and stored locally in case of a snapshot. 47 | Does not work with ressorces from `https` sites. 48 | 49 | #### dest 50 | Type: `String` 51 | Default: `docs` 52 | 53 | Specifies the destination of the pattern files when running a snapshot build and/or running the live server. 54 | 55 | #### ports 56 | Type: `Array` 57 | Default: `[7020, 7030]` 58 | 59 | Ports that should be used when running the live server. The first index of that array will be used to serve the contents of the 60 | `patterns` folder live, the second port will be used to serve your last snapshot build (if one exists). 61 | 62 | #### src 63 | Type: `String` 64 | Default: `public/patterns` 65 | 66 | The location of your pattern catalogue, this source will be used to deliver the pattern catalogue from the live server 67 | and to generate snapshots from it. 68 | 69 | #### snapshot 70 | Type: `Boolean` 71 | Default: `false` 72 | 73 | Determines if a live server should be fired up, or if the output ends up in the via `dest` configured snapshot directory. 74 | 75 | #### index 76 | Type: `Boolean` `String` 77 | Default: `false` 78 | 79 | Define your own index template for the patterns. Should be a file ending with the `.html` extension. 80 | Please omit the closing `` and `` tags, they will be added autmagically. 81 | 82 | ### Usage examples 83 | 84 | #### Live delivery 85 | 86 | This configuration will start a live server that servers your pattern catalogue on port 7020. 87 | 88 | ```js 89 | // Project configuration. 90 | grunt.initConfig({ 91 | patternprimer: { 92 | my_target: { 93 | ports: [7020], 94 | src: 'public/patterns', 95 | wwwroot: 'public', 96 | css: ['global.css'] 97 | } 98 | } 99 | }); 100 | ``` 101 | 102 | #### Creating snapshots 103 | 104 | This configuration will not spin a live server, instead will save your catalogue (for static access) 105 | in the `dest` folder: 106 | 107 | ```js 108 | // Project configuration. 109 | grunt.initConfig({ 110 | patternprimer: { 111 | my_target: { 112 | wwwroot: 'public', 113 | css: ['global.css'], 114 | dest: 'docs', 115 | snapshot: true 116 | } 117 | } 118 | }); 119 | ``` 120 | 121 | #### Getting it all together 122 | 123 | This configuration (my favourite) will enable you to run a live server & do snapshotting by specifying 124 | the task from the cmd. 125 | 126 | `grunt patternprimer:live` will spin up the servers to deliver the live catalogue & the last snapshotted version. 127 | 128 | `grunt patternprimer:snapshot` will generate and save a new snapshot. 129 | 130 | ```js 131 | // Project configuration. 132 | grunt.initConfig({ 133 | patternprimer: { 134 | options: { 135 | wwwroot: 'public', 136 | css: ['global.css'], 137 | dest: 'docs' 138 | }, 139 | live: { 140 | ports: [7020, 7030], 141 | src: 'public/patterns', 142 | }, 143 | snapshot: { 144 | snapshot: true 145 | } 146 | } 147 | }); 148 | ``` 149 | #### Need some Sass? 150 | 151 | Including Sass into my config enabled me to start creating a master library of client style guides, which is very helpful when working with remote/new developers on a project. 152 | 153 | > Sass included version [stevebritton](https://github.com/stevebritton) [Pattern-Primer](https://github.com/stevebritton/grunt-patternprimer) 154 | 155 | ## Release History 156 | * 2014-12-03 v0.1.2 157 | - Script tags added to index template don't get moved to the bottom anymore (https://github.com/asciidisco/grunt-patternprimer/commit/b933db8114a034dc1f49629d9f899f11b9dd6ecb) 158 | - Choose the order patterns are added using the settings option (https://github.com/asciidisco/grunt-patternprimer/commit/c8618225a5e6750586193043340bb4d84c00ad9f) 159 | - Resolve file copy process (https://github.com/asciidisco/grunt-patternprimer/commit/3efcf33084679a4111e049a1b691f593afff88e5) 160 | 161 | * 2014-03-04 v0.1.1 Added remote css & fixed [#3](https://github.com/asciidisco/grunt-patternprimer/issues/3) 162 | * 2013-11-25   v0.1.0   Initial release. 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-patternprimer", 3 | "description": "Jeremy Keiths pattern primer on steroids", 4 | "version": "0.1.2", 5 | "homepage": "https://github.com/asciidisco/grunt-patternprimer", 6 | "author": { 7 | "name": "Sebastian Golasch", 8 | "url": "http://asciidisco.com/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/asciidisco/grunt-patternprimer.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/asciidisco/grunt-patternprimer/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/asciidisco/grunt-patternprimer/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "main": "Gruntfile.js", 24 | "engines": { 25 | "node": ">= 0.8.0" 26 | }, 27 | "scripts": { 28 | "test": "grunt test" 29 | }, 30 | "dependencies": { 31 | "connect": "~2.7.11", 32 | "q": "~1.0.0" 33 | }, 34 | "devDependencies": { 35 | "grunt-contrib-jshint": "~0.6.4", 36 | "grunt-contrib-nodeunit": "~0.2.0", 37 | "grunt": "~0.4.1", 38 | "time-grunt": "~0.2.1", 39 | "load-grunt-tasks": "~0.2.0", 40 | "grunt-contrib-clean": "~0.5.0" 41 | }, 42 | "peerDependencies": { 43 | "grunt": "~0.4.1" 44 | }, 45 | "keywords": [ 46 | "gruntplugin", 47 | "patternprimer", 48 | "connect" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /tasks/patternprimer.js: -------------------------------------------------------------------------------- 1 | // ext. libs 2 | var fs = require('fs'); 3 | var http = require('http'); 4 | var path = require('path'); 5 | 6 | var connect = require('connect'); 7 | var Q = require('q'); 8 | 9 | // grunting grunts for grunt 10 | module.exports = function(grunt) { 11 | 12 | // global settings 13 | var settings = {}; 14 | 15 | // Default sourcefile. 16 | // Can be overwritten using the option `index` 17 | var sourceFile = [ 18 | '', 19 | '', 20 | '', 21 | 'Pattern Primer', 22 | '{{css}}', 23 | '', 40 | '', 41 | ''].join(''); 42 | 43 | // gets the user defined source file, 44 | // or uses the default one 45 | var getSourceFile = function (cb) { 46 | // check if we have a custom index file set 47 | if (settings.index) { 48 | // generate the real index file path 49 | var indexFile = process.cwd() + '/' + settings.index; 50 | 51 | // check if the file exists, throw an error otherwise 52 | if (!grunt.file.exists(indexFile)) { 53 | grunt.log.error('Index file: "' + indexFile + '" not found'); 54 | cb('Index file: "' + indexFile + '" not found'); 55 | return; 56 | } 57 | 58 | // load the file contents 59 | sourceFile = grunt.file.read(indexFile); 60 | } 61 | 62 | // modify the sourcefile css according to the settings 63 | var css = settings.css.map(function (file) { 64 | if (settings.snapshot && file.search('http://') !== -1) { 65 | return ''; 66 | } else { 67 | return ''; 68 | } 69 | }); 70 | sourceFile = sourceFile.replace('{{css}}', css.join('')); 71 | 72 | // spit out the default sourcefile 73 | cb(sourceFile); 74 | }; 75 | 76 | // generates the html output for the patterns 77 | var outputPatterns = function (patternFolder, patterns, cb) { 78 | getSourceFile(function generatePatterns(content) { 79 | patterns.forEach(function (file) { 80 | content += '
'; 81 | content += '
'; 82 | content += file.content; 83 | content += '
'; 86 | content += '

' + file.filename + '

'; 87 | content += '
'; 88 | }); 89 | content += ''; 90 | cb(content); 91 | }); 92 | }; 93 | 94 | // walks through the pattern folder 95 | // reads all the contents of the pattern files 96 | var handleFiles = function (patternFolder, files, cb) { 97 | var file, patterns = []; 98 | files.forEach(function readPattern(pattern) { 99 | file = {filename: pattern}; 100 | file.content = grunt.file.read(patternFolder + '/' + file.filename); 101 | patterns.push(file); 102 | }); 103 | 104 | // call the outputPatterns function that generates 105 | // the html for every pattern 106 | outputPatterns(patternFolder, patterns, cb); 107 | }; 108 | 109 | // simple html escape helper 110 | var simpleEscaper = function (text) { 111 | return text.replace(/&/g, '&').replace(/ 0) { 163 | var patterns = settings.files; 164 | var patternsFolder = settings.src; 165 | 166 | // our main function that starts the process 167 | primer = function (cb) { 168 | handleFiles(patternsFolder, patterns, cb); 169 | }; 170 | 171 | } else { 172 | var patternFolder = './' + settings.src; 173 | 174 | // our main function that starts the process 175 | primer = function (cb) { 176 | readPatterns(patternFolder, cb); 177 | }; 178 | } 179 | 180 | 181 | 182 | // middleware to spit out 404 (in case a non existing ressource is request) 183 | // or to process the `non static` requests 184 | var middleware = function (req, resp) { 185 | if (req.url !== '/') { 186 | resp.writeHead(404, { 187 | 'Content-Length': 0, 188 | 'Content-Type': 'text/plain' 189 | }); 190 | resp.end(); 191 | return; 192 | } 193 | 194 | // 200, success, always 195 | resp.writeHead(200, {'Content-Type': 'text/html'}); 196 | // run the primer with cb 197 | primer(resp.end.bind(resp)); 198 | }; 199 | 200 | // initialize the server with static routes & dynamic template middleware 201 | var liveServer = connect.createServer( 202 | connect.static(process.cwd() + '/' + settings.wwwroot), 203 | middleware 204 | ); 205 | 206 | // initialize the static server pointing to your snapshots 207 | var snapshotServer = connect.createServer(connect.static(process.cwd() + '/' + settings.dest)); 208 | 209 | // starts the live server 210 | var startLiveServer = function () { 211 | liveServer.listen(settings.pattern_port, function () { 212 | grunt.log.ok('You can now visit http://localhost:' + settings.pattern_port + '/ to see your patterns.'); 213 | }); 214 | }; 215 | 216 | // starts the snapshot server 217 | var startSnapshotServer = function () { 218 | snapshotServer.listen(settings.snapshot_port, function () { 219 | grunt.log.ok('You can now visit http://localhost:' + settings.snapshot_port + '/ to see your snaphsot patterns.'); 220 | }); 221 | }; 222 | 223 | // writes the task output to a file 224 | var writeSnapshot = function () { 225 | primer(function (content) { 226 | var promises = []; 227 | // write the index file 228 | grunt.file.write('./' + settings.dest + '/index.html', content); 229 | // copy css files 230 | settings.css.forEach(function (file) { 231 | var deferred = Q.defer(); 232 | promises.push(deferred.promise); 233 | if (file.search('http://') !== -1) { 234 | 235 | var data = ''; 236 | http.get(file, function (res) { 237 | res.on('data', function(chunk) { 238 | data += chunk; 239 | }); 240 | res.on('end', function () { 241 | grunt.file.write('./' + settings.dest + '/style.css', data); 242 | deferred.resolve(); 243 | }); 244 | }) 245 | .on('err', deferred.reject); 246 | } else { 247 | grunt.file.copy('./' + settings.wwwroot + '/' + file, './' + settings.dest + '/' + file); 248 | deferred.resolve(); 249 | } 250 | }); 251 | 252 | grunt.log.ok('Stand-alone output can now be found in "' + settings.dest + '/"'); 253 | grunt.event.emit('patternprimer:snapshot:written'); 254 | if (promises.length === 0) { 255 | done(); 256 | } else { 257 | Q.allSettled(promises).then(done); 258 | } 259 | }); 260 | }; 261 | 262 | // writes to file or starts a server, 263 | // depending on the given snapshot var 264 | if (!!settings.snapshot) { 265 | writeSnapshot(); 266 | } else { 267 | startLiveServer(); 268 | // only start snapshot server, if snapshots are available 269 | if (grunt.file.exists('./' + settings.dest + '/index.html')) { 270 | startSnapshotServer(); 271 | } 272 | } 273 | 274 | }; 275 | 276 | grunt.registerMultiTask('patternprimer', patternprimer); 277 | return patternprimer; 278 | }; 279 | -------------------------------------------------------------------------------- /test/fixtures/global.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /test/fixtures/patterns/headline-1.html: -------------------------------------------------------------------------------- 1 |

Level one heading

-------------------------------------------------------------------------------- /test/patternprimer_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // ext. libs 4 | var grunt = require('grunt'); 5 | var http = require('http'); 6 | 7 | // int. libs 8 | var primer = require('../tasks/patternprimer')(grunt); 9 | 10 | // little helper that checks if a port is blocked 11 | var isPortTaken = function(port, callback) { 12 | var net = require('net'); 13 | var tester = net.createServer(); 14 | tester.once('error', function (err) { 15 | if (err.code === 'EADDRINUSE') { 16 | callback(null, true); 17 | } else { 18 | callback(err); 19 | } 20 | }); 21 | tester.once('listening', function() { 22 | tester.once('close', function() { 23 | callback(null, false); 24 | }); 25 | tester.close(); 26 | }); 27 | tester.listen(port); 28 | }; 29 | 30 | // Tests 31 | exports.patternprimer = { 32 | // test basic server `live` output 33 | basic: function(test) { 34 | test.expect(3); 35 | 36 | var config = { 37 | async: function () {}, 38 | options: function () { return {}; }, 39 | data: {} 40 | }; 41 | 42 | // run the pattern primer 43 | primer.bind(config)(); 44 | 45 | // check if the default port is blocked 46 | isPortTaken(7020, function (err, blocked) { 47 | test.ok(blocked, 'Default port is blocked.'); 48 | http.get('http://localhost:7020', function (res) { 49 | test.equal(res.statusCode, 200, 'Page can be delivered'); 50 | res.on('data', function (buf) { 51 | test.equal(buf+'', 'Cannot find patterns folder: ./public/patterns', 'Patterns not found error message can be delivered'); 52 | test.done(); 53 | }); 54 | }); 55 | }); 56 | }, 57 | // test if patterns config gets loaded & delivered 58 | patternsCanBeDelivered: function(test) { 59 | test.expect(3); 60 | 61 | var config = { 62 | async: function () {}, 63 | options: function () { return {}; }, 64 | data: { 65 | ports: [7021, 7022], 66 | src: 'test/fixtures/patterns' 67 | } 68 | }; 69 | 70 | // run the pattern primer 71 | primer.bind(config)(); 72 | 73 | // check if the defined port is blocked 74 | isPortTaken(7021, function (err, blocked) { 75 | test.ok(blocked, 'User defined port will be used'); 76 | http.get('http://localhost:7021', function (res) { 77 | test.equal(res.statusCode, 200, 'Page can be delivered'); 78 | res.on('data', function (buf) { 79 | test.ok((buf+'').search('

Level one heading

'), 'Configured pattern can be delivered'); 80 | test.done(); 81 | }); 82 | }); 83 | }); 84 | }, 85 | // can generate a snapshot 86 | snapshotCanBeGenerated: function(test) { 87 | test.expect(4); 88 | 89 | var config = { 90 | async: function () { return function () {}; }, 91 | options: function () { return {}; }, 92 | data: { 93 | snapshot: true, 94 | src: 'test/fixtures/patterns', 95 | dest: 'test/fixtures/output', 96 | wwwroot: 'test/fixtures', 97 | css: ['global.css'] 98 | } 99 | }; 100 | 101 | // run the pattern primer 102 | primer.bind(config)(); 103 | 104 | grunt.event.on('patternprimer:snapshot:written', function () { 105 | test.ok(true, 'patternprimer:snapshot:written event fired'); 106 | test.ok(grunt.file.exists(__dirname + '/fixtures/output/index.html'), 'Snapshot pattern can be generated'); 107 | test.ok(grunt.file.exists(__dirname + '/fixtures/output/global.css'), 'CSS can be copied'); 108 | test.ok(grunt.file.read(__dirname + '/fixtures/output/index.html').search('

Level one heading

'), 'Snapshot contains patterns'); 109 | test.done(); 110 | }); 111 | }, 112 | // can deliver a snapshot 113 | snapshotCanBeDelivered: function(test) { 114 | test.expect(3); 115 | 116 | var config = { 117 | async: function () {}, 118 | options: function () { return {}; }, 119 | data: { 120 | ports: [7031, 7032], 121 | src: 'test/fixtures/patterns', 122 | dest: 'test/fixtures/output', 123 | wwwroot: 'test/fixtures', 124 | css: ['global.css'] 125 | } 126 | }; 127 | 128 | // run the pattern primer 129 | primer.bind(config)(); 130 | 131 | // check if the defined port is blocked 132 | isPortTaken(7032, function (err, blocked) { 133 | test.ok(blocked, 'User defined port will be used'); 134 | http.get('http://localhost:7032', function (res) { 135 | test.equal(res.statusCode, 200, 'Page can be delivered'); 136 | res.on('data', function (buf) { 137 | test.ok((buf+'').search('

Level one heading

'), 'Configured pattern can be delivered'); 138 | test.done(); 139 | }); 140 | }); 141 | }); 142 | } 143 | }; --------------------------------------------------------------------------------