UNPKG

toloframework

Version:

Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.

973 lines (890 loc) 31.9 kB
"use strict"; module.exports = { compile, initialize }; /* * When compiling the file `main.html`, we gather all the CSS and JS files * needed. * * If `no-zip` option has not been set, all the CSS files are combined in * `css/@main.css` and all the JS files are combined in `js/@main.js`. * * # Modules * * Modules are stored in folder `mod/`. * * `mymodule/` * * `mymodule.js` * * `mymodule.xjs` * * `mymodule.ini` * * `mymodule.css` * * `mymodule.dep` */ const FS = require( "fs" ), Path = require( "path" ), ToloframeworkPermissiveJson = require( "toloframework-permissive-json" ), MinifyJS = require( "./minifyJS" ), CompilerCOM = require( "./compiler-com" ), CompilerJS = require( "./compiler-js" ), ParserHTML = require( "./tlk-htmlparser" ), PathUtils = require( "./pathutils" ), Template = require( "./template" ), Source = require( "./source" ), Fatal = require( "./fatal" ), Util = require( "./util" ), Tree = require( "./htmltree" ), Libs = require( "./compiler-com-libs" ), Tpl = require( "./template" ), Rework = require( 'rework' ); let Project = null, Components = {}, Scopes = [ {} ]; /** * @param {type} prj - Current project. * @returns {undefined} */ function initialize( prj ) { Project = prj; Components = {}; Scopes = [ {} ]; CompilerCOM.loadComponents( prj ); } /** * @param {string} file - Full path of the HTML file to compile. * @param {object} _options - Options for debug/release mode, etc. * @returns {object} Compiled html source code as an instance of Source. * @see source */ function compile( file, _options ) { const options = typeof _options !== 'undefined' ? _options : {}, sourceHTML = new Source( Project, file ), // Array of the sources we must link. sourcesToLink = []; let // Output of a page file. outputPage = '', /* * In case of multi-pages, the first page has the same name as the `file`. * So it's output must become the output of the `file`. */ outputOfFirstPage = '', // Page filename relative to the sourceHTML. pageFilename = ''; // Check if the file and all its components are uptodate. if ( !isUptodate( sourceHTML ) ) { Scopes[ 0 ].$filename = sourceHTML.name(); console.log( `Compile HTML: ${sourceHTML.name().cyan}` ); let root = ParserHTML.parse( sourceHTML.read() ); // Output of the main file. const output = compileRoot( root, sourceHTML, options ); if ( output ) { sourceHTML.tag( 'output', output ); while ( typeof root.type === 'undefined' && root.children && root.children.length === 1 ) { root = root.children[ 0 ]; } if ( root.type === Tree.PAGES ) { outputPage = ToloframeworkPermissiveJson.parse( JSON.stringify( output ) ); root.children.forEach( function ( child, idx ) { console.log( ( " Page " + ( idx + 1 ) ).cyan ); var src = sourceHTML; pageFilename = src.name(); if ( idx !== 0 ) { pageFilename = file.substr( 0, file.length - 4 ) + idx + '.html'; src = new Source( Project, pageFilename ); } outputPage = compileRoot( child, src, options, ToloframeworkPermissiveJson.parse( JSON.stringify( output ) ) ); outputPage.filename = pageFilename; src.tag( "output", outputPage ); src.save(); sourcesToLink.push( pageFilename ); if ( idx === 0 ) { // This is the first page. outputOfFirstPage = outputPage; } } ); sourceHTML.tag( 'pages', sourcesToLink ); sourceHTML.tag( 'output', outputOfFirstPage ); } sourceHTML.save(); } } // Linking. linkPages( sourceHTML, options ); return sourceHTML; } /** * @param {Source} sourceHTML - Html source instance. * @param {object} options - Options for debug/release mode, etc. * @returns {undefined} */ function linkPages( sourceHTML, options ) { console.log( `Link: ${sourceHTML.name().yellow}` ); const sourcesToLink = sourceHTML.tag( 'pages' ); if ( sourcesToLink ) { // Multi-pages. sourcesToLink.forEach( function forEachPage( pageFilename ) { const src = new Source( Project, pageFilename ); link( src, options ); } ); } else { // Single page. link( sourceHTML, options ); } } /** * @param {object} root - HTML tree. * @param {Source} sourceHTML - Source object represnting the HTML file. * @param {object} options - Build options for release/debug, etc. * @param {object} _output - Stuff used internally in the recursive process to create the final Html file. * @see tlk-htmlparser */ function compileRoot( root, sourceHTML, options, _output ) { // Stuff to create HTML file. const output = typeof _output !== 'undefined' ? _output : { // Code CSS. innerCSS: {}, // CSS files. outerCSS: {}, // Code Javascript to embed in `js/@index.js` file. innerJS: {}, // Javascript modules directly required. require: {}, // All the Javascript modules needed to build this page. modules: [], // Javascript code to insert in a `DOMContentLoaded` event. initJS: {}, // Javascript code to insert in a `DOMContentLoaded` event after all the code of `initJS`. postInitJS: {}, // Files needed to build this file. If a include change, we must recompile. include: {}, // A resource is a file to create in the output folder when this HTML is linked. // the key is the resource name, and the value is an objet depending on the type of resource: // * {dst: "img/plus.png", src: "../gfx/icon-plus.png"} // * {dst: "img/face.svg", txt: "<svg xmlns:svg=..."} resource: {}, // Modules that have no real file in `mod` directory, but // which are created dynamically in Components. dynamicModules: {} }; var libs = Libs( sourceHTML, Components, Scopes, output ); libs.compile( root, options ); Tree.trim( root ); output.root = root; return output; } /** * A source needs to be rebuild if it is not uptodate. * Here are the reasons for a source not to be uptodate: * * Source code more recent than the tags (`Source.isUptodate()`). * * Includes source codes more recent than this source. * @param {Source} sourceHTML - Html source file. */ function isUptodate( sourceHTML ) { console.info("[compiler-html2] sourceHTML=", sourceHTML.name()); if ( !sourceHTML.isUptodate() ) return false; const output = sourceHTML.tag( 'output' ); if ( !output || !Array.isArray( output.include ) ) return false; // Modification time for the current HTML file. const currentFileModificationTime = sourceHTML.modificationTime(); // `includeFilename` is the File name relative to `sourceHTML`. for ( const includeFilename of output.include ) { // Source object of the included file. const includeSource = new Source( Project, sourceHTML.getPathRelativeToSource( includeFilename ) ); const stats = FS.statSync( includeSource.getAbsoluteFilePath() ); if ( stats.mtime > currentFileModificationTime ) { // An included file if more recent. We must recompile. return false; } } return true; } /** * Take a HTML file `filename.html` and combine all the styles in * `css/@filename.css` and all the javascripts in `js/@filename.js`. * For each module, check if there is a folder with the same name. If * yes, copy that resource in `css` dir. For example, it you have * `mod/foobar.js` and a folder `mod/foobar/`, copy it to * `www/css/foobar/`. */ function link( src, options ) { const htmlDir = Path.dirname( src.name() ), pathWWW = Project.wwwPath( htmlDir ), pathJS = Path.join( pathWWW, "js" ), pathCSS = Path.join( pathWWW, "css" ); Project.mkdir( pathJS ); Project.mkdir( pathCSS ); const output = linkForRelease( src, pathJS, pathCSS, options, htmlDir ); PathUtils.file( Project.wwwPath( src.name() ), `<!DOCTYPE html>\n${Tree.toString(output.root).trim()}` ); // Writing resources if any. writeResources( output ); } /** * */ function linkForRelease( src, pathJS, pathCSS, options, htmlDir ) { Project.mkdir( Path.join( pathJS, "map" ) ); Project.mkdir( Path.join( pathCSS, "map" ) ); var key, val; var prj = src.prj(); var nameWithoutExt = src.name().substr( 0, src.name().length - 5 ); // If `nameWithoutExt` is in a subfolder, `backToRoot` must containt // as many `../` as there are subfolders in `nameWithoutExt`. var backToRoot = getBackToRoot( nameWithoutExt ); backToRoot = ''; // Finalement les dépendances sont au même niveau que le fichier HTML. var output = src.tag( "output" ) || {}; var root = output.root; if ( !root ) { Fatal.fire( "The cache seems to be corrupted. Try `tfw clean` to clean it up. " + "And try building again.", "Please cleanup the cache!" ); } output.filename = src.name(); var head = findHead( root ); addDescriptionToHead( head, options ); var innerJS = Tpl.file( "require.js" ).out + concatDicValues( output.innerJS ); innerJS += getInitJS( output ); var innerCSS = concatDicValues( output.innerCSS ); // If there is a CSS file with the same name as the HTML file, embed it. if ( FS.existsSync( Project.srcOrLibPath( nameWithoutExt + '.css' ) ) ) { console.log( "Found: " + ( nameWithoutExt + ".css" ) ); innerCSS += PathUtils.file( Project.srcOrLibPath( nameWithoutExt + '.css' ) ); } function addInnerJS() { // Adding innerJS. const zippedJS = MinifyJS.minify( { name: src.name(), content: innerJS } ).zip; prj.flushContent( "js/" + addFilePrefix( nameWithoutExt ) + ".js", zippedJS, htmlDir ); head.children.push( { type: Tree.TAG, name: 'script', attribs: { defer: null, src: backToRoot + "js/" + addFilePrefix( nameWithoutExt ) + ".js" } } ); } const combination = combineRequires( output, options ); makeDependenciesGraph( combination.js, src, Project ); var externalDeps = lookForExternalDependencies( combination.js ); externalDeps.js.forEach( function ( code ) { innerJS += code; } ); for ( key in externalDeps.res ) { val = externalDeps.res[ key ]; try { Project.copyFile( key, val ); } catch ( ex ) { throw Error( "Unable to copy external dependency `" + key + "` into `" + val + "`!\n" + JSON.stringify( externalDeps, null, ' ' ) ); } } // Used to loop over CSS and JS files. if ( options.dev ) { addInnerJS(); // DEBUG. Do not combine. for ( key in combination.css ) { val = combination.css[ key ]; if ( key.substr( 0, 4 ) == 'mod/' ) { key = key.substr( 4 ); } head.children.push( { type: Tree.TAG, name: 'link', void: true, attribs: { rel: "stylesheet", type: "text/css", href: backToRoot + "css/" + key + ".css" } } ); prj.flushContent( "css/" + key + ".css", val.src, htmlDir ); } for ( key in combination.js ) { val = combination.js[ key ]; if ( key.substr( 0, 4 ) == 'mod/' ) { key = key.substr( 4 ); } head.children.push( { type: Tree.TAG, name: 'script', attribs: { defer: null, src: backToRoot + "js/" + key + ".js" } } ); prj.flushContent( "js/" + key + ".js", val.src, htmlDir ); prj.flushContent( "js/" + key + ".js.map", JSON.stringify( val.map ), htmlDir ); } } else { // RELEASE. for ( key in combination.css ) { val = combination.css[ key ]; innerCSS += val.zip; } for ( key in combination.js ) { val = combination.js[ key ]; innerJS += val.zip + "\n"; } addInnerJS(); } // Adding innerCSS. prj.flushContent( "css/" + addFilePrefix( nameWithoutExt ) + ".css", innerCSS, htmlDir ); head.children.push( { type: Tree.TAG, name: 'link', void: true, attribs: { rel: "stylesheet", type: "text/css", href: backToRoot + "css/" + addFilePrefix( nameWithoutExt ) + ".css" } } ); return output; } /** * Look for any "*.dep" file for all the neededmodules. * * @param {array} jsFiles - Array of the JS files of each module. * @returns {undefined} */ function lookForExternalDependencies( jsFiles ) { const javascriptSources = [], resources = {}; for ( const jsFileName of Object.keys( jsFiles ) ) { const depFileName = `${jsFileName}.dep`; if ( Project.srcOrLibPath( depFileName ) ) { const depFile = new Source( Project, depFileName ); try { const json = depFile.read(), dependencies = ToloframeworkPermissiveJson.parse( json ); // Looking for Javascript dependencies. lookForExternalDependenciesJS( dependencies.js, javascriptSources ); // Looking for other dependencies. lookForExternalDependenciesRES( dependencies.res, resources, depFile ); } catch ( ex ) { Fatal.fire( `Unable to parse JSON file "${depFile.getAbsoluteFilePath()}"!\n${ex}`, Project.srcOrLibPath( jsFileName ) ); } } } return { js: javascriptSources, res: resources }; } /** * Section "res" in a dependency file. * * @param {objects} _def - `{ "bob/foo.png": "", "yo/man.kml": "maps/man.kml" }` * @param {object} resources - * @param {Source} depFile - Source of the dependency file. * @returns {undefined} */ function lookForExternalDependenciesRES( _def, resources, depFile ) { if ( !_def ) return; try { const def = convertExternalDependencyDefinition( _def ); for ( const dep of Object.keys( def ) ) { if ( def[ dep ] === "" ) { // `res: { "bob/foo.png": "" }` is equivalent to // `res: { "bob/foo.png": "bob/foo.png" }` def[ dep ] = dep; } const srcDep = Project.srcOrLibPath( `mod/${dep}` ) || Project.srcOrLibPath( dep ); if ( !srcDep ) { Fatal.fire( `Unable to find dependency file "${dep}" nor "mod/${dep}"!`, depFile.getAbsoluteFilePath() ); } resources[ srcDep ] = Project.wwwPath( def[ dep ] ); } } catch ( ex ) { Fatal.fire( `Unable to parse RES dependencies: ${JSON.stringify(_def)}!\n${ex}`, depFile ); } } /** * Section "js" in a dependency file. * * @param {objects} _def - `{ "helper.js": "" }` or `"helper.js"`. * @param {array} javascriptSources - Array of the Javascript code to include in current HTML page. * @returns {undefined} */ function lookForExternalDependenciesJS( _def, javascriptSources ) { if ( !_def ) return; try { const def = convertExternalDependencyDefinition( _def ); Object.keys( def ).forEach( function ( js ) { const filename = `mod/${js}`, src = new Source( Project, filename ), code = src.read(); pushUnique( javascriptSources, code ); } ); } catch ( ex ) { Fatal.fire( `Unable to parse JS dependencies: ${JSON.stringify(_def)}!\n${ex}`, javascriptSources ); } } /** * There are three ways to define a list of external dependencies: * * "foo.js" * * ["foo.js", "bar.js"] * * { "foo.js": "libs/foo.js", ... } * * This function transforms a definition in its third form: `{ "foo.js": "libs/foo.js", ... }`. * * @param {any} def - Definition of the dependencies. * @returns {object} Definition in the third form. */ function convertExternalDependencyDefinition( def ) { if ( typeof def === 'string' ) { const defAsString = {}; defAsString[ def ] = ""; return defAsString } if ( Array.isArray( def ) ) { const defAsArray = {}; def.forEach( function ( file ) { defAsString[ file ] = ""; } ); } return def; } /** * Push `item` into `arr` if it is not already in. */ function pushUnique( arr, item ) { if ( arr.indexOf( item ) > -1 ) return false; arr.push( item ); return true; } function writeResources( output ) { // Name of the resource. var resourceName; // Data of the resource. var resourceData; // Destination path (in `www`folder). var dstPath; // Source path (in `src` folder). var srcPath; // Resource content. var content; for ( resourceName in output.resource ) { resourceData = output.resource[ resourceName ]; dstPath = Project.wwwPath( resourceData.dst ); if ( PathUtils.isDirectory( dstPath ) ) { // We must copy a whole directory. } else { // Create folders if needed. Project.mkdir( Path.dirname( dstPath ) ); if ( resourceData.src ) { srcPath = Project.srcOrLibPath( resourceData.src ); Project.copyFile( srcPath, dstPath ); } else { content = resourceData.txt; PathUtils.file( dstPath, content ); } } } // Copy modules' resources if any. var moduleName; // Path of the folder containing the resourses of the module (if any). var resourcePath; output.modules.forEach( function ( moduleName ) { resourcePath = Project.srcOrLibPath( moduleName ); if ( resourcePath ) { // Ok, this folder exists. //console.info( "Copy resource: " + ( moduleName + "/" ).cyan ); var dst = Path.join( Path.dirname( output.filename ), moduleName.substr( 4 ) ); dst = dst.replace( /\\/g, '/' ); Project.copyFile( resourcePath, Project.wwwPath( 'css/' + dst ) ); } } ); } function concatDicValues( map ) { if ( !map ) return ''; var key, out = ''; for ( key in map ) { if ( out != '' ) out += "\n"; out += key; } return out; } function findHead( root ) { if ( !root ) return null; var head = Tree.getElementByName( root, "head" ); if ( !head ) { // There is no <head> tag. Create it! var html = Tree.getElementByName( root, "html" ); if ( !html ) { html = { type: Tree.TAG, name: "html", children: [] }; root.children.push( html ); } head = { type: Tree.TAG, name: "head", children: [] }; html.children.push( head ); } return head; } function getInitJS( output ) { var js = ''; var dynamicModule, code; for ( dynamicModule in output.dynamicModules ) { code = output.dynamicModules[ dynamicModule ]; js += code; } js += concatDicValues( output.initJS ) + "\n" + concatDicValues( output.postInitJS ); if ( js.length > 0 ) { return Tpl.file( "init.js", { INIT_JS: js } ).out; } return js; } function writeInnerCSS( innerCSS, pathCSS, nameWithoutExt, head, sourcemap ) { if ( innerCSS.length > 0 ) { // Add inner CSS file. writeCSS( '@' + nameWithoutExt + ".css", innerCSS ); head.children.push( { type: Tree.TAG, name: 'link', void: true, attribs: { rel: "stylesheet", type: "text/css", href: "css/@" + nameWithoutExt + ".css" } } ); } } function writeInnerJS( innerJS, pathJS, nameWithoutExt, head, sourcemap ) { if ( innerJS.length > 0 ) { // Add inner JS file. writeJS( '@' + nameWithoutExt + ".js", innerJS ); head.children.push( { type: Tree.TAG, name: 'script', attribs: { defer: null, src: "js/@" + nameWithoutExt + ".js" } } ); } } /** * @param {object} output - Results of the HTML's compilation. * * @return {object} two attributes: * * __js__: map of Javascript sources. * * __css__: map of stylesheet sources. */ function combineRequires( output, options ) { // The `cache` is used to prevent dependencies cycling. When a // module has been processed, we add its name in the `cache`. Next // time we find a module already in `cache` we will not process it. var cache = {}, // dictionary of directly needed modules. The key is the module's name, the value is always `1`. modules = output.require || {}, // List of modules' names we have to process. fringe = [], // Name of the current module. moduleName, // Style Sheet combined content. css = '', // Source file of the JS or CSS for the current module. src, // Dependencies of the current module's javascript. dependencies, // Map of Javascript sources. No compression. jsFiles = {}, // Map of Stylesheet sources. No compression. cssFiles = {}, // Iterator used for comments visual improvements. i; if ( !Array.isArray( output.modules ) ) output.modules = []; // Fill the cache with all dynamic modules. for ( moduleName in output.dynamicModules ) { cache[ moduleName ] = 1; } // Always include the module `$` which was generated automatically. modules[ 'mod/$' ] = 1; // Fill the fringe with `modules`. for ( moduleName in modules ) { fringe.push( moduleName ); } // Process all required modules by popping the next module's name from the `fringe`. while ( fringe.length > 0 ) { moduleName = fringe.pop(); // Pop the current module from the `fringe`. cache[ moduleName ] = 1; // Don't process this module more than once. if ( moduleName.substr( 0, 4 ) == 'cls/' ) { // We have to include `tfw3.js` for backward compatibility. output.innerJS[ Template.file( 'tfw3.js' ).out ] = 1; } else if ( moduleName.substr( 0, 4 ) == 'mod/' ) { // Remember all the modules used in this HTML page. if ( output.modules.indexOf( moduleName ) < 0 ) { output.modules.push( moduleName ); } } //============ // Javascript //------------ // Compile (if not uptodate) the JS of the current module and // return the source file. src = CompilerJS.compile( Project, moduleName + ".js", options, output ); if ( !jsFiles[ moduleName ] ) { jsFiles[ moduleName ] = { src: src.tag( 'src' ), zip: src.tag( 'zip' ), map: src.tag( 'map' ), dep: src.tag( "dependencies" ) }; } // Adding dependencies to the `fringe`. dependencies = src.tag( "dependencies" ); if ( Array.isArray( dependencies ) ) { dependencies.forEach( function ( dep ) { if ( !cache[ dep ] ) { fringe.push( dep ); } } ); } //============== // Style Sheets //-------------- src = compileCSS( moduleName + ".css", options ); if ( src ) { if ( !cssFiles[ moduleName ] ) { cssFiles[ moduleName ] = { src: src.tag( 'src' ), zip: src.tag( 'zip' ) }; } } } return { js: jsFiles, css: cssFiles }; } function writeJS( name, sourceZip, sourceMap ) { if ( name.substr( -3 ) == '.js' ) { name = name.substr( 0, name.length - 3 ); } var path = Path.join( Project.wwwPath( "js" ), name + ".js" ); FS.writeFileSync( path, sourceZip ); if ( sourceMap ) { path = Path.join( Project.wwwPath( "js" ), name + ".js.map" ); FS.writeFileSync( path, sourceMap ); } // Look for resources. var src = Project.srcOrLibPath( name ); if ( FS.existsSync( src ) ) { var dst = Path.join( Project.wwwPath( "css" ), name ); Project.copyFile( src, dst ); } } function writeCSS( name, content, sourceMap ) { if ( name.substr( -4 ) == '.css' ) { name = name.substr( 0, name.length - 4 ); } var path = Path.join( Project.wwwPath( "css" ), name + ".css" ); FS.writeFileSync( path, content ); if ( sourceMap ) { path = Path.join( Project.wwwPath( "css" ), name + ".css.map" ); FS.writeFileSync( path, sourceMap ); } } function moduleExists( requiredModule ) { var path = Project.srcOrLibPath( requiredModule + ".js" ); if ( path ) return true; return false; } function minifyCSS( name, code, options ) { var result = null; if ( !code ) return null; if ( code.trim().length == 0 ) { // Empty CSS content. console.log( " Warning! ".yellowBG.black + name + " is EMPTY!" ); return null; // {src: "", zip: ""}; } try { var css = Util.zipCSS( code ); result = { src: code, zip: css.styles, map: css.sourceMap }; } catch ( ex ) { throw Error( "Unable to minify CSS \"" + name + "\":\n" + ex + "\n\nCSS content was:\n" + code.substr( 0, 256 ) + ( code.length > 256 ? '\n[...]' : '' ) ); } return result; } /** * @param {string} path Source path relative to the `src` folder. */ function compileCSS( path, options ) { var absPath = Project.srcOrLibPath( path ); if ( !absPath ) return null; var src = new Source( Project, path ); if ( !src.exists() ) return null; if ( !src.isUptodate() ) { console.log( "Compiling CSS " + path.yellow ); var cssCode = src.read(); //var multiBrowserCssCode = Rework( cssCode ).use( Vars({}) ).toString(); var multiBrowserCssCode = Rework( cssCode ).toString(); var minify = minifyCSS( src.name(), multiBrowserCssCode, options ); src.tag( 'src', cssCode ); src.tag( 'zip', minify.zip ); src.tag( 'map', minify.map ); src.save(); } return src; } /** * Add a prefix to a filename. This is not as simple as prepending the * `prefix` to the string `path`, because `path` can contain folders' * separators. The prefix must be prepended to the real file name and * not to the whole path. * Examples with `prefix` == "@": * * `foobar.html`: `@foobar.html` * * `myfolder/myfile.js`: `myfolder/@myfile.js` */ function addFilePrefix( path, prefix ) { if ( typeof prefix === 'undefined' ) prefix = '@'; var separatorPosition = path.lastIndexOf( '/' ); if ( separatorPosition < 0 ) { // Let's try with Windows separators. separatorPosition = path.lastIndexOf( '\\' ); } var filenameStart = separatorPosition > -1 ? separatorPosition + 1 : 0; var result = path.substr( 0, filenameStart ) + prefix + path.substr( filenameStart ); return result.replace( /\\/g, '/' ); } /** * The depth of `path` is the number of subfolders it defines. For * example, `foo.js' defined no subfolder and it is of depth 0. But * `foo/bar/file.html` has two levels of subfolders hence it is of depth * 2. */ function getBackToRoot( path ) { // Counter for '/'. var standardFolderSepCount = 0; // Counter for '\' (windows folder separator). var windowsFolderSepCount = 0; // Loops index used for parsing chars of `path`and to add `../` to the result. var i; // Current char read from `path`. var c; // Counting folders' separators. for ( i = 0; i < path.length; i++ ) { c = path.charAt( i ); if ( c == '/' ) standardFolderSepCount++; if ( c == '\\' ) windowsFolderSepCount++; } var folderSepCount = Math.max( standardFolderSepCount, windowsFolderSepCount ); if ( folderSepCount == 0 ) { // There is no subfolder. return ''; } var result = ''; var folderSep = '/'; // windowsFolderSepCount > standardFolderSepCount ? '\\' : '/'; for ( i = 0; i < folderSepCount; i++ ) { result += '..' + folderSep; } return result; } /** * Add a description in the header if no one was found. * @param {string} options.config.description - The description to use. */ function addDescriptionToHead( head, options ) { if ( !options || !options.config || typeof options.config.description !== 'string' ) { return false; } if ( !Array.isArray( head.children ) ) { head.children = []; } for ( let i = 0; i < head.children.length; i++ ) { const child = head.children[ i ]; if ( child.type !== Tree.ELEMENT ) continue; if ( child.name.toLowerCase() != 'meta' ) continue; if ( !child.attribs ) continue; if ( typeof child.attribs.name !== 'string' ) continue; if ( child.attribs.name.toLowerCase() === 'description' ) { // There is already a description. We don't add a new one. return false; } } head.children.push( { type: Tree.ELEMENT, name: 'meta', attribs: { name: 'description', content: options.config.description } } ); return true; } /** * For `src/foobar.html`, creates a graphviz file `doc/foobar.dot` * with all the dependencies of all modules used by `foobar.html`. * @param {array} modules - `{ "tp4.trace-tools": { dep: [...] }, ... }`. * @param {Source} source - HTML source file. * @param {Project} project - Current project. * @return {undefined} */ function makeDependenciesGraph( modules, source, project ) { const lines = Object.keys( modules ).map( ( moduleName ) => { const module = modules[ moduleName ], moduleShortName = Util.removeFirstSubFolder( moduleName ), mapper = function mapper( dependencyName ) { return ` "${moduleShortName}" -> "${Util.removeFirstSubFolder(dependencyName)}"\n`; }; return module.dep.map( mapper ).join( "\n" ); } ), content = `digraph dependencies {\n${lines.join('')}\n}`, destinationPath = project.docPath( `${source.name()}.dot` ); FS.writeFileSync( destinationPath, content ); }