Fixes #432 (safeHTML), also initial work for #401 (bind additional arguments)

This commit is contained in:
Patrick Steele-Idem 2016-11-14 18:43:00 -07:00
parent 3622cc4421
commit 0e9fe17735
70 changed files with 653 additions and 558 deletions

View File

@ -1,7 +1,8 @@
'use strict'; 'use strict';
var attr = require('../../../../runtime/html/attr'); var runtimeHtmlHelpers = require('../../../../runtime/html/helpers');
var escapeXmlAttr = require('../../../../runtime/html/escapeXml').attr; var attr = runtimeHtmlHelpers.a;
var escapeXmlAttr = runtimeHtmlHelpers.xa;
function isStringLiteral(node) { function isStringLiteral(node) {
return node.type === 'Literal' && typeof node.value === 'string'; return node.type === 'Literal' && typeof node.value === 'string';
@ -76,12 +77,11 @@ function generateCodeForExpressionAttr(name, value, escape, codegen) {
if (isStringLiteral(part)) { if (isStringLiteral(part)) {
part.value = escapeXmlAttr(part.value); part.value = escapeXmlAttr(part.value);
} else if (part.type === 'Literal') { } else if (part.type === 'Literal') {
} else if (isNoEscapeXml(part)) { } else if (isNoEscapeXml(part)) {
part = codegen.builder.functionCall(context.helper('str'), [part]); part = codegen.builder.functionCall(context.helper('str'), [part]);
} else { } else {
if (escape !== false) { if (escape !== false) {
part = codegen.builder.functionCall(context.helper('escapeXmlAttr'), [part]); part = builder.functionCall(context.helper('escapeXmlAttr'), [part]);
} }
} }
addHtml(part); addHtml(part);

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
var escapeXml = require('../../../../runtime/html/escapeXml'); var escapeXml = require('../../../../runtime/html/helpers').x;
var Literal = require('../..//Literal'); var Literal = require('../..//Literal');
module.exports = function(node, codegen) { module.exports = function(node, codegen) {

View File

@ -21,16 +21,6 @@ class TextVDOM extends Node {
vdomUtil.registerOptimizer(context); vdomUtil.registerOptimizer(context);
var args = this.arguments;
for (var i=0, len=args.length; i<len; i++) {
var arg = args[i];
if (arg.type !== 'Literal') {
this.strFuncId = context.helper('str');
break;
}
}
return this; return this;
} }
@ -70,7 +60,6 @@ class TextVDOM extends Node {
let escape = this.escape; let escape = this.escape;
var funcName = escape ? 't' : 'h'; var funcName = escape ? 't' : 'h';
var strFuncId = this.strFuncId;
function writeTextArgs() { function writeTextArgs() {
writer.write('('); writer.write('(');
@ -84,15 +73,7 @@ class TextVDOM extends Node {
writer.writeIndent(); writer.writeIndent();
} }
if (arg.type === 'Literal') { writer.write(arg);
writer.write(arg);
} else {
writer.write(strFuncId);
writer.write('(');
writer.write(arg);
writer.write(')');
}
} }
writer.write(')'); writer.write(')');

View File

@ -57,7 +57,6 @@ function generateNodesForArray(nodes, context, options) {
let nextNodeId = 0; let nextNodeId = 0;
let nextAttrsId = 0; let nextAttrsId = 0;
var optimizeTextNodes = options.optimizeTextNodes !== false;
var optimizeStaticNodes = options.optimizeStaticNodes !== false; var optimizeStaticNodes = options.optimizeStaticNodes !== false;
function generateStaticNode(node) { function generateStaticNode(node) {
@ -112,31 +111,6 @@ function generateNodesForArray(nodes, context, options) {
finalNodes.push(node); finalNodes.push(node);
} }
} else if (node.type === 'TextVDOM') {
if (optimizeTextNodes) {
let firstTextNode = node;
// We will need to merge the text nodes into a single node
while(++i<nodes.length) {
let currentTextNode = nodes[i];
if (currentTextNode.type === 'TextVDOM') {
if (!firstTextNode.append(currentTextNode)) {
// If the current text node was not appendable then
// we will stop. We can only merge text nodes that are compatible
break;
}
} else {
break;
}
}
// firstTextNode.isStatic = false;
finalNodes.push(firstTextNode);
continue;
} else {
finalNodes.push(node);
}
} else { } else {
finalNodes.push(node); finalNodes.push(node);
} }

View File

@ -91,7 +91,7 @@
"jsdom": "^9.6.0", "jsdom": "^9.6.0",
"jshint": "^2.5.0", "jshint": "^2.5.0",
"lasso": "^2.4.1", "lasso": "^2.4.1",
"lasso-marko": "^2.0.4", "lasso-marko": "^2.1.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mocha": "^2.3.3", "mocha": "^2.3.3",
"mocha-phantomjs": "^4.1.0", "mocha-phantomjs": "^4.1.0",

View File

@ -0,0 +1 @@
exports.document = typeof document != 'undefined' && document;

View File

@ -1,6 +1,5 @@
'use strict'; 'use strict';
var isArray = Array.isArray; var isArray = Array.isArray;
var load = require('./loader');
function classListHelper(arg, classNames) { function classListHelper(arg, classNames) {
var len; var len;
@ -93,141 +92,138 @@ LoopStatus.prototype = {
} }
}; };
module.exports = { /**
/** * Internal helper method to prevent null/undefined from being written out
* Internal helper method to prevent null/undefined from being written out * when writing text that resolves to null/undefined
* when writing text that resolves to null/undefined * @private
* @private */
*/ exports.s = function strHelper(str) {
s: function(str) { return (str == null) ? '' : str.toString();
return (str == null) ? '' : str.toString(); };
},
/** /**
* Internal helper method to handle loops with a status variable * Internal helper method to handle loops with a status variable
* @private * @private
*/ */
fv: function (array, callback) { exports.fv = function forEachStatusVariableHelper(array, callback) {
if (!array) { if (!array) {
return; return;
} }
if (!array.forEach) { if (!array.forEach) {
array = [array]; array = [array];
} }
var len = array.length; var len = array.length;
var loopStatus = new LoopStatus(len); var loopStatus = new LoopStatus(len);
for (; loopStatus.i < len; loopStatus.i++) { for (; loopStatus.i < len; loopStatus.i++) {
var o = array[loopStatus.i]; var o = array[loopStatus.i];
callback(o, loopStatus); callback(o, loopStatus);
} }
}, };
/** /**
* Internal helper method to handle loops without a status variable * Internal helper method to handle loops without a status variable
* @private * @private
*/ */
f: function forEach(array, callback) { exports.f = function forEachHelper(array, callback) {
if (isArray(array)) { if (isArray(array)) {
for (var i=0; i<array.length; i++) { for (var i=0; i<array.length; i++) {
callback(array[i]); callback(array[i]);
}
} else if (typeof array === 'function') {
// Also allow the first argument to be a custom iterator function
array(callback);
}
},
/**
* Internal helper method for looping over the properties of any object
* @private
*/
fp: function (o, func) {
if (!o) {
return;
} }
} else if (typeof array === 'function') {
// Also allow the first argument to be a custom iterator function
array(callback);
}
};
/**
* Internal helper method for looping over the properties of any object
* @private
*/
exports.fp = function forEachPropertyHelper(o, func) {
if (!o) {
return;
}
if (Array.isArray(o)) { if (Array.isArray(o)) {
for (var i=0; i<o.length; i++) { for (var i=0; i<o.length; i++) {
func(i, o[i]); func(i, o[i]);
} }
} else { } else {
for (var k in o) { for (var k in o) {
if (o.hasOwnProperty(k)) { if (o.hasOwnProperty(k)) {
func(k, o[k]); func(k, o[k]);
}
} }
} }
}, }
};
/** /**
* Helper to load a custom tag * Helper to load a custom tag
*/ */
t: function (renderer, targetProperty, isRepeated, hasNestedTags) { exports.t = function loadTagHelper(renderer, targetProperty, isRepeated, hasNestedTags) {
if (renderer) { if (renderer) {
renderer = resolveRenderer(renderer); renderer = resolveRenderer(renderer);
} }
if (targetProperty || hasNestedTags) { if (targetProperty || hasNestedTags) {
return function(input, out, parent, renderBody) { return function(input, out, parent, renderBody) {
// Handle nested tags // Handle nested tags
if (renderBody) { if (renderBody) {
renderBody(out, input); renderBody(out, input);
} }
if (targetProperty) { if (targetProperty) {
// If we are nested tag then we do not have a renderer // If we are nested tag then we do not have a renderer
if (isRepeated) { if (isRepeated) {
var existingArray = parent[targetProperty]; var existingArray = parent[targetProperty];
if (existingArray) { if (existingArray) {
existingArray.push(input); existingArray.push(input);
} else {
parent[targetProperty] = [input];
}
} else { } else {
parent[targetProperty] = input; parent[targetProperty] = [input];
} }
} else { } else {
// We are a tag with nested tags, but we have already found parent[targetProperty] = input;
// our nested tags by rendering the body
renderer(input, out);
} }
}; } else {
} else { // We are a tag with nested tags, but we have already found
return renderer; // our nested tags by rendering the body
} renderer(input, out);
},
/**
* Merges object properties
* @param {[type]} object [description]
* @param {[type]} source [description]
* @return {[type]} [description]
*/
m: function(into, source) {
for (var k in source) {
if (source.hasOwnProperty(k) && !into.hasOwnProperty(k)) {
into[k] = source[k];
} }
} };
return into; } else {
}, return renderer;
}
/**
* classList(a, b, c, ...)
* Joines a list of class names with spaces. Empty class names are omitted.
*
* classList('a', undefined, 'b') --> 'a b'
*
*/
cl: function() {
return classList(arguments);
},
/**
* Loads a template (__helpers.l --> marko_loadTemplate(path))
*/
l: load,
i: require('./include')
}; };
/**
* Merges object properties
* @param {[type]} object [description]
* @param {[type]} source [description]
* @return {[type]} [description]
*/
exports.m = function mergeHelper(into, source) {
for (var k in source) {
if (source.hasOwnProperty(k) && !into.hasOwnProperty(k)) {
into[k] = source[k];
}
}
return into;
};
/**
* classList(a, b, c, ...)
* Joines a list of class names with spaces. Empty class names are omitted.
*
* classList('a', undefined, 'b') --> 'a b'
*
*/
exports.cl = function classListHelper() {
return classList(arguments);
};
/**
* Loads a template (__helpers.l --> marko_loadTemplate(path))
*/
exports.l = require('./loader');
exports.i = require('./include');

View File

@ -2,11 +2,9 @@
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
var StringWriter = require('./StringWriter'); var StringWriter = require('./StringWriter');
var BufferedWriter = require('./BufferedWriter'); var BufferedWriter = require('./BufferedWriter');
var attr = require('./attr');
var escapeXml = require('./escapeXml');
var extend = require('raptor-util/extend'); var extend = require('raptor-util/extend');
var documentProvider = require('../document-provider');
var defaultDocument = typeof document != 'undefined' && document; var helpers;
var voidWriter = { write:function(){} }; var voidWriter = { write:function(){} };
@ -412,12 +410,10 @@ var proto = AsyncStream.prototype = {
return new AsyncStream(this.global); return new AsyncStream(this.global);
}, },
beginElement: function(name, attrs) { beginElement: function(name, elementAttrs) {
var str = '<' + name; var str = '<' + name;
for (var attrName in attrs) { helpers.as(elementAttrs);
str += attr(attrName, attrs[attrName]);
}
str += '>'; str += '>';
@ -436,7 +432,7 @@ var proto = AsyncStream.prototype = {
}, },
text: function(str) { text: function(str) {
this.write(escapeXml(str)); this.write(helpers.x(str));
}, },
getNode: function(doc) { getNode: function(doc) {
@ -446,7 +442,7 @@ var proto = AsyncStream.prototype = {
var html = this.getOutput(); var html = this.getOutput();
if (!doc) { if (!doc) {
doc = this.document || defaultDocument; doc = documentProvider.document;
} }
if (!node) { if (!node) {
@ -504,4 +500,6 @@ proto.w = proto.write;
extend(proto, require('../OutMixins')); extend(proto, require('../OutMixins'));
module.exports = AsyncStream; module.exports = AsyncStream;
helpers = require('./helpers');

View File

@ -1,13 +0,0 @@
var escapeXmlAttr = require('./escapeXml').attr;
module.exports = function(name, value, shouldEscape) {
if (typeof value === 'string') {
return ' ' + name + '="' + (shouldEscape !== false ? escapeXmlAttr(value) : value) + '"';
} else if (value === true) {
return ' ' + name;
} else if (value == null || value === false) {
return '';
} else {
return ' ' + name + '="' + value.toString() + '"';
}
};

View File

@ -1,35 +0,0 @@
var elTest = /[&<]/;
var elTestReplace = /[&<]/g;
var attrTest = /[&<\"\n]/;
var attrReplace = /[&<\"\n]/g;
var replacements = {
'<': '&lt;',
'&': '&amp;',
'"': '&quot;',
'\n': '&#10;' //Preserve new lines so that they don't get normalized as space
};
function replaceChar(match) {
return replacements[match];
}
function escapeXml(str) {
// check for most common case first
if (typeof str === 'string') {
return elTest.test(str) ? str.replace(elTestReplace, replaceChar) : str;
}
return (str == null) ? '' : str.toString();
}
function escapeXmlAttr(str) {
if (typeof str === 'string') {
return attrTest.test(str) ? str.replace(attrReplace, replaceChar) : str;
}
return (str == null) ? '' : str.toString();
}
module.exports = escapeXml;
escapeXml.attr = escapeXmlAttr;

View File

@ -1,120 +1,193 @@
'use strict'; 'use strict';
var escapeXml = require('./escapeXml'); require('raptor-polyfill/string/startsWith');
var escapeXmlAttr = escapeXml.attr;
var attr = require('./attr'); var warp10 = require('warp10');
var extend = require('raptor-util/extend'); var extend = require('raptor-util/extend');
var STYLE_ATTR = 'style'; var STYLE_ATTR = 'style';
var CLASS_ATTR = 'class'; var CLASS_ATTR = 'class';
var escapeEndingScriptTagRegExp = /<\//g; var escapeEndingScriptTagRegExp = /<\//g;
var commonHelpers = require('../helpers'); var elTest = /[&<]/;
var elTestReplace = /[&<]/g;
var attrTest = /[&<\"\n]/;
var attrReplace = /[&<\"\n]/g;
var stringifiedAttrTest = /[&\'\n]/;
var stringifiedAttrReplace = /[&\'\n]/g;
var classList = commonHelpers.cl; var classList;
module.exports = extend({ var replacements = {
/** '<': '&lt;',
* Internal method to escape special XML characters '&': '&amp;',
* @private '"': '&quot;',
*/ '\'': '&#39;',
x: escapeXml, '\n': '&#10;' //Preserve new lines so that they don't get normalized as space
/** };
* Internal method to escape special XML characters within an attribute
* @private
*/
xa: escapeXmlAttr,
/** function replaceChar(match) {
* Escapes the '</' sequence in the body of a <script> body to avoid the `<script>` being return replacements[match];
* ended prematurely. }
*
* For example:
* var evil = {
* name: '</script><script>alert(1)</script>'
* };
*
* <script>var foo = ${JSON.stringify(evil)}</script>
*
* Without escaping the ending '</script>' sequence the opening <script> tag would be
* prematurely ended and a new script tag could then be started that could then execute
* arbitrary code.
*/
xs: function(val) {
return (typeof val === 'string') ? val.replace(escapeEndingScriptTagRegExp, '\\u003C/') : val;
},
/** function escapeStr(str, regexpTest, regexpReplace) {
* Internal method to render a single HTML attribute return regexpTest.test(str) ? str.replace(regexpReplace, replaceChar) : str;
* @private }
*/
a: attr,
/** function escapeXmlHelper(value, regexpTest, regexpReplace) {
* Internal method to render multiple HTML attributes based on the properties of an object // check for most common case first
* @private if (typeof value === 'string') {
*/ return escapeStr(value, regexpTest, regexpReplace);
as: function(arg) { } else if (value == null) {
if (typeof arg === 'object') {
var out = '';
for (var attrName in arg) {
out += attr(attrName, arg[attrName]);
}
return out;
} else if (typeof arg === 'string') {
return arg;
}
return ''; return '';
}, } else if (typeof value === 'object') {
var safeHTML = value.safeHTML;
/** if (safeHTML != null) {
* Internal helper method to handle the "style" attribute. The value can either return value.safeHTML;
* be a string or an object with style propertes. For example: } else {
*
* sa('color: red; font-weight: bold') ==> ' style="color: red; font-weight: bold"'
* sa({color: 'red', 'font-weight': 'bold'}) ==> ' style="color: red; font-weight: bold"'
*/
sa: function(style) {
if (!style) {
return ''; return '';
} }
} else if (value === true || value === false || typeof value === 'number') {
return value.toString();
}
if (typeof style === 'string') { return escapeStr(value.toString(), regexpTest, regexpReplace);
return attr(STYLE_ATTR, style, false); }
} else if (typeof style === 'object') {
var parts = []; function escapeXml(value) {
for (var name in style) { return escapeXmlHelper(value, elTest, elTestReplace);
if (style.hasOwnProperty(name)) { }
var value = style[name];
if (value) { function escapeXmlAttr(value) {
parts.push(name + ':' + value); return escapeXmlHelper(value, attrTest, attrReplace);
} }
function attr(name, value, shouldEscape) {
if (typeof value === 'string') {
return ' ' + name + '="' + (shouldEscape !== false ? escapeStr(value, attrTest, attrReplace) : value) + '"';
} else if (value === true) {
return ' ' + name;
} else if (value == null || value === false) {
return '';
} else if (typeof value === 'object') {
if (name.startsWith('data-_')) {
value = warp10.stringify(value);
} else {
value = JSON.stringify(value);
}
return ' ' + name + "='" + escapeStr(value, stringifiedAttrTest, stringifiedAttrReplace) + "'";
} else {
return ' ' + name + '=' + value; // number (doesn't need quotes)
}
}
/**
* Internal method to escape special XML characters
* @private
*/
exports.x = escapeXml;
/**
* Internal method to escape special XML characters within an attribute
* @private
*/
exports.xa = escapeXmlAttr;
/**
* Escapes the '</' sequence in the body of a <script> body to avoid the `<script>` being
* ended prematurely.
*
* For example:
* var evil = {
* name: '</script><script>alert(1)</script>'
* };
*
* <script>var foo = ${JSON.stringify(evil)}</script>
*
* Without escaping the ending '</script>' sequence the opening <script> tag would be
* prematurely ended and a new script tag could then be started that could then execute
* arbitrary code.
*/
exports.xs = function(val) {
return (typeof val === 'string') ? val.replace(escapeEndingScriptTagRegExp, '\\u003C/') : val;
};
/**
* Internal method to render a single HTML attribute
* @private
*/
exports.a = attr;
/**
* Internal method to render multiple HTML attributes based on the properties of an object
* @private
*/
exports.as = function(arg) {
if (typeof arg === 'object') {
var out = '';
for (var attrName in arg) {
out += attr(attrName, arg[attrName]);
}
return out;
} else if (typeof arg === 'string') {
return arg;
}
return '';
};
/**
* Internal helper method to handle the "style" attribute. The value can either
* be a string or an object with style propertes. For example:
*
* sa('color: red; font-weight: bold') ==> ' style="color: red; font-weight: bold"'
* sa({color: 'red', 'font-weight': 'bold'}) ==> ' style="color: red; font-weight: bold"'
*/
exports.sa = function(style) {
if (!style) {
return '';
}
if (typeof style === 'string') {
return attr(STYLE_ATTR, style, false);
} else if (typeof style === 'object') {
var parts = [];
for (var name in style) {
if (style.hasOwnProperty(name)) {
var value = style[name];
if (value) {
parts.push(name + ':' + value);
} }
} }
return parts ? attr(STYLE_ATTR, parts.join(';'), false) : '';
} else {
return '';
} }
}, return parts ? attr(STYLE_ATTR, parts.join(';'), false) : '';
} else {
return '';
}
};
/** /**
* Internal helper method to handle the "class" attribute. The value can either * Internal helper method to handle the "class" attribute. The value can either
* be a string, an array or an object. For example: * be a string, an array or an object. For example:
* *
* ca('foo bar') ==> ' class="foo bar"' * ca('foo bar') ==> ' class="foo bar"'
* ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"' * ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"'
* ca(['foo', 'bar']) ==> ' class="foo bar"' * ca(['foo', 'bar']) ==> ' class="foo bar"'
*/ */
ca: function(classNames) { exports.ca = function(classNames) {
if (!classNames) { if (!classNames) {
return ''; return '';
} }
if (typeof classNames === 'string') { if (typeof classNames === 'string') {
return attr(CLASS_ATTR, classNames, false); return attr(CLASS_ATTR, classNames, false);
} else { } else {
return attr(CLASS_ATTR, classList(classNames), false); return attr(CLASS_ATTR, classList(classNames), false);
} }
}, };
inline: require('./')._inline
}, commonHelpers);
var commonHelpers = require('../helpers');
classList = commonHelpers.cl;
extend(exports, commonHelpers);
exports.inline = require('./')._inline;

View File

@ -12,7 +12,7 @@ exports.c = function createTemplate(path) {
return new Template(path); return new Template(path);
}; };
var AsyncStream = require('./AsyncStream'); var AsyncStream;
function createOut(globalData) { function createOut(globalData) {
return new AsyncStream(globalData); return new AsyncStream(globalData);
@ -161,6 +161,10 @@ exports.Template = Template;
helpers = require('./helpers'); helpers = require('./helpers');
exports.helpers = helpers; exports.helpers = helpers;
AsyncStream = require('./AsyncStream');
exports.enableAsyncStackTrace = AsyncStream.enableAsyncStackTrace; exports.enableAsyncStackTrace = AsyncStream.enableAsyncStackTrace;
require('../')._setRuntime(exports); require('../')._setRuntime(exports);

View File

@ -1,4 +1,6 @@
'use strict'; 'use strict';
var documentProvider = require('./document-provider');
var runtime; var runtime;
function setRuntime(_runtime) { function setRuntime(_runtime) {
@ -6,13 +8,18 @@ function setRuntime(_runtime) {
} }
exports._setRuntime = setRuntime; exports._setRuntime = setRuntime;
var load = require('./loader');
function createOut(globalData) { function createOut(globalData) {
return runtime.createOut(globalData); return runtime.createOut(globalData);
} }
/**
* Used to associate a DOM Document with marko. This is needed
* to parse HTML fragments to insert into the VDOM tree.
*/
exports.setDocument = function(newDoc) {
documentProvider.document = newDoc;
};
exports.createOut = createOut; exports.createOut = createOut;
exports.load = load; exports.load = require('./loader');
exports.events = require('./events'); exports.events = require('./events');

View File

@ -4,11 +4,8 @@ var DocumentFragment = require('./DocumentFragment');
var Comment = require('./Comment'); var Comment = require('./Comment');
var Text = require('./Text'); var Text = require('./Text');
var extend = require('raptor-util/extend'); var extend = require('raptor-util/extend');
var virtualizeHTML = require('./virtualizeHTML');
var virtualize = require('./virtualize'); var documentProvider = require('../document-provider');
var specialHtmlRegexp = /[&<]/;
var defaultDocument = typeof document != 'undefined' && document;
function State(tree) { function State(tree) {
this.remaining = 1; this.remaining = 1;
@ -39,8 +36,6 @@ function AsyncVDOMBuilder(globalData, parentNode, state) {
this._sync = false; this._sync = false;
} }
var range;
var proto = AsyncVDOMBuilder.prototype = { var proto = AsyncVDOMBuilder.prototype = {
isAsyncVDOMBuilder: true, isAsyncVDOMBuilder: true,
@ -71,6 +66,22 @@ var proto = AsyncVDOMBuilder.prototype = {
}, },
text: function(text) { text: function(text) {
var type = typeof text;
if (type !== 'string') {
if (text == null) {
return;
} else if (type === 'object') {
var safeHTML = text.safeHTML;
if (safeHTML) {
var html = typeof safeHTML === 'function' ? text.safeHTML() : safeHTML;
return this.html(html);
}
} else {
text = text.toString();
}
}
var parent = this._parent; var parent = this._parent;
if (parent) { if (parent) {
var lastChild = parent.lastChild; var lastChild = parent.lastChild;
@ -88,39 +99,9 @@ var proto = AsyncVDOMBuilder.prototype = {
}, },
html: function(html) { html: function(html) {
if (!specialHtmlRegexp.test(html)) { if (html != null) {
return this.text(html); var vdomNode = virtualizeHTML(html, documentProvider.document);
} this.node(vdomNode);
var document = this.document;
if (!range && document.createRange) {
range = document.createRange();
range.selectNode(document.body);
}
var vdomFragment;
var fragment;
if (range && range.createContextualFragment) {
fragment = range.createContextualFragment(html);
vdomFragment = virtualize(fragment);
} else {
var container = document.createElement('body');
container.innerHTML = html;
var curChild = container.firstChild;
if (curChild) {
vdomFragment = new DocumentFragment();
while(curChild) {
vdomFragment.appendChild(virtualize(curChild));
curChild = curChild.nextSibling;
}
}
}
if (vdomFragment) {
this.node(vdomFragment);
} }
return this; return this;
@ -291,7 +272,7 @@ var proto = AsyncVDOMBuilder.prototype = {
var vdomTree = this.getOutput(); var vdomTree = this.getOutput();
if (!doc) { if (!doc) {
doc = this.document || defaultDocument; doc = documentProvider.document;
} }
node = vdomTree.actualize(doc); node = vdomTree.actualize(doc);

View File

@ -1,8 +1,11 @@
require('raptor-polyfill/string/startsWith');
var inherit = require('raptor-util/inherit'); var inherit = require('raptor-util/inherit');
var extend = require('raptor-util/extend'); var extend = require('raptor-util/extend');
var Text = require('./Text'); var Text = require('./Text');
var Comment = require('./Comment'); var Comment = require('./Comment');
var Node = require('./Node'); var Node = require('./Node');
var documentProvider = require('../document-provider');
var virtualizeHTML;
var NS_XLINK = 'http://www.w3.org/1999/xlink'; var NS_XLINK = 'http://www.w3.org/1999/xlink';
var ATTR_HREF = 'href'; var ATTR_HREF = 'href';
@ -20,6 +23,16 @@ function removePreservedAttributes(attrs) {
return attrs; return attrs;
} }
function convertAttrValue(type, value) {
if (value === true) {
return '';
} else if (type === 'object') {
return JSON.stringify(value);
} else {
return value.toString();
}
}
function HTMLElementClone(other) { function HTMLElementClone(other) {
extend(this, other); extend(this, other);
this.parentNode = undefined; this.parentNode = undefined;
@ -136,6 +149,19 @@ HTMLElement.prototype = {
if (attrValue == null || attrValue === false) { if (attrValue == null || attrValue === false) {
targetNode.removeAttribute(attrName); targetNode.removeAttribute(attrName);
} else if (oldAttrs[attrName] !== attrValue) { } else if (oldAttrs[attrName] !== attrValue) {
if (attrName.startsWith('data-_')) {
// Special attributes aren't copied to the real DOM. They are only
// kept in the virtual attributes map
continue;
}
var type = typeof attrValue;
if (type !== 'string') {
attrValue = convertAttrValue(type, attrValue);
}
targetNode.setAttribute(attrName, attrValue); targetNode.setAttribute(attrName, attrValue);
} }
} }
@ -182,6 +208,20 @@ HTMLElement.prototype = {
* @param {String} value The text value for the new Text node * @param {String} value The text value for the new Text node
*/ */
t: function(value) { t: function(value) {
var type = typeof value;
if (type !== 'string') {
if (value == null) {
value = '';
} else if (type === 'object') {
var safeHTML = value.safeHTML;
var vdomNode = virtualizeHTML(safeHTML || '', documentProvider.document);
this.appendChild(vdomNode);
return this._finishChild();
} else {
value = value.toString();
}
}
this.appendChild(new Text(value)); this.appendChild(new Text(value));
return this._finishChild(); return this._finishChild();
}, },
@ -221,9 +261,17 @@ HTMLElement.prototype = {
for (var attrName in attributes) { for (var attrName in attributes) {
var attrValue = attributes[attrName]; var attrValue = attributes[attrName];
if (attrName.startsWith('data-_')) {
continue;
}
if (attrValue !== false && attrValue != null) { if (attrValue !== false && attrValue != null) {
if (attrValue === true) { var type = typeof attrValue;
attrValue = '';
if (type !== 'string') {
// Special attributes aren't copied to the real DOM. They are only
// kept in the virtual attributes map
attrValue = convertAttrValue(type, attrValue);
} }
if (attrName === 'xlink:href') { if (attrName === 'xlink:href') {
@ -313,4 +361,6 @@ Object.defineProperty(proto, 'disabled', {
} }
}); });
module.exports = HTMLElement; module.exports = HTMLElement;
virtualizeHTML = require('./virtualizeHTML');

View File

@ -1,19 +1,3 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'; 'use strict';
var HTMLElement = require('./HTMLElement'); var HTMLElement = require('./HTMLElement');
@ -23,67 +7,69 @@ var extend = require('raptor-util/extend');
var classList = commonHelpers.cl; var classList = commonHelpers.cl;
module.exports = extend({ exports.e = function(tagName, attrs, childCount, constId) {
e: function(tagName, attrs, childCount, constId) { return new HTMLElement(tagName, attrs, childCount, constId);
return new HTMLElement(tagName, attrs, childCount, constId); };
},
t: function(value) {
return new Text(value);
},
const: function(id) {
var i=0;
return function() {
return id + (i++);
};
},
/** exports.t = function(value) {
* Helper for generating the string for a style attribute return new Text(value);
* @param {[type]} style [description] };
* @return {[type]} [description]
*/
sa: function(style) {
if (!style) {
return null;
}
if (typeof style === 'string') { exports.const = function(id) {
return style; var i=0;
} else if (typeof style === 'object') { return function() {
var parts = []; return id + (i++);
for (var name in style) { };
if (style.hasOwnProperty(name)) { };
var value = style[name];
if (value) { /**
parts.push(name + ':' + value); * Helper for generating the string for a style attribute
} * @param {[type]} style [description]
* @return {[type]} [description]
*/
exports.sa = function(style) {
if (!style) {
return null;
}
if (typeof style === 'string') {
return style;
} else if (typeof style === 'object') {
var parts = [];
for (var name in style) {
if (style.hasOwnProperty(name)) {
var value = style[name];
if (value) {
parts.push(name + ':' + value);
} }
} }
return parts ? parts.join(';') : null;
} else {
return null;
} }
}, return parts ? parts.join(';') : null;
} else {
return null;
}
};
/** /**
* Internal helper method to handle the "class" attribute. The value can either * Internal helper method to handle the "class" attribute. The value can either
* be a string, an array or an object. For example: * be a string, an array or an object. For example:
* *
* ca('foo bar') ==> ' class="foo bar"' * ca('foo bar') ==> ' class="foo bar"'
* ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"' * ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"'
* ca(['foo', 'bar']) ==> ' class="foo bar"' * ca(['foo', 'bar']) ==> ' class="foo bar"'
*/ */
ca: function(classNames) { exports.ca = function(classNames) {
if (!classNames) { if (!classNames) {
return null; return null;
} }
if (typeof classNames === 'string') { if (typeof classNames === 'string') {
return classNames; return classNames;
} else { } else {
return classList(classNames); return classList(classNames);
} }
}, };
inline: require('./')._inline exports.inline = require('./')._inline;
}, commonHelpers);
extend(exports, commonHelpers);

View File

@ -136,14 +136,6 @@ exports.Template = Template;
exports._inline = createInlineMarkoTemplate; exports._inline = createInlineMarkoTemplate;
/**
* Used to associate a DOM Document with marko. This is needed
* to parse HTML fragments to insert into the VDOM tree.
*/
exports.setDocument = function(newDoc) {
AsyncVDOMBuilder.prototype.document = newDoc;
};
helpers = require('./helpers'); helpers = require('./helpers');
exports.helpers = helpers; exports.helpers = helpers;

View File

@ -0,0 +1,37 @@
var Text = require('./Text');
var DocumentFragment = require('./DocumentFragment');
var virtualize = require('./virtualize');
var specialHtmlRegexp = /[&<]/;
var range;
module.exports = function virtualizeHTML(html, doc) {
if (!specialHtmlRegexp.test(html)) {
return new Text(html);
}
if (!range && doc.createRange) {
range = doc.createRange();
range.selectNode(doc.body);
}
var vdomFragment;
var fragment;
if (range && range.createContextualFragment) {
fragment = range.createContextualFragment(html);
vdomFragment = virtualize(fragment);
} else {
var container = doc.createElement('body');
container.innerHTML = html;
vdomFragment = new DocumentFragment();
var curChild = container.firstChild;
while(curChild) {
vdomFragment.appendChild(virtualize(curChild));
curChild = curChild.nextSibling;
}
}
return vdomFragment;
};

View File

@ -0,0 +1 @@
<div data-foo=1></div>

View File

@ -0,0 +1,2 @@
<div data-foo=1>
</div>

View File

@ -0,0 +1,5 @@
exports.templateData = {
message: {
safeHTML: '<span>Hello World</span>'
}
};

View File

@ -0,0 +1 @@
<div value='{"hello":"world&#39;s"}'></div>

View File

@ -0,0 +1 @@
<div value=data.myObject/>

View File

@ -0,0 +1,3 @@
exports.templateData = {
myObject: {hello: 'world\'s'}
};

View File

@ -0,0 +1 @@
<div data-hello='{"foo":"bar"}'></div>

View File

@ -0,0 +1 @@
<div data-hello={foo: 'bar'}/>

View File

@ -0,0 +1,2 @@
exports.templateData = {};
exports.vdomSkip = false;

View File

@ -0,0 +1 @@
<div data-_onclick='{"o":{"name":"parent","child":{}},"$$":[{"l":["child","parent"],"r":[]}]}'></div>

View File

@ -0,0 +1 @@
<div data-_onclick=data.parent/>

View File

@ -0,0 +1,14 @@
var parent = {
name: 'parent'
};
var child = {
parent: parent
};
parent.child = child;
exports.templateData = {
parent: parent
};
exports.vdomSkip = true;

View File

@ -0,0 +1 @@
<div data-_onclick='{"foo":"bar"}'></div>

View File

@ -0,0 +1 @@
<div data-_onclick=data.specialData/>

View File

@ -0,0 +1,6 @@
exports.templateData = {
specialData: {
foo: 'bar'
}
};
exports.vdomSkip = true;

View File

@ -0,0 +1 @@
<div data-_onclick='{"foo":"bar"}'></div>

View File

@ -0,0 +1 @@
<div data-_onclick={ foo: 'bar' }/>

View File

@ -0,0 +1,6 @@
exports.templateData = {
specialData: {
foo: 'bar'
}
};
exports.vdomSkip = true;

View File

@ -0,0 +1 @@
<div><span>Hello World</span></div>

View File

@ -0,0 +1,3 @@
<div>
${data.message}
</div>

View File

@ -0,0 +1,5 @@
exports.templateData = {
message: {
safeHTML: '<span>Hello World</span>'
}
};

View File

@ -4,7 +4,6 @@ module.exports = template;
var marko_helpers = require("marko/runtime/vdom/helpers"), var marko_helpers = require("marko/runtime/vdom/helpers"),
marko_classList = marko_helpers.cl, marko_classList = marko_helpers.cl,
marko_str = marko_helpers.s,
marko_classAttr = marko_helpers.ca; marko_classAttr = marko_helpers.ca;
function render(data, out) { function render(data, out) {
@ -13,10 +12,10 @@ function render(data, out) {
bar: true, bar: true,
baz: false baz: false
})) }))
}, 1) }, 3)
.t("Hello " + .t("Hello ")
marko_str(name) + .t(name)
"!"); .t("!");
} }
template._ = render; template._ = render;

View File

@ -2,17 +2,14 @@ var template = require("marko/vdom").c(__filename);
module.exports = template; module.exports = template;
var marko_helpers = require("marko/runtime/vdom/helpers"),
marko_str = marko_helpers.s;
function render(data, out) { function render(data, out) {
out.e("div", { out.e("div", {
foo: "bar", foo: "bar",
hello: "world" hello: "world"
}, 1) }, 3)
.t("Hello " + .t("Hello ")
marko_str(name) + .t(name)
"!"); .t("!");
} }
template._ = render; template._ = render;

View File

@ -2,19 +2,16 @@ var template = require("marko/vdom").c(__filename);
module.exports = template; module.exports = template;
var marko_helpers = require("marko/runtime/vdom/helpers"),
marko_str = marko_helpers.s;
function render(data, out) { function render(data, out) {
var attrs = { var attrs = {
foo: "bar", foo: "bar",
hello: "world" hello: "world"
}; };
out.e("div", attrs, 1) out.e("div", attrs, 3)
.t("Hello " + .t("Hello ")
marko_str(name) + .t(name)
"!"); .t("!");
} }
template._ = render; template._ = render;

View File

@ -2,17 +2,15 @@ var template = require("marko/vdom").c(__filename);
module.exports = template; module.exports = template;
var marko_helpers = require("marko/runtime/vdom/helpers"), var marko_attrs0 = {
marko_str = marko_helpers.s,
marko_attrs0 = {
"class": "foo" "class": "foo"
}; };
function render(data, out) { function render(data, out) {
out.e("div", marko_attrs0, 1) out.e("div", marko_attrs0, 3)
.t("Hello " + .t("Hello ")
marko_str(name) + .t(name)
"!"); .t("!");
} }
template._ = render; template._ = render;

View File

@ -2,15 +2,14 @@ var template = require("marko/vdom").c(__filename);
module.exports = template; module.exports = template;
var marko_helpers = require("marko/runtime/vdom/helpers"),
marko_str = marko_helpers.s;
function render(data, out) { function render(data, out) {
out.t("Hello " + out.t("Hello ");
marko_str(name) +
"! ");
out.h(marko_str(message)); out.t(name);
out.t("! ");
out.h(message);
} }
template._ = render; template._ = render;

View File

@ -3,7 +3,6 @@ var template = require("marko/vdom").c(__filename);
module.exports = template; module.exports = template;
var marko_helpers = require("marko/runtime/vdom/helpers"), var marko_helpers = require("marko/runtime/vdom/helpers"),
marko_str = marko_helpers.s,
marko_forEach = marko_helpers.f, marko_forEach = marko_helpers.f,
marko_createElement = marko_helpers.e, marko_createElement = marko_helpers.e,
marko_const = marko_helpers.const, marko_const = marko_helpers.const,
@ -12,17 +11,17 @@ var marko_helpers = require("marko/runtime/vdom/helpers"),
.t("No colors!"); .t("No colors!");
function render(data, out) { function render(data, out) {
out.e("h1", null, 1) out.e("h1", null, 3)
.t("Hello " + .t("Hello ")
marko_str(data.name) + .t(data.name)
"!"); .t("!");
if (data.colors.length) { if (data.colors.length) {
out.be("ul"); out.be("ul");
marko_forEach(data.colors, function(color) { marko_forEach(data.colors, function(color) {
out.e("li", null, 1) out.e("li", null, 1)
.t(marko_str(color)); .t(color);
}); });
out.ee(); out.ee();

View File

@ -3,7 +3,6 @@ var template = require("marko/vdom").c(__filename);
module.exports = template; module.exports = template;
var marko_helpers = require("marko/runtime/vdom/helpers"), var marko_helpers = require("marko/runtime/vdom/helpers"),
marko_str = marko_helpers.s,
marko_createElement = marko_helpers.e, marko_createElement = marko_helpers.e,
marko_const = marko_helpers.const, marko_const = marko_helpers.const,
marko_const_nextId = marko_const("69a896"), marko_const_nextId = marko_const("69a896"),
@ -15,10 +14,10 @@ var marko_helpers = require("marko/runtime/vdom/helpers"),
function render(data, out) { function render(data, out) {
out.e("span", null, 2) out.e("span", null, 2)
.e("h1", null, 1) .e("h1", null, 3)
.t("Hello " + .t("Hello ")
marko_str(data.name) + .t(data.name)
"!") .t("!")
.n(marko_node0); .n(marko_node0);
} }

View File

@ -1,7 +1,5 @@
module.exports = require('marko/widgets').defineComponent({ module.exports = require('marko/widgets').defineComponent({
createOut: require('marko/html').createOut,
renderer: function(input, out) { renderer: function(input, out) {
out.write('Hello ' + input.name + '!'); out.text('Hello ' + input.name + '!');
} }
}); });

View File

@ -6,7 +6,7 @@ module.exports = function(helpers) {
var forElId = label.getAttribute('for'); var forElId = label.getAttribute('for');
var inputEl = document.getElementById(forElId); var inputEl = document.getElementById(forElId);
expect(forElId).to.exist; expect(!!forElId).to.equal(true);
expect(inputEl.value).to.equal('test'); expect(inputEl.value).to.equal('test');
expect(label.getAttribute('for-ref')).to.equal(null); expect(label.getAttribute('for-ref')).to.equal(null);
}; };

View File

@ -3,7 +3,7 @@ var expect = require('chai').expect;
module.exports = function(helpers) { module.exports = function(helpers) {
var widget = helpers.mount(require('./index'), {}); var widget = helpers.mount(require('./index'), {});
expect(widget.__document).to.exist; expect(widget.__document != null).to.equal(true);
expect(widget.__document).to.equal(document); expect(widget.__document).to.equal(document);
var contentWidget = widget.renderIntoIframe(); var contentWidget = widget.renderIntoIframe();

View File

@ -1,7 +1,5 @@
module.exports = require('marko/widgets').defineComponent({ module.exports = require('marko/widgets').defineComponent({
createOut: require('marko/html').createOut,
renderer: function(input, out) { renderer: function(input, out) {
out.write('Hello ' + input.name + '!'); out.text('Hello ' + input.name + '!');
} }
}); });

View File

@ -10,5 +10,5 @@ module.exports = function(helpers) {
widget.destroy(); widget.destroy();
expect(widget.update()).to.be.undefined; expect(widget.update() === undefined).to.equal(true);
}; };

View File

@ -1,5 +1,7 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-async/widget",
"require: ./components/app-hello/widget",
"require: ./components/app-init-async/widget"
] ]
} }

View File

@ -1,4 +1,4 @@
var template = require('marko').load(require.resolve('./template.marko')); var template = require('./template.marko');
module.exports = function(input, out) { module.exports = function(input, out) {
var asyncOut = out.beginAsync(); var asyncOut = out.beginAsync();

View File

@ -1,5 +1,5 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo"
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-simple"
] ]
} }

View File

@ -1,5 +1,6 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo",
"require: ./components/app-bar"
] ]
} }

View File

@ -1,5 +1,7 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo",
"require: ./components/app-bar",
"require: ./components/app-baz"
] ]
} }

View File

@ -1,5 +1,7 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo",
"require: ./components/app-bar",
"require: ./components/app-baz"
] ]
} }

View File

@ -1,5 +1,7 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo",
"require: ./components/app-bar",
"require: ./components/app-baz"
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo"
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/widget.js" "require: ./components/app-button-split/widget"
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo"
] ]
} }

View File

@ -1,5 +1,6 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-fixed-id",
"require: ./components/app-hello/widget"
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo"
] ]
} }

View File

@ -1,5 +1,5 @@
{ {
"dependencies": [ "dependencies": [
"require: ./components/**/*.js" "require: ./components/app-foo"
] ]
} }

View File

@ -1,7 +1,8 @@
'use strict'; 'use strict';
var escapeXml = require('../../runtime/html/escapeXml'); var runtimeHtmlHelpers = require('../../runtime/html/helpers');
var escapeXmlAttr = escapeXml.attr; var escapeXml = runtimeHtmlHelpers.x;
var escapeXmlAttr = runtimeHtmlHelpers.xa;
var openTagOnly = {}; var openTagOnly = {};

View File

@ -9,7 +9,7 @@ const jsdom = require("jsdom").jsdom;
const expect = require('chai').expect; const expect = require('chai').expect;
const defaultDocument = jsdom('<html><body></body></html>'); const defaultDocument = jsdom('<html><body></body></html>');
require('../../runtime/vdom').setDocument(defaultDocument); // We need this to parse HTML fragments on the server require('../../').setDocument(defaultDocument); // We need this to parse HTML fragments on the server
function createAsyncVerifier(main, helpers, out) { function createAsyncVerifier(main, helpers, out) {
@ -179,10 +179,11 @@ module.exports = function runRenderTest(dir, helpers, done, options) {
getExpectedHtml(function(err, expectedHtml) { getExpectedHtml(function(err, expectedHtml) {
fs.writeFileSync(path.join(dir, 'vdom-expected.generated.html'), expectedHtml, { encoding: 'utf8' }); fs.writeFileSync(path.join(dir, 'vdom-expected.generated.html'), expectedHtml, { encoding: 'utf8' });
let actualizedDom = vdomTree.actualize(defaultDocument);
// NOTE: We serialie the virtual DOM tree into an HTML string and reparse so that we can // NOTE: We serialie the virtual DOM tree into an HTML string and reparse so that we can
// normalize the text // normalize the text
let vdomHtml = domToHTML(vdomTree); let vdomHtml = domToHTML(actualizedDom);
let vdomRealDocument = jsdom('<html><body>' + vdomHtml + '</body></html>'); let vdomRealDocument = jsdom('<html><body>' + vdomHtml + '</body></html>');
let vdomString = domToString(vdomRealDocument.body, { childrenOnly: true }); let vdomString = domToString(vdomRealDocument.body, { childrenOnly: true });
helpers.compare(vdomString, 'vdom-', '.generated.html'); helpers.compare(vdomString, 'vdom-', '.generated.html');

View File

@ -1,5 +1,19 @@
var _addEventListener = require('./addEventListener'); var _addEventListener = require('./addEventListener');
var updateManager = require('./update-manager'); var updateManager = require('./update-manager');
var warp10Parse = require('warp10/parse');
function getEventAttribute(el, attrName) {
var virtualAttrs = el._vattrs;
if (virtualAttrs) {
return el._vattrs[attrName];
} else {
var attrValue = el.getAttribute(attrName);
if (attrValue) {
return warp10Parse(attrValue);
}
}
}
var attachBubbleEventListeners = function() { var attachBubbleEventListeners = function() {
var body = document.body; var body = document.body;
@ -30,17 +44,22 @@ var attachBubbleEventListeners = function() {
// Search up the tree looking DOM events mapped to target // Search up the tree looking DOM events mapped to target
// widget methods // widget methods
var attrName = 'data-w-on' + eventType; var attrName = 'data-_on' + eventType;
var targetMethod; var target;
var targetWidget;
// Attributes will have the following form: // Attributes will have the following form:
// w-on<event_type>="<target_method>|<widget_id>" // w-on<event_type>="<target_method>|<widget_id>"
do { do {
if ((targetMethod = curNode.getAttribute(attrName))) { if ((target = getEventAttribute(curNode, attrName))) {
var separator = targetMethod.lastIndexOf('|'); var targetMethod = target[0];
var targetWidgetId = targetMethod.substring(separator+1); var targetWidgetId = target[1];
var targetArgs;
if (target.length > 2) {
targetArgs = target.slice(2);
}
var targetWidgetEl = document.getElementById(targetWidgetId); var targetWidgetEl = document.getElementById(targetWidgetId);
if (!targetWidgetEl) { if (!targetWidgetEl) {
// The target widget is not in the DOM anymore // The target widget is not in the DOM anymore
@ -50,12 +69,11 @@ var attachBubbleEventListeners = function() {
continue; continue;
} }
targetWidget = targetWidgetEl.__widget; var targetWidget = targetWidgetEl.__widget;
if (!targetWidget) { if (!targetWidget) {
throw new Error('Widget not found: ' + targetWidgetId); throw new Error('Widget not found: ' + targetWidgetId);
} }
targetMethod = targetMethod.substring(0, separator);
var targetFunc = targetWidget[targetMethod]; var targetFunc = targetWidget[targetMethod];
if (!targetFunc) { if (!targetFunc) {

View File

@ -42,26 +42,17 @@ function addBubblingEventListener(transformHelper, eventType, targetMethod) {
builder.identifier('widget'), builder.identifier('widget'),
builder.identifier('id')); builder.identifier('id'));
if (targetMethod.type === 'Literal') { // The event handler method is conditional and it may resolve to a null method name. Therefore,
// We know the event handler is method is no conditional so we set the attribute correctly at compile time // we need to use a runtime helper to set the value correctly.
attrValue = builder.concat( var markoWidgetsEventFuncId = transformHelper.context.importModule('markoWidgets_event',
targetMethod, transformHelper.getMarkoWidgetsRequirePath('marko/widgets/taglib/helpers/event'));
builder.literal('|'),
widgetIdExpression);
} else {
// The event handler method is conditional and it may resolve to a null method name. Therefore, attrValue = builder.functionCall(markoWidgetsEventFuncId, [
// we need to use a runtime helper to set the value correctly. targetMethod,
var markoWidgetsEventFuncId = transformHelper.context.importModule('markoWidgets_event', widgetIdExpression
transformHelper.getMarkoWidgetsRequirePath('marko/widgets/taglib/helpers/event')); ]);
attrValue = builder.functionCall(markoWidgetsEventFuncId, [ el.setAttributeValue('data-_on' + eventType.value, attrValue);
targetMethod,
widgetIdExpression
]);
}
el.setAttributeValue('data-w-on' + eventType.value, attrValue);
} }
function addDirectEventListener(transformHelper, eventType, targetMethod) { function addDirectEventListener(transformHelper, eventType, targetMethod) {

View File

@ -1,3 +1,3 @@
module.exports = function(handlerMethodName, widgetId) { module.exports = function(handlerMethodName, widgetId) {
return handlerMethodName && (handlerMethodName + '|' + widgetId); return handlerMethodName && [handlerMethodName, widgetId];
}; };