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 | const 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 | const that = this;
|
106 | const keys = Object.keys( this.requires );
|
107 | keys.sort( function ( a, b ) {
|
108 | const 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( k => `const ${k} = ${that.requires[ k ]};` ) );
|
117 | };
|
118 |
|
119 | Template.prototype.generateFunctions = function () {
|
120 | if ( isEmpty( this.functions ) ) return [];
|
121 |
|
122 | const that = this;
|
123 | return generateSection(
|
124 | "Global functions.",
|
125 | Object.keys( this.functions ).map(
|
126 | k => "function " + k + that.functions[ k ] + ";" ) );
|
127 | };
|
128 |
|
129 | Template.prototype.generateGlobalVariables = function () {
|
130 | if ( isEmpty( this.vars ) ) return [];
|
131 |
|
132 | const that = this;
|
133 | return generateSection(
|
134 | "Global variables.",
|
135 | Object.keys( this.vars ).map( k => `const ${k} = ${that.vars[ k ]};` )
|
136 | );
|
137 | };
|
138 |
|
139 | Template.prototype.generateLinks = function () {
|
140 | const that = this;
|
141 |
|
142 | try {
|
143 | const output = [];
|
144 | var links = this.section.links;
|
145 | if ( links.length === 0 ) return output;
|
146 |
|
147 | links.forEach( function ( link, index ) {
|
148 | try {
|
149 | output.push( "new Link({" );
|
150 | if ( that.debug ) {
|
151 | output.push( " dbg: '" + that.moduleName + "#" + index + "'," );
|
152 | }
|
153 | output.push(
|
154 | " A:" + pod2code.call( that, link.A ) + ",",
|
155 | " B:" + pod2code.call( that, link.B ) + ",",
|
156 | " name:" + JSON.stringify( link.A.path + " > " + link.B.path ),
|
157 | "});" );
|
158 | } catch ( ex ) {
|
159 | throw ex + "\n" + "link = " + JSON.stringify( link );
|
160 | }
|
161 | } );
|
162 |
|
163 | return generateSection( "Links", output );
|
164 | } catch ( ex ) {
|
165 | throw ex + "\n" + JSON.stringify( this.section.links, null, " " ) + "\n" + "generateLinks()";
|
166 | }
|
167 | };
|
168 |
|
169 |
|
170 |
|
171 |
|
172 | function pod2code( pod ) {
|
173 | try {
|
174 | const that = this;
|
175 | var items = [];
|
176 | Object.keys( pod ).forEach( function ( key ) {
|
177 | var val = pod[ key ];
|
178 | if ( key === 'path' ) pod2CodePath.call( that, items, val );
|
179 | else if ( key === 'delay' ) pod2CodeDelay.call( that, items, val );
|
180 | else if ( key === 'action' ) pod2CodeAction.call( that, items, val );
|
181 | else if ( key === 'converter' ) pod2CodeConverter.call( that, items, val );
|
182 | else if ( key === 'format' ) pod2CodeFormat.call( that, items, val );
|
183 | else if ( key === 'map' ) pod2CodeMap.call( that, items, val );
|
184 | else if ( key === 'header' ) pod2CodeHeader.call( that, items, val );
|
185 | else if ( key === 'footer' ) pod2CodeFooter.call( that, items, val );
|
186 | else if ( key === 'open' ) pod2CodeOpen.call( that, items, val );
|
187 | } );
|
188 | return "{" + items.join( ",\n " ) + "}";
|
189 | } catch ( ex ) {
|
190 | throw ex + "\n" + "pod2code( " + JSON.stringify( pod ) + " )";
|
191 | }
|
192 | }
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | function pod2CodePath( items, path ) {
|
198 | var pieces = path.split( "/" )
|
199 | .map( x => x.trim() )
|
200 | .filter( x => x.length > 0 );
|
201 | if ( pieces.length === 1 ) {
|
202 |
|
203 |
|
204 | items.push(
|
205 | "obj: that",
|
206 | "name: '" + camelCase( pieces[ 0 ] ) + "'" );
|
207 | } else {
|
208 | var attName = camelCase( pieces.pop() );
|
209 | var firstPiece = pieces.shift();
|
210 | var objCode = isVarNameAndNotViewId( firstPiece ) ?
|
211 | firstPiece : "e_" + camelCase( firstPiece );
|
212 |
|
213 | objCode += pieces.map( x => keySyntax( x ) ).join( "" );
|
214 | items.push(
|
215 | "obj: " + objCode,
|
216 | "name: '" + attName + "'" );
|
217 | }
|
218 | }
|
219 |
|
220 |
|
221 |
|
222 |
|
223 | function pod2CodeDelay( items, delay ) {
|
224 | items.push( "delay: " + parseInt( delay ) );
|
225 | }
|
226 |
|
227 | function pod2CodeOpen( items, open ) {
|
228 | if ( open === false ) {
|
229 | items.push( "open: false" );
|
230 | }
|
231 | }
|
232 |
|
233 |
|
234 |
|
235 |
|
236 | function pod2CodeAction( items, actions ) {
|
237 | items.push(
|
238 | "action: function(v) {\n" +
|
239 | actions.map( x => " " + x ).join( "\n" ) +
|
240 | "}" );
|
241 | }
|
242 |
|
243 | function pod2CodeConverter( items, converter ) {
|
244 | items.push( "converter: " + converter );
|
245 | }
|
246 |
|
247 | function pod2CodeFormat( items, format ) {
|
248 | items.push( "format: [_, " + JSON.stringify( format ) + "]" );
|
249 | }
|
250 |
|
251 | function pod2CodeMap( items, codeLines ) {
|
252 | items.push(
|
253 | "map: function() {\n" +
|
254 | codeLines.map( x => " " + x ).join( "\n" ) +
|
255 | "}" );
|
256 | }
|
257 |
|
258 | function pod2CodeHeader( items, codeLines ) {
|
259 | items.push(
|
260 | "header: function() {\n" +
|
261 | codeLines.map( x => " " + x ).join( "\n" ) +
|
262 | "}" );
|
263 | }
|
264 |
|
265 | function pod2CodeFooter( items, codeLines ) {
|
266 | items.push(
|
267 | "footer: function() {\n" +
|
268 | codeLines.map( x => " " + x ).join( "\n" ) +
|
269 | "}" );
|
270 | }
|
271 |
|
272 | Template.prototype.addNeededBehindFunction = function ( functionName ) {
|
273 | this.requires.View = "require('tfw.view');";
|
274 | pushUnique( this.neededBehindFunctions, functionName );
|
275 | };
|
276 |
|
277 | Template.prototype.addCast = function ( name, value ) {
|
278 | if ( name.substr( 0, 7 ) === 'behind.' ) {
|
279 | var funcName = name.substr( 7 );
|
280 | this.addNeededBehindFunction( funcName );
|
281 | return "CODE_BEHIND." + funcName + ".bind( this )";
|
282 | } else {
|
283 | if ( typeof value === 'undefined' ) value = "Converters.get('" + name + "')";
|
284 | this.requires[ "Converters" ] = "require('tfw.binding.converters')";
|
285 | this.vars[ "conv_" + name ] = value;
|
286 | return "conv_" + name;
|
287 | }
|
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 | Template.prototype.addLinkFromBind = function ( bind, to ) {
|
317 | try {
|
318 | var A = { path: bind[ 1 ] || bind.path };
|
319 | var B = processLinkFromBindArgumentTo.call( this, to );
|
320 |
|
321 | processBindArguments.call( this, A, B, bind );
|
322 | return this.addLink( A, B );
|
323 | } catch ( ex ) {
|
324 | throw Error(
|
325 | ex + "\n" + "addLinkFromBind(" +
|
326 | JSON.stringify( bind ) + ", " +
|
327 | JSON.stringify( to ) + ")" );
|
328 | }
|
329 | };
|
330 |
|
331 | function processBindArguments( A, B, bind ) {
|
332 | processBindArgsForSource.call( this, A, bind );
|
333 | processBindArgsForDestination.call( this, B, bind );
|
334 | }
|
335 |
|
336 | function processBindArgsForSource( src, bind ) {
|
337 | try {
|
338 | if ( bind.back === false ) src.open = false;
|
339 |
|
340 | var backAttributes = {};
|
341 | var name, value;
|
342 | for ( name in bind ) {
|
343 | if ( name.charAt( 0 ) === '-' ) {
|
344 | backAttributes[ name.substr( 1 ) ] = bind[ name ];
|
345 | }
|
346 | }
|
347 | processBindArgsForDestination( src, backAttributes );
|
348 | } catch ( ex ) {
|
349 | throw ex + "\n...in processBindArgsForSource: " + limitJson( bind );
|
350 | }
|
351 | }
|
352 |
|
353 | function processBindArgsForDestination( dst, bind ) {
|
354 | try {
|
355 | var name, value;
|
356 | for ( name in bind ) {
|
357 | value = bind[ name ];
|
358 | switch ( name ) {
|
359 | case 'delay':
|
360 | if ( typeof value !== 'number' ) {
|
361 | throw "In a {Bind... delay:...} declaration, `delay` must be a number!\n" +
|
362 | " delay: " + limitJson( value );
|
363 | }
|
364 | dst.delay = value;
|
365 | break;
|
366 | case 'const':
|
367 | dst.converter = parseConverter.call( this, { "0": 'Const', "1": value } );
|
368 | break;
|
369 | case 'converter':
|
370 | dst.converter = parseConverter.call( this, value );
|
371 | break;
|
372 | case 'format':
|
373 | dst.format = parseFormat.call( this, value );
|
374 | break;
|
375 | }
|
376 | }
|
377 | } catch ( ex ) {
|
378 | throw ex + "\n...in processBindArgsForDestination: " + limitJson( bind );
|
379 | }
|
380 | }
|
381 |
|
382 | function parseFormat( syntax ) {
|
383 | if ( typeof syntax !== 'string' )
|
384 | throw "In {Bind format:...}, `format` must be a string!";
|
385 | return syntax;
|
386 | }
|
387 |
|
388 | function parseConverter( syntax ) {
|
389 | if ( typeof syntax === 'string' ) return parseConverterString.call( this, syntax );
|
390 | if ( isSpecial( syntax, "behind" ) ) return parseConverterBehind.call( this, syntax[ "1" ] );
|
391 | if ( isSpecial( syntax, 'const' ) ) return parseConverterConst.call( this, syntax[ "1" ] );
|
392 | throw "In a {Bind converter:...}, `converter` must be a string or `{Behind ...}`!\n" +
|
393 | " but we found: " + limitJson( syntax );
|
394 | }
|
395 |
|
396 | function parseConverterString( syntax ) {
|
397 | if ( syntax.substr( 0, 7 ) === 'behind.' ) {
|
398 | var funcName = syntax.substr( 7 );
|
399 | this.addNeededBehindFunction( funcName );
|
400 | return "CODE_BEHIND." + funcName + ".bind( this )";
|
401 | } else {
|
402 | this.vars[ "conv_" + syntax ] = "Converters.get('" + syntax + "')";
|
403 | return "conv_" + syntax;
|
404 | }
|
405 | }
|
406 |
|
407 | function parseConverterConst( value ) {
|
408 | return "function(){return " + JSON.stringify( value ) + "}";
|
409 | }
|
410 |
|
411 | function parseConverterBehind( funcName ) {
|
412 | this.addNeededBehindFunction( funcName );
|
413 | return "CODE_BEHIND." + funcName + ".bind( this )";
|
414 | }
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 | function processLinkFromBindArgumentTo( to ) {
|
422 | if ( typeof to === 'string' ) return { path: to };
|
423 | if ( Array.isArray( to ) ) return { action: to };
|
424 | if ( isSpecial( to, "behind" ) ) {
|
425 | return processLinkFromBindArgumentTo_behind.call( this, to );
|
426 | } else if ( isSpecial( to, "bind" ) ) {
|
427 | return processLinkFromBindArgumentTo_bind.call( this, to );
|
428 | }
|
429 | throw "`to` argument can be only a string, an array or {Behind ...}!";
|
430 | }
|
431 |
|
432 | function processLinkFromBindArgumentTo_behind( to ) {
|
433 | var behindFunctionName = to[ 1 ];
|
434 | if ( typeof behindFunctionName !== 'string' )
|
435 | throw "In a {Behind ...} statement, the second argument must be a string!";
|
436 | pushUnique( this.neededBehindFunctions, behindFunctionName );
|
437 | return { action: [ "CODE_BEHIND." + behindFunctionName + ".call(that, v)" ] };
|
438 | }
|
439 |
|
440 | function processLinkFromBindArgumentTo_bind( to ) {
|
441 | var behindFunctionName;
|
442 | var binding = {};
|
443 | if ( Array.isArray( to.action ) ) binding.action = to.action;
|
444 | [ 'map', 'header', 'footer' ].forEach( function ( id ) {
|
445 | if ( typeof to[ id ] === 'string' ) {
|
446 | behindFunctionName = to[ id ];
|
447 | pushUnique( this.neededBehindFunctions, behindFunctionName );
|
448 | binding[ id ] = [ "return CODE_BEHIND." + behindFunctionName + ".apply(that, arguments)" ];
|
449 | }
|
450 | }, this );
|
451 | return binding;
|
452 | }
|
453 |
|
454 |
|
455 |
|
456 |
|
457 | Template.prototype.addLink = function ( A, B ) {
|
458 | try {
|
459 | this.requires[ "Link" ] = "require('tfw.binding.link')";
|
460 | this.that = true;
|
461 | checkLinkPod( A );
|
462 | checkLinkPod( B );
|
463 | var link = JSON.parse( JSON.stringify( { A: A, B: B } ) );
|
464 | this.section.links.push( link );
|
465 | return link;
|
466 | } catch ( ex ) {
|
467 | throw Error(
|
468 | ex + "\n" + "addLink(" +
|
469 | JSON.stringify( A ) + ", " +
|
470 | JSON.stringify( B ) + ")" );
|
471 | }
|
472 | };
|
473 |
|
474 | function checkLinkPod( pod ) {
|
475 | try {
|
476 | var pathType = typeof pod.path;
|
477 | if ( pathType !== 'undefined' && pathType !== 'string' )
|
478 | throw "Attribute `path` in a link's pod must be a <string>, not a <" + pathType + ">!\n" +
|
479 | "pod.path = " + JSON.stringify( pod.path );
|
480 |
|
481 | var actionType = typeof pod.action;
|
482 | if ( actionType !== 'undefined' && !Array.isArray( pod.action ) )
|
483 | throw "Attribute `action` in a link's pod must be an <array>, not a <" + actionType + ">!\n" +
|
484 | "pod.action = " + JSON.stringify( pod.action );
|
485 |
|
486 | if ( !pod.path && !pod.action )
|
487 | throw "A link's pod must have at least an attribute `path` or `action`!";
|
488 | } catch ( ex ) {
|
489 | throw Error(
|
490 | ex + "\n" +
|
491 | "checkLinkPod( " + JSON.stringify( pod ) + " )" );
|
492 | }
|
493 | }
|
494 |
|
495 | function pushUnique( arr, item ) {
|
496 | if ( arr.indexOf( item ) === -1 )
|
497 | arr.push( item );
|
498 | }
|
499 |
|
500 | function generateSection( sectionName, contentArray, indent ) {
|
501 | if ( typeof indent === 'undefined' ) indent = "";
|
502 |
|
503 | var firstLine = indent + "//";
|
504 | var count = sectionName.length + 2;
|
505 | while ( count-- > 0 ) firstLine += "-";
|
506 | var lines = [ firstLine, indent + "// " + sectionName ];
|
507 | return lines.concat( contentArray );
|
508 | }
|
509 |
|
510 | function isEmpty( value ) {
|
511 | if ( Array.isArray( value ) ) return value.length === 0;
|
512 | if ( typeof value === 'string' ) return value.trim().length === 0;
|
513 | for ( var k in value ) return false;
|
514 | return true;
|
515 | }
|
516 |
|
517 | function object2code( obj ) {
|
518 | if ( Array.isArray( obj ) ) {
|
519 | return "[" + obj.map( x => object2code( x ) ).join( ", " ) + "]";
|
520 | }
|
521 | switch ( typeof obj ) {
|
522 | case 'object':
|
523 | return "{" +
|
524 | Object.keys( obj )
|
525 | .map( k => quotesIfNeeded( k ) + ": " + object2code( obj[ k ] ) )
|
526 | .join( ", " ) +
|
527 | "}";
|
528 | default:
|
529 | return JSON.stringify( obj );
|
530 | }
|
531 | }
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 | const RX_JAVASCRIPT_IDENTIFIER = /^[_$a-z][_$a-z0-9]*$/ig;
|
538 |
|
539 | function quotesIfNeeded( name ) {
|
540 | return RX_JAVASCRIPT_IDENTIFIER.test( name ) ? name : JSON.stringify( name );
|
541 | }
|
542 |
|
543 |
|
544 |
|
545 |
|
546 |
|
547 |
|
548 | function keySyntax( name ) {
|
549 | if ( RX_JAVASCRIPT_IDENTIFIER.test( name ) ) return "." + name;
|
550 | return "[" + JSON.stringify( name ) + "]";
|
551 | }
|
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 | function isSpecial( obj, name ) {
|
558 | if ( !obj ) return false;
|
559 | if ( typeof obj[ 0 ] !== 'string' ) return false;
|
560 | if ( typeof name === 'string' ) {
|
561 | return obj[ 0 ].toLowerCase() === name;
|
562 | }
|
563 | return true;
|
564 | }
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 | function isVarNameAndNotViewId( name ) {
|
575 | return name.substr( 0, 2 ) === 'e_';
|
576 | }
|
577 |
|
578 | function limitJson( obj, max ) {
|
579 | return limit( JSON.stringify( obj ), max );
|
580 | }
|
581 |
|
582 | function limit( txt, max ) {
|
583 | if ( typeof max === 'undefined' ) max = 80;
|
584 | if ( txt === undefined ) txt = "undefined";
|
585 | else if ( txt === null ) txt = "null";
|
586 | if ( txt.length <= max ) return txt;
|
587 | return txt.substr( 0, max ) + "...";
|
588 | }
|
589 |
|
590 |
|
591 |
|
592 |
|
593 |
|
594 |
|
595 | function createSectionStructure() {
|
596 | return {
|
597 | init: null,
|
598 | comments: [],
|
599 | attribs: {
|
600 | define: [],
|
601 | init: []
|
602 | },
|
603 | elements: {
|
604 | define: [],
|
605 | init: []
|
606 | },
|
607 | events: [],
|
608 | links: [],
|
609 | ons: [],
|
610 | statics: []
|
611 | };
|
612 | } |
\ | No newline at end of file |