UNPKG

55.3 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 let 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 return buildElement_tagChildren( def, code, varName );
452 }
453 if ( RX_CLS_NAME.test( type ) ) {
454 buildElementCls( def, code, varName );
455 } else {
456 throw Error( "Unknown element name: " + JSON.stringify( type ) + "!\n" +
457 "Names must have one of theses syntaxes: " +
458 JSON.stringify( RX_TAG_NAME.toString() ) +
459 " or " + JSON.stringify( RX_CLS_NAME.toString() ) + ".\n" +
460 JSON.stringify( def ) );
461 }
462 return varName;
463 } catch ( ex ) {
464 throw Error( `${ex}\n...in buildElement - element "${varName}": ${limitJson( def )}` );
465 } finally {
466 // Store the variable for use in code behind.
467 const id = def[ "view.id" ];
468 if ( typeof id === 'string' ) {
469 code.section.elements.define
470 .push( `this.$elements.${camelCase( id )} = ${varName};` );
471 }
472 }
473}
474
475/**
476 * @param {object} def - Something like `{tfw.view.button ...}`.
477 * @param {Template} code - Helping stuff for code generation.
478 * @param {string} varName - Name of the variable holding this element in the end.
479 * @returns {string} varName.
480 */
481function buildElement_tagChildren( def, code, varName ) {
482 const _children = def[ 1 ];
483 if ( typeof _children === 'undefined' ) return varName;
484 if ( isSpecial( _children ) ) {
485 buildElementSpecialChild( _children, code, varName );
486 return varName;
487 }
488 const children = Array.isArray( _children ) ? _children : [ _children ];
489 const toAdd = [];
490 children.forEach( function ( child ) {
491 if ( typeof child === 'string' || typeof child === 'number' ) {
492 toAdd.push( JSON.stringify( `${child}` ) );
493 } else {
494 const childVarName = buildElement( child, code, code.id( "e_" ) );
495 toAdd.push( childVarName );
496 }
497 } );
498 if ( toAdd.length > 0 ) {
499 code.section.elements.define.push( `$.add( ${varName}, ${toAdd.join( ", " )} );` );
500 }
501 return varName;
502}
503
504/**
505 * `{INPUT placeholder:price value:{Bind price}}`
506 */
507function buildElementTag( def, code, varName ) {
508 const attribs = extractAttribs( def );
509
510 try {
511 const arr = Object.keys( attribs.standard );
512 code.requires[ "Tag" ] = "require('tfw.view').Tag";
513 code.section.elements.define.push(
514 "const " + varName + " = new Tag('" + def[ 0 ] + "'" +
515 ( arr.length > 0 ? ', ' + JSON.stringify( arr ) : '' ) +
516 ");" );
517
518 const initAttribs = buildElementTagAttribsStandard( attribs.standard, code, varName );
519 buildInitAttribsForTag( initAttribs, code, varName );
520 buildElementEvents( attribs.special, code, varName );
521 buildElementTagClassSwitcher( attribs.special, code, varName );
522 buildElementTagAttribSwitcher( attribs.special, code, varName );
523 buildElementTagStyle( attribs.special, code, varName );
524 buildElementTagChildren( attribs.special, code, varName );
525 buildElementTagOn( attribs.special, code, varName );
526 } catch ( ex ) {
527 throw ex + "\n...in tag \"" + varName + "\": " + limitJson( def );
528 }
529}
530
531function buildInitAttribsForTag( initAttribs, code, varName ) {
532 try {
533 initAttribs.forEach( function ( initAttrib ) {
534 try {
535 const
536 attName = initAttrib[ 0 ],
537 attValue = initAttrib[ 1 ];
538 if ( isSpecial( attValue, "verbatim" ) ) {
539 code.section.elements.init.push(
540 varName + keySyntax( attName ) + " = " + attValue[ 1 ] + ";" );
541 } else {
542 if ( [ 'string', 'number', 'boolean' ].indexOf( typeof attValue ) === -1 )
543 throw "A tag's attribute value must be of type string or number!";
544 code.section.elements.init.push(
545 varName + keySyntax( attName ) + " = " + parseComplexValue( code, attValue, ' ' ) + ";" );
546 }
547 } catch ( ex ) {
548 throw ex + `...in buildInitAttribsForTag - tag's attribute: "${attName}"=${limitJson(attValue)}`;
549 }
550 } );
551 } catch ( ex ) {
552 throw `${ex}\n...in buildInitAttribsForTag "${varName}":\ninitAttribs = ${limitJson( initAttribs )}`;
553 }
554}
555
556function buildElementCls( def, code, varName ) {
557 const attribs = extractAttribs( def );
558 try {
559 expandImplicitContent( attribs );
560
561 buildElementEvents( attribs.special, code, varName );
562
563 const
564 arr = Object.keys( attribs.standard ),
565 viewName = CamelCase( def[ 0 ] );
566 code.requires[ viewName ] = "require('" + def[ 0 ] + "')";
567 const initAttribs = buildElementTagAttribsStandard( attribs.standard, code, varName );
568 buildInitAttribsForCls( initAttribs, code, varName, viewName );
569 buildElementTagClassSwitcher( attribs.special, code, varName );
570 buildElementTagAttribSwitcher( attribs.special, code, varName );
571 buildElementTagStyle( attribs.special, code, varName );
572 buildElementClsBind( attribs.special, code, varName );
573 buildElementTagOn( attribs.special, code, varName );
574 } catch ( ex ) {
575 throw ex + "\n...in cls \"" + varName + "\": " + limitJson( attribs );
576 }
577}
578
579/**
580 * The following syntaxes are equivalent:
581 * @example
582 * // Explicit content
583 * {tfw.view.expand content: Hello}
584 * // Implicit content
585 * {tfw.view.expand Hello}
586 */
587function expandImplicitContent( attribs ) {
588 try {
589 if ( !attribs ) return;
590 if ( typeof attribs.implicit === 'undefined' ) return;
591 const implicitContent = attribs.implicit[ 1 ];
592 if ( typeof implicitContent === 'undefined' ) return;
593
594 const explicitContent = attribs.standard ? attribs.standard.content : undefined;
595 if ( explicitContent ) {
596 throw "Can't mix implicit and explicit `content`:\n" +
597 " implicit: " + limitJson( implicitContent ) + "\n" +
598 " explicit: " + limitJson( explicitContent );
599 }
600
601 attribs.standard.content = implicitContent;
602 } catch ( ex ) {
603 throw ex + "\n...in expandImplicitContent: " + limitJson( attribs );
604 }
605}
606
607function buildInitAttribsForCls( initAttribs, code, varName, viewName ) {
608 try {
609 var item, attName, attValue, parsedValue;
610
611 if ( initAttribs.length === 0 ) {
612 code.section.elements.define.push(
613 "const " + varName + " = new " + viewName + "();" );
614 } else if ( initAttribs.length === 1 ) {
615 item = initAttribs[ 0 ];
616 attName = camelCase( item[ 0 ] );
617 attValue = item[ 1 ];
618 parsedValue = parseComplexValue( code, attValue );
619 code.section.elements.define.push(
620 "const " + varName + " = new " + viewName + "({ " + putQuotesIfNeeded( attName ) + ": " +
621 parsedValue + " });" );
622 } else {
623 const out = [ "const " + varName + " = new " + viewName + "({" ];
624 initAttribs.forEach( function ( item, index ) {
625 try {
626 attName = camelCase( item[ 0 ] );
627 attValue = item[ 1 ];
628 parsedValue = parseComplexValue( code, attValue, ' ' );
629 out.push(
630 " " + putQuotesIfNeeded( attName ) + ": " +
631 parsedValue +
632 ( index < initAttribs.length - 1 ? "," : "" ) );
633 } catch ( ex ) {
634 throw "Error while parsing attribute " + JSON.stringify( attName ) +
635 " with value " + JSON.stringify( attValue ) + ":\n" + ex;
636 }
637 } );
638 out.push( "});" );
639 code.section.elements.define.push.apply( code.section.elements.define, out );
640 }
641 } catch ( ex ) {
642 throw ex + "\n...in buildInitAttribsForCls\n" +
643 " varName: " + varName + "\n" +
644 " viewName: " + viewName + "\n" +
645 " initAttribs: " + limitJson( initAttribs ) + "\n" + ex;
646 };
647}
648
649/**
650 * Generate code from attributes starting with "class.".
651 * For instance `class.blue: {Bind focused}` means that the class
652 * `blue` must be added if the attribute `focused` is `true` and
653 * removed if `focused` is `false`.
654 * On the contrary, `class.|red: {Bind focused}` means that the
655 * class `red` must be removed if `focused` is `true` and added if
656 * `focused` is `false`.
657 * Finally, `class.blue|red: {Bind focused}` is the mix of both
658 * previous syntaxes.
659 */
660function buildElementTagClassSwitcher( def, code, varName ) {
661 var attName, attValue, classes, classesNames, className;
662 for ( attName in def ) {
663 classesNames = getSuffix( attName, "class." );
664 if ( !classesNames ) continue;
665 attValue = def[ attName ];
666 if ( classesNames === '*' ) {
667 buildElementTagClassSwitcherStar( attValue, code, varName );
668 continue;
669 }
670 if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
671 throw "Only bindings are accepted as values for class-switchers!\n" +
672 attName + ": " + JSON.stringify( attValue );
673 }
674 classes = classesNames.split( "|" );
675 const actions = [];
676 if ( isNonEmptyString( classes[ 0 ] ) ) {
677 code.requires.$ = "require('dom')";
678 code.functions[ "addClassIfTrue" ] = "(element, className, value) {\n" +
679 " if( value ) $.addClass(element, className);\n" +
680 " else $.removeClass(element, className); };";
681 className = JSON.stringify( classes[ 0 ] );
682 actions.push( "addClassIfTrue( " + varName + ", " + className + ", v );" );
683 }
684 if ( isNonEmptyString( classes[ 1 ] ) ) {
685 code.requires.$ = "require('dom')";
686 code.functions[ "addClassIfFalse" ] = "(element, className, value) {\n" +
687 " if( value ) $.removeClass(element, className);\n" +
688 " else $.addClass(element, className); };";
689 className = JSON.stringify( classes[ 1 ] );
690 actions.push( "addClassIfFalse( " + varName + ", " + className + ", v );" );
691 }
692 code.addLinkFromBind( attValue, actions );
693 }
694}
695
696/**
697 * @example
698 * view.children: {Bind names map:makeListItem}
699 * view.children: {List names map:makeListItem}
700 */
701function buildElementTagChildren( def, code, varName ) {
702 const attValue = def[ "view.children" ];
703 if ( typeof attValue === 'undefined' ) return;
704
705 if ( typeof attValue === 'string' ) {
706 // Transformer la première ligne en la seconde:
707 // 1) view.children: names
708 // 2) view.children: {Bind names}
709 attValue = { "0": 'Bind', "1": attValue };
710 }
711 if ( isSpecial( attValue, "bind" ) ) {
712 code.requires.View = "require('tfw.view');";
713 attValue.action = [
714 "// Updating children of " + varName + ".",
715 "$.clear(" + varName + ");",
716 "if( !Array.isArray( v ) ) v = [v];",
717 "v.forEach(function (elem) {",
718 " $.add(" + varName + ", elem);",
719 "});"
720 ];
721 code.addLinkFromBind( attValue, attValue );
722 } else if ( isSpecial( attValue, "list" ) ) {
723 code.requires.View = "require('tfw.view');";
724 const codeBehindFuncName = attValue.map;
725 code.requires.ListHandler = "require('tfw.binding.list-handler');";
726 if ( typeof codeBehindFuncName === 'string' ) {
727 code.addNeededBehindFunction( codeBehindFuncName );
728 code.section.elements.init.push(
729 "ListHandler(this, " + varName + ", " + JSON.stringify( attValue[ 1 ] ) + ", {",
730 " map: CODE_BEHIND" + keySyntax( codeBehindFuncName ) + ".bind(this)",
731 "});"
732 );
733 } else {
734 // List handler without mapping.
735 code.section.elements.init.push(
736 "ListHandler(this, " + varName + ", " + JSON.stringify( attValue[ 1 ] ) + ");"
737 );
738 }
739 } else {
740 throw "Error in `view.children`: invalid syntax!\n" + JSON.stringify( attValue );
741 }
742}
743
744/**
745 * @example
746 * style.width: {Bind size}
747 * style.width: "24px"
748 */
749function buildElementTagStyle( def, code, varName ) {
750 var attName, attValue, styleName;
751
752 for ( attName in def ) {
753 styleName = getSuffix( attName, "style." );
754 if ( !styleName ) continue;
755 attValue = def[ attName ];
756 if ( typeof attValue === 'string' ) {
757 code.section.elements.init.push(
758 varName + ".$.style" + keySyntax( styleName ) + " = " + JSON.stringify( attValue ) );
759 continue;
760 }
761 if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
762 throw "Only bindings and strings are accepted as values for styles!\n" +
763 attName + ": " + JSON.stringify( attValue );
764 }
765
766 const attributeToBindOn = attValue[ 1 ];
767 if ( typeof attributeToBindOn !== 'string' )
768 throw "Binding syntax error for styles: second argument must be the name of a function behind!\n" +
769 attName + ": " + JSON.stringify( attValue );
770
771 const converter = attValue.converter;
772 if ( typeof converter === 'string' ) {
773 code.addCast( converter );
774 code.section.ons.push(
775 "pm.on(" + JSON.stringify( camelCase( attValue[ 1 ] ) ) + ", function(v) {",
776 " " + varName + ".$.style[" + JSON.stringify( styleName ) + "] = " +
777 "conv_" + converter + "( v );",
778 "});" );
779 } else {
780 code.section.ons.push(
781 "pm.on(" + JSON.stringify( camelCase( attValue[ 1 ] ) ) + ", function(v) {",
782 " " + varName + ".$.style[" + JSON.stringify( styleName ) + "] = v;",
783 "});" );
784 }
785 }
786}
787
788/**
789 * @example
790 * {tfw.view.button on.action: OnAction}
791 * {tfw.view.button on.action: {Toggle show-menu}}
792 */
793function buildElementTagOn( def, code, varName ) {
794 for ( const attName of Object.keys( def ) ) {
795 const targetAttributeName = getSuffix( attName, "on." );
796 if ( !targetAttributeName ) continue;
797
798 const attValue = typeof def[ attName ] === 'string' ? { 0: "Behind", 1: def[ attName ] } : def[ attName ];
799 code.section.ons.push(
800 `PM(${varName}).on(${JSON.stringify(targetAttributeName)},`,
801 buildFunction( attValue, code, varName ),
802 ");"
803 );
804 }
805}
806
807/**
808 * Create the code for a function.
809 *
810 * @example
811 * {Behind onVarChanged}
812 * {Toggle show-menu}
813 *
814 * @param {object} def - Function definition.
815 * @param {object} code - Needed for `code.section.ons`.
816 * @param {string} varName - Name of the object owning the attribute we want to listen on.
817 *
818 * @return {array} Resulting code as an array.
819 */
820function buildFunction( def, code, varName ) {
821 if ( isSpecial( def, "behind" ) ) {
822 const behindFunctionName = def[ 1 ];
823 code.addNeededBehindFunction( behindFunctionName );
824 code.that = true;
825 return [
826 "value => {",
827 " try {",
828 ` CODE_BEHIND${keySyntax(behindFunctionName)}.call(that, value, ${varName});`,
829 " }",
830 " catch( ex ) {",
831 " console.error(`Exception in code behind \"" +
832 behindFunctionName + "\" of module \"" +
833 code.moduleName + "\": ${ex}`);",
834 " }",
835 "}"
836 ];
837 }
838
839 if ( isSpecial( def, "toggle" ) ) {
840 const nameOfTheBooleanToToggle = def[ 1 ];
841 const namesOfTheBooleanToToggle = Array.isArray( nameOfTheBooleanToToggle ) ?
842 nameOfTheBooleanToToggle : [ nameOfTheBooleanToToggle ];
843 const body = namesOfTheBooleanToToggle
844 .map( name => `${getElemVarFromPath(name)} = !${getElemVarFromPath(name)};` );
845 return [
846 "() => {",
847 body,
848 "}"
849 ];
850 }
851
852 throw Error( `Function definition expected, but found ${JSON.stringify(def)}!` );
853}
854
855/**
856 * `{tfw.view.button bind.enabled: enabled}`
857 * is equivalent to:
858 * `{tfw.view.button enabled: {Bind enabled}}`
859 *
860 * `{tfw.view.button bind.enabled: {list converter:isNotEmpty}`
861 * is equivalent to:
862 * `{tfw.view.button enabled: {Bind list converter:isNotEmpty}}`
863 */
864function buildElementClsBind( def, code, varName ) {
865 var attName, attValue, targetAttributeName;
866 var result;
867 var attribs = {};
868
869 for ( attName in def ) {
870 try {
871 targetAttributeName = getSuffix( attName, "bind." );
872 if ( !targetAttributeName ) continue;
873
874 attValue = def[ attName ];
875
876 if ( isSpecial( attValue ) ) {
877 result = { "0": "Bind", "1": def[ attName ][ "0" ] };
878 Object.keys( attValue ).forEach( function ( key ) {
879 if ( key == 0 ) return;
880 result[ key ] = attValue[ key ];
881 } );
882 attribs[ targetAttributeName ] = result;
883 } else {
884 attribs[ targetAttributeName ] = { "0": "Bind", "1": attValue };
885 }
886 } catch ( ex ) {
887 throw ex + "\n...in tag's bind attribute " + varName + "/" + JSON.stringify( attName );
888 }
889 }
890
891 return buildElementTagAttribsStandard( attribs, code, varName );
892}
893
894
895/**
896 * Generate code from attributes starting with "attrib.". For
897 * instance `attrib.|disabled: {Bind enabled}` means that the attrib
898 * `disabled` must be added if the attribute `enabled` is `true` and
899 * removed if `enabled` is `false`.
900 * The syntax is exctly the same as for class switchers.
901 */
902function buildElementTagAttribSwitcher( def, code, varName ) {
903 var attName, attValue, attribs, attribsNames, attribName;
904 for ( attName in def ) {
905 attribsNames = getSuffix( attName, "attrib." );
906 if ( !attribsNames ) continue;
907 attValue = def[ attName ];
908 if ( !attValue || attValue[ 0 ] !== 'Bind' ) {
909 throw "Only bindings are accepted as values for attrib-switchers!\n" +
910 attName + ": " + JSON.stringify( attValue );
911 }
912 attribs = attribsNames.split( "|" );
913 var actions = [];
914 if ( isNonEmptyString( attribs[ 0 ] ) ) {
915 code.requires.$ = "require('dom')";
916 code.functions[ "addAttribIfTrue" ] = "(element, attribName, value) {\n" +
917 " if( value ) $.att(element, attribName);\n" +
918 " else $.removeAtt(element, attribName); };";
919 attribName = JSON.stringify( attribs[ 0 ] );
920 actions.push( "addAttribIfTrue( " + varName + ", " + attribName + ", v );" );
921 }
922 if ( isNonEmptyString( attribs[ 1 ] ) ) {
923 code.requires.$ = "require('dom')";
924 code.functions[ "addAttribIfFalse" ] = "(element, attribName, value) {\n" +
925 " if( value ) $.removeAtt(element, attribName);\n" +
926 " else $.att(element, attribName); };";
927 attribName = JSON.stringify( attribs[ 1 ] );
928 actions.push( "addAttribIfFalse( " + varName + ", " + attribName + ", v );" );
929 }
930 code.addLinkFromBind( attValue, actions );
931 }
932}
933
934/**
935 * @example
936 * class.*: {[flat,type,pressed] getClasses}
937 * class.*: [ {[flat,pressed] getClasses1}, {[value,pressed] getClasses2} ]
938 */
939function buildElementTagClassSwitcherStar( items, code, varName ) {
940 if ( !Array.isArray( items ) ) items = [ items ];
941 items.forEach( function ( item, index ) {
942 code.that = true;
943 if ( typeof item[ 0 ] === 'string' ) item[ 0 ] = [ item[ 0 ] ];
944 var pathes = ensureArrayOfStrings( item[ 0 ], "class.*: " + JSON.stringify( items ) );
945 var behindFunctionName = ensureString( item[ 1 ], "The behind function must be a string!" );
946 pathes.forEach( function ( path ) {
947 code.addLink( { path: path }, {
948 action: [
949 varName + ".applyClass(",
950 " CODE_BEHIND" + keySyntax( behindFunctionName ) +
951 ".call(that,v," + JSON.stringify( path ) + "), " + index + ");"
952 ]
953 } );
954 } );
955 } );
956}
957
958function buildElementEvents( attribs, code, varName ) {
959 var attName, attValue;
960 var eventHandlers = [];
961
962 for ( attName in attribs ) {
963 var eventName = getSuffix( attName, "event." );
964 if ( !eventName ) continue;
965
966 attValue = attribs[ attName ];
967 if ( typeof attValue === 'string' ) {
968 // Using a function from code behind.
969 code.addNeededBehindFunction( attValue );
970 eventHandlers.push(
971 JSON.stringify( eventName ) + ": CODE_BEHIND" + keySyntax( attValue ) + ".bind( this ),"
972 );
973 } else {
974 eventHandlers.push( JSON.stringify( eventName ) + ": function(v) {" );
975 eventHandlers = eventHandlers.concat( generateFunctionBody( attValue, code, " " ) );
976 eventHandlers.push( "}," );
977 }
978 }
979
980 if ( eventHandlers.length > 0 ) {
981 code.requires.View = "require('tfw.view');";
982 code.section.events.push( "View.events(" + varName + ", {" );
983 // Retirer la virgule de la dernière ligne.
984 var lastLine = eventHandlers.pop();
985 eventHandlers.push( lastLine.substr( 0, lastLine.length - 1 ) );
986 // Indenter.
987 code.section.events = code.section.events.concat( eventHandlers.map( x => " " + x ) );
988 code.section.events.push( "});" );
989 }
990}
991
992/**
993 * @returns {array}
994 * If there are constant attribs, they are returned.
995 * For instance, for `{tfw.view.button icon:gear wide:true flat:false content:{Bind label}}`
996 * the result will be : [
997 * ["icon", "gear"],
998 * ["wide", true],
999 * ["flat", false]
1000 * ]
1001 * @example
1002 * {DIV class: {Bind value}}
1003 * {DIV class: hide}
1004 * {DIV textContent: {Intl title}}
1005 */
1006function buildElementTagAttribsStandard( attribs, code, varName ) {
1007 try {
1008 const initParameters = [];
1009 for ( const attName of Object.keys( attribs ) ) {
1010 const attValue = attribs[ attName ];
1011 if ( isSpecial( attValue, "bind" ) ) {
1012 code.addLinkFromBind( attValue, `${varName}/${attName}` );
1013 } else if ( isSpecial( attValue, "intl" ) ) {
1014 initParameters.push( [ attName, verbatim( "_(" + JSON.stringify( attValue[ 1 ] ) + ")" ) ] );
1015 } else if ( isSpecial( attValue, "behind" ) ) {
1016 const
1017 functionBehindName = attValue[ 1 ],
1018 call = `CODE_BEHIND${keySyntax(functionBehindName)}.bind( that )`;
1019 code.addNeededBehindFunction( functionBehindName );
1020 initParameters.push( [ attName, verbatim( call ) ] );
1021 } else {
1022 initParameters.push( [ attName, attValue ] );
1023 }
1024 }
1025 return initParameters;
1026 } catch ( ex ) {
1027 throw Error( `${ex}
1028...in buildElementTagAttribsStandard:
1029 attribs = ${JSON.stringify(attribs)}` );
1030 }
1031}
1032
1033function buildElementSpecialChild( def, code, varName ) {
1034 const type = def[ 0 ];
1035 if ( type !== 'Bind' )
1036 throw "For tag elements, the children can be defined by an array or by `{Bind ...}`!\n" +
1037 "You provided `{" + type + " ...}`.";
1038 code.section.ons.push(
1039 "pm.on('" + def[ 1 ] + "', function(v) { $.clear(" + varName + ", v); });" );
1040}
1041
1042function generateFunctionBody( def, code, indent ) {
1043 code.that = true;
1044 var output = [];
1045 if ( !Array.isArray( def ) ) def = [ def ];
1046 def.forEach( function ( item ) {
1047 if ( typeof item === 'string' ) {
1048 code.that = true;
1049 output.push( indent + item + ".call( that, v );" );
1050 } else if ( isSpecial ) {
1051 var type = item[ 0 ].toLowerCase();
1052 var generator = functionBodyGenerators[ type ];
1053 if ( typeof generator !== 'function' ) {
1054 throw "Don't know how to build a function body from " + JSON.stringify( def ) + "!\n" +
1055 "Known commands are: " + Object.keys( functionBodyGenerators ).join( ", " ) + ".";
1056 }
1057 generator( output, item, code, indent );
1058 }
1059 } );
1060
1061 return output;
1062}
1063
1064const functionBodyGenerators = {
1065 toggle( output, def, code, indent ) {
1066 const elemVar = getElemVarFromPath( def[ 1 ] );
1067 output.push( `${indent}${elemVar} = ${elemVar} ? false : true;` );
1068 },
1069 set( output, def, code, indent ) {
1070 const elemVar = getElemVarFromPath( def[ 1 ] );
1071 output.push( `${indent}${elemVar} = ${getValueCode(def[2])};` );
1072 },
1073 behind( output, def, code, indent ) {
1074 const
1075 behindFunctionName = ensureString( def[ 1 ], "In {Behind <name>}, <name> must be a string!" ),
1076 lines = code.generateBehindCall( behindFunctionName, indent, "v" );
1077 output.push.apply( output, lines );
1078 }
1079};
1080
1081function getValueCode( input ) {
1082 if ( isSpecial( input, "bind" ) ) {
1083 return "that" + keySyntax( input[ 1 ] );
1084 }
1085 return JSON.stringify( input );
1086}
1087
1088/**
1089 * A path is a string which represent a variable.
1090 * For instance: "price" or "plateau/animate-direction".
1091 *
1092 * @example
1093 * getElemVarFromPath("price") === "that.price"
1094 * getElemVarFromPath("plateau/animate-direction") ===
1095 * "that.$elements.plateau.animateDirection"
1096 *
1097 * @param {string} path - "price", "plateau/animate-direction", ...
1098 * @param {string} _root - Optional (default: "that").
1099 * @returns {string} Javascript code to access the variable defined by the path.
1100 */
1101function getElemVarFromPath( path, _root ) {
1102 const root = typeof _root !== "undefined" ? _root : "that";
1103 const items = path.split( "/" ).map( function ( item ) {
1104 return camelCase( item.trim() );
1105 } );
1106 const result = items.map( function ( x, i ) {
1107 if ( i === 0 && items.length > 1 ) return `.$elements${keySyntax(x)}`;
1108 return `.${camelCase(x)}`;
1109 } );
1110 return `${root}${result.join("")}`;
1111}
1112
1113/**
1114 * An attribute is marked as _special_ as soon as it has a dot in its name.
1115 * `view.attribs` is special, but `attribs` is not.
1116 * Attributes with a numeric key are marked as _implicit_.
1117 * @return `{ standard: {...}, special: {...}, implicit: [...] }`.
1118 */
1119function extractAttribs( def ) {
1120 var key, val, attribs = { standard: {}, special: {}, implicit: [] };
1121 for ( key in def ) {
1122 val = def[ key ];
1123 if ( RX_INTEGER.test( key ) ) {
1124 attribs.implicit.push( val );
1125 } else if ( RX_STD_ATT.test( key ) ) {
1126 attribs.standard[ key ] = val;
1127 } else {
1128 attribs.special[ key ] = val;
1129 }
1130 }
1131 return attribs;
1132}
1133
1134/**
1135 * Check if an object has attributes or not.
1136 * It is empty if it has no attribute.
1137 */
1138function isEmptyObj( obj ) {
1139 for ( var k in obj ) return false;
1140 return true;
1141}
1142
1143/**
1144 * Check if an object as at least on attribute.
1145 */
1146function hasAttribs( obj ) {
1147 for ( var k in obj ) return true;
1148 return false;
1149}
1150
1151function getSuffix( text, prefix ) {
1152 if ( text.substr( 0, prefix.length ) !== prefix ) return null;
1153 return text.substr( prefix.length );
1154}
1155
1156function getVarName( def, defaultVarName ) {
1157 var id = def[ "view.id" ];
1158 if ( typeof id === 'undefined' ) {
1159 if ( typeof defaultVarName === 'undefined' ) return "e_";
1160 return defaultVarName;
1161 }
1162 return "e_" + camelCase( id );
1163}
1164
1165function addUnique( arr, item ) {
1166 if ( arr.indexOf( item ) === -1 ) {
1167 arr.push( item );
1168 }
1169}
1170
1171function keySyntax( name ) {
1172 if ( RX_IDENTIFIER.test( name ) ) return "." + name;
1173 return "[" + JSON.stringify( name ) + "]";
1174}
1175
1176function isNonEmptyString( text ) {
1177 if ( typeof text !== 'string' ) return false;
1178 return text.trim().length > 0;
1179}
1180
1181function clone( obj ) {
1182 return JSON.parse( JSON.stringify( obj ) );
1183}
1184
1185function arrayToCode( arr, indent ) {
1186 if ( typeof indent !== 'string' ) indent = "";
1187 return indent + arr.join( "\n" + indent );
1188}
1189
1190function arrayToCodeWithNewLine( arr, indent ) {
1191 if ( arr.length === 0 ) return "";
1192 return arrayToCode( arr, indent ) + "\n";
1193}
1194
1195function ensureArrayOfStrings( arr, msg ) {
1196 if ( typeof msg === 'undefined' ) msg = '';
1197 if ( !Array.isArray( arr ) )
1198 throw ( msg + "\n" + "Expected an array of strings!" ).trim();
1199 arr.forEach( function ( item, index ) {
1200 if ( typeof item !== 'string' )
1201 throw ( msg + "\n" + "Item #" + index + " must be a string!" ).trim();
1202 } );
1203
1204 return arr;
1205}
1206
1207function ensureString( str, msg ) {
1208 if ( typeof msg === 'undefined' ) msg = '';
1209 if ( typeof str !== 'string' )
1210 throw ( msg + "\n" + "Expected a string!" ).trim();
1211 return str;
1212}
1213
1214/**
1215 * Special objects of type "verbatim" must not be transformed.
1216 */
1217function verbatim( text ) {
1218 return { 0: "verbatim", 1: text };
1219}
1220
1221function putQuotesIfNeeded( name ) {
1222 if ( RX_IDENTIFIER.test( name ) ) return name;
1223 return '"' + name + '"';
1224}
1225
1226
1227function parseComplexValue( code, value, indent ) {
1228 try {
1229 if ( typeof indent === 'undefined' ) indent = '';
1230
1231 var lines = recursiveParseComplexValue( code, value );
1232 var out = expandLines( lines )
1233 .map( function ( item, idx ) {
1234 if ( idx === 0 ) return item;
1235 return indent + item;
1236 } )
1237 .join( '\n' );
1238 return out;
1239 } catch ( ex ) {
1240 throw ex + "\n...in parseComplexValue: " + limitJson( value );
1241 }
1242}
1243
1244function expandLines( arr, indent ) {
1245 try {
1246 if ( typeof indent === 'undefined' ) indent = '';
1247 var out = [];
1248 if ( Array.isArray( arr ) ) {
1249 arr.forEach( function ( itm ) {
1250 if ( Array.isArray( itm ) ) {
1251 out.push.apply( out, expandLines( itm, indent + ' ' ) );
1252 } else {
1253 out.push( indent + itm );
1254 }
1255 } );
1256 return out;
1257 }
1258 return [ arr ];
1259 } catch ( ex ) {
1260 throw ex + "\n...in expandLines: " + limitJson( arr );
1261 }
1262}
1263
1264function recursiveParseComplexValue( code, value ) {
1265 try {
1266 if ( value === null ) return "null";
1267 if ( value === undefined ) return "undefined";
1268 // Deal with internationalization: _('blabla')
1269 if ( isSpecial( value, "verbatim" ) ) {
1270 return value[ 1 ];
1271 }
1272
1273 if ( [ 'string', 'number', 'boolean', 'symbol' ].indexOf( typeof value ) !== -1 ) return JSON.stringify( value );
1274 var out = [];
1275 if ( Array.isArray( value ) ) return recursiveParseComplexValueArray( code, value );
1276 else if ( isSpecial( value ) ) return recursiveParseComplexValueSpecial( code, value );
1277 else return recursiveParseComplexValueObject( code, value );
1278 } catch ( ex ) {
1279 throw ex + "\n...in recursiveParseComplexValue: " + limitJson( value );
1280 }
1281 // We should never end here.
1282 return JSON.stringify( value );
1283}
1284
1285function recursiveParseComplexValueArray( code, value ) {
1286 if ( value.length === 0 ) return "[]";
1287 if ( value.length === 1 ) {
1288 var val = recursiveParseComplexValue( code, value[ 0 ] );
1289 if ( typeof val === 'string' )
1290 return "[" + val + "]";
1291 return [ "[", val, "]" ];
1292 }
1293 return [
1294 "[",
1295 value.map( function ( itm, idx ) {
1296 return recursiveParseComplexValue( code, itm ) +
1297 ( idx < value.length - 1 ? "," : "" );
1298 } ),
1299 "]"
1300 ];
1301}
1302
1303function recursiveParseComplexValueObject( code, value ) {
1304 try {
1305 var val;
1306 var keys = Object.keys( value );
1307 if ( keys.length === 0 ) return "{}";
1308 if ( keys.length === 1 ) {
1309 val = recursiveParseComplexValue( code, value[ keys[ 0 ] ] );
1310 if ( typeof val === 'string' ) {
1311 return "{" + putQuotesIfNeeded( keys[ 0 ] ) + ": " + val + "}";
1312 return surround( "{" + putQuotesIfNeeded( keys[ 0 ] ) + ":", val, "}" );
1313 }
1314 }
1315 return [
1316 "{",
1317 keys.map( function ( key, idx ) {
1318 var val = recursiveParseComplexValue( code, value[ key ] );
1319 if ( idx < keys.length - 1 ) {
1320 if ( typeof val === 'string' )
1321 return putQuotesIfNeeded( key ) + ": " + val + ",";
1322 return surround( putQuotesIfNeeded( key ) + ": ", val, "," );
1323 } else {
1324 if ( typeof val === 'string' )
1325 return putQuotesIfNeeded( key ) + ": " + val;
1326 return glueBefore( putQuotesIfNeeded( key ) + ": ", val );
1327 }
1328 } ),
1329 "}"
1330 ];
1331 } catch ( ex ) {
1332 throw ex + "\n...in recursiveParseComplexValueObject: " + limitJson( value );
1333 }
1334}
1335
1336function recursiveParseComplexValueSpecial( code, value ) {
1337 if ( isSpecial( value, 'intl' ) ) {
1338 return '_(' + JSON.stringify( value[ "1" ] ) + ')';
1339 }
1340 if ( isSpecial( value, 'bind' ) ) return value;
1341
1342 return buildElement( value, code, "e_" + code.id() );
1343}
1344
1345function surround( head, arr, tail ) {
1346 glueAfter(
1347 glueBefore( head, arr ),
1348 tail
1349 );
1350 return arr;
1351}
1352
1353function glueBefore( item, arr ) {
1354 if ( arr.length === 0 ) arr.push( item );
1355 else if ( typeof arr[ 0 ] === 'string' ) arr[ 0 ] = item + arr[ 0 ];
1356 else glueBefore( arr[ 0 ], item );
1357 return arr;
1358}
1359
1360function glueAfter( arr, item ) {
1361 const last = arr.length - 1;
1362 if ( arr.length === 0 ) arr.push( item );
1363 else if ( typeof arr[ last ] === 'string' ) arr[ last ] = arr[ last ] + item;
1364 else glueAfter( arr[ last ], item );
1365 return arr;
1366}
1367
1368/**
1369 * @param {any} obj - The object to stringify.
1370 * @param {integer} max - The max size of the stringification.
1371 * @returns {string} A stringified JSON with a limited size.
1372 */
1373function limitJson( obj, max = 100 ) {
1374 return limit( JSON.stringify( obj ), max );
1375}
1376
1377/**
1378 * @param {string} txt - Text to truncate if too long.
1379 * @param {integer} max - The max size of the text.
1380 * @returns {string} The text with ellipsis if too long.
1381 */
1382function limit( txt, max = 100 ) {
1383 if ( typeof txt === 'undefined' ) return "undefined";
1384 else if ( txt === null ) return "null";
1385 if ( txt.length <= max ) return txt;
1386 return `${txt.substr( 0, max )}...`;
1387}
1388
1389
1390/**
1391 * When there is no default value in `view.attribs` item, we can simplify the writting:
1392 *
1393 * @example
1394 * onTap: action
1395 * onTap: {action}
1396 *
1397 * stick: [up, down]
1398 * stick: {[up, down] up}
1399 */
1400function expandViewAttribValue( value ) {
1401 try {
1402 if ( isSpecial( value ) || Array.isArray( value[ 0 ] ) ) return value;
1403 if ( Array.isArray( value ) ) return {
1404 "0": value,
1405 "1": value[ 0 ]
1406 };
1407 return { "0": value };
1408 } catch ( ex ) {
1409 throw ex + "\n...in expandViewAttribValue: " + limitJson( value );
1410 }
1411}
1412
1413/**
1414 * Transform `{View DIV class:foobar, ...}` into `{DIV class:foobar}`.
1415 * Because at this point, "View" is no longer needed for parsing.
1416 *
1417 * @param {object} def - `{View DIV class:foobar, ...}`
1418 * @return {object} `{DIV class:foobar, ...}`
1419 */
1420function removeViewPrefix( def ) {
1421 def[ 0 ] = def[ 1 ];
1422 def[ 1 ] = def[ 2 ];
1423 delete def[ 2 ];
1424 return def;
1425}
1426
1427/**
1428 * Javascript for for root variable declaration.
1429 *
1430 * @param {object} def - `{View ...}`
1431 * @param {Template} code - Helper to write the Javascript code.
1432 *
1433 * @return {undefined}
1434 */
1435function declareRootElement( def, code ) {
1436 const rootElementName = buildElement( def, code );
1437
1438 code.section.elements.define.push( "//-----------------------" );
1439 code.section.elements.define.push( "// Declare root element." );
1440 code.section.elements.define.push(
1441 "Object.defineProperty( this, '$', {",
1442 [
1443 `value: ${rootElementName}.$,`,
1444 "writable: false, ",
1445 "enumerable: false, ",
1446 "configurable: false"
1447 ],
1448 "});"
1449 );
1450}
1451
1452/**
1453 * Generate the JS code for the XJS module.
1454 *
1455 * @param {object} code - Helper for Javascript code writing.
1456 * @param {string} moduleName - Javascript module name.
1457 *
1458 * @return {string} Resulting JS code.
1459 */
1460function outputAll( code, moduleName ) {
1461 try {
1462 let out = outputComments( code );
1463 out += " module.exports = function() {\n";
1464 out += outputNeededConstants( code );
1465 out += ` //-------------------
1466 // Class definition.
1467 const ViewClass = function( args ) {
1468 try {
1469 if( typeof args === 'undefined' ) args = {};
1470 this.$elements = {};
1471 ${outputClassBody(code)}
1472 }
1473 catch( ex ) {
1474 console.error('${moduleName}', ex);
1475 throw Error('Instantiation error in XJS of ${JSON.stringify(moduleName)}:\\n' + ex)
1476 }
1477 };\n`;
1478 out += generate( code.section.statics, "Static members.", " " );
1479 out += " return ViewClass;\n";
1480 out += " }();\n";
1481 return out;
1482 } catch ( ex ) {
1483 bubble( ex, "outputAll()" );
1484 return null;
1485 }
1486}
1487
1488/**
1489 * Possible comments.
1490 *
1491 * @param {object} code - See below.
1492 * @param {array} code.section.comments - Lines of comments.
1493 *
1494 * @return {string} Resulting JS code.
1495 */
1496function outputComments( code ) {
1497 try {
1498 let out = '';
1499 if ( code.section.comments.length > 0 ) {
1500 out += ` /**\n * ${code.section.comments.join("\n * ")}\n */\n`;
1501 }
1502 return out;
1503 } catch ( ex ) {
1504 bubble( ex, "outputComments" );
1505 return null;
1506 }
1507}
1508
1509/**
1510 * Declare requires, converters, global variables, list of needed behind functions, ...
1511 *
1512 * @param {object} code - Helper for Javascript code writing.
1513 *
1514 * @return {string} Resulting JS code.
1515 */
1516function outputNeededConstants( code ) {
1517 try {
1518 let out = arrayToCodeWithNewLine( code.generateRequires(), " " );
1519 out += arrayToCodeWithNewLine( code.generateNeededBehindFunctions(), " " );
1520 out += arrayToCodeWithNewLine( code.generateFunctions(), " " );
1521 out += arrayToCodeWithNewLine( code.generateGlobalVariables(), " " );
1522 return out;
1523 } catch ( ex ) {
1524 bubble( ex, "outputNeededConstants" );
1525 return null;
1526 }
1527}
1528
1529/**
1530 * Class body.
1531 *
1532 * @param {object} code - Helper for Javascript code writing.
1533 *
1534 * @return {string} Resulting JS code.
1535 */
1536function outputClassBody( code ) {
1537 try {
1538 let out = '';
1539 if ( code.that ) out += " const that = this;\n";
1540 if ( code.pm ) out += " const pm = PM(this);\n";
1541 out += generate( code.section.attribs.define, "Create attributes", " " );
1542 out += generate( code.section.elements.define, "Create elements", " " );
1543 out += generate( code.section.events, "Events", " " );
1544 out += arrayToCodeWithNewLine( code.generateLinks(), " " );
1545 out += generate( code.section.ons, "On attribute changed", " " );
1546 out += generate( code.section.elements.init, "Initialize elements", " " );
1547 out += generate( code.section.attribs.init, "Initialize attributes", " " );
1548 if ( code.section.init ) {
1549 out += " // Initialization.\n";
1550 out += ` CODE_BEHIND.${code.section.init}.call( this );\n`;
1551 }
1552 out += " $.addClass(this, 'view', 'custom');\n";
1553 return out;
1554 } catch ( ex ) {
1555 bubble( ex, "outputClassBody" );
1556 return null;
1557 }
1558}
1559
1560/**
1561 * Bubble an exception by providing it's origin.
1562 *
1563 * @param {string|Error} ex - Exception.
1564 * @param {string} origin - From where the exception has been thrown.
1565 *
1566 * @return {undefined}
1567 */
1568function bubble( ex, origin ) {
1569 if ( typeof ex === 'string' ) {
1570 throw Error( `${ex}\n...in ${origin}` );
1571 }
1572 throw Error( `${ex.message}\n...in ${origin}` );
1573}
\No newline at end of file