@zowe/imperative
Version:
framework for building configurable CLIs
318 lines • 15.5 kB
JavaScript
;
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebHelpGenerator = void 0;
const fs = require("fs");
const path = require("path");
const DefaultHelpGenerator_1 = require("./DefaultHelpGenerator");
const ImperativeError_1 = require("../../../error/src/ImperativeError");
/**
* Imperative web help generator. Accepts the command definitions and constructs
* the full help text for the command node.
*
* @export
* @class WebHelpGenerator
*/
class WebHelpGenerator {
/**
* Create an instance of WebHelpGenerator.
* @param {ICommandDefinition} - Imperative command tree to build help for
* @param {ImperativeConfig} - Imperative config containing data about the CLI
* @param {string} - Output directory for web help files
*/
constructor(fullCommandTree, config, webHelpDir) {
/**
* Specifies whether user's home directory should be redacted from help content
* @memberof WebHelpGenerator
*/
this.sanitizeHomeDir = false;
this.mFullCommandTree = fullCommandTree;
this.mConfig = config;
this.mDocsDir = path.join(webHelpDir, "docs");
this.treeNodes = [];
this.aliasList = {};
}
/**
* Build web help files and copy dependencies to output folder
* @param {IHandlerResponseApi} - Command response object to use for output
*/
buildHelp(cmdResponse) {
// Log using buffer to prevent trailing newline from getting added
// This allows printing dot characters on the same line to show progress
cmdResponse.console.log(Buffer.from("Generating web help"));
// Ensure web-help/docs folder exists and is empty
const fsExtra = require("fs-extra");
fsExtra.emptyDirSync(this.mDocsDir);
const webHelpDir = path.join(this.mDocsDir, "..");
// Find web help dist folder
const distDir = path.join(__dirname, "../../../../web-help/dist");
if (!fs.existsSync(distDir)) {
throw new ImperativeError_1.ImperativeError({
msg: `The web-help distribution directory does not exist:\n "${distDir}"`
});
}
// Copy files from dist folder to .zowe home dir
const dirsToCopy = [distDir, path.join(distDir, "css"), path.join(distDir, "js"), path.join(distDir, "assets")];
dirsToCopy.forEach((dir) => {
const destDir = path.join(webHelpDir, path.relative(distDir, dir));
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir);
}
fs.readdirSync(dir)
.filter((pathname) => fs.statSync(path.join(dir, pathname)).isFile())
.forEach((filename) => fsExtra.copySync(path.join(dir, filename), path.join(destDir, filename)));
});
// Copy header image if it exists
if (this.mConfig.loadedConfig.webHelpLogoImgPath) {
fsExtra.copySync(this.mConfig.loadedConfig.webHelpLogoImgPath, path.join(webHelpDir, "header-image.png"));
}
// Replace main.css with custom CSS file if it exists
if (this.mConfig.loadedConfig.webHelpCustomCssPath) {
fsExtra.copySync(this.mConfig.loadedConfig.webHelpCustomCssPath, path.join(webHelpDir, "css/main.css"));
}
// Sort all items in the command tree and remove duplicates
const uniqueDefinitions = this.mFullCommandTree;
uniqueDefinitions.children = uniqueDefinitions.children
.sort((a, b) => a.name.localeCompare(b.name))
.filter((a, pos, self) => self.findIndex((b) => a.name === b.name) === pos);
// Generate HTML help file for the root CLI command
const rootCommandName = this.mConfig.rootCommandName;
const rootHelpHtmlPath = path.join(this.mDocsDir, `${rootCommandName}.html`);
this.treeNodes.push({ id: `${rootCommandName}.html`, text: rootCommandName });
let rootHelpContent = this.genDocsHeader(rootCommandName);
rootHelpContent += `<h2><a href="${rootCommandName}.html" name="${rootCommandName}">${rootCommandName}</a>${this.genPrintButton()}</h2>\n`;
rootHelpContent += this.renderMarkdown(this.mConfig.loadedConfig.rootCommandDescription) + "\n";
const helpGen = new DefaultHelpGenerator_1.DefaultHelpGenerator({ produceMarkdown: true, rootCommandName }, { commandDefinition: uniqueDefinitions, fullCommandTree: uniqueDefinitions, skipTextWrap: true });
rootHelpContent += this.renderMarkdown(this.buildChildrenSummaryTables(helpGen, rootCommandName) + "\n\n" +
helpGen.buildCommandOptionsSection() + helpGen.buildGlobalOptionsSection());
this.singlePageHtml = rootHelpContent.replace(/<h4>Groups[^]+?<\/ul>/, "");
rootHelpContent += this.genDocsFooter();
fs.writeFileSync(rootHelpHtmlPath, rootHelpContent);
cmdResponse.console.log(Buffer.from("."));
// Generate HTML help files for every CLI command
uniqueDefinitions.children.forEach((def) => {
cmdResponse.console.log(Buffer.from("."));
this.genCommandHelpPage(def, def.name, this.mDocsDir, this.treeNodes[0]);
});
// Generate single HTML file for all CLI commands
this.singlePageHtml += this.genDocsFooter();
this.singlePageHtml = this.singlePageHtml.replace(new RegExp(`<a href="(${rootCommandName}.*?)\\.html"`, "g"), "<a href=\"#$1\"");
let cmdTreeHtml = "<ul>";
this.treeNodes[0].children.forEach((node) => {
cmdTreeHtml += this.buildCmdTreeHtml(node);
});
cmdTreeHtml += "</ul>";
cmdTreeHtml = `<div class="page-break print-only"><h4>Table of Contents</h4>\n${cmdTreeHtml}</div>\n`;
const insertIndex = this.singlePageHtml.indexOf("<hr ");
this.singlePageHtml = this.singlePageHtml.slice(0, insertIndex) + cmdTreeHtml + this.singlePageHtml.slice(insertIndex);
fs.writeFileSync(path.join(this.mDocsDir, "all.html"), this.singlePageHtml);
this.writeTreeData();
cmdResponse.console.log("done!");
}
/**
* Converts Markdown string to HTML string. Any HTML tags contained in the
* input Markdown string will be escaped.
* @param markdownContent String containing Markdown content
* @returns String containing HTML content
*/
renderMarkdown(markdownContent) {
if (this.markdownIt == null) {
this.markdownIt = require("markdown-it")({ breaks: true, linkify: true });
this.markdownIt.linkify.set({ fuzzyLink: false }); // Only linkify URLs that have a protocol
}
return this.markdownIt.render(markdownContent);
}
/**
* Returns header HTML for help page
* @private
* @param title - Title string for the page
*/
genDocsHeader(title) {
return `<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>${title}</title>
<link rel="stylesheet" href="../css/bundle-docs.css">
<link rel="stylesheet" href="../css/docs.css">
<article class="markdown-body">
`;
}
/**
* Returns footer HTML for help page
* @private
*/
genDocsFooter() {
return `</article>
<script src="../js/docs.js"></script>
`;
}
/**
* Returns print button to show in header of help page
* @private
*/
genPrintButton() {
return `<button id="btn-print" class="no-print" onclick="window.print();" title="Print">🖨️</button>`;
}
/**
* Builds breadcrumb of HTML links to show command ancestry
* @private
* @param {string} rootCommandName
* @param {string} fullCommandName
*/
genBreadcrumb(rootCommandName, fullCommandName) {
const crumbs = [];
let hrefPrefix = "";
[rootCommandName, ...fullCommandName.split("_")].forEach((linkText) => {
crumbs.push(`<a href="${hrefPrefix}${linkText}.html">${linkText}</a>`);
hrefPrefix += `${linkText}_`;
});
return crumbs.join(" › ");
}
/**
* Builds list of groups/commands with HTML links added
* @private
* @param {DefaultHelpGenerator} helpGen
* @param {string} fullCommandName
*/
buildChildrenSummaryTables(helpGen, fullCommandName) {
const hrefPrefix = fullCommandName + "_";
return helpGen.buildChildrenSummaryTables().split(/\r?\n/g)
.map((line) => {
// Wrap group/command names inside links
const match = line.match(/^\s{0,4}([a-z0-9-]+(?:\s\|\s[a-z0-9-]+)*)\s+\S/i);
if (match) {
const href = `${hrefPrefix}${match[1].split(" ")[0]}.html`;
return `\n* [${match[1]}](${href}) -` + line.slice(match[0].length - 2);
}
return (line.startsWith("#### ") ? "\n" : " ") + line.trim();
}).join("");
}
/**
* Appends help content for individual command/group to single page HTML
* @private
* @param {ICommandDefinition} definition
* @param {string} rootCommandName
* @param {string} fullCommandName
* @param {string} htmlContent
*/
appendToSinglePageHtml(definition, rootCommandName, fullCommandName, htmlContent) {
// Add horizontal line/page break at start of a new top level group
if (fullCommandName.indexOf("_") === -1) {
this.singlePageHtml += "<hr class=\"no-print\">\n";
htmlContent = htmlContent.replace(/<h2/, "<h2 class=\"page-break\"");
}
// Generate HTML anchor in front of header
const anchorText = `<a${definition.type !== "group" ? " class=\"cmd-anchor\"" : ""} name="${rootCommandName}_${fullCommandName}"></a>`;
if (definition.type === "group") {
// Remove sections from HTML that would be redundant
this.singlePageHtml += anchorText + htmlContent.slice(0, htmlContent.indexOf("<h4"));
}
else {
// Make header smaller for commands
this.singlePageHtml += anchorText + htmlContent.replace(/<h2/, "<h3").replace(/h2>/, "h3>");
}
}
/**
* Generates HTML help page for Imperative command
* @private
* @param {ICommandDefinition} definition
* @param {string} fullCommandName
* @param {string} docsDir
* @param {ITreeNode} parentNode
*/
genCommandHelpPage(definition, fullCommandName, docsDir, parentNode) {
const rootCommandName = this.treeNodes[0].text;
const helpGen = new DefaultHelpGenerator_1.DefaultHelpGenerator({ produceMarkdown: true, rootCommandName }, { commandDefinition: definition, fullCommandTree: this.mFullCommandTree, skipTextWrap: true });
let markdownContent = helpGen.buildHelp() + "\n";
if (definition.type === "group") {
// this is disabled for the CLIReadme.md but we want to show children here
// so we'll call the help generator's children summary function even though
// it's usually skipped when producing markdown
markdownContent += this.buildChildrenSummaryTables(helpGen, rootCommandName + "_" + fullCommandName);
}
let htmlContent = "<h2>" + this.genBreadcrumb(rootCommandName, fullCommandName) + this.genPrintButton() + "</h2>\n";
// Make regex check here to fix unintentional escaping in an HTTP(S) link
markdownContent = markdownContent.replaceAll(/https?:\/\/.+?(?=[ \n'"`\])}])/g, (match) => {
return match.replaceAll("\\", "");
});
htmlContent += this.renderMarkdown(markdownContent);
// Add Copy buttons after command line examples
htmlContent = htmlContent.replace(/<code>\$\s*([^<]+)<\/code>/g, `<code>$1</code> <button class="btn-copy no-print" data-balloon-pos="right" data-clipboard-text="$1">Copy</button>`);
// Sanitize references to user's home directory
if (this.sanitizeHomeDir) {
const homeDir = path.dirname(this.mConfig.loadedConfig.defaultHome);
htmlContent = htmlContent.replace(new RegExp(homeDir.replace(/[\\/]/g, "."), "g"), homeDir.slice(0, homeDir.lastIndexOf(path.sep) + 1) + "<user>");
}
this.appendToSinglePageHtml(definition, rootCommandName, fullCommandName, htmlContent);
htmlContent = this.genDocsHeader(fullCommandName.replace(/_/g, " ")) + htmlContent + this.genDocsFooter();
const helpHtmlFile = `${rootCommandName}_${fullCommandName.trim()}.html`;
const helpHtmlPath = path.join(docsDir, helpHtmlFile);
fs.writeFileSync(helpHtmlPath, htmlContent);
// Add command node and list of aliases to tree data
const childNode = {
id: helpHtmlFile,
text: [definition.name, ...definition.aliases].join(" | ")
};
parentNode.children = [...parentNode.children || [], childNode];
definition.aliases.forEach((alias) => {
if (alias !== definition.name) {
if (this.aliasList[alias] === undefined) {
this.aliasList[alias] = [definition.name];
}
else if (this.aliasList[alias].indexOf(definition.name) === -1) {
this.aliasList[alias].push(definition.name);
}
}
});
// Recursively generate HTML help pages if this group/command has children
if (definition.children) {
definition.children.forEach((child) => {
this.genCommandHelpPage(child, `${fullCommandName}_${child.name}`, docsDir, childNode);
});
}
}
/**
* Builds table of contents for flat view
* @private
* @param node - Parent node whose children will be listed
* @returns - HTML list of child nodes
*/
buildCmdTreeHtml(node) {
let cmdTreeHtml = `<li><a href="#${node.id.slice(0, node.id.lastIndexOf("."))}">${node.text}</a>`;
if (node.children) {
node.children.forEach((childNode) => {
cmdTreeHtml += `<ul>${this.buildCmdTreeHtml(childNode)}</ul>`;
});
}
cmdTreeHtml += "</li>\n";
return cmdTreeHtml;
}
/**
* Writes data for building web help command tree to JS file
* @private
*/
writeTreeData() {
const treeDataPath = path.join(this.mDocsDir, "..", "tree-data.js");
let footerStr = `${this.mConfig.callerPackageJson.name} ${this.mConfig.callerPackageJson.version}`;
if (fs.existsSync(__dirname + "/../../../../.git")) {
footerStr += " (dev)";
}
fs.writeFileSync(treeDataPath, "/* This file is automatically generated, do not edit manually! */\n" +
`const headerStr = "${this.mConfig.loadedConfig.productDisplayName}";\n` +
`const footerStr = "${footerStr}";\n` +
"const treeNodes = " + JSON.stringify(this.treeNodes) + ";\n" +
"const aliasList = " + JSON.stringify(this.aliasList) + ";");
}
}
exports.WebHelpGenerator = WebHelpGenerator;
//# sourceMappingURL=WebHelpGenerator.js.map