├── .editorconfig ├── .gitignore ├── Compiler.js ├── LICENSE ├── Parser.js ├── README.md ├── bin └── jadum ├── camelCase.js ├── jadum.js ├── package.json ├── runtime.js └── test ├── compiled ├── listing.jade.js └── listing.jadum.js ├── fixture ├── item.jade └── listing.jade └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Compiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jade = require('jade'); 4 | var util = require('util'); 5 | var BaseCompiler = jade.Compiler; 6 | 7 | function Compiler () { 8 | BaseCompiler.apply(this, arguments); 9 | } 10 | 11 | util.inherits(Compiler, BaseCompiler); 12 | 13 | Compiler.prototype.visitEach = function (each) { 14 | this.buf.push('' 15 | + '// iterate ' + each.obj + '\n' 16 | + ';(function(){\n' 17 | + ' var $$local = locals["' + each.val + '"];\n' 18 | + ' var $$obj = ' + each.obj + ';\n' 19 | + ' if (\'number\' == typeof $$obj.length) {\n'); 20 | 21 | if (each.alternative) { 22 | this.buf.push(' if ($$obj.length) {'); 23 | } 24 | 25 | this.buf.push('' 26 | + ' for (var ' + each.key + ' = 0, $$l = $$obj.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n' 27 | + ' var ' + each.val + ' = locals["' + each.val + '"] = $$obj[' + each.key + '];\n'); 28 | 29 | this.visit(each.block); 30 | 31 | this.buf.push(' }\n'); 32 | 33 | if (each.alternative) { 34 | this.buf.push(' } else {'); 35 | this.visit(each.alternative); 36 | this.buf.push(' }'); 37 | } 38 | 39 | this.buf.push('' 40 | + ' } else {\n' 41 | + ' var $$l = 0;\n' 42 | + ' for (var ' + each.key + ' in $$obj) {\n' 43 | + ' $$l++;' 44 | + ' var ' + each.val + ' = locals["' + each.val + '"] = $$obj[' + each.key + '];\n'); 45 | 46 | this.visit(each.block); 47 | 48 | this.buf.push(' }\n'); 49 | if (each.alternative) { 50 | this.buf.push(' if ($$l === 0) {'); 51 | this.visit(each.alternative); 52 | this.buf.push(' }'); 53 | } 54 | 55 | this.buf.push('' 56 | + ' locals["' + each.val + '"] = $$local;\n' 57 | + ' }\n}).call(this);\n'); 58 | }; 59 | 60 | module.exports = Compiler; 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2014 Nicolas Bevacqua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var jade = require('jade'); 6 | var util = require('util'); 7 | var nodes = jade.nodes; 8 | var BaseParser = jade.Parser; 9 | 10 | function Parser (a,b,c) { 11 | BaseParser.apply(this, arguments); 12 | } 13 | 14 | util.inherits(Parser, BaseParser); 15 | 16 | Parser.prototype.parseInclude = function () { 17 | var tok = this.expect('include').val.trim(); 18 | var file = this.resolvePath(tok, 'include'); 19 | var mod = tok.indexOf('.') === 0 ? tok : './' + tok; 20 | 21 | this.dependencies.push(file); 22 | 23 | return new nodes.Code('buf.push(require("' + mod + '").call(this, locals));'); 24 | }; 25 | 26 | module.exports = Parser; 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jadum 2 | 3 | > A lean Jade compiler that understands Browserify and reuses partials 4 | 5 | Works just like `jade`, except that the CLI compiles views producing a syntax like below. 6 | 7 | ```js 8 | module.exports = function (model) { 9 | // view rendering... 10 | } 11 | ``` 12 | 13 | The other difference is that the traditional Jade compiler inlines partials when it finds `include` statements, whereas `jadum` uses `require` statements to reuse partials, saving precious bytes in client-side code. 14 | 15 | ## Install 16 | 17 | ```shell 18 | npm install -S jadum 19 | ``` 20 | 21 | ## CLI 22 | 23 | The CLI works the same way as the one in `jade`, but it always compiles views for the client-side, as Common.JS modules. 24 | 25 | ```shell 26 | jadum views/**/* -o .bin 27 | ``` 28 | 29 | ## API 30 | 31 | The API is the same as the API in Jade, but it produces `require` statements instead of inlining `include` statements. 32 | 33 | ## Comparison 34 | 35 | Take as input these two files: `listing.jade` and `item.jade`. 36 | 37 | ```jade 38 | h1 This is some awesome listing! 39 | ul 40 | each item in ['a', 'b', 'c'] 41 | include item 42 | ``` 43 | 44 | ```jade 45 | li 46 | span Here comes item: 47 | span Note that this is quite a big template file 48 | span But it's included everywhere, because Jade! 49 | span So that's awesome... but compiling becomes an issue... 50 | span=item 51 | ``` 52 | 53 | As the `item.jade` template grows, every template that depends on it will grow twice as much, as the included template _(`item.jade`)_ **is inlined twice** in its parent _(`listing.jade`)_ template! Here is the output of `jade`. 54 | 55 | ```js 56 | function template(locals) { 57 | var buf = []; 58 | var jade_mixins = {}; 59 | var jade_interp; 60 | 61 | buf.push("

This is some awesome listing!

");;return buf.join(""); 85 | } 86 | ``` 87 | 88 | If we use `jadum`, however, we don't have these issues. We'll always have a fixed size determined by the length of the path passed to `require` statements. Pretty neat! Since `jadum` understands Browserify it won't hope for the best, in terms of expecting the Jade runtime to be globally included in your code. That's why there's the extra `require` at the top for the runtime. Browserify will take over when compiling the template, making sure it's only included once. Furthermore, if you use thee template in various places, then it will only be included once. That's where you save the vast majority of bytes. 89 | 90 | ```js 91 | var jade = require("jadum/runtime"); 92 | module.exports = function listing(locals) { 93 | var buf = []; 94 | var jade_mixins = {}; 95 | var jade_interp; 96 | ;var locals_for_with = (locals || {});(function (require) { 97 | buf.push("

This is some awesome listing!

");}.call(this,"require" in locals_for_with?locals_for_with.require:typeof require!=="undefined"?require:undefined));;return buf.join(""); 123 | } 124 | ``` 125 | 126 | # License 127 | 128 | MIT 129 | -------------------------------------------------------------------------------- /bin/jadum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var argv = require('minimist')(process.argv.slice(2), { 6 | alias: { 7 | output: ['o', 'out'], 8 | pretty: 'p', 9 | watch: 'w' 10 | } 11 | }); 12 | var fs = require('fs'); 13 | var path = require('path'); 14 | var glob = require('glob'); 15 | var contra = require('contra'); 16 | var mkdirp = require('mkdirp'); 17 | var monocle = require('monocle')(); 18 | var jade = require('..'); 19 | var files = argv._; 20 | var extname = '.js'; 21 | var re = /\.jade$/; 22 | var options = {}; 23 | 24 | // worth a shot, tell jade it can use ultramarked for this thing 25 | try { 26 | var transformers = require('jade/node_modules/transformers'); 27 | transformers.markdown.engines.push('megamark'); 28 | } catch (e) { 29 | } 30 | 31 | if (argv.obj) { 32 | options = JSON.parse(argv.obj); 33 | } 34 | 35 | options.compileDebug = argv.debug !== false; 36 | options.out = argv.out || argv.o; 37 | options.pretty = argv.pretty || argv.p; 38 | 39 | if (files.length === 1 && files[0].indexOf('*') !== -1) { 40 | files = glob.sync(files[0]); // `npm run` doesn't expand the pattern :( 41 | } 42 | 43 | contra.each(files, renderFile); 44 | 45 | if (argv.watch) { 46 | monocle.watchFiles({ 47 | files: files, 48 | listener: listener 49 | }); 50 | } 51 | 52 | function renderFile (file, done) { 53 | contra.waterfall([ 54 | function (next) { 55 | fs.lstat(file, next); 56 | }, 57 | function (stat, next) { 58 | if ((stat.isFile() || stat.isSymbolicLink()) && re.test(file)) { 59 | render(next); 60 | } else if (stat.isDirectory()) { 61 | dir(next); 62 | } else { 63 | next(); 64 | } 65 | } 66 | ], done); 67 | 68 | function render (done) { 69 | var prefix = 'var jade = require("jadum/runtime");\nmodule.exports = '; 70 | var fn; 71 | 72 | fs.readFile(file, 'utf8', gotData); 73 | 74 | function gotData (err, data) { 75 | if (err) { 76 | done(err); return; 77 | } 78 | options.filename = file; 79 | fn = jade.compileClient(data, options); 80 | file = file.replace(re, extname); 81 | if (options.out) { 82 | file = path.join(options.out, file); 83 | } 84 | var dir = path.resolve(path.dirname(file)); 85 | mkdirp(dir, ensuredDir); 86 | } 87 | 88 | function ensuredDir (err) { 89 | if (err) { 90 | done(err); return; 91 | } 92 | fs.writeFile(file, prefix + fn, 'utf8', done); 93 | } 94 | } 95 | 96 | function dir (done) { 97 | contra.waterfall([ 98 | function (next) { 99 | fs.readdir(file, next); 100 | }, 101 | function (files, next) { 102 | contra.each(files.map(expand), renderFile, next); 103 | } 104 | ], done); 105 | } 106 | 107 | function expand (filename) { 108 | return file + '/' + filename; 109 | } 110 | } 111 | 112 | function listener (file) { 113 | var relative = path.relative(process.cwd(), file.absolutePath); 114 | console.log('jadum: Changes detected in %s, recompiling.', relative); 115 | renderFile(relative); 116 | } 117 | -------------------------------------------------------------------------------- /camelCase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var spaces = /\s+/g; 4 | var dashes = /[-_]+/g; 5 | var dashesLeadTrail = /^-|-$/g; 6 | var invalid = /[^\x20\x2D0-9A-Z\x5Fa-z\xC0-\xD6\xD8-\xF6\xF8-\xFF]/g; 7 | var accentCodePoints = /[\xC0-\xFF]/g; 8 | var accents = [ 9 | [/[\xC0-\xC5]/g, 'A'], 10 | [/[\xC6]/g, 'AE'], 11 | [/[\xC7]/g, 'C'], 12 | [/[\xC8-\xCB]/g, 'E'], 13 | [/[\xCC-\xCF]/g, 'I'], 14 | [/[\xD0]/g, 'D'], 15 | [/[\xD1]/g, 'N'], 16 | [/[\xD2-\xD6\xD8]/g, 'O'], 17 | [/[\xD9-\xDC]/g, 'U'], 18 | [/[\xDD]/g, 'Y'], 19 | [/[\xDE]/g, 'P'], 20 | [/[\xE0-\xE5]/g, 'a'], 21 | [/[\xE6]/g, 'ae'], 22 | [/[\xE7]/g, 'c'], 23 | [/[\xE8-\xEB]/g, 'e'], 24 | [/[\xEC-\xEF]/g, 'i'], 25 | [/[\xF1]/g, 'n'], 26 | [/[\xF2-\xF6\xF8]/g, 'o'], 27 | [/[\xF9-\xFC]/g, 'u'], 28 | [/[\xFE]/g, 'p'], 29 | [/[\xFD\xFF]/g, 'y'] 30 | ]; 31 | 32 | function slugify (text) { 33 | if (text.search(accentCodePoints) === -1) { 34 | return text; 35 | } 36 | return translate(text, accents); 37 | } 38 | 39 | function translate (text, translations) { 40 | return translations.reduce(function (text, a) { 41 | return text.replace(a[0], a[1]); 42 | }, text); 43 | } 44 | 45 | function slug (text) { 46 | return slugify(text) 47 | .replace(invalid, '-') // remove invalid chars 48 | .replace(spaces, '-') // collapse whitespace and replace by '-' 49 | .replace(dashes, '-') // collapse dashes 50 | .replace(dashesLeadTrail, '') // remove leading or trailing dashes 51 | .trim() 52 | .toLowerCase(); 53 | } 54 | 55 | function upper (m, g) { 56 | return g.toUpperCase(); 57 | } 58 | 59 | function camelCase (text) { 60 | return slug(text).replace(/-(.)/g, upper); 61 | } 62 | 63 | module.exports = camelCase; 64 | -------------------------------------------------------------------------------- /jadum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var jade = require('jade'); 5 | var Compiler = require('./Compiler'); 6 | var Parser = require('./Parser'); 7 | var camelCase = require('./camelCase'); 8 | var api = {}; 9 | 10 | function compileClient (text, o) { 11 | var options = o; 12 | if (options === void 0) { 13 | options = {}; 14 | } 15 | options.compiler = Compiler; 16 | options.parser = Parser; 17 | return jade.compileClient(text, options).replace('template', name(options.filename)); 18 | } 19 | 20 | function name (file) { 21 | return camelCase(path.basename(file, '.jade')); 22 | } 23 | 24 | function map (key) { 25 | api[key] = jade[key]; 26 | } 27 | 28 | Object.keys(jade).forEach(map); 29 | 30 | api.compileClient = compileClient; 31 | 32 | module.exports = api; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jadum", 3 | "description": "A lean Jade compiler that understands Browserify and reuses partials", 4 | "version": "1.4.2", 5 | "homepage": "https://github.com/bevacqua/jadum", 6 | "author": { 7 | "name": "Nicolas Bevacqua", 8 | "email": "nico@bevacqua.io", 9 | "url": "http://bevacqua.io" 10 | }, 11 | "main": "jadum.js", 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/bevacqua/jadum.git" 16 | }, 17 | "bin": { 18 | "jadum": "bin/jadum" 19 | }, 20 | "scripts": { 21 | "compare": "mkdir -p test/compiled && jadum test/fixture/listing.jade && mv test/fixture/listing.js test/compiled/listing.jadum.js && jade test/fixture/listing.jade --client --no-debug && mv test/fixture/listing.js test/compiled/listing.jade.js", 22 | "test": "node test/*.js" 23 | }, 24 | "dependencies": { 25 | "contra": "^1.7.0", 26 | "glob": "^7.1.1", 27 | "jade": "^1.7.0", 28 | "minimist": "^1.1.0", 29 | "mkdirp": "^0.5.0", 30 | "monocle": "^1.1.51" 31 | }, 32 | "devDependencies": { 33 | "tape": "^4.6.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | module.exports = require('jade/runtime'); 2 | -------------------------------------------------------------------------------- /test/compiled/listing.jade.js: -------------------------------------------------------------------------------- 1 | function template(locals) { 2 | var buf = []; 3 | var jade_mixins = {}; 4 | var jade_interp; 5 | 6 | buf.push("

This is some awesome listing!

");;return buf.join(""); 30 | } 31 | -------------------------------------------------------------------------------- /test/compiled/listing.jadum.js: -------------------------------------------------------------------------------- 1 | var jade = require("jadum/runtime"); 2 | module.exports = function listing(locals) { 3 | var buf = []; 4 | var jade_mixins = {}; 5 | var jade_interp; 6 | ;var locals_for_with = (locals || {});(function (require) { 7 | buf.push("

This is some awesome listing!

");}.call(this,"require" in locals_for_with?locals_for_with.require:typeof require!=="undefined"?require:undefined));;return buf.join(""); 33 | } -------------------------------------------------------------------------------- /test/fixture/item.jade: -------------------------------------------------------------------------------- 1 | li 2 | span Here comes item: 3 | span Note that this is quite a big template file 4 | span But it's included everywhere, because Jade! 5 | span So that's awesome... but compiling becomes an issue... 6 | span=item 7 | -------------------------------------------------------------------------------- /test/fixture/listing.jade: -------------------------------------------------------------------------------- 1 | h1 This is some awesome listing! 2 | ul 3 | each item in ['a', 'b', 'c'] 4 | include item 5 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var test = require('tape'); 5 | var jadum = require('..'); 6 | 7 | function read (file) { 8 | return fs.readFileSync(file, 'utf8'); 9 | } 10 | 11 | test(function (t) { 12 | var file = 'test/fixture/listing.jade'; 13 | var compiled = jadum.compileClient(read(file), { filename: file }); 14 | var prefix = 'var jade = require("jadum/runtime");\nmodule.exports = '; 15 | var expected = read('test/compiled/listing.jadum.js'); 16 | 17 | t.equal(prefix + compiled, expected); 18 | t.end(); 19 | }); 20 | --------------------------------------------------------------------------------