Runtime now supports both vdom and html output

All tests are passing
This commit is contained in:
Patrick Steele-Idem 2016-10-11 17:28:09 -06:00
parent 0d6efeaf6d
commit ee815fc49b
84 changed files with 1224 additions and 303 deletions

View File

@ -1,4 +1,7 @@
{
"predef": [
"document"
],
"node" : true,
"esnext": true,
"boss" : false,

View File

@ -55,6 +55,7 @@ const helpers = {
'attrs': 'as',
'classAttr': 'ca',
'classList': 'cl',
'const': 'const',
'createElement': 'e',
'escapeXml': 'x',
'escapeXmlAttr': 'xa',
@ -516,7 +517,7 @@ class CompileContext extends EventEmitter {
get helpersIdentifier() {
if (!this._helpersIdentifier) {
if (this.inline) {
this._helpersIdentifier = this.importModule('__markoHelpers', 'marko/runtime/helpers');
this._helpersIdentifier = this.importModule('__markoHelpers', 'marko/runtime/html/helpers');
} else {
// The helpers variable is a parameter of the outer create function
this._helpersIdentifier = this.builder.identifier('__markoHelpers');

View File

@ -9,7 +9,7 @@ class HtmlJsParser {
parse(src, handlers) {
var listeners = {
onText(event) {
handlers.handleCharacters(event.value);
handlers.handleCharacters(event.value, event.parseMode);
},
onPlaceholder(event) {
@ -32,7 +32,7 @@ class HtmlJsParser {
},
onCDATA(event) {
handlers.handleCharacters(event.value);
handlers.handleCharacters(event.value, 'static-text');
},
onOpenTag(event, parser) {

View File

@ -110,13 +110,17 @@ class Parser {
return rootNode;
}
handleCharacters(text) {
handleCharacters(text, parseMode) {
var builder = this.context.builder;
if (this.prevTextNode && this.prevTextNode.isLiteral()) {
var escape = parseMode !== 'html';
// NOTE: If parseMode is 'static-text' or 'parsed-text' then that means that special
// HTML characters may not have been escaped on the way in so we need to escape
// them on the way out
if (this.prevTextNode && this.prevTextNode.isLiteral() && this.prevTextNode.escape === escape) {
this.prevTextNode.argument.value += text;
} else {
var escape = false;
this.prevTextNode = builder.text(builder.literal(text), escape);
this.parentNode.appendChild(this.prevTextNode);
}

View File

@ -1,6 +1,22 @@
'use strict';
module.exports = function generateCode(node, codegen, vdomUtil) {
node.name = codegen.generateCode(node.name);
var context = codegen.context;
var builder = codegen.builder;
// node.name = codegen.generateCode(node.name);
node.value = codegen.generateCode(node.value);
node.isStatic = vdomUtil.isStaticValue(node.value);
var name = node.name;
if (node.value && node.value.type !== 'Literal') {
if (name === 'class') {
node.value = builder.functionCall(context.helper('classAttr'), [node.value]);
} else if (name === 'style') {
node.value = builder.functionCall(context.helper('styleAttr'), [node.value]);
}
}
return node;
};

View File

@ -19,6 +19,19 @@ class HtmlComment extends Node {
];
}
generateVDOMCode(codegen) {
var comment = this.comment;
var builder = codegen.builder;
return builder.functionCall(
builder.memberExpression(
builder.identifierOut(),
builder.identifier('comment')),
[
comment
]);
}
walk(walker) {
this.comment = walker.walk(this.comment);
}

View File

@ -13,15 +13,6 @@ module.exports = function generateCode(node, codegen) {
tagName = node.tagNameExpression;
}
var context = codegen.context;
if (context.isMacro(node.tagName)) {
// At code generation time, if node tag corresponds to a registered macro
// then invoke the macro based on node HTML element instead of generating
// the code to render an HTML element.
return codegen.builder.invokeMacroFromEl(node);
}
var attributes = node._attributes && node._attributes.all;
var body = node.body;
var argument = node.argument;

View File

@ -43,10 +43,24 @@ class HtmlElement extends Node {
}
generateHTMLCode(codegen) {
if (codegen.context.isMacro(this.tagName)) {
// At code generation time, if node tag corresponds to a registered macro
// then invoke the macro based on node HTML element instead of generating
// the code to render an HTML element.
return codegen.builder.invokeMacroFromEl(this);
}
return generateHTMLCode(this, codegen);
}
generateVDOMCode(codegen) {
if (codegen.context.isMacro(this.tagName)) {
// At code generation time, if node tag corresponds to a registered macro
// then invoke the macro based on node HTML element instead of generating
// the code to render an HTML element.
return codegen.builder.invokeMacroFromEl(this);
}
return generateVDOMCode(this, codegen, vdomUtil);
}

View File

@ -73,6 +73,10 @@ class HtmlElementVDOM extends Node {
attributes.forEach((attr) => {
let value = attr.value;
if (value == null) {
value = builder.literal(true);
}
if (!attr.name) {
return;
}
@ -154,7 +158,6 @@ class HtmlElementVDOM extends Node {
} else if (this.isHtmlOnly) {
writer.write('out.');
funcCall = builder.functionCall(
builder.identifier('e'),
createArgs);
} else {

View File

@ -21,6 +21,7 @@ module.exports = function(node, codegen, vdomUtil) {
var tagName = codegen.generateCode(node.tagNameExpression);
var attributes = codegen.generateCode(node.getAttributes());
var dynamicAttributes = codegen.generateCode(node.dynamicAttributes);
var builder = codegen.builder;
var isAttrsStatic = checkAttributesStatic(attributes);
var isStatic = isAttrsStatic && node.isLiteralTagName();
@ -30,6 +31,9 @@ module.exports = function(node, codegen, vdomUtil) {
for (var i=0; i<body.length; i++) {
let child = body[i];
if (child.type === 'HtmlElementVDOM' || child.type === 'TextVDOM') {
if (child.type === 'TextVDOM' && child.escape === false) {
isHtmlOnly = false;
}
if (!child.isHtmlOnly) {
isStatic = false;
isHtmlOnly = false;
@ -43,6 +47,11 @@ module.exports = function(node, codegen, vdomUtil) {
}
}
var bodyOnlyIf = node.bodyOnlyIf;
if (bodyOnlyIf) {
isHtmlOnly = false;
}
var htmlElVDOM = new HtmlElementVDOM({
tagName,
attributes,
@ -53,7 +62,24 @@ module.exports = function(node, codegen, vdomUtil) {
dynamicAttributes
});
if (isHtmlOnly) {
if (bodyOnlyIf) {
htmlElVDOM.body = null;
var startIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
htmlElVDOM
]);
var endIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
new EndElementVDOM()
]);
return [
startIf,
body,
endIf
];
} else if (isHtmlOnly) {
return htmlElVDOM;
} else {
htmlElVDOM.body = null;

View File

@ -16,13 +16,15 @@ class Literal extends Node {
if (isArray(this.value)) {
this.value = codegen.generateCode(this.value);
} else if (typeof this.value === 'object') {
var newObject = {};
for (var k in this.value) {
if (this.value.hasOwnProperty(k)) {
newObject[k] = codegen.generateCode(this.value[k]);
if (!(this.value instanceof RegExp)) {
var newObject = {};
for (var k in this.value) {
if (this.value.hasOwnProperty(k)) {
newObject[k] = codegen.generateCode(this.value[k]);
}
}
this.value = newObject;
}
this.value = newObject;
}
}
return this;

View File

@ -11,8 +11,6 @@ function createVarsArray(vars) {
});
}
class TemplateRoot extends Node {
constructor(def) {
super('TemplateRoot');

View File

@ -9,9 +9,11 @@ class TextVDOM extends Node {
super('TextVDOM');
this.arguments = [def.argument];
this.isStatic = def.isStatic;
this.escape = def.escape !== false;
this.isHtmlOnly = true;
this.isChild = false;
this.createTextId = undefined;
this.strFuncId = undefined;
}
generateCode(codegen) {
@ -19,9 +21,15 @@ class TextVDOM extends Node {
vdomUtil.registerOptimizer(context);
// if (this.isStatic) {
// this.createTextId = context.importModule('marko_createText', 'marko/vdom/createText');
// }
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;
}
@ -39,49 +47,68 @@ class TextVDOM extends Node {
}
append(textVDOMToAppend) {
if (textVDOMToAppend.escape !== this.escape) {
return false;
}
if (!textVDOMToAppend.isStatic) {
this.isStatic = false;
}
if (textVDOMToAppend.strFuncId) {
this.strFuncId = textVDOMToAppend.strFuncId;
}
textVDOMToAppend.arguments.forEach(this._append, this);
return true;
}
writeCode(writer) {
let builder = writer.builder;
let args = this.arguments;
let textArg = args[0];
for (let i=1; i<args.length; i++) {
textArg = builder.binaryExpression(textArg, '+', args[i]);
let escape = this.escape;
var funcName = escape ? 't' : 'h';
var strFuncId = this.strFuncId;
function writeTextArgs() {
writer.write('(');
for (let i=0, len=args.length; i<len; i++) {
let arg = args[i];
if (i !== 0) {
writer.write(' +\n');
writer.writeLineIndent();
writer.writeIndent();
}
if (arg.type === 'Literal') {
writer.write(arg);
} else {
writer.write(strFuncId);
writer.write('(');
writer.write(arg);
writer.write(')');
}
}
writer.write(')');
}
if (this.isChild) {
let funcCall = builder.functionCall(
builder.identifier('t'),
[
textArg
]);
writer.write('.');
writer.write(funcCall);
} else if (this.isStatic) {
let funcCall = builder.functionCall(
this.createTextId,
[
textArg
]);
writer.write(funcCall);
writer.write(builder.identifier(funcName));
} else if (this.isStatic && this.createTextId) {
writer.write(this.createTextId);
} else {
let funcCall = builder.functionCall(
builder.identifier('t'),
[
textArg
]);
writer.write('out.');
writer.write(funcCall);
writer.write(builder.identifier(funcName));
}
writeTextArgs();
}
}

View File

@ -2,15 +2,38 @@
var TextVDOM = require('./TextVDOM');
var Literal = require('../../Literal');
var he = require('he'); // Used for dealing with HTML entities
module.exports = function(node, codegen, vdomUtil) {
var argument = codegen.generateCode(node.argument);
var escape = node.escape !== false;
var isStatic = null;
if (argument instanceof Literal && argument.value === '') {
// Don't add empty text nodes to the final tree
return null;
if (codegen.context.isFlagSet('SCRIPT_BODY')) {
escape = true;
}
var isStatic = vdomUtil.isStaticValue(argument);
return new TextVDOM({ argument, isStatic });
if (argument instanceof Literal) {
var literalValue = argument.value;
if (literalValue == null || literalValue === '') {
// Don't add empty text nodes to the final tree
return null;
}
if (escape === false) {
escape = true;
if (typeof literalValue === 'string') {
if (literalValue.indexOf('<') !== -1) {
escape = false;
} else if (literalValue.indexOf('&') !== -1) {
argument = codegen.builder.literal(he.decode(literalValue));
}
}
}
}
isStatic = isStatic == null ? vdomUtil.isStaticValue(argument) : isStatic;
return new TextVDOM({ argument, isStatic, escape });
};

View File

@ -34,7 +34,10 @@ if (g.__MARKO_CONFIG) {
* If true, whitespace will be preserved in templates. Defaults to false.
* @type {Boolean}
*/
preserveWhitespace: false
preserveWhitespace: false,
// The default output mode for compiled templates
output: 'html'
};
}

View File

@ -24,6 +24,9 @@ c) Else, generate one of the following:
const Node = require('../../ast/Node');
const nextConstIdFuncSymbol = Symbol();
const OPTIONS_DEFAULT = { optimizeTextNodes: true, optimizeStaticNodes: true };
const OPTIONS_OPTIMIZE_TEXT_NODES = { optimizeTextNodes: true, optimizeStaticNodes: false };
class NodeVDOM extends Node {
constructor(variableIdentifier) {
super('NodeVDOM');
@ -49,20 +52,24 @@ class NodeVDOM extends Node {
}
}
function optimizeVDOMNodes(nodes, context) {
function generateNodesForArray(nodes, context, options) {
let builder = context.builder;
let nextNodeId = 0;
let nextAttrsId = 0;
var optimizeTextNodes = options.optimizeTextNodes !== false;
var optimizeStaticNodes = options.optimizeStaticNodes !== false;
function generateStaticNode(node) {
if (node.type === 'HtmlElementVDOM') {
node.createElementId = context.importModule('marko_createElement', 'marko/vdom/createElement');
node.createElementId = context.helper('createElement');
}/* else {
node.createTextId = context.importModule('marko_createText', 'marko/vdom/createText');
}*/
let nextConstIdFunc = context.data[nextConstIdFuncSymbol];
if (!nextConstIdFunc) {
let constId = context.importModule('marko_const', 'marko/runtime/vdom/const');
let constId = context.helper('const');
let fingerprintLiteral = builder.literal(context.getFingerprint(6));
nextConstIdFunc = context.data[nextConstIdFuncSymbol] = context.addStaticVar('marko_const_nextId', builder.functionCall(constId, [ fingerprintLiteral ]));
}
@ -84,15 +91,16 @@ function optimizeVDOMNodes(nodes, context) {
}
}
function generateNodesForArray(nodes) {
let finalNodes = [];
let i = 0;
let finalNodes = [];
let i = 0;
while (i<nodes.length) {
let node = nodes[i];
if (node.type === 'HtmlElementVDOM') {
while (i<nodes.length) {
let node = nodes[i];
if (node.type === 'HtmlElementVDOM') {
if (optimizeStaticNodes) {
if (node.isStatic) {
finalNodes.push(generateStaticNode(node));
doOptimizeNode(node, context, OPTIONS_OPTIMIZE_TEXT_NODES);
} else {
if (node.isAttrsStatic) {
handleStaticAttributes(node);
@ -100,52 +108,58 @@ function optimizeVDOMNodes(nodes, context) {
finalNodes.push(node);
}
} else if (node.type === 'TextVDOM') {
} else {
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') {
firstTextNode.append(currentTextNode);
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;
}
}
// if (firstTextNode.isStatic) {
// finalNodes.push(generateStaticNode(firstTextNode));
// continue;
// } else {
// finalNodes.push(firstTextNode);
// }
firstTextNode.isStatic = false;
// firstTextNode.isStatic = false;
finalNodes.push(firstTextNode);
continue;
} else {
finalNodes.push(node);
}
i++;
} else {
finalNodes.push(node);
}
return finalNodes;
i++;
}
return finalNodes;
}
function doOptimizeNode(node, context, options) {
let walker = context.createWalker({
enterArray(nodes) {
return generateNodesForArray(nodes);
return generateNodesForArray(nodes, context, options);
}
});
return walker.walk(nodes);
return walker.walk(node);
}
class VDOMOptimizer {
optimize(node, context) {
if (node.body) {
node.body = optimizeVDOMNodes(node.body, context);
}
doOptimizeNode(node, context, OPTIONS_DEFAULT);
}
}

View File

@ -21,10 +21,13 @@ var fsReadOptions = { encoding: 'utf8' };
function compile(templatePath, markoCompiler, compilerOptions) {
var writeToDisk = compilerOptions.writeToDisk;
if (writeToDisk == null) {
writeToDisk = markoCompiler.defaultOptions.writeToDisk;
if (compilerOptions) {
compilerOptions = markoCompiler.defaultOptions;
} else {
compilerOptions = Object.assign({}, markoCompiler.defaultOptions, compilerOptions);
}
var writeToDisk = compilerOptions.writeToDisk;
var templateSrc;
var compiledSrc;
@ -51,7 +54,7 @@ function compile(templatePath, markoCompiler, compilerOptions) {
compiledSrc = fs.readFileSync(targetFile, fsReadOptions);
} else {
templateSrc = fs.readFileSync(templatePath, fsReadOptions);
compiledSrc = markoCompiler.compile(templateSrc, templatePath);
compiledSrc = markoCompiler.compile(templateSrc, templatePath, compilerOptions);
// Write to a temporary file and move it into place to avoid problems
// assocatiated with multiple processes write to the same file. We only
@ -76,7 +79,7 @@ function getLoadedTemplate(path) {
exports.install = function(options) {
options = options || {};
var compilerOptions = options.compilerOptions || {};
var compilerOptions = options.compilerOptions;
var extension = options.extension || '.marko';

View File

@ -28,12 +28,13 @@
],
"dependencies": {
"app-module-path": "^1.0.5",
"async-writer": "^1.4.0",
"async-writer": "^1.4.4",
"browser-refresh-client": "^1.0.0",
"char-props": "~0.1.5",
"deresolve": "^1.0.0",
"esprima": "^2.7.0",
"events": "^1.0.2",
"he": "^1.1.0",
"htmljs-parser": "^1.5.3",
"lasso-package-root": "^1.0.0",
"marko-vdom": "^0.3.0",
@ -59,7 +60,9 @@
"chai": "^3.3.0",
"coveralls": "^2.11.9",
"express": "^4.13.4",
"fs-extra": "^0.30.0",
"istanbul": "^0.4.3",
"jsdom": "^9.6.0",
"jshint": "^2.5.0",
"mocha": "^2.3.3",
"request": "^2.72.0",
@ -69,7 +72,7 @@
"bin": {
"markoc": "bin/markoc"
},
"main": "runtime/marko-runtime.js",
"main": "runtime/html/index.js",
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},

View File

@ -15,14 +15,7 @@
*/
'use strict';
var escapeXml = require('raptor-util/escapeXml');
var escapeXmlAttr = escapeXml.attr;
var runtime = require('./'); // Circular dependency, but that is okay
var attr = require('raptor-util/attr');
var isArray = Array.isArray;
var STYLE_ATTR = 'style';
var CLASS_ATTR = 'class';
var escapeEndingScriptTagRegExp = /<\//g;
function classListHelper(arg, classNames) {
var len;
@ -109,8 +102,9 @@ module.exports = {
* @private
*/
s: function(str) {
return (str == null) ? '' : str;
return (str == null) ? '' : str.toString();
},
/**
* Internal helper method to handle loops with a status variable
* @private
@ -172,128 +166,9 @@ module.exports = {
}
}
},
/**
* Internal method to escape special XML characters
* @private
*/
x: escapeXml,
/**
* Internal method to escape special XML characters within an attribute
* @private
*/
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.
*/
xs: function(val) {
return (typeof val === 'string') ? val.replace(escapeEndingScriptTagRegExp, '\\u003C/') : val;
},
/**
* Internal method to render a single HTML attribute
* @private
*/
a: attr,
/**
* 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;
}
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) {
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 '';
}
},
/**
* 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 '';
}
if (typeof classNames === 'string') {
return attr(CLASS_ATTR, classNames, false);
} else {
return attr(CLASS_ATTR, classList(classNames), false);
}
},
/**
* Loads a template (__helpers.l --> loadTemplate(path))
*/
l: function(path) {
if (typeof path === 'string') {
return runtime.load(path);
} else {
// Assume it is already a pre-loaded template
return path;
}
},
// ----------------------------------
// The helpers listed below require an out
// ----------------------------------
/**
* Invoke a tag handler render function
* Helper to load a custom tag
*/
t: function (renderer, targetProperty, isRepeated, hasNestedTags) {
if (renderer) {
@ -330,6 +205,13 @@ module.exports = {
}
},
// ----------------------------------
// The helpers listed below require an out
// ----------------------------------
/**
* Internal method to handle includes/partials
* @private

147
runtime/html/helpers.js Normal file
View File

@ -0,0 +1,147 @@
/*
* 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 escapeXml = require('raptor-util/escapeXml');
var escapeXmlAttr = escapeXml.attr;
var runtime = require('../'); // Circular dependency, but that is okay
var attr = require('raptor-util/attr');
var extend = require('raptor-util/extend');
var STYLE_ATTR = 'style';
var CLASS_ATTR = 'class';
var escapeEndingScriptTagRegExp = /<\//g;
var commonHelpers = require('../helpers');
var classList = commonHelpers.cl;
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,
/**
* 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;
},
/**
* Internal method to render a single HTML attribute
* @private
*/
a: attr,
/**
* 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;
}
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) {
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 '';
}
},
/**
* 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 '';
}
if (typeof classNames === 'string') {
return attr(CLASS_ATTR, classNames, false);
} else {
return attr(CLASS_ATTR, classList(classNames), false);
}
},
/**
* Loads a template (__helpers.l --> marko_loadTemplate(path))
*/
l: function(path) {
if (typeof path === 'string') {
return runtime.load(path);
} else {
// Assume it is already a pre-loaded template
return path;
}
}
}, commonHelpers);

View File

@ -88,15 +88,18 @@ Template.prototype = {
this._ = createFunc(helpers);
},
renderSync: function(data) {
var localData = data || {};
var out = new AsyncWriter();
out.sync();
var localData;
var globalData;
if (localData.$global) {
out.global = extend(out.global, localData.$global);
localData.$global = null;
if ((localData = data)) {
globalData = localData.$global;
} else {
localData = {};
}
var out = new AsyncWriter(null, null, globalData);
out.sync();
this._(localData, out);
return out.getOutput();
},
@ -275,4 +278,4 @@ exports._inline = createInlineMarkoTemplate;
// loaded and cached. On the server, the loader will use
// the compiler to compile the template and then load the generated
// module file using the Node.js module loader
loader = require('./loader');
loader = require('../loader');

View File

@ -20,8 +20,7 @@ var fs = require('fs');
var Module = require('module').Module;
var markoCompiler = require('../../compiler');
var cwd = process.cwd();
var fsReadOptions = {encoding: 'utf8'};
var extend = require('raptor-util/extend');
var fsOptions = {encoding: 'utf8'};
if (process.env.hasOwnProperty('MARKO_HOT_RELOAD')) {
require('../../hot-reload').enable();
@ -79,6 +78,8 @@ function getLoadedTemplate(path) {
}
function loadFile(templatePath, options) {
options = Object.assign({}, markoCompiler.defaultOptions, options);
var targetFile = templatePath + '.js';
// Short-circuit loading if the template has already been cached in the Node.js require cache
@ -97,8 +98,6 @@ function loadFile(templatePath, options) {
return cachedTemplate;
}
options = extend(extend({}, markoCompiler.defaultOptions), options);
// If the `assumeUpToDate` option is true then we just assume that the compiled template on disk is up-to-date
// if it exists
if (options.assumeUpToDate) {
@ -120,22 +119,16 @@ function loadFile(templatePath, options) {
var filename = nodePath.basename(targetFile);
var targetDir = nodePath.dirname(targetFile);
var tempFile = nodePath.join(targetDir, '.' + process.pid + '.' + Date.now() + '.' + filename);
fs.writeFileSync(tempFile, compiledSrc, fsReadOptions);
fs.writeFileSync(tempFile, compiledSrc, fsOptions);
fs.renameSync(tempFile, targetFile);
return require(targetFile);
}
module.exports = function load(templatePath, templateSrc, options) {
var writeToDisk;
options = Object.assign({}, markoCompiler.defaultOptions, options);
if (options && (options.writeToDisk != null)) {
// options is provided and options.writeToDisk is non-null
writeToDisk = options.writeToDisk;
} else {
// writeToDisk should be inferred from defaultOptions
writeToDisk = markoCompiler.defaultOptions.writeToDisk;
}
var writeToDisk = options.writeToDisk;
// If the template source is provided then we can compile the string
// in memory and there is no need to read template file from disk or
@ -148,10 +141,16 @@ module.exports = function load(templatePath, templateSrc, options) {
// directly from the compiled source using the internals of the
// Node.js module loading system.
if (templateSrc === undefined) {
templateSrc = fs.readFileSync(templatePath, fsReadOptions);
templateSrc = fs.readFileSync(templatePath, fsOptions);
}
var compiledSrc = markoCompiler.compile(templateSrc, templatePath, options);
if (writeToDisk === true) {
var targetFile = templatePath + '.js';
fs.writeFileSync(targetFile, compiledSrc, fsOptions);
}
return loadSource(templatePath, compiledSrc);
} else {
return loadFile(templatePath, options);

View File

@ -1,7 +1,6 @@
{
"main": "./marko-runtime.js",
"main": "./html/index.js",
"browser": {
"./loader/index.js": "./loader/index-browser.js",
"./stream/index.js": "./stream/index-browser.js"
"./loader/index.js": "./loader/index-browser.js"
}
}

View File

@ -45,7 +45,7 @@ Readable.prototype = {
require('raptor-util/inherit')(Readable, stream.Readable);
require('./').Template.prototype.stream = function(data) {
require('./html').Template.prototype.stream = function(data) {
if (!stream) {
throw new Error('Module not found: stream');
}

View File

@ -17,8 +17,13 @@
'use strict';
var markoVDOM = require('marko-vdom');
var commonHelpers = require('../helpers');
var extend = require('raptor-util/extend');
var runtime;
module.exports = {
var classList = commonHelpers.cl;
module.exports = extend({
e: markoVDOM.createElement,
t: markoVDOM.createText,
const: function(id) {
@ -26,5 +31,66 @@ module.exports = {
return function() {
return id + (i++);
};
},
/**
* Loads a template (__helpers.l --> marko_loadTemplate(path))
*/
l: function(path) {
if (typeof path === 'string') {
return runtime.load(path);
} else {
// Assume it is already a pre-loaded template
return path;
}
},
/**
* Helper for generating the string for a style attribute
* @param {[type]} style [description]
* @return {[type]} [description]
*/
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;
}
},
/**
* 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;
}
if (typeof classNames === 'string') {
return classNames;
} else {
return classList(classNames);
}
}
};
}, commonHelpers);
runtime = require('./');

View File

@ -0,0 +1,281 @@
/*
* 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.
*/
/**
* This module provides the lightweight runtime for loading and rendering
* templates. The compilation is handled by code that is part of the
* [marko/compiler](https://github.com/raptorjs/marko/tree/master/compiler)
* module. If rendering a template on the client, only the runtime is needed
* on the client and not the compiler
*/
// async-writer provides all of the magic to support asynchronous
// rendering to a stream
'use strict';
/**
* Method is for internal usage only. This method
* is invoked by code in a compiled Marko template and
* it is used to create a new Template instance.
* @private
*/
exports.c = function createTemplate(path) {
return new Template(path);
};
var asyncVdomBuilder = require('async-vdom-builder');
// helpers provide a core set of various utility methods
// that are available in every template (empty, notEmpty, etc.)
var helpers = require('./helpers');
var loader;
// If the optional "stream" module is available
// then Readable will be a readable stream
var AsyncVDOMBuilder = asyncVdomBuilder.AsyncVDOMBuilder;
var extend = require('raptor-util/extend');
function renderCallback(renderFunc, data, globalData, callback) {
var out = new AsyncVDOMBuilder(globalData);
if (globalData) {
extend(out.global, globalData);
}
renderFunc(data, out);
return out.end()
.on('finish', function() {
callback(null, out.getOutput(), out);
})
.once('error', callback);
}
function Template(path, func) {
this.path = path;
this._ = func;
}
Template.prototype = {
createOut() {
return new AsyncVDOMBuilder();
},
/**
* Internal method to initialize a loaded template with a
* given create function that was generated by the compiler.
* Warning: User code should not depend on this method.
*
* @private
* @param {Function(__helpers)} createFunc The function used to produce the render(data, out) function.
*/
c: function(createFunc) {
this._ = createFunc(helpers);
},
renderSync: function(data) {
var localData;
var globalData;
if (data) {
localData = data;
globalData = data.$global;
localData.$global = null;
} else {
localData = {};
}
var out = new AsyncVDOMBuilder(globalData);
out.sync();
this._(localData, out);
return out.getOutput();
},
/**
* Renders a template to either a stream (if the last
* argument is a Stream instance) or
* provides the output to a callback function (if the last
* argument is a Function).
*
* Supported signatures:
*
* render(data, callback)
* render(data, out)
* render(data, stream)
* render(data, out, callback)
* render(data, stream, callback)
*
* @param {Object} data The view model data for the template
* @param {AsyncVDOMBuilder} out A Stream or an AsyncVDOMBuilder instance
* @param {Function} callback A callback function
* @return {AsyncVDOMBuilder} Returns the AsyncVDOMBuilder instance that the template is rendered to
*/
render: function(data, out, callback) {
var renderFunc = this._;
var finalData;
var globalData;
if (data) {
finalData = data;
if ((globalData = data.$global)) {
// We will *move* the "$global" property
// into the "out.global" object
data.$global = null;
}
} else {
finalData = {};
}
if (typeof out === 'function') {
// Short circuit for render(data, callback)
return renderCallback(renderFunc, finalData, globalData, out);
}
// NOTE: We create new vars here to avoid a V8 de-optimization due
// to the following:
// Assignment to parameter in arguments object
var finalOut = out;
var shouldEnd = false;
if (arguments.length === 3) {
if (globalData) {
extend(finalOut.global, globalData);
}
finalOut
.on('finish', function() {
callback(null, finalOut.getOutput(), finalOut);
})
.once('error', callback);
} else if (!finalOut) {
// Assume the "finalOut" is really a stream
//
// By default, we will buffer rendering to a stream to prevent
// the response from being "too chunky".
finalOut = new AsyncVDOMBuilder(globalData);
shouldEnd = true;
}
// Invoke the compiled template's render function to have it
// write out strings to the provided out.
renderFunc(finalData, finalOut);
// Automatically end output stream (the writer) if we
// had to create an async writer (which might happen
// if the caller did not provide a writer/out or the
// writer/out was not an AsyncVDOMBuilder).
//
// If out parameter was originally an AsyncVDOMBuilder then
// we assume that we are writing to output that was
// created in the context of another rendering job.
return shouldEnd ? finalOut.end() : finalOut;
}
};
function createRenderProxy(template) {
return function(data, out) {
template._(data, out);
};
}
function initTemplate(rawTemplate, templatePath) {
if (rawTemplate.render) {
return rawTemplate;
}
var createFunc = rawTemplate.create || rawTemplate;
var template = createFunc.loaded;
if (!template) {
template = createFunc.loaded = new Template(templatePath);
template.c(createFunc);
}
return template;
}
function load(templatePath, templateSrc, options) {
if (!templatePath) {
throw new Error('"templatePath" is required');
}
if (arguments.length === 1) {
// templateSrc and options not provided
} else if (arguments.length === 2) {
// see if second argument is templateSrc (a String)
// or options (an Object)
var lastArg = arguments[arguments.length - 1];
if (typeof lastArg !== 'string') {
options = arguments[1];
templateSrc = undefined;
}
} else if (arguments.length === 3) {
// assume function called according to function signature
} else {
throw new Error('Illegal arguments');
}
var template;
if (typeof templatePath === 'string') {
template = initTemplate(loader(templatePath, templateSrc, options), templatePath);
} else if (templatePath.render) {
template = templatePath;
} else {
template = initTemplate(templatePath);
}
if (options && (options.buffer != null)) {
template = new Template(
template.path,
createRenderProxy(template),
options);
}
return template;
}
function createInlineMarkoTemplate(filename, renderFunc) {
return new Template(filename, renderFunc);
}
exports.load = load;
exports.createOut = function() {
return new AsyncVDOMBuilder();
};
exports.helpers = helpers;
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;
};
// The loader is used to load templates that have not already been
// loaded and cached. On the server, the loader will use
// the compiler to compile the template and then load the generated
// module file using the Node.js module loader
loader = require('../loader');

View File

@ -16,9 +16,12 @@
'use strict';
module.exports = function render(input, out) {
out.write('<!--');
if (input.renderBody) {
input.renderBody(out);
if (out.write) {
out.write('<!--');
if (input.renderBody) {
input.renderBody(out);
}
out.write('-->');
}
out.write('-->');
};

View File

@ -1,4 +1,4 @@
module.exports = function render(input, context) {
module.exports = function render(input, out) {
var content = {};
if (input.getContent) {
@ -20,5 +20,5 @@ module.exports = function render(input, context) {
}
}
templateData.layoutContent = content;
input.__template.render(templateData, context);
input.__template.render(templateData, out);
};

View File

@ -22,9 +22,9 @@ var path = require('path');
var assert = require('assert');
function compareHelper(dir, actual, suffix) {
var actualPath = path.join(dir, 'actual' + suffix);
var expectedPath = path.join(dir, 'expected' + suffix);
function compareHelper(dir, actual, prefix, suffix) {
var actualPath = path.join(dir, prefix + 'actual' + suffix);
var expectedPath = path.join(dir, prefix + 'expected' + suffix);
var isObject = typeof actual === 'string' ? false : true;
var actualString = isObject ? JSON.stringify(actual, null, 4) : actual;
@ -51,8 +51,12 @@ function autoTest(name, dir, run, options, done) {
options = options || {};
var helpers = {
compare(actual, suffix) {
compareHelper(dir, actual, suffix);
compare(actual, prefix, suffix) {
if (arguments.length === 2) {
suffix = prefix;
prefix = null;
}
compareHelper(dir, actual, prefix || '', suffix || '');
}
};
@ -67,6 +71,10 @@ exports.scanDir = function(autoTestDir, run, options) {
return;
}
if (name.endsWith('.skip')) {
return;
}
if (enabledTests && !enabledTests[name]) {
return;
}

View File

@ -2,6 +2,10 @@ actual.js
actual.html
actual.json
*.marko.js
*.marko.vdom.js
actual.txt
error.txt
actual-*
actual-*
*.generated.*
__vdom__
*.skip/

View File

@ -1,3 +1,3 @@
<div foo='Hello ${data.foo}'>
<div foo='Hello ${data.foo || ''}'>
Hello World!
</div>

View File

@ -1,3 +1,3 @@
<div foo='Hello $!{data.foo}'>
<div foo='Hello $!{data.foo || ''}'>
Hello World!
</div>

View File

@ -1 +1 @@
<hello>
&lt;hello>

View File

@ -0,0 +1 @@
<div class="foo baz"></div>

View File

@ -0,0 +1,2 @@
<div.foo class={ bar: false, baz: true }>
</div>

View File

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

View File

@ -1,13 +1,24 @@
exports.render = function(input, out) {
out.write('Hello ' + input.name + '!');
var text = 'Hello ' + input.name + '!';
if (input.adult === true) {
out.write(' (adult)');
text += ' (adult)';
} else if (input.adult === false) {
out.write(' (child)');
text += ' (child)';
}
if (input.renderBody) {
text += ' BODY: ';
}
if (out.write) {
out.write(text);
} else {
out.text(text);
}
if (input.renderBody) {
out.write(' BODY: ');
input.renderBody(out);
}

View File

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

View File

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

View File

@ -1,3 +1,5 @@
exports.templateData = {
attrs: ' foo="bar" baz'
};
exports.vdomSkip = true;

View File

@ -1 +1 @@
<p><div><span>Hello Frank!</span></div></p>
<p><span><a>Hello Frank!</a></span></p>

View File

@ -1,6 +1,6 @@
p
<div>
<span>
<span>
<a>
Hello ${data.name}!
</>
</>

View File

@ -1 +1 @@
<div data-attr="Hello &quot;John&quot; &lt;foo&gt;">Hello &lt;John&gt;© <hello></div> &copy;
<div data-attr="Hello &quot;John&quot; &lt;foo&gt;">Hello &lt;John&gt;© &lt;hello></div> &copy;

View File

@ -7,7 +7,7 @@ exports.checkError = function(e) {
expect(e.message).to.contain('<custom-tag>');
//includes the line number of the template
expect(e.message).to.contain('error-thrown-in-generator/template.marko:2');
expect(e.message).to.contain('template.marko:2');
//retains original stack trace
expect(e.stack.toString()).to.contain('custom-tag.js:2:11');

View File

@ -3,3 +3,5 @@ exports.templateData = {
name: 'Evil </script>'
}
};
exports.vdomSkip = true;

View File

@ -1 +1 @@
<!--[if lt IE 9]><div><![endif]-->
<!--This is a comment-->

View File

@ -1 +1 @@
<html-comment><![CDATA[[if lt IE 9]><div><![endif]]]></html-comment>
<html-comment>This is a comment</html-comment>

View File

@ -1,3 +1,5 @@
exports.templateData = {
"name": "World"
};
exports.vdomSkip = true;

View File

@ -1,3 +1,5 @@
exports.templateData = {
"name": "World"
};
exports.vdomSkip = true;

View File

@ -1 +1 @@
<div><span if(foo)> Hello Frank! </span></div>
<div>&lt;span if(foo)> Hello Frank! &lt;/span></div>

View File

@ -1 +1 @@
<div><span if(foo)> Hello ${THIS IS NOT VALID}! </span></div>
<div>&lt;span if(foo)> Hello ${THIS IS NOT VALID}! &lt;/span></div>

View File

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

View File

@ -1,5 +1,5 @@
<div class="overlay">
<div class="overlay-header ${data.header.className}" if(data.header)>
<div class="overlay-header ${data.header.className || ''}" if(data.header)>
<invoke data.header.renderBody(out)/>
</div>

View File

@ -1 +1 @@
<div class="overlay"><div class="overlay-header ">Header content!</div><div class="overlay-body my-body">Body content</div><div class="overlay-footer my-footer">Footer content</div></div>
<div class="overlay"><div class="overlay-header">Header content!</div><div class="overlay-body my-body">Body content</div><div class="overlay-footer my-footer">Footer content</div></div>

View File

@ -1,5 +1,5 @@
<div class="overlay">
<div class="overlay-header ${data.header.className}" if(data.header)>
<div class="overlay-header" if(data.header)>
<invoke data.header.renderBody(out)/>
</div>

View File

@ -1,2 +1,3 @@
exports.templateData = {};
exports.preserveWhitespaceGlobal = true;
exports.preserveWhitespaceGlobal = true;
exports.vdomSkip = true;

View File

@ -2,3 +2,4 @@ exports.templateData = {};
exports.loadOptions = {
preserveWhitespace: true
};
exports.vdomSkip = true;

View File

@ -1,3 +1,5 @@
exports.templateData = {
"name": "<script>evil</script>"
};
exports.vdomSkip = true;

View File

@ -1 +1,3 @@
exports.templateData = {};
exports.vdomSkip = true;

View File

@ -1 +1,3 @@
exports.templateData = {};
exports.vdomSkip = true;

View File

@ -1,3 +1,5 @@
exports.templateData = {
colors: ['red', 'green', 'blue']
};
exports.vdomSkip = true;

View File

@ -1,3 +1,5 @@
exports.templateData = {
colors: ['red', 'green', 'blue']
};
exports.vdomSkip = true;

View File

@ -1,3 +1,5 @@
exports.templateData = {
colors: ['red', 'green', 'blue']
};
exports.vdomSkip = true;

View File

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

View File

@ -1 +1 @@
<p>A <i>B</i> C</p> --- <p>D <i>E</i> F</p> --- <p>G <i>H</i> I</p> --- <p>J <div>K</div> L <div>M</div> N</p> --- <p><div>O</div><div>P</div><span>Q</span> <span>R</span></p>
<p>A <i>B</i> C</p> --- <p>D <i>E</i> F</p> --- <p>G <i>H</i> I</p> --- <p>J <strong>K</strong> L <strong>M</strong> N</p> --- <p><strong>O</strong><strong>P</strong><span>Q</span> <span>R</span></p>

View File

@ -14,16 +14,16 @@
---
<p>
J
<div>K</div>
<strong>K</strong>
L
<div>M</div>
<strong>M</strong>
N
</p>
---
<p>
<div>O</div>
<strong>O</strong>
<div>P</div>
<strong>P</strong>
<span>Q</span> <span>R</span>
</p>

View File

@ -0,0 +1,19 @@
function create(__markoHelpers) {
var marko_classList = __markoHelpers.cl,
marko_str = __markoHelpers.s,
marko_classAttr = __markoHelpers.ca;
return function render(data, out) {
out.e("div", {
"class": marko_classAttr(marko_classList("foo", {
bar: true,
baz: false
}))
}, 1)
.t("Hello " +
marko_str(name) +
"!");
};
}
(module.exports = require("marko/vdom").c(__filename)).c(create);

View File

@ -0,0 +1,3 @@
<div.foo class={ bar: true, baz: false }>
Hello ${name}!
</div>

View File

@ -1,10 +1,14 @@
function create(__markoHelpers) {
var marko_str = __markoHelpers.s;
return function render(data, out) {
out.e("div", {
foo: "bar",
hello: "world"
}, 1)
.t(("Hello " + name) + "!");
.t("Hello " +
marko_str(name) +
"!");
};
}

View File

@ -1,4 +1,6 @@
function create(__markoHelpers) {
var marko_str = __markoHelpers.s;
return function render(data, out) {
var attrs = {
foo: "bar",
@ -6,7 +8,9 @@ function create(__markoHelpers) {
};
out.e("div", attrs, 1)
.t(("Hello " + name) + "!");
.t("Hello " +
marko_str(name) +
"!");
};
}

View File

@ -1,11 +1,14 @@
function create(__markoHelpers) {
var marko_attrs0 = {
var marko_str = __markoHelpers.s,
marko_attrs0 = {
"class": "foo"
};
return function render(data, out) {
out.e("div", marko_attrs0, 1)
.t(("Hello " + name) + "!");
.t("Hello " +
marko_str(name) +
"!");
};
}

View File

@ -0,0 +1,13 @@
function create(__markoHelpers) {
var marko_str = __markoHelpers.s;
return function render(data, out) {
out.t("Hello " +
marko_str(name) +
"! ");
out.h(marko_str(message));
};
}
(module.exports = require("marko/vdom").c(__filename)).c(create);

View File

@ -0,0 +1 @@
- Hello ${name}! $!{message}

View File

@ -1,21 +1,24 @@
function create(__markoHelpers) {
var marko_forEach = __markoHelpers.f,
marko_createElement = require("marko/vdom/createElement"),
marko_const = require("marko/runtime/vdom/const"),
var marko_str = __markoHelpers.s,
marko_forEach = __markoHelpers.f,
marko_createElement = __markoHelpers.e,
marko_const = __markoHelpers.const,
marko_const_nextId = marko_const("733fee"),
marko_node0 = marko_createElement("div", null, 1, marko_const_nextId())
.t("No colors!");
return function render(data, out) {
out.e("h1", null, 1)
.t(("Hello " + data.name) + "!");
.t("Hello " +
marko_str(data.name) +
"!");
if (data.colors.length) {
out.be("ul");
marko_forEach(data.colors, function(color) {
out.e("li", null, 1)
.t(color);
.t(marko_str(color));
});
out.ee();

View File

@ -1,6 +1,7 @@
function create(__markoHelpers) {
var marko_createElement = require("marko/vdom/createElement"),
marko_const = require("marko/runtime/vdom/const"),
var marko_str = __markoHelpers.s,
marko_createElement = __markoHelpers.e,
marko_const = __markoHelpers.const,
marko_const_nextId = marko_const("69a896"),
marko_node0 = marko_createElement("div", {
"class": "hello",
@ -11,7 +12,9 @@ function create(__markoHelpers) {
return function render(data, out) {
out.e("span", null, 2)
.e("h1", null, 1)
.t(("Hello " + data.name) + "!")
.t("Hello " +
marko_str(data.name) +
"!")
.n(marko_node0);
};
}

View File

@ -1,6 +1,6 @@
function create(__markoHelpers) {
var marko_createElement = require("marko/vdom/createElement"),
marko_const = require("marko/runtime/vdom/const"),
var marko_createElement = __markoHelpers.e,
marko_const = __markoHelpers.const,
marko_const_nextId = marko_const("0524f9"),
marko_node0 = marko_createElement("div", {
"class": "hello",

View File

@ -0,0 +1,15 @@
function create(__markoHelpers) {
var marko_loadTag = __markoHelpers.t,
test_hello = marko_loadTag(require("./tags/test-hello/renderer"));
return function render(data, out) {
test_hello({
name: "World",
renderBody: function renderBody(out) {
out.t("Body content");
}
}, out);
};
}
(module.exports = require("marko/vdom").c(__filename)).c(create);

View File

@ -0,0 +1,3 @@
{
"tags-dir": "./tags"
}

View File

@ -0,0 +1,5 @@
{
"renderer": "./renderer.js",
"@name": "string",
"@adult": "boolean"
}

View File

@ -0,0 +1,2 @@
exports.render = function(input, out) {
};

View File

@ -0,0 +1,3 @@
<test-hello name="World">
Body content
</test-hello>

View File

@ -4,10 +4,17 @@ var Module = require('module').Module;
var oldResolveFilename = Module._resolveFilename;
var rootDir = nodePath.join(__dirname, '../');
Module._resolveFilename = function(request, parent, isMain) {
if (request.startsWith('marko')) {
request = request.substring('marko'.length);
request = rootDir + request;
}
if (request.charAt(0) !== '.') {
var firstSlash = request.indexOf('/');
var targetPackageName = firstSlash === -1 ? request : request.substring(0, firstSlash);
if (targetPackageName === 'marko') {
request = request.substring('marko'.length);
request = rootDir + request;
}
}
return oldResolveFilename.call(this, request, parent, isMain);
};

117
test/util/domToHTML.js Normal file
View File

@ -0,0 +1,117 @@
function ltrim(s) {
return s ? s.replace(/^\s\s*/,'') : '';
}
function vdomToHTML(node, options) {
// NOTE: We don't use XMLSerializer because we need to sort the attributes to correctly compare output HTML strings
// BAD: return (new XMLSerializer()).serializeToString(node);
var html = '';
function serializeHelper(node, indent) {
if (node.nodeType === 1) {
serializeElHelper(node, indent);
} else if (node.nodeType === 3) {
serializeTextHelper(node, indent);
} else if (node.nodeType === 8) {
serializeCommentHelper(node, indent);
} else {
console.log('Invalid node:', node);
html += indent + `INVALID NODE TYPE ${node.nodeType}\n`;
// throw new Error('Unexpected node type');
}
}
function serializeElHelper(el, indent) {
var tagName = el.nodeName;
if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
tagName = 'svg:' + tagName;
} else if (el.namespaceURI === 'http://www.w3.org/1998/Math/MathML') {
tagName = 'math:' + tagName;
}
html += indent + '<' + tagName;
var attributes = el.attributes;
var attributesArray = [];
var attrName;
if (typeof attributes.length === 'number') {
for (var i=0; i<attributes.length; i++) {
var attr = attributes[i];
if (attr.namespaceURI) {
attrName = attr.namespaceURI + ':' + attr.localName;
} else {
attrName = attr.name;
}
if (attrName === 'data-marko-const') {
continue;
}
attributesArray.push(' ' + attrName + '="' + attr.value + '"');
}
} else {
for (attrName in attributes) {
if (attrName === 'data-marko-const') {
continue;
}
var attrValue = attributes[attrName];
if (typeof attrValue !== 'string') {
if (attrValue === true) {
attrValue = '';
} else if (!attrValue) {
continue;
}
}
if (attrName === 'xlink:href') {
attrName = 'http://www.w3.org/1999/xlink:href';
}
attributesArray.push(' ' + attrName + '="' + attrValue + '"');
}
}
attributesArray.sort();
html += attributesArray.join('');
html += '>\n';
if (tagName.toUpperCase() === 'TEXTAREA') {
html += indent + ' VALUE: ' + JSON.stringify(ltrim(el.value)) + '\n';
} else {
if (tagName.toUpperCase() === 'PRE' && el.firstChild && el.firstChild.nodeType === 3) {
el.firstChild.nodeValue = ltrim(el.firstChild.nodeValue);
}
var curChild = el.firstChild;
while(curChild) {
serializeHelper(curChild, indent + ' ');
curChild = curChild.nextSibling;
}
}
}
function serializeTextHelper(node, indent) {
html += indent + JSON.stringify(node.nodeValue) + '\n';
}
function serializeCommentHelper(node, indent) {
html += indent + '<!--' + JSON.stringify(node.nodeValue) + '-->\n';
}
if (node.nodeType === 11 /* DocumentFragment */ || (options && options.childrenOnly)) {
var curChild = node.firstChild;
while(curChild) {
serializeHelper(curChild, '');
curChild = curChild.nextSibling;
}
} else {
serializeHelper(node, '');
}
return html;
}
module.exports = vdomToHTML;

139
test/vdom-render-test.js Normal file
View File

@ -0,0 +1,139 @@
'use strict';
require('./patch-module');
var chai = require('chai');
chai.config.includeStack = true;
var path = require('path');
var marko = require('../');
var markoVDOM = require('../vdom');
var autotest = require('./autotest');
var fs = require('fs');
var fsExtra = require('fs-extra');
var domToHTML = require('./util/domToHTML');
var jsdom = require("jsdom").jsdom;
require('../node-require').install();
var defaultDocument = jsdom('<html><body></body></html>');
markoVDOM.setDocument(defaultDocument); // We need this to parse HTML fragments on the server
describe('render-vdom', function() {
var autoTestDir = path.join(__dirname, 'autotests/render');
autotest.scanDir(
autoTestDir,
function run(dir, helpers, done) {
require('../compiler').configure({ output: 'html' });
var vdomDir = path.join(dir, '../' + path.basename(dir) + '_vdom.skip');
fsExtra.removeSync(vdomDir);
fsExtra.copySync(dir, vdomDir, {
filter: function(file) {
if (file.endsWith('.marko.js') || file.indexOf('.generated.') !== -1) {
return false;
}
return true;
}
});
var htmlTemplatePath = path.join(dir, 'template.marko');
var vdomMainPath = path.join(vdomDir, 'test.js');
var htmlMainPath = path.join(dir, 'test.js');
var htmlMain = fs.existsSync(htmlMainPath) ? require(htmlMainPath) : {};
var htmlTemplate = htmlMain.checkError ? null : marko.load(htmlTemplatePath);
require('../compiler').configure({ output: 'vdom' });
var oldDone = done;
done = function(err) {
require('../compiler').configure({ output: 'html' });
oldDone(err);
};
var vdomMain = fs.existsSync(vdomMainPath) ? require(vdomMainPath) : {};
if (vdomMain && vdomMain.vdomSkip) {
return done();
}
var loadOptions = vdomMain.loadOptions;
if (loadOptions) {
loadOptions = Object.assign({}, loadOptions);
} else {
loadOptions = {};
}
loadOptions.output = 'vdom';
// loadOptions.writeToDisk = false;
if (vdomMain.writeToDisk === false) {
require('marko/compiler').defaultOptions.writeToDisk = false;
}
if (vdomMain.preserveWhitespaceGlobal === true) {
require('marko/compiler').defaultOptions.preserveWhitespace = true;
}
var templateSrc = fs.readFileSync(htmlTemplatePath, { encoding: 'utf8' });
var vdomTemplatePath = path.join(vdomDir, 'template.marko');
try {
if (vdomMain.checkError) {
var e;
try {
marko.load(vdomTemplatePath, templateSrc, loadOptions);
} catch(_e) {
e = _e;
var errorFile = path.join(dir, 'error.txt');
fs.writeFileSync(errorFile, e.stack.toString(), { encoding: 'utf8' });
}
if (!e) {
throw new Error('Error expected');
}
vdomMain.checkError(e);
require('../compiler').configure({ output: 'html' });
return done();
} else {
var vdomTemplate = marko.load(vdomTemplatePath, loadOptions);
var templateData = vdomMain.templateData || {};
var vdomTree = vdomTemplate.renderSync(templateData);
var expectedHtml;
try {
expectedHtml = fs.readFileSync(path.join(dir, 'vdom-expected.html'), { encoding: 'utf8'});
} catch(e) {}
if (!expectedHtml) {
var html = htmlTemplate.renderSync(htmlMain.templateData || {});
var document = jsdom('<html><body>' + html + '</body></html>');
expectedHtml = domToHTML(document.body, { childrenOnly: true });
}
fs.writeFileSync(path.join(dir, 'vdom-expected.generated.html'), expectedHtml, { encoding: 'utf8' });
var vdomHtml = domToHTML(vdomTree.actualize(defaultDocument));
helpers.compare(vdomHtml, 'vdom-', '.generated.html');
require('../compiler').configure({ output: 'html' });
return done();
}
} finally {
if (vdomMain.writeToDisk === false) {
delete require('marko/compiler').defaultOptions.writeToDisk;
}
if (vdomMain.preserveWhitespaceGlobal === true) {
delete require('marko/compiler').defaultOptions.preserveWhitespace;
}
}
});
});