toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
1,402 lines (1,300 loc) • 55.3 kB
JavaScript
"use strict";
const
Template = require( "./boilerplate.view.template" ),
Common = require( "./boilerplate.view.common" );
const
camelCase = Common.camelCase,
CamelCase = Common.CamelCase,
isSpecial = Common.isSpecial;
const
RX_VIEW_ATTRIB = /^[a-z][a-z0-9]+(-[a-z0-9]+)*$/,
RX_INTEGER = /^[0-9]+$/,
RX_STD_ATT = /^([a-zA-Z]+:)?[A-Za-z0-9-]+$/,
RX_TAG_NAME = /^H[1-9]|[A-Z]+$/,
RX_CLS_NAME = /^[a-z][a-z0-9]*\.[a-z0-9.-]+$/,
RX_IDENTIFIER = /^[_$a-z][_$a-z0-9]+$/i;
/**
* Generate Javascript code for XJS View.
*
* @param {object} _def - `{View ...}`
* @param {string} codeBehind - Piece of Javascript code to which we will add the generated one.
* @param {string} moduleName - NAme of the Javascript module.
*
* @return {string} Code generated from the XJS definition.
*/
exports.generateCodeFrom = function ( _def, codeBehind, moduleName ) {
try {
const
def = removeViewPrefix( _def ),
code = new Template( codeBehind, moduleName );
// Debug mode ?
code.debug = Boolean( def[ "view.debug" ] );
// Define attributes.
buildViewAttribs( def, code );
buildViewAttribsFire( def, code );
// Define methods.
buildViewPrototype( def, code );
// Define static members.
buildViewStatics( def, code );
// Look for any intialize function in code behinid.
buildViewInit( def, code );
// Transform `def` to make it look like an HTML tag.
declareRootElement( def, code );
// Generate full code.
code.requires.$ = "require('dom')";
if ( code.pm ) {
code.requires.PM = "require('tfw.binding.property-manager')";
}
let out = arrayToCodeWithNewLine( [
codeBehind,
"\n",
"//===============================",
"// XJS:View autogenerated code.",
"try {"
] );
out += outputAll( code, moduleName );
out += arrayToCodeWithNewLine( [
"}",
"catch( ex ) {",
` throw Error('Definition error in XJS of "${moduleName}"\\n' + ex);`,
"}"
] );
return out;
} catch ( ex ) {
throw Error( `${ex}\n...in module ${JSON.stringify(moduleName)}` );
}
};
/**
* Return the code from an array and an identation.
* The code is preceded by a comment.
*
* @param {array} arr - Array of strings and/or arrays.
* @param {string} comment - Heading comment. Optional.
* @param {string} _indent - Indetation before each line.
*
* @returns {string} Readable Javascript code.
*/
function generate( arr, comment, _indent ) {
try {
const indent = typeof _indent !== "undefined" ? _indent : " ";
if ( arr.length === 0 ) return "";
let out = '';
if ( comment && comment.length > 0 ) {
let
len = comment.length + 3,
dashes = '';
while ( len-- > 0 ) dashes += "-";
out += `${indent}//${dashes}\n${indent}// ${comment}.\n`;
}
arr.forEach( function ( line ) {
if ( Array.isArray( line ) ) {
out += generate( line, null, ` ${indent}` );
} else {
out += `${indent}${line}\n`;
}
} );
return out;
} catch ( ex ) {
throw ex + "\n...in generate: " + JSON.stringify( comment );
}
}
/**
* @example
* {View DIV view.init:intialize}
*/
function buildViewInit( def, code ) {
try {
const init = def[ "view.init" ];
if ( typeof init === 'undefined' ) return;
if ( typeof init !== 'string' )
throw "The value of attribute `view.init` must be a string!";
code.addNeededBehindFunction( init );
code.section.init = init;
} catch ( ex ) {
throw ex + "\n...in view.init: " + JSON.stringify( init, null, " " );
}
}
/**
* Add static functions to the current class.
*
* @example
* view.statics: ["show", "check"]
* view.statics: "show"
*/
function buildViewStatics( def, code ) {
try {
let statics = def[ "view.statics" ];
if ( typeof statics === 'undefined' ) return;
if ( typeof statics === 'string' ) statics = [ statics ];
else if ( !Array.isArray( statics ) ) {
throw Error( "view.statics must be a string or an array of strings!" );
}
statics.forEach( function ( name ) {
code.addNeededBehindFunction( name );
code.section.statics.push(
"ViewClass" + keySyntax( name ) + " = CODE_BEHIND" + keySyntax( name ) + ".bind(ViewClass);"
);
} );
} catch ( ex ) {
throw ex + "\n...in view.statics: " + JSON.stringify( statics );
}
}
function buildViewPrototype( def, code ) {
let proto = def[ "view.prototype" ];
try {
if ( typeof proto === 'undefined' ) return;
if ( !Array.isArray( proto ) ) proto = [ proto ];
proto.forEach( function ( name ) {
code.addNeededBehindFunction( name );
code.section.statics.push(
"ViewClass.prototype" + keySyntax( name ) + " = CODE_BEHIND" + keySyntax( name ) + ";"
);
} );
} catch ( ex ) {
throw ex + "\n...in view.prototype: " + JSON.stringify( proto, null, " " );
}
}
/**
* @param {object} def - ``.
* @param {Template} code - Template.
*
* @example
* view.attribs: {
* flat: true
* type: [default, primary, secondary]
* count: {integer}
* content: {string ok behind: onContentChanged}
* }
*
* @returns {undefined}
*/
function buildViewAttribs( def, code ) {
code.pm = true;
const attribs = def[ "view.attribs" ];
if ( typeof attribs !== 'object' ) return;
try {
for ( const attName of Object.keys( attribs ) ) {
if ( !RX_VIEW_ATTRIB.test( attName ) )
throw Error( `Bad syntax for attribute's name: ${JSON.stringify(attName)}
Example of valid syntax: action, duration-visible, ...
Example of invalid syntax: 0, durationVisible, ...` );
const
attValue = expandViewAttribValue( attribs[ attName ] ),
camelCaseAttName = camelCase( attName );
if ( isSpecial( attValue ) || Array.isArray( attValue[ 0 ] ) ) {
buildViewAttribsSpecial( camelCaseAttName, attValue, code );
} else {
// flat: true
buildViewAttribsInit( camelCaseAttName, attValue, code );
code.section.attribs.define
.push( `pm.create(${JSON.stringify(camelCaseAttName)}, {init: ${JSON.stringify(attValue)}});` );
}
}
} catch ( ex ) {
bubble( ex, `view.attribs: ${JSON.stringify(attribs, null, " ")}` );
}
}
/**
* Trigger the fire event on each attribute.
*
* @param {[type]} def [description]
* @param {[type]} code [description]
* @returns {undefined}
*/
function buildViewAttribsFire( def, code ) {
code.pm = true;
const attribs = def[ "view.attribs" ];
if ( typeof attribs !== 'object' ) return;
try {
for ( const attName of Object.keys( attribs ) ) {
const camelCaseAttName = camelCase( attName );
if ( !code.isAction( attName ) ) {
buildViewAttribsInitFire( camelCaseAttName, code );
}
}
} catch ( ex ) {
bubble( ex, `...in buildViewAttribsFire - view.attribs: ${JSON.stringify(attribs, null, " ")}` );
}
}
/**
* Attribute with casting.
* @example
* type: {[default, primary, secondary]}
* count: {integer}
* content: {string ok behind: onContentChanged}
*/
function buildViewAttribsSpecial( attName, attValue, code ) {
const
type = attValue[ 0 ],
init = attValue[ 1 ];
let requireConverter = false;
try {
if ( typeof attValue.behind !== 'undefined' ) {
buildViewAttribsSpecialCodeBehind( attName, attValue.behind, code );
}
if ( typeof attValue.debug !== 'undefined' ) {
buildViewAttribsSpecialDebug( attName, attValue.debug, code );
}
if ( Array.isArray( type ) ) {
// Enumerate.
code.addCast( "enum" );
code.section.attribs.define.push(
"pm.create(" + JSON.stringify( attName ) +
`, { cast: conv_enum(${JSON.stringify(type)}), init: ${JSON.stringify(init)} });` );
buildViewAttribsInit( attName, init, code );
} else {
switch ( type ) {
case 'action':
code.actions.push( attName );
code.section.attribs.define.push(
"pm.createAction(" + JSON.stringify( attName ) + ")" );
break;
case 'any':
code.section.attribs.define.push(
"pm.create(" + JSON.stringify( attName ) + ");" );
buildViewAttribsInit( attName, init, code );
break;
case 'boolean':
case 'booleans':
case 'date':
case 'color':
case 'string':
case 'strings':
case 'array':
case 'list':
case 'intl':
case 'time':
case 'unit':
case 'units':
case 'multilang':
case 'validator':
requireConverter = true;
code.vars[ "conv_" + type ] = "Converters.get('" + type + "')";
code.section.attribs.define.push(
"pm.create(" + JSON.stringify( attName ) +
", { cast: conv_" + type + " });" );
buildViewAttribsInit( attName, init, code );
break;
case 'integer':
case 'float':
requireConverter = true;
code.vars[ "conv_" + type ] = "Converters.get('" + type + "')";
// Is Not a number, take this default value.
let nanValue = attValue.default;
if ( typeof nanValue !== 'number' || isNaN( nanValue ) ) {
nanValue = 0;
}
code.section.attribs.define.push(
"pm.create(" + JSON.stringify( attName ) +
", { cast: conv_" + type + "(" + nanValue + ") });" );
buildViewAttribsInit( attName, init, code );
break;
default:
throw "Unknown type \"" + type + "\" for attribute \"" + attName + "\"!";
}
}
if ( requireConverter ) {
code.requires.Converters = "require('tfw.binding.converters')";
}
} catch ( ex ) {
bubble(
ex,
`buildViewAttribsSpecial(${attName}: ${JSON.stringify(attValue)})`
);
}
}
/**
* @example
* view.attribs: {
* size: {unit "32px", debug: "Width of the picture"}
* }
*/
function buildViewAttribsSpecialDebug( attName, debug, code ) {
try {
console.warn( `Debug is set for ${code.moduleName}/${attName}!`.bgYellow.black );
code.section.ons.push(
`pm.on("${attName}, function(v) {`,
` console.info(${JSON.stringify(debug)});`,
" const attribs = {};",
" Object.keys(that).forEach(function(key) {",
" attribs[key] = that[key];",
" });",
` console.info('${code.moduleName}/${attName} =', v);`,
" console.info(attribs);",
" if( typeof console.trace === 'function' ) console.trace();",
"});"
);
} catch ( ex ) {
bubble(
ex,
`buildViewAttribsSpecialDebug(${attName}: ${JSON.stringify(debug)})`
);
}
}
/**
* @example
* view.attribs: {
* size: {unit "32px", behind: onSizeChanged}
* }
*/
function buildViewAttribsSpecialCodeBehind( attName, functionBehindName, code ) {
try {
if ( typeof functionBehindName !== 'string' ) {
throw Error( `The property "Behind" of the attribute "${attName} must be a string!` );
}
code.that = true;
code.addNeededBehindFunction( functionBehindName );
code.section.ons.push(
`pm.on("${attName}", function(v) {`,
" try {",
` CODE_BEHIND${keySyntax(functionBehindName)}.call( that, v );`,
" }",
" catch( ex ) {",
" console.error(",
` 'Exception in function behind "${functionBehindName}" of module "${code.moduleName}" for attribute "${attName}"!'`,
" );",
" console.error( ex );",
" }",
"});"
);
} catch ( ex ) {
bubble(
ex,
`buildViewAttribsSpecialCodeBehind(${attName}, ${functionBehindName})`
);
}
}
/**
* Initialize attribute with a value. Priority to the value set in the
* contructor args.
*/
function buildViewAttribsInit( attName, attValue, code ) {
try {
if ( typeof attValue === "undefined" ) {
// code.section.attribs.init.push(`this.${attName} = args[${JSON.stringify(attName)}];`);
code.section.attribs.init
.push( `pm.set("${attName}", args[${JSON.stringify(attName)}]);` );
} else {
code.functions.defVal = "(args, attName, attValue) " +
"{ return args[attName] === undefined ? attValue : args[attName]; }";
// code.section.attribs.init.push( "this." + attName + " = defVal(args, " +
// JSON.stringify( attName ) + ", " + JSON.stringify( attValue ) +
// ");" );
code.section.attribs.init
.push( `pm.set("${attName}", defVal(args, "${attName}", ${JSON.stringify( attValue )}));` );
}
} catch ( ex ) {
bubble(
ex,
`buildViewAttribsInit(${attName}, ${JSON.stringify(attValue)})`
);
}
}
/**
* Once every attribute has been set, we must fire them.
*
* @param {[type]} attName [description]
* @param {[type]} code [description]
* @returns {undefined}
*/
function buildViewAttribsInitFire( attName, code ) {
try {
code.section.attribs.init
.push( `pm.fire("${attName}");` );
} catch ( ex ) {
bubble(
ex,
`buildViewAttribsInitFire(${attName})`
);
}
}
/**
* An element can be a tag (`{DIV...}`) or a view (`{tfw.view.button...}`).
*
* @param {object} def - Something like `{tfw.view.button ...}`.
* @param {Template} code - Helping stuff for code generation.
* @param {string} _varName - Name of the variable holding this element in the end.
* @returns {undefined}
*/
function buildElement( def, code, _varName ) {
const varName = getVarName( def, _varName );
addUnique( code.elementNames, varName );
try {
const type = def[ 0 ];
if ( RX_TAG_NAME.test( type ) ) {
buildElementTag( def, code, varName );
return buildElement_tagChildren( def, code, varName );
}
if ( RX_CLS_NAME.test( type ) ) {
buildElementCls( def, code, varName );
} else {
throw Error( "Unknown element name: " + JSON.stringify( type ) + "!\n" +
"Names must have one of theses syntaxes: " +
JSON.stringify( RX_TAG_NAME.toString() ) +
" or " + JSON.stringify( RX_CLS_NAME.toString() ) + ".\n" +
JSON.stringify( def ) );
}
return varName;
} catch ( ex ) {
throw Error( `${ex}\n...in buildElement - element "${varName}": ${limitJson( def )}` );
} finally {
// Store the variable for use in code behind.
const id = def[ "view.id" ];
if ( typeof id === 'string' ) {
code.section.elements.define
.push( `this.$elements.${camelCase( id )} = ${varName};` );
}
}
}
/**
* @param {object} def - Something like `{tfw.view.button ...}`.
* @param {Template} code - Helping stuff for code generation.
* @param {string} varName - Name of the variable holding this element in the end.
* @returns {string} varName.
*/
function buildElement_tagChildren( def, code, varName ) {
const _children = def[ 1 ];
if ( typeof _children === 'undefined' ) return varName;
if ( isSpecial( _children ) ) {
buildElementSpecialChild( _children, code, varName );
return varName;
}
const children = Array.isArray( _children ) ? _children : [ _children ];
const toAdd = [];
children.forEach( function ( child ) {
if ( typeof child === 'string' || typeof child === 'number' ) {
toAdd.push( JSON.stringify( `${child}` ) );
} else {
const childVarName = buildElement( child, code, code.id( "e_" ) );
toAdd.push( childVarName );
}
} );
if ( toAdd.length > 0 ) {
code.section.elements.define.push( `$.add( ${varName}, ${toAdd.join( ", " )} );` );
}
return varName;
}
/**
* `{INPUT placeholder:price value:{Bind price}}`
*/
function buildElementTag( def, code, varName ) {
const attribs = extractAttribs( def );
try {
const arr = Object.keys( attribs.standard );
code.requires[ "Tag" ] = "require('tfw.view').Tag";
code.section.elements.define.push(
"const " + varName + " = new Tag('" + def[ 0 ] + "'" +
( arr.length > 0 ? ', ' + JSON.stringify( arr ) : '' ) +
");" );
const initAttribs = buildElementTagAttribsStandard( attribs.standard, code, varName );
buildInitAttribsForTag( initAttribs, code, varName );
buildElementEvents( attribs.special, code, varName );
buildElementTagClassSwitcher( attribs.special, code, varName );
buildElementTagAttribSwitcher( attribs.special, code, varName );
buildElementTagStyle( attribs.special, code, varName );
buildElementTagChildren( attribs.special, code, varName );
buildElementTagOn( attribs.special, code, varName );
} catch ( ex ) {
throw ex + "\n...in tag \"" + varName + "\": " + limitJson( def );
}
}
function buildInitAttribsForTag( initAttribs, code, varName ) {
try {
initAttribs.forEach( function ( initAttrib ) {
try {
const
attName = initAttrib[ 0 ],
attValue = initAttrib[ 1 ];
if ( isSpecial( attValue, "verbatim" ) ) {
code.section.elements.init.push(
varName + keySyntax( attName ) + " = " + attValue[ 1 ] + ";" );
} else {
if ( [ 'string', 'number', 'boolean' ].indexOf( typeof attValue ) === -1 )
throw "A tag's attribute value must be of type string or number!";
code.section.elements.init.push(
varName + keySyntax( attName ) + " = " + parseComplexValue( code, attValue, ' ' ) + ";" );
}
} catch ( ex ) {
throw ex + `...in buildInitAttribsForTag - tag's attribute: "${attName}"=${limitJson(attValue)}`;
}
} );
} catch ( ex ) {
throw `${ex}\n...in buildInitAttribsForTag "${varName}":\ninitAttribs = ${limitJson( initAttribs )}`;
}
}
function buildElementCls( def, code, varName ) {
const attribs = extractAttribs( def );
try {
expandImplicitContent( attribs );
buildElementEvents( attribs.special, code, varName );
const
arr = Object.keys( attribs.standard ),
viewName = CamelCase( def[ 0 ] );
code.requires[ viewName ] = "require('" + def[ 0 ] + "')";
const initAttribs = buildElementTagAttribsStandard( attribs.standard, code, varName );
buildInitAttribsForCls( initAttribs, code, varName, viewName );
buildElementTagClassSwitcher( attribs.special, code, varName );
buildElementTagAttribSwitcher( attribs.special, code, varName );
buildElementTagStyle( attribs.special, code, varName );
buildElementClsBind( attribs.special, code, varName );
buildElementTagOn( attribs.special, code, varName );
} catch ( ex ) {
throw ex + "\n...in cls \"" + varName + "\": " + limitJson( attribs );
}
}
/**
* The following syntaxes are equivalent:
* @example
* // Explicit content
* {tfw.view.expand content: Hello}
* // Implicit content
* {tfw.view.expand Hello}
*/
function expandImplicitContent( attribs ) {
try {
if ( !attribs ) return;
if ( typeof attribs.implicit === 'undefined' ) return;
const implicitContent = attribs.implicit[ 1 ];
if ( typeof implicitContent === 'undefined' ) return;
const explicitContent = attribs.standard ? attribs.standard.content : undefined;
if ( explicitContent ) {
throw "Can't mix implicit and explicit `content`:\n" +
" implicit: " + limitJson( implicitContent ) + "\n" +
" explicit: " + limitJson( explicitContent );
}
attribs.standard.content = implicitContent;
} catch ( ex ) {
throw ex + "\n...in expandImplicitContent: " + limitJson( attribs );
}
}
function buildInitAttribsForCls( initAttribs, code, varName, viewName ) {
try {
var item, attName, attValue, parsedValue;
if ( initAttribs.length === 0 ) {
code.section.elements.define.push(
"const " + varName + " = new " + viewName + "();" );
} else if ( initAttribs.length === 1 ) {
item = initAttribs[ 0 ];
attName = camelCase( item[ 0 ] );
attValue = item[ 1 ];
parsedValue = parseComplexValue( code, attValue );
code.section.elements.define.push(
"const " + varName + " = new " + viewName + "({ " + putQuotesIfNeeded( attName ) + ": " +
parsedValue + " });" );
} else {
const out = [ "const " + varName + " = new " + viewName + "({" ];
initAttribs.forEach( function ( item, index ) {
try {
attName = camelCase( item[ 0 ] );
attValue = item[ 1 ];
parsedValue = parseComplexValue( code, attValue, ' ' );
out.push(
" " + putQuotesIfNeeded( attName ) + ": " +
parsedValue +
( index < initAttribs.length - 1 ? "," : "" ) );
} catch ( ex ) {
throw "Error while parsing attribute " + JSON.stringify( attName ) +
" with value " + JSON.stringify( attValue ) + ":\n" + ex;
}
} );
out.push( "});" );
code.section.elements.define.push.apply( code.section.elements.define, out );
}
} catch ( ex ) {
throw ex + "\n...in buildInitAttribsForCls\n" +
" varName: " + varName + "\n" +
" viewName: " + viewName + "\n" +
" initAttribs: " + limitJson( initAttribs ) + "\n" + ex;
};
}
/**
* Generate code from attributes starting with "class.".
* For instance `class.blue: {Bind focused}` means that the class
* `blue` must be added if the attribute `focused` is `true` and
* removed if `focused` is `false`.
* On the contrary, `class.|red: {Bind focused}` means that the
* class `red` must be removed if `focused` is `true` and added if
* `focused` is `false`.
* Finally, `class.blue|red: {Bind focused}` is the mix of both
* previous syntaxes.
*/
function buildElementTagClassSwitcher( def, code, varName ) {
var attName, attValue, classes, classesNames, className;
for ( attName in def ) {
classesNames = getSuffix( attName, "class." );
if ( !classesNames ) continue;
attValue = def[ attName ];
if ( classesNames === '*' ) {
buildElementTagClassSwitcherStar( attValue, code, varName );
continue;
}
if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
throw "Only bindings are accepted as values for class-switchers!\n" +
attName + ": " + JSON.stringify( attValue );
}
classes = classesNames.split( "|" );
const actions = [];
if ( isNonEmptyString( classes[ 0 ] ) ) {
code.requires.$ = "require('dom')";
code.functions[ "addClassIfTrue" ] = "(element, className, value) {\n" +
" if( value ) $.addClass(element, className);\n" +
" else $.removeClass(element, className); };";
className = JSON.stringify( classes[ 0 ] );
actions.push( "addClassIfTrue( " + varName + ", " + className + ", v );" );
}
if ( isNonEmptyString( classes[ 1 ] ) ) {
code.requires.$ = "require('dom')";
code.functions[ "addClassIfFalse" ] = "(element, className, value) {\n" +
" if( value ) $.removeClass(element, className);\n" +
" else $.addClass(element, className); };";
className = JSON.stringify( classes[ 1 ] );
actions.push( "addClassIfFalse( " + varName + ", " + className + ", v );" );
}
code.addLinkFromBind( attValue, actions );
}
}
/**
* @example
* view.children: {Bind names map:makeListItem}
* view.children: {List names map:makeListItem}
*/
function buildElementTagChildren( def, code, varName ) {
const attValue = def[ "view.children" ];
if ( typeof attValue === 'undefined' ) return;
if ( typeof attValue === 'string' ) {
// Transformer la première ligne en la seconde:
// 1) view.children: names
// 2) view.children: {Bind names}
attValue = { "0": 'Bind', "1": attValue };
}
if ( isSpecial( attValue, "bind" ) ) {
code.requires.View = "require('tfw.view');";
attValue.action = [
"// Updating children of " + varName + ".",
"$.clear(" + varName + ");",
"if( !Array.isArray( v ) ) v = [v];",
"v.forEach(function (elem) {",
" $.add(" + varName + ", elem);",
"});"
];
code.addLinkFromBind( attValue, attValue );
} else if ( isSpecial( attValue, "list" ) ) {
code.requires.View = "require('tfw.view');";
const codeBehindFuncName = attValue.map;
code.requires.ListHandler = "require('tfw.binding.list-handler');";
if ( typeof codeBehindFuncName === 'string' ) {
code.addNeededBehindFunction( codeBehindFuncName );
code.section.elements.init.push(
"ListHandler(this, " + varName + ", " + JSON.stringify( attValue[ 1 ] ) + ", {",
" map: CODE_BEHIND" + keySyntax( codeBehindFuncName ) + ".bind(this)",
"});"
);
} else {
// List handler without mapping.
code.section.elements.init.push(
"ListHandler(this, " + varName + ", " + JSON.stringify( attValue[ 1 ] ) + ");"
);
}
} else {
throw "Error in `view.children`: invalid syntax!\n" + JSON.stringify( attValue );
}
}
/**
* @example
* style.width: {Bind size}
* style.width: "24px"
*/
function buildElementTagStyle( def, code, varName ) {
var attName, attValue, styleName;
for ( attName in def ) {
styleName = getSuffix( attName, "style." );
if ( !styleName ) continue;
attValue = def[ attName ];
if ( typeof attValue === 'string' ) {
code.section.elements.init.push(
varName + ".$.style" + keySyntax( styleName ) + " = " + JSON.stringify( attValue ) );
continue;
}
if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
throw "Only bindings and strings are accepted as values for styles!\n" +
attName + ": " + JSON.stringify( attValue );
}
const attributeToBindOn = attValue[ 1 ];
if ( typeof attributeToBindOn !== 'string' )
throw "Binding syntax error for styles: second argument must be the name of a function behind!\n" +
attName + ": " + JSON.stringify( attValue );
const converter = attValue.converter;
if ( typeof converter === 'string' ) {
code.addCast( converter );
code.section.ons.push(
"pm.on(" + JSON.stringify( camelCase( attValue[ 1 ] ) ) + ", function(v) {",
" " + varName + ".$.style[" + JSON.stringify( styleName ) + "] = " +
"conv_" + converter + "( v );",
"});" );
} else {
code.section.ons.push(
"pm.on(" + JSON.stringify( camelCase( attValue[ 1 ] ) ) + ", function(v) {",
" " + varName + ".$.style[" + JSON.stringify( styleName ) + "] = v;",
"});" );
}
}
}
/**
* @example
* {tfw.view.button on.action: OnAction}
* {tfw.view.button on.action: {Toggle show-menu}}
*/
function buildElementTagOn( def, code, varName ) {
for ( const attName of Object.keys( def ) ) {
const targetAttributeName = getSuffix( attName, "on." );
if ( !targetAttributeName ) continue;
const attValue = typeof def[ attName ] === 'string' ? { 0: "Behind", 1: def[ attName ] } : def[ attName ];
code.section.ons.push(
`PM(${varName}).on(${JSON.stringify(targetAttributeName)},`,
buildFunction( attValue, code, varName ),
");"
);
}
}
/**
* Create the code for a function.
*
* @example
* {Behind onVarChanged}
* {Toggle show-menu}
*
* @param {object} def - Function definition.
* @param {object} code - Needed for `code.section.ons`.
* @param {string} varName - Name of the object owning the attribute we want to listen on.
*
* @return {array} Resulting code as an array.
*/
function buildFunction( def, code, varName ) {
if ( isSpecial( def, "behind" ) ) {
const behindFunctionName = def[ 1 ];
code.addNeededBehindFunction( behindFunctionName );
code.that = true;
return [
"value => {",
" try {",
` CODE_BEHIND${keySyntax(behindFunctionName)}.call(that, value, ${varName});`,
" }",
" catch( ex ) {",
" console.error(`Exception in code behind \"" +
behindFunctionName + "\" of module \"" +
code.moduleName + "\": ${ex}`);",
" }",
"}"
];
}
if ( isSpecial( def, "toggle" ) ) {
const nameOfTheBooleanToToggle = def[ 1 ];
const namesOfTheBooleanToToggle = Array.isArray( nameOfTheBooleanToToggle ) ?
nameOfTheBooleanToToggle : [ nameOfTheBooleanToToggle ];
const body = namesOfTheBooleanToToggle
.map( name => `${getElemVarFromPath(name)} = !${getElemVarFromPath(name)};` );
return [
"() => {",
body,
"}"
];
}
throw Error( `Function definition expected, but found ${JSON.stringify(def)}!` );
}
/**
* `{tfw.view.button bind.enabled: enabled}`
* is equivalent to:
* `{tfw.view.button enabled: {Bind enabled}}`
*
* `{tfw.view.button bind.enabled: {list converter:isNotEmpty}`
* is equivalent to:
* `{tfw.view.button enabled: {Bind list converter:isNotEmpty}}`
*/
function buildElementClsBind( def, code, varName ) {
var attName, attValue, targetAttributeName;
var result;
var attribs = {};
for ( attName in def ) {
try {
targetAttributeName = getSuffix( attName, "bind." );
if ( !targetAttributeName ) continue;
attValue = def[ attName ];
if ( isSpecial( attValue ) ) {
result = { "0": "Bind", "1": def[ attName ][ "0" ] };
Object.keys( attValue ).forEach( function ( key ) {
if ( key == 0 ) return;
result[ key ] = attValue[ key ];
} );
attribs[ targetAttributeName ] = result;
} else {
attribs[ targetAttributeName ] = { "0": "Bind", "1": attValue };
}
} catch ( ex ) {
throw ex + "\n...in tag's bind attribute " + varName + "/" + JSON.stringify( attName );
}
}
return buildElementTagAttribsStandard( attribs, code, varName );
}
/**
* Generate code from attributes starting with "attrib.". For
* instance `attrib.|disabled: {Bind enabled}` means that the attrib
* `disabled` must be added if the attribute `enabled` is `true` and
* removed if `enabled` is `false`.
* The syntax is exctly the same as for class switchers.
*/
function buildElementTagAttribSwitcher( def, code, varName ) {
var attName, attValue, attribs, attribsNames, attribName;
for ( attName in def ) {
attribsNames = getSuffix( attName, "attrib." );
if ( !attribsNames ) continue;
attValue = def[ attName ];
if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
throw "Only bindings are accepted as values for attrib-switchers!\n" +
attName + ": " + JSON.stringify( attValue );
}
attribs = attribsNames.split( "|" );
var actions = [];
if ( isNonEmptyString( attribs[ 0 ] ) ) {
code.requires.$ = "require('dom')";
code.functions[ "addAttribIfTrue" ] = "(element, attribName, value) {\n" +
" if( value ) $.att(element, attribName);\n" +
" else $.removeAtt(element, attribName); };";
attribName = JSON.stringify( attribs[ 0 ] );
actions.push( "addAttribIfTrue( " + varName + ", " + attribName + ", v );" );
}
if ( isNonEmptyString( attribs[ 1 ] ) ) {
code.requires.$ = "require('dom')";
code.functions[ "addAttribIfFalse" ] = "(element, attribName, value) {\n" +
" if( value ) $.removeAtt(element, attribName);\n" +
" else $.att(element, attribName); };";
attribName = JSON.stringify( attribs[ 1 ] );
actions.push( "addAttribIfFalse( " + varName + ", " + attribName + ", v );" );
}
code.addLinkFromBind( attValue, actions );
}
}
/**
* @example
* class.*: {[flat,type,pressed] getClasses}
* class.*: [ {[flat,pressed] getClasses1}, {[value,pressed] getClasses2} ]
*/
function buildElementTagClassSwitcherStar( items, code, varName ) {
if ( !Array.isArray( items ) ) items = [ items ];
items.forEach( function ( item, index ) {
code.that = true;
if ( typeof item[ 0 ] === 'string' ) item[ 0 ] = [ item[ 0 ] ];
var pathes = ensureArrayOfStrings( item[ 0 ], "class.*: " + JSON.stringify( items ) );
var behindFunctionName = ensureString( item[ 1 ], "The behind function must be a string!" );
pathes.forEach( function ( path ) {
code.addLink( { path: path }, {
action: [
varName + ".applyClass(",
" CODE_BEHIND" + keySyntax( behindFunctionName ) +
".call(that,v," + JSON.stringify( path ) + "), " + index + ");"
]
} );
} );
} );
}
function buildElementEvents( attribs, code, varName ) {
var attName, attValue;
var eventHandlers = [];
for ( attName in attribs ) {
var eventName = getSuffix( attName, "event." );
if ( !eventName ) continue;
attValue = attribs[ attName ];
if ( typeof attValue === 'string' ) {
// Using a function from code behind.
code.addNeededBehindFunction( attValue );
eventHandlers.push(
JSON.stringify( eventName ) + ": CODE_BEHIND" + keySyntax( attValue ) + ".bind( this ),"
);
} else {
eventHandlers.push( JSON.stringify( eventName ) + ": function(v) {" );
eventHandlers = eventHandlers.concat( generateFunctionBody( attValue, code, " " ) );
eventHandlers.push( "}," );
}
}
if ( eventHandlers.length > 0 ) {
code.requires.View = "require('tfw.view');";
code.section.events.push( "View.events(" + varName + ", {" );
// Retirer la virgule de la dernière ligne.
var lastLine = eventHandlers.pop();
eventHandlers.push( lastLine.substr( 0, lastLine.length - 1 ) );
// Indenter.
code.section.events = code.section.events.concat( eventHandlers.map( x => " " + x ) );
code.section.events.push( "});" );
}
}
/**
* @returns {array}
* If there are constant attribs, they are returned.
* For instance, for `{tfw.view.button icon:gear wide:true flat:false content:{Bind label}}`
* the result will be : [
* ["icon", "gear"],
* ["wide", true],
* ["flat", false]
* ]
* @example
* {DIV class: {Bind value}}
* {DIV class: hide}
* {DIV textContent: {Intl title}}
*/
function buildElementTagAttribsStandard( attribs, code, varName ) {
try {
const initParameters = [];
for ( const attName of Object.keys( attribs ) ) {
const attValue = attribs[ attName ];
if ( isSpecial( attValue, "bind" ) ) {
code.addLinkFromBind( attValue, `${varName}/${attName}` );
} else if ( isSpecial( attValue, "intl" ) ) {
initParameters.push( [ attName, verbatim( "_(" + JSON.stringify( attValue[ 1 ] ) + ")" ) ] );
} else if ( isSpecial( attValue, "behind" ) ) {
const
functionBehindName = attValue[ 1 ],
call = `CODE_BEHIND${keySyntax(functionBehindName)}.bind( that )`;
code.addNeededBehindFunction( functionBehindName );
initParameters.push( [ attName, verbatim( call ) ] );
} else {
initParameters.push( [ attName, attValue ] );
}
}
return initParameters;
} catch ( ex ) {
throw Error( `${ex}
...in buildElementTagAttribsStandard:
attribs = ${JSON.stringify(attribs)}` );
}
}
function buildElementSpecialChild( def, code, varName ) {
const type = def[ 0 ];
if ( type !== 'Bind' )
throw "For tag elements, the children can be defined by an array or by `{Bind ...}`!\n" +
"You provided `{" + type + " ...}`.";
code.section.ons.push(
"pm.on('" + def[ 1 ] + "', function(v) { $.clear(" + varName + ", v); });" );
}
function generateFunctionBody( def, code, indent ) {
code.that = true;
var output = [];
if ( !Array.isArray( def ) ) def = [ def ];
def.forEach( function ( item ) {
if ( typeof item === 'string' ) {
code.that = true;
output.push( indent + item + ".call( that, v );" );
} else if ( isSpecial ) {
var type = item[ 0 ].toLowerCase();
var generator = functionBodyGenerators[ type ];
if ( typeof generator !== 'function' ) {
throw "Don't know how to build a function body from " + JSON.stringify( def ) + "!\n" +
"Known commands are: " + Object.keys( functionBodyGenerators ).join( ", " ) + ".";
}
generator( output, item, code, indent );
}
} );
return output;
}
const functionBodyGenerators = {
toggle( output, def, code, indent ) {
const elemVar = getElemVarFromPath( def[ 1 ] );
output.push( `${indent}${elemVar} = ${elemVar} ? false : true;` );
},
set( output, def, code, indent ) {
const elemVar = getElemVarFromPath( def[ 1 ] );
output.push( `${indent}${elemVar} = ${getValueCode(def[2])};` );
},
behind( output, def, code, indent ) {
const
behindFunctionName = ensureString( def[ 1 ], "In {Behind <name>}, <name> must be a string!" ),
lines = code.generateBehindCall( behindFunctionName, indent, "v" );
output.push.apply( output, lines );
}
};
function getValueCode( input ) {
if ( isSpecial( input, "bind" ) ) {
return "that" + keySyntax( input[ 1 ] );
}
return JSON.stringify( input );
}
/**
* A path is a string which represent a variable.
* For instance: "price" or "plateau/animate-direction".
*
* @example
* getElemVarFromPath("price") === "that.price"
* getElemVarFromPath("plateau/animate-direction") ===
* "that.$elements.plateau.animateDirection"
*
* @param {string} path - "price", "plateau/animate-direction", ...
* @param {string} _root - Optional (default: "that").
* @returns {string} Javascript code to access the variable defined by the path.
*/
function getElemVarFromPath( path, _root ) {
const root = typeof _root !== "undefined" ? _root : "that";
const items = path.split( "/" ).map( function ( item ) {
return camelCase( item.trim() );
} );
const result = items.map( function ( x, i ) {
if ( i === 0 && items.length > 1 ) return `.$elements${keySyntax(x)}`;
return `.${camelCase(x)}`;
} );
return `${root}${result.join("")}`;
}
/**
* An attribute is marked as _special_ as soon as it has a dot in its name.
* `view.attribs` is special, but `attribs` is not.
* Attributes with a numeric key are marked as _implicit_.
* @return `{ standard: {...}, special: {...}, implicit: [...] }`.
*/
function extractAttribs( def ) {
var key, val, attribs = { standard: {}, special: {}, implicit: [] };
for ( key in def ) {
val = def[ key ];
if ( RX_INTEGER.test( key ) ) {
attribs.implicit.push( val );
} else if ( RX_STD_ATT.test( key ) ) {
attribs.standard[ key ] = val;
} else {
attribs.special[ key ] = val;
}
}
return attribs;
}
/**
* Check if an object has attributes or not.
* It is empty if it has no attribute.
*/
function isEmptyObj( obj ) {
for ( var k in obj ) return false;
return true;
}
/**
* Check if an object as at least on attribute.
*/
function hasAttribs( obj ) {
for ( var k in obj ) return true;
return false;
}
function getSuffix( text, prefix ) {
if ( text.substr( 0, prefix.length ) !== prefix ) return null;
return text.substr( prefix.length );
}
function getVarName( def, defaultVarName ) {
var id = def[ "view.id" ];
if ( typeof id === 'undefined' ) {
if ( typeof defaultVarName === 'undefined' ) return "e_";
return defaultVarName;
}
return "e_" + camelCase( id );
}
function addUnique( arr, item ) {
if ( arr.indexOf( item ) === -1 ) {
arr.push( item );
}
}
function keySyntax( name ) {
if ( RX_IDENTIFIER.test( name ) ) return "." + name;
return "[" + JSON.stringify( name ) + "]";
}
function isNonEmptyString( text ) {
if ( typeof text !== 'string' ) return false;
return text.trim().length > 0;
}
function clone( obj ) {
return JSON.parse( JSON.stringify( obj ) );
}
function arrayToCode( arr, indent ) {
if ( typeof indent !== 'string' ) indent = "";
return indent + arr.join( "\n" + indent );
}
function arrayToCodeWithNewLine( arr, indent ) {
if ( arr.length === 0 ) return "";
return arrayToCode( arr, indent ) + "\n";
}
function ensureArrayOfStrings( arr, msg ) {
if ( typeof msg === 'undefined' ) msg = '';
if ( !Array.isArray( arr ) )
throw ( msg + "\n" + "Expected an array of strings!" ).trim();
arr.forEach( function ( item, index ) {
if ( typeof item !== 'string' )
throw ( msg + "\n" + "Item #" + index + " must be a string!" ).trim();
} );
return arr;
}
function ensureString( str, msg ) {
if ( typeof msg === 'undefined' ) msg = '';
if ( typeof str !== 'string' )
throw ( msg + "\n" + "Expected a string!" ).trim();
return str;
}
/**
* Special objects of type "verbatim" must not be transformed.
*/
function verbatim( text ) {
return { 0: "verbatim", 1: text };
}
function putQuotesIfNeeded( name ) {
if ( RX_IDENTIFIER.test( name ) ) return name;
return '"' + name + '"';
}
function parseComplexValue( code, value, indent ) {
try {
if ( typeof indent === 'undefined' ) indent = '';
var lines = recursiveParseComplexValue( code, value );
var out = expandLines( lines )
.map( function ( item, idx ) {
if ( idx === 0 ) return item;
return indent + item;
} )
.join( '\n' );
return out;
} catch ( ex ) {
throw ex + "\n...in parseComplexValue: " + limitJson( value );
}
}
function expandLines( arr, indent ) {
try {
if ( typeof indent === 'undefined' ) indent = '';
var out = [];
if ( Array.isArray( arr ) ) {
arr.forEach( function ( itm ) {
if ( Array.isArray( itm ) ) {
out.push.apply( out, expandLines( itm, indent + ' ' ) );
} else {
out.push( indent + itm );
}
} );
return out;
}
return [ arr ];
} catch ( ex ) {
throw ex + "\n...in expandLines: " + limitJson( arr );
}
}
function recursiveParseComplexValue( code, value ) {
try {
if ( value === null ) return "null";
if ( value === undefined ) return "undefined";
// Deal with internationalization: _('blabla')
if ( isSpecial( value, "verbatim" ) ) {
return value[ 1 ];
}
if ( [ 'string', 'number', 'boolean', 'symbol' ].indexOf( typeof value ) !== -1 ) return JSON.stringify( value );
var out = [];
if ( Array.isArray( value ) ) return recursiveParseComplexValueArray( code, value );
else if ( isSpecial( value ) ) return recursiveParseComplexValueSpecial( code, value );
else return recursiveParseComplexValueObject( code, value );
} catch ( ex ) {
throw ex + "\n...in recursiveParseComplexValue: " + limitJson( value );
}
// We should never end here.
return JSON.stringify( value );
}
function recursiveParseComplexValueArray( code, value ) {
if ( value.length === 0 ) return "[]";
if ( value.length === 1 ) {
var val = recursiveParseComplexValue( code, value[ 0 ] );
if ( typeof val === 'string' )
return "[" + val + "]";
return [ "[", val, "]" ];
}
return [
"[",
value.map( function ( itm, idx ) {
return recursiveParseComplexValue( code, itm ) +
( idx < value.length - 1 ? "," : "" );
} ),
"]"
];
}
function recursiveParseComplexValueObject( code, value ) {
try {
var val;
var keys = Object.keys( value );
if ( keys.length === 0 ) return "{}";
if ( keys.length === 1 ) {
val = recursiveParseComplexValue( code, value[ keys[ 0 ] ] );
if ( typeof val === 'string' ) {
return "{" + putQuotesIfNeeded( keys[ 0 ] ) + ": " + val + "}";
return surround( "{" + putQuotesIfNeeded( keys[ 0 ] ) + ":", val, "}" );
}
}
return [
"{",
keys.map( function ( key, idx ) {
var val = recursiveParseComplexValue( code, value[ key ] );
if ( idx < keys.length - 1 ) {
if ( typeof val === 'string' )
return putQuotesIfNeeded( key ) + ": " + val + ",";
return surround( putQuotesIfNeeded( key ) + ": ", val, "," );
} else {
if ( typeof val === 'string' )
return putQuotesIfNeeded( key ) + ": " + val;
return glueBefore( putQuotesIfNeeded( key ) + ": ", val );
}
} ),
"}"
];
} catch ( ex ) {
throw ex + "\n...in recursiveParseComplexValueObject: " + limitJson( value );
}
}
function recursiveParseComplexValueSpecial( code, value ) {
if ( isSpecial( value, 'intl' ) ) {
return '_(' + JSON.stringify( value[ "1" ] ) + ')';
}
if ( isSpecial( value, 'bind' ) ) return value;
return buildElement( value, code, "e_" + code.id() );
}
function surround( head, arr, tail ) {
glueAfter(
glueBefore( head, arr ),
tail
);
return arr;
}
function glueBefore( item, arr ) {
if ( arr.length === 0 ) arr.push( item );
else if ( typeof arr[ 0 ] === 'string' ) arr[ 0 ] = item + arr[ 0 ];
else glueBefore( arr[ 0 ], item );
return arr;
}
function glueAfter( arr, item ) {
const last = arr.length - 1;
if ( arr.length === 0 ) arr.push( item );
else if ( typeof arr[ last ] === 'string' ) arr[ last ] = arr[ last ] + item;
else glueAfter( arr[ last ], item );
return arr;
}
/**
* @param {any} obj - The object to stringify.
* @param {integer} max - The max size of the stringification.
* @returns {string} A stringified JSON with a limited size.
*/
function limitJson( obj, max = 100 ) {
return limit( JSON.stringify( obj ), max );
}
/**
* @param {string} txt - Text to truncate if too long.
* @param {integer} max - The max size of the text.
* @returns {string} The text with ellipsis if too long.
*/
function limit( txt, max = 100 ) {
if ( typeof txt === 'undefined' ) return "undefined";
else if ( txt === null ) return "null";
if ( txt.length <= max ) return txt;
return `${txt.substr( 0, max )}...`;
}
/**
* When there is no default value in `view.attribs` item, we can simplify the writting:
*
* @example
* onTap: action
* onTap: {action}
*
* stick: [up, down]
* stick: {[up, down] up}
*/
function expandViewAttribValue( value ) {
try {
if (