Skip to content

Commit

Permalink
feat: add support for handlebars helpers in compile mode
Browse files Browse the repository at this point in the history
  • Loading branch information
webdiscus committed Dec 8, 2024
1 parent 9229d0a commit d71644f
Show file tree
Hide file tree
Showing 18 changed files with 195 additions and 20 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change log

## 4.10.0 (2024-12-08)

- feat: add support for handlebars helpers in compile mode

## 4.9.2 (2024-12-07)

- fix: Error Cannot find module 'nunjucks', introduced in `4.9.1`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "html-bundler-webpack-plugin",
"version": "4.9.2",
"version": "4.10.0",
"description": "Processing HTML templates and bundling assets. Build-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.",
"keywords": [
"html",
Expand Down
18 changes: 16 additions & 2 deletions src/Loader/Preprocessors/Handlebars/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const path = require('path');
const { stringifyJSON } = require('../../Utils');
const { stringifyJSON, stringifyFn } = require('../../Utils');
const { loadModule, readDirRecursiveSync } = require('../../../Common/FileUtils');
const { isWin, pathToPosix } = require('../../../Common/Helpers');
const LoaderFactory = require('../../LoaderFactory');
Expand Down Expand Up @@ -250,11 +250,25 @@ const preprocessor = (loaderContext, options) => {
export(precompiledTemplate, { data }) {
const exportFunctionName = 'templateFn';
const exportCode = 'module.exports=';
let precompiledHelpers = '';

if (options.helpers || Array.isArray(options.helpers)) {
const helpers = getHelpers(options.helpers);

for (let name in helpers) {
let helper = helpers[name];
let source = stringifyFn(helper);

precompiledHelpers += `
Handlebars.registerHelper('${name}', ${source});`;
}
}

return `
var Handlebars = require('${runtimeFile}');
var data = ${stringifyJSON(data)};
${precompiledTemplate};
${precompiledHelpers}
${precompiledTemplate}
var ${exportFunctionName} = (context) => {
var template = (Handlebars['default'] || Handlebars).template(precompiledTemplate);
return template(Object.assign({}, data, context));
Expand Down
23 changes: 19 additions & 4 deletions src/Loader/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,11 @@ const stringifyJSON = (data) => {
if (typeof value === 'function') {
value = value.toString().replace(/\n/g, '');

// transform `{ fn() {} }` to `{"fn":()=>{}}`
// transform `{ fn() {} }` to `{"fn":function(){}}`
const keySize = key.length;
if (key === value.slice(0, keySize)) {
const pos = value.indexOf(')', keySize + 1) + 1;
value = value.slice(keySize, pos) + '=>' + value.slice(pos).trimStart();
const pos = value.indexOf('(');
if (pos > 0 && value.slice(0, pos).trim() !== 'function') {
value = 'function' + value.slice(keySize);
}

value = quoteMark + value + quoteMark;
Expand All @@ -202,6 +202,20 @@ const stringifyJSON = (data) => {
: json || '{}';
};

const stringifyFn = (fn) => {
let value = fn.toString().replace(/\n/g, '');
let isArrowFunction = value.indexOf('=>', 1) > 0;

if (!isArrowFunction) {
const pos = value.indexOf('(');
if (pos > 0 && value.slice(0, pos).trim() !== 'function') {
value = 'function' + value.slice(pos);
}
}

return value;
};

module.exports = {
baseUri,
urlPathPrefix,
Expand All @@ -220,4 +234,5 @@ module.exports = {
escapeSequences,
escapeCodesForJSON,
stringifyJSON,
stringifyFn,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
root = true

[*.html]
insert_final_newline = true

[*.hbs]
insert_final_newline = true

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<!-- load the rendered template in JS and add it into HTML in runtime -->
<script src="app.js" defer="defer"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>Hello {{#bold}}{{helloName}}{{/bold}}!</p>
{{> 'footer'}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import tmpl from './app.hbs';

const locals = {
helloName: 'World',
};

document.getElementById('app').innerHTML = tmpl(locals);

console.log('>> app');
11 changes: 11 additions & 0 deletions test/cases/_preprocessor/js-tmpl-hbs-compile-helpers/src/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<!-- load the rendered template in JS and add it into HTML in runtime -->
<script src="app.js" defer="defer"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div>-= FOOTER =-</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const path = require('path');
const HtmlBundlerPlugin = require('@test/html-bundler-webpack-plugin');
const Handlebars = require('handlebars');

module.exports = {
mode: 'production',

output: {
path: path.join(__dirname, 'dist/'),
},

resolve: {
alias: {
'@images': path.join(__dirname, '../../../fixtures/images'),
},
},

plugins: [
new HtmlBundlerPlugin({
entry: {
index: './src/index.hbs',
},

preprocessor: 'handlebars',
preprocessorOptions: {
//knownHelpersOnly: false,
// define helpers manually
helpers: {
// WARNING: don't use the arrow function with `this` otherwise webpack compile `this` into `void 0`
//bold: (options) => new Handlebars.SafeString(`<strong>${options.fn(this)}</strong>`),

bold(options) {
return new Handlebars.SafeString(`<strong>${options.fn(this)}</strong>`);
},
},
partials: ['src/partials'],
},
}),
],

module: {
rules: [
{
test: /\.(png|svg|jpe?g|webp)$/i,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash:8][ext]',
},
},
],
},
};

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
<script src="app.js" defer="defer"></script>
</head>
<body>
<div id="main"></div>
<div id="app"></div>
</body>
</html>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ const locals = {
navText: 'Navigation',
};

document.getElementById('main').innerHTML = tmpl(locals);
document.getElementById('app').innerHTML = tmpl(locals);

console.log('>> app');
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
<script src="app.js" defer="defer"></script>
</head>
<body>
<div id="main"></div>
<div id="app"></div>
</body>
</html>
</html>
1 change: 1 addition & 0 deletions test/preprocessor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ describe('usage template in js on client side', () => {

// Handlebars
test('hbs: compile to fn', () => compareFiles('_preprocessor/js-tmpl-hbs-compile'));
test('hbs: compile to fn with helpers', () => compareFiles('_preprocessor/js-tmpl-hbs-compile-helpers'));
test('hbs: compile to fn with partials', () => compareFiles('_preprocessor/js-tmpl-hbs-compile-partials'));
test('hbs: compile to fn with variables', () => compareFiles('_preprocessor/js-tmpl-hbs-compile-variables'));

Expand Down
61 changes: 54 additions & 7 deletions test/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HtmlParser } from '../src/Common/HtmlParser';
import { isDir, loadModule, resolveFile, filterParentPaths, relativePathVerbose } from '../src/Common/FileUtils';
import {
stringifyJSON,
stringifyFn,
injectBeforeEndHead,
injectBeforeEndBody,
escapeSequences,
Expand Down Expand Up @@ -236,13 +237,6 @@ describe('file extension', () => {
});

describe('utils', () => {
test('stringifyJSON', () => {
const json = { fn() {} };
const received = stringifyJSON(json);
const expected = `{"fn":()=>{}}`;
return expect(received).toEqual(expected);
});

test('injectBeforeEndHead', () => {
const html = `<html><head><title>test</title></head><body><p>body</p></body></html>`;
const received = injectBeforeEndHead(html, `<script src="test.js"></script>`);
Expand Down Expand Up @@ -297,6 +291,59 @@ describe('utils', () => {
});
});

describe('stringifyJSON', () => {
test('{ fn() {} }', () => {
const json = { fn() {} };
const received = stringifyJSON(json);
const expected = `{"fn":function() {}}`;
return expect(received).toEqual(expected);
});

test('{ fn: () => {} }', () => {
const json = { fn: () => {} };
const received = stringifyJSON(json);
const expected = `{"fn":() => {}}`;
return expect(received).toEqual(expected);
});

test('{ fn: function() {} }', () => {
const json = { fn: function () {} };
const received = stringifyJSON(json);
const expected = `{"fn":function () {}}`;
return expect(received).toEqual(expected);
});
});

describe('stringifyFn', () => {
test('{ fn() {} }', () => {
const obj = { fn() {} };
const received = stringifyFn(obj.fn);
const expected = `function() {}`;
return expect(received).toEqual(expected);
});

test('{ fn: function() {} }', () => {
const obj = { fn: function () {} };
const received = stringifyFn(obj.fn);
const expected = `function () {}`;
return expect(received).toEqual(expected);
});

test('{ fn: () => {} }', () => {
const obj = { fn: () => {} };
const received = stringifyFn(obj.fn);
const expected = `() => {}`;
return expect(received).toEqual(expected);
});

test('{ fn: o => o.toString() }', () => {
const obj = { fn: (o) => o.toString() };
const received = stringifyFn(obj.fn);
const expected = `o => o.toString()`;
return expect(received).toEqual(expected);
});
});

describe('parse attributes in tag', () => {
test('parseTag img without src attr', () => {
const source = '<img alt="apple">';
Expand Down

0 comments on commit d71644f

Please sign in to comment.