UNPKG

55.4 kBJavaScriptView Raw
1"use strict";
2
3const
4 Template = require( "./boilerplate.view.template" ),
5 Common = require( "./boilerplate.view.common" );
6
7const
8 camelCase = Common.camelCase,
9 CamelCase = Common.CamelCase,
10 isSpecial = Common.isSpecial;
11
12const
13 RX_VIEW_ATTRIB = /^[a-z][a-z0-9]+(-[a-z0-9]+)*$/,
14 RX_INTEGER = /^[0-9]+$/,
15 RX_STD_ATT = /^([a-zA-Z]+:)?[A-Za-z0-9-]+$/,
16 RX_TAG_NAME = /^H[1-9]|[A-Z]+$/,
17 RX_CLS_NAME = /^[a-z][a-z0-9]*\.[a-z0-9.-]+$/,
18 RX_IDENTIFIER = /^[_$a-z][_$a-z0-9]+$/i;
19
20
21/**
22 * Generate Javascript code for XJS View.
23 *
24 * @param {object} _def - `{View ...}`
25 * @param {string} codeBehind - Piece of Javascript code to which we will add the generated one.
26 * @param {string} moduleName - NAme of the Javascript module.
27 *
28 * @return {string} Code generated from the XJS definition.
29 */
30exports.generateCodeFrom = function ( _def, codeBehind, moduleName ) {
31 try {
32 const
33 def = removeViewPrefix( _def ),
34 code = new Template( codeBehind, moduleName );
35 // Debug mode ?
36 code.debug = Boolean( def[ "view.debug" ] );
37
38 // Define attributes.
39 buildViewAttribs( def, code );
40 buildViewAttribsFire( def, code );
41 // Define methods.
42 buildViewPrototype( def, code );
43 // Define static members.
44 buildViewStatics( def, code );
45 // Look for any intialize function in code behinid.
46 buildViewInit( def, code );
47 // Transform `def` to make it look like an HTML tag.
48 declareRootElement( def, code );
49
50 // Generate full code.
51 code.requires.$ = "require('dom')";
52 if ( code.pm ) {
53 code.requires.PM = "require('tfw.binding.property-manager')";
54 }
55 let out = arrayToCodeWithNewLine( [
56 codeBehind,
57 "\n",
58 "//===============================",
59 "// XJS:View autogenerated code.",
60 "try {"
61 ] );
62 out += outputAll( code, moduleName );
63 out += arrayToCodeWithNewLine( [
64 "}",
65 "catch( ex ) {",
66 ` throw Error('Definition error in XJS of "${moduleName}"\\n' + ex);`,
67 "}"
68 ] );
69 return out;
70 } catch ( ex ) {
71 throw Error( `${ex}\n...in module ${JSON.stringify(moduleName)}` );
72 }
73};
74
75/**
76 * Return the code from an array and an identation.
77 * The code is preceded by a comment.
78 *
79 * @param {array} arr - Array of strings and/or arrays.
80 * @param {string} comment - Heading comment. Optional.
81 * @param {string} _indent - Indetation before each line.
82 *
83 * @returns {string} Readable Javascript code.
84 */
85function generate( arr, comment, _indent ) {
86 try {
87 const indent = typeof _indent !== "undefined" ? _indent : " ";
88 if ( arr.length === 0 ) return "";
89 let out = '';
90 if ( comment && comment.length > 0 ) {
91 let
92 len = comment.length + 3,
93 dashes = '';
94 while ( len-- > 0 ) dashes += "-";
95 out += `${indent}//${dashes}\n${indent}// ${comment}.\n`;
96 }
97 arr.forEach( function ( line ) {
98 if ( Array.isArray( line ) ) {
99 out += generate( line, null, ` ${indent}` );
100 } else {
101 out += `${indent}${line}\n`;
102 }
103 } );
104 return out;
105 } catch ( ex ) {
106 throw ex + "\n...in generate: " + JSON.stringify( comment );
107 }
108}
109
110/**
111 * @example
112 * {View DIV view.init:intialize}
113 */
114function buildViewInit( def, code ) {
115 try {
116 const init = def[ "view.init" ];
117 if ( typeof init === 'undefined' ) return;
118 if ( typeof init !== 'string' )
119 throw "The value of attribute `view.init` must be a string!";
120 code.addNeededBehindFunction( init );
121 code.section.init = init;
122 } catch ( ex ) {
123 throw ex + "\n...in view.init: " + JSON.stringify( init, null, " " );
124 }
125}
126
127
128/**
129 * Add static functions to the current class.
130 *
131 * @example
132 * view.statics: ["show", "check"]
133 * view.statics: "show"
134 */
135function buildViewStatics( def, code ) {
136 try {
137 let statics = def[ "view.statics" ];
138
139 if ( typeof statics === 'undefined' ) return;
140 if ( typeof statics === 'string' ) statics = [ statics ];
141 else if ( !Array.isArray( statics ) ) {
142 throw Error( "view.statics must be a string or an array of strings!" );
143 }
144
145 statics.forEach( function ( name ) {
146 code.addNeededBehindFunction( name );
147 code.section.statics.push(
148 "ViewClass" + keySyntax( name ) + " = CODE_BEHIND" + keySyntax( name ) + ".bind(ViewClass);"
149 );
150 } );
151 } catch ( ex ) {
152 throw ex + "\n...in view.statics: " + JSON.stringify( statics );
153 }
154}
155
156function buildViewPrototype( def, code ) {
157 let proto = def[ "view.prototype" ];
158 try {
159 if ( typeof proto === 'undefined' ) return;
160 if ( !Array.isArray( proto ) ) proto = [ proto ];
161
162 proto.forEach( function ( name ) {
163 code.addNeededBehindFunction( name );
164 code.section.statics.push(
165 "ViewClass.prototype" + keySyntax( name ) + " = CODE_BEHIND" + keySyntax( name ) + ";"
166 );
167 } );
168 } catch ( ex ) {
169 throw ex + "\n...in view.prototype: " + JSON.stringify( proto, null, " " );
170 }
171}
172
173/**
174 * @param {object} def - ``.
175 * @param {Template} code - Template.
176 *
177 * @example
178 * view.attribs: {
179 * flat: true
180 * type: [default, primary, secondary]
181 * count: {integer}
182 * content: {string ok behind: onContentChanged}
183 * }
184 *
185 * @returns {undefined}
186 */
187function buildViewAttribs( def, code ) {
188 code.pm = true;
189 const attribs = def[ "view.attribs" ];
190 if ( typeof attribs !== 'object' ) return;
191 try {
192 for ( const attName of Object.keys( attribs ) ) {
193 if ( !RX_VIEW_ATTRIB.test( attName ) )
194 throw Error( `Bad syntax for attribute's name: ${JSON.stringify(attName)}
195 Example of valid syntax: action, duration-visible, ...
196 Example of invalid syntax: 0, durationVisible, ...` );
197 const
198 attValue = expandViewAttribValue( attribs[ attName ] ),
199 camelCaseAttName = camelCase( attName );
200 if ( isSpecial( attValue ) || Array.isArray( attValue[ 0 ] ) ) {
201 buildViewAttribsSpecial( camelCaseAttName, attValue, code );
202 } else {
203 // flat: true
204 buildViewAttribsInit( camelCaseAttName, attValue, code );
205 code.section.attribs.define
206 .push( `pm.create(${JSON.stringify(camelCaseAttName)}, {init: ${JSON.stringify(attValue)}});` );
207 }
208 }
209 } catch ( ex ) {
210 bubble( ex, `view.attribs: ${JSON.stringify(attribs, null, " ")}` );
211 }
212}
213
214/**
215 * Trigger the fire event on each attribute.
216 *
217 * @param {[type]} def [description]
218 * @param {[type]} code [description]
219 * @returns {undefined}
220 */
221function buildViewAttribsFire( def, code ) {
222 code.pm = true;
223 const attribs = def[ "view.attribs" ];
224 if ( typeof attribs !== 'object' ) return;
225 try {
226 for ( const attName of Object.keys( attribs ) ) {
227 const camelCaseAttName = camelCase( attName );
228 if ( !code.isAction( attName ) ) {
229 buildViewAttribsInitFire( camelCaseAttName, code );
230 }
231 }
232 } catch ( ex ) {
233 bubble( ex, `...in buildViewAttribsFire - view.attribs: ${JSON.stringify(attribs, null, " ")}` );
234 }
235}
236
237/**
238 * Attribute with casting.
239 * @example
240 * type: {[default, primary, secondary]}
241 * count: {integer}
242 * content: {string ok behind: onContentChanged}
243 */
244function buildViewAttribsSpecial( attName, attValue, code ) {
245 const
246 type = attValue[ 0 ],
247 init = attValue[ 1 ];
248 let requireConverter = false;
249
250 try {
251 if ( typeof attValue.behind !== 'undefined' ) {
252 buildViewAttribsSpecialCodeBehind( attName, attValue.behind, code );
253 }
254 if ( typeof attValue.debug !== 'undefined' ) {
255 buildViewAttribsSpecialDebug( attName, attValue.debug, code );
256 }
257 if ( Array.isArray( type ) ) {
258 // Enumerate.
259 code.addCast( "enum" );
260 code.section.attribs.define.push(
261 "pm.create(" + JSON.stringify( attName ) +
262 `, { cast: conv_enum(${JSON.stringify(type)}), init: ${JSON.stringify(init)} });` );
263 buildViewAttribsInit( attName, init, code );
264 } else {
265 switch ( type ) {
266 case 'action':
267 code.actions.push( attName );
268 code.section.attribs.define.push(
269 "pm.createAction(" + JSON.stringify( attName ) + ")" );
270 break;
271 case 'any':
272 code.section.attribs.define.push(
273 "pm.create(" + JSON.stringify( attName ) + ");" );
274 buildViewAttribsInit( attName, init, code );
275 break;
276 case 'boolean':
277 case 'booleans':
278 case 'date':
279 case 'color':
280 case 'string':
281 case 'strings':
282 case 'array':
283 case 'list':
284 case 'intl':
285 case 'time':
286 case 'unit':
287 case 'units':
288 case 'multilang':
289 case 'validator':
290 requireConverter = true;
291 code.vars[ "conv_" + type ] = "Converters.get('" + type + "')";
292 code.section.attribs.define.push(
293 "pm.create(" + JSON.stringify( attName ) +
294 ", { cast: conv_" + type + " });" );
295 buildViewAttribsInit( attName, init, code );
296 break;
297 case 'integer':
298 case 'float':
299 requireConverter = true;
300 code.vars[ "conv_" + type ] = "Converters.get('" + type + "')";
301 // Is Not a number, take this default value.
302 var nanValue = attValue.default;
303 if ( typeof nanValue !== 'number' || isNaN( nanValue ) ) {
304 nanValue = 0;
305 }
306 code.section.attribs.define.push(
307 "pm.create(" + JSON.stringify( attName ) +
308 ", { cast: conv_" + type + "(" + nanValue + ") });" );
309 buildViewAttribsInit( attName, init, code );
310 break;
311 default:
312 throw "Unknown type \"" + type + "\" for attribute \"" + attName + "\"!";
313 }
314 }
315
316 if ( requireConverter ) {
317 code.requires.Converters = "require('tfw.binding.converters')";
318 }
319 } catch ( ex ) {
320 bubble(
321 ex,
322 `buildViewAttribsSpecial(${attName}: ${JSON.stringify(attValue)})`
323 );
324 }
325}
326
327/**
328 * @example
329 * view.attribs: {
330 * size: {unit "32px", debug: "Width of the picture"}
331 * }
332 */
333function buildViewAttribsSpecialDebug( attName, debug, code ) {
334 try {
335 console.warn( `Debug is set for ${code.moduleName}/${attName}!`.bgYellow.black );
336 code.section.ons.push(
337 `pm.on("${attName}, function(v) {`,
338 ` console.info(${JSON.stringify(debug)});`,
339 " const attribs = {};",
340 " Object.keys(that).forEach(function(key) {",
341 " attribs[key] = that[key];",
342 " });",
343 ` console.info('${code.moduleName}/${attName} =', v);`,
344 " console.info(attribs);",
345 " if( typeof console.trace === 'function' ) console.trace();",
346 "});"
347 );
348 } catch ( ex ) {
349 bubble(
350 ex,
351 `buildViewAttribsSpecialDebug(${attName}: ${JSON.stringify(debug)})`
352 );
353 }
354}
355
356/**
357 * @example
358 * view.attribs: {
359 * size: {unit "32px", behind: onSizeChanged}
360 * }
361 */
362function buildViewAttribsSpecialCodeBehind( attName, functionBehindName, code ) {
363 try {
364 if ( typeof functionBehindName !== 'string' ) {
365 throw Error( `The property "Behind" of the attribute "${attName} must be a string!` );
366 }
367 code.that = true;
368 code.addNeededBehindFunction( functionBehindName );
369 code.section.ons.push(
370 `pm.on("${attName}", function(v) {`,
371 " try {",
372 ` CODE_BEHIND${keySyntax(functionBehindName)}.call( that, v );`,
373 " }",
374 " catch( ex ) {",
375 " console.error(",
376 ` 'Exception in function behind "${functionBehindName}" of module "${code.moduleName}" for attribute "${attName}"!'`,
377 " );",
378 " console.error( ex );",
379 " }",
380 "});"
381 );
382 } catch ( ex ) {
383 bubble(
384 ex,
385 `buildViewAttribsSpecialCodeBehind(${attName}, ${functionBehindName})`
386 );
387 }
388}
389
390/**
391 * Initialize attribute with a value. Priority to the value set in the
392 * contructor args.
393 */
394function buildViewAttribsInit( attName, attValue, code ) {
395 try {
396 if ( typeof attValue === "undefined" ) {
397 // code.section.attribs.init.push(`this.${attName} = args[${JSON.stringify(attName)}];`);
398 code.section.attribs.init
399 .push( `pm.set("${attName}", args[${JSON.stringify(attName)}]);` );
400 } else {
401 code.functions.defVal = "(args, attName, attValue) " +
402 "{ return args[attName] === undefined ? attValue : args[attName]; }";
403 // code.section.attribs.init.push( "this." + attName + " = defVal(args, " +
404 // JSON.stringify( attName ) + ", " + JSON.stringify( attValue ) +
405 // ");" );
406 code.section.attribs.init
407 .push( `pm.set("${attName}", defVal(args, "${attName}", ${JSON.stringify( attValue )}));` );
408 }
409 } catch ( ex ) {
410 bubble(
411 ex,
412 `buildViewAttribsInit(${attName}, ${JSON.stringify(attValue)})`
413 );
414 }
415}
416
417/**
418 * Once every attribute has been set, we must fire them.
419 *
420 * @param {[type]} attName [description]
421 * @param {[type]} code [description]
422 * @returns {undefined}
423 */
424function buildViewAttribsInitFire( attName, code ) {
425 try {
426 code.section.attribs.init
427 .push( `pm.fire("${attName}");` );
428 } catch ( ex ) {
429 bubble(
430 ex,
431 `buildViewAttribsInitFire(${attName})`
432 );
433 }
434}
435
436/**
437 * An element can be a tag (`{DIV...}`) or a view (`{tfw.view.button...}`).
438 *
439 * @param {object} def - Something like `{tfw.view.button ...}`.
440 * @param {Template} code - Helping stuff for code generation.
441 * @param {string} _varName - Name of the variable holding this element in the end.
442 * @returns {undefined}
443 */
444function buildElement( def, code, _varName ) {
445 const varName = getVarName( def, _varName );
446 addUnique( code.elementNames, varName );
447 try {
448 const type = def[ 0 ];
449 if ( RX_TAG_NAME.test( type ) ) {
450 buildElementTag( def, code, varName );
451 // Children.
452 let children = def[ 1 ];
453 if ( typeof children === 'undefined' ) return varName;
454 if ( isSpecial( children ) ) {
455 buildElementSpecialChild( children, code, varName );
456 return varName;
457 }
458 if ( !Array.isArray( children ) ) children = [ children ];
459 const toAdd = [];
460 children.forEach( function ( child, index ) {
461 if ( typeof child === 'string' || typeof child === 'number' ) {
462 toAdd.push( JSON.stringify( "" + child ) );
463 } else {
464 const childVarName = buildElement( child, code, code.id( "e_" ) );
465 toAdd.push( childVarName );
466 }
467 } );
468 if ( toAdd.length > 0 ) {
469 code.section.elements.define.push(
470 "$.add( " + varName + ", " +
471 toAdd.join( ", " ) + " );" );
472 }
473 return varName;
474 //buildElementTagChildren( def, code, varName );
475 } else if ( RX_CLS_NAME.test( type ) ) {
476 buildElementCls( def, code, varName );
477 } else throw Error( "Unknown element name: " + JSON.stringify( type ) + "!\n" +
478 "Names must have one of theses syntaxes: " +
479 JSON.stringify( RX_TAG_NAME.toString() ) +
480 " or " + JSON.stringify( RX_CLS_NAME.toString() ) + ".\n" +
481 JSON.stringify( def ) );
482 } catch ( ex ) {
483 throw Error( `${ex}\n...in buildElement - element "${varName}": ${limitJson( def )}` );
484 } finally {
485 // Store the variable for use in code behind.
486 const id = def[ "view.id" ];
487 if ( typeof id === 'string' ) {
488 code.section.elements.define
489 .push( `this.$elements.${camelCase( id )} = ${varName};` );
490 }
491 }
492 return varName;
493}
494
495/**
496 * @param {object} def - Something like `{tfw.view.button ...}`.
497 * @param {Template} code - Helping stuff for code generation.
498 * @param {string} varName - Name of the variable holding this element in the end.
499 * @returns {undefined}
500 */
501function buildElementTagChildren( def, code, varName ) {
502 const childOrChildren = def[ 1 ];
503 if ( typeof childOrChildren === 'undefined' ) return;
504 if ( isSpecial( childOrChildren ) ) {
505 buildElementSpecialChild( childOrChildren, code, varName );
506 return;
507 }
508 const
509 children = Array.isArray( childOrChildren ) ? childOrChildren : [ childOrChildren ],
510 toAdd = [];
511 children.forEach( function ( child, index ) {
512 try {
513 if ( typeof child === 'string' || typeof child === 'number' ) {
514 toAdd.push( JSON.stringify( `${child}` ) );
515 } else {
516 const childVarName = buildElement( child, code, code.id( "e_" ) );
517 toAdd.push( childVarName );
518 }
519 } catch ( ex ) {
520 const message = `...in child #${index} - ${ex}`;
521 console.error( message );
522 throw Error( message );
523 }
524 } );
525 if ( toAdd.length > 0 ) {
526 code.section.elements.define
527 .push( `$.add(${varName}, ${toAdd.join( ", " )});` );
528 }
529}
530
531/**
532 * `{INPUT placeholder:price value:{Bind price}}`
533 */
534function buildElementTag( def, code, varName ) {
535 const attribs = extractAttribs( def );
536
537 try {
538 const arr = Object.keys( attribs.standard );
539 code.requires[ "Tag" ] = "require('tfw.view').Tag";
540 code.section.elements.define.push(
541 "const " + varName + " = new Tag('" + def[ 0 ] + "'" +
542 ( arr.length > 0 ? ', ' + JSON.stringify( arr ) : '' ) +
543 ");" );
544
545 const initAttribs = buildElementTagAttribsStandard( attribs.standard, code, varName );
546 buildInitAttribsForTag( initAttribs, code, varName );
547 buildElementEvents( attribs.special, code, varName );
548 buildElementTagClassSwitcher( attribs.special, code, varName );
549 buildElementTagAttribSwitcher( attribs.special, code, varName );
550 buildElementTagStyle( attribs.special, code, varName );
551 buildElementTagChildren( attribs.special, code, varName );
552 buildElementTagOn( attribs.special, code, varName );
553 } catch ( ex ) {
554 throw ex + "\n...in tag \"" + varName + "\": " + limitJson( def );
555 }
556}
557
558function buildInitAttribsForTag( initAttribs, code, varName ) {
559 initAttribs.forEach( function ( initAttrib ) {
560 try {
561 const
562 attName = initAttrib[ 0 ],
563 attValue = initAttrib[ 1 ];
564 if ( isSpecial( attValue, "verbatim" ) ) {
565 code.section.elements.init.push(
566 varName + keySyntax( attName ) + " = " + attValue[ 1 ] + ";" );
567 } else {
568 if ( [ 'string', 'number', 'boolean' ].indexOf( typeof attValue ) === -1 )
569 throw "A tag's attribute value must be of type string or number!";
570 code.section.elements.init.push(
571 varName + keySyntax( attName ) + " = " + parseComplexValue( code, attValue, ' ' ) + ";" );
572 }
573 } catch ( ex ) {
574 throw ex + `...in buildInitAttribsForTag - tag's attribute: "${attName}"=${limitJson(attValue)}`;
575 }
576 } );
577}
578
579function buildElementCls( def, code, varName ) {
580 const attribs = extractAttribs( def );
581 try {
582 expandImplicitContent( attribs );
583
584 buildElementEvents( attribs.special, code, varName );
585
586 const
587 arr = Object.keys( attribs.standard ),
588 viewName = CamelCase( def[ 0 ] );
589 code.requires[ viewName ] = "require('" + def[ 0 ] + "')";
590 const initAttribs = buildElementTagAttribsStandard( attribs.standard, code, varName );
591 buildInitAttribsForCls( initAttribs, code, varName, viewName );
592 buildElementTagClassSwitcher( attribs.special, code, varName );
593 buildElementTagAttribSwitcher( attribs.special, code, varName );
594 buildElementTagStyle( attribs.special, code, varName );
595 buildElementClsBind( attribs.special, code, varName );
596 buildElementTagOn( attribs.special, code, varName );
597 } catch ( ex ) {
598 throw ex + "\n...in cls \"" + varName + "\": " + limitJson( attribs );
599 }
600}
601
602/**
603 * The following syntaxes are equivalent:
604 * @example
605 * // Explicit content
606 * {tfw.view.expand content: Hello}
607 * // Implicit content
608 * {tfw.view.expand Hello}
609 */
610function expandImplicitContent( attribs ) {
611 try {
612 if ( !attribs ) return;
613 if ( typeof attribs.implicit === 'undefined' ) return;
614 const implicitContent = attribs.implicit[ 1 ];
615 if ( typeof implicitContent === 'undefined' ) return;
616
617 const explicitContent = attribs.standard ? attribs.standard.content : undefined;
618 if ( explicitContent ) {
619 throw "Can't mix implicit and explicit `content`:\n" +
620 " implicit: " + limitJson( implicitContent ) + "\n" +
621 " explicit: " + limitJson( explicitContent );
622 }
623
624 attribs.standard.content = implicitContent;
625 } catch ( ex ) {
626 throw ex + "\n...in expandImplicitContent: " + limitJson( attribs );
627 }
628}
629
630function buildInitAttribsForCls( initAttribs, code, varName, viewName ) {
631 try {
632 var item, attName, attValue, parsedValue;
633
634 if ( initAttribs.length === 0 ) {
635 code.section.elements.define.push(
636 "const " + varName + " = new " + viewName + "();" );
637 } else if ( initAttribs.length === 1 ) {
638 item = initAttribs[ 0 ];
639 attName = camelCase( item[ 0 ] );
640 attValue = item[ 1 ];
641 parsedValue = parseComplexValue( code, attValue );
642 code.section.elements.define.push(
643 "const " + varName + " = new " + viewName + "({ " + putQuotesIfNeeded( attName ) + ": " +
644 parsedValue + " });" );
645 } else {
646 const out = [ "const " + varName + " = new " + viewName + "({" ];
647 initAttribs.forEach( function ( item, index ) {
648 try {
649 attName = camelCase( item[ 0 ] );
650 attValue = item[ 1 ];
651 parsedValue = parseComplexValue( code, attValue, ' ' );
652 out.push(
653 " " + putQuotesIfNeeded( attName ) + ": " +
654 parsedValue +
655 ( index < initAttribs.length - 1 ? "," : "" ) );
656 } catch ( ex ) {
657 throw "Error while parsing attribute " + JSON.stringify( attName ) +
658 " with value " + JSON.stringify( attValue ) + ":\n" + ex;
659 }
660 } );
661 out.push( "});" );
662 code.section.elements.define.push.apply( code.section.elements.define, out );
663 }
664 } catch ( ex ) {
665 throw ex + "\n...in buildInitAttribsForCls\n" +
666 " varName: " + varName + "\n" +
667 " viewName: " + viewName + "\n" +
668 " initAttribs: " + limitJson( initAttribs ) + "\n" + ex;
669 };
670}
671
672/**
673 * Generate code from attributes starting with "class.".
674 * For instance `class.blue: {Bind focused}` means that the class
675 * `blue` must be added if the attribute `focused` is `true` and
676 * removed if `focused` is `false`.
677 * On the contrary, `class.|red: {Bind focused}` means that the
678 * class `red` must be removed if `focused` is `true` and added if
679 * `focused` is `false`.
680 * Finally, `class.blue|red: {Bind focused}` is the mix of both
681 * previous syntaxes.
682 */
683function buildElementTagClassSwitcher( def, code, varName ) {
684 var attName, attValue, classes, classesNames, className;
685 for ( attName in def ) {
686 classesNames = getSuffix( attName, "class." );
687 if ( !classesNames ) continue;
688 attValue = def[ attName ];
689 if ( classesNames === '*' ) {
690 buildElementTagClassSwitcherStar( attValue, code, varName );
691 continue;
692 }
693 if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
694 throw "Only bindings are accepted as values for class-switchers!\n" +
695 attName + ": " + JSON.stringify( attValue );
696 }
697 classes = classesNames.split( "|" );
698 const actions = [];
699 if ( isNonEmptyString( classes[ 0 ] ) ) {
700 code.requires.$ = "require('dom')";
701 code.functions[ "addClassIfTrue" ] = "(element, className, value) {\n" +
702 " if( value ) $.addClass(element, className);\n" +
703 " else $.removeClass(element, className); };";
704 className = JSON.stringify( classes[ 0 ] );
705 actions.push( "addClassIfTrue( " + varName + ", " + className + ", v );" );
706 }
707 if ( isNonEmptyString( classes[ 1 ] ) ) {
708 code.requires.$ = "require('dom')";
709 code.functions[ "addClassIfFalse" ] = "(element, className, value) {\n" +
710 " if( value ) $.removeClass(element, className);\n" +
711 " else $.addClass(element, className); };";
712 className = JSON.stringify( classes[ 1 ] );
713 actions.push( "addClassIfFalse( " + varName + ", " + className + ", v );" );
714 }
715 code.addLinkFromBind( attValue, actions );
716 }
717}
718
719/**
720 * @example
721 * view.children: {Bind names map:makeListItem}
722 * view.children: {List names map:makeListItem}
723 */
724function buildElementTagChildren( def, code, varName ) {
725 const attValue = def[ "view.children" ];
726 if ( typeof attValue === 'undefined' ) return;
727
728 if ( typeof attValue === 'string' ) {
729 // Transformer la première ligne en la seconde:
730 // 1) view.children: names
731 // 2) view.children: {Bind names}
732 attValue = { "0": 'Bind', "1": attValue };
733 }
734 if ( isSpecial( attValue, "bind" ) ) {
735 code.requires.View = "require('tfw.view');";
736 attValue.action = [
737 "// Updating children of " + varName + ".",
738 "$.clear(" + varName + ");",
739 "if( !Array.isArray( v ) ) v = [v];",
740 "v.forEach(function (elem) {",
741 " $.add(" + varName + ", elem);",
742 "});"
743 ];
744 code.addLinkFromBind( attValue, attValue );
745 } else if ( isSpecial( attValue, "list" ) ) {
746 code.requires.View = "require('tfw.view');";
747 const codeBehindFuncName = attValue.map;
748 code.requires.ListHandler = "require('tfw.binding.list-handler');";
749 if ( typeof codeBehindFuncName === 'string' ) {
750 code.addNeededBehindFunction( codeBehindFuncName );
751 code.section.elements.init.push(
752 "ListHandler(this, " + varName + ", " + JSON.stringify( attValue[ 1 ] ) + ", {",
753 " map: CODE_BEHIND" + keySyntax( codeBehindFuncName ) + ".bind(this)",
754 "});"
755 );
756 } else {
757 // List handler without mapping.
758 code.section.elements.init.push(
759 "ListHandler(this, " + varName + ", " + JSON.stringify( attValue[ 1 ] ) + ");"
760 );
761 }
762 } else {
763 throw "Error in `view.children`: invalid syntax!\n" + JSON.stringify( attValue );
764 }
765}
766
767/**
768 * @example
769 * style.width: {Bind size}
770 * style.width: "24px"
771 */
772function buildElementTagStyle( def, code, varName ) {
773 var attName, attValue, styleName;
774
775 for ( attName in def ) {
776 styleName = getSuffix( attName, "style." );
777 if ( !styleName ) continue;
778 attValue = def[ attName ];
779 if ( typeof attValue === 'string' ) {
780 code.section.elements.init.push(
781 varName + ".$.style" + keySyntax( styleName ) + " = " + JSON.stringify( attValue ) );
782 continue;
783 }
784 if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
785 throw "Only bindings and strings are accepted as values for styles!\n" +
786 attName + ": " + JSON.stringify( attValue );
787 }
788
789 const attributeToBindOn = attValue[ 1 ];
790 if ( typeof attributeToBindOn !== 'string' )
791 throw "Binding syntax error for styles: second argument must be the name of a function behind!\n" +
792 attName + ": " + JSON.stringify( attValue );
793
794 const converter = attValue.converter;
795 if ( typeof converter === 'string' ) {
796 code.addCast( converter );
797 code.section.ons.push(
798 "pm.on(" + JSON.stringify( camelCase( attValue[ 1 ] ) ) + ", function(v) {",
799 " " + varName + ".$.style[" + JSON.stringify( styleName ) + "] = " +
800 "conv_" + converter + "( v );",
801 "});" );
802 } else {
803 code.section.ons.push(
804 "pm.on(" + JSON.stringify( camelCase( attValue[ 1 ] ) ) + ", function(v) {",
805 " " + varName + ".$.style[" + JSON.stringify( styleName ) + "] = v;",
806 "});" );
807 }
808 }
809}
810
811/**
812 * @example
813 * {tfw.view.button on.action: OnAction}
814 * {tfw.view.button on.action: {Toggle show-menu}}
815 */
816function buildElementTagOn( def, code, varName ) {
817 for ( const attName of Object.keys( def ) ) {
818 const targetAttributeName = getSuffix( attName, "on." );
819 if ( !targetAttributeName ) continue;
820
821 const attValue = typeof def[ attName ] === 'string' ? { 0: "Behind", 1: def[ attName ] } : def[ attName ];
822 code.section.ons.push(
823 `PM(${varName}).on(${JSON.stringify(targetAttributeName)},`,
824 buildFunction( attValue, code, varName ),
825 ");"
826 );
827 }
828}
829
830/**
831 * Create the code for a function.
832 *
833 * @example
834 * {Behind onVarChanged}
835 * {Toggle show-menu}
836 *
837 * @param {object} def - Function definition.
838 * @param {object} code - Needed for `code.section.ons`.
839 * @param {string} varName - Name of the object owning the attribute we want to listen on.
840 *
841 * @return {array} Resulting code as an array.
842 */
843function buildFunction( def, code, varName ) {
844 if ( isSpecial( def, "behind" ) ) {
845 const behindFunctionName = def[ 1 ];
846 code.addNeededBehindFunction( behindFunctionName );
847 code.that = true;
848 return [
849 "value => {",
850 " try {",
851 ` CODE_BEHIND${keySyntax(behindFunctionName)}.call(that, value, ${varName});`,
852 " }",
853 " catch( ex ) {",
854 " console.error(`Exception in code behind \"" +
855 behindFunctionName + "\" of module \"" +
856 code.moduleName + "\": ${ex}`);",
857 " }",
858 "}"
859 ];
860 }
861
862 if ( isSpecial( def, "toggle" ) ) {
863 const nameOfTheBooleanToToggle = def[ 1 ];
864 const namesOfTheBooleanToToggle = Array.isArray( nameOfTheBooleanToToggle ) ?
865 nameOfTheBooleanToToggle : [ nameOfTheBooleanToToggle ];
866 const body = namesOfTheBooleanToToggle
867 .map( name => `${getElemVarFromPath(name)} = !${getElemVarFromPath(name)};` );
868 return [
869 "() => {",
870 body,
871 "}"
872 ];
873 }
874
875 throw Error( `Function definition expected, but found ${JSON.stringify(def)}!` );
876}
877
878/**
879 * `{tfw.view.button bind.enabled: enabled}`
880 * is equivalent to:
881 * `{tfw.view.button enabled: {Bind enabled}}`
882 *
883 * `{tfw.view.button bind.enabled: {list converter:isNotEmpty}`
884 * is equivalent to:
885 * `{tfw.view.button enabled: {Bind list converter:isNotEmpty}}`
886 */
887function buildElementClsBind( def, code, varName ) {
888 var attName, attValue, targetAttributeName;
889 var result;
890 var attribs = {};
891
892 for ( attName in def ) {
893 try {
894 targetAttributeName = getSuffix( attName, "bind." );
895 if ( !targetAttributeName ) continue;
896
897 attValue = def[ attName ];
898
899 if ( isSpecial( attValue ) ) {
900 result = { "0": "Bind", "1": def[ attName ][ "0" ] };
901 Object.keys( attValue ).forEach( function ( key ) {
902 if ( key == 0 ) return;
903 result[ key ] = attValue[ key ];
904 } );
905 attribs[ targetAttributeName ] = result;
906 } else {
907 attribs[ targetAttributeName ] = { "0": "Bind", "1": attValue };
908 }
909 } catch ( ex ) {
910 throw ex + "\n...in tag's bind attribute " + varName + "/" + JSON.stringify( attName );
911 }
912 }
913
914 return buildElementTagAttribsStandard( attribs, code, varName );
915}
916
917
918/**
919 * Generate code from attributes starting with "attrib.". For
920 * instance `attrib.|disabled: {Bind enabled}` means that the attrib
921 * `disabled` must be added if the attribute `enabled` is `true` and
922 * removed if `enabled` is `false`.
923 * The syntax is exctly the same as for class switchers.
924 */
925function buildElementTagAttribSwitcher( def, code, varName ) {
926 var attName, attValue, attribs, attribsNames, attribName;
927 for ( attName in def ) {
928 attribsNames = getSuffix( attName, "attrib." );
929 if ( !attribsNames ) continue;
930 attValue = def[ attName ];
931 if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
932 throw "Only bindings are accepted as values for attrib-switchers!\n" +
933 attName + ": " + JSON.stringify( attValue );
934 }
935 attribs = attribsNames.split( "|" );
936 var actions = [];
937 if ( isNonEmptyString( attribs[ 0 ] ) ) {
938 code.requires.$ = "require('dom')";
939 code.functions[ "addAttribIfTrue" ] = "(element, attribName, value) {\n" +
940 " if( value ) $.att(element, attribName);\n" +
941 " else $.removeAtt(element, attribName); };";
942 attribName = JSON.stringify( attribs[ 0 ] );
943 actions.push( "addAttribIfTrue( " + varName + ", " + attribName + ", v );" );
944 }
945 if ( isNonEmptyString( attribs[ 1 ] ) ) {
946 code.requires.$ = "require('dom')";
947 code.functions[ "addAttribIfFalse" ] = "(element, attribName, value) {\n" +
948 " if( value ) $.removeAtt(element, attribName);\n" +
949 " else $.att(element, attribName); };";
950 attribName = JSON.stringify( attribs[ 1 ] );
951 actions.push( "addAttribIfFalse( " + varName + ", " + attribName + ", v );" );
952 }
953 code.addLinkFromBind( attValue, actions );
954 }
955}
956
957/**
958 * @example
959 * class.*: {[flat,type,pressed] getClasses}
960 * class.*: [ {[flat,pressed] getClasses1}, {[value,pressed] getClasses2} ]
961 */
962function buildElementTagClassSwitcherStar( items, code, varName ) {
963 if ( !Array.isArray( items ) ) items = [ items ];
964 items.forEach( function ( item, index ) {
965 code.that = true;
966 if ( typeof item[ 0 ] === 'string' ) item[ 0 ] = [ item[ 0 ] ];
967 var pathes = ensureArrayOfStrings( item[ 0 ], "class.*: " + JSON.stringify( items ) );
968 var behindFunctionName = ensureString( item[ 1 ], "The behind function must be a string!" );
969 pathes.forEach( function ( path ) {
970 code.addLink( { path: path }, {
971 action: [
972 varName + ".applyClass(",
973 " CODE_BEHIND" + keySyntax( behindFunctionName ) +
974 ".call(that,v," + JSON.stringify( path ) + "), " + index + ");"
975 ]
976 } );
977 } );
978 } );
979}
980
981function buildElementEvents( attribs, code, varName ) {
982 var attName, attValue;
983 var eventHandlers = [];
984
985 for ( attName in attribs ) {
986 var eventName = getSuffix( attName, "event." );
987 if ( !eventName ) continue;
988
989 attValue = attribs[ attName ];
990 if ( typeof attValue === 'string' ) {
991 // Using a function from code behind.
992 code.addNeededBehindFunction( attValue );
993 eventHandlers.push(
994 JSON.stringify( eventName ) + ": CODE_BEHIND" + keySyntax( attValue ) + ".bind( this ),"
995 );
996 } else {
997 eventHandlers.push( JSON.stringify( eventName ) + ": function(v) {" );
998 eventHandlers = eventHandlers.concat( generateFunctionBody( attValue, code, " " ) );
999 eventHandlers.push( "}," );
1000 }
1001 }
1002
1003 if ( eventHandlers.length > 0 ) {
1004 code.requires.View = "require('tfw.view');";
1005 code.section.events.push( "View.events(" + varName + ", {" );
1006 // Retirer la virgule de la dernière ligne.
1007 var lastLine = eventHandlers.pop();
1008 eventHandlers.push( lastLine.substr( 0, lastLine.length - 1 ) );
1009 // Indenter.
1010 code.section.events = code.section.events.concat( eventHandlers.map( x => " " + x ) );
1011 code.section.events.push( "});" );
1012 }
1013}
1014
1015/**
1016 * @return
1017 * If there are constant attribs, they are returned.
1018 * For instance, for `{tfw.view.button icon:gear wide:true flat:false content:{Bind label}}`
1019 * the result will be : [
1020 * ["icon", "gear"],
1021 * ["wide", true],
1022 * ["flat", false]
1023 * ]
1024 * @example
1025 * {DIV class: {Bind value}}
1026 * {DIV class: hide}
1027 * {DIV textContent: {Intl title}}
1028 */
1029function buildElementTagAttribsStandard( attribs, code, varName ) {
1030 var attName, attValue;
1031 var initParameters = [];
1032 for ( attName in attribs ) {
1033 attValue = attribs[ attName ];
1034 if ( isSpecial( attValue, "bind" ) ) {
1035 code.addLinkFromBind( attValue, varName + "/" + attName );
1036 } else if ( isSpecial( attValue, "intl" ) ) {
1037 initParameters.push( [ attName, verbatim( "_(" + JSON.stringify( attValue[ 1 ] ) + ")" ) ] );
1038 } else {
1039 //initParameters.push([ attName, JSON.stringify( attValue ) ]);
1040 initParameters.push( [ attName, attValue ] );
1041 }
1042 }
1043 return initParameters;
1044}
1045
1046function buildElementSpecialChild( def, code, varName ) {
1047 var type = def[ 0 ];
1048 if ( type !== 'Bind' )
1049 throw "For tag elements, the children can be defined by an array or by `{Bind ...}`!\n" +
1050 "You provided `{" + type + " ...}`.";
1051 code.section.ons.push(
1052 "pm.on('" + def[ 1 ] + "', function(v) { $.clear(" + varName + ", v); });" );
1053}
1054
1055function generateFunctionBody( def, code, indent ) {
1056 code.that = true;
1057 var output = [];
1058 if ( !Array.isArray( def ) ) def = [ def ];
1059 def.forEach( function ( item ) {
1060 if ( typeof item === 'string' ) {
1061 code.that = true;
1062 output.push( indent + item + ".call( that, v );" );
1063 } else if ( isSpecial ) {
1064 var type = item[ 0 ].toLowerCase();
1065 var generator = functionBodyGenerators[ type ];
1066 if ( typeof generator !== 'function' ) {
1067 throw "Don't know how to build a function body from " + JSON.stringify( def ) + "!\n" +
1068 "Known commands are: " + Object.keys( functionBodyGenerators ).join( ", " ) + ".";
1069 }
1070 generator( output, item, code, indent );
1071 }
1072 } );
1073
1074 return output;
1075}
1076
1077const functionBodyGenerators = {
1078 toggle( output, def, code, indent ) {
1079 const elemVar = getElemVarFromPath( def[ 1 ] );
1080 output.push( `${indent}${elemVar} = ${elemVar} ? false : true;` );
1081 },
1082 set( output, def, code, indent ) {
1083 const elemVar = getElemVarFromPath( def[ 1 ] );
1084 output.push( `${indent}${elemVar} = ${getValueCode(def[2])};` );
1085 },
1086 behind( output, def, code, indent ) {
1087 const
1088 behindFunctionName = ensureString( def[ 1 ], "In {Behind <name>}, <name> must be a string!" ),
1089 lines = code.generateBehindCall( behindFunctionName, indent, "v" );
1090 output.push.apply( output, lines );
1091 }
1092};
1093
1094function getValueCode( input ) {
1095 if ( isSpecial( input, "bind" ) ) {
1096 return "that" + keySyntax( input[ 1 ] );
1097 }
1098 return JSON.stringify( input );
1099}
1100
1101/**
1102 * A path is a string which represent a variable.
1103 * For instance: "price" or "plateau/animate-direction".
1104 *
1105 * @example
1106 * getElemVarFromPath("price") === "that.price"
1107 * getElemVarFromPath("plateau/animate-direction") ===
1108 * "that.$elements.plateau.animateDirection"
1109 *
1110 * @param {string} path - "price", "plateau/animate-direction", ...
1111 * @param {string} _root - Optional (default: "that").
1112 * @returns {string} Javascript code to access the variable defined by the path.
1113 */
1114function getElemVarFromPath( path, _root ) {
1115 const root = typeof _root !== "undefined" ? _root : "that";
1116 const items = path.split( "/" ).map( function ( item ) {
1117 return camelCase( item.trim() );
1118 } );
1119 const result = items.map( function ( x, i ) {
1120 if ( i === 0 && items.length > 1 ) return `.$elements${keySyntax(x)}`;
1121 return `.${camelCase(x)}`;
1122 } );
1123 return `${root}${result.join("")}`;
1124}
1125
1126/**
1127 * An attribute is marked as _special_ as soon as it has a dot in its name.
1128 * `view.attribs` is special, but `attribs` is not.
1129 * Attributes with a numeric key are marked as _implicit_.
1130 * @return `{ standard: {...}, special: {...}, implicit: [...] }`.
1131 */
1132function extractAttribs( def ) {
1133 var key, val, attribs = { standard: {}, special: {}, implicit: [] };
1134 for ( key in def ) {
1135 val = def[ key ];
1136 if ( RX_INTEGER.test( key ) ) {
1137 attribs.implicit.push( val );
1138 } else if ( RX_STD_ATT.test( key ) ) {
1139 attribs.standard[ key ] = val;
1140 } else {
1141 attribs.special[ key ] = val;
1142 }
1143 }
1144 return attribs;
1145}
1146
1147/**
1148 * Check if an object has attributes or not.
1149 * It is empty if it has no attribute.
1150 */
1151function isEmptyObj( obj ) {
1152 for ( var k in obj ) return false;
1153 return true;
1154}
1155
1156/**
1157 * Check if an object as at least on attribute.
1158 */
1159function hasAttribs( obj ) {
1160 for ( var k in obj ) return true;
1161 return false;
1162}
1163
1164function getSuffix( text, prefix ) {
1165 if ( text.substr( 0, prefix.length ) !== prefix ) return null;
1166 return text.substr( prefix.length );
1167}
1168
1169function getVarName( def, defaultVarName ) {
1170 var id = def[ "view.id" ];
1171 if ( typeof id === 'undefined' ) {
1172 if ( typeof defaultVarName === 'undefined' ) return "e_";
1173 return defaultVarName;
1174 }
1175 return "e_" + camelCase( id );
1176}
1177
1178function addUnique( arr, item ) {
1179 if ( arr.indexOf( item ) === -1 ) {
1180 arr.push( item );
1181 }
1182}
1183
1184function keySyntax( name ) {
1185 if ( RX_IDENTIFIER.test( name ) ) return "." + name;
1186 return "[" + JSON.stringify( name ) + "]";
1187}
1188
1189function isNonEmptyString( text ) {
1190 if ( typeof text !== 'string' ) return false;
1191 return text.trim().length > 0;
1192}
1193
1194function clone( obj ) {
1195 return JSON.parse( JSON.stringify( obj ) );
1196}
1197
1198function arrayToCode( arr, indent ) {
1199 if ( typeof indent !== 'string' ) indent = "";
1200 return indent + arr.join( "\n" + indent );
1201}
1202
1203function arrayToCodeWithNewLine( arr, indent ) {
1204 if ( arr.length === 0 ) return "";
1205 return arrayToCode( arr, indent ) + "\n";
1206}
1207
1208function ensureArrayOfStrings( arr, msg ) {
1209 if ( typeof msg === 'undefined' ) msg = '';
1210 if ( !Array.isArray( arr ) )
1211 throw ( msg + "\n" + "Expected an array of strings!" ).trim();
1212 arr.forEach( function ( item, index ) {
1213 if ( typeof item !== 'string' )
1214 throw ( msg + "\n" + "Item #" + index + " must be a string!" ).trim();
1215 } );
1216
1217 return arr;
1218}
1219
1220function ensureString( str, msg ) {
1221 if ( typeof msg === 'undefined' ) msg = '';
1222 if ( typeof str !== 'string' )
1223 throw ( msg + "\n" + "Expected a string!" ).trim();
1224 return str;
1225}
1226
1227/**
1228 * Special objects of type "verbatim" must not be transformed.
1229 */
1230function verbatim( text ) {
1231 return { 0: "verbatim", 1: text };
1232}
1233
1234function putQuotesIfNeeded( name ) {
1235 if ( RX_IDENTIFIER.test( name ) ) return name;
1236 return '"' + name + '"';
1237}
1238
1239
1240function parseComplexValue( code, value, indent ) {
1241 try {
1242 if ( typeof indent === 'undefined' ) indent = '';
1243
1244 var lines = recursiveParseComplexValue( code, value );
1245 var out = expandLines( lines )
1246 .map( function ( item, idx ) {
1247 if ( idx === 0 ) return item;
1248 return indent + item;
1249 } )
1250 .join( '\n' );
1251 return out;
1252 } catch ( ex ) {
1253 throw ex + "\n...in parseComplexValue: " + limitJson( value );
1254 }
1255}
1256
1257function expandLines( arr, indent ) {
1258 try {
1259 if ( typeof indent === 'undefined' ) indent = '';
1260 var out = [];
1261 if ( Array.isArray( arr ) ) {
1262 arr.forEach( function ( itm ) {
1263 if ( Array.isArray( itm ) ) {
1264 out.push.apply( out, expandLines( itm, indent + ' ' ) );
1265 } else {
1266 out.push( indent + itm );
1267 }
1268 } );
1269 return out;
1270 }
1271 return [ arr ];
1272 } catch ( ex ) {
1273 throw ex + "\n...in expandLines: " + limitJson( arr );
1274 }
1275}
1276
1277function recursiveParseComplexValue( code, value ) {
1278 try {
1279 if ( value === null ) return "null";
1280 if ( value === undefined ) return "undefined";
1281 // Deal with internationalization: _('blabla')
1282 if ( isSpecial( value, "verbatim" ) ) {
1283 return value[ 1 ];
1284 }
1285
1286 if ( [ 'string', 'number', 'boolean', 'symbol' ].indexOf( typeof value ) !== -1 ) return JSON.stringify( value );
1287 var out = [];
1288 if ( Array.isArray( value ) ) return recursiveParseComplexValueArray( code, value );
1289 else if ( isSpecial( value ) ) return recursiveParseComplexValueSpecial( code, value );
1290 else return recursiveParseComplexValueObject( code, value );
1291 } catch ( ex ) {
1292 throw ex + "\n...in recursiveParseComplexValue: " + limitJson( value );
1293 }
1294 // We should never end here.
1295 return JSON.stringify( value );
1296}
1297
1298function recursiveParseComplexValueArray( code, value ) {
1299 if ( value.length === 0 ) return "[]";
1300 if ( value.length === 1 ) {
1301 var val = recursiveParseComplexValue( code, value[ 0 ] );
1302 if ( typeof val === 'string' )
1303 return "[" + val + "]";
1304 return [ "[", val, "]" ];
1305 }
1306 return [
1307 "[",
1308 value.map( function ( itm, idx ) {
1309 return recursiveParseComplexValue( code, itm ) +
1310 ( idx < value.length - 1 ? "," : "" );
1311 } ),
1312 "]"
1313 ];
1314}
1315
1316function recursiveParseComplexValueObject( code, value ) {
1317 try {
1318 var val;
1319 var keys = Object.keys( value );
1320 if ( keys.length === 0 ) return "{}";
1321 if ( keys.length === 1 ) {
1322 val = recursiveParseComplexValue( code, value[ keys[ 0 ] ] );
1323 if ( typeof val === 'string' ) {
1324 return "{" + putQuotesIfNeeded( keys[ 0 ] ) + ": " + val + "}";
1325 return surround( "{" + putQuotesIfNeeded( keys[ 0 ] ) + ":", val, "}" );
1326 }
1327 }
1328 return [
1329 "{",
1330 keys.map( function ( key, idx ) {
1331 var val = recursiveParseComplexValue( code, value[ key ] );
1332 if ( idx < keys.length - 1 ) {
1333 if ( typeof val === 'string' )
1334 return putQuotesIfNeeded( key ) + ": " + val + ",";
1335 return surround( putQuotesIfNeeded( key ) + ": ", val, "," );
1336 } else {
1337 if ( typeof val === 'string' )
1338 return putQuotesIfNeeded( key ) + ": " + val;
1339 return glueBefore( putQuotesIfNeeded( key ) + ": ", val );
1340 }
1341 } ),
1342 "}"
1343 ];
1344 } catch ( ex ) {
1345 throw ex + "\n...in recursiveParseComplexValueObject: " + limitJson( value );
1346 }
1347}
1348
1349function recursiveParseComplexValueSpecial( code, value ) {
1350 if ( isSpecial( value, 'intl' ) ) {
1351 return '_(' + JSON.stringify( value[ "1" ] ) + ')';
1352 }
1353 if ( isSpecial( value, 'bind' ) ) return value;
1354
1355 return buildElement( value, code, "e_" + code.id() );
1356}
1357
1358function surround( head, arr, tail ) {
1359 glueAfter(
1360 glueBefore( head, arr ),
1361 tail
1362 );
1363 return arr;
1364}
1365
1366function glueBefore( item, arr ) {
1367 if ( arr.length === 0 ) arr.push( item );
1368 else if ( typeof arr[ 0 ] === 'string' ) arr[ 0 ] = item + arr[ 0 ];
1369 else glueBefore( arr[ 0 ], item );
1370 return arr;
1371}
1372
1373function glueAfter( arr, item ) {
1374 const last = arr.length - 1;
1375 if ( arr.length === 0 ) arr.push( item );
1376 else if ( typeof arr[ last ] === 'string' ) arr[ last ] = arr[ last ] + item;
1377 else glueAfter( arr[ last ], item );
1378 return arr;
1379}
1380
1381function limitJson( obj, max ) {
1382 return limit( JSON.stringify( obj ), max );
1383}
1384
1385function limit( txt, max ) {
1386 if ( typeof max === 'undefined' ) max = 80;
1387 if ( txt === undefined ) txt = "undefined";
1388 else if ( txt === null ) txt = "null";
1389 if ( txt.length <= max ) return txt;
1390 return txt.substr( 0, max ) + "...";
1391}
1392
1393
1394/**
1395 * When there is no default value in `view.attribs` item, we can simplify the writting:
1396 *
1397 * @example
1398 * onTap: action
1399 * onTap: {action}
1400 *
1401 * stick: [up, down]
1402 * stick: {[up, down] up}
1403 */
1404function expandViewAttribValue( value ) {
1405 try {
1406 if ( isSpecial( value ) || Array.isArray( value[ 0 ] ) ) return value;
1407 if ( Array.isArray( value ) ) return {
1408 "0": value,
1409 "1": value[ 0 ]
1410 };
1411 return { "0": value };
1412 } catch ( ex ) {
1413 throw ex + "\n...in expandViewAttribValue: " + limitJson( value );
1414 }
1415}
1416
1417/**
1418 * Transform `{View DIV class:foobar, ...}` into `{DIV class:foobar}`.
1419 * Because at this point, "View" is no longer needed for parsing.
1420 *
1421 * @param {object} def - `{View DIV class:foobar, ...}`
1422 * @return {object} `{DIV class:foobar, ...}`
1423 */
1424function removeViewPrefix( def ) {
1425 def[ 0 ] = def[ 1 ];
1426 def[ 1 ] = def[ 2 ];
1427 delete def[ 2 ];
1428 return def;
1429}
1430
1431/**
1432 * Javascript for for root variable declaration.
1433 *
1434 * @param {object} def - `{View ...}`
1435 * @param {Template} code - Helper to write the Javascript code.
1436 *
1437 * @return {undefined}
1438 */
1439function declareRootElement( def, code ) {
1440 const rootElementName = buildElement( def, code );
1441
1442 code.section.elements.define.push( "//-----------------------" );
1443 code.section.elements.define.push( "// Declare root element." );
1444 code.section.elements.define.push(
1445 "Object.defineProperty( this, '$', {",
1446 [
1447 `value: ${rootElementName}.$,`,
1448 "writable: false, ",
1449 "enumerable: false, ",
1450 "configurable: false"
1451 ],
1452 "});"
1453 );
1454}
1455
1456/**
1457 * Generate the JS code for the XJS module.
1458 *
1459 * @param {object} code - Helper for Javascript code writing.
1460 * @param {string} moduleName - Javascript module name.
1461 *
1462 * @return {string} Resulting JS code.
1463 */
1464function outputAll( code, moduleName ) {
1465 try {
1466 let out = outputComments( code );
1467 out += " module.exports = function() {\n";
1468 out += outputNeededConstants( code );
1469 out += ` //-------------------
1470 // Class definition.
1471 const ViewClass = function( args ) {
1472 try {
1473 if( typeof args === 'undefined' ) args = {};
1474 this.$elements = {};
1475 ${outputClassBody(code)}
1476 }
1477 catch( ex ) {
1478 console.error('${moduleName}', ex);
1479 throw Error('Instantiation error in XJS of ${JSON.stringify(moduleName)}:\\n' + ex)
1480 }
1481 };\n`;
1482 out += generate( code.section.statics, "Static members.", " " );
1483 out += " return ViewClass;\n";
1484 out += " }();\n";
1485 return out;
1486 } catch ( ex ) {
1487 bubble( ex, "outputAll()" );
1488 return null;
1489 }
1490}
1491
1492/**
1493 * Possible comments.
1494 *
1495 * @param {object} code - See below.
1496 * @param {array} code.section.comments - Lines of comments.
1497 *
1498 * @return {string} Resulting JS code.
1499 */
1500function outputComments( code ) {
1501 try {
1502 let out = '';
1503 if ( code.section.comments.length > 0 ) {
1504 out += ` /**\n * ${code.section.comments.join("\n * ")}\n */\n`;
1505 }
1506 return out;
1507 } catch ( ex ) {
1508 bubble( ex, "outputComments" );
1509 return null;
1510 }
1511}
1512
1513/**
1514 * Declare requires, converters, global variables, list of needed behind functions, ...
1515 *
1516 * @param {object} code - Helper for Javascript code writing.
1517 *
1518 * @return {string} Resulting JS code.
1519 */
1520function outputNeededConstants( code ) {
1521 try {
1522 let out = arrayToCodeWithNewLine( code.generateRequires(), " " );
1523 out += arrayToCodeWithNewLine( code.generateNeededBehindFunctions(), " " );
1524 out += arrayToCodeWithNewLine( code.generateFunctions(), " " );
1525 out += arrayToCodeWithNewLine( code.generateGlobalVariables(), " " );
1526 return out;
1527 } catch ( ex ) {
1528 bubble( ex, "outputNeededConstants" );
1529 return null;
1530 }
1531}
1532
1533/**
1534 * Class body.
1535 *
1536 * @param {object} code - Helper for Javascript code writing.
1537 *
1538 * @return {string} Resulting JS code.
1539 */
1540function outputClassBody( code ) {
1541 try {
1542 let out = '';
1543 if ( code.that ) out += " const that = this;\n";
1544 if ( code.pm ) out += " const pm = PM(this);\n";
1545 out += generate( code.section.attribs.define, "Create attributes", " " );
1546 out += generate( code.section.elements.define, "Create elements", " " );
1547 out += generate( code.section.events, "Events", " " );
1548 out += arrayToCodeWithNewLine( code.generateLinks(), " " );
1549 out += generate( code.section.ons, "On attribute changed", " " );
1550 out += generate( code.section.elements.init, "Initialize elements", " " );
1551 out += generate( code.section.attribs.init, "Initialize attributes", " " );
1552 if ( code.section.init ) {
1553 out += " // Initialization.\n";
1554 out += ` CODE_BEHIND.${code.section.init}.call( this );\n`;
1555 }
1556 out += " $.addClass(this, 'view', 'custom');\n";
1557 return out;
1558 } catch ( ex ) {
1559 bubble( ex, "outputClassBody" );
1560 return null;
1561 }
1562}
1563
1564/**
1565 * Bubble an exception by providing it's origin.
1566 *
1567 * @param {string|Error} ex - Exception.
1568 * @param {string} origin - From where the exception has been thrown.
1569 *
1570 * @return {undefined}
1571 */
1572function bubble( ex, origin ) {
1573 if ( typeof ex === 'string' ) {
1574 throw Error( `${ex}\n...in ${origin}` );
1575 }
1576 throw Error( `${ex.message}\n...in ${origin}` );
1577}
\No newline at end of file