Compare commits

..

3 Commits

Author SHA1 Message Date
admin 5fd0d7cd2b wip
CI / Lint (push) Failing after 1m9s
CI / Test (Node) (20, ubuntu-latest) (push) Failing after 51s
CI / Test (Node) (22, ubuntu-latest) (push) Failing after 52s
CI / Test (Node) (24, ubuntu-latest) (push) Failing after 36s
CI / Test (Browser) (push) Successful in 2m13s
Release / Publish to AWS S3 (push) Failing after 34s
CI / Test (Node) (20, windows-latest) (push) Has been cancelled
CI / Test (Node) (22, windows-latest) (push) Has been cancelled
CI / Test (Node) (24, windows-latest) (push) Has been cancelled
2026-05-06 11:54:02 +02:00
Theodore Brown 3105ca73c1 Fix errors with strict lookup in compat mode (#2150)
Fixes #2149, fixes #1741
2026-04-21 22:30:55 +03:00
Igor Savin 0e1d35582a Switch to ESM (#2138) 2026-04-21 22:02:44 +03:00
167 changed files with 2370 additions and 7571 deletions
+1 -1
View File
@@ -116,7 +116,7 @@
}
},
{
"files": ["tests/bench/**/*.mjs"],
"files": ["tests/bench/**/*.js"],
"rules": {
"no-console": "off"
}
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://swc.rs/schema.json",
"module": {
"type": "commonjs",
"importInterop": "swc"
},
"sourceMaps": "inline"
}
+3 -23
View File
@@ -1,11 +1,8 @@
#!/usr/bin/env node
import { createRequire } from 'node:module';
import { loadTemplates, cli } from '../lib/precompiler.js';
import yargs from 'yargs';
const require = createRequire(import.meta.url);
const Precompiler = require('../dist/cjs/precompiler');
const parser = yargs(process.argv.slice(2))
.usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...')
.help(false)
@@ -19,23 +16,6 @@ const parser = yargs(process.argv.slice(2))
type: 'string',
description: 'Source Map File',
})
.option('a', {
type: 'boolean',
description: 'Exports amd style (require.js)',
alias: 'amd',
})
.option('c', {
type: 'string',
description: 'Exports CommonJS style, path to Handlebars module',
alias: 'commonjs',
default: null,
})
.option('h', {
type: 'string',
description: 'Path to handlebar.js (only valid for amd-style)',
alias: 'handlebarPath',
default: '',
})
.option('k', {
type: 'string',
description: 'Known helpers',
@@ -117,7 +97,7 @@ const argv = parser.parseSync();
argv.files = argv._;
delete argv._;
Precompiler.loadTemplates(argv, function (err, opts) {
loadTemplates(argv, function (err, opts) {
if (err) {
throw err;
}
@@ -127,7 +107,7 @@ Precompiler.loadTemplates(argv, function (err, opts) {
} else {
// cli() is async (returns a Promise), so errors would become unhandled
// rejections. Re-throw via nextTick to surface them as uncaught exceptions.
Promise.resolve(Precompiler.cli(opts)).catch((error) => {
Promise.resolve(cli(opts)).catch((error) => {
process.nextTick(() => {
throw error;
});
+127
View File
@@ -0,0 +1,127 @@
import Handlebars from './lib/index.js';
import fs from 'fs';
// Read the basic.js test file and extract test cases
// For now, manually define a set of representative test cases
const tests = [];
function T(template, input, expected, opts) {
tests.push({
template,
input: input ?? {},
expected,
compileOpts: opts?.compile ?? {},
runtimeOpts: opts?.runtime ?? {},
});
}
// ============ basic.js equivalents ============
const g = (t, i, e, co, ro) => T(t, i, e, { compile: co, runtime: ro });
g('{{foo}}', { foo: 'foo' }, 'foo');
g('\\{{foo}}', { foo: 'food' }, '{{foo}}');
g('content \\{{foo}}', { foo: 'food' }, 'content {{foo}}');
g('\\\\{{foo}}', { foo: 'food' }, '\\food');
g('\\\\ {{foo}}', { foo: 'food' }, '\\\\ food');
g(
'Goodbye\n{{cruel}}\n{{world}}!',
{ cruel: 'cruel', world: 'world' },
'Goodbye\ncruel\nworld!'
);
g('{{.}}{{length}}', 'bye', 'bye3');
g('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!');
g(
'{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!',
{ cruel: 'cruel', world: 'world' },
'Goodbye\ncruel\nworld!'
);
g(' {{~! comment ~}} blah', {}, 'blah');
g(' {{~!-- long-comment --~}} blah', {}, 'blah');
g(' {{! comment ~}} blah', {}, ' blah');
g(' {{~! comment}} blah', {}, ' blah');
g('{{#if foo}}foo{{/if}}', { foo: true }, 'foo');
g('{{#if foo}}foo{{else}}bar{{/if}}', { foo: false }, 'bar');
g('{{#unless foo}}bar{{/unless}}', { foo: false }, 'bar');
g('{{#with foo}}{{bar}}{{/with}}', { foo: { bar: 'baz' } }, 'baz');
g('{{#each items}}{{this}}{{/each}}', { items: ['a', 'b', 'c'] }, 'abc');
g('{{#each items}}{{@index}}{{/each}}', { items: ['a', 'b'] }, '01');
g('<b>{{foo}}</b>', { foo: '&' }, '<b>&amp;</b>');
g('{{{foo}}}', { foo: '<b>' }, '<b>');
g('{{foo.bar}}', { foo: { bar: 'baz' } }, 'baz');
g('{{foo/bar}}', { foo: { bar: 'baz' } }, 'baz');
g('{{../foo}}', {}, ''); // depth at root = empty
g('{{{foo}}}', { foo: '&' }, '&');
// ============ blocks.js equivalents ============
g(
'{{#list people}}{{firstName}} {{lastName}}\n{{/list}}',
{
people: [
{ firstName: 'Yehuda', lastName: 'Katz' },
{ firstName: 'Carl', lastName: 'Lerche' },
],
},
'Yehuda Katz\nCarl Lerche\n'
);
g(
'{{#list people}}{{../prefix}} {{firstName}}\n{{/list}}',
{ people: [{ firstName: 'Yehuda' }, { firstName: 'Carl' }], prefix: 'Mr' },
'Mr Yehuda\nMr Carl\n'
);
g(
'{{#each people as |person|}}{{person}}\n{{/each}}',
{ people: ['Yehuda', 'Carl'] },
'Yehuda\nCarl\n'
);
// ============ builtins.js equivalents ============
g(
'{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!',
{ goodbye: true, world: 'world' },
'GOODBYE cruel world!'
);
g(
'{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!',
{ goodbye: false, world: 'world' },
'cruel world!'
);
g('{{lookup foo "bar"}}', { foo: { bar: 'val' } }, 'val');
g(
'{{#each items as |value key|}}{{key}}:{{value}},{{/each}}',
{ items: { a: 1, b: 2 } },
'a:1,b:2,'
);
// Write output
const results = tests.map((t, i) => {
let result,
error = null;
try {
const env = Handlebars.create();
const compiled = env.compile(t.template, t.compileOpts);
result = compiled(t.input, t.runtimeOpts);
} catch (e) {
error = e.message;
result = null;
}
return {
i,
template: t.template,
input: JSON.stringify(t.input),
expected: t.expected,
result,
error,
};
});
fs.writeFileSync('/tmp/golden_results.json', JSON.stringify(results, null, 2));
console.log(`Generated ${results.length} golden results`);
const failures = results.filter((r) => r.result !== r.expected);
if (failures.length) {
console.log(`\nFAILURES (${failures.length}):`);
failures.forEach((f) =>
console.log(
` [${f.i}] "${f.template}" expected="${f.expected}" got="${f.result}" error="${f.error}"`
)
);
}
+9 -5
View File
@@ -5,14 +5,18 @@ import {
Visitor,
} from '@handlebars/parser';
import runtime from './handlebars.runtime';
import runtime from './handlebars.runtime.js';
// Compiler imports
import AST from './handlebars/compiler/ast';
import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
import AST from './handlebars/compiler/ast.js';
import {
Compiler,
compile,
precompile,
} from './handlebars/compiler/compiler.js';
import JavaScriptCompiler from './handlebars/compiler/javascript-compiler.js';
import noConflict from './handlebars/no-conflict';
import noConflict from './handlebars/no-conflict.js';
let _create = runtime.create;
function create() {
+17 -6
View File
@@ -1,13 +1,13 @@
import { Exception } from '@handlebars/parser';
import * as base from './handlebars/base';
import * as base from './handlebars/base.js';
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
import SafeString from './handlebars/safe-string';
import * as Utils from './handlebars/utils';
import * as runtime from './handlebars/runtime';
// (This is done to easily share code between module systems and browser envs)
import SafeString from './handlebars/safe-string.js';
import * as Utils from './handlebars/utils.js';
import * as runtime from './handlebars/runtime.js';
import noConflict from './handlebars/no-conflict';
import noConflict from './handlebars/no-conflict.js';
// For compatibility and usage outside of module systems, make the Handlebars object a namespace
function create() {
@@ -37,4 +37,15 @@ noConflict(inst);
inst['default'] = inst;
// Named re-exports for CJS interop.
// See the comment in lib/index.js for the full explanation. In short:
// require('handlebars/runtime').COMPILER_REVISION must be directly accessible
// for tools like handlebars-loader that compare compiler and runtime revisions.
export {
VERSION,
COMPILER_REVISION,
LAST_COMPATIBLE_COMPILER_REVISION,
REVISION_CHANGES,
} from './handlebars/base.js';
export default inst;
+5 -5
View File
@@ -1,9 +1,9 @@
import { Exception } from '@handlebars/parser';
import { createFrame, extend, toString } from './utils';
import { registerDefaultHelpers } from './helpers';
import { registerDefaultDecorators } from './decorators';
import logger from './logger';
import { resetLoggedProperties } from './internal/proto-access';
import { createFrame, extend, toString } from './utils.js';
import { registerDefaultHelpers } from './helpers.js';
import { registerDefaultDecorators } from './decorators.js';
import logger from './logger.js';
import { resetLoggedProperties } from './internal/proto-access.js';
export const VERSION = '4.7.7';
export const COMPILER_REVISION = 8;
+2 -53
View File
@@ -1,56 +1,5 @@
/* global define */
import { isArray } from '../utils';
let SourceNode;
try {
/* v8 ignore next */
if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we assume that
// they are running on the browser and thus have no need for the source-map library.
// The variable indirection prevents bundlers from statically resolving and bundling
// source-map (which requires Node-built-ins). The stub SourceNode below handles
// browser/bundled usage. Bundlers may emit a "Critical dependency" warning — this
// is expected and harmless.
let mod = 'source-map';
let SourceMap = require(mod);
SourceNode = SourceMap.SourceNode;
}
// oxlint-disable-next-line no-unused-vars -- Babel 5 requires named catch param
} catch (err) {
/* NOP */
}
/* v8 ignore next -- tested but not covered due to dist build */
if (!SourceNode) {
SourceNode = function (line, column, srcFile, chunks) {
this.src = '';
if (chunks) {
this.add(chunks);
}
};
/* v8 ignore next */
SourceNode.prototype = {
add: function (chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src += chunks;
},
prepend: function (chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src = chunks + this.src;
},
toStringWithSourceMap: function () {
return { code: this.toString() };
},
toString: function () {
return this.src;
},
};
}
import { isArray } from '../utils.js';
import { SourceNode } from '#source-node';
function castChunk(chunk, codeGen, loc) {
if (isArray(chunk)) {
+2 -2
View File
@@ -1,6 +1,6 @@
import { Exception } from '@handlebars/parser';
import { isArray, indexOf, extend } from '../utils';
import AST from './ast';
import { isArray, indexOf, extend } from '../utils.js';
import AST from './ast.js';
const slice = [].slice;
+32 -16
View File
@@ -1,7 +1,7 @@
import { Exception } from '@handlebars/parser';
import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
import { isArray } from '../utils';
import CodeGen from './code-gen';
import { COMPILER_REVISION, REVISION_CHANGES } from '../base.js';
import { isArray } from '../utils.js';
import CodeGen from './code-gen.js';
function Literal(value) {
this.value = value;
@@ -164,10 +164,12 @@ JavaScriptCompiler.prototype = {
let { programs, decorators } = this.context;
for (i = 0, l = programs.length; i < l; i++) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
if (programs[i]) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
}
}
@@ -834,8 +836,8 @@ JavaScriptCompiler.prototype = {
let existing = this.matchExistingProgram(child);
if (existing == null) {
// Placeholder to prevent name conflicts for nested children
let index = this.context.programs.push('') - 1;
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
let index = this.context.programs.length;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(
@@ -1173,20 +1175,34 @@ function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
stack = compiler.nameLookup(stack, parts[i], type);
}
if (requireTerminal) {
if (!requireTerminal) {
return stack;
}
if (startPartIndex > len) {
// Compat mode already consumed all parts via depthedLookup; the stack
// holds the resolved value, not an object to look the property up on.
// Use container.strictLookup to traverse depths with a strict error on miss.
return [
compiler.aliasable('container.strict'),
'(',
stack,
', ',
compiler.aliasable('container.strictLookup'),
'(depths, ',
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )',
];
} else {
return stack;
}
return [
compiler.aliasable('container.strict'),
'(',
stack,
', ',
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )',
];
}
export default JavaScriptCompiler;
@@ -0,0 +1,31 @@
import { isArray } from '../utils.js';
// Lightweight stub for browser environments where the source-map package
// (which depends on Node.js built-ins) is not available.
export function SourceNode(line, column, srcFile, chunks) {
this.src = '';
if (chunks) {
this.add(chunks);
}
}
SourceNode.prototype = {
add(chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src += chunks;
},
prepend(chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src = chunks + this.src;
},
toStringWithSourceMap() {
return { code: this.toString() };
},
toString() {
return this.src;
},
};
@@ -0,0 +1 @@
export { SourceNode } from 'source-map';
+1 -1
View File
@@ -1,4 +1,4 @@
import registerInline from './decorators/inline';
import registerInline from './decorators/inline.js';
export function registerDefaultDecorators(instance) {
registerInline(instance);
+1 -1
View File
@@ -1,4 +1,4 @@
import { extend } from '../utils';
import { extend } from '../utils.js';
export default function (instance) {
instance.registerDecorator(
+7 -7
View File
@@ -1,10 +1,10 @@
import registerBlockHelperMissing from './helpers/block-helper-missing';
import registerEach from './helpers/each';
import registerHelperMissing from './helpers/helper-missing';
import registerIf from './helpers/if';
import registerLog from './helpers/log';
import registerLookup from './helpers/lookup';
import registerWith from './helpers/with';
import registerBlockHelperMissing from './helpers/block-helper-missing.js';
import registerEach from './helpers/each.js';
import registerHelperMissing from './helpers/helper-missing.js';
import registerIf from './helpers/if.js';
import registerLog from './helpers/log.js';
import registerLookup from './helpers/lookup.js';
import registerWith from './helpers/with.js';
export function registerDefaultHelpers(instance) {
registerBlockHelperMissing(instance);
@@ -1,4 +1,4 @@
import { isArray } from '../utils';
import { isArray } from '../utils.js';
export default function (instance) {
instance.registerHelper('blockHelperMissing', function (context, options) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { createFrame, isArray, isFunction, isMap, isSet } from '../utils';
import { createFrame, isArray, isFunction, isMap, isSet } from '../utils.js';
export default function (instance) {
instance.registerHelper('each', function (context, options) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
import { isEmpty, isFunction } from '../utils.js';
export default function (instance) {
instance.registerHelper('if', function (conditional, options) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
import { isEmpty, isFunction } from '../utils.js';
export default function (instance) {
instance.registerHelper('with', function (context, options) {
+2 -2
View File
@@ -1,5 +1,5 @@
import { extend } from '../utils';
import logger from '../logger';
import { extend } from '../utils.js';
import logger from '../logger.js';
const loggedProperties = Object.create(null);
+1 -1
View File
@@ -1,4 +1,4 @@
import { indexOf } from './utils';
import { indexOf } from './utils.js';
let logger = {
methodMap: ['debug', 'info', 'warn', 'error'],
+21 -5
View File
@@ -1,17 +1,17 @@
import { Exception } from '@handlebars/parser';
import * as Utils from './utils';
import * as Utils from './utils.js';
import {
COMPILER_REVISION,
createFrame,
LAST_COMPATIBLE_COMPILER_REVISION,
REVISION_CHANGES,
} from './base';
import { moveHelperToHooks } from './helpers';
import { wrapHelper } from './internal/wrapHelper';
} from './base.js';
import { moveHelperToHooks } from './helpers.js';
import { wrapHelper } from './internal/wrapHelper.js';
import {
createProtoAccessControl,
resultIsAllowed,
} from './internal/proto-access';
} from './internal/proto-access.js';
export function checkRevision(compilerInfo) {
const compilerRevision = (compilerInfo && compilerInfo[0]) || 1,
@@ -116,6 +116,22 @@ export function template(templateSpec, env) {
}
return container.lookupProperty(obj, name);
},
strictLookup: function (depths, name, loc) {
const len = depths.length;
let depth;
for (let i = 0; i < len; i++) {
const d = depths[i];
if (
d &&
(typeof d === 'object' || typeof d === 'function') &&
name in d
) {
depth = d;
break;
}
}
return container.strict(depth, name, loc);
},
lookupProperty: function (parent, propertyName) {
if (Utils.isMap(parent)) {
return parent.get(propertyName);
+44 -23
View File
@@ -1,25 +1,46 @@
// USAGE:
// var handlebars = require('handlebars');
/* eslint-disable no-var */
import handlebars from './handlebars.js';
import { PrintVisitor, print } from '@handlebars/parser';
// var local = handlebars.create();
handlebars.PrintVisitor = PrintVisitor;
handlebars.print = print;
var handlebars = require('../dist/cjs/handlebars')['default'];
var parser = require('@handlebars/parser');
handlebars.PrintVisitor = parser.PrintVisitor;
handlebars.print = parser.print;
module.exports = handlebars;
// Publish a Node.js require() handler for .handlebars and .hbs files
function extension(module, filename) {
var fs = require('fs');
var templateString = fs.readFileSync(filename, 'utf8');
module.exports = handlebars.compile(templateString);
}
/* v8 ignore next 4 */
if (typeof require !== 'undefined' && require.extensions) {
require.extensions['.handlebars'] = extension;
require.extensions['.hbs'] = extension;
}
// Named exports for CJS interop.
//
// When Node.js (v22+) runs require() on an ESM module, it returns the module
// namespace object — only named exports become direct properties. Without these,
// require('handlebars') would return { default: inst } and calls like
// Handlebars.precompile() or Handlebars.COMPILER_REVISION would be undefined.
//
// Tools like handlebars-loader rely on these being directly accessible via
// require('handlebars').create(), require('handlebars').COMPILER_REVISION, etc.
export const {
create,
compile,
precompile,
parse,
parseWithoutProcessing,
COMPILER_REVISION,
LAST_COMPATIBLE_COMPILER_REVISION,
REVISION_CHANGES,
VERSION,
AST,
Compiler,
JavaScriptCompiler,
Parser,
Visitor,
SafeString,
Exception,
Utils,
escapeExpression,
VM,
template,
log,
registerHelper,
unregisterHelper,
registerPartial,
unregisterPartial,
registerDecorator,
unregisterDecorator,
} = handlebars;
export { PrintVisitor, print };
export default handlebars;
+16 -45
View File
@@ -1,11 +1,11 @@
/* eslint-disable no-console */
import Async from 'neo-async';
import fs from 'fs';
import Handlebars from './handlebars';
import Handlebars from './handlebars.js';
import { basename } from 'path';
import { SourceMapConsumer, SourceNode } from 'source-map';
module.exports.loadTemplates = function (opts, callback) {
export function loadTemplates(opts, callback) {
loadStrings(opts, function (err, strings) {
if (err) {
callback(err);
@@ -20,7 +20,7 @@ module.exports.loadTemplates = function (opts, callback) {
});
}
});
};
}
function loadStrings(opts, callback) {
let strings = arrayCast(opts.string),
@@ -152,7 +152,7 @@ function loadFiles(opts, callback) {
);
}
module.exports.cli = async function (opts) {
export async function cli(opts) {
if (opts.version) {
console.log(Handlebars.VERSION);
return;
@@ -176,12 +176,7 @@ module.exports.cli = async function (opts) {
}
// Force simple mode if we have only one template and it's unnamed.
if (
!opts.amd &&
!opts.commonjs &&
opts.templates.length === 1 &&
!opts.templates[0].name
) {
if (opts.templates.length === 1 && !opts.templates[0].name) {
opts.simple = true;
}
@@ -200,17 +195,7 @@ module.exports.cli = async function (opts) {
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
output.add(
"define(['" +
opts.handlebarPath +
'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
);
} else if (opts.commonjs) {
output.add('var Handlebars = require("' + opts.commonjs + '");');
} else {
output.add('(function() {\n');
}
output.add('(function() {\n');
output.add(' var template = Handlebars.template, templates = ');
if (opts.namespace) {
output.add(opts.namespace);
@@ -253,9 +238,6 @@ module.exports.cli = async function (opts) {
throw new Handlebars.Exception('Name missing for template');
}
if (opts.amd && !multiple) {
output.add('return ');
}
output.add([
objectName,
"['",
@@ -269,14 +251,7 @@ module.exports.cli = async function (opts) {
// Output the content
if (!opts.simple) {
if (opts.amd) {
if (multiple) {
output.add(['return ', objectName, ';\n']);
}
output.add('});');
} else if (!opts.commonjs) {
output.add('})();');
}
output.add('})();');
}
if (opts.map) {
@@ -287,7 +262,7 @@ module.exports.cli = async function (opts) {
output.map = output.map + '';
if (opts.min) {
output = minify(output, opts.map);
output = await minify(output, opts.map);
}
if (opts.map) {
@@ -300,7 +275,7 @@ module.exports.cli = async function (opts) {
} else {
console.log(output);
}
};
}
function arrayCast(value) {
value = value != null ? value : [];
@@ -313,29 +288,25 @@ function arrayCast(value) {
/**
* Run uglify to minify the compiled template, if uglify exists in the dependencies.
*
* We are using `require` instead of `import` here, because es6-modules do not allow
* dynamic imports and uglify-js is an optional dependency. Since we are inside NodeJS here, this
* should not be a problem.
*
* @param {string} output the compiled template
* @param {string} sourceMapFile the file to write the source map to.
*/
function minify(output, sourceMapFile) {
async function minify(output, sourceMapFile) {
let uglify;
try {
// Try to resolve uglify-js in order to see if it does exist
require.resolve('uglify-js');
uglify = await import('uglify-js');
// Handle both default and named exports
uglify = uglify.default || uglify;
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
// Something else seems to be wrong
if (e.code !== 'ERR_MODULE_NOT_FOUND' && e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
// it does not exist!
console.error(
'Code minimization is disabled due to missing uglify-js dependency'
);
return output;
}
return require('uglify-js').minify(output.code, {
return uglify.minify(output.code, {
sourceMap: {
content: output.map,
url: sourceMapFile,
+18 -3648
View File
File diff suppressed because it is too large Load Diff
+25 -22
View File
@@ -16,26 +16,40 @@
"url": "https://github.com/handlebars-lang/handlebars.js.git"
},
"bin": {
"handlebars": "bin/handlebars.mjs"
"handlebars": "bin/handlebars.js"
},
"files": [
"bin",
"dist/*.js",
"dist/cjs/**/*.js",
"lib",
"release-notes.md",
"runtime.js",
"types/*.d.ts",
"runtime.d.ts"
],
"type": "module",
"main": "lib/index.js",
"browser": "./dist/cjs/handlebars.js",
"types": "types/index.d.ts",
"imports": {
"#source-node": {
"node": "./lib/handlebars/compiler/source-node.node.js",
"default": "./lib/handlebars/compiler/source-node.browser.js"
}
},
"exports": {
".": {
"types": "./types/index.d.ts",
"default": "./lib/index.js"
},
"./runtime": {
"types": "./runtime.d.ts",
"default": "./lib/handlebars.runtime.js"
},
"./package.json": "./package.json"
},
"scripts": {
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('tmp',{recursive:true,force:true})\"",
"build:cjs": "swc lib --out-dir dist/cjs --strip-leading-paths --ignore \"**/index.js\"",
"build:bundles": "rspack build",
"build": "npm run clean && npm run build:cjs && npm run build:bundles",
"clean": "node --input-type=module -e \"import fs from 'fs';fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tmp',{recursive:true,force:true})\"",
"build": "npm run clean && rspack build",
"release": "npm run build",
"publish:aws": "npm run build && npm run test:tasks && node tasks/publish-to-aws.js",
"format": "oxfmt --write . && oxlint --fix .",
@@ -52,14 +66,14 @@
"test:browser-smoke": "playwright test --config tests/browser/playwright.config.js",
"test:serve": "npx serve -l 9999 .",
"test:integration": "npm run build && ./tests/integration/run-integration-tests.sh",
"bench": "node tests/bench/perf.mjs",
"bench:compare": "node tests/bench/compare.mjs",
"bench:size": "node tests/bench/size.mjs",
"bench": "node tests/bench/perf.js",
"bench:compare": "node tests/bench/compare.js",
"bench:size": "node tests/bench/size.js",
"--- combined tasks ---": "",
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:test"
},
"dependencies": {
"@handlebars/parser": "^2.1.0",
"@handlebars/parser": "^2.2.2",
"neo-async": "^2.6.2",
"source-map": "^0.7.6",
"yargs": "^18.0.0"
@@ -69,8 +83,6 @@
"@playwright/test": "^1.58.2",
"@rspack/cli": "^1.7.8",
"@rspack/core": "^1.7.8",
"@swc/cli": "^0.8.0",
"@swc/core": "^1.15.18",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
@@ -116,14 +128,5 @@
],
"engines": {
"node": ">=20"
},
"jspm": {
"main": "handlebars",
"directories": {
"lib": "dist/cjs"
},
"buildConfig": {
"minify": true
}
}
}
+38 -23
View File
@@ -1,8 +1,14 @@
const { rspack } = require('@rspack/core');
const path = require('path');
const fs = require('fs');
import { rspack } from '@rspack/core';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const pkg = require('./package.json');
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const pkg = JSON.parse(
fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf8')
);
const license = fs.readFileSync(path.resolve(__dirname, 'LICENSE'), 'utf8');
const banner = `/*!
@@ -29,28 +35,11 @@ function createConfig(entry, filename, minimize) {
filename,
library: {
name: 'Handlebars',
type: 'umd',
type: 'self',
export: 'default',
},
globalObject: 'this',
clean: false,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'ecmascript' },
},
},
},
},
],
},
optimization: {
minimize,
minimizer: minimize
@@ -71,12 +60,38 @@ function createConfig(entry, filename, minimize) {
: [],
},
plugins,
resolve: {
extensions: ['.js', '.json'],
mainFields: ['main'],
},
module: {
rules: [
{
test: /\.js$/,
resolve: {
fullySpecified: false,
},
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'ecmascript' },
},
},
},
},
],
},
target: ['web', 'browserslist'],
devtool: false,
};
}
module.exports = [
export default [
createConfig('./lib/handlebars.js', 'handlebars.js', false),
createConfig('./lib/handlebars.runtime.js', 'handlebars.runtime.js', false),
createConfig('./lib/handlebars.js', 'handlebars.min.js', true),
+3 -2
View File
@@ -1,3 +1,4 @@
import Handlebars = require('handlebars');
import Handlebars from 'handlebars';
declare module 'handlebars/runtime' {}
declare const runtime: typeof Handlebars;
export default runtime;
+3 -3
View File
@@ -1,3 +1,3 @@
// Create a simple path alias to allow browserify to resolve
// the runtime on a supported path.
module.exports = require('./dist/cjs/handlebars.runtime')['default'];
import runtime from './lib/handlebars.runtime.js';
export default runtime;
+6 -3
View File
@@ -1,7 +1,10 @@
require('./common');
import './common.js';
var fs = require('fs'),
vm = require('vm');
import fs from 'fs';
import vm from 'vm';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
global.Handlebars = 'no-conflict';
+3 -2
View File
@@ -1,6 +1,7 @@
require('./common');
import './common.js';
import Handlebars from '../../lib/index.js';
global.Handlebars = require('../../lib');
global.Handlebars = Handlebars;
global.CompilerContext = {
compile: function (template, options) {
-6
View File
@@ -1,6 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['bom'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "a";
},"useData":true});
});
-6
View File
@@ -1,6 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
-1
View File
@@ -1 +0,0 @@
define(["handlebars.runtime"],function(e){var t=(e=e.default).template;return(e.templates=e.templates||{}).empty=t({compiler:[8,">= 4.3.0"],main:function(e,t,a,n,r){return""},useData:!0})});
-6
View File
@@ -1,6 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
-3
View File
@@ -1,3 +0,0 @@
{"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true}
-10
View File
@@ -1,10 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['firstTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>1</div>";
},"useData":true});
templates['secondTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>2</div>";
},"useData":true});
return templates;
});
@@ -1,5 +1,5 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
-6
View File
@@ -1,6 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['artifacts/partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
-6
View File
@@ -1,6 +0,0 @@
define(['some-path/handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+19 -22
View File
@@ -1,25 +1,22 @@
Precompile handlebar templates.
Usage: handlebars.mjs [template|directory]...
Usage: handlebars.js [template|directory]...
Options:
-f, --output Output File [string]
--map Source Map File [string]
-a, --amd Exports amd style (require.js) [boolean]
-c, --commonjs Exports CommonJS style, path to Handlebars module [string] [default: null]
-h, --handlebarPath Path to handlebar.js (only valid for amd-style) [string] [default: ""]
-k, --known Known helpers [string]
-o, --knownOnly Known helpers only [boolean]
-m, --min Minimize output [boolean]
-n, --namespace Template namespace [string] [default: "Handlebars.templates"]
-s, --simple Output template function only. [boolean]
-N, --name Name of passed string templates. Optional if running in a simple mode. Required when operating on
multiple templates. [string]
-i, --string Generates a template from the passed CLI argument.
"-" is treated as a special value and causes stdin to be read for the template value. [string]
-r, --root Template root. Base value that will be stripped from template names. [string]
-p, --partial Compiling a partial template [boolean]
-d, --data Include data when compiling [boolean]
-e, --extension Template extension. [string] [default: "handlebars"]
-b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
-v, --version Prints the current compiler version [boolean]
--help Outputs this message [boolean]
-f, --output Output File [string]
--map Source Map File [string]
-k, --known Known helpers [string]
-o, --knownOnly Known helpers only [boolean]
-m, --min Minimize output [boolean]
-n, --namespace Template namespace [string] [default: "Handlebars.templates"]
-s, --simple Output template function only. [boolean]
-N, --name Name of passed string templates. Optional if running in a simple mode. Required when operating on
multiple templates. [string]
-i, --string Generates a template from the passed CLI argument.
"-" is treated as a special value and causes stdin to be read for the template value. [string]
-r, --root Template root. Base value that will be stripped from template names. [string]
-p, --partial Compiling a partial template [boolean]
-d, --data Include data when compiling [boolean]
-e, --extension Template extension. [string] [default: "handlebars"]
-b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
-v, --version Prints the current compiler version [boolean]
--help Outputs this message [boolean]
-10
View File
@@ -1,10 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = someNameSpace = someNameSpace || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
return templates;
});
@@ -1,6 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['non.default.extension'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>This is a test</div>";
},"useData":true});
});
@@ -1,24 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['known.helpers'] = template({"0":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
}
return undefined
};
return " <div>Some known helper</div>\n"
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
},"1":function(container,depth0,helpers,partials,data) {
return " <div>Another known helper</div>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
}
return undefined
};
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(0, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
},"useData":true});
});
-6
View File
@@ -1,6 +0,0 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return Handlebars.partials['partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
-2
View File
@@ -1,2 +0,0 @@
define(["handlebars.runtime"],function(e){var t=(e=e.default).template;return(e.templates=e.templates||{}).test=t({compiler:[8,">= 4.3.0"],main:function(e,t,a,n,i){return"<div>1</div>"},useData:!0})});
//# sourceMappingURL=./spec/tmp/source.map.amd.txt
+25 -114
View File
@@ -1,15 +1,19 @@
/* eslint-disable no-console */
import Handlebars from '../lib/index.js';
import * as Precompiler from '../lib/precompiler.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import uglify from 'uglify-js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('precompiler', function () {
// NOP Under non-node environments
if (typeof process === 'undefined') {
return;
}
var Handlebars = require('../lib'),
Precompiler = require('../dist/cjs/precompiler'),
fs = require('fs'),
uglify = require('uglify-js');
var log,
logFunction,
errorLog,
@@ -25,34 +29,6 @@ describe('precompiler', function () {
content,
writeFileSync;
/**
* Mock the Module.prototype.require-function such that an error is thrown, when "uglify-js" is loaded.
*
* The function cleans up its mess when "callback" is finished
*
* @param {Error} loadError the error that should be thrown if uglify is loaded
* @param {function} callback a callback-function to run when the mock is active.
*/
async function mockRequireUglify(loadError, callback) {
var Module = require('module');
var _resolveFilename = Module._resolveFilename;
delete require.cache[require.resolve('uglify-js')];
delete require.cache[require.resolve('../dist/cjs/precompiler')];
Module._resolveFilename = function (request, mod) {
if (request === 'uglify-js') {
throw loadError;
}
return _resolveFilename.call(this, request, mod);
};
try {
await callback();
} finally {
Module._resolveFilename = _resolveFilename;
delete require.cache[require.resolve('uglify-js')];
delete require.cache[require.resolve('../dist/cjs/precompiler')];
}
}
beforeEach(function () {
precompile = Handlebars.precompile;
minify = uglify.minify;
@@ -117,7 +93,10 @@ describe('precompiler', function () {
});
it('should throw when missing name', async function () {
await expect(
Precompiler.cli({ templates: [{ source: '' }], amd: true })
Precompiler.cli({
templates: [{ source: '' }, { source: '' }],
hasDirectory: true,
})
).rejects.toThrow('Name missing for template');
});
it('should throw when combining simple and directories', async function () {
@@ -140,56 +119,14 @@ describe('precompiler', function () {
await Precompiler.cli({ templates: [{ source: '' }] });
expect(log).toBe('simple\n');
});
it('should output amd templates', async function () {
it('should output wrapped templates', async function () {
Handlebars.precompile = function () {
return 'amd';
return 'wrapped';
};
await Precompiler.cli({ templates: [emptyTemplate], amd: true });
expect(log).toMatch(/template\(amd\)/);
});
it('should output multiple amd', async function () {
Handlebars.precompile = function () {
return 'amd';
};
await Precompiler.cli({
templates: [emptyTemplate, emptyTemplate],
amd: true,
namespace: 'foo',
});
expect(log).toMatch(/templates = foo = foo \|\|/);
expect(log).toMatch(/return templates/);
expect(log).toMatch(/template\(amd\)/);
});
it('should output amd partials', async function () {
Handlebars.precompile = function () {
return 'amd';
};
await Precompiler.cli({
templates: [emptyTemplate],
amd: true,
partial: true,
});
expect(log).toMatch(/return Handlebars\.partials\['empty'\]/);
expect(log).toMatch(/template\(amd\)/);
});
it('should output multiple amd partials', async function () {
Handlebars.precompile = function () {
return 'amd';
};
await Precompiler.cli({
templates: [emptyTemplate, emptyTemplate],
amd: true,
partial: true,
});
expect(log).not.toMatch(/return Handlebars\.partials\[/);
expect(log).toMatch(/template\(amd\)/);
});
it('should output commonjs templates', async function () {
Handlebars.precompile = function () {
return 'commonjs';
};
await Precompiler.cli({ templates: [emptyTemplate], commonjs: true });
expect(log).toMatch(/template\(commonjs\)/);
await Precompiler.cli({ templates: [emptyTemplate] });
expect(log).toMatch(/template\(wrapped\)/);
expect(log).toMatch(/\(function\(\)/);
});
it('should set data flag', async function () {
@@ -233,7 +170,7 @@ describe('precompiler', function () {
it('should output minimized templates', async function () {
Handlebars.precompile = function () {
return 'amd';
return 'iife';
};
uglify.minify = function () {
return { code: 'min' };
@@ -242,33 +179,6 @@ describe('precompiler', function () {
expect(log).toBe('min');
});
it('should omit minimization gracefully, if uglify-js is missing', async function () {
var error = new Error("Cannot find module 'uglify-js'");
error.code = 'MODULE_NOT_FOUND';
await mockRequireUglify(error, async function () {
var Precompiler = require('../dist/cjs/precompiler');
Handlebars.precompile = function () {
return 'amd';
};
await Precompiler.cli({ templates: [emptyTemplate], min: true });
expect(log).toMatch(/template\(amd\)/);
expect(log).toMatch(/\n/);
expect(errorLog).toMatch(/Code minimization is disabled/);
});
});
it('should fail on errors (other than missing module) while loading uglify-js', async function () {
await mockRequireUglify(new Error('Mock Error'), async function () {
var Precompiler = require('../dist/cjs/precompiler');
Handlebars.precompile = function () {
return 'amd';
};
await expect(
Precompiler.cli({ templates: [emptyTemplate], min: true })
).rejects.toThrow('Mock Error');
});
});
it('should output map', async function () {
await Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' });
@@ -367,11 +277,12 @@ describe('precompiler', function () {
expect(opts.templates[1].source).toBe('bar');
});
it('should accept stdin input', async function () {
var stdin = require('mock-stdin').stdin();
var { stdin } = await import('mock-stdin');
var stdinMock = stdin();
var promise = loadTemplatesAsync({ string: '-' });
stdin.send('fo');
stdin.send('o');
stdin.end();
stdinMock.send('fo');
stdinMock.send('o');
stdinMock.end();
var opts = await promise;
expect(opts.templates[0].source).toBe('foo');
});
-23
View File
@@ -1,23 +0,0 @@
if (typeof require !== 'undefined' && require.extensions['.handlebars']) {
describe('Require', function () {
it('Load .handlebars files with require()', function () {
var template = require('./artifacts/example_1');
expect(template).toBe(require('./artifacts/example_1.handlebars'));
var expected = 'foo\n';
var result = template({ foo: 'foo' });
expect(result).toBe(expected);
});
it('Load .hbs files with require()', function () {
var template = require('./artifacts/example_2');
expect(template).toBe(require('./artifacts/example_2.hbs'));
var expected = 'Hello, World!\n';
var result = template({ name: 'World' });
expect(result).toBe(expected);
});
});
}
+5 -4
View File
@@ -1,8 +1,9 @@
import { createRequire } from 'module';
var SourceMap, SourceMapConsumer;
try {
if (typeof define !== 'function' || !define.amd) {
var SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer;
}
SourceMap = createRequire(import.meta.url)('source-map');
SourceMapConsumer = SourceMap.SourceMapConsumer;
} catch {
/* NOP for in browser */
}
+7 -3
View File
@@ -1,16 +1,20 @@
import fs from 'fs';
import { fileURLToPath } from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('spec', function () {
// NOP Under non-node environments
if (typeof process === 'undefined') {
return;
}
var fs = require('fs');
var specDir = __dirname + '/mustache/specs/';
var specs = fs.readdirSync(specDir).filter((name) => /.*\.json$/.test(name));
specs.forEach(function (name) {
var spec = require(specDir + name);
var spec = JSON.parse(fs.readFileSync(specDir + name, 'utf8'));
spec.tests.forEach(function (test) {
// Our lambda implementation knowingly deviates from the optional Mustache lambda spec
// We also do not support alternative delimiters
+57
View File
@@ -121,6 +121,63 @@ describe('strict', function () {
});
});
describe('strict and compat mode', function () {
it('GH-1741: should render a simple variable', function () {
expectTemplate('{{v}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ v: 'a' })
.toCompileTo('a');
});
it('should throw for a missing variable', function () {
expectTemplate('{{v}}')
.withCompileOptions({ strict: true, compat: true })
.toThrow(Exception, /"v" not defined in/);
});
it('GH-2149: should render correctly when block context is a boolean', function () {
expectTemplate('{{#foo}}Hello {{bar}}{{/foo}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ foo: true, bar: 'World' })
.toCompileTo('Hello World');
});
it('GH-2149: should render correctly when looking up a property on each item', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ items: [{ name: 'Hello' }] })
.toCompileTo('Hello');
});
it('should still throw when a property is missing at all depths', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ items: [1, 2] })
.toThrow(Exception, /"name" not defined in/);
});
it('should still perform recursive lookup when a property is not in the current context', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ name: 'root', items: [{}] })
.toCompileTo('root');
});
it('should still perform recursive lookup with a multi-part path not in context', function () {
expectTemplate('{{#with child}}{{name.first}}{{/with}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ name: { first: 'root' }, child: { name: null } })
.toCompileTo('root');
});
it('should directly return an explicitly null property', function () {
expectTemplate('{{#each items}}{{name}}{{/each}}')
.withCompileOptions({ strict: true, compat: true })
.withInput({ name: 'root', items: [{ name: null }] })
.toCompileTo('');
});
});
describe('assume objects', function () {
it('should ignore missing property', function () {
expectTemplate('{{hello}}')
-796
View File
@@ -1,796 +0,0 @@
function shouldMatchTokens(result, tokens) {
for (var index = 0; index < result.length; index++) {
expect(result[index].name).toBe(tokens[index]);
}
}
function shouldBeToken(result, name, text) {
expect(result.name).toBe(name);
expect(result.text).toBe(text);
}
describe('Tokenizer', function () {
if (!Handlebars.Parser) {
return;
}
function tokenize(template) {
var parser = Handlebars.Parser,
lexer = parser.lexer;
lexer.setInput(template);
var out = [],
token;
while ((token = lexer.lex())) {
var result = parser.terminals_[token] || token;
if (!result || result === 'EOF' || result === 'INVALID') {
break;
}
out.push({ name: result, text: lexer.yytext });
}
return out;
}
it('tokenizes a simple mustache as "OPEN ID CLOSE"', function () {
var result = tokenize('{{foo}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], 'ID', 'foo');
});
it('supports unescaping with &', function () {
var result = tokenize('{{&bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[0], 'OPEN', '{{&');
shouldBeToken(result[1], 'ID', 'bar');
});
it('supports unescaping with {{{', function () {
var result = tokenize('{{{bar}}}');
shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
shouldBeToken(result[1], 'ID', 'bar');
});
it('supports escaping delimiters', function () {
var result = tokenize('{{foo}} \\{{bar}} {{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[3], 'CONTENT', ' ');
shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
});
it('supports escaping multiple delimiters', function () {
var result = tokenize('{{foo}} \\{{bar}} \\{{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'CONTENT',
'CONTENT',
]);
shouldBeToken(result[3], 'CONTENT', ' ');
shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
shouldBeToken(result[5], 'CONTENT', '{{baz}}');
});
it('supports escaping a triple stash', function () {
var result = tokenize('{{foo}} \\{{{bar}}} {{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[4], 'CONTENT', '{{{bar}}} ');
});
it('supports escaping escape character', function () {
var result = tokenize('{{foo}} \\\\{{bar}} {{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
});
it('supports escaping multiple escape characters', function () {
var result = tokenize('{{foo}} \\\\{{bar}} \\\\{{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
shouldBeToken(result[7], 'CONTENT', ' \\');
shouldBeToken(result[9], 'ID', 'baz');
});
it('supports escaped mustaches after escaped escape characters', function () {
var result = tokenize('{{foo}} \\\\{{bar}} \\{{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'CONTENT',
'CONTENT',
]);
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[4], 'OPEN', '{{');
shouldBeToken(result[5], 'ID', 'bar');
shouldBeToken(result[7], 'CONTENT', ' ');
shouldBeToken(result[8], 'CONTENT', '{{baz}}');
});
it('supports escaped escape characters after escaped mustaches', function () {
var result = tokenize('{{foo}} \\{{bar}} \\\\{{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'CONTENT',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
shouldBeToken(result[5], 'CONTENT', '\\');
shouldBeToken(result[6], 'OPEN', '{{');
shouldBeToken(result[7], 'ID', 'baz');
});
it('supports escaped escape character on a triple stash', function () {
var result = tokenize('{{foo}} \\\\{{{bar}}} {{baz}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'CLOSE',
'CONTENT',
'OPEN_UNESCAPED',
'ID',
'CLOSE_UNESCAPED',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
});
it('tokenizes a simple path', function () {
var result = tokenize('{{foo/bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows dot notation', function () {
var result = tokenize('{{foo.bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(tokenize('{{foo.bar.baz}}'), [
'OPEN',
'ID',
'SEP',
'ID',
'SEP',
'ID',
'CLOSE',
]);
});
it('allows path literals with []', function () {
var result = tokenize('{{foo.[bar]}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows multiple path literals on a line with []', function () {
var result = tokenize('{{foo.[bar]}}{{foo.[baz]}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'SEP',
'ID',
'CLOSE',
'OPEN',
'ID',
'SEP',
'ID',
'CLOSE',
]);
});
it('allows escaped literals in []', function () {
var result = tokenize('{{foo.[bar\\]]}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('tokenizes {{.}} as OPEN ID CLOSE', function () {
var result = tokenize('{{.}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
});
it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function () {
var result = tokenize('{{../foo/bar}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'SEP',
'ID',
'SEP',
'ID',
'CLOSE',
]);
shouldBeToken(result[1], 'ID', '..');
});
it('tokenizes a path with .. as a parent path', function () {
var result = tokenize('{{../foo.bar}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'SEP',
'ID',
'SEP',
'ID',
'CLOSE',
]);
shouldBeToken(result[1], 'ID', '..');
});
it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function () {
var result = tokenize('{{this/foo}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], 'ID', 'this');
shouldBeToken(result[3], 'ID', 'foo');
});
it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function () {
var result = tokenize('{{ foo }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"', function () {
var result = tokenize('{{ foo \n bar }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes raw content as "CONTENT"', function () {
var result = tokenize('foo {{ bar }} baz');
shouldMatchTokens(result, ['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
shouldBeToken(result[0], 'CONTENT', 'foo ');
shouldBeToken(result[4], 'CONTENT', ' baz');
});
it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function () {
var result = tokenize('{{> foo}}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function () {
var result = tokenize('{{> foo bar }}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
});
it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function () {
var result = tokenize('{{>foo}}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function () {
var result = tokenize('{{>foo }}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function () {
var result = tokenize('{{>foo/bar.baz }}');
shouldMatchTokens(result, [
'OPEN_PARTIAL',
'ID',
'SEP',
'ID',
'SEP',
'ID',
'CLOSE',
]);
});
it('tokenizes partial block declarations', function () {
var result = tokenize('{{#> foo}}');
shouldMatchTokens(result, ['OPEN_PARTIAL_BLOCK', 'ID', 'CLOSE']);
});
it('tokenizes a comment as "COMMENT"', function () {
var result = tokenize('foo {{! this is a comment }} bar {{ baz }}');
shouldMatchTokens(result, [
'CONTENT',
'COMMENT',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[1], 'COMMENT', '{{! this is a comment }}');
});
it('tokenizes a block comment as "COMMENT"', function () {
var result = tokenize('foo {{!-- this is a {{comment}} --}} bar {{ baz }}');
shouldMatchTokens(result, [
'CONTENT',
'COMMENT',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[1], 'COMMENT', '{{!-- this is a {{comment}} --}}');
});
it('tokenizes a block comment with whitespace as "COMMENT"', function () {
var result = tokenize(
'foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}'
);
shouldMatchTokens(result, [
'CONTENT',
'COMMENT',
'CONTENT',
'OPEN',
'ID',
'CLOSE',
]);
shouldBeToken(result[1], 'COMMENT', '{{!-- this is a\n{{comment}}\n--}}');
});
it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function () {
var result = tokenize('{{#foo}}content{{/foo}}');
shouldMatchTokens(result, [
'OPEN_BLOCK',
'ID',
'CLOSE',
'CONTENT',
'OPEN_ENDBLOCK',
'ID',
'CLOSE',
]);
});
it('tokenizes directives', function () {
shouldMatchTokens(tokenize('{{#*foo}}content{{/foo}}'), [
'OPEN_BLOCK',
'ID',
'CLOSE',
'CONTENT',
'OPEN_ENDBLOCK',
'ID',
'CLOSE',
]);
shouldMatchTokens(tokenize('{{*foo}}'), ['OPEN', 'ID', 'CLOSE']);
});
it('tokenizes inverse sections as "INVERSE"', function () {
shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']);
shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']);
shouldMatchTokens(tokenize('{{ else }}'), ['INVERSE']);
});
it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function () {
var result = tokenize('{{^foo}}');
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function () {
var result = tokenize('{{^ foo }}');
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function () {
var result = tokenize('{{ foo bar baz }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[2], 'ID', 'bar');
shouldBeToken(result[3], 'ID', 'baz');
});
it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function () {
var result = tokenize('{{ foo bar "baz" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], 'STRING', 'baz');
});
it('tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"', function () {
var result = tokenize("{{ foo bar 'baz' }}");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], 'STRING', 'baz');
});
it('tokenizes String params with spaces inside as "STRING"', function () {
var result = tokenize('{{ foo bar "baz bat" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], 'STRING', 'baz bat');
});
it('tokenizes String params with escapes quotes as STRING', function () {
var result = tokenize('{{ foo "bar\\"baz" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[2], 'STRING', 'bar"baz');
});
it('tokenizes String params using single quotes with escapes quotes as STRING', function () {
var result = tokenize("{{ foo 'bar\\'baz' }}");
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[2], 'STRING', "bar'baz");
});
it('tokenizes numbers', function () {
var result = tokenize('{{ foo 1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], 'NUMBER', '1');
result = tokenize('{{ foo 1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], 'NUMBER', '1.1');
result = tokenize('{{ foo -1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], 'NUMBER', '-1');
result = tokenize('{{ foo -1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], 'NUMBER', '-1.1');
});
it('tokenizes booleans', function () {
var result = tokenize('{{ foo true }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], 'BOOLEAN', 'true');
result = tokenize('{{ foo false }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], 'BOOLEAN', 'false');
});
it('tokenizes undefined and null', function () {
var result = tokenize('{{ foo undefined null }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'UNDEFINED', 'NULL', 'CLOSE']);
shouldBeToken(result[2], 'UNDEFINED', 'undefined');
shouldBeToken(result[3], 'NULL', 'null');
});
it('tokenizes hash arguments', function () {
var result = tokenize('{{ foo bar=baz }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize('{{ foo bar baz=bat }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'ID',
'CLOSE',
]);
result = tokenize('{{ foo bar baz=1 }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'NUMBER',
'CLOSE',
]);
result = tokenize('{{ foo bar baz=true }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'BOOLEAN',
'CLOSE',
]);
result = tokenize('{{ foo bar baz=false }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'BOOLEAN',
'CLOSE',
]);
result = tokenize('{{ foo bar\n baz=bat }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'ID',
'CLOSE',
]);
result = tokenize('{{ foo bar baz="bat" }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'STRING',
'CLOSE',
]);
result = tokenize('{{ foo bar baz="bat" bam=wot }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'STRING',
'ID',
'EQUALS',
'ID',
'CLOSE',
]);
result = tokenize('{{foo omg bar=baz bat="bam"}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'ID',
'EQUALS',
'ID',
'ID',
'EQUALS',
'STRING',
'CLOSE',
]);
shouldBeToken(result[2], 'ID', 'omg');
});
it('tokenizes special @ identifiers', function () {
var result = tokenize('{{ @foo }}');
shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[2], 'ID', 'foo');
result = tokenize('{{ foo @bar }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[3], 'ID', 'bar');
result = tokenize('{{ foo bar=@baz }}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'ID',
'EQUALS',
'DATA',
'ID',
'CLOSE',
]);
shouldBeToken(result[5], 'ID', 'baz');
});
it('does not time out in a mustache with a single } followed by EOF', function () {
shouldMatchTokens(tokenize('{{foo}'), ['OPEN', 'ID']);
});
it('does not time out in a mustache when invalid ID characters are used', function () {
shouldMatchTokens(tokenize('{{foo & }}'), ['OPEN', 'ID']);
});
it('tokenizes subexpressions', function () {
var result = tokenize('{{foo (bar)}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'OPEN_SEXPR',
'ID',
'CLOSE_SEXPR',
'CLOSE',
]);
shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[3], 'ID', 'bar');
result = tokenize('{{foo (a-x b-y)}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'OPEN_SEXPR',
'ID',
'ID',
'CLOSE_SEXPR',
'CLOSE',
]);
shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[3], 'ID', 'a-x');
shouldBeToken(result[4], 'ID', 'b-y');
});
it('tokenizes nested subexpressions', function () {
var result = tokenize('{{foo (bar (lol rofl)) (baz)}}');
shouldMatchTokens(result, [
'OPEN',
'ID',
'OPEN_SEXPR',
'ID',
'OPEN_SEXPR',
'ID',
'ID',
'CLOSE_SEXPR',
'CLOSE_SEXPR',
'OPEN_SEXPR',
'ID',
'CLOSE_SEXPR',
'CLOSE',
]);
shouldBeToken(result[3], 'ID', 'bar');
shouldBeToken(result[5], 'ID', 'lol');
shouldBeToken(result[6], 'ID', 'rofl');
shouldBeToken(result[10], 'ID', 'baz');
});
it('tokenizes nested subexpressions: literals', function () {
var result = tokenize(
'{{foo (bar (lol true) false) (baz 1) (blah \'b\') (blorg "c")}}'
);
shouldMatchTokens(result, [
'OPEN',
'ID',
'OPEN_SEXPR',
'ID',
'OPEN_SEXPR',
'ID',
'BOOLEAN',
'CLOSE_SEXPR',
'BOOLEAN',
'CLOSE_SEXPR',
'OPEN_SEXPR',
'ID',
'NUMBER',
'CLOSE_SEXPR',
'OPEN_SEXPR',
'ID',
'STRING',
'CLOSE_SEXPR',
'OPEN_SEXPR',
'ID',
'STRING',
'CLOSE_SEXPR',
'CLOSE',
]);
});
it('tokenizes block params', function () {
var result = tokenize('{{#foo as |bar|}}');
shouldMatchTokens(result, [
'OPEN_BLOCK',
'ID',
'OPEN_BLOCK_PARAMS',
'ID',
'CLOSE_BLOCK_PARAMS',
'CLOSE',
]);
result = tokenize('{{#foo as |bar baz|}}');
shouldMatchTokens(result, [
'OPEN_BLOCK',
'ID',
'OPEN_BLOCK_PARAMS',
'ID',
'ID',
'CLOSE_BLOCK_PARAMS',
'CLOSE',
]);
result = tokenize('{{#foo as | bar baz |}}');
shouldMatchTokens(result, [
'OPEN_BLOCK',
'ID',
'OPEN_BLOCK_PARAMS',
'ID',
'ID',
'CLOSE_BLOCK_PARAMS',
'CLOSE',
]);
result = tokenize('{{#foo as as | bar baz |}}');
shouldMatchTokens(result, [
'OPEN_BLOCK',
'ID',
'ID',
'OPEN_BLOCK_PARAMS',
'ID',
'ID',
'CLOSE_BLOCK_PARAMS',
'CLOSE',
]);
result = tokenize('{{else foo as |bar baz|}}');
shouldMatchTokens(result, [
'OPEN_INVERSE_CHAIN',
'ID',
'OPEN_BLOCK_PARAMS',
'ID',
'ID',
'CLOSE_BLOCK_PARAMS',
'CLOSE',
]);
});
it('tokenizes raw blocks', function () {
var result = tokenize(
'{{{{a}}}} abc {{{{/a}}}} aaa {{{{a}}}} abc {{{{/a}}}}'
);
shouldMatchTokens(result, [
'OPEN_RAW_BLOCK',
'ID',
'CLOSE_RAW_BLOCK',
'CONTENT',
'END_RAW_BLOCK',
'CONTENT',
'OPEN_RAW_BLOCK',
'ID',
'CLOSE_RAW_BLOCK',
'CONTENT',
'END_RAW_BLOCK',
]);
});
});
-58
View File
@@ -1,58 +0,0 @@
<!doctype html>
<html>
<head>
<title>Handlebars Runtime UMD Smoke Test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Handlebars Runtime UMD Smoke Test</h1>
<pre id="results"></pre>
<script src="/spec/vendor/require.js"></script>
<script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) {
tests++;
if (!condition) {
failures++;
results.textContent += 'FAIL: ' + message + '\n';
} else {
results.textContent += 'PASS: ' + message + '\n';
}
}
requirejs.config({
paths: {
'handlebars.runtime': '/dist/handlebars.runtime',
},
});
require(['handlebars.runtime'], function (Handlebars) {
try {
assert(
typeof Handlebars !== 'undefined',
'Handlebars runtime loaded via RequireJS'
);
assert(
typeof Handlebars.template === 'function',
'Handlebars.template exists'
);
assert(
typeof Handlebars.VERSION === 'string',
'Handlebars.VERSION exists'
);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
}
results.textContent +=
'\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
});
</script>
</body>
</html>
-66
View File
@@ -1,66 +0,0 @@
<!doctype html>
<html>
<head>
<title>Handlebars UMD Module Smoke Test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>Handlebars UMD Module Smoke Test</h1>
<pre id="results"></pre>
<script src="/spec/vendor/require.js"></script>
<script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) {
tests++;
if (!condition) {
failures++;
results.textContent += 'FAIL: ' + message + '\n';
} else {
results.textContent += 'PASS: ' + message + '\n';
}
}
requirejs.config({
paths: {
handlebars: '/dist/handlebars',
},
});
require(['handlebars'], function (Handlebars) {
try {
assert(
typeof Handlebars !== 'undefined',
'Handlebars loaded via RequireJS'
);
assert(
typeof Handlebars.compile === 'function',
'Handlebars.compile exists'
);
assert(
typeof Handlebars.template === 'function',
'Handlebars.template exists'
);
assert(
typeof Handlebars.VERSION === 'string',
'Handlebars.VERSION exists'
);
var template = Handlebars.compile('Hello {{name}}!');
var output = template({ name: 'UMD' });
assert(output === 'Hello UMD!', 'Basic compilation works: ' + output);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
}
results.textContent +=
'\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
});
</script>
</body>
</html>
-2054
View File
File diff suppressed because it is too large Load Diff
+12 -19
View File
@@ -1,10 +1,10 @@
/* eslint-disable no-console */
const fs = require('fs');
const { S3, PutObjectCommand } = require('@aws-sdk/client-s3');
const git = require('./util/git');
const semver = require('semver');
import fs from 'fs';
import { S3, PutObjectCommand } from '@aws-sdk/client-s3';
import * as git from './util/git.js';
import semver from 'semver';
const PUBLISHED_FILES = [
export const PUBLISHED_FILES = [
'handlebars.js',
'handlebars.min.js',
'handlebars.runtime.js',
@@ -29,7 +29,7 @@ async function main() {
}
}
function buildSuffixes(commitInfo) {
export function buildSuffixes(commitInfo) {
const suffixes = [];
if (commitInfo.isMaster) {
@@ -44,7 +44,7 @@ function buildSuffixes(commitInfo) {
return suffixes;
}
function validateS3Env() {
export function validateS3Env() {
const bucket = process.env.S3_BUCKET_NAME,
region = process.env.S3_REGION,
key = process.env.S3_ACCESS_KEY_ID,
@@ -55,7 +55,7 @@ function validateS3Env() {
}
}
async function publish(suffixes, overrides) {
export async function publish(suffixes, overrides) {
const publishPromises = suffixes.map((suffix) =>
publishSuffix(suffix, overrides)
);
@@ -98,24 +98,17 @@ function getS3Client() {
return s3Client;
}
function getNameInBucket(filename, suffix) {
export function getNameInBucket(filename, suffix) {
return filename.replace(/\.js$/, suffix + '.js');
}
function getLocalFile(filename) {
export function getLocalFile(filename) {
return 'dist/' + filename;
}
module.exports = {
PUBLISHED_FILES,
buildSuffixes,
validateS3Env,
publish,
getNameInBucket,
getLocalFile,
};
import { fileURLToPath } from 'url';
if (require.main === module) {
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((err) => {
console.error(err);
process.exit(1);
+28 -113
View File
@@ -1,9 +1,9 @@
const fs = require('fs');
const { exec } = require('child_process');
const { execCommand, FileTestHelper } = require('cli-testlab');
const Handlebars = require('../../lib');
import fs from 'fs';
import { exec } from 'child_process';
import { execCommand, FileTestHelper } from 'cli-testlab';
import Handlebars from '../../lib/index.js';
const cli = 'node ./bin/handlebars.mjs';
const cli = 'node ./bin/handlebars.js';
expect.extend({
toEqualWithRelaxedSpace(received, expected) {
@@ -58,71 +58,22 @@ describe('bin/handlebars', function () {
});
});
describe('AMD output', function () {
it('-a produces AMD output', async function () {
const result = await execCommand(
`${cli} -a spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.js')
);
});
it('-a -s produces simple AMD output', async function () {
const result = await execCommand(
`${cli} -a -s spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.simple.js')
);
});
it('-a -m produces minified AMD output', async function () {
const result = await execCommand(
`${cli} -a -m spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.min.js')
);
});
});
describe('CommonJS output', function () {
it('-c produces CommonJS output', async function () {
const result = await execCommand(
`${cli} spec/artifacts/empty.handlebars -c`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.common.js')
);
});
});
describe('namespace', function () {
it('-n sets custom namespace', async function () {
const result = await execCommand(
`${cli} -a -n CustomNamespace.templates spec/artifacts/empty.handlebars`
`${cli} -n CustomNamespace.templates spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.namespace.js')
expectedFile('./spec/expected/empty.namespace.js')
);
});
it('--namespace sets custom namespace', async function () {
const result = await execCommand(
`${cli} -a --namespace CustomNamespace.templates spec/artifacts/empty.handlebars`
`${cli} --namespace CustomNamespace.templates spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.namespace.js')
);
});
it('multiple files share a namespace', async function () {
const result = await execCommand(
`${cli} spec/artifacts/empty.handlebars spec/artifacts/empty.handlebars -a -n someNameSpace`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/namespace.amd.js')
expectedFile('./spec/expected/empty.namespace.js')
);
});
});
@@ -144,14 +95,12 @@ describe('bin/handlebars', function () {
files.registerForCleanup(outputFile);
await execCommand(
`${cli} -a -f ${outputFile} spec/artifacts/empty.handlebars`
`${cli} -f ${outputFile} spec/artifacts/empty.handlebars`
);
expect(files.fileExists(outputFile)).toBe(true);
const content = files.getFileTextContent(outputFile);
expect(content).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.amd.js')
);
expect(content).toMatch(/template\(/);
});
it('--map writes source map and appends sourceMappingURL', async function () {
@@ -159,7 +108,7 @@ describe('bin/handlebars', function () {
files.registerForCleanup(mapFile);
const result = await execCommand(
`${cli} -i "<div>1</div>" -a -m -N test --map ${mapFile}`
`${cli} -i "<div>1</div>" -m -N test --map ${mapFile}`
);
expect(result.stdout).toContain('sourceMappingURL=');
@@ -174,75 +123,52 @@ describe('bin/handlebars', function () {
describe('template options', function () {
it('-e sets custom extension', async function () {
const result = await execCommand(
`${cli} -a -e hbs ./spec/artifacts/non.default.extension.hbs`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/non.default.extension.amd.js')
`${cli} -e hbs ./spec/artifacts/non.default.extension.hbs`
);
expect(result.stdout).toMatch(/template\(/);
});
it('-p compiles as partial', async function () {
const result = await execCommand(
`${cli} -a -p ./spec/artifacts/partial.template.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/partial.template.js')
`${cli} -p ./spec/artifacts/partial.template.handlebars`
);
expect(result.stdout).toMatch(/Handlebars\.partials/);
});
it('-r strips root from template names', async function () {
const result = await execCommand(
`${cli} spec/artifacts/partial.template.handlebars -r spec -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.root.amd.js')
`${cli} spec/artifacts/partial.template.handlebars -r spec`
);
expect(result.stdout).toMatch(/template\(/);
});
it('-b strips BOM', async function () {
const result = await execCommand(
`${cli} ./spec/artifacts/bom.handlebars -b -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/bom.amd.js')
);
});
it('-h sets custom handlebar path', async function () {
const result = await execCommand(
`${cli} spec/artifacts/empty.handlebars -h some-path/ -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/handlebar.path.amd.js')
`${cli} ./spec/artifacts/bom.handlebars -b`
);
expect(result.stdout).toMatch(/template\(/);
});
it('-k -o sets known helpers only', async function () {
const result = await execCommand(
`${cli} spec/artifacts/known.helpers.handlebars -a -k someHelper -k anotherHelper -o`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/non.empty.amd.known.helper.js')
`${cli} spec/artifacts/known.helpers.handlebars -k someHelper -k anotherHelper -o`
);
expect(result.stdout).toMatch(/template\(/);
});
});
describe('inline templates', function () {
it('-i compiles inline template', async function () {
const result = await execCommand(
`${cli} -i "<div>hello</div>" -a -N myTemplate`
`${cli} -i "<div>hello</div>" -N myTemplate`
);
expect(result.stdout).toContain("define(['handlebars.runtime']");
expect(result.stdout).toContain("templates['myTemplate']");
});
it('-i compiles multiple inline templates with -N', async function () {
const result = await execCommand(
`${cli} -i "<div>1</div>" -i "<div>2</div>" -N firstTemplate -N secondTemplate -a`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.name.amd.js')
);
it('-i compiles simple unnamed inline template', async function () {
const result = await execCommand(`${cli} -i "<div>hello</div>"`);
// Unnamed single template defaults to simple mode
expect(result.stdout).toMatch(/function/);
});
});
@@ -250,7 +176,7 @@ describe('bin/handlebars', function () {
it('should not produce unhandled promise rejections on async errors', async function () {
const { stderr } = await new Promise((resolve) => {
exec(
`node --unhandled-rejections=warn ./bin/handlebars.mjs -i "<div>test</div>" -N test --map /nonexistent/dir/test.map`,
`node --unhandled-rejections=warn ./bin/handlebars.js -i "<div>test</div>" -N test --map /nonexistent/dir/test.map`,
(error, stdout, stderr) => {
resolve({ exitCode: error ? error.code : 0, stderr });
}
@@ -260,15 +186,4 @@ describe('bin/handlebars', function () {
expect(stderr).not.toMatch(/unhandled/i);
});
});
describe('negated boolean flags', function () {
it('--no-amd negates --amd (issue #1673)', async function () {
const result = await execCommand(
`${cli} --amd --no-amd spec/artifacts/empty.handlebars`
);
expect(result.stdout).toEqualWithRelaxedSpace(
expectedFile('./spec/expected/empty.common.js')
);
});
});
});
+2 -4
View File
@@ -1,10 +1,10 @@
const http = require('http');
import http from 'http';
/**
* Minimal in-memory S3-compatible server for testing.
* Supports PutObject and GetObject via path-style requests.
*/
function createFakeS3() {
export function createFakeS3() {
const buckets = new Map();
const server = http.createServer((req, res) => {
@@ -77,5 +77,3 @@ function parsePath(url) {
key: parts.slice(1).join('/') || null,
};
}
module.exports = { createFakeS3 };
+5 -6
View File
@@ -1,9 +1,8 @@
const os = require('os');
const path = require('path');
const fs = require('fs-extra');
const { spawnSync } = require('child_process');
const git = require('../util/git');
import os from 'os';
import path from 'path';
import fs from 'fs-extra';
import { spawnSync } from 'child_process';
import * as git from '../util/git.js';
const tmpBaseDir = path.join(os.tmpdir(), 'handlebars-task-tests');
const tmpDir = path.join(tmpBaseDir, Date.now().toString(36));
+5 -5
View File
@@ -1,17 +1,17 @@
const fs = require('fs');
const { S3, GetObjectCommand } = require('@aws-sdk/client-s3');
const { createFakeS3 } = require('./fake-s3');
import fs from 'fs';
import { S3, GetObjectCommand } from '@aws-sdk/client-s3';
import { createFakeS3 } from './fake-s3.js';
const BUCKET = 'test-bucket';
const {
import {
PUBLISHED_FILES,
buildSuffixes,
validateS3Env,
publish,
getNameInBucket,
getLocalFile,
} = require('../publish-to-aws');
} from '../publish-to-aws.js';
let server;
let s3Client;
-17
View File
@@ -1,17 +0,0 @@
const childProcess = require('child_process');
module.exports = {
execNodeJsScriptWithInheritedOutput,
};
async function execNodeJsScriptWithInheritedOutput(command, args) {
return new Promise((resolve, reject) => {
const child = childProcess.fork(command, args, { stdio: 'inherit' });
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Child process failed with exit-code ${code}`));
}
resolve();
});
});
}
+29 -26
View File
@@ -1,30 +1,33 @@
const childProcess = require('child_process');
import childProcess from 'child_process';
module.exports = {
async remotes() {
return git('remote', '-v');
},
async branches() {
return git('branch', '-a');
},
async commitInfo() {
const headSha = await getHeadSha();
const masterSha = await getMasterSha();
return {
headSha,
masterSha,
tagName: await getTagName(),
isMaster: headSha === masterSha,
};
},
async add(path) {
return git('add', '-f', path);
},
async commit(message) {
return git('commit', '--message', message);
},
git, // visible for testing
};
export async function remotes() {
return git('remote', '-v');
}
export async function branches() {
return git('branch', '-a');
}
export async function commitInfo() {
const headSha = await getHeadSha();
const masterSha = await getMasterSha();
return {
headSha,
masterSha,
tagName: await getTagName(),
isMaster: headSha === masterSha,
};
}
export async function add(path) {
return git('add', '-f', path);
}
export async function commit(message) {
return git('commit', '--message', message);
}
export { git };
async function getHeadSha() {
const stdout = await git('rev-parse', '--short', 'HEAD');
+7 -7
View File
@@ -1,7 +1,7 @@
const fs = require('fs');
const { execSync } = require('child_process');
const git = require('./util/git');
const semver = require('semver');
import fs from 'fs';
import { execSync } from 'child_process';
import * as git from './util/git.js';
import semver from 'semver';
async function main() {
const version = process.argv[2];
@@ -50,16 +50,16 @@ async function main() {
execSync('npm run build', { stdio: 'inherit' });
}
async function replaceAndAdd(filePath, regex, value) {
export async function replaceAndAdd(filePath, regex, value) {
let content = fs.readFileSync(filePath, 'utf8');
content = content.replace(regex, value);
fs.writeFileSync(filePath, content);
await git.add(filePath);
}
module.exports = { replaceAndAdd };
import { fileURLToPath } from 'url';
if (require.main === module) {
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
+52
View File
@@ -0,0 +1,52 @@
{
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "Hello ",
"value": "Hello ",
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 6 }
}
},
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "name",
"tail": [],
"parts": ["name"],
"original": "name",
"loc": {
"start": { "line": 1, "column": 8 },
"end": { "line": 1, "column": 12 }
}
},
"params": [],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 6 },
"end": { "line": 1, "column": 14 }
}
},
{
"type": "ContentStatement",
"original": "!",
"value": "!",
"loc": {
"start": { "line": 1, "column": 14 },
"end": { "line": 1, "column": 15 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 15 }
}
}
+1
View File
@@ -0,0 +1 @@
Hello {{name}}!
+88
View File
@@ -0,0 +1,88 @@
{
"type": "Program",
"body": [
{
"type": "BlockStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "if",
"tail": [],
"parts": ["if"],
"original": "if",
"loc": {
"start": { "line": 1, "column": 3 },
"end": { "line": 1, "column": 5 }
}
},
"params": [
{
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "active",
"tail": [],
"parts": ["active"],
"original": "active",
"loc": {
"start": { "line": 1, "column": 6 },
"end": { "line": 1, "column": 12 }
}
}
],
"program": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "yes",
"value": "yes",
"loc": {
"start": { "line": 1, "column": 14 },
"end": { "line": 1, "column": 17 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 14 },
"end": { "line": 1, "column": 17 }
}
},
"inverse": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "no",
"value": "no",
"loc": {
"start": { "line": 1, "column": 25 },
"end": { "line": 1, "column": 27 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 25 },
"end": { "line": 1, "column": 27 }
}
},
"openStrip": { "open": false, "close": false },
"inverseStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 34 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 34 }
}
}
+1
View File
@@ -0,0 +1 @@
{{#if active}}yes{{else}}no{{/if}}
+44
View File
@@ -0,0 +1,44 @@
{
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "helper",
"tail": [],
"parts": ["helper"],
"original": "helper",
"loc": {
"start": { "line": 1, "column": 2 },
"end": { "line": 1, "column": 8 }
}
},
"params": [
{
"type": "NumberLiteral",
"value": 42,
"original": 42,
"loc": {
"start": { "line": 1, "column": 9 },
"end": { "line": 1, "column": 11 }
}
}
],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 13 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 13 }
}
}
+1
View File
@@ -0,0 +1 @@
{{helper 42}}
+53
View File
@@ -0,0 +1,53 @@
{
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "helper",
"tail": [],
"parts": ["helper"],
"original": "helper",
"loc": {
"start": { "line": 1, "column": 2 },
"end": { "line": 1, "column": 8 }
}
},
"params": [
{
"type": "BooleanLiteral",
"value": true,
"original": true,
"loc": {
"start": { "line": 1, "column": 9 },
"end": { "line": 1, "column": 13 }
}
},
{
"type": "BooleanLiteral",
"value": false,
"original": false,
"loc": {
"start": { "line": 1, "column": 14 },
"end": { "line": 1, "column": 19 }
}
}
],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 21 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 21 }
}
}
+1
View File
@@ -0,0 +1 @@
{{helper true false}}
+44
View File
@@ -0,0 +1,44 @@
{
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "helper",
"tail": [],
"parts": ["helper"],
"original": "helper",
"loc": {
"start": { "line": 1, "column": 2 },
"end": { "line": 1, "column": 8 }
}
},
"params": [
{
"type": "StringLiteral",
"value": "str",
"original": "str",
"loc": {
"start": { "line": 1, "column": 9 },
"end": { "line": 1, "column": 14 }
}
}
],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 16 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 16 }
}
}
+1
View File
@@ -0,0 +1 @@
{{helper "str"}}
+34
View File
@@ -0,0 +1,34 @@
{
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "foo",
"tail": ["bar", "baz"],
"parts": ["foo", "bar", "baz"],
"original": "foo.bar.baz",
"loc": {
"start": { "line": 1, "column": 2 },
"end": { "line": 1, "column": 13 }
}
},
"params": [],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 15 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 15 }
}
}
+1
View File
@@ -0,0 +1 @@
{{foo.bar.baz}}
+34
View File
@@ -0,0 +1,34 @@
{
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 2,
"head": "parent",
"tail": [],
"parts": ["parent"],
"original": "../../parent",
"loc": {
"start": { "line": 1, "column": 2 },
"end": { "line": 1, "column": 14 }
}
},
"params": [],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 16 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 16 }
}
}
+1
View File
@@ -0,0 +1 @@
{{../../parent}}
+69
View File
@@ -0,0 +1,69 @@
{
"type": "Program",
"body": [
{
"type": "BlockStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "each",
"tail": [],
"parts": ["each"],
"original": "each",
"loc": {
"start": { "line": 1, "column": 3 },
"end": { "line": 1, "column": 7 }
}
},
"params": [
{
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "items",
"tail": [],
"parts": ["items"],
"original": "items",
"loc": {
"start": { "line": 1, "column": 8 },
"end": { "line": 1, "column": 13 }
}
}
],
"program": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "it",
"value": "it",
"loc": {
"start": { "line": 1, "column": 29 },
"end": { "line": 1, "column": 31 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 29 },
"end": { "line": 1, "column": 31 }
},
"blockParams": ["item", "idx"]
},
"openStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 40 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 40 }
}
}
+1
View File
@@ -0,0 +1 @@
{{#each items as |item idx|}}it{{/each}}
+63
View File
@@ -0,0 +1,63 @@
{
"type": "Program",
"body": [
{
"type": "DecoratorBlock",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "inline",
"tail": [],
"parts": ["inline"],
"original": "inline",
"loc": {
"start": { "line": 1, "column": 4 },
"end": { "line": 1, "column": 10 }
}
},
"params": [
{
"type": "StringLiteral",
"value": "foo",
"original": "foo",
"loc": {
"start": { "line": 1, "column": 11 },
"end": { "line": 1, "column": 16 }
}
}
],
"program": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "bar",
"value": "bar",
"loc": {
"start": { "line": 1, "column": 18 },
"end": { "line": 1, "column": 21 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 18 },
"end": { "line": 1, "column": 21 }
}
},
"openStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 32 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 32 }
}
}
+1
View File
@@ -0,0 +1 @@
{{#*inline "foo"}}bar{{/inline}}
+52
View File
@@ -0,0 +1,52 @@
{
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "before ",
"value": "before ",
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 7 }
}
},
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "name",
"tail": [],
"parts": ["name"],
"original": "name",
"loc": {
"start": { "line": 1, "column": 9 },
"end": { "line": 1, "column": 13 }
}
},
"params": [],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 7 },
"end": { "line": 1, "column": 15 }
}
},
{
"type": "ContentStatement",
"original": " after",
"value": " after",
"loc": {
"start": { "line": 1, "column": 15 },
"end": { "line": 1, "column": 21 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 21 }
}
}
+1
View File
@@ -0,0 +1 @@
before {{name}} after
+52
View File
@@ -0,0 +1,52 @@
{
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "before ",
"value": "before ",
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 7 }
}
},
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "name",
"tail": [],
"parts": ["name"],
"original": "name",
"loc": {
"start": { "line": 1, "column": 9 },
"end": { "line": 1, "column": 13 }
}
},
"params": [],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 7 },
"end": { "line": 1, "column": 15 }
}
},
{
"type": "ContentStatement",
"original": " after",
"value": " after",
"loc": {
"start": { "line": 1, "column": 15 },
"end": { "line": 1, "column": 21 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 21 }
}
}
+1
View File
@@ -0,0 +1 @@
before {{name}} after
+68
View File
@@ -0,0 +1,68 @@
{
"type": "Program",
"body": [
{
"type": "BlockStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "each",
"tail": [],
"parts": ["each"],
"original": "each",
"loc": {
"start": { "line": 1, "column": 3 },
"end": { "line": 1, "column": 7 }
}
},
"params": [
{
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "items",
"tail": [],
"parts": ["items"],
"original": "items",
"loc": {
"start": { "line": 1, "column": 8 },
"end": { "line": 1, "column": 13 }
}
}
],
"program": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "item",
"value": "item",
"loc": {
"start": { "line": 1, "column": 15 },
"end": { "line": 1, "column": 19 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 15 },
"end": { "line": 1, "column": 19 }
}
},
"openStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 28 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 28 }
}
}
+1
View File
@@ -0,0 +1 @@
{{#each items}}item{{/each}}
+68
View File
@@ -0,0 +1,68 @@
{
"type": "Program",
"body": [
{
"type": "BlockStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "each",
"tail": [],
"parts": ["each"],
"original": "each",
"loc": {
"start": { "line": 1, "column": 3 },
"end": { "line": 1, "column": 7 }
}
},
"params": [
{
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "items",
"tail": [],
"parts": ["items"],
"original": "items",
"loc": {
"start": { "line": 1, "column": 8 },
"end": { "line": 1, "column": 13 }
}
}
],
"program": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "item",
"value": "item",
"loc": {
"start": { "line": 1, "column": 15 },
"end": { "line": 1, "column": 19 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 15 },
"end": { "line": 1, "column": 19 }
}
},
"openStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 28 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 28 }
}
}
+1
View File
@@ -0,0 +1 @@
{{#each items}}item{{/each}}
+53
View File
@@ -0,0 +1,53 @@
{
"type": "Program",
"body": [
{
"type": "BlockStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "inverted",
"tail": [],
"parts": ["inverted"],
"original": "inverted",
"loc": {
"start": { "line": 1, "column": 3 },
"end": { "line": 1, "column": 11 }
}
},
"params": [],
"inverse": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "body",
"value": "body",
"loc": {
"start": { "line": 1, "column": 13 },
"end": { "line": 1, "column": 17 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 13 },
"end": { "line": 1, "column": 17 }
}
},
"openStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 30 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 30 }
}
}
+1
View File
@@ -0,0 +1 @@
{{^inverted}}body{{/inverted}}
+34
View File
@@ -0,0 +1,34 @@
{
"type": "Program",
"body": [
{
"type": "PartialStatement",
"name": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "partial",
"tail": [],
"parts": ["partial"],
"original": "partial",
"loc": {
"start": { "line": 1, "column": 4 },
"end": { "line": 1, "column": 11 }
}
},
"params": [],
"indent": "",
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 13 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 13 }
}
}
+1
View File
@@ -0,0 +1 @@
{{> partial}}
+53
View File
@@ -0,0 +1,53 @@
{
"type": "Program",
"body": [
{
"type": "PartialBlockStatement",
"name": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "layout",
"tail": [],
"parts": ["layout"],
"original": "layout",
"loc": {
"start": { "line": 1, "column": 5 },
"end": { "line": 1, "column": 11 }
}
},
"params": [],
"program": {
"type": "Program",
"body": [
{
"type": "ContentStatement",
"original": "body",
"value": "body",
"loc": {
"start": { "line": 1, "column": 13 },
"end": { "line": 1, "column": 17 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 13 },
"end": { "line": 1, "column": 17 }
}
},
"openStrip": { "open": false, "close": false },
"closeStrip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 28 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 28 }
}
}
+1
View File
@@ -0,0 +1 @@
{{#> layout}}body{{/layout}}
+80
View File
@@ -0,0 +1,80 @@
{
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "helper",
"tail": [],
"parts": ["helper"],
"original": "helper",
"loc": {
"start": { "line": 1, "column": 2 },
"end": { "line": 1, "column": 8 }
}
},
"params": [
{
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "param1",
"tail": [],
"parts": ["param1"],
"original": "param1",
"loc": {
"start": { "line": 1, "column": 9 },
"end": { "line": 1, "column": 15 }
}
}
],
"hash": {
"type": "Hash",
"pairs": [
{
"type": "HashPair",
"key": "key",
"value": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "value",
"tail": [],
"parts": ["value"],
"original": "value",
"loc": {
"start": { "line": 1, "column": 20 },
"end": { "line": 1, "column": 25 }
}
},
"loc": {
"start": { "line": 1, "column": 16 },
"end": { "line": 1, "column": 25 }
}
}
],
"loc": {
"start": { "line": 1, "column": 16 },
"end": { "line": 1, "column": 25 }
}
},
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 27 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 27 }
}
}
+1
View File
@@ -0,0 +1 @@
{{helper param1 key=value}}
+44
View File
@@ -0,0 +1,44 @@
{
"type": "Program",
"body": [
{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"this": false,
"data": false,
"depth": 0,
"head": "helper",
"tail": [],
"parts": ["helper"],
"original": "helper",
"loc": {
"start": { "line": 1, "column": 2 },
"end": { "line": 1, "column": 8 }
}
},
"params": [
{
"type": "NumberLiteral",
"value": 42,
"original": 42,
"loc": {
"start": { "line": 1, "column": 9 },
"end": { "line": 1, "column": 11 }
}
}
],
"escaped": true,
"strip": { "open": false, "close": false },
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 13 }
}
}
],
"strip": {},
"loc": {
"start": { "line": 1, "column": 0 },
"end": { "line": 1, "column": 13 }
}
}
+1
View File
@@ -0,0 +1 @@
{{helper 42}}

Some files were not shown because too many files have changed in this diff Show More