├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── jhaml ├── jhaml.txt ├── jhamltohtml └── jhamltohtml.txt ├── index.js ├── lib ├── Engine.js ├── Parser.js ├── engines │ ├── Javascript.js │ └── Push.js ├── parsers │ ├── Attributes.js │ ├── Element.js │ └── Haml.js └── utils.js ├── package.json └── test ├── benchmark ├── README.md ├── all.haml ├── express.js ├── index.js └── package.json ├── express.js ├── fixtures ├── filters │ ├── filter.cdata.haml │ ├── filter.cdata.html │ ├── filter.cdata.whitespace.haml │ ├── filter.cdata.whitespace.html │ ├── filter.javascript.haml │ ├── filter.javascript.html │ ├── filter.plain.haml │ └── filter.plain.html ├── haml.js │ ├── class.haml │ ├── class.html │ ├── classes.haml │ ├── classes.html │ ├── code.escape.haml │ ├── code.escape.html │ ├── code.haml │ ├── code.html │ ├── code.if.else.haml │ ├── code.if.else.html │ ├── code.if.haml │ ├── code.if.html │ ├── code.nested.haml │ ├── code.nested.html │ ├── comment.block.conditional.haml │ ├── comment.block.conditional.html │ ├── comment.block.haml │ ├── comment.block.html │ ├── comment.haml │ ├── comment.html │ ├── comment.tag.haml │ ├── comment.tag.html │ ├── comment.text.complex.haml │ ├── comment.text.complex.html │ ├── comment.text.haml │ ├── comment.text.html │ ├── cr.haml │ ├── cr.html │ ├── crlf.haml │ ├── crlf.html │ ├── doctype.haml │ ├── doctype.html │ ├── doctype.xml.case.haml │ ├── doctype.xml.case.html │ ├── doctype.xml.haml │ ├── doctype.xml.html │ ├── error.haml │ ├── error.html │ ├── escape.haml │ ├── escape.html │ ├── html.haml │ ├── html.html │ ├── id.haml │ ├── id.html │ ├── issue.#10.haml │ ├── issue.#10.html │ ├── literals.haml │ ├── literals.html │ ├── namespace.tag.haml │ ├── namespace.tag.html │ ├── nesting.complex.haml │ ├── nesting.complex.html │ ├── nesting.simple.haml │ ├── nesting.simple.html │ ├── newlines.haml │ ├── newlines.html │ ├── newlines.within-tags.haml │ ├── newlines.within-tags.html │ ├── string.complex-interpolation.haml │ ├── string.complex-interpolation.html │ ├── string.interpolation.haml │ ├── string.interpolation.html │ ├── string.multiple-interpolation.haml │ ├── string.multiple-interpolation.html │ ├── tag.attrs.bools.haml │ ├── tag.attrs.bools.html │ ├── tag.attrs.brackets.haml │ ├── tag.attrs.brackets.html │ ├── tag.attrs.escape.haml │ ├── tag.attrs.escape.html │ ├── tag.attrs.haml │ ├── tag.attrs.html │ ├── tag.class.attribute.haml │ ├── tag.class.attribute.html │ ├── tag.class.haml │ ├── tag.class.html │ ├── tag.classes.haml │ ├── tag.classes.html │ ├── tag.code.haml │ ├── tag.code.html │ ├── tag.code.no-escape.haml │ ├── tag.code.no-escape.html │ ├── tag.complex.haml │ ├── tag.complex.html │ ├── tag.empty.haml │ ├── tag.empty.html │ ├── tag.escape.haml │ ├── tag.escape.html │ ├── tag.self-close.haml │ ├── tag.self-close.html │ ├── tag.simple.haml │ ├── tag.simple.html │ ├── tag.text.block.complex.haml │ ├── tag.text.block.complex.html │ ├── tag.text.block.haml │ ├── tag.text.block.html │ ├── tag.text.haml │ ├── tag.text.html │ ├── trailing-indent.haml │ └── trailing-indent.html └── jhaml │ ├── eval │ ├── all.haml │ ├── all.html │ ├── encode.haml │ ├── encode.html │ ├── errors │ │ ├── anothererror.haml │ │ ├── anothererror.html │ │ ├── syntaxerror.haml │ │ ├── syntaxerror.html │ │ ├── undefined.haml │ │ └── undefined.html │ ├── execute.haml │ ├── execute.html │ ├── interpolate.haml │ ├── interpolate.html │ ├── switch.haml │ ├── switch.html │ ├── whitespace.haml │ └── whitespace.html │ ├── html │ ├── attributes.angular2.haml │ ├── attributes.angular2.html │ ├── attributes.boolean.haml │ ├── attributes.boolean.html │ ├── attributes.empty.haml │ ├── attributes.empty.html │ ├── attributes.haml │ ├── attributes.html │ ├── attributes.numbers.haml │ ├── attributes.numbers.html │ ├── comment.haml │ ├── comment.html │ ├── conditionalcomment.haml │ ├── conditionalcomment.html │ ├── elements.haml │ ├── elements.html │ ├── errors │ │ ├── indent.haml │ │ ├── indent.html │ │ ├── invalid.haml │ │ └── invalid.html │ ├── escape.haml │ ├── escape.html │ ├── htmlcomment.haml │ ├── htmlcomment.html │ ├── htmlspecialchar.haml │ ├── htmlspecialchar.html │ ├── quoteinsidequote.haml │ ├── quoteinsidequote.html │ ├── sidebuttons.haml │ ├── sidebuttons.html │ ├── whitespace.haml │ └── whitespace.html │ └── stream.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | report.html 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | - master 5 | 6 | node_js: 7 | - "4" 8 | 9 | matrix: 10 | fast_finish: true 11 | 12 | # container-base 13 | sudo: false 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Jhaml - javascript streamable haml parser 2 | Copyright © 2016 Antoine Bluchet 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JHAML 2 | 3 | [![Build Status](https://travis-ci.org/soyuka/jhaml.svg?branch=master)](https://travis-ci.org/soyuka/jhaml) 4 | 5 | Note: Currently a work in progress, filters are not available yet amongst [some features](https://github.com/soyuka/jhaml#todo). 6 | 7 | ![gulp-ruby-haml vs jhaml](https://pbs.twimg.com/media/Ca7dBSAWIAAPkz_.png:large) 8 | 9 | Lazy? [Jump to usage](https://github.com/soyuka/jhaml#usage) 10 | 11 | ## Introduction 12 | 13 | jHaml stands for Javascript Haml. I'm using scoped packages: 14 | 15 | ``` 16 | npm install @soyuka/jhaml 17 | ``` 18 | 19 | ### Why another HAML implementation? 20 | 21 | Because I wanted a streamable HAML implementation. Also, some things didn't work on javascript implementation as they should in comparison to the ruby version (which I was still using with gulp!). 22 | 23 | For example: 24 | ```haml 25 | %div{attr1: 'one', 26 | attr2: 'two'} some content 27 | ``` 28 | 29 | Errors thrown for no obvious reasons: 30 | 31 | ```haml 32 | %div test 33 | test 34 | ``` 35 | 36 | What about this syntax: 37 | 38 | ```haml 39 | %div{ng: {click: 'test()', if: 'ok === true'}} 40 | -# Results in
, the `_` separator can be configured 41 | ``` 42 | 43 | Also, I almost never need code interpretation inside HAML but I instead write html templates to be used by angular. This is why this parser provides two different streaming engines: 44 | - one that transforms haml to html 45 | - one that transforms haml to javascript which then evaluates to html 46 | 47 | Both are implementing the same `one-pass` algorithm: 48 | 49 | - `jhamltohtml` has a really low memory footprint and is not executing javascript. It just transforms to HTML and writes data as it comes. 50 | - `jhaml` builds javascript in-memory, executes and then streams the result out. 51 | 52 | ### What's different 53 | 54 | **It's a stream!** 55 | 56 | Also, when using code interpretation, the haml code is translated to **strict** ES6 javascript. 57 | 58 | This means: 59 | - it requires node > 4 60 | - it runs with the help of es6 templates 61 | - you have to declare variables 62 | 63 | For example with [tj/haml.js](https://github.com/tj/haml.js) implementation you was able to do: 64 | 65 | ```haml 66 | - name = 'test' 67 | %p= name 68 | ``` 69 | 70 | This implementation would require the variable to be defined, mainly because we're in strict mode: 71 | 72 | ```haml 73 | - let name = 'test' 74 | %p= name 75 | ``` 76 | 77 | You've only access (for now) to basic loops (while, for) and conditions (if, else, switch) but not `.forEach` or `.map` for bloc operations. 78 | 79 | For example this will work: 80 | 81 | ```haml 82 | - let a = [0,1,2].map(i => i+1) 83 | - for(let i in a) 84 | %p= a[i] 85 | ``` 86 | 87 | You may think a `forEach` loop could work, for example: 88 | 89 | ```haml 90 | - ;[0,1,2].forEach(function(e) 91 | %p=e 92 | ``` 93 | 94 | But this will not be closed properly. If you want to do things like that (you should not need this in a template), it'd be written like this: 95 | 96 | ```haml 97 | - ;[0,1,2].forEach(e => __html += `

${e}

`) 98 | ``` 99 | 100 | `__html` is a global scope variable in which html is being appended. 101 | 102 | And here's how a `switch` would be implemented: 103 | 104 | ```haml 105 | - switch(interpolated) 106 | - case 'Test': 107 | %p Ok 108 | - break; 109 | - default: 110 | %p Not ok 111 | ``` 112 | 113 | Basically, code that get's an indented block will be surrounded by `{` and `}`. Code that's on the same indentation will just be executed as is. 114 | 115 | An `if/else` condition is written like this: 116 | 117 | ```haml 118 | - if(disabled) 119 | %p Disabled 120 | - else if(disabled === false) 121 | %p Not disabled 122 | - else 123 | %p Disabled is not defined: "#{disabled}" 124 | ``` 125 | 126 | You can of course execute you own methods: 127 | 128 | ```haml 129 | - let t = myfunction('foo') 130 | %p= t 131 | -# same result as: 132 | %p #{myfunction('foo')} 133 | ``` 134 | 135 | ### Compatibility 136 | 137 | This script runs the [tj/haml.js](https://github.com/tj/haml.js) test suite except non-standard `for each` loops. 138 | 139 | **This does not mean that it'll produce the same output!** 140 | 141 | ## Usage 142 | 143 | ``` 144 | npm install -g @soyuka/jhaml 145 | ``` 146 | 147 | Programmaticaly, `jhaml` gives you two parameters : 148 | - the scope 149 | - options 150 | 151 | For example: 152 | 153 | ```javascript 154 | const jhaml = require('jhaml') 155 | const fs = require('fs') 156 | const scope = {foo: 'bar'} 157 | 158 | fs.createReadStream('source.haml') 159 | .pipe(jhaml(scope, {attributes_separator: '_'})) 160 | ``` 161 | 162 | Current available options are: 163 | - `attributes_separator` (string): a separator for embed attributes. Default to `-`. 164 | - `eval` (boolean): Only available with the Javascript engine (ie when using code interpretation). If set to false, it'll output javascript instead of html. 165 | 166 | The attributes separator is only used when parsing recursive attributes: 167 | 168 | ```haml 169 | %div{ng: {click: 'test()', if: 'available'}} 170 | ``` 171 | 172 | will render: 173 | 174 | ```html 175 |
176 | ``` 177 | 178 | ### With code interpretation 179 | 180 | #### CLI 181 | 182 | Format: `jhaml [--eval] [json scope] [output file]` 183 | 184 | To javascript: 185 | 186 | ```bash 187 | jhaml --no-eval < file.haml '{"foo": "bar"}' 188 | ``` 189 | 190 | To Haml, pipe output: 191 | 192 | ```bash 193 | jhaml < file.haml '{"foo": "bar"}' > file.html 194 | ``` 195 | 196 | To Haml, creates a write stream: 197 | 198 | ```bash 199 | jhaml < file.haml output.html 200 | ``` 201 | 202 | #### Programmatic 203 | 204 | ```javascript 205 | const jhaml = require('jhaml') 206 | const fs = require('fs') 207 | const scope = {foo: 'bar'} 208 | 209 | fs.createReadStream('source.haml') 210 | .pipe(jhaml(scope)) 211 | ``` 212 | 213 | ### Without code interpretation 214 | 215 | #### CLI 216 | 217 | ```bash 218 | jhamltohtml < file.haml > output.html 219 | jhamltohtml < file.haml output.html 220 | ``` 221 | 222 | #### Programmatic 223 | 224 | ```javascript 225 | const jhaml = require('jhaml') 226 | const fs = require('fs') 227 | const scope = {foo: 'bar'} 228 | 229 | fs.createReadStream('source.haml') 230 | .pipe(jhaml.tohtml(scope)) 231 | ``` 232 | 233 | ### Gulp 234 | 235 | [See here for the full documentation](https://github.com/soyuka/gulp-jhaml) 236 | 237 | ``` 238 | npm install @soyuka/gulp-jhaml --save-dev 239 | ``` 240 | 241 | ```javascript 242 | gulp.task('haml', ['haml:clean'], function() { 243 | return gulp.src(['./client/haml/**/*.haml']) 244 | .pipe(haml({}, {eval: false})) 245 | .on('error', function(err) { 246 | console.error(err.stack) 247 | }) 248 | .pipe(gulp.dest('./html')) 249 | }) 250 | ``` 251 | 252 | ### Express 253 | 254 | [See here](https://github.com/soyuka/jhaml/blob/master/test/express.js) or [here with caching abilities](https://github.com/soyuka/jhaml/blob/master/test/benchmark/express.js#L92) 255 | 256 | ## Todo 257 | 258 | - filters 259 | - multi line (`|`) 260 | - support other attributes forms? `:attr => value`, array value, `(type="test")`. needed? 261 | -------------------------------------------------------------------------------- /bin/jhaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | const args = require('minimist')(process.argv.slice(2)) 4 | const Parser = require('../lib/Parser.js') 5 | const fs = require('fs') 6 | const Javascript = require('../lib/engines/Javascript.js') 7 | 8 | if(args.help || args.h) { 9 | let stream = fs.createReadStream(`${__dirname}/jhaml.txt`) 10 | stream.pipe(process.stdout) 11 | stream.on('end', () => process.exit(0)) 12 | return 13 | } 14 | 15 | let Push = require('../lib/engines/Push.js') 16 | 17 | let jsonScope = '' 18 | let output = process.stdout 19 | let scope = null 20 | 21 | if(args._[1]) { 22 | scope = JSON.parse(args._[1]) 23 | output = args._[0] 24 | } else if(args._[0]) { 25 | try { 26 | scope = JSON.parse(args._[0]) 27 | } catch (e) {} 28 | 29 | if(scope === null) 30 | output = args._[0] 31 | } 32 | 33 | let e = args.eval !== undefined ? args.eval : true 34 | let hamltohtml = new Parser({engine: new Javascript({eval: e})}, scope || {}) 35 | 36 | process.stdin.setEncoding('utf8') 37 | 38 | process.stdin 39 | .pipe(hamltohtml) 40 | .pipe(output) 41 | -------------------------------------------------------------------------------- /bin/jhaml.txt: -------------------------------------------------------------------------------- 1 | Name 2 | jhaml - Haml to html (javascript evaluation) 3 | 4 | Synopsis 5 | jhaml [ destination ] [ scope ] [ --eval ] 6 | 7 | Description 8 | Transform : 9 | 10 | jhaml < data.haml > data.html 11 | 12 | Or for code interpretion : 13 | 14 | jhaml < data.haml '{"some": "json"}' --eval 15 | 16 | Options 17 | --eval Eval the code 18 | -------------------------------------------------------------------------------- /bin/jhamltohtml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | const args = require('minimist')(process.argv.slice(2)) 4 | const Parser = require('../lib/Parser.js') 5 | const fs = require('fs') 6 | const Push = require('../lib/engines/Push.js') 7 | 8 | if(args.help || args.h) { 9 | let stream = fs.createReadStream(`${__dirname}/hamltohtml`) 10 | stream.pipe(process.stdout) 11 | stream.on('end', () => process.exit(0)) 12 | return 13 | } 14 | 15 | let hamltohtml = new Parser({engine: new Push()}) 16 | 17 | let output = process.stdout 18 | 19 | if(args._[0]) { 20 | output = fs.createWriteStream(args._[0]) 21 | } 22 | 23 | process.stdin.setEncoding('utf8') 24 | 25 | process.stdin 26 | .pipe(hamltohtml) 27 | .pipe(output) 28 | -------------------------------------------------------------------------------- /bin/jhamltohtml.txt: -------------------------------------------------------------------------------- 1 | Name 2 | hamltohtml - Basic Hml To Html (no code evaluation) 3 | 4 | Synopsis 5 | hamltohtml [ destination ] 6 | 7 | Description 8 | 9 | Transform : 10 | 11 | hamltohtml < data.haml > data.html 12 | 13 | Or 14 | 15 | hamltohtml < data.haml data.html 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let Javascript = require('./lib/engines/Javascript.js') 3 | let Parser = require('./lib/Parser.js') 4 | 5 | function Jhaml(scope, opts) { 6 | return new Parser({engine: new Javascript(opts)}, scope) 7 | } 8 | 9 | Jhaml.tohtml = function hamlToHtml(scope, opts) { 10 | return new Parser(opts, scope) 11 | } 12 | 13 | module.exports = Jhaml 14 | -------------------------------------------------------------------------------- /lib/Engine.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | function Engine(engines) { 3 | if(!(this instanceof Engine)) 4 | return new Engine(engines) 5 | 6 | if(!Array.isArray(engines)) 7 | engines = [engines] 8 | 9 | this.engines = engines 10 | this.length = engines.length 11 | } 12 | 13 | /** 14 | * Loop through engines, 15 | */ 16 | Engine.prototype._loop = function() { 17 | let args = [].slice.call(arguments) 18 | let method = args.shift() 19 | 20 | for(let i = 0; i < this.engine.length; i++) { 21 | if(!(method in this.engine.engines[i])) { 22 | continue 23 | } 24 | 25 | this.currentEngine = this.engine.engines[i] 26 | this.currentEngine[method].apply(this, args) 27 | } 28 | } 29 | 30 | /** 31 | * Default methods 32 | */ 33 | ;['start', 'end', 'close', 'code', 'open', 'shadowLine'] 34 | .forEach(function(e) { 35 | Engine.prototype[e] = function(element, indent) { 36 | return this.engine._loop.bind(this)(e, element, indent) 37 | } 38 | }) 39 | 40 | /** 41 | * Content replaces the previous content 42 | */ 43 | Engine.prototype.content = function(content, element, indent) { 44 | 45 | for(let i = 0; i < this.engine.length; i++) { 46 | if(!('content' in this.engine.engines[i])) { 47 | continue 48 | } 49 | 50 | this.currentEngine = this.engine.engines[i] 51 | content = this.currentEngine.content.bind(this)(content, element, indent) 52 | } 53 | 54 | } 55 | 56 | module.exports = Engine 57 | -------------------------------------------------------------------------------- /lib/Parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const Transform = require('stream').Transform 4 | const util = require('util') 5 | const Element = require('./parsers/Element.js') 6 | const Attributes = require('./parsers/Attributes.js') 7 | const Haml = require('./parsers/Haml.js') 8 | const Push = require('./engines/Push.js') 9 | const Engine = require('./Engine.js') 10 | 11 | const utils = require('./utils.js') 12 | 13 | const bufferToString = utils.bufferToString 14 | 15 | /** 16 | * Main transform stream 17 | * parses the input and handles HAML through parsers 18 | * data is then handled by Engines to produce an output 19 | */ 20 | function Parser(opt, scope) { 21 | if(!(this instanceof Parser)) { 22 | return new Parser(opt, scope) 23 | } 24 | 25 | if(!opt) 26 | opt = {} 27 | 28 | Transform.call(this) 29 | 30 | //Set up the engine 31 | this.engine = opt.engine ? new Engine(opt.engine) : new Engine(new Push()) 32 | //Stack of non-closed elements to close 33 | this.stack = [] 34 | //Garbage collector 35 | this.garbage = null 36 | //Line cursor 37 | this.line = 0 38 | //line we started at 39 | this.startline = null 40 | //Previous indentation 41 | this.previousIndent = 0 42 | //Current indentation 43 | this.indentation = null 44 | //HAML Parser instance 45 | this.haml = new Haml() 46 | //End 47 | this.closed = false 48 | 49 | this.options = opt 50 | 51 | //The eval scope 52 | this.scope = util._extend({ 53 | __encode: function(str) { 54 | str = ''+str 55 | return str.replace(/&/g, '&') 56 | .replace(/>/g, '>') 57 | .replace(/ this.startline 73 | } 74 | }) 75 | 76 | /** 77 | * @inheritdoc 78 | */ 79 | Parser.prototype._transform = function(chunk, encoding, callback) { 80 | 81 | if(this.indentation === null && this.line === 0) { 82 | this.engine.start.bind(this)() 83 | } 84 | 85 | let length = chunk.length 86 | 87 | if(this.hasGarbage()) { 88 | chunk = Buffer.concat([this.garbage, chunk], this.garbage.length + length) 89 | length = chunk.length 90 | } 91 | 92 | let start = 0 93 | let end = 0 94 | 95 | try { 96 | for(let i = 0; i <= length; i++) { 97 | if(this.hasFullLine(chunk, end)) { 98 | this.processLine(chunk.slice(start, end)) 99 | this.line++ 100 | start = end 101 | } 102 | 103 | end++ 104 | } 105 | } catch(e) { 106 | callback(e) 107 | } 108 | 109 | this.garbage = chunk.slice(start, chunk.length) 110 | 111 | callback() 112 | } 113 | 114 | /** 115 | * Tests that the garbage contains more than spaces, newlines or null values 116 | * if it contains only those we reached EOF 117 | * @return {Boolean} 118 | */ 119 | Parser.prototype.hasGarbage = function() { 120 | if(!this.garbage) 121 | return 122 | 123 | let length = this.garbage.length 124 | 125 | for(let i = 0; i < length; i++) { 126 | if(!~[this.space, this.cr, this.nl, null].indexOf(this.garbage[i])) 127 | return true 128 | } 129 | 130 | return false 131 | } 132 | 133 | /** 134 | * Check if a chunk ends with a comma (might be followed by spaces) 135 | * @param {Buffer} chunk 136 | * @param {Number} end position 137 | */ 138 | Parser.prototype.endsWithComma = function(chunk, end) { 139 | let i = end 140 | while(i--) { 141 | if(chunk[i] === this.space) 142 | continue 143 | 144 | if(chunk[i] === this.comma) 145 | return true 146 | 147 | return false 148 | } 149 | } 150 | 151 | /** 152 | * Tests that the chunk is a full line 153 | * a full line is identified by \r?\n or a comma 154 | * Having a comma at the end means that we're in the middle of attributes, 155 | * in this case, the parser will continue until it reaches EOL 156 | * @param {Buffer} chunk 157 | * @param {Number} end position 158 | */ 159 | Parser.prototype.hasFullLine = function(chunk, end) { 160 | let endsWithComma = this.endsWithComma(chunk, end) 161 | 162 | if(chunk[end] === this.nl) 163 | return !endsWithComma 164 | 165 | if(chunk[end] === this.cr) { 166 | return !endsWithComma 167 | } 168 | 169 | if(chunk[end] === this.cr && chunk[end + 1] === this.nl) { 170 | return !endsWithComma 171 | } 172 | 173 | return chunk.length === end 174 | } 175 | 176 | /** 177 | * Is the buffer an opening/closing quote? 178 | * @param {Buffer} chunk 179 | * @param {Number} i 180 | * @param {Number} previousQuote 181 | */ 182 | Parser.prototype.isQuote = function(chunk, i, previousQuote) { 183 | let quote = chunk[i] 184 | 185 | if(!(quote === this.singleQuote || quote === this.doubleQuote)) 186 | return false 187 | 188 | if(previousQuote === null || previousQuote === quote) { 189 | if(chunk[i-1] !== this.escape) { 190 | return true 191 | } 192 | } 193 | 194 | return false 195 | } 196 | 197 | /** 198 | * Line processing 199 | * This parses the line buffer and gets element, attributes and content 200 | * @param {Buffer} chunk a full line buffer 201 | * @return void 202 | */ 203 | Parser.prototype.processLine = function(chunk) { 204 | //missing : 205 | //:filters 206 | let length = chunk.length 207 | let indent = 0 208 | let began = false 209 | let inQuote = false 210 | let quote = null 211 | 212 | let content = null 213 | let attributes = new Attributes(this.options) 214 | let element = new Element(this.options) 215 | element.attributes = attributes 216 | 217 | this.haml.handled = false 218 | 219 | for(let i = 0; i <= length; i++) { 220 | //toggle quote 221 | if(this.isQuote(chunk, i, quote)) { 222 | inQuote = !inQuote 223 | quote = !inQuote ? null : chunk[i] 224 | } 225 | 226 | //null nl/cr chunks, we're in a full line, we don't need them 227 | if(~[this.nl, this.cr].indexOf(chunk[i])) { 228 | chunk[i] = null 229 | continue 230 | } 231 | 232 | //This is were the first not-space character is detected 233 | if(began) { 234 | //we don't care to parse things inside quotes 235 | if(inQuote) { 236 | continue 237 | } 238 | 239 | //Does the attributes start here? 240 | if(element.started() && chunk[i - 1] !== this.space && chunk[i] === this.attributes.start) { 241 | attributes.start = i 242 | 243 | //according to haml specs if the attributes starts, element ends 244 | //we could change this behavior to enable classes/ids after attributes 245 | //for example: %label{for: 'foo'}.bar 246 | if(!element.ended()) 247 | element.end = i 248 | 249 | continue 250 | } 251 | 252 | //we're currently inside attributes, just wait until they ends 253 | if(attributes.started() && !attributes.ended()) { 254 | if(chunk[i] === this.attributes.start) { 255 | attributes.embed++ 256 | continue 257 | } 258 | 259 | if(chunk[i] !== this.attributes.end) { 260 | continue 261 | } else if(attributes.embed > 0) { 262 | attributes.embed-- 263 | continue 264 | } 265 | 266 | attributes.end = i + 1 267 | continue 268 | } 269 | 270 | if(this.haml.handled === false) { 271 | content = this.haml.handle(chunk, i, element, attributes, indent) 272 | } 273 | 274 | if(element.closed) 275 | break 276 | 277 | //If the element hasn't started yet, the previous character is the first 278 | if(!element.started() && (!chunk[i-2] || chunk[i-2] === this.space)) { 279 | let isElement = ~[this.id, this.classname, this.element].indexOf(chunk[i - 1]) 280 | 281 | if(isElement) { 282 | element.start = i - 1 283 | continue 284 | } 285 | } 286 | 287 | if(content !== null) 288 | break 289 | 290 | //the chunk is a space, we're not inside attributes 291 | if(chunk[i] === this.space) { 292 | //there might be no elements, and if there are no attributes, 293 | //element ends here 294 | if(element.started() && !attributes.started() && !element.ended()) { 295 | element.end = i 296 | } 297 | 298 | //next characters are content, we can end the loop 299 | if(attributes.ended() || element.ended()) { 300 | content = i + 1 301 | break 302 | } 303 | //element has not started and this is not an haml reserved character 304 | } else if(!element.started() && !~[this.encode_interpolated, this.interpolate_encode, this.code, this.exclamation].indexOf(chunk[i])) { 305 | element.contentOnly = true 306 | element.closed = true 307 | content = i - 1 308 | break 309 | } 310 | } 311 | 312 | //Parse indentation 313 | if(!began && chunk[i] === this.space) { 314 | indent++ 315 | continue 316 | } 317 | 318 | //not a space, the next characters are building our haml element/content 319 | began = true 320 | 321 | //remove dual spaces 322 | if(chunk[i] === this.space && chunk[i + 1] === this.space) { 323 | chunk[i] = null 324 | } 325 | 326 | } 327 | 328 | //get file indentation once 329 | if(this.indentation === null && indent > 0) { 330 | this.indentation = indent 331 | } 332 | 333 | //haml comments or empty elements 334 | if(element.skip || element.tag.open === null && !element.started() && !element.closed) { 335 | this.engine.shadowLine.bind(this)(element, indent) 336 | return 337 | } 338 | 339 | //parse element 340 | if(!element.closed) { 341 | element.parse(chunk) 342 | } 343 | 344 | //parse attributes 345 | if(attributes.ended()) { 346 | attributes.parse(chunk) 347 | } else { 348 | //handles element attributes if any 349 | attributes.parse() 350 | } 351 | 352 | //there was no element, chunk is content-only 353 | if(element.tag.open === null && !element.closed) { 354 | content = bufferToString(chunk, indent === 0 ? 0 : indent + 1) 355 | } else { 356 | content = content !== null ? bufferToString(chunk, content) : '' 357 | } 358 | 359 | if(content.trim().length > 0) { 360 | element.content = true 361 | } else { 362 | element.content = false 363 | } 364 | 365 | if(this.startline === null && (element.started() || (element.contentOnly && element.content)) && !element.code) { 366 | this.startline = this.line 367 | } 368 | 369 | //Close previously opened tags, to do so we just remove elements from 370 | //our stack one by one and close them 371 | if(indent <= this.previousIndent) { 372 | let cursor = this.previousIndent + this.indentation 373 | 374 | while(cursor > indent) { 375 | let previous = this.stack.pop() 376 | 377 | cursor -= this.indentation 378 | 379 | if(!previous) 380 | break 381 | 382 | if(previous.tag.open === null || previous.closed) 383 | continue 384 | 385 | this.engine.close.bind(this)(previous, cursor) 386 | } 387 | 388 | this.previousIndent = indent 389 | 390 | let previous = this.stack[0] 391 | 392 | if(previous && indent === 0 && previous.content === true) { 393 | this.engine.close.bind(this)(previous, indent) 394 | this.stack.pop() 395 | } 396 | } 397 | 398 | //Open the current element 399 | if(indent >= this.previousIndent) { 400 | if(element.started()) { 401 | this.engine.open.bind(this)(element, indent) 402 | } 403 | 404 | //Got content in it 405 | if(element.content === true) { 406 | this.engine.content.bind(this)(content, element, indent) 407 | } 408 | } 409 | 410 | this.stack.push(element) 411 | 412 | this.previousIndent = indent 413 | } 414 | 415 | //@TODO fix this.closed === false, this should be called once 416 | Parser.prototype._flush = function(callback) { 417 | let e = this.stack.pop() 418 | let indent = null 419 | 420 | while(e) { 421 | indent = indent === null ? this.previousIndent : indent 422 | 423 | if(!e.closed) { 424 | this.engine.close.bind(this)(e, indent) 425 | } 426 | 427 | indent -= this.indentation 428 | 429 | e = this.stack.pop() 430 | } 431 | 432 | if(this.stack.length === 0 && this.closed === false) { 433 | this.closed = true 434 | 435 | this.engine.end.bind(this)(callback) 436 | } 437 | } 438 | 439 | module.exports = Parser 440 | -------------------------------------------------------------------------------- /lib/engines/Javascript.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const vm = require('vm') 3 | const util = require('util') 4 | const EOL = require('os').EOL 5 | const spaces = require('../utils.js').spaces 6 | 7 | /** 8 | * Matches interpolated content #{some "{{content}}" here} 9 | */ 10 | const interpolatedContentRegex = new RegExp(/#{[^}]*?(?:(?:('|")[^'"]*?\1)[^}]*?)*}/g) 11 | 12 | function Javascript(opts) { 13 | if(!(this instanceof Javascript)) 14 | return new Javascript(opts) 15 | 16 | if(!opts) 17 | opts = {} 18 | 19 | if(opts.filename) 20 | this.filename = opts.filename 21 | else 22 | this.filename = 'JHaml' 23 | 24 | this._javascript = '' 25 | this._lineOffset = 0 26 | this.eval = opts.eval === undefined ? true : opts.eval 27 | } 28 | 29 | /** 30 | * Push wrapped data to append it to the __html string 31 | * @param {String} data 32 | */ 33 | Javascript.prototype.wrapAndPush = function(data) { 34 | return this.push(`__html += \`${data}\`;`) 35 | } 36 | 37 | /** 38 | * Push code to the _javascript string 39 | * @param {String} data 40 | */ 41 | Javascript.prototype.push = function(data) { 42 | this._javascript += '\n' + data 43 | } 44 | 45 | /** 46 | * We've a line that will not end up in the html, increase the offset 47 | * @inheritdoc 48 | */ 49 | Javascript.prototype.shadowLine = function(element, indent) { 50 | this.currentEngine._lineOffset++ 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | Javascript.prototype.start = function() { 57 | this.currentEngine.push("'use strict';") 58 | this.currentEngine.push('for(let i in scope) { if(!global[i]) { global[i] = scope[i]; }}') 59 | this.currentEngine.push("var __html = '';") 60 | this.currentEngine._lineOffset = this.currentEngine._lineOffset - 2 61 | } 62 | 63 | /** 64 | * Open code 65 | * @TODO improve open/closing tag so that .forEach() can be used 66 | * @param {Number} indent 67 | */ 68 | Javascript.prototype.openCode = function(indent) { 69 | let previous = this.stack[this.stack.length - 1] 70 | 71 | if(!previous) 72 | return 73 | 74 | if(previous.code === true && this.previousIndent < indent) { 75 | this.currentEngine.push('{') 76 | previous.codeOpen = true 77 | } 78 | } 79 | 80 | /** 81 | * Close Code 82 | * @TODO improve open/closing tag so that .forEach() can be used 83 | * @param {Number} indent 84 | */ 85 | Javascript.prototype.closeCode = function(element, indent) { 86 | if(element.codeOpen) 87 | this.currentEngine.push('}') 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | */ 93 | Javascript.prototype.open = function(element, indent) { 94 | 95 | this.currentEngine.openCode.bind(this)(indent) 96 | 97 | if(element.code) { 98 | this.currentEngine.push(spaces(indent) + element.open()) 99 | return 100 | } 101 | 102 | let previous = this.stack[this.stack.length - 1] 103 | let space = true 104 | 105 | if(element.whitespaceremoval.before === true) 106 | space = false 107 | else if(previous) { 108 | if(previous.whitespaceremoval.after === true) 109 | space = false 110 | } 111 | 112 | let prefix = space ? spaces(indent) : '' 113 | 114 | if(this.started && space) { 115 | prefix = '\\n'+prefix 116 | } 117 | 118 | this.currentEngine.wrapAndPush(prefix + element.open()) 119 | } 120 | 121 | /** 122 | * @inheritdoc 123 | */ 124 | Javascript.prototype.close = function(element, indent, test) { 125 | let prefix = '' 126 | 127 | if(element.whitespaceremoval.after === false && element.content === false && !element.code) { 128 | prefix = '\\n' + spaces(indent) 129 | } 130 | 131 | this.currentEngine._lineOffset-- 132 | 133 | if(element.code) { 134 | this.currentEngine.push(spaces(indent) + element.close()) 135 | this.currentEngine.closeCode.bind(this)(element, indent) 136 | } else 137 | this.currentEngine.wrapAndPush(prefix + element.close()) 138 | } 139 | 140 | /** 141 | * @inheritdoc 142 | */ 143 | Javascript.prototype.content = function(content, element, indent) { 144 | 145 | if(element.interpolate && element.encode === false) 146 | content = '${'+content+'}' 147 | else if(element.interpolate) 148 | content = '${__encode('+content+')}' 149 | 150 | if(/#{/g.test(content)) { 151 | let match = interpolatedContentRegex.exec(content) 152 | 153 | while(match) { 154 | let str = match[0].replace(/^#{/, '').replace(/}$/, '') 155 | if(element.encode !== false) 156 | str = '__encode('+str+')' 157 | 158 | content = content.replace(match[0], '${'+str+'}') 159 | match = interpolatedContentRegex.exec(content) 160 | } 161 | } 162 | 163 | if(element.tag.open !== null) 164 | this.currentEngine._lineOffset-- 165 | 166 | let previous = this.stack[this.stack.length - 1] 167 | 168 | let whitespaceremoval = previous && previous.whitespaceremoval.before === true 169 | 170 | if(element.contentOnly && this.started && !whitespaceremoval) 171 | content = '\\n' + spaces(indent) + content 172 | 173 | content = `__html += \`${content}\`;` 174 | 175 | this.currentEngine.push(content) 176 | 177 | return content 178 | } 179 | 180 | /** 181 | * @inheritdoc 182 | */ 183 | Javascript.prototype.end = function(cb) { 184 | let sandbox = util._extend({}, this.scope) 185 | 186 | if(!this.currentEngine.eval) { 187 | this.push(this.currentEngine._javascript) 188 | return cb() 189 | } 190 | 191 | this.currentEngine._javascript += 'return __html;'; 192 | 193 | try { 194 | let compile = new Function('scope', this.currentEngine._javascript) 195 | 196 | this.push(compile(sandbox)) 197 | } catch(e) { 198 | let stack = e.stack.split(EOL) 199 | 200 | if(e instanceof SyntaxError) { 201 | console.error(e.stack) 202 | return cb(e) 203 | } 204 | 205 | if(stack.length) { 206 | let line = stack[1].trim().match(/:(\d+):(\d+)\)/) 207 | let lines = this.currentEngine._javascript.split(EOL) 208 | 209 | ;[line[1] - 4, line[1] - 3, -1, line[1] - 2].forEach(num => { 210 | 211 | if(num === -1) { 212 | console.error(new Array(+line[2] - 1).fill('-').join('') + '^') //cursor 213 | let hamlLine = line[1] - 3 + this.currentEngine._lineOffset 214 | 215 | console.error(`${e.name}: ${e.message} (${this.currentEngine.filename}:${hamlLine})`) 216 | return 217 | } 218 | 219 | console.error(lines[num]) 220 | }) 221 | } 222 | 223 | return cb(e) 224 | } 225 | 226 | cb() 227 | } 228 | 229 | module.exports = Javascript 230 | -------------------------------------------------------------------------------- /lib/engines/Push.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const spaces = require('../utils.js').spaces 3 | 4 | /** 5 | * Engine that just writes HTML 6 | */ 7 | function Push() { 8 | if(!(this instanceof Push)) 9 | return new Push() 10 | } 11 | 12 | /** 13 | * @inheritdoc 14 | */ 15 | Push.prototype.open = function(element, indent) { 16 | 17 | let previous = this.stack[this.stack.length - 1] 18 | let space = true 19 | 20 | if(element.whitespaceremoval.before === true) 21 | space = false 22 | else if(previous) { 23 | if(previous.whitespaceremoval.after === true) 24 | space = false 25 | } 26 | 27 | if(this.started && space) 28 | this.push('\n') 29 | 30 | if(space) 31 | this.push(spaces(indent)) 32 | 33 | this.push(element.open()) 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | Push.prototype.close = function(element, indent) { 40 | if(element.whitespaceremoval.after === false && element.content === false) 41 | this.push('\n' + spaces(indent)) 42 | 43 | this.push(element.close()) 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | Push.prototype.content = function(content, element, indent) { 50 | let previous = this.stack[this.stack.length - 1] 51 | 52 | let whitespaceremoval = previous && previous.whitespaceremoval.before === true 53 | 54 | if(element.contentOnly && this.started && !whitespaceremoval) 55 | this.push('\n' + spaces(indent)) 56 | 57 | this.push(content) 58 | } 59 | 60 | Push.prototype.end = function(cb) { 61 | cb() 62 | } 63 | 64 | module.exports = Push 65 | -------------------------------------------------------------------------------- /lib/parsers/Attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const bufferToString = require('../utils.js').bufferToString 3 | const isNumeric = require('../utils.js').isNumeric 4 | 5 | /** 6 | * Attributes parser 7 | */ 8 | function Attributes(opts) { 9 | if(!(this instanceof Attributes)) 10 | return new Attributes(opts) 11 | 12 | this.start = null 13 | this.end = null 14 | this.merge = {} 15 | this.attributes = '' 16 | this.html = '' 17 | this.embed = 0 18 | //Separator for embed attributes 19 | this.separator = opts.attributes_separator !== undefined ? opts.attributes_separator : '-' 20 | } 21 | 22 | /** 23 | * Have we found the start position ? 24 | * @return {Boolean} 25 | */ 26 | Attributes.prototype.started = function() { 27 | return this.start !== null 28 | } 29 | 30 | /** 31 | * Have we reached the end position ? 32 | * @return {Boolean} 33 | */ 34 | Attributes.prototype.ended = function() { 35 | return this.start !== null && this.end !== null 36 | } 37 | 38 | /** 39 | * Parse transforms the chunk in a string 40 | * except code see cssParser 41 | * @return {this} 42 | */ 43 | Attributes.prototype.parse = function(chunk) { 44 | if(!chunk) 45 | return this.toHtml() 46 | 47 | chunk = bufferToString(chunk, this.start, this.end) 48 | 49 | this.attributes = chunk 50 | 51 | return this.toHtml() 52 | } 53 | 54 | /** 55 | * Has attributes? 56 | * @return {Boolean} 57 | */ 58 | Attributes.prototype.has = function() { 59 | let l = Object.keys(this.merge).filter(e => this.merge[e] !== '').length 60 | return (this.attributes.length + l) > 0 61 | } 62 | 63 | /** 64 | * Adds attributes that will be merged 65 | * @param {Object} o 66 | */ 67 | Attributes.prototype.add = function(o) { 68 | for(let i in o) { 69 | if(this.merge[i]) 70 | this.merge[i] += o[i] 71 | else 72 | this.merge[i] = o[i] 73 | } 74 | 75 | return this 76 | } 77 | 78 | /** 79 | * A kind of recursive json parser 80 | * this allows attributes to be complex: 81 | * {ng: {foo: 'bar', bar: {foo: 'bar', bar: 'foo'}}} 82 | * results in ng_foo: 'bar', ng_bar_foo: 'bar', ng_bar_bar: 'foo' 83 | * @param {String} data 84 | * @return {Object} 85 | */ 86 | Attributes.prototype.parseAttributes = function(data) { 87 | let attrs = {} 88 | let i = 0 89 | let length = data.length 90 | let inQuote = false 91 | let key = {start: null, end: null} 92 | let embed = 0 93 | let hasEmbed = false 94 | let quote = null 95 | 96 | for(let i = 0; i < length; i++) { 97 | let char = data[i] 98 | 99 | //test for quotes 100 | if(char === '\'' || char === '"') { 101 | if(quote === null || quote === char) { 102 | if(data[i - 1] != '\\') { 103 | inQuote = !inQuote 104 | quote = !inQuote ? null : char 105 | } 106 | } 107 | } 108 | 109 | //inside quotes we don't care 110 | if(inQuote) 111 | continue 112 | 113 | //key start/end positions 114 | if(char === '{' && key.start === null) { 115 | key.start = i + 1 116 | continue 117 | } else if(key.start !== null && char === ':' && key.end === null) { 118 | key.end = i 119 | continue 120 | } 121 | 122 | //Key ends 123 | if(key.end !== null) { 124 | //It's an embed attribute 125 | if(char === '{') { 126 | embed++ 127 | hasEmbed = true 128 | } else if(char === '}') { 129 | embed-- 130 | } 131 | 132 | if(hasEmbed && embed != -1) 133 | continue 134 | 135 | //Attribute key/value pair ends 136 | if(char === '}' || char === ',') { 137 | let k = data.slice(key.start, key.end) 138 | .replace(/^,\s?/, '') 139 | .replace(/'|"/g, '') 140 | .trim() 141 | let v = data.slice(key.end + 1, i).trim() 142 | 143 | if(hasEmbed) { 144 | //recursion 145 | attrs[k] = this.parseAttributes(v) 146 | } else { 147 | if(attrs[k]) 148 | attrs[k] += v 149 | else 150 | attrs[k] = v 151 | } 152 | 153 | 154 | key.start = i + 1 155 | key.end = null 156 | continue; 157 | } 158 | } 159 | } 160 | 161 | return attrs 162 | } 163 | 164 | /** 165 | * Transforms a recursive object to proper key/value pairs 166 | * for example {ng: {click: 'go()'}} will be ng_click: 'go()' 167 | * @param {Object} attrs 168 | * @param {String|undefined} key 169 | */ 170 | Attributes.prototype.recurseAttributes = function(attrs, key) { 171 | let o = {} 172 | 173 | for(let i in attrs) { 174 | let k = key ? key + this.separator + i : i 175 | 176 | if(typeof attrs[i] === 'object') { 177 | let a = this.recurseAttributes(attrs[i], k) 178 | for(let j in a) { 179 | o[j] = a[j] 180 | } 181 | } else { 182 | o[k] = attrs[i] 183 | } 184 | } 185 | 186 | return o 187 | } 188 | 189 | /** 190 | * Transforms attributes to html 191 | * @return {this} 192 | */ 193 | Attributes.prototype.toHtml = function() { 194 | if(!this.has()) 195 | return '' 196 | 197 | let attrs = {} 198 | 199 | if(this.attributes) { 200 | attrs = this.recurseAttributes(this.parseAttributes(this.attributes)) 201 | } 202 | 203 | for(let i in this.merge) { 204 | if(!this.merge[i]) 205 | continue 206 | 207 | if(i in attrs && typeof attrs[i] === 'string' && (attrs[i][0] == '"' || attrs[i][0] == "'")) { 208 | attrs[i] = attrs[i][0] + this.merge[i] + ' ' + attrs[i].substring(1, attrs[i].length) 209 | } else { 210 | attrs[i] = `"${this.merge[i]}"` 211 | } 212 | } 213 | 214 | this.html = '' 215 | 216 | for(let i in attrs) { 217 | this.html += ` ${i}` 218 | 219 | if(attrs[i] === 'true') 220 | continue 221 | 222 | if(isNumeric(attrs[i])) 223 | attrs[i] = `"${attrs[i]}"` 224 | 225 | if(attrs[i][0] === "'" || attrs[i][0] === '"' ) { 226 | this.html += `=${attrs[i]}` 227 | } else { 228 | this.html += '="${'+attrs[i]+'}"' 229 | } 230 | } 231 | 232 | return this 233 | } 234 | 235 | module.exports = Attributes 236 | -------------------------------------------------------------------------------- /lib/parsers/Element.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Attributes = require('./Attributes.js') 3 | const bufferToString = require('../utils.js').bufferToString 4 | 5 | const selfClosing = [ 6 | 'meta', 'img', 'link', 'br', 'hr', 'input', 'area', 'base' 7 | ] 8 | 9 | const cssRegExp = new RegExp('([#|\.|%][^#|\.|%]*)', 'gi') 10 | 11 | /** 12 | * Element parser 13 | */ 14 | function Element() { 15 | if(!(this instanceof Element)) 16 | return new Element() 17 | 18 | //start position 19 | this.start = null 20 | //end position 21 | this.end = null 22 | //Attributes instance 23 | this.attributes = null 24 | 25 | //is the element open yet 26 | this.opened = false 27 | //is the element cloed yet 28 | this.closed = false 29 | //is there a following content on the same line? %p stuff 30 | this.content = false 31 | this.contentOnly = false 32 | //is this code? 33 | this.code = false 34 | //should it be htmlencoded 35 | this.encode = false 36 | //should it be interpolated 37 | this.interpolate = false 38 | //force auto close (ex DOCTYPE) 39 | this.selfClose = false 40 | //skip element 41 | this.skip = false 42 | //white space removal 43 | this.whitespaceremoval = {before: false, after: false} 44 | 45 | //prefix and suffix for open/close state 46 | this.prefix = {open: '<', close: '', close: '>'} 48 | 49 | //tags 50 | this.tag = {open: null, close: null} 51 | } 52 | 53 | /** 54 | * Have we found the start position ? 55 | * @return {Boolean} 56 | */ 57 | Element.prototype.started = function() { 58 | return this.start !== null 59 | } 60 | 61 | /** 62 | * Have we reached the end position ? 63 | * @return {Boolean} 64 | */ 65 | Element.prototype.ended = function() { 66 | return this.start !== null && this.end !== null 67 | } 68 | 69 | /** 70 | * Parse transforms the chunk in a string 71 | * except code see cssParser 72 | * @return {this} 73 | */ 74 | Element.prototype.parse = function(chunk) { 75 | chunk = bufferToString(chunk, this.start, this.end) 76 | 77 | if(this.code) { 78 | this.tag.open = chunk 79 | return this 80 | } 81 | 82 | return this.cssParser(chunk) 83 | } 84 | 85 | /** 86 | * Parses the element string, adds class/id attributes 87 | * %ul#test.foo.bar =>
    88 | * @return {this} 89 | */ 90 | Element.prototype.cssParser = function(chunk) { 91 | let match = cssRegExp.exec(chunk) 92 | let attributes = {class: '', id: ''} 93 | 94 | while(match !== null) { 95 | let c = match[0].charAt(0) 96 | if(c === '%') { 97 | this.tag.open = match[0].replace(c, '') 98 | } else if(c === '#') { 99 | attributes.id = match[0].replace(c, '') 100 | } else { 101 | attributes.class += match[0].replace(c, attributes.class === '' ? '' : ' ') 102 | } 103 | 104 | match = cssRegExp.exec(chunk) 105 | } 106 | 107 | this.attributes.add(attributes) 108 | 109 | if(this.attributes.has() && this.tag.open === null) 110 | this.tag.open = 'div' 111 | 112 | this.tag.close = this.tag.open 113 | 114 | return this 115 | } 116 | 117 | /** 118 | * Checks if self closing on known elements 119 | * @return {String} 120 | */ 121 | Element.prototype.selfClosing = function() { 122 | return ~selfClosing.indexOf(this.tag.open) ? ' /' : '' 123 | } 124 | 125 | /** 126 | * Opens an element 127 | * @return {String} 128 | */ 129 | Element.prototype.open = function() { 130 | this.opened = true 131 | 132 | this.closed = this.selfClose || !!this.selfClosing() 133 | 134 | if(this.tag.open === null) 135 | return '' 136 | 137 | return `${this.prefix.open}${this.tag.open}${this.attributes.html}${this.selfClosing()}${this.suffix.open}` 138 | } 139 | 140 | /** 141 | * Closes an element 142 | * @return {String} 143 | */ 144 | Element.prototype.close = function() { 145 | this.closed = true 146 | 147 | if(this.tag.close === null) 148 | return '' 149 | 150 | return `${this.prefix.close}${this.tag.close}${this.suffix.close}` 151 | } 152 | 153 | module.exports = Element 154 | -------------------------------------------------------------------------------- /lib/parsers/Haml.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('../utils') 3 | 4 | /** 5 | * HAML syntax handler 6 | */ 7 | function Haml() { 8 | if(!(this instanceof Haml)) 9 | return new Haml() 10 | 11 | utils.toCharBufferMap(utils.CHAR_MAP, this) 12 | 13 | this.handled = false 14 | this.skip_indent = null 15 | } 16 | 17 | /** 18 | * Whether we're in a HAML comment or not 19 | * @param {Number} indent 20 | * @return {Boolean} 21 | */ 22 | Haml.prototype.skip = function(indent) { 23 | if(this.skip_indent !== null) { 24 | if(indent > this.skip_indent) { 25 | return true 26 | } 27 | 28 | if(indent <= this.skip_indent) { 29 | this.skip_indent = null 30 | } 31 | } 32 | 33 | return false 34 | } 35 | 36 | /** 37 | * Handles the chunk and modify the element/attributes according to the parsing 38 | * result 39 | * @param {Buffer} chunk 40 | * @param {Number} i - current char cursor 41 | * @param {Element} element - current element 42 | * @param {Attributes} attributes - current attributes 43 | * @param {Number} indent - current indentation 44 | * @return {Number|null} content the content new index (will break the loop) or null 45 | */ 46 | Haml.prototype.handle = function(chunk, i, element, attributes, indent) { 47 | let content = null 48 | let first = chunk[i - 1] 49 | 50 | //Escape content 51 | if(first === this.escape) { 52 | element.start = 0 53 | element.end = 1 54 | element.closed = true 55 | content = i 56 | element.content = true 57 | this.handled = true 58 | return content 59 | } 60 | 61 | let array = [first, chunk[i]] 62 | 63 | //HAML comments 64 | if(this.skip(indent) || utils.equalsBuffer(array, this.comment)) { 65 | element.skip = true 66 | element.closed = true 67 | 68 | if(this.skip_indent === null) 69 | this.skip_indent = indent 70 | 71 | this.handled = true 72 | return content 73 | } 74 | 75 | //white space removal only works after a tag 76 | if(element.started() && (first === this.lt || first === this.gt)) { 77 | if(first === this.lt) { 78 | element.whitespaceremoval.after = true 79 | } else if(first === this.gt) { 80 | element.whitespaceremoval.before = true 81 | } 82 | 83 | if(!element.ended()) 84 | element.end = i - 1 85 | 86 | return content 87 | } 88 | 89 | if(!element.started()) { 90 | //Code 91 | if(first === this.code) { 92 | element.start = i 93 | element.end = chunk.length 94 | element.code = true 95 | element.prefix.open = '' 96 | element.suffix.open = '' 97 | element.tag.close = '' 98 | content = chunk.length 99 | element.prefix.close = '' 100 | element.suffix.close = '' 101 | this.handled = true 102 | return content 103 | } 104 | } 105 | 106 | //Conditionnal comment 107 | if(utils.equalsBuffer(array, this.conditionalComment.start)) { 108 | 109 | for(let j = i + 1; j < chunk.length; j++) { 110 | if(chunk[j] === this.conditionalComment.end) { 111 | content = j 112 | break; 113 | } 114 | } 115 | 116 | if(content === null) 117 | throw new SyntaxError('Conditionnal comment is not closed') 118 | 119 | element.start = i 120 | element.tag.open = '' 121 | element.prefix.open = '' 124 | content = content + 1 125 | 126 | this.handled = true 127 | return content 128 | } 129 | 130 | //HTML comment (and is not an ending html tag ' 138 | element.content = true 139 | content = i 140 | 141 | this.handled = true 142 | return content 143 | } 144 | 145 | array.push(chunk[i+1]) 146 | 147 | //Doctype 148 | if(utils.equalsBuffer(array, this.doctype)) { 149 | let type = utils.bufferToString(chunk, i + 3, chunk.length).trim().toLowerCase() 150 | 151 | element.tag.open = utils.DOCTYPES[type] 152 | 153 | if(element.tag.open === undefined) { 154 | console.error('Doctype %s not found, falling back to default', type) 155 | element.tag.open = utils.DOCTYPES[''] 156 | } 157 | 158 | element.start = i + 3 159 | element.selfClose = true 160 | element.prefix.open = type === 'xml' ? '' : '>' 162 | content = chunk.length 163 | 164 | this.handled = true 165 | return content 166 | } 167 | 168 | let inAttributes = attributes.started() && !attributes.ended() 169 | 170 | //Parses =, !=, &, &= when we've reach the element 171 | if(!inAttributes && chunk[i] === this.space) { 172 | let first = chunk[i-2] 173 | let second = chunk[i-1] 174 | let array = [first, second] 175 | 176 | element.interpolate = second === this.interpolate_encode 177 | element.encode = true 178 | element.content = true 179 | 180 | //number of characters that we should remove from element 181 | let chars = null 182 | 183 | // != 184 | if(utils.equalsBuffer(array, this.interpolate)) { 185 | chars = 2 186 | element.encode = false 187 | // &= 188 | } else if (utils.equalsBuffer(array, this.encode)) { 189 | chars = 2 190 | } else if(second === this.encode_interpolated) { 191 | chars = 1 192 | element.interpolate = false 193 | } else if(second === this.interpolate_encode) { 194 | chars = 1 195 | } 196 | 197 | if(chars === null) { 198 | return content 199 | } 200 | 201 | if(!element.started()) 202 | element.start = i 203 | 204 | //hypothetical start cursor 205 | let startCursor = i - chars - 1 206 | if(startCursor < 0) startCursor = 0 207 | 208 | //if this an full-part element? != 'test' instead of %p!= 'test' 209 | if(startCursor === 0 || chunk[startCursor] === this.space) { 210 | content = i + chars - 1 211 | element.closed = true 212 | this.handled = true 213 | 214 | return content 215 | } 216 | 217 | if(!element.ended()) 218 | element.end = i - chars 219 | 220 | content = i + 1 221 | this.handled = true 222 | 223 | return content 224 | } 225 | 226 | return content 227 | } 228 | 229 | module.exports = Haml 230 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * HAML character map 5 | */ 6 | const CHAR_MAP = { 7 | cr: '\r', 8 | nl: '\n', 9 | tab: '\t', 10 | space: ' ', 11 | comma: ',', 12 | element: '%', 13 | classname: '.', 14 | id: '#', 15 | singleQuote: '\'', 16 | doubleQuote: '"', 17 | attributes: { 18 | start: '{', 19 | end: '}' 20 | }, 21 | null: null, 22 | comment: '-#', 23 | htmlComment: '/', 24 | conditionalComment: { 25 | start: '/[', 26 | end: ']' 27 | }, 28 | escape: '\\', 29 | doctype: '!!!', 30 | code: '-', 31 | interpolate_encode: '=', 32 | interpolate: '!=', 33 | encode: '&=', 34 | encode_interpolated: '&', 35 | lt: '<', 36 | gt: '>', 37 | exclamation: '!' 38 | } 39 | 40 | const BUFFER_CHAR_MAP = {} 41 | toCharBufferMap(CHAR_MAP, BUFFER_CHAR_MAP) 42 | 43 | const BUFFER_SPACE_SET = [BUFFER_CHAR_MAP.cr, BUFFER_CHAR_MAP.nl, BUFFER_CHAR_MAP.tab] 44 | 45 | const DOCTYPES = { 46 | '': 'DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"', 47 | 'strict': 'DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"', 48 | 'frameset': 'DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"', 49 | '5': 'DOCTYPE html', 50 | '1.1': 'DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"', 51 | 'basic': 'DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd"', 52 | 'mobile': 'DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd"', 53 | 'rdfa': 'DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd"', 54 | 'xml': 'xml version="1.0" encoding="utf-8"' 55 | } 56 | 57 | /** 58 | * Removes BUFFER_SPACE_SET from the buffer by setting null values instead 59 | * @param {Buffer} chunk 60 | * @return {Buffer} 61 | */ 62 | function sanitizeBuffer(chunk) { 63 | let length = chunk.length 64 | for(let i = 0; i < length; i++) { 65 | if( 66 | ~[BUFFER_SPACE_SET].indexOf(chunk[i]) || 67 | chunk[i] === BUFFER_CHAR_MAP.space && chunk[i + 1] === BUFFER_CHAR_MAP.space 68 | ) { 69 | chunk[i] = null 70 | } 71 | } 72 | 73 | return chunk 74 | } 75 | 76 | /** 77 | * Transforms a buffer to a string after sanitizing it 78 | * @param {Buffer} chunk 79 | * @param {Number} start optional start position 80 | * @param {Number} end optional end posititon 81 | */ 82 | function bufferToString(chunk, start, end) { 83 | if(start !== undefined) 84 | chunk = chunk.slice(start, end || chunk.length) 85 | 86 | return sanitizeBuffer(chunk).toString() 87 | .replace(/\0/g, '') 88 | .replace(/`/g, '\\`') 89 | } 90 | 91 | /** 92 | * Recursively transforms a char to an ascii byte code 93 | * or to a Buffer (if more than 1 char) 94 | * those are set on the self element 95 | * @param {mixed} o 96 | * @param {Object} self 97 | */ 98 | function toCharBufferMap(o, self) { 99 | for(let i in o) { 100 | if(typeof o[i] === 'string') 101 | self[i] = o[i].length === 1 ? new Buffer(o[i])[0] : new Buffer(o[i]) 102 | else if(typeof o[i] === 'object') { 103 | self[i] = {} 104 | toCharBufferMap(o[i], self[i]) 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Compares an array of ascii bytes to a Buffer 111 | * @param {Array} array 112 | * @param {Buffer} buffer 113 | * @return {Boolean} 114 | */ 115 | function equalsBuffer(array, buffer) { 116 | return array.filter((e, i) => e !== buffer[i]).length === 0 117 | } 118 | 119 | /** 120 | * A padLeft function to add spaces 121 | * @param {Number} num 122 | * @return {String} 123 | */ 124 | function spaces(num) { 125 | if(!num) 126 | num = 0 127 | 128 | let str = '' 129 | 130 | for(let i = 0; i < num; i++) { str += ' ' } 131 | 132 | return str 133 | } 134 | 135 | function isNumeric(n) { 136 | return !isNaN(parseFloat(n)) && isFinite(n) 137 | } 138 | 139 | module.exports = { 140 | bufferToString: bufferToString, 141 | toCharBufferMap: toCharBufferMap, 142 | equalsBuffer: equalsBuffer, 143 | spaces: spaces, 144 | isNumeric: isNumeric, 145 | CHAR_MAP: CHAR_MAP, 146 | DOCTYPES: DOCTYPES 147 | } 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@soyuka/jhaml", 3 | "version": "1.0.8", 4 | "description": "Stream HAML to HTML", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/soyuka/jhaml.git" 9 | }, 10 | "bin": { 11 | "jhaml": "./bin/jhaml", 12 | "jhamltohtml": "./bin/jhamltohtml" 13 | }, 14 | "scripts": { 15 | "test": "mocha test/index.js" 16 | }, 17 | "keywords": [], 18 | "author": "soyuka ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "minimist": "^1.2.0" 22 | }, 23 | "devDependencies": { 24 | "event-stream": "^3.3.2", 25 | "mocha": "^2.4.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | [![asciicast](https://asciinema.org/a/36893.png)](https://asciinema.org/a/36893) 4 | 5 | http://soyuka.me/benchmarking-nodejs-streams/ 6 | 7 | Computer informations: 8 | 9 | ``` 10 | Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz 11 | MemTotal: 8096856 kB 12 | Device Model: Samsung SSD 840 EVO 250GB 13 | ``` 14 | 15 | ## Api-benchmark 16 | 17 | ```bash 18 | cd test/benchmark 19 | npm install 20 | node express.js #listens on 3002 21 | ``` 22 | 23 | The express app has 5 routes: 24 | - `/` - normal render mode 25 | - `/pipe` - just pipe haml to res 26 | - `/cached` - cache javascript per view 27 | - `/hamljs` - hamljs rendering 28 | - `/hamljs/cached` - hamljs cached rendering 29 | 30 | Start api benchmarks: 31 | 32 | ```bash 33 | node index.js 34 | ``` 35 | 36 | Results: 37 | 38 | ```bash 39 | server1/cached x 700 ops/sec ±8.35% (1000 runs sampled) 40 | server1/pipe x 295 ops/sec ±5.59% (1000 runs sampled) 41 | server1/render x 294 ops/sec ±10.09% (1000 runs sampled) 42 | server1/hamljs x 1,401 ops/sec ±3.71% (1000 runs sampled) 43 | server1/hamljscached x 1,923 ops/sec ±5.92% (1000 runs sampled) 44 | ``` 45 | 46 | ## Siege results 47 | 48 | Normal render function: 49 | 50 | ```bash 51 | (master ✗)❯ sudo siege -c1 -b -t10s http://localhost:3002/ 52 | 53 | Transactions: 3402 hits 54 | Availability: 100.00 % 55 | Elapsed time: 9.83 secs 56 | Data transferred: 5.16 MB 57 | Response time: 0.00 secs 58 | Transaction rate: 346.08 trans/sec 59 | Throughput: 0.52 MB/sec 60 | Concurrency: 0.99 61 | Successful transactions: 3402 62 | Failed transactions: 0 63 | Longest transaction: 0.16 64 | Shortest transaction: 0.00 65 | ``` 66 | 67 | Pipe: 68 | 69 | ```bash 70 | (master ✗)❯ sudo siege -c1 -b -t10s http://localhost:3002/pipe 71 | 72 | Transactions: 3237 hits 73 | Availability: 100.00 % 74 | Elapsed time: 9.77 secs 75 | Data transferred: 4.91 MB 76 | Response time: 0.00 secs 77 | Transaction rate: 331.32 trans/sec 78 | Throughput: 0.50 MB/sec 79 | Concurrency: 0.99 80 | Successful transactions: 3237 81 | Failed transactions: 0 82 | Longest transaction: 0.25 83 | Shortest transaction: 0.00 84 | ``` 85 | 86 | Cache: 87 | 88 | ```bash 89 | (master ✗)❯ sudo siege -c1 -b -t10s http://localhost:3002/cached 90 | 91 | Transactions: 9299 hits 92 | Availability: 100.00 % 93 | Elapsed time: 9.36 secs 94 | Data transferred: 14.09 MB 95 | Response time: 0.00 secs 96 | Transaction rate: 993.48 trans/sec 97 | Throughput: 1.51 MB/sec 98 | Concurrency: 0.99 99 | Successful transactions: 9299 100 | Failed transactions: 0 101 | Longest transaction: 0.30 102 | Shortest transaction: 0.00 103 | ``` 104 | 105 | Hamljs: 106 | 107 | ```bash 108 | (master ✗)❯ sudo siege -c1 -b -t10s http://localhost:3002/hamljs 109 | 110 | Transactions: 22514 hits 111 | Availability: 100.00 % 112 | Elapsed time: 9.50 secs 113 | Data transferred: 32.89 MB 114 | Response time: 0.00 secs 115 | Transaction rate: 2369.89 trans/sec 116 | Throughput: 3.46 MB/sec 117 | Concurrency: 0.98 118 | Successful transactions: 22514 119 | Failed transactions: 0 120 | Longest transaction: 0.01 121 | Shortest transaction: 0.00 122 | ``` 123 | 124 | Hamljs-cached: 125 | 126 | ```bash 127 | (master ✗)❯ sudo siege -c1 -b -t10s http://localhost:3002/hamljs/cached 128 | 129 | Transactions: 37803 hits 130 | Availability: 100.00 % 131 | Elapsed time: 9.80 secs 132 | Data transferred: 55.23 MB 133 | Response time: 0.00 secs 134 | Transaction rate: 3857.45 trans/sec 135 | Throughput: 5.64 MB/sec 136 | Concurrency: 0.97 137 | Successful transactions: 37803 138 | Failed transactions: 0 139 | Longest transaction: 0.02 140 | Shortest transaction: 0.00 141 | ``` 142 | -------------------------------------------------------------------------------- /test/benchmark/all.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %div.search-container 3 | %input.action-sheet-container.class2{type: 'search', ng_value: 'value'} 4 | #search-results.foo{zf_action_sheet: true, class: 'bar'} 5 | %div{zf_as_content: true, position: 'bottom'} 6 | %ul 7 | %li{ng_repeat: 'result in results', 8 | ng_class: '{active: results[index] == result}', 9 | ng_click: 'selectResult(results.indexOf(result))'} 10 | %a {{result.text}} 11 | %span 12 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 13 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 14 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, 15 | no sea takimata sanctus est Lorem ipsum dolor sit amet. 16 | %label.middle 17 | %strong price 18 | %span 0.131 € 19 | -# HAML comment 20 | this should not be printed 21 | neither should this 22 | %p 23 | \ %p escape 24 | \ %div 25 | The following has an html comment 26 | / this is an html comment 27 | this too btw 28 | hidden in the text 29 | %p!= 'this will not be encoded' 30 | %p= 'this will be encoded' 31 | /[if IE>5] 32 | %noscript holy shit a conditional comment, are we in 2016 yet? 33 | 34 | %select{disabled: disabled === true} 35 | - for(var i in select) 36 | %option{value: select[i].value}= select[i].text 37 | 38 | %p Variable #{interpolated} 39 | != test 40 | = test 41 | &= 'this & is encoded' 42 | & this works #{'this & is encoded'} too, so is #{'this -------------------------------------------------------------------------------- /test/fixtures/filters/filter.cdata.whitespace.haml: -------------------------------------------------------------------------------- 1 | %script 2 | :cdata 3 | $(function(){ 4 | if (foo) 5 | if (bar) 6 | alert('yay') 7 | }) -------------------------------------------------------------------------------- /test/fixtures/filters/filter.cdata.whitespace.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/filters/filter.javascript.haml: -------------------------------------------------------------------------------- 1 | %head 2 | :javascript 3 | if (foo) 4 | if (bar) 5 | alert("baz") 6 | foo() 7 | bar() 8 | %title Yay -------------------------------------------------------------------------------- /test/fixtures/filters/filter.javascript.html: -------------------------------------------------------------------------------- 1 | 9 | Yay -------------------------------------------------------------------------------- /test/fixtures/filters/filter.plain.haml: -------------------------------------------------------------------------------- 1 | %body 2 | :plain 3 | .foo 4 | bar 5 | = baz -------------------------------------------------------------------------------- /test/fixtures/filters/filter.plain.html: -------------------------------------------------------------------------------- 1 | .foo 2 | bar 3 | = baz -------------------------------------------------------------------------------- /test/fixtures/haml.js/class.haml: -------------------------------------------------------------------------------- 1 | .users -------------------------------------------------------------------------------- /test/fixtures/haml.js/class.html: -------------------------------------------------------------------------------- 1 |
    2 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/classes.haml: -------------------------------------------------------------------------------- 1 | .foo.bar.baz_is-awesome -------------------------------------------------------------------------------- /test/fixtures/haml.js/classes.html: -------------------------------------------------------------------------------- 1 |
    2 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.escape.haml: -------------------------------------------------------------------------------- 1 | = "" -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.escape.html: -------------------------------------------------------------------------------- 1 | <br/> -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.haml: -------------------------------------------------------------------------------- 1 | != "foo" + "bar" -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.html: -------------------------------------------------------------------------------- 1 | foobar -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.if.else.haml: -------------------------------------------------------------------------------- 1 | - let name = "not tj :(" 2 | - if (name === 'tj') 3 | %p You are logged in 4 | - else 5 | %p You are NOT logged in 6 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.if.else.html: -------------------------------------------------------------------------------- 1 | 2 |

    You are NOT logged in

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.if.haml: -------------------------------------------------------------------------------- 1 | - let name = "tj" 2 | - if (name === 'tj') 3 | %p You are logged in 4 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.if.html: -------------------------------------------------------------------------------- 1 |

    You are logged in

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.nested.haml: -------------------------------------------------------------------------------- 1 | - if (true) 2 | - if (false) 3 | %p nope 4 | - if (true) 5 | %p yay -------------------------------------------------------------------------------- /test/fixtures/haml.js/code.nested.html: -------------------------------------------------------------------------------- 1 | 2 |

    yay

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.block.conditional.haml: -------------------------------------------------------------------------------- 1 | /[if IE] 2 | %a{ 'href' : 'http://www.mozilla.com/en-US/firefox/' } 3 | %h1 Get Firefox 4 | /[if IE] 5 | %a{ 'href' : 'http://www.mozilla.com/en-US/firefox/' } 6 | /[if IE 6] 7 | %h1 Get Firefox (IE6 user) 8 | /[if IE 7] 9 | %h1 Get Firefox (IE7 user) -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.block.conditional.html: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.block.haml: -------------------------------------------------------------------------------- 1 | / 2 | %ul 3 | %li nope 4 | %li nope 5 | %li nope -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.block.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.haml: -------------------------------------------------------------------------------- 1 | -# nothing 2 | %p yay 3 | -# whatever you want -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.html: -------------------------------------------------------------------------------- 1 |

    yay

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.tag.haml: -------------------------------------------------------------------------------- 1 | / 2 | %p foo 3 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.tag.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.text.complex.haml: -------------------------------------------------------------------------------- 1 | %ul 2 | %li one 3 | / first item 4 | %li two 5 | / second item 6 | %ul 7 | %li three -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.text.complex.html: -------------------------------------------------------------------------------- 1 |
      2 |
    • one
    • 3 | 4 |
    • two
    • 5 | 6 |
        7 |
      • three
      • 8 |
      9 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.text.haml: -------------------------------------------------------------------------------- 1 | %p 2 | / foo bar baz -------------------------------------------------------------------------------- /test/fixtures/haml.js/comment.text.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 |

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/cr.haml: -------------------------------------------------------------------------------- 1 | #foo .bar -------------------------------------------------------------------------------- /test/fixtures/haml.js/cr.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/crlf.haml: -------------------------------------------------------------------------------- 1 | #foo 2 | .bar -------------------------------------------------------------------------------- /test/fixtures/haml.js/crlf.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/doctype.haml: -------------------------------------------------------------------------------- 1 | !!! -------------------------------------------------------------------------------- /test/fixtures/haml.js/doctype.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/doctype.xml.case.haml: -------------------------------------------------------------------------------- 1 | !!! xml -------------------------------------------------------------------------------- /test/fixtures/haml.js/doctype.xml.case.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/doctype.xml.haml: -------------------------------------------------------------------------------- 1 | !!! XML -------------------------------------------------------------------------------- /test/fixtures/haml.js/doctype.xml.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/error.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %body 3 | %p -------------------------------------------------------------------------------- /test/fixtures/haml.js/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    4 |

    5 | 6 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/escape.haml: -------------------------------------------------------------------------------- 1 | %p 2 | \.foo -------------------------------------------------------------------------------- /test/fixtures/haml.js/escape.html: -------------------------------------------------------------------------------- 1 |

    2 | .foo 3 |

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/html.haml: -------------------------------------------------------------------------------- 1 | %div 2 |

    literal

    3 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/html.html: -------------------------------------------------------------------------------- 1 |
    2 |

    literal

    3 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/id.haml: -------------------------------------------------------------------------------- 1 | #users -------------------------------------------------------------------------------- /test/fixtures/haml.js/id.html: -------------------------------------------------------------------------------- 1 |
    2 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/issue.#10.haml: -------------------------------------------------------------------------------- 1 | %label{ for: "forsomething"} -------------------------------------------------------------------------------- /test/fixtures/haml.js/issue.#10.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/literals.haml: -------------------------------------------------------------------------------- 1 | - let user = 'user' 2 | %p= user 3 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/literals.html: -------------------------------------------------------------------------------- 1 |

    user

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/namespace.tag.haml: -------------------------------------------------------------------------------- 1 | %body 2 | %fb:test.a 3 | %fb:bar 4 | %fb:foo{ title: 'home' } 5 | %fb:login-button 6 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/namespace.tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/nesting.complex.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %title Page Title 4 | %body 5 | %h1 Title 6 | %p some stuff -------------------------------------------------------------------------------- /test/fixtures/haml.js/nesting.complex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Page Title 4 | 5 | 6 |

    Title

    7 |

    some stuff

    8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/nesting.simple.haml: -------------------------------------------------------------------------------- 1 | %ul 2 | %li one 3 | %li two 4 | %li 5 | %ul 6 | %li three -------------------------------------------------------------------------------- /test/fixtures/haml.js/nesting.simple.html: -------------------------------------------------------------------------------- 1 |
      2 |
    • one
    • 3 |
    • two
    • 4 |
    • 5 |
        6 |
      • three
      • 7 |
      8 |
    • 9 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/newlines.haml: -------------------------------------------------------------------------------- 1 | %ul 2 | %li one 3 | %li two 4 | %li three 5 | %li 6 | %ul 7 | %li four 8 | 9 | %p foo 10 | %p 11 | bar 12 | baz -------------------------------------------------------------------------------- /test/fixtures/haml.js/newlines.html: -------------------------------------------------------------------------------- 1 |
      2 |
    • one
    • 3 |
    • two
    • 4 |
    • three
    • 5 |
    • 6 |
        7 |
      • four
      • 8 |
      9 |
    • 10 |
    11 |

    foo

    12 |

    13 | bar 14 | baz 15 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/newlines.within-tags.haml: -------------------------------------------------------------------------------- 1 | %head 2 | %title 3 | 4 | tabineko 5 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/newlines.within-tags.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | tabineko 4 | 5 | yay it works! {oh noes} y u do dis to me

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/string.interpolation.haml: -------------------------------------------------------------------------------- 1 | %p yay #{message} 2 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/string.interpolation.html: -------------------------------------------------------------------------------- 1 |

    yay it works!

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/string.multiple-interpolation.haml: -------------------------------------------------------------------------------- 1 | %p yay #{message} - #{stuff} 2 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/string.multiple-interpolation.html: -------------------------------------------------------------------------------- 1 |

    yay it works! - just fine!

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.bools.haml: -------------------------------------------------------------------------------- 1 | %input{ type: 'checkbox', checked: true } -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.bools.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.brackets.haml: -------------------------------------------------------------------------------- 1 | %a{ href: '/', title: '{{title}}' } 2 | %a{ href: '/', title: 'the title is {{title}} !' } 3 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.brackets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.escape.haml: -------------------------------------------------------------------------------- 1 | %option{ value: '' } -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.escape.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.haml: -------------------------------------------------------------------------------- 1 | %a{ href: '/', title: 'home' } -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.attrs.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.class.attribute.haml: -------------------------------------------------------------------------------- 1 | %h1.title{ class: "red" } 2 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.class.attribute.html: -------------------------------------------------------------------------------- 1 |

    2 |

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.class.haml: -------------------------------------------------------------------------------- 1 | %h1.title -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.class.html: -------------------------------------------------------------------------------- 1 |

    2 |

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.classes.haml: -------------------------------------------------------------------------------- 1 | %body.front-page.editing-user -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.classes.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.code.haml: -------------------------------------------------------------------------------- 1 | %div= "yay" -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.code.html: -------------------------------------------------------------------------------- 1 |
    yay
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.code.no-escape.haml: -------------------------------------------------------------------------------- 1 | %div!= "
    " -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.code.no-escape.html: -------------------------------------------------------------------------------- 1 |

    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.complex.haml: -------------------------------------------------------------------------------- 1 | %a#delete-user.first.button{ href: '/', title: 'Click to delete' }= "Delete " + "tj" -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.complex.html: -------------------------------------------------------------------------------- 1 | Delete tj -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.empty.haml: -------------------------------------------------------------------------------- 1 | %body 2 | %div.a 3 | %div.b 4 | .c 5 | .d 6 | #e 7 | #f -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.empty.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.escape.haml: -------------------------------------------------------------------------------- 1 | %div= "
    " -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.escape.html: -------------------------------------------------------------------------------- 1 |
    <br/>
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.self-close.haml: -------------------------------------------------------------------------------- 1 | %br -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.self-close.html: -------------------------------------------------------------------------------- 1 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.simple.haml: -------------------------------------------------------------------------------- 1 | %div -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.simple.html: -------------------------------------------------------------------------------- 1 |
    2 |
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.text.block.complex.haml: -------------------------------------------------------------------------------- 1 | %p 2 | Visit 3 | %a{ href: 'http://vision-media.ca' } Vision Media 4 | because im amazing, 5 | yes... -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.text.block.complex.html: -------------------------------------------------------------------------------- 1 |

    2 | Visit 3 | Vision Media 4 | because im amazing, 5 | yes... 6 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.text.block.haml: -------------------------------------------------------------------------------- 1 | %p 2 | some text 3 | and more text 4 | and even more text! 5 | OMG -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.text.block.html: -------------------------------------------------------------------------------- 1 |

    2 | some text 3 | and more text 4 | and even more text! 5 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.text.haml: -------------------------------------------------------------------------------- 1 | %div some text -------------------------------------------------------------------------------- /test/fixtures/haml.js/tag.text.html: -------------------------------------------------------------------------------- 1 |

    some text
    -------------------------------------------------------------------------------- /test/fixtures/haml.js/trailing-indent.haml: -------------------------------------------------------------------------------- 1 | %a 2 | %b 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/haml.js/trailing-indent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/jhaml/eval/all.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %div.search-container 3 | %input.action-sheet-container.class2{type: 'search', ng_value: 'value'} 4 | #search-results.foo{zf_action_sheet: true, class: 'bar'} 5 | %div{zf_as_content: true, position: 'bottom'} 6 | %ul 7 | %li{ng_repeat: 'result in results', 8 | ng_class: '{active: results[index] == result}', 9 | ng_click: 'selectResult(results.indexOf(result))'} 10 | %a {{result.text}} 11 | %span 12 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 13 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 14 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, 15 | no sea takimata sanctus est Lorem ipsum dolor sit amet. 16 | %label.middle 17 | %strong price 18 | %span 0.131 € 19 | -# HAML comment 20 | this should not be printed 21 | neither should this 22 | %p 23 | \ %p escape 24 | \ %div 25 | The following has an html comment 26 | / this is an html comment 27 | this too btw 28 | hidden in the text 29 | %p!= 'this will not be encoded' 30 | %p= 'this will be encoded' 31 | /[if IE>5] 32 | %noscript holy shit a conditional comment, are we in 2016 yet? 33 | 34 | %select{disabled: disabled === true} 35 | - for(let i in select) 36 | %option{value: select[i].value}= select[i].text 37 | 38 | %p Variable #{interpolated} 39 | != test 40 | = test 41 | &= 'this & is encoded' 42 | & this works #{'this & is encoded'} too, so is #{'this