foam-framework
Version:
MVC metaprogramming framework
398 lines (341 loc) • 12.7 kB
JavaScript
/**
* @license
* Copyright 2013 Google Inc. All Rights Reserved.
*
* 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.
*/
/**
* Simple template system modelled after JSP's.
*
* Syntax:
* <% code %>: code inserted into template, but nothing implicitly output
* <%= comma-separated-values %>: all values are appended to template output
* <%# expression %>: dynamic (auto-updating) expression is output
* \<new-line>: ignored
* %%value(<whitespace>|<): output a single value to the template output
* $$feature(<whitespace>|<): output the View or Action for the current Value
*/
MODEL({
name: 'TemplateParser',
extends: 'grammar',
methods: {
START: sym('markup'),
markup: repeat0(alt(
sym('comment'),
sym('foamTag'),
sym('create child'),
sym('simple value'),
sym('live value tag'),
sym('raw values tag'),
sym('values tag'),
sym('code tag'),
sym('ignored newline'),
sym('newline'),
sym('single quote'),
sym('text')
)),
'comment': seq1(1, '<!--', repeat0(not('-->', anyChar)), '-->'),
'foamTag': sym('foamTag_'),
'foamTag_': function() { }, // placeholder until gets filled in after HTMLParser is built
'create child': seq(
'$$',
repeat(notChars(' $\r\n<{,.')),
optional(JSONParser.export('objAsString'))),
'simple value': seq('%%', repeat(notChars(' ()-"\r\n><:;,')), optional('()')),
'live value tag': seq('<%#', repeat(not('%>', anyChar)), '%>'),
'raw values tag': alt(
seq('<%=', repeat(not('%>', anyChar)), '%>'),
seq('{{{', repeat(not('}}}', anyChar)), '}}}')
),
'values tag': seq('{{', repeat(not('}}', anyChar)), '}}'),
'code tag': seq('<%', repeat(not('%>', anyChar)), '%>'),
'ignored newline': alt(
literal('\\\r\\\n'),
literal('\\\n')
),
newline: alt(
literal('\r\n'),
literal('\n')
),
'single quote': literal("'"),
text: anyChar
}
});
var TemplateOutput = {
/**
* obj - Parent object. If objects are output and have an initHTML() method, then they
* are added to the parent by calling obj.addChild().
**/
// TODO(kgr): redesign, I think this is actually broken. If we call appendHTML() of
// a sub-view then it will be added to the wrong parent.
create: function(obj) {
console.assert(obj, 'Owner required when creating TemplateOutput.');
var buf = [];
var f = function templateOut(/* arguments */) {
for ( var i = 0 ; i < arguments.length ; i++ ) {
var o = arguments[i];
if ( typeof o === 'string' ) {
buf.push(o);
} else if ( o && 'Element' === o.name_ ) {
// Temporary bridge for working with foam.u2 Views
var s = o.createOutputStream();
o.output(s);
buf.push(s.toString());
// Needs to be bound, since o is a loop variable and will otherwise
// be the final element of the arguments array, not the correct one.
obj.addChild({ initHTML: o.load.bind(o) });
} else {
if ( o && o.toView_ ) o = o.toView_();
if ( ! ( o === null || o === undefined ) ) {
if ( o.appendHTML ) {
o.appendHTML(this);
} else if ( o.toHTML ) {
buf.push(o.toHTML());
} else {
buf.push(o);
}
if ( o.initHTML && obj && obj.addChild ) obj.addChild(o);
}
}
}
};
f.toString = function() {
if ( buf.length === 0 ) return '';
if ( buf.length > 1 ) buf = [buf.join('')];
return buf[0];
}
return f;
}
};
// Called from generated template code.
function elementFromString(str) {
return str.element || ( str.element = HTMLParser.create().parseString(str).children[0] );
}
var ConstantTemplate = function(str) {
var TemplateOutputCreate = TemplateOutput.create.bind(TemplateOutput);
var f = function(opt_out) {
var out = opt_out ? opt_out : TemplateOutputCreate(this);
out(str);
return out.toString();
};
f.toString = function() {
return 'ConstantTemplate("' + str.replace(/\n/g, "\\n").replace(/"/g, '\\"').replace(/\r/g, '') + '")';
};
return f;
};
var TemplateCompiler = {
__proto__: TemplateParser,
out: [],
simple: true, // True iff the template is just one string literal.
push: function() { this.simple = false; this.pushSimple.apply(this, arguments); },
pushSimple: function() { this.out.push.apply(this.out, arguments); }
}.addActions({
markup: function (v) {
var wasSimple = this.simple;
var ret = wasSimple ? null : this.out.join('');
this.out = [];
this.simple = true;
return [wasSimple, ret];
},
'create child': function(v) {
var name = v[1].join('');
this.push(
"', self.createTemplateView('", name, "'",
v[2] ? ', ' + v[2] : '',
"),\n'");
},
foamTag: function(e) {
// A Feature
var fName = e.getAttribute('f');
if ( fName ) {
this.push("', self.createTemplateView('", fName, "',{}).fromElement(FOAM(");
this.push(JSONUtil.where(NOT_TRANSIENT).stringify(e));
this.push('))');
}
// A Model
else {
this.push("', (function() { var tagView = X.foam.ui.FoamTagView.create({element: FOAM(");
this.push(JSONUtil.where(NOT_TRANSIENT).stringify(e));
this.push(')}, Y); self.addDataChild(tagView); return tagView; })() ');
}
this.push(",\n'");
},
'simple value': function(v) { this.push("',\n self.", v[1].join(''), v[2], ",\n'"); },
'raw values tag': function (v) { this.push("',\n", v[1].join(''), ",\n'"); },
'values tag': function (v) { this.push("',\nescapeHTML(", v[1].join(''), "),\n'"); },
'live value tag': function (v) { this.push("',\nself.dynamicTag('span', function() { return ", v[1].join(''), "; }.bind(this)),\n'"); },
'code tag': function (v) { this.push("');\n", v[1].join(''), ";out('"); },
'single quote': function () { this.pushSimple("\\'"); },
newline: function () { this.pushSimple('\\n'); },
text: function(v) { this.pushSimple(v); }
});
MODEL({
name: 'TemplateUtil',
constants: {
HEADER: 'var self = this, X = this.X, Y = this.Y;' +
'var out = opt_out ? opt_out : TOC(this);' +
"out('",
FOOTERS: {
html: "');return out.toString();",
css: "');return " +
'X.foam.grammars.CSSDecl.create().parser.parseString(out.toString());'
},
},
methods: {
/** Create a method which only compiles the template when first used. **/
lazyCompile: function(t) {
var delegate;
var f = function() {
if ( ! delegate ) {
if ( ! t.template )
throw 'Must arequire() template model before use for ' + this.name_ + '.' + t.name;
else
delegate = TemplateUtil.compile(Template.isInstance(t) ? t : Template.create(t), this.model_);
}
return delegate.apply(this, arguments);
};
f.toString = function() { return delegate ? delegate.toString() : t.toString(); };
return f;
},
compile_: function(t, code, model) {
var args = ['opt_out'];
for ( var i = 0 ; i < t.args.length ; i++ ) {
args.push(t.args[i].name);
}
return eval(
'(function() { ' +
'var escapeHTML = XMLUtil.escape, TOC = TemplateOutput.create.bind(TemplateOutput); ' +
'return function(' + args.join(',') + '){' + code + '};})()' +
(model ? '\n\n//# sourceURL=' + model.id.replace(/\./g, '/') + '.' + t.name + '\n' : ''));
},
parseCSS: function(t) {
var parser = this.CSSParser_ || ( this.CSSParser_ = X.foam.grammars.CSSDecl.create());
return parser.parser.parseString(t).toString();
},
parseU2: function(t) {
var parser = this.U2Parser_ || ( this.U2Parser_ = X.foam.u2.ElementParser.parser__.create() );
return parser.parseString(t.trim()).toString();
},
compile: function(t, model) {
if ( t.name !== 'CSS' ) {
if ( model.isSubModel(X.lookup('foam.u2.Element')) ) {
return eval('(function() { return ' + this.parseU2(t.template) + '; })()');
}
if ( t.template.startsWith('#U2') ) {
var code = '(function() { return ' + this.parseU2(t.template.substring(3)) + '; })()';
return eval(code);
}
}
// Parse result: [isSimple, maybeCode]: [true, null] or [false, codeString].
var parseResult = TemplateCompiler.parseString(t.template);
// Simple case, just a string literal
if ( parseResult[0] )
return ConstantTemplate(t.language === 'css' ?
this.parseCSS(t.template) :
t.template) ;
var code = this.HEADER + parseResult[1] + this.FOOTERS[t.language];
// Need to compile an actual method
try {
return this.compile_(t, code, model);
} catch (err) {
console.log('Template Error: ', err);
console.log(parseResult);
return function() {};
}
},
/**
* Combinator which takes a template which expects an output parameter and
* converts it into a function which returns a string.
*/
stringifyTemplate: function (template) {
return function() {
var buf = [];
this.output(buf.push.bind(buf), obj);
return buf.join('');
};
},
expandTemplate: function(self, t, opt_X) {
/*
* If a template is supplied as a function, treat it as a multiline string.
* Parse function arguments to populate template.args.
* Load template from file if external.
* Setup template future.
*/
var X = opt_X || self.X;
if ( typeof t === 'function' ) {
t = X.Template.create({
name: t.name,
// ignore first argument, which should be 'opt_out'
args: t.toString().match(/\((.*?)\)/)[1].split(',').slice(1).map(function(a) {
return X.Arg.create({name: a.trim()});
}),
template: multiline(t)});
} else if ( typeof t === 'string' ) {
t = docTemplate = X.Template.create({
name: 'body',
template: t
});
} else if ( ! t.template && ! t.code ) {
t = X.Template.create(t);
var future = afuture();
var path = self.sourcePath;
t.futureTemplate = future.get;
path = path.substring(0, path.lastIndexOf('/')+1);
path += t.path ? t.path : self.name + '_' + t.name + '.ft';
if ( window.XMLHttpRequest ) {
var xhr = new XMLHttpRequest();
xhr.open("GET", path);
xhr.asend(function(data) {
t.template = data;
future.set(Template.create(t));
});
} else {
var fs = require('fs');
fs.readFile(path, function(err, data) {
t.template = data.toString();
future.set(Template.create(t));
});
}
} else if ( typeof t.template === 'function' ) {
t.template = multiline(t.template);
}
if ( ! t.futureTemplate ) t.futureTemplate = aconstant(t);
// We haven't FOAMalized the template, and there's no crazy multiline functions.
// Note that Model and boostrappy models must use this case, as Template is not
// yet defined at bootstrap time. Use a Template object definition with a bare
// string template body in those cases.
if ( ! t.template$ ) {
t = ( typeof X.Template !== 'undefined' ) ? JSONUtil.mapToObj(X, t, X.Template) : t ;
}
return t;
},
expandModelTemplates: function(self) {
var templates = self.templates;
for ( var i = 0; i < templates.length; i++ ) {
templates[i] = TemplateUtil.expandTemplate(self, templates[i]);
}
}
}
});
/** Is actually synchronous but is replaced in ChromeApp with an async version. **/
var aeval = function(src) {
return aconstant(eval('(' + src + ')'));
};
var aevalTemplate = function(t, model) {
return aseq(
t.futureTemplate,
function(ret, t) {
ret(TemplateUtil.lazyCompile(t));
});
};
var escapeHTML = XMLUtil.escape, TOC = TemplateOutput.create.bind(TemplateOutput);