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

View File

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

View File

@ -21,16 +21,6 @@ class TextVDOM extends Node {
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;
}
@ -70,7 +60,6 @@ class TextVDOM extends Node {
let escape = this.escape;
var funcName = escape ? 't' : 'h';
var strFuncId = this.strFuncId;
function writeTextArgs() {
writer.write('(');
@ -84,15 +73,7 @@ class TextVDOM extends Node {
writer.writeIndent();
}
if (arg.type === 'Literal') {
writer.write(arg);
} else {
writer.write(strFuncId);
writer.write('(');
writer.write(arg);
writer.write(')');
}
writer.write(arg);
}
writer.write(')');

View File

@ -57,7 +57,6 @@ function generateNodesForArray(nodes, context, options) {
let nextNodeId = 0;
let nextAttrsId = 0;
var optimizeTextNodes = options.optimizeTextNodes !== false;
var optimizeStaticNodes = options.optimizeStaticNodes !== false;
function generateStaticNode(node) {
@ -112,31 +111,6 @@ function generateNodesForArray(nodes, context, options) {
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 {
finalNodes.push(node);
}

View File

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

View File

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

View File

@ -1,6 +1,5 @@
'use strict';
var isArray = Array.isArray;
var load = require('./loader');
function classListHelper(arg, classNames) {
var len;
@ -93,141 +92,138 @@ LoopStatus.prototype = {
}
};
module.exports = {
/**
* Internal helper method to prevent null/undefined from being written out
* when writing text that resolves to null/undefined
* @private
*/
s: function(str) {
return (str == null) ? '' : str.toString();
},
/**
* Internal helper method to prevent null/undefined from being written out
* when writing text that resolves to null/undefined
* @private
*/
exports.s = function strHelper(str) {
return (str == null) ? '' : str.toString();
};
/**
* Internal helper method to handle loops with a status variable
* @private
*/
fv: function (array, callback) {
if (!array) {
return;
}
if (!array.forEach) {
array = [array];
}
/**
* Internal helper method to handle loops with a status variable
* @private
*/
exports.fv = function forEachStatusVariableHelper(array, callback) {
if (!array) {
return;
}
if (!array.forEach) {
array = [array];
}
var len = array.length;
var loopStatus = new LoopStatus(len);
var len = array.length;
var loopStatus = new LoopStatus(len);
for (; loopStatus.i < len; loopStatus.i++) {
var o = array[loopStatus.i];
callback(o, loopStatus);
}
},
for (; loopStatus.i < len; loopStatus.i++) {
var o = array[loopStatus.i];
callback(o, loopStatus);
}
};
/**
* Internal helper method to handle loops without a status variable
* @private
*/
f: function forEach(array, callback) {
if (isArray(array)) {
for (var i=0; i<array.length; 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;
/**
* Internal helper method to handle loops without a status variable
* @private
*/
exports.f = function forEachHelper(array, callback) {
if (isArray(array)) {
for (var i=0; i<array.length; 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
*/
exports.fp = function forEachPropertyHelper(o, func) {
if (!o) {
return;
}
if (Array.isArray(o)) {
for (var i=0; i<o.length; i++) {
func(i, o[i]);
}
} else {
for (var k in o) {
if (o.hasOwnProperty(k)) {
func(k, o[k]);
}
if (Array.isArray(o)) {
for (var i=0; i<o.length; i++) {
func(i, o[i]);
}
} else {
for (var k in o) {
if (o.hasOwnProperty(k)) {
func(k, o[k]);
}
}
},
}
};
/**
* Helper to load a custom tag
*/
t: function (renderer, targetProperty, isRepeated, hasNestedTags) {
if (renderer) {
renderer = resolveRenderer(renderer);
}
/**
* Helper to load a custom tag
*/
exports.t = function loadTagHelper(renderer, targetProperty, isRepeated, hasNestedTags) {
if (renderer) {
renderer = resolveRenderer(renderer);
}
if (targetProperty || hasNestedTags) {
return function(input, out, parent, renderBody) {
// Handle nested tags
if (renderBody) {
renderBody(out, input);
}
if (targetProperty || hasNestedTags) {
return function(input, out, parent, renderBody) {
// Handle nested tags
if (renderBody) {
renderBody(out, input);
}
if (targetProperty) {
// If we are nested tag then we do not have a renderer
if (isRepeated) {
var existingArray = parent[targetProperty];
if (existingArray) {
existingArray.push(input);
} else {
parent[targetProperty] = [input];
}
if (targetProperty) {
// If we are nested tag then we do not have a renderer
if (isRepeated) {
var existingArray = parent[targetProperty];
if (existingArray) {
existingArray.push(input);
} else {
parent[targetProperty] = input;
parent[targetProperty] = [input];
}
} else {
// We are a tag with nested tags, but we have already found
// our nested tags by rendering the body
renderer(input, out);
parent[targetProperty] = input;
}
};
} else {
return renderer;
}
},
/**
* 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];
} else {
// We are a tag with nested tags, but we have already found
// our nested tags by rendering the body
renderer(input, out);
}
}
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'
*
*/
cl: function() {
return classList(arguments);
},
/**
* Loads a template (__helpers.l --> marko_loadTemplate(path))
*/
l: load,
i: require('./include')
};
} else {
return renderer;
}
};
/**
* 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 StringWriter = require('./StringWriter');
var BufferedWriter = require('./BufferedWriter');
var attr = require('./attr');
var escapeXml = require('./escapeXml');
var extend = require('raptor-util/extend');
var defaultDocument = typeof document != 'undefined' && document;
var documentProvider = require('../document-provider');
var helpers;
var voidWriter = { write:function(){} };
@ -412,12 +410,10 @@ var proto = AsyncStream.prototype = {
return new AsyncStream(this.global);
},
beginElement: function(name, attrs) {
beginElement: function(name, elementAttrs) {
var str = '<' + name;
for (var attrName in attrs) {
str += attr(attrName, attrs[attrName]);
}
helpers.as(elementAttrs);
str += '>';
@ -436,7 +432,7 @@ var proto = AsyncStream.prototype = {
},
text: function(str) {
this.write(escapeXml(str));
this.write(helpers.x(str));
},
getNode: function(doc) {
@ -446,7 +442,7 @@ var proto = AsyncStream.prototype = {
var html = this.getOutput();
if (!doc) {
doc = this.document || defaultDocument;
doc = documentProvider.document;
}
if (!node) {
@ -504,4 +500,6 @@ proto.w = proto.write;
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';
var escapeXml = require('./escapeXml');
var escapeXmlAttr = escapeXml.attr;
var attr = require('./attr');
require('raptor-polyfill/string/startsWith');
var warp10 = require('warp10');
var extend = require('raptor-util/extend');
var STYLE_ATTR = 'style';
var CLASS_ATTR = 'class';
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({
/**
* Internal method to escape special XML characters
* @private
*/
x: escapeXml,
/**
* Internal method to escape special XML characters within an attribute
* @private
*/
xa: escapeXmlAttr,
var replacements = {
'<': '&lt;',
'&': '&amp;',
'"': '&quot;',
'\'': '&#39;',
'\n': '&#10;' //Preserve new lines so that they don't get normalized as space
};
/**
* 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.
*/
xs: function(val) {
return (typeof val === 'string') ? val.replace(escapeEndingScriptTagRegExp, '\\u003C/') : val;
},
function replaceChar(match) {
return replacements[match];
}
/**
* Internal method to render a single HTML attribute
* @private
*/
a: attr,
function escapeStr(str, regexpTest, regexpReplace) {
return regexpTest.test(str) ? str.replace(regexpReplace, replaceChar) : str;
}
/**
* Internal method to render multiple HTML attributes based on the properties of an object
* @private
*/
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;
}
function escapeXmlHelper(value, regexpTest, regexpReplace) {
// check for most common case first
if (typeof value === 'string') {
return escapeStr(value, regexpTest, regexpReplace);
} else if (value == null) {
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"'
*/
sa: function(style) {
if (!style) {
} else if (typeof value === 'object') {
var safeHTML = value.safeHTML;
if (safeHTML != null) {
return value.safeHTML;
} else {
return '';
}
} else if (value === true || value === false || typeof value === 'number') {
return value.toString();
}
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 escapeStr(value.toString(), regexpTest, regexpReplace);
}
function escapeXml(value) {
return escapeXmlHelper(value, elTest, elTestReplace);
}
function escapeXmlAttr(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
* be a string, an array or an object. For example:
*
* ca('foo bar') ==> ' class="foo bar"'
* ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"'
* ca(['foo', 'bar']) ==> ' class="foo bar"'
*/
ca: function(classNames) {
if (!classNames) {
return '';
}
/**
* Internal helper method to handle the "class" attribute. The value can either
* be a string, an array or an object. For example:
*
* ca('foo bar') ==> ' class="foo bar"'
* ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"'
* ca(['foo', 'bar']) ==> ' class="foo bar"'
*/
exports.ca = function(classNames) {
if (!classNames) {
return '';
}
if (typeof classNames === 'string') {
return attr(CLASS_ATTR, classNames, false);
} else {
return attr(CLASS_ATTR, classList(classNames), false);
}
},
if (typeof classNames === 'string') {
return attr(CLASS_ATTR, classNames, false);
} else {
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);
};
var AsyncStream = require('./AsyncStream');
var AsyncStream;
function createOut(globalData) {
return new AsyncStream(globalData);
@ -161,6 +161,10 @@ exports.Template = Template;
helpers = require('./helpers');
exports.helpers = helpers;
AsyncStream = require('./AsyncStream');
exports.enableAsyncStackTrace = AsyncStream.enableAsyncStackTrace;
require('../')._setRuntime(exports);

View File

@ -1,4 +1,6 @@
'use strict';
var documentProvider = require('./document-provider');
var runtime;
function setRuntime(_runtime) {
@ -6,13 +8,18 @@ function setRuntime(_runtime) {
}
exports._setRuntime = setRuntime;
var load = require('./loader');
function 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.load = load;
exports.load = require('./loader');
exports.events = require('./events');

View File

@ -4,11 +4,8 @@ var DocumentFragment = require('./DocumentFragment');
var Comment = require('./Comment');
var Text = require('./Text');
var extend = require('raptor-util/extend');
var virtualize = require('./virtualize');
var specialHtmlRegexp = /[&<]/;
var defaultDocument = typeof document != 'undefined' && document;
var virtualizeHTML = require('./virtualizeHTML');
var documentProvider = require('../document-provider');
function State(tree) {
this.remaining = 1;
@ -39,8 +36,6 @@ function AsyncVDOMBuilder(globalData, parentNode, state) {
this._sync = false;
}
var range;
var proto = AsyncVDOMBuilder.prototype = {
isAsyncVDOMBuilder: true,
@ -71,6 +66,22 @@ var proto = AsyncVDOMBuilder.prototype = {
},
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;
if (parent) {
var lastChild = parent.lastChild;
@ -88,39 +99,9 @@ var proto = AsyncVDOMBuilder.prototype = {
},
html: function(html) {
if (!specialHtmlRegexp.test(html)) {
return this.text(html);
}
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);
if (html != null) {
var vdomNode = virtualizeHTML(html, documentProvider.document);
this.node(vdomNode);
}
return this;
@ -291,7 +272,7 @@ var proto = AsyncVDOMBuilder.prototype = {
var vdomTree = this.getOutput();
if (!doc) {
doc = this.document || defaultDocument;
doc = documentProvider.document;
}
node = vdomTree.actualize(doc);

View File

@ -1,8 +1,11 @@
require('raptor-polyfill/string/startsWith');
var inherit = require('raptor-util/inherit');
var extend = require('raptor-util/extend');
var Text = require('./Text');
var Comment = require('./Comment');
var Node = require('./Node');
var documentProvider = require('../document-provider');
var virtualizeHTML;
var NS_XLINK = 'http://www.w3.org/1999/xlink';
var ATTR_HREF = 'href';
@ -20,6 +23,16 @@ function removePreservedAttributes(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) {
extend(this, other);
this.parentNode = undefined;
@ -136,6 +149,19 @@ HTMLElement.prototype = {
if (attrValue == null || attrValue === false) {
targetNode.removeAttribute(attrName);
} 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);
}
}
@ -182,6 +208,20 @@ HTMLElement.prototype = {
* @param {String} value The text value for the new Text node
*/
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));
return this._finishChild();
},
@ -221,9 +261,17 @@ HTMLElement.prototype = {
for (var attrName in attributes) {
var attrValue = attributes[attrName];
if (attrName.startsWith('data-_')) {
continue;
}
if (attrValue !== false && attrValue != null) {
if (attrValue === true) {
attrValue = '';
var type = typeof 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') {
@ -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';
var HTMLElement = require('./HTMLElement');
@ -23,67 +7,69 @@ var extend = require('raptor-util/extend');
var classList = commonHelpers.cl;
module.exports = extend({
e: function(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.e = function(tagName, attrs, childCount, constId) {
return new HTMLElement(tagName, attrs, childCount, constId);
};
/**
* Helper for generating the string for a style attribute
* @param {[type]} style [description]
* @return {[type]} [description]
*/
sa: function(style) {
if (!style) {
return null;
}
exports.t = function(value) {
return new Text(value);
};
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);
}
exports.const = function(id) {
var i=0;
return function() {
return id + (i++);
};
};
/**
* 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
* be a string, an array or an object. For example:
*
* ca('foo bar') ==> ' class="foo bar"'
* ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"'
* ca(['foo', 'bar']) ==> ' class="foo bar"'
*/
ca: function(classNames) {
if (!classNames) {
return null;
}
/**
* Internal helper method to handle the "class" attribute. The value can either
* be a string, an array or an object. For example:
*
* ca('foo bar') ==> ' class="foo bar"'
* ca({foo: true, bar: false, baz: true}) ==> ' class="foo baz"'
* ca(['foo', 'bar']) ==> ' class="foo bar"'
*/
exports.ca = function(classNames) {
if (!classNames) {
return null;
}
if (typeof classNames === 'string') {
return classNames;
} else {
return classList(classNames);
}
},
if (typeof classNames === 'string') {
return classNames;
} else {
return classList(classNames);
}
};
inline: require('./')._inline
}, commonHelpers);
exports.inline = require('./')._inline;
extend(exports, commonHelpers);

View File

@ -136,14 +136,6 @@ exports.Template = Template;
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');
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"),
marko_classList = marko_helpers.cl,
marko_str = marko_helpers.s,
marko_classAttr = marko_helpers.ca;
function render(data, out) {
@ -13,10 +12,10 @@ function render(data, out) {
bar: true,
baz: false
}))
}, 1)
.t("Hello " +
marko_str(name) +
"!");
}, 3)
.t("Hello ")
.t(name)
.t("!");
}
template._ = render;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
module.exports = require('marko/widgets').defineComponent({
createOut: require('marko/html').createOut,
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 inputEl = document.getElementById(forElId);
expect(forElId).to.exist;
expect(!!forElId).to.equal(true);
expect(inputEl.value).to.equal('test');
expect(label.getAttribute('for-ref')).to.equal(null);
};

View File

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

View File

@ -1,7 +1,5 @@
module.exports = require('marko/widgets').defineComponent({
createOut: require('marko/html').createOut,
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();
expect(widget.update()).to.be.undefined;
expect(widget.update() === undefined).to.equal(true);
};

View File

@ -1,5 +1,7 @@
{
"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) {
var asyncOut = out.beginAsync();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ const jsdom = require("jsdom").jsdom;
const expect = require('chai').expect;
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) {
@ -179,10 +179,11 @@ module.exports = function runRenderTest(dir, helpers, done, options) {
getExpectedHtml(function(err, expectedHtml) {
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
// normalize the text
let vdomHtml = domToHTML(vdomTree);
let vdomHtml = domToHTML(actualizedDom);
let vdomRealDocument = jsdom('<html><body>' + vdomHtml + '</body></html>');
let vdomString = domToString(vdomRealDocument.body, { childrenOnly: true });
helpers.compare(vdomString, 'vdom-', '.generated.html');

View File

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

View File

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

View File

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