UNPKG

19.3 kBJavaScriptView Raw
1"use strict";
2
3const Common = require( "./boilerplate.view.common" );
4
5const
6 camelCase = Common.camelCase,
7 CamelCase = Common.CamelCase,
8 contains = Common.contains;
9
10
11/**
12 * XJS View must be converted in valid Javascript code.
13 * This class helps remembering all is needed to create such valid code.
14 * It also offered lot of util functions to do the job.
15 *
16 * @param {[type]} codeBehind [description]
17 * @param {[type]} moduleName [description]
18 * @constructor
19 */
20class Template {
21 constructor( codeBehind, moduleName ) {
22 this._counter = -1;
23 this.codeBehind = codeBehind;
24 this.moduleName = moduleName;
25 this.debug = false;
26 // List of behind function that need to be defined.
27 this.neededBehindFunctions = [];
28 this.requires = {};
29 this.functions = {};
30 this.elementNames = [];
31 this.vars = {};
32 this.that = false;
33 this.pm = false;
34 this.aliases = {};
35 // Names of action attributes.
36 // Such attributes must not be fired at init time, for example.
37 this.actions = [];
38 this.section = createSectionStructure();
39 }
40
41 isAction( attName ) {
42 const camelCaseAttName = camelCase( attName );
43 return contains( this.actions, attName ) || contains( this.actions, camelCaseAttName );
44 }
45}
46
47module.exports = Template;
48
49
50const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
51
52/**
53 * @member Template.id
54 * @param
55 */
56Template.prototype.id = function ( prefix, counter ) {
57 if ( typeof prefix === 'undefined' ) prefix = "";
58 if ( typeof counter !== 'number' ) {
59 this._counter++;
60 counter = this._counter;
61 }
62 while ( counter >= DIGITS.length ) {
63 var modulo = counter % DIGITS.length;
64 prefix += DIGITS.charAt( modulo );
65 counter = Math.floor( counter / DIGITS.length );
66 }
67 prefix += DIGITS.charAt( counter );
68 return prefix;
69};
70
71
72Template.prototype.generateNeededBehindFunctions = function () {
73 if ( this.neededBehindFunctions.length === 0 ) return [];
74
75 var names = this.neededBehindFunctions.map( name => '"' + name + '"' );
76 return generateSection(
77 "Check if needed functions are defined in code behind.",
78 [
79 "View.ensureCodeBehind( CODE_BEHIND, " + names.join( ", " ) + " );"
80 ]
81 );
82};
83
84Template.prototype.generateBehindCall = function ( behindFunctionName, indent, args ) {
85 if ( typeof indent === 'undefined' ) indent = "";
86 if ( typeof args === 'undefined' ) args = "";
87 this.addNeededBehindFunction( behindFunctionName );
88 this.that = true;
89 if ( args.trim() != '' ) args = ", " + args;
90
91 return [
92 indent + "try {",
93 indent + " CODE_BEHIND." + behindFunctionName + ".call(that" + args + ");",
94 indent + "}",
95 indent + "catch( ex ) {",
96 indent + " console.error('Exception thrown in code behind `" +
97 behindFunctionName + "`: ', ex);",
98 indent + "}",
99 ];
100};
101
102Template.prototype.generateRequires = function () {
103 if ( isEmpty( this.requires ) ) return [];
104
105 var that = this;
106 var keys = Object.keys( this.requires );
107 keys.sort( function ( a, b ) {
108 var deltaLen = a.length - b.length;
109 if ( deltaLen != 0 ) return deltaLen;
110 if ( a < b ) return -1;
111 if ( a > b ) return 1;
112 return 0;
113 } );
114 return generateSection(
115 "Dependent modules.",
116 keys.map(
117 k => "var " + k + " = " + that.requires[ k ] + ";" ) );
118};
119
120Template.prototype.generateFunctions = function () {
121 if ( isEmpty( this.functions ) ) return [];
122
123 var that = this;
124 return generateSection(
125 "Global functions.",
126 Object.keys( this.functions ).map(
127 k => "function " + k + that.functions[ k ] + ";" ) );
128};
129
130Template.prototype.generateGlobalVariables = function () {
131 if ( isEmpty( this.vars ) ) return [];
132
133 var that = this;
134 return generateSection(
135 "Global variables.",
136 Object.keys( this.vars ).map(
137 k => "var " + k + " = " + that.vars[ k ] + ";" ) );
138};
139
140Template.prototype.generateLinks = function () {
141 var that = this;
142
143 try {
144 var output = [];
145 var links = this.section.links;
146 if ( links.length === 0 ) return output;
147
148 links.forEach( function ( link, index ) {
149 try {
150 output.push( "new Link({" );
151 if ( that.debug ) {
152 output.push( " dbg: '" + that.moduleName + "#" + index + "'," );
153 }
154 output.push(
155 " A:" + pod2code.call( that, link.A ) + ",",
156 " B:" + pod2code.call( that, link.B ) + ",",
157 " name:" + JSON.stringify( link.A.path + " > " + link.B.path ),
158 "});" );
159 } catch ( ex ) {
160 throw ex + "\n" + "link = " + JSON.stringify( link );
161 }
162 } );
163
164 return generateSection( "Links", output );
165 } catch ( ex ) {
166 throw ex + "\n" + JSON.stringify( this.section.links, null, " " ) + "\n" + "generateLinks()";
167 }
168};
169
170/**
171 *
172 */
173function pod2code( pod ) {
174 try {
175 var that = this;
176 var items = [];
177 Object.keys( pod ).forEach( function ( key ) {
178 var val = pod[ key ];
179 if ( key === 'path' ) pod2CodePath.call( that, items, val );
180 else if ( key === 'delay' ) pod2CodeDelay.call( that, items, val );
181 else if ( key === 'action' ) pod2CodeAction.call( that, items, val );
182 else if ( key === 'converter' ) pod2CodeConverter.call( that, items, val );
183 else if ( key === 'format' ) pod2CodeFormat.call( that, items, val );
184 else if ( key === 'map' ) pod2CodeMap.call( that, items, val );
185 else if ( key === 'header' ) pod2CodeHeader.call( that, items, val );
186 else if ( key === 'footer' ) pod2CodeFooter.call( that, items, val );
187 else if ( key === 'open' ) pod2CodeOpen.call( that, items, val );
188 } );
189 return "{" + items.join( ",\n " ) + "}";
190 } catch ( ex ) {
191 throw ex + "\n" + "pod2code( " + JSON.stringify( pod ) + " )";
192 }
193}
194
195/**
196 *
197 */
198function pod2CodePath( items, path ) {
199 var pieces = path.split( "/" )
200 .map( x => x.trim() )
201 .filter( x => x.length > 0 );
202 if ( pieces.length === 1 ) {
203 // The simplest path describes an attribute of the main view
204 // object.
205 items.push(
206 "obj: that",
207 "name: '" + camelCase( pieces[ 0 ] ) + "'" );
208 } else {
209 var attName = camelCase( pieces.pop() );
210 var firstPiece = pieces.shift();
211 var objCode = isVarNameAndNotViewId( firstPiece ) ?
212 firstPiece : "e_" + camelCase( firstPiece );
213 //? firstPiece : "that.$elements." + camelCase( firstPiece );
214 objCode += pieces.map( x => keySyntax( x ) ).join( "" );
215 items.push(
216 "obj: " + objCode,
217 "name: '" + attName + "'" );
218 }
219}
220
221/**
222 *
223 */
224function pod2CodeDelay( items, delay ) {
225 items.push( "delay: " + parseInt( delay ) );
226}
227
228function pod2CodeOpen( items, open ) {
229 if ( open === false ) {
230 items.push( "open: false" );
231 }
232}
233
234/**
235 *
236 */
237function pod2CodeAction( items, actions ) {
238 items.push(
239 "action: function(v) {\n" +
240 actions.map( x => " " + x ).join( "\n" ) +
241 "}" );
242}
243
244function pod2CodeConverter( items, converter ) {
245 items.push( "converter: " + converter );
246}
247
248function pod2CodeFormat( items, format ) {
249 items.push( "format: [_, " + JSON.stringify( format ) + "]" );
250}
251
252function pod2CodeMap( items, codeLines ) {
253 items.push(
254 "map: function() {\n" +
255 codeLines.map( x => " " + x ).join( "\n" ) +
256 "}" );
257}
258
259function pod2CodeHeader( items, codeLines ) {
260 items.push(
261 "header: function() {\n" +
262 codeLines.map( x => " " + x ).join( "\n" ) +
263 "}" );
264}
265
266function pod2CodeFooter( items, codeLines ) {
267 items.push(
268 "footer: function() {\n" +
269 codeLines.map( x => " " + x ).join( "\n" ) +
270 "}" );
271}
272
273Template.prototype.addNeededBehindFunction = function ( functionName ) {
274 this.requires.View = "require('tfw.view');";
275 pushUnique( this.neededBehindFunctions, functionName );
276};
277
278Template.prototype.addCast = function ( name, value ) {
279 if ( name.substr( 0, 7 ) === 'behind.' ) {
280 var funcName = name.substr( 7 );
281 this.addNeededBehindFunction( funcName );
282 return "CODE_BEHIND." + funcName + ".bind( this )";
283 } else {
284 if ( typeof value === 'undefined' ) value = "Converters.get('" + name + "')";
285 this.requires[ "Converters" ] = "require('tfw.binding.converters')";
286 this.vars[ "conv_" + name ] = value;
287 return "conv_" + name;
288 }
289};
290
291/**
292 * @example
293 * bind: {Bind duration}
294 * to: "value"
295 * return: {A:{path: "duration"}, B:{path: "value"}}
296 *
297 * bind: {Bind names, converter: length}
298 * to: "count"
299 * return: {A:{path: "names"}, B:{path: "count", converter: length}}
300 *
301 * bind: {Bind duration, delay: 300}
302 * to: "value"
303 * return: {A:{path: "duration"}, B:{path: "value", delay: 300}}
304 *
305 * bind: {Bind duration, delay: 300}
306 * to: ["$.addClass(elem000, 'hide')"]
307 * return: {A:{path: "duration"}, B:{action: ["$.addClass(elem000, 'hide')"], delay: 300}}
308 *
309 * bind: {Bind duration, delay: 300}
310 * to: {Behind onDurationChange}
311 * return: {A:{path: "duration"}, B:{action: {Behind onDurationChange}, delay: 300}}
312 *
313 * bind: {Bind value, -delay: 350 }
314 * to: "e_input/value"
315 *
316 */
317Template.prototype.addLinkFromBind = function ( bind, to ) {
318 try {
319 var A = { path: bind[ 1 ] || bind.path };
320 var B = processLinkFromBindArgumentTo.call( this, to );
321
322 processBindArguments.call( this, A, B, bind );
323 return this.addLink( A, B );
324 } catch ( ex ) {
325 throw Error(
326 ex + "\n" + "addLinkFromBind(" +
327 JSON.stringify( bind ) + ", " +
328 JSON.stringify( to ) + ")" );
329 }
330};
331
332function processBindArguments( A, B, bind ) {
333 processBindArgsForSource.call( this, A, bind );
334 processBindArgsForDestination.call( this, B, bind );
335}
336
337function processBindArgsForSource( src, bind ) {
338 try {
339 if ( bind.back === false ) src.open = false;
340 // Atributes starting with a `-`.
341 var backAttributes = {};
342 var name, value;
343 for ( name in bind ) {
344 if ( name.charAt( 0 ) === '-' ) {
345 backAttributes[ name.substr( 1 ) ] = bind[ name ];
346 }
347 }
348 processBindArgsForDestination( src, backAttributes );
349 } catch ( ex ) {
350 throw ex + "\n...in processBindArgsForSource: " + limitJson( bind );
351 }
352}
353
354function processBindArgsForDestination( dst, bind ) {
355 try {
356 var name, value;
357 for ( name in bind ) {
358 value = bind[ name ];
359 switch ( name ) {
360 case 'delay':
361 if ( typeof value !== 'number' ) {
362 throw "In a {Bind... delay:...} declaration, `delay` must be a number!\n" +
363 " delay: " + limitJson( value );
364 }
365 dst.delay = value;
366 break;
367 case 'const':
368 dst.converter = parseConverter.call( this, { "0": 'Const', "1": value } );
369 break;
370 case 'converter':
371 dst.converter = parseConverter.call( this, value );
372 break;
373 case 'format':
374 dst.format = parseFormat.call( this, value );
375 break;
376 }
377 }
378 } catch ( ex ) {
379 throw ex + "\n...in processBindArgsForDestination: " + limitJson( bind );
380 }
381}
382
383function parseFormat( syntax ) {
384 if ( typeof syntax !== 'string' )
385 throw "In {Bind format:...}, `format` must be a string!";
386 return syntax;
387}
388
389function parseConverter( syntax ) {
390 if ( typeof syntax === 'string' ) return parseConverterString.call( this, syntax );
391 if ( isSpecial( syntax, "behind" ) ) return parseConverterBehind.call( this, syntax[ "1" ] );
392 if ( isSpecial( syntax, 'const' ) ) return parseConverterConst.call( this, syntax[ "1" ] );
393 throw "In a {Bind converter:...}, `converter` must be a string or `{Behind ...}`!\n" +
394 " but we found: " + limitJson( syntax );
395}
396
397function parseConverterString( syntax ) {
398 if ( syntax.substr( 0, 7 ) === 'behind.' ) {
399 var funcName = syntax.substr( 7 );
400 this.addNeededBehindFunction( funcName );
401 return "CODE_BEHIND." + funcName + ".bind( this )";
402 } else {
403 this.vars[ "conv_" + syntax ] = "Converters.get('" + syntax + "')";
404 return "conv_" + syntax;
405 }
406}
407
408function parseConverterConst( value ) {
409 return "function(){return " + JSON.stringify( value ) + "}";
410}
411
412function parseConverterBehind( funcName ) {
413 this.addNeededBehindFunction( funcName );
414 return "CODE_BEHIND." + funcName + ".bind( this )";
415}
416
417/**
418 * "value" -> { path: "value" }
419 * ["$.clear(elem0)"] -> { action: ["$.clear(elem0)"] }
420 * {Behind onValueChanged} -> { action: ["CODE_BEHIND.onValueChanged.call( this, v )"] }
421 */
422function processLinkFromBindArgumentTo( to ) {
423 if ( typeof to === 'string' ) return { path: to };
424 if ( Array.isArray( to ) ) return { action: to };
425 if ( isSpecial( to, "behind" ) ) {
426 return processLinkFromBindArgumentTo_behind.call( this, to );
427 } else if ( isSpecial( to, "bind" ) ) {
428 return processLinkFromBindArgumentTo_bind.call( this, to );
429 }
430 throw "`to` argument can be only a string, an array or {Behind ...}!";
431}
432
433function processLinkFromBindArgumentTo_behind( to ) {
434 var behindFunctionName = to[ 1 ];
435 if ( typeof behindFunctionName !== 'string' )
436 throw "In a {Behind ...} statement, the second argument must be a string!";
437 pushUnique( this.neededBehindFunctions, behindFunctionName );
438 return { action: [ "CODE_BEHIND." + behindFunctionName + ".call(that, v)" ] };
439}
440
441function processLinkFromBindArgumentTo_bind( to ) {
442 var behindFunctionName;
443 var binding = {};
444 if ( Array.isArray( to.action ) ) binding.action = to.action;
445 [ 'map', 'header', 'footer' ].forEach( function ( id ) {
446 if ( typeof to[ id ] === 'string' ) {
447 behindFunctionName = to[ id ];
448 pushUnique( this.neededBehindFunctions, behindFunctionName );
449 binding[ id ] = [ "return CODE_BEHIND." + behindFunctionName + ".apply(that, arguments)" ];
450 }
451 }, this );
452 return binding;
453}
454
455/**
456 * Prepare a link with `path` insteadof `obj`/`name`.
457 */
458Template.prototype.addLink = function ( A, B ) {
459 try {
460 this.requires[ "Link" ] = "require('tfw.binding.link')";
461 this.that = true;
462 checkLinkPod( A );
463 checkLinkPod( B );
464 var link = JSON.parse( JSON.stringify( { A: A, B: B } ) );
465 this.section.links.push( link );
466 return link;
467 } catch ( ex ) {
468 throw Error(
469 ex + "\n" + "addLink(" +
470 JSON.stringify( A ) + ", " +
471 JSON.stringify( B ) + ")" );
472 }
473};
474
475function checkLinkPod( pod ) {
476 try {
477 var pathType = typeof pod.path;
478 if ( pathType !== 'undefined' && pathType !== 'string' )
479 throw "Attribute `path` in a link's pod must be a <string>, not a <" + pathType + ">!\n" +
480 "pod.path = " + JSON.stringify( pod.path );
481
482 var actionType = typeof pod.action;
483 if ( actionType !== 'undefined' && !Array.isArray( pod.action ) )
484 throw "Attribute `action` in a link's pod must be an <array>, not a <" + actionType + ">!\n" +
485 "pod.action = " + JSON.stringify( pod.action );
486
487 if ( !pod.path && !pod.action )
488 throw "A link's pod must have at least an attribute `path` or `action`!";
489 } catch ( ex ) {
490 throw Error(
491 ex + "\n" +
492 "checkLinkPod( " + JSON.stringify( pod ) + " )" );
493 }
494}
495
496function pushUnique( arr, item ) {
497 if ( arr.indexOf( item ) === -1 )
498 arr.push( item );
499}
500
501function generateSection( sectionName, contentArray, indent ) {
502 if ( typeof indent === 'undefined' ) indent = "";
503
504 var firstLine = indent + "//";
505 var count = sectionName.length + 2;
506 while ( count-- > 0 ) firstLine += "-";
507 var lines = [ firstLine, indent + "// " + sectionName ];
508 return lines.concat( contentArray );
509}
510
511function isEmpty( value ) {
512 if ( Array.isArray( value ) ) return value.length === 0;
513 if ( typeof value === 'string' ) return value.trim().length === 0;
514 for ( var k in value ) return false;
515 return true;
516}
517
518function object2code( obj ) {
519 if ( Array.isArray( obj ) ) {
520 return "[" + obj.map( x => object2code( x ) ).join( ", " ) + "]";
521 }
522 switch ( typeof obj ) {
523 case 'object':
524 return "{" +
525 Object.keys( obj )
526 .map( k => quotesIfNeeded( k ) + ": " + object2code( obj[ k ] ) )
527 .join( ", " ) +
528 "}";
529 default:
530 return JSON.stringify( obj );
531 }
532}
533
534/**
535 * If `name` is a valid Javascript identifier, return it
536 * verbatim. Otherwise, return it surrounded by double quotes.
537 */
538var RX_JAVASCRIPT_IDENTIFIER = /^[_$a-z][_$a-z0-9]*$/ig;
539
540function quotesIfNeeded( name ) {
541 return RX_JAVASCRIPT_IDENTIFIER.test( name ) ? name : JSON.stringify( name );
542}
543
544/**
545 * @example
546 * keySyntax( "value" ) === ".value"
547 * keySyntax( "diff-value" ) === '["diff-value"]'
548 */
549function keySyntax( name ) {
550 if ( RX_JAVASCRIPT_IDENTIFIER.test( name ) ) return "." + name;
551 return "[" + JSON.stringify( name ) + "]";
552}
553
554/**
555 * An object is special of and only if it's attribute of key "0" is a
556 * string.
557 */
558function isSpecial( obj, name ) {
559 if ( !obj ) return false;
560 if ( typeof obj[ 0 ] !== 'string' ) return false;
561 if ( typeof name === 'string' ) {
562 return obj[ 0 ].toLowerCase() === name;
563 }
564 return true;
565}
566
567/**
568 * In a binding, you can use the "source/attribute" syntax to bind to
569 * an attribute of a descendant of the root element. If the source has
570 * a "view.id", it is refered like this: `that.$elements.id`.
571 * If not, it is refered by its var name: `e_xxx`.
572 *
573 * ViewId are camelCase and VarName start with "e_".
574 */
575function isVarNameAndNotViewId( name ) {
576 return name.substr( 0, 2 ) === 'e_';
577}
578
579function limitJson( obj, max ) {
580 return limit( JSON.stringify( obj ), max );
581}
582
583function limit( txt, max ) {
584 if ( typeof max === 'undefined' ) max = 80;
585 if ( txt === undefined ) txt = "undefined";
586 else if ( txt === null ) txt = "null";
587 if ( txt.length <= max ) return txt;
588 return txt.substr( 0, max ) + "...";
589}
590
591/**
592 * Returns the initial content of Template.section.
593 *
594 * @returns {object} Initial content of Template.section.
595 */
596function createSectionStructure() {
597 return {
598 init: null,
599 comments: [],
600 attribs: {
601 define: [],
602 init: []
603 },
604 elements: {
605 define: [],
606 init: []
607 },
608 events: [],
609 links: [],
610 ons: [],
611 statics: []
612 };
613}
\No newline at end of file