1 | "use strict";
|
2 |
|
3 | const Common = require( "./boilerplate.view.common" );
|
4 |
|
5 | const
|
6 | camelCase = Common.camelCase,
|
7 | CamelCase = Common.CamelCase,
|
8 | contains = Common.contains;
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | class Template {
|
21 | constructor( codeBehind, moduleName ) {
|
22 | this._counter = -1;
|
23 | this.codeBehind = codeBehind;
|
24 | this.moduleName = moduleName;
|
25 | this.debug = false;
|
26 |
|
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 |
|
36 |
|
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 |
|
47 | module.exports = Template;
|
48 |
|
49 |
|
50 | const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | Template.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 |
|
72 | Template.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 |
|
84 | Template.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 |
|
102 | Template.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 |
|
120 | Template.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 |
|
130 | Template.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 |
|
140 | Template.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 |
|
173 | function 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 |
|
198 | function 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 |
|
204 |
|
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 |
|
214 | objCode += pieces.map( x => keySyntax( x ) ).join( "" );
|
215 | items.push(
|
216 | "obj: " + objCode,
|
217 | "name: '" + attName + "'" );
|
218 | }
|
219 | }
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | function pod2CodeDelay( items, delay ) {
|
225 | items.push( "delay: " + parseInt( delay ) );
|
226 | }
|
227 |
|
228 | function pod2CodeOpen( items, open ) {
|
229 | if ( open === false ) {
|
230 | items.push( "open: false" );
|
231 | }
|
232 | }
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | function pod2CodeAction( items, actions ) {
|
238 | items.push(
|
239 | "action: function(v) {\n" +
|
240 | actions.map( x => " " + x ).join( "\n" ) +
|
241 | "}" );
|
242 | }
|
243 |
|
244 | function pod2CodeConverter( items, converter ) {
|
245 | items.push( "converter: " + converter );
|
246 | }
|
247 |
|
248 | function pod2CodeFormat( items, format ) {
|
249 | items.push( "format: [_, " + JSON.stringify( format ) + "]" );
|
250 | }
|
251 |
|
252 | function pod2CodeMap( items, codeLines ) {
|
253 | items.push(
|
254 | "map: function() {\n" +
|
255 | codeLines.map( x => " " + x ).join( "\n" ) +
|
256 | "}" );
|
257 | }
|
258 |
|
259 | function pod2CodeHeader( items, codeLines ) {
|
260 | items.push(
|
261 | "header: function() {\n" +
|
262 | codeLines.map( x => " " + x ).join( "\n" ) +
|
263 | "}" );
|
264 | }
|
265 |
|
266 | function pod2CodeFooter( items, codeLines ) {
|
267 | items.push(
|
268 | "footer: function() {\n" +
|
269 | codeLines.map( x => " " + x ).join( "\n" ) +
|
270 | "}" );
|
271 | }
|
272 |
|
273 | Template.prototype.addNeededBehindFunction = function ( functionName ) {
|
274 | this.requires.View = "require('tfw.view');";
|
275 | pushUnique( this.neededBehindFunctions, functionName );
|
276 | };
|
277 |
|
278 | Template.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 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 | Template.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 |
|
332 | function processBindArguments( A, B, bind ) {
|
333 | processBindArgsForSource.call( this, A, bind );
|
334 | processBindArgsForDestination.call( this, B, bind );
|
335 | }
|
336 |
|
337 | function processBindArgsForSource( src, bind ) {
|
338 | try {
|
339 | if ( bind.back === false ) src.open = false;
|
340 |
|
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 |
|
354 | function 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 |
|
383 | function parseFormat( syntax ) {
|
384 | if ( typeof syntax !== 'string' )
|
385 | throw "In {Bind format:...}, `format` must be a string!";
|
386 | return syntax;
|
387 | }
|
388 |
|
389 | function 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 |
|
397 | function 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 |
|
408 | function parseConverterConst( value ) {
|
409 | return "function(){return " + JSON.stringify( value ) + "}";
|
410 | }
|
411 |
|
412 | function parseConverterBehind( funcName ) {
|
413 | this.addNeededBehindFunction( funcName );
|
414 | return "CODE_BEHIND." + funcName + ".bind( this )";
|
415 | }
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 | function 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 |
|
433 | function 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 |
|
441 | function 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 |
|
457 |
|
458 | Template.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 |
|
475 | function 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 |
|
496 | function pushUnique( arr, item ) {
|
497 | if ( arr.indexOf( item ) === -1 )
|
498 | arr.push( item );
|
499 | }
|
500 |
|
501 | function 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 |
|
511 | function 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 |
|
518 | function 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 |
|
536 |
|
537 |
|
538 | var RX_JAVASCRIPT_IDENTIFIER = /^[_$a-z][_$a-z0-9]*$/ig;
|
539 |
|
540 | function quotesIfNeeded( name ) {
|
541 | return RX_JAVASCRIPT_IDENTIFIER.test( name ) ? name : JSON.stringify( name );
|
542 | }
|
543 |
|
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 | function keySyntax( name ) {
|
550 | if ( RX_JAVASCRIPT_IDENTIFIER.test( name ) ) return "." + name;
|
551 | return "[" + JSON.stringify( name ) + "]";
|
552 | }
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 | function 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 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 | function isVarNameAndNotViewId( name ) {
|
576 | return name.substr( 0, 2 ) === 'e_';
|
577 | }
|
578 |
|
579 | function limitJson( obj, max ) {
|
580 | return limit( JSON.stringify( obj ), max );
|
581 | }
|
582 |
|
583 | function 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 |
|
593 |
|
594 |
|
595 |
|
596 | function 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 |