├── CHANGELOG.markdown ├── LICENSE ├── README.markdown ├── lib ├── cli.js └── haml.js ├── package.json ├── test.haml └── test ├── alt_attribs.haml ├── alt_attribs.html ├── blank.haml ├── blank.html ├── comments.haml ├── comments.html ├── css.haml ├── css.html ├── div_nesting.haml ├── div_nesting.html ├── doctype.haml ├── doctype.html ├── else.haml ├── else.html ├── embedded_code.haml ├── embedded_code.html ├── embedded_code.js ├── escaping.haml ├── escaping.html ├── escaping.js ├── foreach.haml ├── foreach.html ├── foreach.js ├── if.haml ├── if.html ├── if.js ├── interpolation.haml ├── interpolation.html ├── meta.haml ├── meta.html ├── nanline.haml ├── nanline.html ├── nested_context.haml ├── nested_context.html ├── nested_context.js ├── no_self_close_div.haml ├── no_self_close_div.html ├── non-string-attribs.haml ├── non-string-attribs.html ├── optional_attribs.haml ├── optional_attribs.html ├── other ├── custom_escape.haml ├── custom_escape.html ├── escape_by_default.haml └── escape_by_default.html ├── raw.haml ├── raw.html ├── raw_complex.haml ├── raw_complex.html ├── script_css.haml ├── script_css.html ├── self_close.haml ├── self_close.html ├── self_close.js ├── standard.haml ├── standard.html ├── standard.js ├── test-commonjs.js ├── test.js ├── whitespace.haml └── whitespace.html /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | # HAML-JS Changelog 2 | 3 | - **v0.4.0** 4 | Breaking Changes: 5 | Made interpolation #{} escaped by default. Use !{} for unsafe interpolation. 6 | 7 | New Features: 8 | * Optionally exclude `html_escape` function definition from every template -- provide your own escape function invocation string ("MyApp.htmlEscape") and it will be used instead, dramatically shrinking template sizes. 9 | * Optionally escape all output of `=` by default. Set the escapeHtmlByDefault configuration variable. 10 | * New never-escaped `!=` recommended for when you **want** to output strings that contain html. 11 | * More test coverage for interpolation and escaping 12 | 13 | Bugfix: "inside" whitespace was not concatenating properly in some cases. 14 | 15 | - **v0.3.0** 16 | New features: 17 | * Comments -- Haml comments, HTML comments, JavaScript Comments 18 | * Raw JS -- this lets you use if/else, switch, try/catch, et cetera 19 | in your views (use cautiously!) 20 | * Whitespace insertion -- Now, you can insert whitespace in and/or 21 | around tags using < and >. Check the docs. 22 | * Blank templates are now valid! 23 | * More test coverage 24 | 25 | - **v0.2.5** - *2010-05-06* - NPM support 26 | 27 | Fixed to work with Node Package Manager 28 | 29 | - **v0.2.4** - *2010-04-16* - Bug fixes, XML support 30 | 31 | Allow for commas in calls to helpers in attributes. Also make haml more XML friendly. 32 | 33 | - **v0.2.3** - *2010-04-10* - Bug fixes 34 | 35 | Fixed an issue where "content" html attributes got munched. (This broke meta tags) 36 | 37 | - **v0.2.2** - *2010-04-05* - Bug fixes 38 | 39 | Fixed two issues where the parser incorrectly parsed blank lines and extra spaces in attribute blocks. 40 | 41 | - **v0.2.1** - *2010-04-01* - Minor speed tweak 42 | 43 | `Haml()` now caches the eval step so that there is no eval in executing a compiled template. This should make things a bit faster. 44 | 45 | - **v0.2.0** - *2010-03-31* - Function based API, Safe whitespace, Code interpolation. 46 | 47 | At the request of some users, I've removed the new insertion into the generated html. This means that most html will be on one long line, but as an added advantage you won't have that extra whitespace next to your anchor labels messing up your visual display. 48 | 49 | Also I added string interpolation to every place I could fit it. This means you can do crazy stuff like interpolate within strings in attributes, in the body on plain text sections, and of course in javascript and css plugin blocks. 50 | 51 | In order to tame the API, I deprecated the four old interfaces `compile`, `optimize`, `execute` and `render`. The new API is that the Haml/exports object itself is now a function that takes in haml text and outputs a compiled, optimized, ready to execute function. 52 | 53 | - **0.1.2** - *2010-02-03* - Bug fixes, plugin aliases, CommonJS, and more... 54 | 55 | This is a big release with many improvements. First haml-js is now a CommonJS module and is in the Tusk repository. Thanks to Tom Robinson for helping with that. Some of the plugins got aliases for people who didn't like the original name. For example, you can now do `:javascript` instead of `:script` and `:for` instead of `:each`. There were many bug fixes now that the code is starting to be actually used by myself and others. 56 | 57 | - **0.1.1** - *2010-01-09* - Add :css and :script plugins 58 | 59 | Added two quick plugins that make working with javascript and css much easier. 60 | 61 | - **0.1.0** - *2010-01-09* - Complete Rewrite 62 | 63 | Rewrote the compiler to be recursive and compile to JavaScript code instead of JSON data structures. This fixes all the outstanding bugs and simplifies the code. Pending is restoring the `:script` and `:css` plugins. 64 | 65 | - **0.0.1** - *2009-12-16* - Initial release 66 | 67 | Change how haml is packaged. It is a pure JS function with no node dependencies. There is an exports hook for commonjs usability. It's now the responsibility of the script user to acquire the haml text. 68 | 69 | 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Tim Caswell 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # haml-js - Server side templating language for JavaScript 2 | 3 | Ever wanted to use the excellent HAML syntax on a javascript project? Me too, so I made one!. This has most of the same functionality as the traditional [haml][]. 4 | 5 | ## About the language 6 | 7 | Here is the first example(with a little extra added) from the [haml][] site converted to haml-js: 8 | 9 | **haml-js** 10 | 11 | !!! XML 12 | !!! strict 13 | %html{ xmlns: "http://www.w3.org/1999/xhtml" } 14 | %head 15 | %title Sample haml template 16 | %body 17 | .profile 18 | .left.column 19 | #date= print_date() 20 | #address= current_user.address 21 | .right.column 22 | #email= current_user.email 23 | #bio= current_user.bio 24 | 25 | **html** 26 | 27 | 28 | 29 | Sample haml template 30 |
January 1, 2009 31 |
Richardson, TX 32 |
tim@creationix.com 33 |
Experienced software professional... 34 |
35 | 36 | Note that this works almost the same as ruby's [haml][], but doesn't pretty print the html. This would greatly slow down and complicate the code. If you really want pretty printed html, then I suggest writing one using the xml parser library and process the resulting html.. 37 | 38 | ## API 39 | 40 | ### Haml(haml) -> template(locals) -> html 41 | 42 | This is the new (as of 0.2.0) way to generate haml templates. A haml template is a live function that takes in "this" context and a "locals" variable. This compile step takes a few milliseconds to complete so it should be done at startup and the resulting function should be cached. Then to use the template function you simply call it with the desired local variables and it will output html at blazing speeds (we're talking millions per second on my 13" MBP) 43 | 44 | Compile and store a template: 45 | 46 | var main = Haml(main_haml); 47 | 48 | Then use it whenever you need a new version: 49 | 50 | main({name: "Tim", age: 28}); 51 | 52 | That's it. Haml templating made easy! 53 | 54 | If you want to store the generated javascript to a file to skip the compile step later on you can either decompile the template function or use the `compile` and `optimize` advanced functions directly. 55 | 56 | 57 | ### Haml.compile(text) -> JavaScript compiled template 58 | 59 | Given a haml template as raw text, this compiles it to a javascript expression 60 | that can later be eval'ed to get the final HTML. 61 | 62 | The following input: 63 | 64 | #home 65 | = title 66 | %ul.menu 67 | %li Go Home 68 | %li Go Back 69 | 70 | Produces the following JavaScript expression: 71 | 72 | "
" + 73 | title + 74 | "\n" + 75 | "" + 83 | "
" 84 | 85 | ### Haml.optimize(js) -> optimized JavaScript expression 86 | 87 | Takes the output of compile and optimizes it to run faster with the tradeoff of longer compile time. This is useful for framework developers wanting to use haml in their framework and want to cache the compiled templates for performance. 88 | 89 | With the previous input it outputs: 90 | 91 | "
" + 92 | title + 93 | "\n
" 94 | 95 | Notice how congruent static strings are merged into a single string literal when possible. 96 | 97 | ### Haml.execute(js, context, locals) -> Executes a compiled template 98 | 99 | Context is the value of `this` in the template, and locals is a hash of local variables. 100 | 101 | ### Haml.render(text, options) -> html text 102 | 103 | This is a convenience function that compiles and executes to html in one shot. Most casual users will want to use this function exclusively. 104 | 105 | The `text` parameter is the haml source already read from a file. 106 | 107 | The three recognized `options` are: 108 | 109 | - **context**: This is the `this` context within the haml template. 110 | - **locals**: This is an object that's used in the `with` scope. Basically it creates local variables and function accessible to the haml template. 111 | - **optimize**: This is a flag to tell the compiler to use the extra optimizations. 112 | 113 | See [test.js][] for an example usage of Haml.render 114 | 115 | ## Executable JavaScript (not output) 116 | 117 | New in version 0.2.6 is the ability to embed javascript in your template function. This lets you do variable assignments, if/else, switch statements, and even define functions. In Haml.js, execution blocks begin with a `-` and define a raw js block. This behaves slightly differently from Ruby's Haml. The advantage is that you can easily have multi-line executable blocks and comments, but the downside is that that you have to "outdent" the haml if you want to output from within a javascript block. 118 | 119 | Simple example: 120 | 121 | - var area = 0.5 * length * height 122 | .area= area 123 | 124 | Multi-line example: 125 | 126 | - var obj = { 127 | area: 0.5 * b * h, 128 | r: opposite / adjacent 129 | } 130 | .triangle-details Area is: #{area} and the ratio is: #{r} 131 | 132 | "Outdent" the haml in a javascript block (the "goodbye" div is not rendered!) 133 | 134 | .conditional 135 | - var a = "strings are truthy" 136 | - if(a){ 137 | .hello 138 | - } else{ 139 | .goodbye 140 | - } 141 | 142 | You can even define functions: 143 | 144 | - function b(item){ 145 | .item 146 | %b= item 147 | %span.length= item.length 148 | - } 149 | - b("Hi") 150 | - b("World") 151 | 152 | This outputs: 153 | 154 |
Hi2
World5
155 | 156 | Please see test/raw_complex.haml for more details and examples. 157 | 158 | ## Comments 159 | 160 | Comments that will **not** appear in the compiled JS function nor the output begin with `-#` 161 | 162 | -# This is a comment 163 | - # This is a syntax error because of the extraneous space between the - and #. 164 | 165 | If you want to have comments that will be in the compiled JS function but *NOT* the final HTML output: 166 | 167 | - /* 168 | here we can have a comment that will not be output. Since executable-JS is block-level, 169 | we can have as much comment as we want, and it will not be output to html */ 170 | 171 | If you want an HTML comment that **WILL** be in the final HTML, begin with `/` 172 | 173 | ## Whitespace 174 | 175 | By default, Haml.js **has no whitespace between tags**. In this way, Haml.js is the opposite of Haml in Ruby. You can insert whitespace around or inside tags with `>` and `<`, respectively. 176 | 177 | Most commonly, you want to have an `a` or `span` with whitespace around it: 178 | 179 | Download the file 180 | %a(href="/home")> here 181 | now. 182 | 183 | Will produce: 184 | 185 | Download the file here now. 186 | 187 | You can also combine them if you want to have whitespace around and inside your tag. 188 | 189 | %span<> This will have space in and around it. 190 | %span>< This will, too. 191 | %span><= "also works with code".toUpperCase() 192 | 193 | Please see `test/whitespace.haml` for more examples. 194 | 195 | ## Code interpolation 196 | 197 | As of version 0.2.0 there is string interpolation throughout. This means that the body of regular text areas can have embedded code. This is true for attributes and the contents of plugins like javascript and markdown also. If you notice an area that doesn't support interpolation and it should then send me a note and I'll add it. 198 | 199 | For interpolation, you may use `#{}` for escaped interpolation or `!{}` for unsafe interpolation. 200 | 201 | ## Html Escaping / Sanitizer 202 | 203 | You probably don't want to put unescaped user input right into your html. http://xkcd.com/327/ HTML/XSS sanitization is the new "Bobby Tables." 204 | 205 | Let's assume we have a malicious username: `name = ""` 206 | 207 | Always unsafe: 208 | 209 | %span!= name 210 | 211 | 212 | 213 | Always safe: 214 | 215 | %span&= name 216 | <script>...</script> 217 | 218 | Sometimes safe: 219 | 220 | %span= name 221 | 222 | The behavior of `=` depends on the setting of the `escapeHtmlByDefault` configuration variable. To make `=` safe, call Haml like this: 223 | 224 | Haml(src, {escapeHtmlByDefault: true}) 225 | 226 | ## Plugins 227 | 228 | There are plugins in the parser for things like inline script tags, css blocks, and support for if statements and for loops. 229 | 230 | ### `:if/:else` statements 231 | 232 | `if` statements evaluate a condition for truthiness (as opposed to a strict comparison to `true`) and includes the content inside the block if it's truthy. An optional `else` is also supported. 233 | 234 | :if todolist.length > 20 235 | %p Oh my, you are a busy fellow! 236 | 237 | :if val == selectedVal 238 | %option{value: val, selected: true}= val 239 | :else 240 | %option{value: val}= val 241 | 242 | ### `:each` loops 243 | 244 | `:each` loops allow you to loop over a collection including a block of content once for each item. You need to what variable to pull the data from and where to put the index and value. The index variable is optional and defaults to `__key__`. 245 | 246 | Here is an example over a simple array. 247 | 248 | %ul.todolist 249 | :each item in todolist 250 | %li= item.description 251 | 252 | You can loop over the keys and values of objects too (Note the inner `:each` loop) 253 | 254 | :each item in data 255 | :if item.age < 100 256 | %dl 257 | :each name, value in item 258 | %dt&= name 259 | %dd&= value 260 | 261 | ### `:css` and `:script` helpers. 262 | 263 | It's easy to embed script and css tags in an haml document. Note that both `:script` and `:javascript` will work. 264 | 265 | %head 266 | :javascript 267 | function greet(message) { 268 | alert("Message from MCP: " + message); 269 | } 270 | %title Script and Css test 271 | :css 272 | body { 273 | color: pink; 274 | } 275 | %body{ onload: "greet(\"I'm Pink\")" } COLOR ME PINK 276 | 277 | This compiles to the following HTML: 278 | 279 | 280 | 287 | Script and Css test 288 | 289 | 294 | COLOR ME PINK 295 | 296 | 297 | 298 | ## Custom Escaper 299 | 300 | By default, Haml(src) returns a completely self-sufficient function, including a nested `html_escape` function. However, repeating the html_escape function definition in each of your templates is going to use more size than necessary. So, you may pass the name of a custom escaper in an optional config variable. 301 | 302 | Haml(src, {customEscape: "MyApp.esc"}) 303 | 304 | Then, the output template function definition will call `MyApp.esc(string)` and will omit the `html_escape` function definition. Haml.html_escape exposes the default escape function. If you are going to render your templates in the same context where you compile them (for instance, if you are only rendering them on the server side,) it might make sense to use `Haml(src, {customEscape: "Haml.html_escape"})` 305 | 306 | ## Get Involved 307 | 308 | If you want to use this project and something is missing then send me a message. I'm very busy and have several open source projects I manage. I'll contribute to this project as I have time, but if there is more interest for some particular aspect, I'll work on it a lot faster. Also you're welcome to fork this project and send me patches/pull-requests. 309 | 310 | ## About Performance 311 | 312 | The haml compiler isn't built for speed, it's built for maintainability. The actual generated templates, however are blazing fast. I benchmarked them with over 65 million renders per second on a small (20 line) template with some dynamic data on my laptop. Compare this to the 629 compiles per second I got out of the compiler. The idea is that you pre-compile your templates and reuse them on every request. While 629 per second is nothing compared to 65 million, that still means that your server with over 600 different views can boot up in about a second. I think that's fine for something that only happens every few weeks. 313 | 314 | ## License 315 | 316 | Haml-js is [licensed][] under the [MIT license][]. 317 | 318 | [MIT license]: http://creativecommons.org/licenses/MIT/ 319 | [licensed]: http://github.com/creationix/haml-js/blob/master/LICENSE 320 | [jquery-haml]: http://github.com/creationix/jquery-haml 321 | [haml]: http://haml.info/ 322 | [test.js]: http://github.com/creationix/haml-js/blob/master/test/test.js 323 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Haml = require('./haml'); 4 | 5 | var readUntilEnd = function(stream, callback) { 6 | var chunks = []; 7 | stream.on('data', function(chunk) { 8 | chunks.push(chunk.toString('utf-8')); 9 | }); 10 | stream.on('end', function() { 11 | callback(chunks.join('')); 12 | }); 13 | } 14 | 15 | readUntilEnd(process.openStdin(), function(haml) { 16 | var result; 17 | 18 | if (haml.length == 0) { 19 | console.log("Error: HAML expected on stdin") 20 | process.exit(1); 21 | } 22 | 23 | // --html 24 | if ((process.argv.length >= 3) && (process.argv[2] == '--html')) { 25 | result = Haml.render(haml); 26 | } 27 | 28 | // --js 29 | else { 30 | result = Haml.optimize( 31 | Haml.compile( 32 | haml)); 33 | } 34 | 35 | process.stdout.write(result); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/haml.js: -------------------------------------------------------------------------------- 1 | var Haml; 2 | 3 | (function () { 4 | 5 | var matchers, self_close_tags, embedder, forceXML, escaperName = 'html_escape', escapeHtmlByDefault; 6 | 7 | function html_escape(text) { 8 | return (text + ""). 9 | replace(/&/g, "&"). 10 | replace(//g, ">"). 12 | replace(/\"/g, """); 13 | } 14 | 15 | function render_attribs(attribs) { 16 | var key, value, result = []; 17 | for (key in attribs) { 18 | if (key[key.length - 1] == "?") { 19 | value = escaperName + '(' + attribs[key] + ')'; 20 | result.push('" + (' + attribs[key] + ' ? " ' + key.replace('?', '') + '=\\"" + ' + value + ' + "\\"" : "") + "'); 21 | continue; 22 | } 23 | if (key !== '_content' && attribs.hasOwnProperty(key)) { 24 | switch (attribs[key]) { 25 | case 'undefined': 26 | case 'false': 27 | case 'null': 28 | case '""': 29 | break; 30 | default: 31 | try { 32 | value = JSON.parse("[" + attribs[key] +"]")[0]; 33 | if (value === true) { 34 | value = key; 35 | } else if (typeof value === 'string' && embedder.test(value)) { 36 | value = '" +\n' + parse_interpol(html_escape(value)) + ' +\n"'; 37 | } else { 38 | value = html_escape(value); 39 | } 40 | result.push(" " + key + '=\\"' + value + '\\"'); 41 | } catch (e) { 42 | result.push(" " + key + '=\\"" + '+escaperName+'(' + attribs[key] + ') + "\\"'); 43 | } 44 | } 45 | } 46 | } 47 | return result.join(""); 48 | } 49 | 50 | // Parse the attribute block using a state machine 51 | function parse_attribs(line) { 52 | var attributes = {}, 53 | l = line.length, 54 | i, c, 55 | count = 1, 56 | quote = false, 57 | skip = false, 58 | open, close, joiner, seperator, 59 | pair = { 60 | start: 1, 61 | middle: null, 62 | end: null 63 | }; 64 | 65 | if (!(l > 0 && (line.charAt(0) === '{' || line.charAt(0) === '('))) { 66 | return { 67 | _content: line[0] === ' ' ? line.substr(1, l) : line 68 | }; 69 | } 70 | open = line.charAt(0); 71 | close = (open === '{') ? '}' : ')'; 72 | joiner = (open === '{') ? ':' : '='; 73 | seperator = (open === '{') ? ',' : ' '; 74 | 75 | function process_pair() { 76 | if (typeof pair.start === 'number' && 77 | typeof pair.middle === 'number' && 78 | typeof pair.end === 'number') { 79 | var key = line.substr(pair.start, pair.middle - pair.start).trim(), 80 | value = line.substr(pair.middle + 1, pair.end - pair.middle - 1).trim(); 81 | attributes[key] = value; 82 | } 83 | pair = { 84 | start: null, 85 | middle: null, 86 | end: null 87 | }; 88 | } 89 | 90 | for (i = 1; count > 0; i += 1) { 91 | 92 | // If we reach the end of the line, then there is a problem 93 | if (i > l) { 94 | throw "Malformed attribute block"; 95 | } 96 | 97 | c = line.charAt(i); 98 | if (skip) { 99 | skip = false; 100 | } else { 101 | if (quote) { 102 | if (c === '\\') { 103 | skip = true; 104 | } 105 | if (c === quote) { 106 | quote = false; 107 | } 108 | } else { 109 | if (c === '"' || c === "'") { 110 | quote = c; 111 | } 112 | 113 | if (count === 1) { 114 | if (c === joiner) { 115 | pair.middle = i; 116 | } 117 | if (c === seperator || c === close) { 118 | pair.end = i; 119 | process_pair(); 120 | if (c === seperator) { 121 | pair.start = i + 1; 122 | } 123 | } 124 | } 125 | 126 | if (c === open || c === "(") { 127 | count += 1; 128 | } 129 | if (c === close || (count > 1 && c === ")")) { 130 | count -= 1; 131 | } 132 | } 133 | } 134 | } 135 | attributes._content = line.substr(i, line.length); 136 | return attributes; 137 | } 138 | 139 | // Split interpolated strings into an array of literals and code fragments. 140 | function parse_interpol(value) { 141 | var items = [], 142 | pos = 0, 143 | next = 0, 144 | match; 145 | while (true) { 146 | // Match up to embedded string 147 | next = value.substr(pos).search(embedder); 148 | if (next < 0) { 149 | if (pos < value.length) { 150 | items.push(JSON.stringify(value.substr(pos))); 151 | } 152 | break; 153 | } 154 | items.push(JSON.stringify(value.substr(pos, next))); 155 | pos += next; 156 | 157 | // Match embedded string 158 | match = value.substr(pos).match(embedder); 159 | next = match[0].length; 160 | if (next < 0) { break; } 161 | if(match[1] === "#"){ 162 | items.push(escaperName+"("+(match[2] || match[3])+")"); 163 | }else{ 164 | //unsafe!!! 165 | items.push(match[2] || match[3]); 166 | } 167 | 168 | pos += next; 169 | } 170 | return items.filter(function (part) { return part && part.length > 0}).join(" +\n"); 171 | } 172 | 173 | // Used to find embedded code in interpolated strings. 174 | embedder = /([#!])\{([^}]*)\}/; 175 | 176 | self_close_tags = ["meta", "img", "link", "br", "hr", "input", "area", "base"]; 177 | 178 | // All matchers' regexps should capture leading whitespace in first capture 179 | // and trailing content in last capture 180 | matchers = [ 181 | // html tags 182 | { 183 | name: "html tags", 184 | regexp: /^(\s*)((?:[.#%][a-z_\-][a-z0-9_:\-]*)+)(.*)$/i, 185 | process: function () { 186 | var line_beginning, tag, classes, ids, attribs, content, whitespaceSpecifier, whitespace={}, output; 187 | line_beginning = this.matches[2]; 188 | classes = line_beginning.match(/\.([a-z_\-][a-z0-9_\-]*)/gi); 189 | ids = line_beginning.match(/\#([a-z_\-][a-z0-9_\-]*)/gi); 190 | tag = line_beginning.match(/\%([a-z_\-][a-z0-9_:\-]*)/gi); 191 | 192 | // Default to
tag 193 | tag = tag ? tag[0].substr(1, tag[0].length) : 'div'; 194 | 195 | attribs = this.matches[3]; 196 | if (attribs) { 197 | attribs = parse_attribs(attribs); 198 | if (attribs._content) { 199 | var leader0 = attribs._content.charAt(0), 200 | leader1 = attribs._content.charAt(1), 201 | leaderLength = 0; 202 | 203 | if(leader0 == "<"){ 204 | leaderLength++; 205 | whitespace.inside = true; 206 | if(leader1 == ">"){ 207 | leaderLength++; 208 | whitespace.around = true; 209 | } 210 | }else if(leader0 == ">"){ 211 | leaderLength++; 212 | whitespace.around = true; 213 | if(leader1 == "<"){ 214 | leaderLength++; 215 | whitespace.inside = true; 216 | } 217 | } 218 | attribs._content = attribs._content.substr(leaderLength); 219 | //once we've identified the tag and its attributes, the rest is content. 220 | // this is currently trimmed for neatness. 221 | this.contents.unshift(attribs._content.trim()); 222 | delete(attribs._content); 223 | } 224 | } else { 225 | attribs = {}; 226 | } 227 | 228 | if (classes) { 229 | classes = classes.map(function (klass) { 230 | return klass.substr(1, klass.length); 231 | }).join(' '); 232 | if (attribs['class']) { 233 | try { 234 | attribs['class'] = JSON.stringify(classes + " " + JSON.parse(attribs['class'])); 235 | } catch (e) { 236 | attribs['class'] = JSON.stringify(classes + " ") + " + " + attribs['class']; 237 | } 238 | } else { 239 | attribs['class'] = JSON.stringify(classes); 240 | } 241 | } 242 | if (ids) { 243 | ids = ids.map(function (id) { 244 | return id.substr(1, id.length); 245 | }).join(' '); 246 | if (attribs.id) { 247 | attribs.id = JSON.stringify(ids + " ") + attribs.id; 248 | } else { 249 | attribs.id = JSON.stringify(ids); 250 | } 251 | } 252 | 253 | attribs = render_attribs(attribs); 254 | 255 | content = this.render_contents(); 256 | if (content === '""') { 257 | content = ''; 258 | } 259 | 260 | if(whitespace.inside){ 261 | if(content.length==0){ 262 | content='" "' 263 | }else{ 264 | try{ //remove quotes if they are there 265 | content = '" '+JSON.parse(content)+' "'; 266 | }catch(e){ 267 | content = '" "+\n'+content+'+\n" "'; 268 | } 269 | } 270 | } 271 | 272 | if (forceXML ? content.length > 0 : self_close_tags.indexOf(tag) == -1) { 273 | output = '"<' + tag + attribs + '>"' + 274 | (content.length > 0 ? ' + \n' + content : "") + 275 | ' + \n""'; 276 | } else { 277 | output = '"<' + tag + attribs + ' />"'; 278 | } 279 | 280 | if(whitespace.around){ 281 | //output now contains '"hello"' 282 | //we need to crack it open to insert whitespace. 283 | output = '" '+output.substr(1, output.length - 2)+' "'; 284 | } 285 | 286 | return output; 287 | } 288 | }, 289 | 290 | // each loops 291 | { 292 | name: "each loop", 293 | regexp: /^(\s*)(?::for|:each)\s+(?:([a-z_][a-z_\-]*),\s*)?([a-z_][a-z_\-]*)\s+in\s+(.*)(\s*)$/i, 294 | process: function () { 295 | var ivar = this.matches[2] || '__key__', // index 296 | vvar = this.matches[3], // value 297 | avar = this.matches[4], // array 298 | rvar = '__result__'; // results 299 | 300 | if (this.matches[5]) { 301 | this.contents.unshift(this.matches[5]); 302 | } 303 | return '(function () { ' + 304 | 'var ' + rvar + ' = [], ' + ivar + ', ' + vvar + '; ' + 305 | 'for (' + ivar + ' in ' + avar + ') { ' + 306 | 'if (' + avar + '.hasOwnProperty(' + ivar + ')) { ' + 307 | vvar + ' = ' + avar + '[' + ivar + ']; ' + 308 | rvar + '.push(\n' + (this.render_contents() || "''") + '\n); ' + 309 | '} } return ' + rvar + '.join(""); }).call(this)'; 310 | } 311 | }, 312 | 313 | // if statements 314 | { 315 | name: "if", 316 | regexp: /^(\s*):if\s+(.*)\s*$/i, 317 | process: function () { 318 | var condition = this.matches[2]; 319 | this.pushIfCondition([condition]); 320 | return '(function () { ' + 321 | 'if (' + condition + ') { ' + 322 | 'return (\n' + (this.render_contents() || '') + '\n);' + 323 | '} else { return ""; } }).call(this)'; 324 | } 325 | }, 326 | 327 | // else if statements 328 | { 329 | name: "else if", 330 | regexp: /^(\s*):else if\s+(.*)\s*$/i, 331 | process: function () { 332 | var condition = this.matches[2], 333 | conditionsArray = this.getIfConditions()[this.getIfConditions().length - 1], 334 | ifArray = [], 335 | ifStatement; 336 | for (var i=0, l=conditionsArray.length; i"'; 385 | } 386 | }, 387 | 388 | // raw js 389 | { 390 | name: "rawjs", 391 | regexp: /^(\s*)-\s*(.*)\s*$/i, 392 | process: function () { 393 | this.contents.unshift(this.matches[2]); 394 | return '"";' + this.contents.join("\n")+"; _$output = _$output "; 395 | } 396 | }, 397 | 398 | // raw js 399 | { 400 | name: "pre", 401 | regexp: /^(\s*):pre(\s+(.*)|$)/i, 402 | process: function () { 403 | this.contents.unshift(this.matches[2]); 404 | return '"
"+\n' + JSON.stringify(this.contents.join("\n"))+'+\n"
"'; 405 | } 406 | }, 407 | 408 | // declarations 409 | { 410 | name: "doctype", 411 | regexp: /^()!!!(?:\s*(.*))\s*$/, 412 | process: function () { 413 | var line = ''; 414 | switch ((this.matches[2] || '').toLowerCase()) { 415 | case '': 416 | // XHTML 1.0 Transitional 417 | line = ''; 418 | break; 419 | case 'strict': 420 | case '1.0': 421 | // XHTML 1.0 Strict 422 | line = ''; 423 | break; 424 | case 'frameset': 425 | // XHTML 1.0 Frameset 426 | line = ''; 427 | break; 428 | case '5': 429 | // XHTML 5 430 | line = ''; 431 | break; 432 | case '1.1': 433 | // XHTML 1.1 434 | line = ''; 435 | break; 436 | case 'basic': 437 | // XHTML Basic 1.1 438 | line = ''; 439 | break; 440 | case 'mobile': 441 | // XHTML Mobile 1.2 442 | line = ''; 443 | break; 444 | case 'xml': 445 | // XML 446 | line = ""; 447 | break; 448 | case 'xml iso-8859-1': 449 | // XML iso-8859-1 450 | line = ""; 451 | break; 452 | } 453 | return JSON.stringify(line + "\n"); 454 | } 455 | }, 456 | 457 | // Embedded markdown. Needs to be added to exports externally. 458 | { 459 | name: "markdown", 460 | regexp: /^(\s*):markdown\s*$/i, 461 | process: function () { 462 | return parse_interpol(exports.Markdown.encode(this.contents.join("\n"))); 463 | } 464 | }, 465 | 466 | // script blocks 467 | { 468 | name: "script", 469 | regexp: /^(\s*):(?:java)?script\s*$/, 470 | process: function () { 471 | return parse_interpol('\n\n"); 475 | } 476 | }, 477 | 478 | // css blocks 479 | { 480 | name: "css", 481 | regexp: /^(\s*):css\s*$/, 482 | process: function () { 483 | return JSON.stringify('"); 486 | } 487 | } 488 | 489 | ]; 490 | 491 | function compile(lines) { 492 | var block = false, 493 | output = [], 494 | ifConditions = []; 495 | 496 | // If lines is a string, turn it into an array 497 | if (typeof lines === 'string') { 498 | lines = lines.trim().replace(/\n\r|\r/g, '\n').split('\n'); 499 | } 500 | 501 | lines.forEach(function(line) { 502 | var match, found = false; 503 | 504 | // Collect all text as raw until outdent 505 | if (block) { 506 | match = block.check_indent.exec(line); 507 | if (match) { 508 | block.contents.push(match[1] || ""); 509 | return; 510 | } else { 511 | output.push(block.process()); 512 | block = false; 513 | } 514 | } 515 | 516 | matchers.forEach(function (matcher) { 517 | if (!found) { 518 | match = matcher.regexp.exec(line); 519 | if (match) { 520 | block = { 521 | contents: [], 522 | indent_level: (match[1]), 523 | matches: match, 524 | check_indent: new RegExp("^(?:\\s*|" + match[1] + " (.*))$"), 525 | process: matcher.process, 526 | getIfConditions: function() { 527 | return ifConditions; 528 | }, 529 | pushIfCondition: function(condition) { 530 | ifConditions.push(condition); 531 | }, 532 | popIfCondition: function() { 533 | return ifConditions.pop(); 534 | }, 535 | render_contents: function () { 536 | return compile(this.contents); 537 | } 538 | }; 539 | found = true; 540 | } 541 | } 542 | }); 543 | 544 | // Match plain text 545 | if (!found) { 546 | output.push(function () { 547 | // Escaped plain text 548 | if (line[0] === '\\') { 549 | return parse_interpol(line.substr(1, line.length)); 550 | } 551 | 552 | 553 | function escapedLine(){ 554 | try { 555 | return escaperName+'('+JSON.stringify(JSON.parse(line)) +')'; 556 | } catch (e2) { 557 | return escaperName+'(' + line + ')'; 558 | } 559 | } 560 | 561 | function unescapedLine(){ 562 | try { 563 | return parse_interpol(JSON.parse(line)); 564 | } catch (e) { 565 | return line; 566 | } 567 | } 568 | 569 | // always escaped 570 | if((line.substr(0, 2) === "&=")) { 571 | line = line.substr(2, line.length).trim(); 572 | return escapedLine(); 573 | } 574 | 575 | //never escaped 576 | if((line.substr(0, 2) === "!=")) { 577 | line = line.substr(2, line.length).trim(); 578 | return unescapedLine(); 579 | } 580 | 581 | // sometimes escaped 582 | if ( (line[0] === '=')) { 583 | line = line.substr(1, line.length).trim(); 584 | if(escapeHtmlByDefault){ 585 | return escapedLine(); 586 | }else{ 587 | return unescapedLine(); 588 | } 589 | } 590 | 591 | // Plain text 592 | return parse_interpol(line); 593 | }()); 594 | } 595 | 596 | }); 597 | if (block) { 598 | output.push(block.process()); 599 | } 600 | 601 | var txt = output.filter(function (part) { return part && part.length > 0}).join(" +\n"); 602 | if(txt.length == 0){ 603 | txt = '""'; 604 | } 605 | return txt; 606 | }; 607 | 608 | function optimize(js) { 609 | var new_js = [], buffer = [], part, end; 610 | 611 | function flush() { 612 | if (buffer.length > 0) { 613 | new_js.push(JSON.stringify(buffer.join("")) + end); 614 | buffer = []; 615 | } 616 | } 617 | js.replace(/\n\r|\r/g, '\n').split('\n').forEach(function (line) { 618 | part = line.match(/^(\".*\")(\s*\+\s*)?$/); 619 | if (!part) { 620 | flush(); 621 | new_js.push(line); 622 | return; 623 | } 624 | end = part[2] || ""; 625 | part = part[1]; 626 | try { 627 | buffer.push(JSON.parse(part)); 628 | } catch (e) { 629 | flush(); 630 | new_js.push(line); 631 | } 632 | }); 633 | flush(); 634 | return new_js.join("\n"); 635 | }; 636 | 637 | function render(text, options) { 638 | options = options || {}; 639 | text = text || ""; 640 | var js = compile(text, options); 641 | if (options.optimize) { 642 | js = Haml.optimize(js); 643 | } 644 | return execute(js, options.context || Haml, options.locals); 645 | }; 646 | 647 | function execute(js, self, locals) { 648 | return (function () { 649 | with(locals || {}) { 650 | try { 651 | var _$output; 652 | eval("_$output =" + js ); 653 | return _$output; //set in eval 654 | } catch (e) { 655 | return "\n
" + html_escape(e + "\n" + e.stack) + "
\n"; 656 | } 657 | 658 | } 659 | }).call(self); 660 | }; 661 | 662 | Haml = function Haml(haml, config) { 663 | if(typeof(config) != "object"){ 664 | forceXML = config; 665 | config = {}; 666 | } 667 | 668 | var escaper; 669 | if(config.customEscape){ 670 | escaper = ""; 671 | escaperName = config.customEscape; 672 | }else{ 673 | escaper = html_escape.toString() + "\n"; 674 | escaperName = "html_escape"; 675 | } 676 | 677 | escapeHtmlByDefault = (config.escapeHtmlByDefault || config.escapeHTML || config.escape_html); 678 | 679 | var js = optimize(compile(haml)); 680 | 681 | var str = "with(locals || {}) {\n" + 682 | " try {\n" + 683 | " var _$output=" + js + ";\n return _$output;" + 684 | " } catch (e) {\n" + 685 | " return \"\\n
\" + "+escaperName+"(e + \"\\n\" + e.stack) + \"
\\n\";\n" + 686 | " }\n" + 687 | "}" 688 | 689 | try{ 690 | var f = new Function("locals", escaper + str ); 691 | return f; 692 | }catch(e){ 693 | if ( typeof(console) !== 'undefined' ) { console.error(str); } 694 | throw e; 695 | } 696 | } 697 | 698 | Haml.compile = compile; 699 | Haml.optimize = optimize; 700 | Haml.render = render; 701 | Haml.execute = execute; 702 | Haml.html_escape = html_escape; 703 | }()); 704 | 705 | // Hook into module system 706 | if (typeof module !== 'undefined') { 707 | module.exports = Haml; 708 | } 709 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haml", 3 | "description": "Haml ported to server-side Javascript. This is a traditional server-side templating language.", 4 | "keywords": ["haml", "template"], 5 | "homepage": "https://github.com/creationix/haml-js", 6 | "main" : "./lib/haml", 7 | "bin": { 8 | "haml-js": "./lib/cli.js" 9 | }, 10 | "author": "Aaron Blohowiak , Tim Caswell ", 11 | "version": "0.4.3" 12 | } 13 | -------------------------------------------------------------------------------- /test.haml: -------------------------------------------------------------------------------- 1 | .class1 2 | .class2#testid -------------------------------------------------------------------------------- /test/alt_attribs.haml: -------------------------------------------------------------------------------- 1 | %tag(name="value" name2=true ns:tag=100) 2 | %input#space-end(type="hidden" value="3" ) 3 | %input#space-start( type="hidden" value="3" ) 4 | %input#space-middle(type="hidden" value="3") 5 | -------------------------------------------------------------------------------- /test/alt_attribs.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/blank.haml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/haml-js/c7df550fe7947f64c23a97aff6fed2e8c4e600d3/test/blank.haml -------------------------------------------------------------------------------- /test/blank.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creationix/haml-js/c7df550fe7947f64c23a97aff6fed2e8c4e600d3/test/blank.html -------------------------------------------------------------------------------- /test/comments.haml: -------------------------------------------------------------------------------- 1 | -# should not be displayed 2 | .display-this 3 | -# will be hidden 4 | Will also be hidden 5 | So comments 6 | Are block-level 7 | - var a=2 8 | =a 9 | -# Ensure that executable JS blocks still work 10 | - /* this will be in the compiled function, 11 | but not in the final HTML 12 | */ 13 | / This should be a Haml comment 14 | and it should be in the resulting html (even with "double quotes") -------------------------------------------------------------------------------- /test/comments.html: -------------------------------------------------------------------------------- 1 |
2 -------------------------------------------------------------------------------- /test/css.haml: -------------------------------------------------------------------------------- 1 | :css 2 | #pants{ 3 | font-weight:"bold"; 4 | } 5 | 6 | a:link{ 7 | color: "red"; 8 | } 9 | 10 | a:visited{ 11 | color: "#ff00ff"; 12 | } 13 | 14 | .visited{ 15 | font-weight: bold; 16 | } -------------------------------------------------------------------------------- /test/css.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/div_nesting.haml: -------------------------------------------------------------------------------- 1 | %div 2 | Does not close properly 3 | %div Nested same level as next div 4 | %div 5 | Will be nested, but should be top level -------------------------------------------------------------------------------- /test/div_nesting.html: -------------------------------------------------------------------------------- 1 |
Does not close properly
Nested same level as next div
Will be nested, but should be top level
-------------------------------------------------------------------------------- /test/doctype.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | !!! strict 3 | !!! 1.1 4 | !!! 5 5 | !!! xml 6 | -------------------------------------------------------------------------------- /test/doctype.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/else.haml: -------------------------------------------------------------------------------- 1 | :if 1 + 1 == 3 2 | %p 1+1 = 3 3 | :else 4 | %p 1+1 != 3 5 | -------------------------------------------------------------------------------- /test/else.html: -------------------------------------------------------------------------------- 1 |

1+1 != 3

-------------------------------------------------------------------------------- /test/embedded_code.haml: -------------------------------------------------------------------------------- 1 | %head 2 | :javascript 3 | Page.chapter = !{JSON.stringify(chapter)}; 4 | %body 5 | %h1 Welcome #{name} 6 | %div{class: "div_#{id}"} 7 | -------------------------------------------------------------------------------- /test/embedded_code.html: -------------------------------------------------------------------------------- 1 | 2 | 7 |

Welcome Tim

-------------------------------------------------------------------------------- /test/embedded_code.js: -------------------------------------------------------------------------------- 1 | { 2 | locals: { 3 | chapter: {name: "Ninja", page: 42}, 4 | name: "Tim", 5 | id: 42 6 | } 7 | } -------------------------------------------------------------------------------- /test/escaping.haml: -------------------------------------------------------------------------------- 1 | %a{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol} 2 | %a{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol}<> 3 | %a{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol}&= "
" 4 | %p{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol}= "
" 5 | %p<> 6 | %p<> 7 | %p(attr0=attr[0] attr1=attr[1]) -------------------------------------------------------------------------------- /test/escaping.html: -------------------------------------------------------------------------------- 1 | <br>


-------------------------------------------------------------------------------- /test/escaping.js: -------------------------------------------------------------------------------- 1 | { 2 | locals: { 3 | apos: "'", 4 | amp: '&', 5 | carrots: '<>', 6 | quo: '"', 7 | sol: '/', 8 | attr:[ 9 | '"">', 10 | 'javascript:alert(String.fromCharCode(88,83,83)' 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /test/foreach.haml: -------------------------------------------------------------------------------- 1 | :each color in colors 2 | .preview{style: "color: " + color + ";"}&= name 3 | :each item in data 4 | :if item.age < 100 5 | %dl 6 | :each name, value in item 7 | %dt&= name 8 | %dd&= value 9 | :each number in [1,2,3,4,5,6,7] 10 | = number 11 | :for word in "Hello World".split(" ") 12 | = word -------------------------------------------------------------------------------- /test/foreach.html: -------------------------------------------------------------------------------- 1 |
My Rainbow
My Rainbow
My Rainbow
name
Tim Caswell
age
27
1234567HelloWorld -------------------------------------------------------------------------------- /test/foreach.js: -------------------------------------------------------------------------------- 1 | { 2 | locals: { 3 | colors: ["#f80", "#08f", "#4f4"], 4 | name: "My Rainbow", 5 | data: [ 6 | {name: "Tim Caswell", age: 27}, 7 | {name: "John Smith", age: 107}, 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /test/if.haml: -------------------------------------------------------------------------------- 1 | %ul 2 | :each number in numbers 3 | %li 4 | :if number.one 5 | %ul 6 | :each color in number.one 7 | %li 8 | :if color.a 9 | = color.a 10 | :else if color.b 11 | = color.b 12 | :else if color.c 13 | = color.c 14 | :else 15 | = color.d 16 | :else if number.two 17 | %ul 18 | :each color in number.two 19 | %li 20 | :if color.e 21 | = color.e 22 | :else if color.f 23 | = color.f 24 | :else if color.g 25 | = color.g 26 | :else 27 | = color.h 28 | :else 29 | %ul 30 | :each color in number.three 31 | %li 32 | :if color.i 33 | = color.i 34 | :else if color.j 35 | = color.j 36 | :else if color.k 37 | = color.k 38 | :else 39 | = color.l -------------------------------------------------------------------------------- /test/if.html: -------------------------------------------------------------------------------- 1 |
    • A
    • B
    • C
    • D
    • E
    • F
    • G
    • H
    • I
    • J
    • K
    • L
-------------------------------------------------------------------------------- /test/if.js: -------------------------------------------------------------------------------- 1 | { 2 | locals: { 3 | numbers: { 4 | first: { 5 | one: { 6 | red: { 7 | a: "A" 8 | }, 9 | orange: { 10 | b: "B" 11 | }, 12 | yellow: { 13 | c: "C" 14 | }, 15 | green: { 16 | d: "D" 17 | } 18 | } 19 | }, 20 | second: { 21 | two: { 22 | blue: { 23 | e: "E" 24 | }, 25 | indigo: { 26 | f: "F" 27 | }, 28 | violet: { 29 | g: "G" 30 | }, 31 | black: { 32 | h: "H" 33 | } 34 | } 35 | }, 36 | third: { 37 | three: { 38 | grey: { 39 | i: "I" 40 | }, 41 | white: { 42 | j: "J" 43 | }, 44 | pink: { 45 | k: "K" 46 | }, 47 | brown: { 48 | l: "L" 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /test/interpolation.haml: -------------------------------------------------------------------------------- 1 | - var amp = "&", 2 | quo = '"', 3 | carrots = "<>", 4 | tag="
"; 5 | 6 | %p(id="p-#{amp}") Well then #{carrots} 7 | %p(id="p-!{amp}") Well then !{carrots} 8 | 9 | This is some text #{amp} it is cool. !{tag} i am doing fun stuff! 10 | 11 | %p(src="!{quo}!{tag}!{quo}") 12 | 13 | -------------------------------------------------------------------------------- /test/interpolation.html: -------------------------------------------------------------------------------- 1 |

Well then <>

Well then <>

This is some text & it is cool.
i am doing fun stuff!

"">

-------------------------------------------------------------------------------- /test/meta.haml: -------------------------------------------------------------------------------- 1 | %meta(http-equiv="content-type" content="text/html; charset=UTF-8") -------------------------------------------------------------------------------- /test/meta.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/nanline.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html 3 | %head 4 | %title atomix 5 | 6 | %script(src='atomix_xlib.js') 7 | -------------------------------------------------------------------------------- /test/nanline.html: -------------------------------------------------------------------------------- 1 | 2 | atomix -------------------------------------------------------------------------------- /test/nested_context.haml: -------------------------------------------------------------------------------- 1 | %p= this.name 2 | #main 3 | :each item in items 4 | .item= this.name + ": " + item 5 | :if items 6 | #cool= this.name -------------------------------------------------------------------------------- /test/nested_context.html: -------------------------------------------------------------------------------- 1 |

Frank

Frank: 1
Frank: 2
Frank: 3
Frank
-------------------------------------------------------------------------------- /test/nested_context.js: -------------------------------------------------------------------------------- 1 | { 2 | context: { 3 | name: "Frank" 4 | }, 5 | locals: { 6 | items: [1,2,3] 7 | } 8 | } -------------------------------------------------------------------------------- /test/no_self_close_div.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %body 3 | %div#a 4 | %div I do not self close. 5 | :javascript 6 | (function(){ 7 | document.getElementById('a').textContent='I self close'; 8 | })(); 9 | -------------------------------------------------------------------------------- /test/no_self_close_div.html: -------------------------------------------------------------------------------- 1 |
I do not self close.
2 | 9 | -------------------------------------------------------------------------------- /test/non-string-attribs.haml: -------------------------------------------------------------------------------- 1 | #plain= "Plain Text" 2 | #escaped&= "" 3 | %input{checked: true} 4 | %input{checked: false} 5 | %input{checked: null} 6 | %input{checked: undefined} 7 | %input{checked: 0} 8 | %input{checked: ""} -------------------------------------------------------------------------------- /test/non-string-attribs.html: -------------------------------------------------------------------------------- 1 |
Plain Text
<escaped>
-------------------------------------------------------------------------------- /test/optional_attribs.haml: -------------------------------------------------------------------------------- 1 | %input{type: "checkbox", disabled?: "disabled", checked?: "checked", readonly?: "readonly"} 2 | %input{type: "checkbox", disabled?: (5 < 5), checked?: (5 == 5), readonly?: (5 == 5 ? "yes" : false)} 3 | %input{type: "checkbox", disabled?: (5 < 5), checked?: (5 > 5), readonly?: (5 != 5 ? "yes" : "")} -------------------------------------------------------------------------------- /test/optional_attribs.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/other/custom_escape.haml: -------------------------------------------------------------------------------- 1 | %a{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol} 2 | %a{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol}<> 3 | %a{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol}&= "
" 4 | %p{apos:apos, amp:amp, carrots:carrots, quo:quo, sol:sol}= "
" 5 | %p<> 6 | %p<> 7 | %p(attr0=attr[0] attr1=attr[1]) 8 | 9 | 10 | 11 | %h1(id="#{carrots} blah") How#{apos}s it going #{amp} how are you? 12 | 13 | -------------------------------------------------------------------------------- /test/other/custom_escape.html: -------------------------------------------------------------------------------- 1 | moo


Howmoos it going moo how are you?

-------------------------------------------------------------------------------- /test/other/escape_by_default.haml: -------------------------------------------------------------------------------- 1 | - tag = ""; 2 | 3 | .safe<>= tag 4 | .safe>&= tag 5 | .safe<&= tag 6 | .unsafe!= tag 8 | .unsafe!= tag -------------------------------------------------------------------------------- /test/other/escape_by_default.html: -------------------------------------------------------------------------------- 1 |
<script> bad things</script>
<script> bad things</script>
<script> bad things</script>
-------------------------------------------------------------------------------- /test/raw.haml: -------------------------------------------------------------------------------- 1 | .conditional 2 | - var a = "strings are truthy" 3 | - if(a){ 4 | .hello 5 | - } else{ 6 | .goodbye 7 | - } 8 | -------------------------------------------------------------------------------- /test/raw.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/raw_complex.haml: -------------------------------------------------------------------------------- 1 | - /* 2 | here we can have a comment that will not be output. 3 | Since executable-JS is a block-level thing, we can 4 | have as much comment as we want */ 5 | 6 | 7 | - /* now you can have arbitrary control logic 8 | This will output
1
2
3
9 | notice: the %div isn't indented! Explained below! 10 | */ 11 | 12 | - for(var i=1; i < 4; i++){ 13 | %div= i 14 | -} 15 | 16 | 17 | %h1 Woah! 18 | 19 | - /* we can include new variable declarations as well! */ 20 | var someObj = { 21 | a: 1, 22 | b: 2 23 | } 24 | = someObj.b 25 | 26 | 27 | %br 28 | 29 | 30 | - /* this is going to be funky. i DO NOT expect you to do this. 31 | Here we will begin with a comment, then define our variable. 32 | Next we begin a function definition. 33 | 34 | We will use this function to output a div that contains a number. 35 | Successive calls should output the next number in the div. 36 | 37 | We want our function to output the value of the counter, 38 | so we "outdent" to escape the JavaScript block in haml. 39 | Note that we have not closed our function definition's 40 | parenthesis yet. So, even though we outdent, the 41 | concatenation statement will be in the function's body. 42 | */ 43 | 44 | var counter = 0; 45 | function increment(){ 46 | counter++; 47 | .count 48 | = counter 49 | -} 50 | - increment() /* the tags wil be appended to the buffer, so use - instead of = */ 51 | - increment() 52 | 53 | 54 | 55 | 56 | - function b(item){ 57 | .item 58 | %b= item 59 | %span.length= item.length 60 | - } 61 | - b("Hi") 62 | - b("World") -------------------------------------------------------------------------------- /test/raw_complex.html: -------------------------------------------------------------------------------- 1 |
1
2
3

Woah!

2
1
2
Hi2
World5
-------------------------------------------------------------------------------- /test/script_css.haml: -------------------------------------------------------------------------------- 1 | %head 2 | :javascript 3 | function greet(message) { 4 | alert("Message from MCP: " + message); 5 | } 6 | %title Script and Css test 7 | :css 8 | body { 9 | color: pink; 10 | } 11 | %body{onload: "greet(\"I'm Pink\")"} COLOR ME PINK 12 | 13 | -------------------------------------------------------------------------------- /test/script_css.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | Script and Css testCOLOR ME PINK -------------------------------------------------------------------------------- /test/self_close.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | :if url !='/' 4 | %script 5 | %meta{name: "test", value:"Monkey"} 6 | %body 7 | %a{ href: url } 8 | link -------------------------------------------------------------------------------- /test/self_close.html: -------------------------------------------------------------------------------- 1 | link -------------------------------------------------------------------------------- /test/self_close.js: -------------------------------------------------------------------------------- 1 | { 2 | locals: { 3 | url: "http://nodejs.org/" 4 | } 5 | } -------------------------------------------------------------------------------- /test/standard.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | !!! strict 3 | %html{ xmlns: "http://www.w3.org/1999/xhtml" } 4 | %head 5 | %title 6 | Sample haml template 7 | %body 8 | .profile 9 | .left.column 10 | #date= print_date() 11 | #address= current_user.address 12 | .right.column 13 | #email= current_user.email 14 | #bio= current_user.bio -------------------------------------------------------------------------------- /test/standard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample haml template
January 1, 2009
Richardson, TX
tim@creationix.com
Experienced software professional...
-------------------------------------------------------------------------------- /test/standard.js: -------------------------------------------------------------------------------- 1 | { 2 | locals: { 3 | print_date: function () { 4 | return 'January 1, 2009'; 5 | }, 6 | current_user: { 7 | address: "Richardson, TX", 8 | email: "tim@creationix.com", 9 | bio: "Experienced software professional..." 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/test-commonjs.js: -------------------------------------------------------------------------------- 1 | var FILE = require("file"); 2 | var ASSERT = require("assert"); 3 | 4 | var Haml = require("../lib/haml"); 5 | 6 | FILE.glob("test/*.haml").forEach(function(hamlFile) { 7 | exports["test " + hamlFile] = function() { 8 | var scopeFile = hamlFile.replace(/haml$/, "js"); 9 | var htmlFile = hamlFile.replace(/haml$/, "html"); 10 | 11 | var haml = FILE.read(hamlFile); 12 | var expected = FILE.read(htmlFile); 13 | var scope = FILE.exists(scopeFile) ? eval("("+FILE.read(scopeFile)+")") : {}; 14 | 15 | var js = Haml.compile(haml); 16 | var js_opt = Haml.optimize(js); 17 | var actual = Haml.execute(js_opt, scope.context, scope.locals); 18 | ASSERT.equal(actual.trim(), expected.trim()); 19 | } 20 | }); 21 | 22 | if (module == require.main) 23 | require("os").exit(require("test").run(exports)); 24 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var assert = require('assert'); 3 | var sys = require('sys'); 4 | 5 | var Haml = require("../lib/haml"); 6 | 7 | 8 | 9 | function compare(haml_file, haml, expected, scope, options){ 10 | options || (options = {}); 11 | try { 12 | sys.puts(haml_file + " Begun") 13 | var js = Haml.compile(haml); 14 | var js_opt = Haml.optimize(js); 15 | var jsFn = Haml(haml, options); 16 | var actual = jsFn.call(scope.context, scope.locals); 17 | 18 | assert.equal(actual, expected); 19 | sys.puts(haml_file + " Passed") 20 | 21 | actual = Haml.render(haml, {context:scope.context, locals:scope.locals}) 22 | 23 | assert.equal(actual, expected); 24 | sys.puts(haml_file + " Haml.render Passed") 25 | 26 | } catch (e) { 27 | var message = e.name; 28 | if (e.message) { message += ": " + e.message; } 29 | sys.error(haml_file + " FAILED") 30 | sys.error(message); 31 | sys.error("\nJS:\n\n" + js); 32 | sys.error("\nOptimized JS:\n\n" + js_opt); 33 | sys.error("\nJS fn:\n\n"+jsFn.toString()); 34 | sys.error("\nStack:\n\n"+e.stack); 35 | try{ 36 | sys.error("\nActual["+actual.length+"]:\n\n" + actual); 37 | sys.error("\nExpected["+expected.length+"]:\n\n" + expected); 38 | }catch(e2){} 39 | 40 | process.exit(); 41 | } 42 | } 43 | 44 | 45 | fs.readdir('.', function (err, files) { 46 | files.forEach(function (haml_file) { 47 | var m = haml_file.match(/^(.*)\.haml/), 48 | base; 49 | if (!m) { 50 | return; 51 | } 52 | base = m[1]; 53 | 54 | function load_haml(scope) { 55 | fs.readFile(haml_file, "utf8", function (err, haml) { 56 | fs.readFile(base + ".html", "utf8", function (err, expected) { 57 | compare(haml_file, haml, expected, scope) 58 | }); 59 | }); 60 | } 61 | 62 | // Load scope 63 | if (files.indexOf(base + ".js") >= 0) { 64 | fs.readFile(base + ".js", "utf8", function (err, js) { 65 | load_haml(eval("(" + js + ")")); 66 | }); 67 | } else { 68 | load_haml({}); 69 | } 70 | }); 71 | }); 72 | 73 | (function(){ 74 | var hamlSrc = fs.readFileSync("alt_attribs.haml", "utf8"); 75 | var includeEscape = Haml(hamlSrc).toString(); 76 | var customEscape = Haml(hamlSrc, {customEscape:"$esc"}).toString(); 77 | try{ 78 | assert.ok(customEscape.length < includeEscape.length); 79 | }catch(e){ 80 | sys.error(e.stack); 81 | sys.error(customEscape); 82 | process.exit(); 83 | } 84 | })(); 85 | 86 | 87 | (function(){ 88 | var hamlSrc = fs.readFileSync("./other/custom_escape.haml", "utf8"); 89 | var expected = fs.readFileSync("./other/custom_escape.html", "utf8"); 90 | var scope = eval("(" + fs.readFileSync("escaping.js") + ")"); 91 | 92 | sys.puts("custom_escape" + " Begun") 93 | var jsFn = Haml(hamlSrc, {customEscape:"$esc"}); 94 | 95 | this.$esc = function(){ 96 | return "moo" 97 | }; 98 | 99 | var actual = jsFn.call(scope.context, scope.locals); 100 | try{ 101 | assert.equal(actual, expected); 102 | }catch(e){ 103 | sys.error("\nActual["+actual.length+"]:\n\n" + actual); 104 | sys.error("\nExpected["+expected.length+"]:\n\n" + expected); 105 | process.exit(); 106 | } 107 | sys.puts("custom_escape" + " Passed") 108 | 109 | })(); 110 | 111 | 112 | (function(){ 113 | var hamlSrc = fs.readFileSync("./other/escape_by_default.haml", "utf8"); 114 | var expected = fs.readFileSync("./other/escape_by_default.html", "utf8"); 115 | var scope = {}; 116 | 117 | sys.puts("escape_by_default" + " Begun") 118 | var js = Haml.compile(hamlSrc); 119 | 120 | var jsFn = Haml(hamlSrc, {escapeHtmlByDefault:true}); 121 | 122 | this.$esc = function(){ 123 | return "moo" 124 | }; 125 | 126 | var actual = jsFn.call(scope.context, scope.locals); 127 | try{ 128 | assert.equal(actual, expected); 129 | }catch(e){ 130 | sys.error("\nActual["+actual.length+"]:\n\n" + actual); 131 | sys.error("\nExpected["+expected.length+"]:\n\n" + expected); 132 | process.exit(); 133 | } 134 | sys.puts("escape_by_default" + " Passed") 135 | 136 | })(); 137 | 138 | -------------------------------------------------------------------------------- /test/whitespace.haml: -------------------------------------------------------------------------------- 1 | %p I want my words 2 | %a> to have spaces on the outside 3 | So they don't run together. 4 | But i also want some to have 5 | %a< spaces on the inside 6 | and still others to have 7 | %a<> spaces on either side 8 | even if it has 9 | %a<>= "code" 10 | on the line 11 | %a>= "or" 12 | just code with space on the outside 13 | 14 | 15 | %p 16 | %a link 17 | s that touch their neighbor. 18 | 19 | 20 | %p 21 | And 22 | %a>links 23 | that do not 24 | 25 | %p 26 | Or a 27 | %b<> important thing 28 | with tons of space 29 | 30 | %p 31 | Download the file 32 | %a(href="/home")> here 33 | now. 34 | 35 | -# empty tag 36 | %p<> 37 | 38 | - id="123456"; locals={name:"hi"} 39 | 40 | .item.pending{data-id: id} 41 | %input.checkbox(type="checkbox" id="todo-"+id) 42 | %label.description(name=("todo-"+id))= (locals.name || "") 43 | %span<>= id 44 | %a.delete.live{href:"/todo/"+id+"/delete"}> delete -------------------------------------------------------------------------------- /test/whitespace.html: -------------------------------------------------------------------------------- 1 |

I want my words to have spaces on the outside So they don't run together. But i also want some to have spaces on the inside and still others to have spaces on either side even if it has code on the line or just code with space on the outside

links that touch their neighbor.

And links that do not

Or a important thing with tons of space

Download the file here now.

123456 delete
--------------------------------------------------------------------------------