toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
612 lines (546 loc) • 19.3 kB
JavaScript
"use strict";
const Common = require( "./boilerplate.view.common" );
const
camelCase = Common.camelCase,
CamelCase = Common.CamelCase,
contains = Common.contains;
/**
* XJS View must be converted in valid Javascript code.
* This class helps remembering all is needed to create such valid code.
* It also offered lot of util functions to do the job.
*
* @param {[type]} codeBehind [description]
* @param {[type]} moduleName [description]
* @constructor
*/
class Template {
constructor( codeBehind, moduleName ) {
this._counter = -1;
this.codeBehind = codeBehind;
this.moduleName = moduleName;
this.debug = false;
// List of behind function that need to be defined.
this.neededBehindFunctions = [];
this.requires = {};
this.functions = {};
this.elementNames = [];
this.vars = {};
this.that = false;
this.pm = false;
this.aliases = {};
// Names of action attributes.
// Such attributes must not be fired at init time, for example.
this.actions = [];
this.section = createSectionStructure();
}
isAction( attName ) {
const camelCaseAttName = camelCase( attName );
return contains( this.actions, attName ) || contains( this.actions, camelCaseAttName );
}
}
module.exports = Template;
const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
* @member Template.id
* @param
*/
Template.prototype.id = function ( prefix, counter ) {
if ( typeof prefix === 'undefined' ) prefix = "";
if ( typeof counter !== 'number' ) {
this._counter++;
counter = this._counter;
}
while ( counter >= DIGITS.length ) {
const modulo = counter % DIGITS.length;
prefix += DIGITS.charAt( modulo );
counter = Math.floor( counter / DIGITS.length );
}
prefix += DIGITS.charAt( counter );
return prefix;
};
Template.prototype.generateNeededBehindFunctions = function () {
if ( this.neededBehindFunctions.length === 0 ) return [];
var names = this.neededBehindFunctions.map( name => '"' + name + '"' );
return generateSection(
"Check if needed functions are defined in code behind.",
[
"View.ensureCodeBehind( CODE_BEHIND, " + names.join( ", " ) + " );"
]
);
};
Template.prototype.generateBehindCall = function ( behindFunctionName, indent, args ) {
if ( typeof indent === 'undefined' ) indent = "";
if ( typeof args === 'undefined' ) args = "";
this.addNeededBehindFunction( behindFunctionName );
this.that = true;
if ( args.trim() != '' ) args = ", " + args;
return [
indent + "try {",
indent + " CODE_BEHIND." + behindFunctionName + ".call(that" + args + ");",
indent + "}",
indent + "catch( ex ) {",
indent + " console.error('Exception thrown in code behind `" +
behindFunctionName + "`: ', ex);",
indent + "}",
];
};
Template.prototype.generateRequires = function () {
if ( isEmpty( this.requires ) ) return [];
const that = this;
const keys = Object.keys( this.requires );
keys.sort( function ( a, b ) {
const deltaLen = a.length - b.length;
if ( deltaLen !== 0 ) return deltaLen;
if ( a < b ) return -1;
if ( a > b ) return 1;
return 0;
} );
return generateSection(
"Dependent modules.",
keys.map( k => `const ${k} = ${that.requires[ k ]};` ) );
};
Template.prototype.generateFunctions = function () {
if ( isEmpty( this.functions ) ) return [];
const that = this;
return generateSection(
"Global functions.",
Object.keys( this.functions ).map(
k => "function " + k + that.functions[ k ] + ";" ) );
};
Template.prototype.generateGlobalVariables = function () {
if ( isEmpty( this.vars ) ) return [];
const that = this;
return generateSection(
"Global variables.",
Object.keys( this.vars ).map( k => `const ${k} = ${that.vars[ k ]};` )
);
};
Template.prototype.generateLinks = function () {
const that = this;
try {
const output = [];
var links = this.section.links;
if ( links.length === 0 ) return output;
links.forEach( function ( link, index ) {
try {
output.push( "new Link({" );
if ( that.debug ) {
output.push( " dbg: '" + that.moduleName + "#" + index + "'," );
}
output.push(
" A:" + pod2code.call( that, link.A ) + ",",
" B:" + pod2code.call( that, link.B ) + ",",
" name:" + JSON.stringify( link.A.path + " > " + link.B.path ),
"});" );
} catch ( ex ) {
throw ex + "\n" + "link = " + JSON.stringify( link );
}
} );
return generateSection( "Links", output );
} catch ( ex ) {
throw ex + "\n" + JSON.stringify( this.section.links, null, " " ) + "\n" + "generateLinks()";
}
};
/**
*
*/
function pod2code( pod ) {
try {
const that = this;
var items = [];
Object.keys( pod ).forEach( function ( key ) {
var val = pod[ key ];
if ( key === 'path' ) pod2CodePath.call( that, items, val );
else if ( key === 'delay' ) pod2CodeDelay.call( that, items, val );
else if ( key === 'action' ) pod2CodeAction.call( that, items, val );
else if ( key === 'converter' ) pod2CodeConverter.call( that, items, val );
else if ( key === 'format' ) pod2CodeFormat.call( that, items, val );
else if ( key === 'map' ) pod2CodeMap.call( that, items, val );
else if ( key === 'header' ) pod2CodeHeader.call( that, items, val );
else if ( key === 'footer' ) pod2CodeFooter.call( that, items, val );
else if ( key === 'open' ) pod2CodeOpen.call( that, items, val );
} );
return "{" + items.join( ",\n " ) + "}";
} catch ( ex ) {
throw ex + "\n" + "pod2code( " + JSON.stringify( pod ) + " )";
}
}
/**
*
*/
function pod2CodePath( items, path ) {
var pieces = path.split( "/" )
.map( x => x.trim() )
.filter( x => x.length > 0 );
if ( pieces.length === 1 ) {
// The simplest path describes an attribute of the main view
// object.
items.push(
"obj: that",
"name: '" + camelCase( pieces[ 0 ] ) + "'" );
} else {
var attName = camelCase( pieces.pop() );
var firstPiece = pieces.shift();
var objCode = isVarNameAndNotViewId( firstPiece ) ?
firstPiece : "e_" + camelCase( firstPiece );
//? firstPiece : "that.$elements." + camelCase( firstPiece );
objCode += pieces.map( x => keySyntax( x ) ).join( "" );
items.push(
"obj: " + objCode,
"name: '" + attName + "'" );
}
}
/**
*
*/
function pod2CodeDelay( items, delay ) {
items.push( "delay: " + parseInt( delay ) );
}
function pod2CodeOpen( items, open ) {
if ( open === false ) {
items.push( "open: false" );
}
}
/**
*
*/
function pod2CodeAction( items, actions ) {
items.push(
"action: function(v) {\n" +
actions.map( x => " " + x ).join( "\n" ) +
"}" );
}
function pod2CodeConverter( items, converter ) {
items.push( "converter: " + converter );
}
function pod2CodeFormat( items, format ) {
items.push( "format: [_, " + JSON.stringify( format ) + "]" );
}
function pod2CodeMap( items, codeLines ) {
items.push(
"map: function() {\n" +
codeLines.map( x => " " + x ).join( "\n" ) +
"}" );
}
function pod2CodeHeader( items, codeLines ) {
items.push(
"header: function() {\n" +
codeLines.map( x => " " + x ).join( "\n" ) +
"}" );
}
function pod2CodeFooter( items, codeLines ) {
items.push(
"footer: function() {\n" +
codeLines.map( x => " " + x ).join( "\n" ) +
"}" );
}
Template.prototype.addNeededBehindFunction = function ( functionName ) {
this.requires.View = "require('tfw.view');";
pushUnique( this.neededBehindFunctions, functionName );
};
Template.prototype.addCast = function ( name, value ) {
if ( name.substr( 0, 7 ) === 'behind.' ) {
var funcName = name.substr( 7 );
this.addNeededBehindFunction( funcName );
return "CODE_BEHIND." + funcName + ".bind( this )";
} else {
if ( typeof value === 'undefined' ) value = "Converters.get('" + name + "')";
this.requires[ "Converters" ] = "require('tfw.binding.converters')";
this.vars[ "conv_" + name ] = value;
return "conv_" + name;
}
};
/**
* @example
* bind: {Bind duration}
* to: "value"
* return: {A:{path: "duration"}, B:{path: "value"}}
*
* bind: {Bind names, converter: length}
* to: "count"
* return: {A:{path: "names"}, B:{path: "count", converter: length}}
*
* bind: {Bind duration, delay: 300}
* to: "value"
* return: {A:{path: "duration"}, B:{path: "value", delay: 300}}
*
* bind: {Bind duration, delay: 300}
* to: ["$.addClass(elem000, 'hide')"]
* return: {A:{path: "duration"}, B:{action: ["$.addClass(elem000, 'hide')"], delay: 300}}
*
* bind: {Bind duration, delay: 300}
* to: {Behind onDurationChange}
* return: {A:{path: "duration"}, B:{action: {Behind onDurationChange}, delay: 300}}
*
* bind: {Bind value, -delay: 350 }
* to: "e_input/value"
*
*/
Template.prototype.addLinkFromBind = function ( bind, to ) {
try {
var A = { path: bind[ 1 ] || bind.path };
var B = processLinkFromBindArgumentTo.call( this, to );
processBindArguments.call( this, A, B, bind );
return this.addLink( A, B );
} catch ( ex ) {
throw Error(
ex + "\n" + "addLinkFromBind(" +
JSON.stringify( bind ) + ", " +
JSON.stringify( to ) + ")" );
}
};
function processBindArguments( A, B, bind ) {
processBindArgsForSource.call( this, A, bind );
processBindArgsForDestination.call( this, B, bind );
}
function processBindArgsForSource( src, bind ) {
try {
if ( bind.back === false ) src.open = false;
// Atributes starting with a `-`.
var backAttributes = {};
var name, value;
for ( name in bind ) {
if ( name.charAt( 0 ) === '-' ) {
backAttributes[ name.substr( 1 ) ] = bind[ name ];
}
}
processBindArgsForDestination( src, backAttributes );
} catch ( ex ) {
throw ex + "\n...in processBindArgsForSource: " + limitJson( bind );
}
}
function processBindArgsForDestination( dst, bind ) {
try {
var name, value;
for ( name in bind ) {
value = bind[ name ];
switch ( name ) {
case 'delay':
if ( typeof value !== 'number' ) {
throw "In a {Bind... delay:...} declaration, `delay` must be a number!\n" +
" delay: " + limitJson( value );
}
dst.delay = value;
break;
case 'const':
dst.converter = parseConverter.call( this, { "0": 'Const', "1": value } );
break;
case 'converter':
dst.converter = parseConverter.call( this, value );
break;
case 'format':
dst.format = parseFormat.call( this, value );
break;
}
}
} catch ( ex ) {
throw ex + "\n...in processBindArgsForDestination: " + limitJson( bind );
}
}
function parseFormat( syntax ) {
if ( typeof syntax !== 'string' )
throw "In {Bind format:...}, `format` must be a string!";
return syntax;
}
function parseConverter( syntax ) {
if ( typeof syntax === 'string' ) return parseConverterString.call( this, syntax );
if ( isSpecial( syntax, "behind" ) ) return parseConverterBehind.call( this, syntax[ "1" ] );
if ( isSpecial( syntax, 'const' ) ) return parseConverterConst.call( this, syntax[ "1" ] );
throw "In a {Bind converter:...}, `converter` must be a string or `{Behind ...}`!\n" +
" but we found: " + limitJson( syntax );
}
function parseConverterString( syntax ) {
if ( syntax.substr( 0, 7 ) === 'behind.' ) {
var funcName = syntax.substr( 7 );
this.addNeededBehindFunction( funcName );
return "CODE_BEHIND." + funcName + ".bind( this )";
} else {
this.vars[ "conv_" + syntax ] = "Converters.get('" + syntax + "')";
return "conv_" + syntax;
}
}
function parseConverterConst( value ) {
return "function(){return " + JSON.stringify( value ) + "}";
}
function parseConverterBehind( funcName ) {
this.addNeededBehindFunction( funcName );
return "CODE_BEHIND." + funcName + ".bind( this )";
}
/**
* "value" -> { path: "value" }
* ["$.clear(elem0)"] -> { action: ["$.clear(elem0)"] }
* {Behind onValueChanged} -> { action: ["CODE_BEHIND.onValueChanged.call( this, v )"] }
*/
function processLinkFromBindArgumentTo( to ) {
if ( typeof to === 'string' ) return { path: to };
if ( Array.isArray( to ) ) return { action: to };
if ( isSpecial( to, "behind" ) ) {
return processLinkFromBindArgumentTo_behind.call( this, to );
} else if ( isSpecial( to, "bind" ) ) {
return processLinkFromBindArgumentTo_bind.call( this, to );
}
throw "`to` argument can be only a string, an array or {Behind ...}!";
}
function processLinkFromBindArgumentTo_behind( to ) {
var behindFunctionName = to[ 1 ];
if ( typeof behindFunctionName !== 'string' )
throw "In a {Behind ...} statement, the second argument must be a string!";
pushUnique( this.neededBehindFunctions, behindFunctionName );
return { action: [ "CODE_BEHIND." + behindFunctionName + ".call(that, v)" ] };
}
function processLinkFromBindArgumentTo_bind( to ) {
var behindFunctionName;
var binding = {};
if ( Array.isArray( to.action ) ) binding.action = to.action;
[ 'map', 'header', 'footer' ].forEach( function ( id ) {
if ( typeof to[ id ] === 'string' ) {
behindFunctionName = to[ id ];
pushUnique( this.neededBehindFunctions, behindFunctionName );
binding[ id ] = [ "return CODE_BEHIND." + behindFunctionName + ".apply(that, arguments)" ];
}
}, this );
return binding;
}
/**
* Prepare a link with `path` insteadof `obj`/`name`.
*/
Template.prototype.addLink = function ( A, B ) {
try {
this.requires[ "Link" ] = "require('tfw.binding.link')";
this.that = true;
checkLinkPod( A );
checkLinkPod( B );
var link = JSON.parse( JSON.stringify( { A: A, B: B } ) );
this.section.links.push( link );
return link;
} catch ( ex ) {
throw Error(
ex + "\n" + "addLink(" +
JSON.stringify( A ) + ", " +
JSON.stringify( B ) + ")" );
}
};
function checkLinkPod( pod ) {
try {
var pathType = typeof pod.path;
if ( pathType !== 'undefined' && pathType !== 'string' )
throw "Attribute `path` in a link's pod must be a <string>, not a <" + pathType + ">!\n" +
"pod.path = " + JSON.stringify( pod.path );
var actionType = typeof pod.action;
if ( actionType !== 'undefined' && !Array.isArray( pod.action ) )
throw "Attribute `action` in a link's pod must be an <array>, not a <" + actionType + ">!\n" +
"pod.action = " + JSON.stringify( pod.action );
if ( !pod.path && !pod.action )
throw "A link's pod must have at least an attribute `path` or `action`!";
} catch ( ex ) {
throw Error(
ex + "\n" +
"checkLinkPod( " + JSON.stringify( pod ) + " )" );
}
}
function pushUnique( arr, item ) {
if ( arr.indexOf( item ) === -1 )
arr.push( item );
}
function generateSection( sectionName, contentArray, indent ) {
if ( typeof indent === 'undefined' ) indent = "";
var firstLine = indent + "//";
var count = sectionName.length + 2;
while ( count-- > 0 ) firstLine += "-";
var lines = [ firstLine, indent + "// " + sectionName ];
return lines.concat( contentArray );
}
function isEmpty( value ) {
if ( Array.isArray( value ) ) return value.length === 0;
if ( typeof value === 'string' ) return value.trim().length === 0;
for ( var k in value ) return false;
return true;
}
function object2code( obj ) {
if ( Array.isArray( obj ) ) {
return "[" + obj.map( x => object2code( x ) ).join( ", " ) + "]";
}
switch ( typeof obj ) {
case 'object':
return "{" +
Object.keys( obj )
.map( k => quotesIfNeeded( k ) + ": " + object2code( obj[ k ] ) )
.join( ", " ) +
"}";
default:
return JSON.stringify( obj );
}
}
/**
* If `name` is a valid Javascript identifier, return it
* verbatim. Otherwise, return it surrounded by double quotes.
*/
const RX_JAVASCRIPT_IDENTIFIER = /^[_$a-z][_$a-z0-9]*$/ig;
function quotesIfNeeded( name ) {
return RX_JAVASCRIPT_IDENTIFIER.test( name ) ? name : JSON.stringify( name );
}
/**
* @example
* keySyntax( "value" ) === ".value"
* keySyntax( "diff-value" ) === '["diff-value"]'
*/
function keySyntax( name ) {
if ( RX_JAVASCRIPT_IDENTIFIER.test( name ) ) return "." + name;
return "[" + JSON.stringify( name ) + "]";
}
/**
* An object is special of and only if it's attribute of key "0" is a
* string.
*/
function isSpecial( obj, name ) {
if ( !obj ) return false;
if ( typeof obj[ 0 ] !== 'string' ) return false;
if ( typeof name === 'string' ) {
return obj[ 0 ].toLowerCase() === name;
}
return true;
}
/**
* In a binding, you can use the "source/attribute" syntax to bind to
* an attribute of a descendant of the root element. If the source has
* a "view.id", it is refered like this: `that.$elements.id`.
* If not, it is refered by its var name: `e_xxx`.
*
* ViewId are camelCase and VarName start with "e_".
*/
function isVarNameAndNotViewId( name ) {
return name.substr( 0, 2 ) === 'e_';
}
function limitJson( obj, max ) {
return limit( JSON.stringify( obj ), max );
}
function limit( txt, max ) {
if ( typeof max === 'undefined' ) max = 80;
if ( txt === undefined ) txt = "undefined";
else if ( txt === null ) txt = "null";
if ( txt.length <= max ) return txt;
return txt.substr( 0, max ) + "...";
}
/**
* Returns the initial content of Template.section.
*
* @returns {object} Initial content of Template.section.
*/
function createSectionStructure() {
return {
init: null,
comments: [],
attribs: {
define: [],
init: []
},
elements: {
define: [],
init: []
},
events: [],
links: [],
ons: [],
statics: []
};
}