UNPKG

31.9 kBJavaScriptView Raw
1"use strict";
2
3module.exports = {
4 compile,
5 initialize
6};
7
8
9/*
10 * When compiling the file `main.html`, we gather all the CSS and JS files
11 * needed.
12 *
13 * If `no-zip` option has not been set, all the CSS files are combined in
14 * `css/@main.css` and all the JS files are combined in `js/@main.js`.
15 *
16 * # Modules
17 *
18 * Modules are stored in folder `mod/`.
19 * * `mymodule/`
20 * * `mymodule.js`
21 * * `mymodule.xjs`
22 * * `mymodule.ini`
23 * * `mymodule.css`
24 * * `mymodule.dep`
25 */
26
27const
28FS = require( "fs" ),
29Path = require( "path" ),
30ToloframeworkPermissiveJson = require( "toloframework-permissive-json" ),
31MinifyJS = require( "./minifyJS" ),
32CompilerCOM = require( "./compiler-com" ),
33CompilerJS = require( "./compiler-js" ),
34ParserHTML = require( "./tlk-htmlparser" ),
35PathUtils = require( "./pathutils" ),
36Template = require( "./template" ),
37Source = require( "./source" ),
38Fatal = require( "./fatal" ),
39Util = require( "./util" ),
40Tree = require( "./htmltree" ),
41Libs = require( "./compiler-com-libs" ),
42Tpl = require( "./template" ),
43Rework = require( 'rework' );
44
45let
46Project = null,
47Components = {},
48Scopes = [ {} ];
49
50
51/**
52 * @param {type} prj - Current project.
53 * @returns {undefined}
54 */
55function initialize( prj ) {
56 Project = prj;
57 Components = {};
58 Scopes = [ {} ];
59 CompilerCOM.loadComponents( prj );
60}
61
62
63/**
64 * @param {string} file - Full path of the HTML file to compile.
65 * @param {object} _options - Options for debug/release mode, etc.
66 * @returns {object} Compiled html source code as an instance of Source.
67 * @see source
68 */
69function compile( file, _options ) {
70 const
71 options = typeof _options !== 'undefined' ? _options : {},
72 sourceHTML = new Source( Project, file ),
73 // Array of the sources we must link.
74 sourcesToLink = [];
75 let
76 // Output of a page file.
77 outputPage = '',
78
79 /*
80 * In case of multi-pages, the first page has the same name as the `file`.
81 * So it's output must become the output of the `file`.
82 */
83 outputOfFirstPage = '',
84 // Page filename relative to the sourceHTML.
85 pageFilename = '';
86 // Check if the file and all its components are uptodate.
87 if ( !isUptodate( sourceHTML ) ) {
88 Scopes[ 0 ].$filename = sourceHTML.name();
89 console.log( `Compile HTML: ${sourceHTML.name().cyan}` );
90 let root = ParserHTML.parse( sourceHTML.read() );
91 // Output of the main file.
92 const output = compileRoot( root, sourceHTML, options );
93 if ( output ) {
94 sourceHTML.tag( 'output', output );
95 while ( typeof root.type === 'undefined' &&
96 root.children &&
97 root.children.length === 1 ) {
98 root = root.children[ 0 ];
99 }
100 if ( root.type === Tree.PAGES ) {
101 outputPage = ToloframeworkPermissiveJson.parse( JSON.stringify( output ) );
102 root.children.forEach( function ( child, idx ) {
103 console.log( ( " Page " + ( idx + 1 ) ).cyan );
104 var src = sourceHTML;
105 pageFilename = src.name();
106 if ( idx !== 0 ) {
107 pageFilename = file.substr( 0, file.length - 4 ) + idx + '.html';
108 src = new Source( Project, pageFilename );
109 }
110 outputPage = compileRoot(
111 child, src, options, ToloframeworkPermissiveJson.parse( JSON.stringify( output ) )
112 );
113 outputPage.filename = pageFilename;
114 src.tag( "output", outputPage );
115 src.save();
116 sourcesToLink.push( pageFilename );
117 if ( idx === 0 ) {
118 // This is the first page.
119 outputOfFirstPage = outputPage;
120 }
121 } );
122 sourceHTML.tag( 'pages', sourcesToLink );
123 sourceHTML.tag( 'output', outputOfFirstPage );
124 }
125 sourceHTML.save();
126 }
127 }
128
129 // Linking.
130 linkPages( sourceHTML, options );
131 return sourceHTML;
132}
133
134
135/**
136 * @param {Source} sourceHTML - Html source instance.
137 * @param {object} options - Options for debug/release mode, etc.
138 * @returns {undefined}
139 */
140function linkPages( sourceHTML, options ) {
141 console.log( `Link: ${sourceHTML.name().yellow}` );
142 const sourcesToLink = sourceHTML.tag( 'pages' );
143 if ( sourcesToLink ) {
144 // Multi-pages.
145 sourcesToLink.forEach( function forEachPage( pageFilename ) {
146 const src = new Source( Project, pageFilename );
147 link( src, options );
148 } );
149 } else {
150 // Single page.
151 link( sourceHTML, options );
152 }
153}
154
155/**
156 * @param {object} root - HTML tree.
157 * @param {Source} sourceHTML - Source object represnting the HTML file.
158 * @param {object} options - Build options for release/debug, etc.
159 * @param {object} _output - Stuff used internally in the recursive process to create the final Html file.
160 * @see tlk-htmlparser
161 */
162function compileRoot( root, sourceHTML, options, _output ) {
163 // Stuff to create HTML file.
164 const output = typeof _output !== 'undefined' ? _output : {
165 // Code CSS.
166 innerCSS: {},
167 // CSS files.
168 outerCSS: {},
169 // Code Javascript to embed in `js/@index.js` file.
170 innerJS: {},
171 // Javascript modules directly required.
172 require: {},
173 // All the Javascript modules needed to build this page.
174 modules: [],
175 // Javascript code to insert in a `DOMContentLoaded` event.
176 initJS: {},
177 // Javascript code to insert in a `DOMContentLoaded` event after all the code of `initJS`.
178 postInitJS: {},
179 // Files needed to build this file. If a include change, we must recompile.
180 include: {},
181 // A resource is a file to create in the output folder when this HTML is linked.
182 // the key is the resource name, and the value is an objet depending on the type of resource:
183 // * {dst: "img/plus.png", src: "../gfx/icon-plus.png"}
184 // * {dst: "img/face.svg", txt: "<svg xmlns:svg=..."}
185 resource: {},
186 // Modules that have no real file in `mod` directory, but
187 // which are created dynamically in Components.
188 dynamicModules: {}
189 };
190 var libs = Libs( sourceHTML, Components, Scopes, output );
191 libs.compile( root, options );
192 Tree.trim( root );
193 output.root = root;
194 return output;
195}
196
197/**
198 * A source needs to be rebuild if it is not uptodate.
199 * Here are the reasons for a source not to be uptodate:
200 * * Source code more recent than the tags (`Source.isUptodate()`).
201 * * Includes source codes more recent than this source.
202 * @param {Source} sourceHTML - Html source file.
203 */
204function isUptodate( sourceHTML ) {
205 console.info("[compiler-html2] sourceHTML=", sourceHTML.name());
206 if ( !sourceHTML.isUptodate() ) return false;
207
208 const output = sourceHTML.tag( 'output' );
209 if ( !output || !Array.isArray( output.include ) ) return false;
210 // Modification time for the current HTML file.
211 const currentFileModificationTime = sourceHTML.modificationTime();
212 // `includeFilename` is the File name relative to `sourceHTML`.
213 for ( const includeFilename of output.include ) {
214 // Source object of the included file.
215 const includeSource = new Source(
216 Project,
217 sourceHTML.getPathRelativeToSource( includeFilename )
218 );
219 const stats = FS.statSync( includeSource.getAbsoluteFilePath() );
220 if ( stats.mtime > currentFileModificationTime ) {
221 // An included file if more recent. We must recompile.
222 return false;
223 }
224 }
225 return true;
226}
227
228/**
229 * Take a HTML file `filename.html` and combine all the styles in
230 * `css/@filename.css` and all the javascripts in `js/@filename.js`.
231 * For each module, check if there is a folder with the same name. If
232 * yes, copy that resource in `css` dir. For example, it you have
233 * `mod/foobar.js` and a folder `mod/foobar/`, copy it to
234 * `www/css/foobar/`.
235 */
236function link( src, options ) {
237 const
238 htmlDir = Path.dirname( src.name() ),
239 pathWWW = Project.wwwPath( htmlDir ),
240 pathJS = Path.join( pathWWW, "js" ),
241 pathCSS = Path.join( pathWWW, "css" );
242
243 Project.mkdir( pathJS );
244 Project.mkdir( pathCSS );
245
246 const output = linkForRelease( src, pathJS, pathCSS, options, htmlDir );
247
248 PathUtils.file(
249 Project.wwwPath( src.name() ),
250 `<!DOCTYPE html>\n${Tree.toString(output.root).trim()}`
251 );
252
253 // Writing resources if any.
254 writeResources( output );
255}
256
257/**
258 *
259 */
260function linkForRelease( src, pathJS, pathCSS, options, htmlDir ) {
261 Project.mkdir( Path.join( pathJS, "map" ) );
262 Project.mkdir( Path.join( pathCSS, "map" ) );
263
264 var key, val;
265 var prj = src.prj();
266 var nameWithoutExt = src.name().substr( 0, src.name().length - 5 );
267 // If `nameWithoutExt` is in a subfolder, `backToRoot` must containt
268 // as many `../` as there are subfolders in `nameWithoutExt`.
269 var backToRoot = getBackToRoot( nameWithoutExt );
270 backToRoot = ''; // Finalement les dépendances sont au même niveau que le fichier HTML.
271
272 var output = src.tag( "output" ) || {};
273 var root = output.root;
274 if ( !root ) {
275 Fatal.fire(
276 "The cache seems to be corrupted. Try `tfw clean` to clean it up. " +
277 "And try building again.",
278 "Please cleanup the cache!"
279 );
280 }
281 output.filename = src.name();
282 var head = findHead( root );
283 addDescriptionToHead( head, options );
284 var innerJS = Tpl.file( "require.js" ).out + concatDicValues( output.innerJS );
285 innerJS += getInitJS( output );
286
287 var innerCSS = concatDicValues( output.innerCSS );
288
289 // If there is a CSS file with the same name as the HTML file, embed it.
290 if ( FS.existsSync( Project.srcOrLibPath( nameWithoutExt + '.css' ) ) ) {
291 console.log( "Found: " + ( nameWithoutExt + ".css" ) );
292 innerCSS += PathUtils.file( Project.srcOrLibPath( nameWithoutExt + '.css' ) );
293 }
294
295 function addInnerJS() {
296 // Adding innerJS.
297 const zippedJS = MinifyJS.minify( {
298 name: src.name(),
299 content: innerJS
300 } ).zip;
301 prj.flushContent( "js/" + addFilePrefix( nameWithoutExt ) + ".js", zippedJS, htmlDir );
302 head.children.push( {
303 type: Tree.TAG,
304 name: 'script',
305 attribs: {
306 defer: null,
307 src: backToRoot + "js/" + addFilePrefix( nameWithoutExt ) + ".js"
308 }
309 } );
310 }
311
312 const combination = combineRequires( output, options );
313 makeDependenciesGraph( combination.js, src, Project );
314
315 var externalDeps = lookForExternalDependencies( combination.js );
316 externalDeps.js.forEach( function ( code ) {
317 innerJS += code;
318 } );
319 for ( key in externalDeps.res ) {
320 val = externalDeps.res[ key ];
321 try {
322 Project.copyFile( key, val );
323 } catch ( ex ) {
324 throw Error( "Unable to copy external dependency `" + key + "` into `" + val + "`!\n" +
325 JSON.stringify( externalDeps, null, ' ' ) );
326 }
327 }
328
329 // Used to loop over CSS and JS files.
330 if ( options.dev ) {
331 addInnerJS();
332 // DEBUG. Do not combine.
333 for ( key in combination.css ) {
334 val = combination.css[ key ];
335 if ( key.substr( 0, 4 ) == 'mod/' ) {
336 key = key.substr( 4 );
337 }
338 head.children.push( {
339 type: Tree.TAG,
340 name: 'link',
341 void: true,
342 attribs: {
343 rel: "stylesheet",
344 type: "text/css",
345 href: backToRoot + "css/" + key + ".css"
346 }
347 } );
348 prj.flushContent( "css/" + key + ".css", val.src, htmlDir );
349 }
350 for ( key in combination.js ) {
351 val = combination.js[ key ];
352 if ( key.substr( 0, 4 ) == 'mod/' ) {
353 key = key.substr( 4 );
354 }
355 head.children.push( {
356 type: Tree.TAG,
357 name: 'script',
358 attribs: {
359 defer: null,
360 src: backToRoot + "js/" + key + ".js"
361 }
362 } );
363 prj.flushContent( "js/" + key + ".js", val.src, htmlDir );
364 prj.flushContent( "js/" + key + ".js.map", JSON.stringify( val.map ), htmlDir );
365 }
366 } else {
367 // RELEASE.
368 for ( key in combination.css ) {
369 val = combination.css[ key ];
370 innerCSS += val.zip;
371 }
372 for ( key in combination.js ) {
373 val = combination.js[ key ];
374 innerJS += val.zip + "\n";
375 }
376 addInnerJS();
377 }
378
379 // Adding innerCSS.
380 prj.flushContent( "css/" + addFilePrefix( nameWithoutExt ) + ".css", innerCSS, htmlDir );
381 head.children.push( {
382 type: Tree.TAG,
383 name: 'link',
384 void: true,
385 attribs: {
386 rel: "stylesheet",
387 type: "text/css",
388 href: backToRoot + "css/" + addFilePrefix( nameWithoutExt ) + ".css"
389 }
390 } );
391
392 return output;
393}
394
395/**
396 * Look for any "*.dep" file for all the neededmodules.
397 *
398 * @param {array} jsFiles - Array of the JS files of each module.
399 * @returns {undefined}
400 */
401function lookForExternalDependencies( jsFiles ) {
402 const
403 javascriptSources = [],
404 resources = {};
405
406 for ( const jsFileName of Object.keys( jsFiles ) ) {
407 const depFileName = `${jsFileName}.dep`;
408 if ( Project.srcOrLibPath( depFileName ) ) {
409 const depFile = new Source( Project, depFileName );
410 try {
411 const
412 json = depFile.read(),
413 dependencies = ToloframeworkPermissiveJson.parse( json );
414 // Looking for Javascript dependencies.
415 lookForExternalDependenciesJS( dependencies.js, javascriptSources );
416 // Looking for other dependencies.
417 lookForExternalDependenciesRES( dependencies.res, resources, depFile );
418 } catch ( ex ) {
419 Fatal.fire(
420 `Unable to parse JSON file "${depFile.getAbsoluteFilePath()}"!\n${ex}`,
421 Project.srcOrLibPath( jsFileName )
422 );
423 }
424 }
425 }
426
427 return {
428 js: javascriptSources,
429 res: resources
430 };
431}
432
433/**
434 * Section "res" in a dependency file.
435 *
436 * @param {objects} _def - `{ "bob/foo.png": "", "yo/man.kml": "maps/man.kml" }`
437 * @param {object} resources -
438 * @param {Source} depFile - Source of the dependency file.
439 * @returns {undefined}
440 */
441function lookForExternalDependenciesRES( _def, resources, depFile ) {
442 if ( !_def ) return;
443 try {
444 const def = convertExternalDependencyDefinition( _def );
445 for ( const dep of Object.keys( def ) ) {
446 if ( def[ dep ] === "" ) {
447 // `res: { "bob/foo.png": "" }` is equivalent to
448 // `res: { "bob/foo.png": "bob/foo.png" }`
449 def[ dep ] = dep;
450 }
451 const
452 srcDep = Project.srcOrLibPath( `mod/${dep}` ) ||
453 Project.srcOrLibPath( dep );
454 if ( !srcDep ) {
455 Fatal.fire(
456 `Unable to find dependency file "${dep}" nor "mod/${dep}"!`,
457 depFile.getAbsoluteFilePath()
458 );
459 }
460 resources[ srcDep ] = Project.wwwPath( def[ dep ] );
461 }
462 } catch ( ex ) {
463 Fatal.fire(
464 `Unable to parse RES dependencies: ${JSON.stringify(_def)}!\n${ex}`,
465 depFile
466 );
467 }
468}
469
470/**
471 * Section "js" in a dependency file.
472 *
473 * @param {objects} _def - `{ "helper.js": "" }` or `"helper.js"`.
474 * @param {array} javascriptSources - Array of the Javascript code to include in current HTML page.
475 * @returns {undefined}
476 */
477function lookForExternalDependenciesJS( _def, javascriptSources ) {
478 if ( !_def ) return;
479 try {
480 const def = convertExternalDependencyDefinition( _def );
481 Object.keys( def ).forEach( function ( js ) {
482 const
483 filename = `mod/${js}`,
484 src = new Source( Project, filename ),
485 code = src.read();
486 pushUnique( javascriptSources, code );
487 } );
488 } catch ( ex ) {
489 Fatal.fire(
490 `Unable to parse JS dependencies: ${JSON.stringify(_def)}!\n${ex}`,
491 javascriptSources
492 );
493 }
494}
495
496/**
497 * There are three ways to define a list of external dependencies:
498 * * "foo.js"
499 * * ["foo.js", "bar.js"]
500 * * { "foo.js": "libs/foo.js", ... }
501 *
502 * This function transforms a definition in its third form: `{ "foo.js": "libs/foo.js", ... }`.
503 *
504 * @param {any} def - Definition of the dependencies.
505 * @returns {object} Definition in the third form.
506 */
507function convertExternalDependencyDefinition( def ) {
508 if ( typeof def === 'string' ) {
509 const defAsString = {};
510 defAsString[ def ] = "";
511 return defAsString
512 }
513 if ( Array.isArray( def ) ) {
514 const defAsArray = {};
515 def.forEach( function ( file ) {
516 defAsString[ file ] = "";
517 } );
518 }
519 return def;
520}
521
522/**
523 * Push `item` into `arr` if it is not already in.
524 */
525function pushUnique( arr, item ) {
526 if ( arr.indexOf( item ) > -1 ) return false;
527 arr.push( item );
528 return true;
529}
530
531function writeResources( output ) {
532 // Name of the resource.
533 var resourceName;
534 // Data of the resource.
535 var resourceData;
536 // Destination path (in `www`folder).
537 var dstPath;
538 // Source path (in `src` folder).
539 var srcPath;
540 // Resource content.
541 var content;
542
543 for ( resourceName in output.resource ) {
544 resourceData = output.resource[ resourceName ];
545 dstPath = Project.wwwPath( resourceData.dst );
546 if ( PathUtils.isDirectory( dstPath ) ) {
547 // We must copy a whole directory.
548
549 } else {
550 // Create folders if needed.
551 Project.mkdir( Path.dirname( dstPath ) );
552 if ( resourceData.src ) {
553 srcPath = Project.srcOrLibPath( resourceData.src );
554 Project.copyFile( srcPath, dstPath );
555 } else {
556 content = resourceData.txt;
557 PathUtils.file( dstPath, content );
558 }
559 }
560 }
561
562 // Copy modules' resources if any.
563 var moduleName;
564 // Path of the folder containing the resourses of the module (if any).
565 var resourcePath;
566 output.modules.forEach( function ( moduleName ) {
567 resourcePath = Project.srcOrLibPath( moduleName );
568 if ( resourcePath ) {
569 // Ok, this folder exists.
570 //console.info( "Copy resource: " + ( moduleName + "/" ).cyan );
571 var dst = Path.join( Path.dirname( output.filename ), moduleName.substr( 4 ) );
572 dst = dst.replace( /\\/g, '/' );
573 Project.copyFile( resourcePath, Project.wwwPath( 'css/' + dst ) );
574 }
575 } );
576}
577
578
579function concatDicValues( map ) {
580 if ( !map ) return '';
581 var key, out = '';
582 for ( key in map ) {
583 if ( out != '' ) out += "\n";
584 out += key;
585 }
586 return out;
587}
588
589
590function findHead( root ) {
591 if ( !root ) return null;
592
593 var head = Tree.getElementByName( root, "head" );
594 if ( !head ) {
595 // There is no <head> tag. Create it!
596 var html = Tree.getElementByName( root, "html" );
597 if ( !html ) {
598 html = {
599 type: Tree.TAG,
600 name: "html",
601 children: []
602 };
603 root.children.push( html );
604 }
605 head = {
606 type: Tree.TAG,
607 name: "head",
608 children: []
609 };
610 html.children.push( head );
611 }
612 return head;
613}
614
615
616function getInitJS( output ) {
617 var js = '';
618 var dynamicModule, code;
619 for ( dynamicModule in output.dynamicModules ) {
620 code = output.dynamicModules[ dynamicModule ];
621 js += code;
622 }
623 js += concatDicValues( output.initJS ) +
624 "\n" + concatDicValues( output.postInitJS );
625 if ( js.length > 0 ) {
626 return Tpl.file( "init.js", {
627 INIT_JS: js
628 } ).out;
629 }
630 return js;
631}
632
633
634function writeInnerCSS( innerCSS, pathCSS, nameWithoutExt, head, sourcemap ) {
635 if ( innerCSS.length > 0 ) {
636 // Add inner CSS file.
637 writeCSS( '@' + nameWithoutExt + ".css", innerCSS );
638 head.children.push( {
639 type: Tree.TAG,
640 name: 'link',
641 void: true,
642 attribs: {
643 rel: "stylesheet",
644 type: "text/css",
645 href: "css/@" + nameWithoutExt + ".css"
646 }
647 } );
648 }
649}
650
651
652function writeInnerJS( innerJS, pathJS, nameWithoutExt, head, sourcemap ) {
653 if ( innerJS.length > 0 ) {
654 // Add inner JS file.
655 writeJS( '@' + nameWithoutExt + ".js", innerJS );
656 head.children.push( {
657 type: Tree.TAG,
658 name: 'script',
659 attribs: {
660 defer: null,
661 src: "js/@" + nameWithoutExt + ".js"
662 }
663 } );
664 }
665}
666
667
668/**
669 * @param {object} output - Results of the HTML's compilation.
670 *
671 * @return {object} two attributes:
672 * * __js__: map of Javascript sources.
673 * * __css__: map of stylesheet sources.
674 */
675function combineRequires( output, options ) {
676 // The `cache` is used to prevent dependencies cycling. When a
677 // module has been processed, we add its name in the `cache`. Next
678 // time we find a module already in `cache` we will not process it.
679 var cache = {},
680 // dictionary of directly needed modules. The key is the module's name, the value is always `1`.
681 modules = output.require || {},
682 // List of modules' names we have to process.
683 fringe = [],
684 // Name of the current module.
685 moduleName,
686 // Style Sheet combined content.
687 css = '',
688 // Source file of the JS or CSS for the current module.
689 src,
690 // Dependencies of the current module's javascript.
691 dependencies,
692 // Map of Javascript sources. No compression.
693 jsFiles = {},
694 // Map of Stylesheet sources. No compression.
695 cssFiles = {},
696 // Iterator used for comments visual improvements.
697 i;
698
699 if ( !Array.isArray( output.modules ) ) output.modules = [];
700
701 // Fill the cache with all dynamic modules.
702 for ( moduleName in output.dynamicModules ) {
703 cache[ moduleName ] = 1;
704 }
705
706 // Always include the module `$` which was generated automatically.
707 modules[ 'mod/$' ] = 1;
708 // Fill the fringe with `modules`.
709 for ( moduleName in modules ) {
710 fringe.push( moduleName );
711 }
712
713 // Process all required modules by popping the next module's name from the `fringe`.
714 while ( fringe.length > 0 ) {
715 moduleName = fringe.pop(); // Pop the current module from the `fringe`.
716 cache[ moduleName ] = 1; // Don't process this module more than once.
717 if ( moduleName.substr( 0, 4 ) == 'cls/' ) {
718 // We have to include `tfw3.js` for backward compatibility.
719 output.innerJS[ Template.file( 'tfw3.js' ).out ] = 1;
720 } else if ( moduleName.substr( 0, 4 ) == 'mod/' ) {
721 // Remember all the modules used in this HTML page.
722 if ( output.modules.indexOf( moduleName ) < 0 ) {
723 output.modules.push( moduleName );
724 }
725 }
726 //============
727 // Javascript
728 //------------
729 // Compile (if not uptodate) the JS of the current module and
730 // return the source file.
731 src = CompilerJS.compile( Project, moduleName + ".js", options, output );
732 if ( !jsFiles[ moduleName ] ) {
733 jsFiles[ moduleName ] = {
734 src: src.tag( 'src' ),
735 zip: src.tag( 'zip' ),
736 map: src.tag( 'map' ),
737 dep: src.tag( "dependencies" )
738 };
739 }
740 // Adding dependencies to the `fringe`.
741 dependencies = src.tag( "dependencies" );
742 if ( Array.isArray( dependencies ) ) {
743 dependencies.forEach( function ( dep ) {
744 if ( !cache[ dep ] ) {
745 fringe.push( dep );
746 }
747 } );
748 }
749 //==============
750 // Style Sheets
751 //--------------
752 src = compileCSS( moduleName + ".css", options );
753 if ( src ) {
754 if ( !cssFiles[ moduleName ] ) {
755 cssFiles[ moduleName ] = {
756 src: src.tag( 'src' ),
757 zip: src.tag( 'zip' )
758 };
759 }
760 }
761 }
762
763 return {
764 js: jsFiles,
765 css: cssFiles
766 };
767}
768
769
770function writeJS( name, sourceZip, sourceMap ) {
771 if ( name.substr( -3 ) == '.js' ) {
772 name = name.substr( 0, name.length - 3 );
773 }
774 var path = Path.join( Project.wwwPath( "js" ), name + ".js" );
775 FS.writeFileSync( path, sourceZip );
776 if ( sourceMap ) {
777 path = Path.join( Project.wwwPath( "js" ), name + ".js.map" );
778 FS.writeFileSync( path, sourceMap );
779 }
780 // Look for resources.
781 var src = Project.srcOrLibPath( name );
782 if ( FS.existsSync( src ) ) {
783 var dst = Path.join( Project.wwwPath( "css" ), name );
784 Project.copyFile( src, dst );
785 }
786}
787
788
789function writeCSS( name, content, sourceMap ) {
790 if ( name.substr( -4 ) == '.css' ) {
791 name = name.substr( 0, name.length - 4 );
792 }
793 var path = Path.join( Project.wwwPath( "css" ), name + ".css" );
794 FS.writeFileSync( path, content );
795 if ( sourceMap ) {
796 path = Path.join( Project.wwwPath( "css" ), name + ".css.map" );
797 FS.writeFileSync( path, sourceMap );
798 }
799}
800
801
802function moduleExists( requiredModule ) {
803 var path = Project.srcOrLibPath( requiredModule + ".js" );
804 if ( path ) return true;
805 return false;
806}
807
808
809function minifyCSS( name, code, options ) {
810 var result = null;
811 if ( !code ) return null;
812
813 if ( code.trim().length == 0 ) {
814 // Empty CSS content.
815 console.log( " Warning! ".yellowBG.black + name + " is EMPTY!" );
816 return null; // {src: "", zip: ""};
817 }
818
819 try {
820 var css = Util.zipCSS( code );
821 result = {
822 src: code,
823 zip: css.styles,
824 map: css.sourceMap
825 };
826 } catch ( ex ) {
827 throw Error( "Unable to minify CSS \"" + name + "\":\n" + ex +
828 "\n\nCSS content was:\n" + code.substr( 0, 256 ) +
829 ( code.length > 256 ? '\n[...]' : '' ) );
830 }
831 return result;
832}
833
834/**
835 * @param {string} path Source path relative to the `src` folder.
836 */
837function compileCSS( path, options ) {
838 var absPath = Project.srcOrLibPath( path );
839 if ( !absPath ) return null;
840 var src = new Source( Project, path );
841 if ( !src.exists() ) return null;
842 if ( !src.isUptodate() ) {
843 console.log( "Compiling CSS " + path.yellow );
844 var cssCode = src.read();
845 //var multiBrowserCssCode = Rework( cssCode ).use( Vars({}) ).toString();
846 var multiBrowserCssCode = Rework( cssCode ).toString();
847 var minify = minifyCSS( src.name(), multiBrowserCssCode, options );
848 src.tag( 'src', cssCode );
849 src.tag( 'zip', minify.zip );
850 src.tag( 'map', minify.map );
851 src.save();
852 }
853 return src;
854}
855
856
857/**
858 * Add a prefix to a filename. This is not as simple as prepending the
859 * `prefix` to the string `path`, because `path` can contain folders'
860 * separators. The prefix must be prepended to the real file name and
861 * not to the whole path.
862 * Examples with `prefix` == "@":
863 * * `foobar.html`: `@foobar.html`
864 * * `myfolder/myfile.js`: `myfolder/@myfile.js`
865 */
866function addFilePrefix( path, prefix ) {
867 if ( typeof prefix === 'undefined' ) prefix = '@';
868
869 var separatorPosition = path.lastIndexOf( '/' );
870 if ( separatorPosition < 0 ) {
871 // Let's try with Windows separators.
872 separatorPosition = path.lastIndexOf( '\\' );
873 }
874 var filenameStart = separatorPosition > -1 ? separatorPosition + 1 : 0;
875 var result = path.substr( 0, filenameStart ) + prefix + path.substr( filenameStart );
876 return result.replace( /\\/g, '/' );
877}
878
879/**
880 * The depth of `path` is the number of subfolders it defines. For
881 * example, `foo.js' defined no subfolder and it is of depth 0. But
882 * `foo/bar/file.html` has two levels of subfolders hence it is of depth
883 * 2.
884 */
885function getBackToRoot( path ) {
886 // Counter for '/'.
887 var standardFolderSepCount = 0;
888 // Counter for '\' (windows folder separator).
889 var windowsFolderSepCount = 0;
890 // Loops index used for parsing chars of `path`and to add `../` to the result.
891 var i;
892 // Current char read from `path`.
893 var c;
894 // Counting folders' separators.
895 for ( i = 0; i < path.length; i++ ) {
896 c = path.charAt( i );
897 if ( c == '/' ) standardFolderSepCount++;
898 if ( c == '\\' ) windowsFolderSepCount++;
899 }
900 var folderSepCount = Math.max( standardFolderSepCount, windowsFolderSepCount );
901 if ( folderSepCount == 0 ) {
902 // There is no subfolder.
903 return '';
904 }
905
906 var result = '';
907 var folderSep = '/'; // windowsFolderSepCount > standardFolderSepCount ? '\\' : '/';
908 for ( i = 0; i < folderSepCount; i++ ) {
909 result += '..' + folderSep;
910 }
911 return result;
912}
913
914
915/**
916 * Add a description in the header if no one was found.
917 * @param {string} options.config.description - The description to use.
918 */
919function addDescriptionToHead( head, options ) {
920 if ( !options || !options.config || typeof options.config.description !== 'string' ) {
921 return false;
922 }
923
924 if ( !Array.isArray( head.children ) ) {
925 head.children = [];
926 }
927 for ( let i = 0; i < head.children.length; i++ ) {
928 const child = head.children[ i ];
929 if ( child.type !== Tree.ELEMENT ) continue;
930 if ( child.name.toLowerCase() != 'meta' ) continue;
931 if ( !child.attribs ) continue;
932 if ( typeof child.attribs.name !== 'string' ) continue;
933 if ( child.attribs.name.toLowerCase() === 'description' ) {
934 // There is already a description. We don't add a new one.
935 return false;
936 }
937 }
938 head.children.push( {
939 type: Tree.ELEMENT,
940 name: 'meta',
941 attribs: {
942 name: 'description',
943 content: options.config.description
944 }
945 } );
946 return true;
947}
948
949
950/**
951 * For `src/foobar.html`, creates a graphviz file `doc/foobar.dot`
952 * with all the dependencies of all modules used by `foobar.html`.
953 * @param {array} modules - `{ "tp4.trace-tools": { dep: [...] }, ... }`.
954 * @param {Source} source - HTML source file.
955 * @param {Project} project - Current project.
956 * @return {undefined}
957 */
958function makeDependenciesGraph( modules, source, project ) {
959 const
960 lines = Object.keys( modules ).map( ( moduleName ) => {
961 const
962 module = modules[ moduleName ],
963 moduleShortName = Util.removeFirstSubFolder( moduleName ),
964 mapper = function mapper( dependencyName ) {
965 return ` "${moduleShortName}" -> "${Util.removeFirstSubFolder(dependencyName)}"\n`;
966 };
967 return module.dep.map( mapper ).join( "\n" );
968 } ),
969 content = `digraph dependencies {\n${lines.join('')}\n}`,
970 destinationPath = project.docPath( `${source.name()}.dot` );
971 FS.writeFileSync( destinationPath, content );
972}