UNPKG

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
"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 (