UNPKG

reclass-doc

Version:

Reclass model documentation generator.

857 lines (856 loc) 33.5 kB
"use strict"; /** * Reclass doc generator * * @author Jiri Hybek <jiri@hybek.cz> * @license Apache-2.0 (c) 2017 Jiri Hybek */ Object.defineProperty(exports, "__esModule", { value: true }); var fs = require("fs"); var fsExtra = require("fs-extra"); var pug = require("pug"); var markdownIt = require("markdown-it"); var hljs = require("highlight.js"); var Resolver_1 = require("./Resolver"); var YamlTokenizer_1 = require("./YamlTokenizer"); var Util_1 = require("./Util"); /** * Renderer class */ var Renderer = (function () { /** * Renderer constructor * * @param config Renderer config * @param logger Logger facility */ function Renderer(config, inventory, logger) { /** Inventory item cache - path => fingerprint */ this.cache = {}; this.tplCache = {}; this.outputDir = config.outputDir; this.classesDir = config.classesDir || '/classes'; this.nodesDir = config.nodesDir || '/nodes'; this.sourcePostfix = config.sourcePostfix || '.source'; this.templateDir = config.templateDir; this.assetsSrcDir = config.assetsSrcDir || '/assets/dist'; this.assetsOutDir = config.assetsOutDir || '/assets'; this.mediaSrcDir = config.mediaSrcDir || '/media'; this.mediaOutDir = config.mediaOutDir || '/media'; this.globals = config.globals || {}; this.options = config.options || { pretty: true }; this.globals.siteTitle = config.title || 'Reclass reference'; this.globals.siteLogo = config.logoUrl || null; this.inventory = inventory; this.logger = logger; this.markdown = new markdownIt({ highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(lang, str).value; } catch (__) { } } return ''; // use external default escaping } }); } /** * Filters output html * * @param html Rendered html * @param path Current path */ Renderer.prototype.filterOutput = function (html, path) { var _path = path.split("/"); var basePathParts = []; if (path != "") { _path.pop(); for (var i in _path) if (_path[i].trim() != "") basePathParts.push(".."); } var basePath = basePathParts.length > 0 ? "./" + basePathParts.join("/") : "."; html = html.replace(/{{base}}/g, basePath); html = html.replace(/{{media}}/g, basePath + "/" + this.mediaOutDir); return html; }; /** * Renders template * * @param templateName Template name * @param locals Local vars */ Renderer.prototype.renderTpl = function (templateName, locals) { if (locals === void 0) { locals = {}; } var tpl = this.tplCache[templateName]; if (!tpl) tpl = this.tplCache[templateName] = pug.compileFile(this.templateDir + "/" + templateName + ".pug", this.options); return tpl(Util_1.merge(this.globals, locals)); }; /** * Generates link to class page * * @param className Class name */ Renderer.prototype.getClassLink = function (className) { var prefixDir = (className.type == Util_1.CLASS_TYPE.NODE ? this.nodesDir : this.classesDir); var classPath, localName; if (className.type == Util_1.CLASS_TYPE.CLASS) { classPath = className.fullName.split("."); localName = classPath.pop(); } else { classPath = className.fullName.split("/"); localName = classPath.pop(); } return "{{base}}" + prefixDir + (classPath.length > 0 ? "/" + classPath.join("/") : "") + (className.isInit ? "/" + localName + "/index.html" : "/class." + localName + ".html"); }; /** * Generates link to class source page * * @param className Class name */ Renderer.prototype.getClassSourceLink = function (className) { var prefixDir = (className.type == Util_1.CLASS_TYPE.NODE ? this.nodesDir : this.classesDir); var classPath, localName; if (className.type == Util_1.CLASS_TYPE.CLASS) { classPath = className.fullName.split("."); localName = classPath.pop(); } else { classPath = className.fullName.split("/"); localName = classPath.pop(); } return "{{base}}" + prefixDir + (classPath.length > 0 ? "/" + classPath.join("/") : "") + (className.isInit ? "/" + localName + "/init.source.html" : "/class." + localName + ".source.html"); }; /** * Generates document link * * @param path Directory path * @param name Document name */ Renderer.prototype.getDocumentLink = function (path, name) { return "{{base}}" + path + "/doc." + name + ".html"; }; /** * Generates document link * * @param path Directory path * @param name Document name */ Renderer.prototype.getDocumentSourceLink = function (path) { return "{{base}}" + path + ".source.html"; }; /** * Generates link to directory index * * @param path Path */ Renderer.prototype.getDirectoryLink = function (path) { return "{{base}}" + path + "/index.html"; }; /** * Prepares navigation tree for template * * @param index Inventory index * @param path Relative path */ Renderer.prototype.prepareTree = function (index, path) { if (path === void 0) { path = ''; } //Create nav item var item = { type: (index.classes['init'] ? 'class' : 'directory'), label: index.name, link: "{{base}}" + path + "/index.html", id: "dir:" + path, items: [], flags: {}, fulltext: [index.name, path, path.replace(/\//g, ' ')].join(" ") }; if ((index.classes['init'] && index.classes['init'].error) || (index.docs['README'] && index.docs['README'].error)) item.flags['error'] = true; //Add classes for (var i in index.classes) if (i !== 'init') item.items.push({ type: 'class', label: index.classes[i].name.name, link: this.getClassLink(index.classes[i].name), id: "class:" + index.classes[i].name.fullName, items: null, flags: (index.classes[i].error ? { "error": true } : {}), fulltext: [index.classes[i].name.fullName, index.classes[i].name.fullName.replace(/(\.|_)/g, ' '), path, path.replace(/\//g, ' ')].join(" ") }); for (var i in index.docs) if (i !== 'README') item.items.push({ type: 'document', label: index.docs[i].name, link: this.getDocumentLink(path, index.docs[i].name), id: "doc:" + path + "/doc." + index.docs[i].name, items: null, flags: (index.docs[i].error ? { "error": true } : {}), fulltext: [index.docs[i].name, index.docs[i].name.replace(/(\.|_)/g, ' '), path, path.replace(/\//g, ' ')].join(" ") }); for (var i in index.dirs) item.items.push(this.prepareTree(index.dirs[i], path + "/" + index.dirs[i].name)); //Sort item.items.sort(function (a, b) { return a.label.localeCompare(b.label); }); //Return return item; }; /** * Prepares document section * * @param doc Inventory document * @param path Current path */ Renderer.prototype.prepareDocument = function (doc, path) { var section = { title: doc.name, type: 'document', contents: null, sourceLink: (doc.contents ? this.getDocumentSourceLink(path) : null), errors: (doc.error ? [doc.error] : []) }; section.contents = this.markdown.render(String(doc.contents)); return section; }; /** * Prepares class section * * @param class Inventory class * @param path Current path */ Renderer.prototype.prepareClass = function (iClass, path) { var _this = this; //Create section var section = { title: iClass.name.fullName, class: null, type: 'class', sourceLink: this.getClassSourceLink(iClass.name), errors: iClass.error ? [iClass.error] : [] }; //Skip if class was not resolved if (!iClass.class) return section; //Create class var _class = { className: iClass.name.fullName, dependencies: [], dependents: [], applications: [], props: null }; //Add dependencies var createClassLink = function (dClass) { var _ref = _this.inventory.getClassRef(dClass.name); var _link = { className: dClass.name, link: (_ref ? _this.getClassLink(_ref.name) : null), dependencies: [], flags: (dClass.error ? { error: true } : {}) }; for (var i = 0; i < dClass.classes.length; i++) _link.dependencies.push(createClassLink(dClass.classes[i])); return _link; }; for (var i = 0; i < iClass.class.classes.length; i++) _class.dependencies.push(createClassLink(iClass.class.classes[i])); //Add dependants for (var i in iClass.class.dependents) { var _ref = void 0; if (iClass.class.dependents[i].type == Util_1.CLASS_TYPE.CLASS) _ref = this.inventory.getClassRef(iClass.class.dependents[i].name); else _ref = this.inventory.getNodeRef(iClass.class.dependents[i].name); _class.dependents.push({ className: iClass.class.dependents[i].name, link: (_ref ? this.getClassLink(_ref.name) : null), dependencies: [], flags: {} }); } //Add applications for (var i in iClass.class.applications) { var _app = iClass.class.applications[i]; var app = { name: i, sources: [], comment: [], ownProp: false }; for (var j = 0; j < _app.sources.length; j++) { var _ref = void 0; if (_app.sources[j].classType == Util_1.CLASS_TYPE.CLASS) _ref = this.inventory.getClassRef(_app.sources[j].className); else _ref = this.inventory.getNodeRef(_app.sources[j].className); app.sources.push({ className: _app.sources[j].className, classLink: (_ref ? this.getClassLink(_ref.name) : null), sourceLink: (_ref ? this.getClassSourceLink(_ref.name) : null), comment: _app.sources[j].comment.join("\n") }); if (_app.sources[j].comment.length > 0) app.comment.unshift(_app.sources[j].comment.join("\n")); if (j === _app.sources.length - 1) { if (_ref == iClass) app.ownProp = true; } } _class.applications.unshift(app); } //Prepare props var type2str = function (type) { switch (type) { case YamlTokenizer_1.TOKEN_TYPE.MAP: return 'map'; case YamlTokenizer_1.TOKEN_TYPE.SEQUENCE: return 'sequence'; case YamlTokenizer_1.TOKEN_TYPE.VALUE: return 'value'; } }; var merge2str = function (type) { switch (type) { case Resolver_1.MERGE_TYPE.MERGED: return 'merged'; case Resolver_1.MERGE_TYPE.REPLACED: return 'replaced'; case Resolver_1.MERGE_TYPE.ORIGIN: return 'origin'; } }; var prepareProp = function (name, param, prefix) { if (prefix === void 0) { prefix = null; } //Prepare prop var prop = { id: (prefix !== null ? prefix + ":" : "") + (name !== null ? name : "param"), name: name, type: type2str(param.type), ownProp: false, value: null, ref: null, comment: [], sources: [], fulltext: null }; var fulltext = [prop.id.substr(6).replace(/\:/g, '.')]; if (prop.name) { fulltext.push(prop.name); fulltext.push(prop.name.replace(/_/g, ' ')); } //Add sources for (var i = 0; i < param.sources.length; i++) { var _source = param.sources[i]; var _sourceRef = void 0; if (_source.classType === Util_1.CLASS_TYPE.CLASS) _sourceRef = _this.inventory.getClassRef(_source.className); else _sourceRef = _this.inventory.getNodeRef(_source.className); var source = { className: _source.className, classLink: (_sourceRef ? _this.getClassLink(_sourceRef.name) + "#" + prop.id : null), sourceLink: (_sourceRef ? _this.getClassSourceLink(_sourceRef.name) + "#line:" + (_source.token.line + 1) : null), mergeType: merge2str(_source.mergeType), type: type2str(_source.type), value: _source.value, comment: _source.comment.join("\n") }; prop.sources.unshift(source); if (prop.comment.indexOf(source.comment) < 0) prop.comment.unshift(source.comment); if (i === param.sources.length - 1) { if (_sourceRef == iClass) prop.ownProp = true; } if (fulltext.indexOf(source.value) < 0) fulltext.push(source.value); if (fulltext.indexOf(source.comment) < 0) fulltext.push(source.comment); } //Set value if (param.value !== null) { if (param.type === YamlTokenizer_1.TOKEN_TYPE.MAP) { prop.value = {}; for (var i in param.value) prop.value[i] = prepareProp(i, param.value[i], prop.id); } else if (param.type === YamlTokenizer_1.TOKEN_TYPE.SEQUENCE) { prop.value = new Array(param.value.length); for (var i = 0; i < param.value.length; i++) prop.value[i] = prepareProp(String(i), param.value[i], prop.id); } else if (String(param.value).substr(0, 7) == "#!ref!#") { prop.type = 'ref'; prop.value = "#param:" + String(param.value).substr(7); fulltext.push(String(param.value).substr(7)); } else { prop.value = param.value; fulltext.push(prop.value); } } //Set refs if (param.ref) { prop.ref = []; for (var i = 0; i < param.ref.length; i++) { prop.ref.push({ name: param.ref[i].substr(2, param.ref[i].length - 3), link: "#param:" + param.ref[i].substr(2, param.ref[i].length - 3) }); fulltext.push(param.ref[i].substr(2, param.ref[i].length - 3)); } } prop.fulltext = fulltext.join(" "); return prop; }; if (iClass.class.params) _class.props = prepareProp(null, iClass.class.params, null); section.class = _class; return section; }; /** * Prepares class list section * * @param index Inventory index * @param path Current path */ Renderer.prototype.prepareClassList = function (index, path) { var section = { type: 'class-list', classes: [] }; for (var i in index.classes) { if (i == 'init') continue; section.classes.push({ label: index.classes[i].name.fullName, link: this.getClassLink(index.classes[i].name), flags: (index.classes[i].error ? { error: true } : {}) }); } for (var i in index.dirs) { var _index = index.dirs[i]; section.classes.push({ label: _index.classes['init'] ? _index.classes['init'].name.fullName : _index.name, link: this.getDirectoryLink(path + "/" + _index.name), flags: (_index.classes['init'] && _index.classes['init'].error ? { error: true } : {}) }); } return section; }; /** * Prepares source code section * * @param filename Filename */ Renderer.prototype.prepareSource = function (filename, lang) { var section = { type: 'source', title: filename.split("/").pop(), contents: null }; if (!hljs.getLanguage(lang)) throw new Error("Unsupported source language: " + lang); var code = fs.readFileSync(filename, { encoding: 'utf-8' }); var html = hljs.highlight(lang, code).value; var lines = html.split("\n"); section.contents = ''; for (var i = 0; i < lines.length; i++) section.contents += '<div class="code-line" id="line:' + (i + 1) + '"><span class="code-line-nr">' + (i + 1) + '</span>' + lines[i] + '</div>'; //section.contents = this.markdown.render('```' + lang + '\n' + code + '\n```'); return section; }; /** * Renders index page * * @param index Inventory index */ Renderer.prototype.renderIndex = function (index) { //Prepare page var page = { type: 'overview', label: 'Overview', title: 'Overview', crumbs: [], sections: {}, id: 'overview' }; var _crumbs = []; _crumbs.push({ label: "Overview", link: "{{base}}/index.html" }); //Add readme if (index.docs['README']) { page.sections['readme'] = this.prepareDocument(index.docs['README'], "/README"); this.renderSource(index.docs['README'].filename, "/README", _crumbs, 'markdown'); } //Add nodes if (index.dirs['nodes']) page.sections['nodes'] = this.prepareClassList(index.dirs['nodes'], this.nodesDir); //Add classes if (index.dirs['classes']) page.sections['classes'] = this.prepareClassList(index.dirs['classes'], this.classesDir); var outputFilename = this.outputDir + '/index.html'; try { this.logger.info("Rendering index to '" + outputFilename + "'..."); var html = this.renderTpl("index", { title: page.title, page: page }); html = this.filterOutput(html, ''); fs.writeFileSync(outputFilename, html, { encoding: 'utf-8' }); } catch (err) { this.logger.warn("Failed to render index:", err); } }; /** * Renders source code * * @param filename Source filename * @param path Index path * @param crumbs Previous index crumbs * @param lang Code language */ Renderer.prototype.renderSource = function (filename, path, crumbs, lang) { if (crumbs === void 0) { crumbs = []; } //Prepare page var page = { type: 'source', label: filename.split("/").pop(), title: 'File ' + filename.split("/").pop(), crumbs: crumbs, sections: [], id: "source:" + path }; //Add source section page.sections.push(this.prepareSource(filename, lang)); //Render page try { var outputFilename = this.outputDir + path + ".source.html"; this.logger.info("Rendering source page to '" + outputFilename + "'..."); var html = this.renderTpl("page", { title: page.title, page: page }); html = this.filterOutput(html, path); fs.writeFileSync(outputFilename, html, { encoding: 'utf-8' }); } catch (err) { this.logger.warn("Failed to render source page:", err); } }; /** * Renders document * * @param doc Inventory document * @param path Index path * @param crumbs Previous index crumbs * @param force If to ignore cache */ Renderer.prototype.renderDocument = function (doc, path, crumbs, force) { if (crumbs === void 0) { crumbs = []; } if (force === void 0) { force = false; } //Prepare page if (this.cache[path] != doc.fingerprint || force) { this.cache[path] = doc.fingerprint; //Update crumbs var _crumbs = crumbs.slice(); _crumbs.push({ label: doc.name, link: "{{base}}" + path + ".html" }); var page = { type: 'document', title: doc.name, label: doc.name, crumbs: crumbs, sections: [], id: "doc:" + path }; //Add content page.sections.push(this.prepareDocument(doc, path)); this.renderSource(doc.filename, path, _crumbs, 'markdown'); //Render page try { var outputFilename = this.outputDir + path + ".html"; this.logger.info("Rendering document page to '" + outputFilename + "'..."); var html = this.renderTpl("page", { title: page.title, page: page }); html = this.filterOutput(html, path); fs.writeFileSync(outputFilename, html, { encoding: 'utf-8' }); } catch (err) { this.logger.warn("Failed to render document page:", err); } } }; /** * Renders class * * @param doc Inventory class * @param path Index path * @param crumbs Previous index crumbs * @param force If to ignore cache */ Renderer.prototype.renderClass = function (rClass, path, crumbs, force) { if (crumbs === void 0) { crumbs = []; } if (force === void 0) { force = false; } //Prepare page if ((rClass.class && this.cache[path] != rClass.class.fingerprint) || force) { if (rClass.class) this.cache[path] = rClass.class.fingerprint; //Update crumbs var _crumbs = crumbs.slice(); _crumbs.push({ label: rClass.name.fullName, link: this.getClassLink(rClass.name) }); var page = { type: 'class', title: 'Class ' + rClass.name.fullName, label: rClass.name.name, crumbs: crumbs, sections: [], id: "class:" + rClass.name.fullName }; //Add content page.sections.push(this.prepareClass(rClass, path)); this.renderSource(rClass.filename, path, _crumbs, 'yaml'); //Render page try { var outputFilename = this.outputDir + path + ".html"; this.logger.info("Rendering class page to '" + outputFilename + "'..."); var html = this.renderTpl("page", { title: page.title, page: page }); html = this.filterOutput(html, path); fs.writeFileSync(outputFilename, html, { encoding: 'utf-8' }); } catch (err) { this.logger.warn("Failed to render class page:", err); } } }; /** * Renders index (directory) * * @param index Inventory index * @param path Index path * @param crumbs Previous index crumbs * @param force If to ignore cache */ Renderer.prototype.renderDirectory = function (index, path, crumbs, force) { if (crumbs === void 0) { crumbs = []; } if (force === void 0) { force = false; } //Check directory var outputDir = this.outputDir + path; if (!fs.existsSync(outputDir)) { this.logger.info("Creating directory '" + outputDir + "'..."); fs.mkdirSync(outputDir); } //Update crumbs var _crumbs = crumbs.slice(); _crumbs.push({ label: index.name, link: this.getDirectoryLink(path) }); //Prepare page if (this.cache[path] != index.contentFingerprint || force) { this.cache[path] = index.contentFingerprint; var page = { type: (index.classes['init'] ? 'class' : 'directory'), title: (index.classes['init'] ? 'Class ' + index.classes['init'].name.fullName : 'Directory ' + index.name), label: index.name, crumbs: crumbs, sections: [], id: "dir:" + path }; //Add readme if (index.docs['README']) { page.sections.push(this.prepareDocument(index.docs['README'], path + "/README")); this.renderSource(index.docs['README'].filename, path + "/README", _crumbs, 'markdown'); } //Add subclasses if ((index.classes['init'] && Object.keys(index.classes).length > 1) || (!index.classes['init'] && Object.keys(index.classes).length > 0) || Object.keys(index.dirs).length > 0) page.sections.push(this.prepareClassList(index, path)); //Add class if (index.classes['init']) { page.sections.push(this.prepareClass(index.classes['init'], path + "/init")); this.renderSource(index.classes['init'].filename, path + "/init", _crumbs, 'yaml'); } //Render page try { var outputFilename = this.outputDir + path + "/index.html"; this.logger.info("Rendering dir page to '" + outputFilename + "'..."); var html = this.renderTpl("page", { title: page.title, page: page }); html = this.filterOutput(html, path + "/"); fs.writeFileSync(outputFilename, html, { encoding: 'utf-8' }); } catch (err) { this.logger.warn("Failed to render dir page:", err); } } //Render classes for (var i in index.classes) { if (i == "init") continue; try { this.renderClass(index.classes[i], path + "/class." + index.classes[i].name.name, _crumbs, force); } catch (err) { this.logger.error("Failed to render '" + path + "/class." + index.classes[i].name.name + "':", err); } } //Render documents for (var i in index.docs) { if (i == "README") continue; try { this.renderDocument(index.docs[i], path + "/doc." + index.docs[i].name, _crumbs, force); } catch (err) { this.logger.error("Failed to render '" + path + "/doc." + index.docs[i].name + "':", err); } } //Render sub directories for (var i in index.dirs) { try { this.renderDirectory(index.dirs[i], path + "/" + i, _crumbs, force); } catch (err) { this.logger.error("Failed to render '" + (path + "/" + i) + "':", err); } } }; /** * Copy assets from template to output for given directory * * @param path Path */ Renderer.prototype.copyAssets = function (srcDir, dstDir, path) { var realPath = srcDir + path; var dstPath = this.outputDir + dstDir + path; this.logger.debug("Checking assets in '" + realPath + "'..."); //Check output dir if (!fs.existsSync(dstPath)) { this.logger.info("Creating directory '" + dstPath + "'..."); fs.mkdirSync(dstPath); } //Read dir var files = fs.readdirSync(realPath); for (var i in files) { var file = files[i]; if (file == "." || file == ".." || file.substr(0, 1) == ".") continue; var srcFilename = srcDir + path + "/" + file; var dstFilename = this.outputDir + dstDir + path + "/" + file; var assetId = "__assets__" + srcDir + path + "/" + file; var stat = fs.statSync(srcFilename); if (stat.isDirectory()) { try { this.copyAssets(srcDir, dstDir, path + "/" + file); } catch (err) { this.logger.error("Failed to create asset directory '" + dstFilename + "':", err); } } else { if (this.cache[assetId] == String(stat.mtime.getTime())) { this.logger.debug("Asset file '" + dstFilename + "' up to date, skiping..."); continue; } this.logger.info("Copying asset file '" + dstFilename + "'..."); //Copy file try { fsExtra.copySync(srcFilename, dstFilename, { overwrite: true }); } catch (err) { this.logger.error("Failed to copy asset file '" + srcFilename + "' to '" + dstFilename + "':", err); } this.cache[assetId] = String(stat.mtime.getTime()); } } }; /** * Returns if template file has changed * * @param name Template name * @param updateCache If to update cached value */ Renderer.prototype.hasTemplateChanged = function (path) { var files = fs.readdirSync(path); for (var i in files) { var file = files[i]; var filename = path + "/" + file; var stat = fs.statSync(filename); if (stat.isDirectory()) { var res = this.hasTemplateChanged(path + "/" + file); if (res) return true; } else { if (file.substr(file.length - 4, 4) !== ".pug") continue; if (this.cache['__template__/' + path] != String(stat.mtime.getTime())) { this.cache['__template__/' + path] = String(stat.mtime.getTime()); return true; } } } return false; }; /** * Renders all items in inventory * * @param index Inventory index */ Renderer.prototype.render = function (index, treeHash) { //Check output directory if (!fs.existsSync(this.outputDir)) { this.logger.info("Creating output directory '" + this.outputDir + "'..."); fsExtra.mkdirpSync(this.outputDir); } //Copy assets this.copyAssets(this.templateDir + this.assetsSrcDir, this.assetsOutDir, ""); //Copy media files if (fs.existsSync(this.mediaSrcDir)) this.copyAssets(this.mediaSrcDir, this.mediaOutDir, ""); //Check if template has changed var tplChanged = this.hasTemplateChanged(this.templateDir); if (tplChanged) { this.tplCache = {}; this.logger.debug("Template changed, will rebuild all..."); } //Update globals this.globals['navTree'] = { classes: this.prepareTree(index.dirs['classes'], this.classesDir), nodes: this.prepareTree(index.dirs['nodes'], this.nodesDir) }; //Render index if (this.cache[index.path] !== index.fingerprint) this.renderIndex(index); var crumbs = [{ label: "Overview", link: "{{base}}/index.html" }]; //Render nodes this.renderDirectory(index.dirs['nodes'], this.nodesDir, crumbs, tplChanged); //Render classes this.renderDirectory(index.dirs['classes'], this.classesDir, crumbs, tplChanged); }; return Renderer; }()); exports.Renderer = Renderer;