From dc6c9c990e5ea2b966db6e95a9a14d95f1ebd853 Mon Sep 17 00:00:00 2001 From: Graham Dixon Date: Tue, 10 Jul 2018 15:46:45 +0100 Subject: [PATCH] Initial commit --- .gitignore | 61 ++++++ LICENSE | 21 +++ lib/domvm-hbs-loader.js | 35 ++++ lib/domvm-hbs-runtime.js | 78 ++++++++ lib/domvm-hbs.js | 390 +++++++++++++++++++++++++++++++++++++++ package.json | 18 ++ readme.md | 125 +++++++++++++ 7 files changed, 728 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 lib/domvm-hbs-loader.js create mode 100644 lib/domvm-hbs-runtime.js create mode 100644 lib/domvm-hbs.js create mode 100644 package.json create mode 100644 readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03e00cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +public/app.js \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9450565 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Vincent Racine, Graham Dixon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/domvm-hbs-loader.js b/lib/domvm-hbs-loader.js new file mode 100644 index 0000000..3efc920 --- /dev/null +++ b/lib/domvm-hbs-loader.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018 Graham Dixon + * All rights reserved. (MIT Licensed) + * + * domvm-hbs-loader.js + * this package compiles handlebar templates to JSX at build time + * @preserve https://github.com/gdixon/domvm-hbs + */ + +// parser to convert hbs to hyperscript el calls +const domvm_hbs = require('domvm-hbs'); + +// export the hbs file as callable hyperscript +module.exports = function(content) { + // allow the response to be cached + this.cacheable = true; + + // options outside of the call + const options = { + 'pragma': "el", + raw: true + }; + + // precompile the hbs to jsx (cant bind context yet) + const precompile = domvm_hbs.compile(content, options); + + // locally scope the hbs runtime and jsx el function assigning the el fn to options + const locallyScoped = "var domvm_hbs_runtime = require('domvm-hbs/lib/domvm-hbs-runtime'); var el = require('domvm-jsx'); var options = {el: el};"; + + // compile the hbs template before allowing it to repsond to vm calls - inline options and pragma fn + return "module.exports = (function() { " + locallyScoped + " return " + precompile + ";})()"; +} + +// run separate from the module system +module.exports.seperable = true; diff --git a/lib/domvm-hbs-runtime.js b/lib/domvm-hbs-runtime.js new file mode 100644 index 0000000..835f404 --- /dev/null +++ b/lib/domvm-hbs-runtime.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2018 Vincent Racine, Graham Dixon + * All rights reserved. (MIT Licensed) + * + * domvm-hbs-runtime.js + * this package compiles handlebar templates to JSX at build time (forked from vincentracine/hyperbars) + * @preserve https://github.com/gdixon/domvm-hbs + */ + +module.exports = (function(domvm_hbs_runtime) { + 'use strict'; + + var domvm_hbs_runtime = {}; + + var isObject = function(a) { + return Object.prototype.toString.call(a) === '[object Object]' + }; + + domvm_hbs_runtime.prototype = { + + 'setup': function(obj) { + obj = obj || {}; + }, + + /** + * Register helpers + */ + 'registerHelper': function(name, handler) { + domvm_hbs_runtime.methods[name] = handler; + } + }; + + domvm_hbs_runtime.methods = { + 'if': function(context, expression, callback) { + if (expression.value) { + return callback(isObject(expression.value) ? expression.value : context, context); + } + return ""; + }, + 'unless': function(context, expression, callback) { + if (!expression.value) { + return callback(isObject(expression.value) ? expression.value : context, context); + } + return ""; + }, + 'each': function(context, expression, callback) { + return expression.value.map(function(item, index, array) { + var options = {}; + options['@index'] = index; + options['@first'] = index == 0; + options['@last'] = index == array.length - 1; + return callback(item, context, options) + }) + }, + /** + * credit: http://stackoverflow.com/a/8625261/5678694 + */ + 'merge': function() { + var obj = {}, + i = 0, + il = arguments.length, + key; + for (; i < il; i++) { + for (key in arguments[i]) { + if (arguments[i].hasOwnProperty(key)) { + obj[key] = arguments[i][key]; + } + } + } + return obj; + } + }; + + return domvm_hbs_runtime; + +})(function() { + this.debug = false; +}); diff --git a/lib/domvm-hbs.js b/lib/domvm-hbs.js new file mode 100644 index 0000000..ae178f1 --- /dev/null +++ b/lib/domvm-hbs.js @@ -0,0 +1,390 @@ +/** + * Copyright (c) 2018 Vincent Racine, Graham Dixon + * All rights reserved. (MIT Licensed) + * + * domvm-hbs.js + * this package compiles handlebar templates to JSX at build time (forked from vincentracine/hyperbars) + * @preserve https://github.com/gdixon/domvm-hbs + */ + +module.exports = (function(domvm_hbs) { + 'use strict'; + + var htmlparser = require("htmlparser2"); + + /** + * Parse handlebar template + * @param html + * @returns {Array} + */ + var parse = function(html) { + var tree = [], + current = null; + + // Create parser + var parser = new htmlparser.Parser({ + onopentag: function(name, attrs) { + var node = { + type: 'tag', + parent: current, + name: name, + attributes: attrs, + children: [] + }; + + if (current) { + current.children.push(node); + } else { + tree.push(node); + } + current = node; + }, + ontext: function(text) { + // Deal with adjacent blocks and expressions + var multiple = text.search(/{({[^{}]+})}/) > -1; + if (multiple) { + text = text.split(/{({[^{}]+})}/g); + text = text.map(function(item, index, array) { + if (!item || item == "") return undefined; + if (item == "{") return ""; + + if (item[0] == "{" && item.length > 1 && array[index + 1] != "}") { + item = "{" + item + "}"; + } + if (item[0] == "{" && item.length > 1 && array[index + 1] == "}") { + item = "{{" + item + "}}"; + text[index + 1] = ""; + } + + return item; + }).filter(function(item) { + return item != "{" || item != "}" + }); + } else { + text = [text]; + } + + text = text.filter(Boolean); + + text.forEach(function(text) { + var node = { + type: 'text', + content: text.replace(/'/g, "\\'") + }; + if (current) { + current.children.push(node); + } else { + tree.push(node); + } + }); + }, + onclosetag: function(tagname) { + current = current ? current.parent : null; + } + }, { + decodeEntities: true + }); + + // Initiate parsing + parser.write(html); + + // Stop parser + parser.end(); + + // Return parsed html tree + return tree; + }; + + domvm_hbs.prototype = { + + 'setup': function(obj) { + obj = obj || {}; + htmlparser = obj.htmlparser; + }, + + /** + * Compiles HTML to use with virtual-dom + * + * options params: + * | name | default | description + * --------------------------------- + * | debug | false | outputs the js to console + * | raw | false | returns the compiled function as a string + * + * @param template html + * @param options options + * @returns * compiled function + */ + 'compile': function(template, options, originalState) { + var partials = this.partials; + options = options || {}; + options.debug = options.debug || false; + options.raw = options.raw || false; + options.cache = options.cache || true; + options.pragma = options.pragma || "h"; + + // Remove special characters + template = template.replace(/> <') + .replace(/> {{/g, '>{{') + .replace(/}} ') return injectPartial(string); + var sanitised = string.replace(/(this).?/, '').replace(/..\//g, 'parent.'), + options = ""; + + if (string.indexOf('.') > -1 && string.indexOf('..') == -1) { + var dot = sanitised.indexOf('.'); + options = sanitised.slice(dot); + sanitised = sanitised.slice(0, dot); + } + + // Do not encode HTML + if (sanitised[0] == "{") { + sanitised = sanitised.slice(1); + return [ + "options." + options.pragma + "('div',{'innerHTML':", + "''+" + (sanitised.indexOf('parent') == 0 ? sanitised : "context['" + sanitised + "']" + options), + "}, [])" + ].join(''); + } + return "''+" + (sanitised.indexOf('parent') == 0 ? sanitised : "context['" + sanitised + "']" + options); + }; + + /** + * Places single quotes around a string. + * @param string + * @returns {string} + */ + var string2js = function(string) { + var open = string.indexOf('{{'), + close = string.indexOf('}}'), + value = string.slice(open + 2, close); + if (open != -1 && close != -1) { + return open > 0 ? "'" + string.slice(0, open) + "'+" + block2js(value) : block2js(value); + } else { + return "'" + string + "'" + } + }; + + /** + * Convert vnode to javascript + * @param vnode + * @returns {string} + */ + var node2js = function(vnode) { + if (!vnode.children || !vnode.children.length) { + vnode.children = '[]'; + } + return "options." + options.pragma + '(' + [string2js(vnode.name), vnode.attributes, vnode.children].join(',') + ')'; + }; + + /** + * Converts vtext node to javascript + * @param vtext + * @returns {*} + */ + var text2js = function(vtext) { + return string2js(vtext.content); + }; + + /** + * Converts handlebar expression to javascript + * @param expression + * @returns {*} + */ + var expression2js = function(expression) { + if (expression.indexOf('{{/') > -1) { + return ']})'; + } + + // Parse + expression = expression + .replace(/(this).?/, '') + .replace(/..\//g, 'parent.'); + + // Function extraction + var whitespace = expression.indexOf(' '), + fn = expression.slice(3, whitespace); + + // Attribute extraction + var regex = /([\S]+="[^"]*")/g, + parameters = expression + .substring(whitespace, expression.length) + .replace('}}', '') + .split(regex) + .filter(function(string) { + return !!string && string != " " + }) + .map(function(string) { + if (string.indexOf("=") > -1) { + var s = string.trim().split("="); + s[0] = block2js(s[0]); + if (s[1][0] != '"' && s[1].slice(-1) != '"') { + s[1] = block2js(s[1]); + if (s[1].indexOf("''+") == 0) { + s[1] = s[1].slice(3); + } + } + return `{ left: ${s[0]}, right: ${s[1]} }`; + } else { + string = block2js(string.trim()); + if (string.indexOf("''+") == 0) { + string = string.slice(3); + } + return string; + } + }); + return [ + "Runtime.", + fn, + "(context, " + "{ value: " + parameters + " }" + ", function(context, parent, options){return [" + ].join(''); + }; + + /** + * Converts attribute value to javascript + * @param attribute + * @returns {string} + */ + var attrs2js = function(attribute) { + attribute = attribute.replace(/'/g, "\\'"); + var blocks = attribute.split(/({{[^{}]+)}}/g); + blocks = blocks.map(function(block) { + return isHandlebarExpression(block) ? expression2js(block) : block.indexOf('{{') > -1 ? block2js(block.slice(2)) : "'" + block + "'" + }).join('+'); + return blocks.replace(/\[\+/g, "[").replace(/\[''\+/g, "[").replace(/\+['']*\]/g, "]"); + }; + + /** + * True is the argument contains handlebar expression + * @param string + * @returns {boolean} + */ + var isHandlebarExpression = function(string) { + return string.indexOf('{{#') > -1 || string.indexOf('{{/') > -1 + }; + + /** + * True is the argument contains handlebar expression + * @param string + * @returns {boolean} + */ + var isHandlebarBlock = function(string) { + return string.indexOf('{{') > -1 && string.indexOf('}}') > -1 + }; + + /** + * Converts vnode to javascript + * @param node + */ + var toJavaScript = function(node) { + if (node.children && node.children.length) { + node.children = [ + '[', node.children.map(toJavaScript).join(','), ']' + ].join('').replace(/return \[,/g, "return [").replace(/,\]}\)\]/g, "]})]"); + } + + if (node.attributes) { + node.attributes = [ + '{', + Object.keys(node.attributes).map(function(name) { + return [string2js(name), attrs2js(node.attributes[name])].join(':') + }).join(','), + '}' + ].join('') + } + + if (node.type == 'text') { + // Deal with handlebar expressions in text + if (isHandlebarExpression(node.content)) { + return expression2js(node.content); + } else { + return text2js(node); + } + } + + if (node.type == 'tag') { + return node2js(node); + } + }; + + // Parse handlebar template using htmlparser + var parsed = parse(template)[0]; + + // Convert to hyperscript + var fn = [ + '(function(vm, state) { var Runtime = domvm_hbs_runtime.methods; return function(){ var context = (state || originalState); return ', + toJavaScript(parsed), + '}.bind({})}.bind({}))' + ].join(''); + + // Remove those pesky line-breaks! + fn = fn.replace(/(\r\n|\n|\r)/gm, ""); + + if (options.debug || this.debug) { + console.log(fn); + } + + // function is currently a string so eval it and return it + return options.raw ? fn : eval(fn); + }, + + /** + * Dependencies + */ + 'htmlparser': htmlparser + }; + + return new domvm_hbs(); + +})(function() { + this.debug = false; + this.partials = {}; +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..d21bfbe --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "domvm-hbs", + "version": "1.0.1", + "description": "This package compiles handlebar templates to JSX at build time.", + "keywords": [ + "domvm", + "hbs" + ], + "author": "GDixon", + "repository": "github:gdixon/domvm-hbs", + "main": "lib/domvm-hbs.js", + "license": "MIT", + "dependencies": { + "domvm": "^3.3.3", + "domvm-jsx": "^1.0.0", + "htmlparser2": "^3.9.2" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..07c32cd --- /dev/null +++ b/readme.md @@ -0,0 +1,125 @@ +# domvm-hbs + + > This package compiles handlebar templates to JSX at build time. + +-------- + +## Using domvm and handlebars + +This package will take a handlebars template and convert it to JSX at build time allowing for the template to be constructed against domvm (or any virtual dom that supports JSX via a pragma call) and to be rendered at runtime against live state. + +While not all of domvm's features can be accommodated by JSX syntax (and therefore handlebar templates), it's possible to cover a fairly large subset via a defineElementSpread pragma. Please refer to demos and examples in the [JSX wiki](https://github.com/domvm/domvm/wiki/JSX). + +## Quick start + +- install via npm/yarn and set-up config to match the rest of this readme + ``` + npm install domvm-hbs --save + ``` + +## Combining domvm and handlebars with webpack and babel + +- package.json + ``` + ... + + "devDependencies": { + "webpack": "^3.8.1", + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-preset-env": "^1.6.1", + "babel-plugin-transform-react-jsx": "^6.24.1" + }, + "dependencies": { + "domvm": "^3.3.3", + "domvm-hbs": "^1.0.0" + } + + ... + ``` +- webpack.config.js + ``` + ... + + module: { + loaders: [ + { + test: /\.(jsx|js)$/, + loader: ['babel-loader'] + }, + { + test: /\.hbs$/, + loader: ['domvm-hbs/lib/domvm-hbs-loader'], + exclude: /node_modules/ + } + ] + } + + ... + ``` +- babel.rc + ``` + { + "presets": [ + "env" + ], + "plugins": [ + [ + "transform-react-jsx", + { + "pragma": "el" + } + ] + ] + } + ``` + +- template.hbs + ``` +
+

Handlebars test

+
+ Name: + {{#if profile}} + {{name}} + {{/if}} +
+
+ ``` + +- your-project-file.js + ``` + ... + + // inlcude a handlebars template (which will be converted to jsx on inclusion) + const template = require('template.hbs'); + + // create a view layer carrying the state to the template + const view = function(vm, state) { + + // apply the state to the hbs template's JSX function + return template(vm, state); + }; + + // state is provided to the template at runtime + let state = {profile: {name: "Foo bar"}}; + + // create a vm from the view + const vm = domvm.createView(view, state); + + // mount to the dom + vm.mount(document.getElementById('root')); + + ... + ``` + +## Acknowledgements + +This package is a fork from [vincentracine/hyperbars](https://github.com/vincentracine/hyperbars) - this package separates the core functionality from the runtime allowing for the templates to be compiled at build time and to be executed later. + + - [Leon Sorokin (leeoniya)](https://github.com/leeoniya) - Author of domvm + - [Vincent Racine (vincentracine)](https://github.com/vincentracine) - Author of hyperbars + +## License + +* [Licensed](https://github.com/gdixon/domvm-hbs/blob/master/LICENSE) under the MIT License (MIT).