├── .gitignore ├── LICENSE ├── README.md ├── file-loader.js ├── index.js ├── lib ├── attributeParser.js ├── macroParser.js └── macros.js ├── package.json ├── runtime └── index.js └── test ├── attributeTest.js ├── lib ├── WebpackLoaderMock.js ├── loadOutput.js ├── loadTemplate.js └── removeFirstline.js ├── loaderTest.js ├── macroTest.js └── templates ├── absolute-image.html ├── compilation-options.html ├── custom-attributes.html ├── custom-macro.html ├── dynamic-attribute-with-parseDynamicRoutes.html ├── dynamic-attribute-with-root.html ├── dynamic-attribute.html ├── image.html ├── include.html ├── macro.html ├── macro_argument_list.html ├── macro_boolean_args.html ├── macro_escaped.html ├── macro_numeric_args.html ├── macro_repeat.html ├── macro_string_args.html ├── output ├── absolute-image-with-root.txt ├── absolute-image.txt ├── compilation-options.txt ├── custom-attributes.txt ├── custom-macro.txt ├── disabled-macro.txt ├── dynamic-attribute-with-parseDynamicRoutes.txt ├── dynamic-attribute-with-root.txt ├── dynamic-attribute.txt ├── image.txt ├── include.txt ├── macro_argument_list.txt ├── macro_boolean_args.txt ├── macro_escaped.txt ├── macro_numeric_args.txt ├── macro_repeat.txt ├── macro_string_args.txt ├── require.txt ├── simple-with-comment.txt └── simple.txt ├── require.html └── simple.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 - 2017 Emmanuel Antico 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | handlebars-template-loader 2 | ========================== 3 | 4 | A Handlebars template loader for Webpack 5 | 6 | ### Changelog 7 | 8 |
9 | * 1.0: Loader now works with Webpack 4. Still a beta release. 10 | 11 |
12 | 13 | 14 | **Table of Contents** 15 | 16 | - [Installation](#installation) 17 | - [Usage](#usage) 18 | - [Loading templates](#loading-templates) 19 | - [Using helpers](#using-helpers) 20 | - [Using partials](#using-partials) 21 | - [Options](#options) 22 | - [Prepending filename comment](#prepending-filename-comment) 23 | - [Images](#images) 24 | - [Runtime path](#runtime-path) 25 | - [Compilation options](#compilation-options) 26 | - [Macros](#macros) 27 | - [require](#require) 28 | - [include](#include) 29 | - [repeat](#repeat) 30 | - [Custom macros](#custom-macros) 31 | - [Disabling macros](#disabling-macros) 32 | - [Arguments](#arguments) 33 | - [Escaping](#escaping) 34 | - [License](#license) 35 | 36 | 37 | 38 |
39 | 40 | # Installation # 41 | 42 |
43 | 44 | ```bash 45 | npm install handlebars-template-loader 46 | ``` 47 | 48 |
49 | 50 | >Since version 0.5.4, this loaders does not include Handlebars in its dependency list. Make sure to install Handlebars before running webpack. Read https://github.com/npm/npm/issues/6565 for details. 51 | 52 |
53 | 54 | # Usage # 55 | 56 |
57 | 58 | ```javascript 59 | module.exports = { 60 | //... 61 | 62 | module: { 63 | loaders: [ 64 | { test: /\.hbs/, loader: "handlebars-template-loader" } 65 | ] 66 | }, 67 | 68 | node: { 69 | fs: "empty" // avoids error messages 70 | } 71 | }; 72 | ``` 73 | 74 |
75 | 76 | # Loading templates # 77 | 78 |
79 | 80 | ```html 81 | 82 |

Hello {{name}}

83 | ``` 84 | 85 |
86 | 87 | ```javascript 88 | // File: app.js 89 | var compiled = require('./hello.hbs'); 90 | return compiled({name: "world"}); 91 | ``` 92 | 93 |
94 | 95 | # Using helpers # 96 | 97 |
98 | 99 | ```javascript 100 | // File: helpers.js 101 | 102 | // Get Handlebars instance 103 | var Handlebars = require('handlebars-template-loader/runtime'); 104 | 105 | Handlebars.registerHelper('list', function(items, options) { 106 | var out = ""; 113 | }); 114 | 115 | Handlebars.registerHelper('link', function(text, url) { 116 | text = Handlebars.Utils.escapeExpression(text); 117 | url = Handlebars.Utils.escapeExpression(url); 118 | 119 | var result = '' + text + ''; 120 | 121 | return new Handlebars.SafeString(result); 122 | }); 123 | ``` 124 | 125 |
126 | 127 | ```javascript 128 | // File: main.js 129 | 130 | // Include template helpers 131 | require("./helpers.js"); 132 | ``` 133 | 134 |
135 | 136 | # Using partials # 137 | 138 |
139 | 140 | ```javascript 141 | // Get Handlebars instance 142 | var Handlebars = require('handlebars-template-loader/runtime'); 143 | 144 | // Require partial 145 | var partial = require('path/to/my/_partial.hbs'); 146 | 147 | // Register partial 148 | Handlebars.registerPartial('my_partial_name', partial); 149 | 150 | ``` 151 | 152 |
153 | 154 | # Options # 155 | 156 |
157 | 158 | ## Prepending filename comment ## 159 | 160 |
161 | 162 | When debugging a large single page app with the DevTools, it's often hard to find the template that contains a bug. With the following config a HTML comment is prepended to the template with the relative path in it (e.g. ``). 163 | 164 |
165 | 166 | ```javascript 167 | module.exports = { 168 | //... 169 | 170 | module: { 171 | loaders: [ 172 | { 173 | test: /\.hbs$/, 174 | loader: "handlebars-template-loader", 175 | query: { 176 | prependFilenameComment: __dirname, 177 | } 178 | } 179 | ] 180 | } 181 | }; 182 | ``` 183 | 184 |
185 | 186 | ## Images ## 187 | 188 |
189 | 190 | In order to load images you must install either the `file-loader` or the `url-loader` package. 191 | 192 | ```javascript 193 | module.exports = { 194 | //... 195 | 196 | module: { 197 | loaders: [ 198 | //... 199 | { test: /\.hbs/, loader: "handlebars-template-loader" }, 200 | { test: /\.jpg/, loader: "file-loader" }, 201 | { test: /\.png/, loader: "url-loader?mimetype=image/png" }, 202 | ] 203 | } 204 | }; 205 | ``` 206 | 207 |
208 | 209 | ```html 210 | 211 | 212 | 213 | 214 | 215 | ``` 216 | 217 |
218 | 219 | Images with an absolute path are not translated unless a `root` option is defined 220 | 221 | ```html 222 | 223 | 224 | 225 | 226 | 227 | ``` 228 | 229 |
230 | 231 | In order to deactivate image processing define the `attributes` option as an empty array. 232 | 233 | ```javascript 234 | module.exports = { 235 | //... 236 | 237 | module: { 238 | loaders: [ 239 | { 240 | test: /\.hbs$/, 241 | loader: "handlebars-template-loader", 242 | query: { 243 | attributes: [] 244 | } 245 | } 246 | ] 247 | } 248 | }; 249 | ``` 250 | 251 |
252 | 253 | You could also add which attributes need to be processed in the form of pairs *tag:attribute*. 254 | 255 |
256 | 257 | ```javascript 258 | module.exports = { 259 | //... 260 | 261 | module: { 262 | loaders: [ 263 | { 264 | test: /\.hbs$/, 265 | loader: "handlebars-template-loader", 266 | query: { 267 | attributes: ['img:src', 'x-img:src'] 268 | } 269 | } 270 | ] 271 | } 272 | }; 273 | ``` 274 | 275 | Dynamic attributes won't be afected by this behaviour by default. 276 | 277 | ```html 278 | 279 | 280 | ``` 281 | 282 | In order to append the root directory you'll need to specify the `parseDynamicRoutes` argument. 283 | 284 | ```javascript 285 | module.exports = { 286 | //... 287 | 288 | module: { 289 | loaders: [ 290 | { 291 | test: /\.html$/, 292 | loader: "handlebars-template-loader", 293 | query: { 294 | root: "myapp", 295 | parseDynamicRoutes: true 296 | } 297 | } 298 | ] 299 | } 300 | }; 301 | ``` 302 | 303 | ```html 304 | 305 | 306 | ``` 307 | 308 |
309 | 310 | ## Runtime path ## 311 | 312 | If you have a custom location for your Handlebars runtime module then you can set that in your `query` object via the `runtimePath` property. This is the path to the Handlebars runtime that every `.hbs` file will require and use. By default this loader looks up the absolute path to the `handlebars/runtime` in your `node_modules` folder. Changing this property is useful if you are doing somethign non-standard with your Handlebar templates, for example setting an alias for the `handlebars/runtime` path. 313 | 314 |
315 | 316 | ```javascript 317 | module.exports = { 318 | //... 319 | 320 | module: { 321 | loaders: [ 322 | { 323 | test: /\.html$/, 324 | loader: "handlebars-template-loader", 325 | query: { 326 | runtimePath: 'handlebars/runtime' 327 | } 328 | } 329 | ] 330 | } 331 | }; 332 | ``` 333 | 334 |
335 | 336 | ## Compilation options ## 337 | 338 |
339 | 340 | Handlebars does support [additional compilation options](http://handlebarsjs.com/reference.html) that you can specify in your `query` object literal. 341 | 342 |
343 | 344 | ```javascript 345 | module.exports = { 346 | //... 347 | 348 | module: { 349 | loaders: [ 350 | { 351 | test: /\.html$/, 352 | loader: "handlebars-template-loader", 353 | query: { 354 | root: "myapp", 355 | strict: true, 356 | noEscape: true 357 | } 358 | } 359 | ] 360 | } 361 | }; 362 | ``` 363 | 364 |
365 | 366 | # Macros # 367 | 368 |
369 | Macros allow additional features like including templates or inserting custom text in a compiled templates. 370 | 371 |
372 | 373 | ## require ## 374 | 375 |
376 | 377 | The `require` macro expects a path to a handlebars template. The macro is then translated to a webpack require expression that evaluates the template using the same arguments. 378 | 379 |
380 | 381 | ```html 382 |

Profile

383 | 384 | Name: {{name}} 385 |
386 | Surname: {{surname}} 387 |
388 | @require('profile-details.hbs') 389 |
390 | ``` 391 | 392 |
393 | 394 | ## include ## 395 | 396 |
397 | 398 | While the `require` macro expects a resource that returns a function, the `include` macro can be used for resources that return plain text. For example, we can include text loaded through the `html-loader` directly in our template. 399 | 400 | ```html 401 |
402 |

Introduction

403 | @include('intro.htm') 404 |

Authors

405 | @include('authors.htm') 406 |
407 | ``` 408 | 409 |
410 | 411 | ## repeat ## 412 | 413 |
414 | 415 | The `repeat` macro will repeat the given string the amount of times as specified by the second argument (default to 1). It will only accept string literals. 416 | 417 | ```html 418 |

Lorem ipsum

419 | @repeat('
', 3) 420 |

Sit amet

421 | @repeat('\n') 422 | ``` 423 | 424 |
425 | 426 | ## Custom macros ## 427 | 428 |
429 | 430 | We can include additional macros by defining them in the webpack configuration file. Remember that the value returned by a macro is inserted as plain javascript, so in order to insert a custom text we need to use nested quotes. For example, let's say that we want a macro that includes a copyright string in our template. 431 | 432 |
433 | 434 | ```javascript 435 | // File: webpack.config.js 436 | module.exports = { 437 | // ... 438 | 439 | module: { 440 | loaders: { 441 | // ... 442 | { test: /\.hbs/, loader: "handlebars-template-loader" }, 443 | } 444 | }, 445 | 446 | macros: { 447 | copyright: function () { 448 | return "'

Copyright FakeCorp 2014 - 2015

'"; 449 | } 450 | } 451 | } 452 | ``` 453 | 454 |
455 | 456 | We then invoke our macro from within the template as usual. 457 | 458 |
459 | 460 | ```html 461 | 464 | ``` 465 | 466 |
467 | 468 | ## Disabling macros ## 469 | 470 |
471 | 472 | You can disable macros if you are a bit unsure about their usage or just simply want faster processing. This is achieved by setting the `parseMacros` options to false. 473 | 474 |
475 | 476 | ```javascript 477 | module.exports = { 478 | // ... 479 | 480 | module: { 481 | loaders: { 482 | // ... 483 | { 484 | test: /\.hbs/, 485 | loader: "handlebars-template-loader", 486 | query: { 487 | parseMacros: false 488 | } 489 | }, 490 | } 491 | } 492 | } 493 | ``` 494 | 495 |
496 | 497 | ## Arguments ## 498 | 499 |
500 | 501 | Macros can accept an arbitrary number of arguments. Only boolean, strings and numeric types are supported. 502 | 503 |
504 | 505 | ```javascript 506 | // File: webpack.config.js 507 | module.exports = { 508 | // ... 509 | 510 | module: { 511 | loaders: { 512 | // ... 513 | { test: /\.html$/, loader: "handlebars-template-loader" }, 514 | } 515 | }, 516 | 517 | macros: { 518 | header: function (size, content) { 519 | return "'" + content + "'"; 520 | } 521 | } 522 | } 523 | ``` 524 | 525 |
526 | 527 | ```html 528 | @header(1, 'Welcome') 529 |

Lorem ipsum

530 | @header(3, 'Contents') 531 |

Sit amet

532 | ``` 533 | 534 |
535 | 536 | ## Escaping ## 537 | 538 |
539 | 540 | Macro expressions can be escaped with the `\` character. 541 | 542 | ```html 543 | @repeat('
', 3) 544 | \@escaped() 545 | @custom_macro() 546 | ``` 547 | 548 | Translates to 549 | 550 | ```html 551 |


552 | @escaped() 553 | custom string 554 | ``` 555 | 556 |
557 | 558 | # License # 559 | 560 | Released under the MIT license. 561 | -------------------------------------------------------------------------------- /file-loader.js: -------------------------------------------------------------------------------- 1 | var loaderUtils = require('loader-utils'); 2 | 3 | module.exports = function (source) { 4 | if (this.cacheable) { 5 | this.cacheable(); 6 | } 7 | var query = this.query instanceof Object ? this.query : loaderUtils.parseQuery(this.query); 8 | 9 | var allLoadersButThisOne = this.loaders.filter(function(loader) { 10 | return loader.module !== module.exports; 11 | }); 12 | 13 | // This loader shouldn't kick in if there is any other loader 14 | if (allLoadersButThisOne.length > 0) { 15 | return source; 16 | } 17 | 18 | return 'module.exports = ' + JSON.stringify(query.url); 19 | }; 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require('handlebars'); 2 | var HbsRuntime = require('./runtime'); 3 | var loaderUtils = require('loader-utils'); 4 | var path = require('path'); 5 | 6 | // Parsers 7 | var attributeParser = require('./lib/attributeParser'); 8 | var macroParser = require('./lib/macroParser'); 9 | 10 | // Helpers 11 | var _extend = function(obj, from) { 12 | for (var key in from) { 13 | if (!from.hasOwnProperty(key)) continue; 14 | obj[key] = from[key]; 15 | } 16 | return obj; 17 | }; 18 | 19 | // Extendable arguments 20 | var macros = _extend({}, require('./lib/macros')); 21 | 22 | module.exports = function(content) { 23 | if (this.cacheable) this.cacheable(); 24 | var callback = this.async(); 25 | 26 | // Default arguments 27 | var root, 28 | parseMacros = true, 29 | attributes = ['img:src'], 30 | parseDynamicRoutes = false, 31 | runtimePath = require.resolve('handlebars/runtime').replace(/\\/g, '/'); 32 | 33 | // Parse arguments 34 | var query = this.query instanceof Object ? this.query : loaderUtils.parseQuery(this.query); 35 | 36 | if (typeof(query) === 'object') { 37 | if (query.attributes !== undefined) { 38 | attributes = Array.isArray(query.attributes) ? query.attributes : []; 39 | } 40 | 41 | root = query.root; 42 | 43 | if (query.parseMacros !== undefined) { 44 | parseMacros = !!query.parseMacros; 45 | } 46 | 47 | // Prepend a html comment with the filename in it 48 | if (query.prependFilenameComment) { 49 | var filenameRelative = path.relative(query.prependFilenameComment, this.resource); 50 | content = '\n\n' + content; 51 | } 52 | 53 | // Check if dynamic routes must be parsed 54 | if (query.parseDynamicRoutes !== undefined) { 55 | parseDynamicRoutes = !!query.parseDynamicRoutes; 56 | } 57 | 58 | if (query.runtimePath) { 59 | runtimePath = query.runtimePath; 60 | } 61 | } 62 | 63 | // Include additional macros 64 | if (this.options && this.options.macros instanceof Object) { 65 | _extend(macros, this.options.macros); 66 | } 67 | 68 | // Parser contexts 69 | var macrosContext, attributesContext; 70 | 71 | // Parse macros 72 | if (parseMacros) { 73 | macrosContext = macroParser(content, function (macro) { 74 | return macros[macro] !== undefined && typeof(macros[macro]) === 'function'; 75 | }, 'MACRO'); 76 | content = macrosContext.replaceMatches(content); 77 | } 78 | 79 | // Parse attributes 80 | attributesContext = attributeParser(content, function (tag, attr) { 81 | return attributes.indexOf(tag + ':' + attr) !== -1; 82 | }, 'ATTRIBUTE', root, parseDynamicRoutes); 83 | content = attributesContext.replaceMatches(content); 84 | 85 | // Compile template 86 | var source = Handlebars.precompile(content, query); 87 | 88 | // Resolve macros 89 | if (parseMacros) { 90 | source = macrosContext.resolveMacros(source, macros); 91 | } 92 | 93 | // Resolve attributes 94 | source = attributesContext.resolveAttributes(source); 95 | 96 | callback(null, 'var Handlebars = require(\'' + runtimePath + '\');\n' + 97 | 'module.exports = (Handlebars[\'default\'] || Handlebars).template(' + source + ');'); 98 | }; 99 | 100 | module.exports.Handlebars = HbsRuntime; 101 | -------------------------------------------------------------------------------- /lib/attributeParser.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var url = require('url'); 3 | var Parser = require("fastparse"); 4 | var loaderUtils = require('loader-utils'); 5 | 6 | /** 7 | * HELPERS 8 | */ 9 | 10 | // Reminder: path.isAbsolute is not available in Node 0.10.x 11 | var pathIsAbsolute = function (attrValue) { 12 | return path.resolve(attrValue) == path.normalize(attrValue); 13 | }; 14 | 15 | // Detects is a given resource string includes a custom loader 16 | var hasCustomLoader = function (resourcePath) { 17 | var resource = resourcePath.split('!\.'); 18 | if (resource.length > 1) 19 | return resource; 20 | return false; 21 | }; 22 | 23 | // Checks whether a string contains a template expression 24 | var isTemplate = function (expression) { 25 | return /\{\{.*\}\}/.test(expression); 26 | }; 27 | 28 | /** 29 | * ATTRIBUTECONTEXT CLASS 30 | */ 31 | var AttributeContext = function(isRelevantTagAttr, usid, root, parseDynamicRoutes) { 32 | this.matches = []; 33 | this.isRelevantTagAttr = isRelevantTagAttr; 34 | this.usid = usid; 35 | this.data = {}; 36 | this.root = root; 37 | this.parseDynamicRoutes = parseDynamicRoutes; 38 | 39 | // The ident method builds a unique string expression 40 | // that replaces an attribute value. 41 | // This value is replaced again for the expected expression 42 | // once the template has been transformed to a function 43 | this.ident = function() { 44 | return "____" + usid + Math.random() + "____"; 45 | }; 46 | }; 47 | 48 | // The replaceMatches method does an initial parse 49 | // that replaces attribute values with a unique expression 50 | AttributeContext.prototype.replaceMatches = function(content) { 51 | var self = this; 52 | content = [content]; 53 | this.matches.reverse(); 54 | 55 | this.matches.forEach(function(match) { 56 | // Determine if the attribute contains a template expresssion 57 | if (match.containsTemplate) { 58 | // Check if path should be modified to include the "root" option 59 | if (pathIsAbsolute(match.value) && self.root !== undefined) { 60 | var x = content.pop(); 61 | content.push(x.substr(match.start + match.length)); 62 | content.push(self.parseDynamicRoutes ? loaderUtils.urlToRequest(match.value, self.root) : match.value); 63 | content.push(x.substr(0, match.start)); 64 | } 65 | } else { 66 | // Ignore if path is absolute and no root path has been defined 67 | if (pathIsAbsolute(match.value) && self.root === undefined) { 68 | return; 69 | } 70 | 71 | // Ignore if is a URL 72 | if (!loaderUtils.isUrlRequest(match.value, self.root)) { 73 | return; 74 | } 75 | 76 | var uri = url.parse(match.value); 77 | if (uri.hash !== null && uri.hash !== undefined) { 78 | uri.hash = null; 79 | match.value = uri.format(); 80 | match.length = match.value.length; 81 | } 82 | 83 | do { 84 | var ident = self.ident(); 85 | } while (self.data[ident]); 86 | 87 | self.data[ident] = match; 88 | 89 | var x = content.pop(); 90 | content.push(x.substr(match.start + match.length)); 91 | content.push(ident); 92 | content.push(x.substr(0, match.start)); 93 | } 94 | }); 95 | 96 | content.reverse(); 97 | return content.join(''); 98 | }; 99 | 100 | // Replaces the expressions inserted by replaceMatches with the corresponding requires 101 | AttributeContext.prototype.resolveAttributes = function(content) { 102 | var regex = new RegExp('____' + this.usid + '[0-9\\.]+____', 'g'); 103 | var self = this; 104 | return content.replace(regex, function(match) { 105 | if (!self.data[match]) { 106 | return match; 107 | } 108 | 109 | var url = self.data[match].value; 110 | // Make resource available through file-loader 111 | var fallbackLoader = require.resolve('../file-loader.js') + '?url=' + encodeURIComponent(url); 112 | return "\" + require(" + JSON.stringify(fallbackLoader + '!' + loaderUtils.urlToRequest(url, self.root)) + ") + \""; 113 | }); 114 | }; 115 | 116 | /** 117 | * PARSER 118 | */ 119 | 120 | // Process a tag attribute 121 | var processMatch = function(match, strUntilValue, name, value, index) { 122 | var self = this; 123 | var containsTemplate = false; 124 | 125 | // Check if attribute is included in the "attributes" option 126 | if (!this.isRelevantTagAttr(this.currentTag, name)) { 127 | return; 128 | } 129 | 130 | this.matches.push({ 131 | start: index + strUntilValue.length, 132 | length: value.length, 133 | value: value, 134 | containsTemplate: isTemplate(value) 135 | }); 136 | }; 137 | 138 | // Parser configuration 139 | var specs = { 140 | outside: { 141 | "": true, 142 | "": true, 143 | "<[!\\?].*?>": true, 144 | "<\/[^>]+>": true, 145 | "<([a-zA-Z\\-:]+)\\s*": function(match, tagName) { 146 | this.currentTag = tagName; 147 | return 'inside'; 148 | } 149 | }, 150 | 151 | inside: { 152 | "\\s+": true, // Eat up whitespace 153 | ">": 'outside', // End of attributes 154 | "(([a-zA-Z\\-]+)\\s*=\\s*\")([^\"]*)\"": processMatch, 155 | "(([a-zA-Z\\-]+)\\s*=\\s*\')([^\']*)\'": processMatch, 156 | "(([a-zA-Z\\-]+)\\s*=\\s*)([^\\s>]+)": processMatch 157 | } 158 | }; 159 | 160 | var parser = new Parser(specs); 161 | 162 | module.exports = function parse(html, isRelevantTagAttr, usid, root) { 163 | var context = new AttributeContext(isRelevantTagAttr, usid, root); 164 | return parser.parse('outside', html, context); 165 | }; 166 | -------------------------------------------------------------------------------- /lib/macroParser.js: -------------------------------------------------------------------------------- 1 | var Parser = require("fastparse"); 2 | 3 | /** 4 | * MACRO CLASS 5 | */ 6 | var Macro = function(name, index, length) { 7 | this.name = name; 8 | this.start = index; 9 | this.length = length; 10 | this.args = []; 11 | }; 12 | 13 | Macro.prototype.getArguments = function() { 14 | var args = []; 15 | 16 | this.args.forEach(function(arg) { 17 | args.push(arg.value); 18 | }); 19 | 20 | return args; 21 | }; 22 | 23 | /** 24 | * MACROCONTEXT CLASS 25 | */ 26 | var MacroContext = function(isMacroAvailable, usid) { 27 | this.currentDirective = null; 28 | this.matches = []; 29 | this.isMacroAvailable = isMacroAvailable; 30 | this.usid = usid; 31 | this.data = {}; 32 | 33 | // The ident method builds a unique string expression that replaces an macro. 34 | // This value is replaced again for the expected expression once the template 35 | // has been transformed to a function 36 | this.ident = function() { 37 | return "____" + usid + Math.random() + "____"; 38 | }; 39 | }; 40 | 41 | MacroContext.prototype.replaceMatches = function(content) { 42 | var self = this; 43 | content = [content]; 44 | this.matches.reverse(); 45 | 46 | this.matches.forEach(function(match) { 47 | do { 48 | var ident = self.ident(); 49 | } while (self.data[ident]); 50 | 51 | self.data[ident] = match; 52 | 53 | var x = content.pop(); 54 | content.push(x.substr(match.start + match.length)); 55 | content.push(ident); 56 | content.push(x.substr(0, match.start)); 57 | }); 58 | 59 | content.reverse(); 60 | return content.join(''); 61 | }; 62 | 63 | MacroContext.prototype.resolveMacros = function(content, macros) { 64 | var regex = new RegExp('____' + this.usid + '[0-9\\.]+____', 'g'); 65 | var self = this; 66 | 67 | // Replace macro expressions 68 | content = content.code || content; 69 | content = content.replace(regex, function(match) { 70 | if (!self.data[match]) { 71 | return match; 72 | } 73 | 74 | // TODO: invoke macros with configuration context 75 | var macro = self.data[match]; 76 | return '" + ' + macros[macro.name].apply(null, macro.getArguments()) + ' + "'; 77 | }); 78 | 79 | // Replace escaped macros 80 | content = content.replace(/\\+(@[\w]+)/, function(match, expr) { 81 | return expr; 82 | }); 83 | 84 | return content; 85 | }; 86 | 87 | // Parses a macro string argument 88 | var processStringArg = function(match, value, index, length) { 89 | if (!this.currentMacro) return; 90 | this.currentMacro.args.push({ 91 | start: index + value.length, 92 | index: index, 93 | length: length, 94 | value: value 95 | }); 96 | }; 97 | 98 | // Parses a macro numeric argument 99 | var processNumArg = function(match, value, index, length) { 100 | if (!this.currentMacro) return; 101 | this.currentMacro.args.push({ 102 | start: index + value.length, 103 | index: index, 104 | length: length, 105 | value: parseFloat(value) 106 | }); 107 | }; 108 | 109 | // Parses a macro boolean argument 110 | var processBooleanArg = function(match, value, index, length) { 111 | if (!this.currentMacro) return; 112 | this.currentMacro.args.push({ 113 | start: index + value.length, 114 | index: index, 115 | length: length, 116 | value: value === 'true' 117 | }); 118 | }; 119 | 120 | // Parser configuration 121 | var specs = { 122 | outside: { 123 | "^@(\\w+)\\(|([^\\\\])@(\\w+)\\(": function(match, name, prefix, _name, index, length) { 124 | name = name || _name; 125 | 126 | if (!this.isMacroAvailable(name)) { 127 | this.currentMacro = null; 128 | return 'inside'; 129 | } 130 | 131 | var macro = new Macro(name, prefix ? index + 1 : index, length); 132 | this.matches.push(macro); 133 | this.currentMacro = macro; 134 | return 'inside'; 135 | } 136 | }, 137 | 138 | inside: { 139 | "\\)": function(match, index) { 140 | if (this.currentMacro !== null) { 141 | this.currentMacro.length = 1 + index - this.currentMacro.start; 142 | } 143 | return 'outside'; 144 | }, 145 | "\'([^\']*)\'": processStringArg, 146 | "\"([^\"]*)\"": processStringArg, 147 | "\\s*([\\d|\\.]+)\\s*": processNumArg, 148 | "\\s*(true|false)\\s*": processBooleanArg, 149 | "\\s+": true 150 | } 151 | }; 152 | 153 | var parser = new Parser(specs); 154 | 155 | module.exports = function parse(html, isMacroAvailable, usid) { 156 | var context = new MacroContext(isMacroAvailable, usid); 157 | return parser.parse('outside', html, context); 158 | }; 159 | -------------------------------------------------------------------------------- /lib/macros.js: -------------------------------------------------------------------------------- 1 | var loaderUtils = require('loader-utils'); 2 | 3 | var strRepeat = function(str, times) { 4 | var result = ''; 5 | 6 | for (var i = 0; i < times; i++) { 7 | result += str; 8 | } 9 | 10 | return result; 11 | }; 12 | 13 | // Default macros 14 | module.exports = { 15 | // Includes a child template using the same context 16 | require: function(resourcePath) { 17 | // Since Handlebars v4, an extra argument called "container" is passed to the helper wrapper. 18 | // In order to keep compatibility, we remove the first argument from the list if we detect that more than 6 arguments are available. 19 | // See issue #9 for details. 20 | return "require(" + JSON.stringify(loaderUtils.urlToRequest(resourcePath)) + ").apply(null, Array.prototype.slice.call(arguments, arguments.length > 6))"; 21 | }, 22 | 23 | // Includes the contents of a given resource 24 | include: function(resourcePath) { 25 | return "require(" + JSON.stringify(loaderUtils.urlToRequest(resourcePath)) + ")"; 26 | }, 27 | 28 | repeat: function(str, times) { 29 | var text = strRepeat(str || '', typeof(times) == 'undefined' ? 1 : parseInt(times)); 30 | return "'" + text + "'"; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handlebars-template-loader", 3 | "version": "1.0.0", 4 | "description": "A Handlebars template loader for Webpack", 5 | "main": "index.js", 6 | "homepage": "https://github.com/emaphp/handlebars-template-loader", 7 | "license": "MIT", 8 | "bugs": { 9 | "url": "https://github.com/emaphp/handlebars-template-loader/issues" 10 | }, 11 | "author": { 12 | "name": "Emmanuel Antico", 13 | "email": "emmanuel.antico@gmail.com" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Jérôme Steunou (JSteunou)" 18 | }, 19 | { 20 | "name": "Patrick Browne (ptbrowne)" 21 | }, 22 | { 23 | "name": "Ivan (Ivanca)" 24 | }, 25 | { 26 | "name": "Matt Thompson (whatknight)" 27 | }, 28 | { 29 | "name": "Harry Wolff (hswolff)" 30 | } 31 | ], 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/emaphp/handlebars-template-loader.git" 35 | }, 36 | "dependencies": { 37 | "fastparse": "^1.1.1", 38 | "loader-utils": "^0.2.15" 39 | }, 40 | "keywords": [ 41 | "handlebars", 42 | "template", 43 | "webpack", 44 | "loader" 45 | ], 46 | "readmeFilename": "README.md", 47 | "devDependencies": { 48 | "chai": "^3.5.0", 49 | "chai-string": "^1.2.0", 50 | "istanbul": "^0.4.4", 51 | "mocha": "^2.5.3" 52 | }, 53 | "scripts": { 54 | "test": "mocha", 55 | "cover": "istanbul cover node_modules/mocha/bin/_mocha" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /runtime/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('handlebars/runtime'); -------------------------------------------------------------------------------- /test/attributeTest.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var chai = require('chai'); 3 | var assert = chai.assert; 4 | chai.use(require('chai-string')); 5 | 6 | var attributeParser = require('../lib/attributeParser'); 7 | var attributes = ['img:src', 'link:href']; 8 | 9 | // Helper to test the attributeParser. 10 | function testMatch(name, html, result) { 11 | it('should parse ' + name, function() { 12 | var parsed = attributeParser(html, function(tag, attr) { 13 | return attributes.indexOf(tag + ':' + attr) != -1; 14 | }, 'ATTRIBUTE', '/asdf/'); 15 | 16 | // We are only interested in the `value` property of `matches`. 17 | var values = parsed.matches.map(function(match) { return match.value; }); 18 | assert.deepEqual(values, result); 19 | }); 20 | } 21 | 22 | // Helper to test the replaceMatches function. 23 | function replaceMatch(html) { 24 | return attributeParser(html, function(tag, attr) { 25 | return attributes.indexOf(tag + ':' + attr) != -1; 26 | }, 'ATTRIBUTE').replaceMatches(html); 27 | } 28 | 29 | // Testcases based on https://github.com/webpack/html-loader/blob/master/test/parserTest.js 30 | describe('attribute parser', function() { 31 | testMatch('normal', 'Text ', ['image.png', 'image2.png']); 32 | testMatch('single-quotes', "Text ", ['image.png', 'image2.png']); 33 | testMatch('whitespace', 'T ex t ', ['image.png', 'image2.png']); 34 | testMatch('whitespace2', 'Text < img src="image.png" >', []); 35 | testMatch('wrong <', 'Text <', ['image.png']); 36 | testMatch("wrong >", 'Text >', ['image.png']); 37 | testMatch('no quot', '', ['image.png']); 38 | testMatch('first tag', '', ['image.png']); 39 | testMatch('comment', '', []); 40 | testMatch('comment2', '', []); 41 | testMatch('comment3', '-->', []); 42 | testMatch('comment4', '-->', ['image.png']); 43 | testMatch('tags', '', ['image.png', 'style.css']); 44 | testMatch('cdata', ']]>', ['image2.png']); 45 | testMatch('doctype', '', ['image.png']); 46 | 47 | it('should replace image paths', function() { 48 | var html = ''; 49 | var result = replaceMatch(html); 50 | assert.startsWith(result, ''; 55 | var result = replaceMatch(html); 56 | assert.endsWith(result, '____#asdf">'); 57 | }); 58 | 59 | it('should not replace urls', function() { 60 | var html = ''; 61 | var result = replaceMatch(html); 62 | assert.equal(result, html); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/lib/WebpackLoaderMock.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | function WebpackLoaderMock (options) { 5 | this.context = options.context || ''; 6 | this.query = options.query; 7 | this.options = options.options || {}; 8 | this.resource = options.resource; 9 | this._asyncCallback = options.async; 10 | this._resolveStubs = options.resolveStubs || {}; 11 | } 12 | 13 | WebpackLoaderMock.prototype.async = function () { 14 | return this._asyncCallback; 15 | }; 16 | 17 | module.exports = WebpackLoaderMock; 18 | -------------------------------------------------------------------------------- /test/lib/loadOutput.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | function loadOutput(outputPath) { 5 | return fs.readFileSync(path.join(path.dirname(__dirname), 'templates/output', outputPath)) 6 | .toString() 7 | .replace(/%%LOADER%%/g, require.resolve('../../file-loader.js')); 8 | } 9 | 10 | module.exports = loadOutput; 11 | -------------------------------------------------------------------------------- /test/lib/loadTemplate.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | function loadTemplate(templatePath) { 5 | return fs.readFileSync(path.join(path.dirname(__dirname), 'templates', templatePath)).toString(); 6 | } 7 | 8 | module.exports = loadTemplate; 9 | -------------------------------------------------------------------------------- /test/lib/removeFirstline.js: -------------------------------------------------------------------------------- 1 | function removeFirstline(str) { 2 | return str.substr(str.indexOf("\n") + 1); 3 | } 4 | 5 | module.exports = removeFirstline; 6 | -------------------------------------------------------------------------------- /test/loaderTest.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var chai = require('chai'); 4 | var assert = chai.assert; 5 | chai.use(require('chai-string')); 6 | 7 | var loader = require('../'); 8 | var WebpackLoaderMock = require('./lib/WebpackLoaderMock'); 9 | var loadTemplate = require('./lib/loadTemplate'); 10 | var loadOutput = require('./lib/loadOutput'); 11 | var removeFirstline = require('./lib/removeFirstline'); 12 | 13 | function testTemplate(loader, template, options, testFn) { 14 | loader.call(new WebpackLoaderMock({ 15 | query: options.query, 16 | resource: path.join(__dirname, 'templates', template), 17 | options: options.options, 18 | async: function(err, source) { 19 | testFn(source); 20 | } 21 | }), loadTemplate(template)); 22 | } 23 | 24 | describe('loader', function() { 25 | it('should load simple handlebars template', function(done) { 26 | testTemplate(loader, 'simple.html', {}, function(output) { 27 | // Copy and paste the result of `console.log(output)` to templates/output/simple.txt 28 | assert.equal(removeFirstline(output), loadOutput('simple.txt').trimRight()); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('should prepend html comment', function(done) { 34 | testTemplate(loader, 'simple.html', { 35 | query: { 36 | prependFilenameComment: __dirname 37 | } 38 | }, function(output) { 39 | assert.equal(removeFirstline(output), loadOutput('simple-with-comment.txt').trimRight()); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should be possible to require a template', function(done) { 45 | testTemplate(loader, 'require.html', {}, function(output) { 46 | assert.equal(removeFirstline(output), loadOutput('require.txt').trimRight()); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should be possible to include a template', function(done) { 52 | testTemplate(loader, 'include.html', {}, function(output) { 53 | assert.equal(removeFirstline(output), loadOutput('include.txt').trimRight()); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should require an image', function(done) { 59 | testTemplate(loader, 'image.html', {}, function(output) { 60 | assert.equal(removeFirstline(output), loadOutput('image.txt').trimRight()); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('should require given custom attributes', function(done) { 66 | testTemplate(loader, 'custom-attributes.html', { 67 | query: { 68 | attributes: ['img:src', 'link:href'] 69 | } 70 | }, function(output) { 71 | assert.equal(removeFirstline(output), loadOutput('custom-attributes.txt').trimRight()); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should not parse an absolute image without root option given', function(done) { 77 | testTemplate(loader, 'absolute-image.html', {}, function(output) { 78 | assert.equal(removeFirstline(output), loadOutput('absolute-image.txt').trimRight()); 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should parse an absolute image if root option is given', function(done) { 84 | testTemplate(loader, 'absolute-image.html', { 85 | query: { 86 | root: '/bar' 87 | } 88 | }, function(output) { 89 | assert.equal(removeFirstline(output), loadOutput('absolute-image-with-root.txt').trimRight()); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('should leave dynamic attribute unaltered', function(done) { 95 | testTemplate(loader, 'dynamic-attribute.html', { 96 | query: {} 97 | }, function(output) { 98 | assert.equal(removeFirstline(output), loadOutput('dynamic-attribute.txt').trimRight()); 99 | done(); 100 | }); 101 | }); 102 | 103 | it('should ignore root option if parseDynamicRoutes is not specified', function(done) { 104 | testTemplate(loader, 'dynamic-attribute-with-root.html', { 105 | query: { 106 | root: '/bar' 107 | } 108 | }, function(output) { 109 | assert.equal(removeFirstline(output), loadOutput('dynamic-attribute-with-root.txt').trimRight()); 110 | done(); 111 | }); 112 | }); 113 | 114 | it('should modify dynamic routes', function(done) { 115 | testTemplate(loader, 'dynamic-attribute-with-parseDynamicRoutes.html', { 116 | query: { 117 | root: '/bar', 118 | parseDynamicRoutes: true 119 | } 120 | }, function(output) { 121 | assert.equal(removeFirstline(output), loadOutput('dynamic-attribute-with-parseDynamicRoutes.txt').trimRight()); 122 | done(); 123 | }); 124 | }); 125 | 126 | it('should support compilation options', function(done) { 127 | // srcName makes the loader return a {code, map} object literal 128 | testTemplate(loader, 'compilation-options.html', { 129 | query: { 130 | srcName: 'foo.js' 131 | } 132 | }, function(output) { 133 | assert.equal(removeFirstline(output), loadOutput('compilation-options.txt').trimRight()); 134 | done(); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/macroTest.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var chai = require('chai'); 4 | var assert = chai.assert; 5 | chai.use(require('chai-string')); 6 | 7 | var loader = require('../'); 8 | var WebpackLoaderMock = require('./lib/WebpackLoaderMock'); 9 | var loadTemplate = require('./lib/loadTemplate'); 10 | var loadOutput = require('./lib/loadOutput'); 11 | var removeFirstline = require('./lib/removeFirstline'); 12 | 13 | function testTemplate(loader, template, options, testFn) { 14 | loader.call(new WebpackLoaderMock({ 15 | query: options.query, 16 | resource: path.join(__dirname, 'templates', template), 17 | options: options.options, 18 | async: function(err, source) { 19 | testFn(source); 20 | } 21 | }), loadTemplate(template)); 22 | } 23 | 24 | describe('macro', function() { 25 | it('should be parsed', function(done) { 26 | testTemplate(loader, 'custom-macro.html', { 27 | options: { 28 | macros: { 29 | foo: function() { 30 | return '"

bar

"'; 31 | } 32 | } 33 | } 34 | }, function(output) { 35 | assert.equal(removeFirstline(output), loadOutput('custom-macro.txt').trimRight()); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should receive boolean arguments', function(done) { 41 | testTemplate(loader, 'macro_boolean_args.html', { 42 | options: { 43 | macros: { 44 | bool_test: function(arg) { 45 | assert.typeOf(arg, 'boolean'); 46 | return arg ? '"

TRUE

"' : '"

FALSE

"'; 47 | } 48 | } 49 | } 50 | }, function(output) { 51 | assert.equal(removeFirstline(output), loadOutput('macro_boolean_args.txt').trimRight()); 52 | done(); 53 | }); 54 | }); 55 | 56 | it('should receive numeric arguments', function(done) { 57 | testTemplate(loader, 'macro_numeric_args.html', { 58 | options: { 59 | macros: { 60 | num_test: function(arg) { 61 | assert.typeOf(arg, 'number'); 62 | return '"

' + arg + '

"'; 63 | } 64 | } 65 | } 66 | }, function(output) { 67 | assert.equal(removeFirstline(output), loadOutput('macro_numeric_args.txt').trimRight()); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should receive string arguments', function(done) { 73 | testTemplate(loader, 'macro_string_args.html', { 74 | options: { 75 | macros: { 76 | str_test: function(arg) { 77 | assert.typeOf(arg, 'string'); 78 | return '"

' + arg.toUpperCase() + '

"'; 79 | } 80 | } 81 | } 82 | }, function(output) { 83 | assert.equal(removeFirstline(output), loadOutput('macro_string_args.txt').trimRight()); 84 | done(); 85 | }); 86 | }); 87 | 88 | it('should receive argument list', function(done) { 89 | testTemplate(loader, 'macro_argument_list.html', { 90 | options: { 91 | macros: { 92 | numbers: function(first, second, third) { 93 | assert.typeOf(first, 'number'); 94 | assert.typeOf(second, 'number'); 95 | assert.typeOf(third, 'number'); 96 | var output = ''; 97 | for (var i = 0; i < 3; i++) { 98 | output += '

' + arguments[i] + '

'; 99 | } 100 | return '"' + output + '"'; 101 | }, 102 | 103 | booleans: function(first, second, third) { 104 | assert.typeOf(first, 'boolean'); 105 | assert.typeOf(second, 'boolean'); 106 | assert.typeOf(third, 'boolean'); 107 | var output = ''; 108 | for (var i = 0; i < 3; i++) { 109 | output += '

' + (arguments[i] ? 'TRUE' : 'FALSE') + '

'; 110 | } 111 | return '"' + output + '"'; 112 | }, 113 | 114 | strings: function(first, second, third) { 115 | assert.typeOf(first, 'string'); 116 | assert.typeOf(second, 'string'); 117 | assert.typeOf(third, 'string'); 118 | var output = ''; 119 | for (var i = 0; i < 4; i++) { 120 | output += '

' + arguments[i].toLowerCase().replace(/"/g, "\\\"") + '

'; 121 | } 122 | return '"' + output + '"'; 123 | }, 124 | 125 | mixed: function() { 126 | assert.equal(arguments.length, 6); 127 | assert.typeOf(arguments[0], 'boolean'); 128 | assert.typeOf(arguments[1], 'number'); 129 | assert.typeOf(arguments[2], 'string'); 130 | assert.typeOf(arguments[3], 'boolean'); 131 | assert.typeOf(arguments[4], 'string'); 132 | assert.typeOf(arguments[5], 'number'); 133 | 134 | var output = ''; 135 | 136 | for (var i = 0; i < 6; i++) { 137 | var type = typeof(arguments[i]); 138 | 139 | if (type == 'string') { 140 | output += '

' + arguments[i].toLowerCase().replace(/"/g, "\\\"") + '

'; 141 | } else if (type == 'number') { 142 | output += '

' + arguments[i] + '

'; 143 | } else if (type == 'boolean') { 144 | output += '

' + (arguments[i] ? 'TRUE' : 'FALSE') + '

'; 145 | } 146 | } 147 | 148 | return '"' + output + '"'; 149 | } 150 | } 151 | } 152 | }, function(output) { 153 | assert.equal(removeFirstline(output), loadOutput('macro_argument_list.txt').trimRight()); 154 | done(); 155 | }); 156 | }); 157 | 158 | it('should not be evaluated', function(done) { 159 | testTemplate(loader, 'macro.html', { 160 | query: { 161 | parseMacros: false 162 | } 163 | }, function(output) { 164 | assert.equal(removeFirstline(output), loadOutput('disabled-macro.txt').trimRight()); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('should be replaced when escaped', function(done) { 170 | testTemplate(loader, 'macro_escaped.html', { 171 | options: { 172 | macros: { 173 | unescaped: function() { 174 | return '"

Ok

"'; 175 | } 176 | } 177 | } 178 | }, function(output) { 179 | assert.equal(removeFirstline(output), loadOutput('macro_escaped.txt').trimRight()); 180 | done(); 181 | }); 182 | }); 183 | 184 | it('should replace repeat string macros', function(done) { 185 | testTemplate(loader, 'macro_repeat.html', { 186 | options: {} 187 | }, function(output) { 188 | assert.equal(removeFirstline(output), loadOutput('macro_repeat.txt').trimRight()); 189 | done(); 190 | }); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /test/templates/absolute-image.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/compilation-options.html: -------------------------------------------------------------------------------- 1 |

{name}@repeat('test')

2 | -------------------------------------------------------------------------------- /test/templates/custom-attributes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/templates/custom-macro.html: -------------------------------------------------------------------------------- 1 | What, @foo(3) 2 | -------------------------------------------------------------------------------- /test/templates/dynamic-attribute-with-parseDynamicRoutes.html: -------------------------------------------------------------------------------- 1 | {{imgDescription}} 2 | -------------------------------------------------------------------------------- /test/templates/dynamic-attribute-with-root.html: -------------------------------------------------------------------------------- 1 | {{imgDescription}} 2 | -------------------------------------------------------------------------------- /test/templates/dynamic-attribute.html: -------------------------------------------------------------------------------- 1 | {{imgDescription}} 2 | -------------------------------------------------------------------------------- /test/templates/image.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/include.html: -------------------------------------------------------------------------------- 1 | Hello, @include('./bar.html') 2 | -------------------------------------------------------------------------------- /test/templates/macro.html: -------------------------------------------------------------------------------- 1 | Hi. 2 | @repeat('
', 3) 3 | @repeat("\n", 2) 4 | -------------------------------------------------------------------------------- /test/templates/macro_argument_list.html: -------------------------------------------------------------------------------- 1 | @numbers(1 3.534 000000000000000005) 2 | @booleans(true false true) 3 | @strings('Lorem' "Ipsum" '"Sit"' "'Amet'") 4 | @mixed(false 423.429 "Hello world" true 'Lorem' 00000000003) -------------------------------------------------------------------------------- /test/templates/macro_boolean_args.html: -------------------------------------------------------------------------------- 1 | @bool_test(true) 2 |
3 | @bool_test(false) -------------------------------------------------------------------------------- /test/templates/macro_escaped.html: -------------------------------------------------------------------------------- 1 | \@escaped(1 2 3) 2 | @unescaped() 3 | \@unescaped() -------------------------------------------------------------------------------- /test/templates/macro_numeric_args.html: -------------------------------------------------------------------------------- 1 | @num_test(5) 2 |
3 | @num_test(6.71) 4 |
5 | @num_test(00000000000000002) -------------------------------------------------------------------------------- /test/templates/macro_repeat.html: -------------------------------------------------------------------------------- 1 |

Hello

2 | @repeat('
', 3) 3 | @repeat('test') 4 |

Hola

5 | @repeat('\n', 2) 6 | -------------------------------------------------------------------------------- /test/templates/macro_string_args.html: -------------------------------------------------------------------------------- 1 | @str_test('Lorem ipsum') 2 |
3 | @str_test("Sit amet") -------------------------------------------------------------------------------- /test/templates/output/absolute-image-with-root.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/absolute-image.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/compilation-options.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "

{name}" + 'test' + "

\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/custom-attributes.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "\n\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/custom-macro.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "What, " + "

bar

" + "\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/disabled-macro.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "Hi.\n@repeat('
', 3)\n@repeat(\"\\n\", 2)\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/dynamic-attribute-with-parseDynamicRoutes.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; 3 | 4 | return "\""\n"; 11 | },"useData":true}); 12 | -------------------------------------------------------------------------------- /test/templates/output/dynamic-attribute-with-root.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; 3 | 4 | return "\""\n"; 11 | },"useData":true}); 12 | -------------------------------------------------------------------------------- /test/templates/output/dynamic-attribute.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; 3 | 4 | return "\""\n"; 11 | },"useData":true}); 12 | -------------------------------------------------------------------------------- /test/templates/output/image.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/include.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "Hello, " + require("./bar.html") + "\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/macro_argument_list.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "" + "

1

3.534

5

" + "\n" + "

TRUE

FALSE

TRUE

" + "\n" + "

lorem

ipsum

\"sit\"

'amet'

" + "\n" + "

FALSE

423.429

hello world

TRUE

lorem

3

" + ""; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/macro_boolean_args.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "" + "

TRUE

" + "\n
\n" + "

FALSE

" + ""; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/macro_escaped.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "@escaped(1 2 3)\n" + "

Ok

" + "\n\\@unescaped()"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/macro_numeric_args.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "" + "

5

" + "\n
\n" + "

6.71

" + "\n
\n" + "

2

" + ""; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/macro_repeat.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "

Hello

\n" + '


' + "\n" + 'test' + "\n

Hola

\n" + '\n\n' + "\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/macro_string_args.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "" + "

LOREM IPSUM

" + "\n
\n" + "

SIT AMET

" + ""; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/require.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 2 | return "Hello, " + require("./bar.html").apply(null, Array.prototype.slice.call(arguments, arguments.length > 6)) + "\n"; 3 | },"useData":true}); 4 | -------------------------------------------------------------------------------- /test/templates/output/simple-with-comment.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"1":function(container,depth0,helpers,partials,data) { 2 | var helper; 3 | 4 | return "

" 5 | + container.escapeExpression(((helper = (helper = helpers.description || (depth0 != null ? depth0.description : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"description","hash":{},"data":data}) : helper))) 6 | + "

\n"; 7 | },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 8 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}); 9 | 10 | return "\n\n

" 11 | + container.escapeExpression(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) 12 | + "

\n

A simple template

\n\n" 13 | + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.description : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); 14 | },"useData":true}); 15 | -------------------------------------------------------------------------------- /test/templates/output/simple.txt: -------------------------------------------------------------------------------- 1 | module.exports = (Handlebars['default'] || Handlebars).template({"1":function(container,depth0,helpers,partials,data) { 2 | var helper; 3 | 4 | return "

" 5 | + container.escapeExpression(((helper = (helper = helpers.description || (depth0 != null ? depth0.description : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"description","hash":{},"data":data}) : helper))) 6 | + "

\n"; 7 | },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { 8 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}); 9 | 10 | return "

" 11 | + container.escapeExpression(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) 12 | + "

\n

A simple template

\n\n" 13 | + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.description : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : ""); 14 | },"useData":true}); 15 | -------------------------------------------------------------------------------- /test/templates/require.html: -------------------------------------------------------------------------------- 1 | Hello, @require('./bar.html') 2 | -------------------------------------------------------------------------------- /test/templates/simple.html: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |

A simple template

3 | 4 | {{#if description}} 5 |

{{description}}

6 | {{/if}} 7 | --------------------------------------------------------------------------------