UNPKG

@canalplus/readme.doc

Version:

Readme's an Extremely Accessible Documentation MakEr

380 lines (379 loc) 16.8 kB
import * as fs from "fs"; import * as path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import { rimrafSync } from "rimraf"; import AnchorChecker from "./anchor_checker.js"; import createDocumentationPage from "./create_documentation_page.js"; import generateHeaderHtml from "./generate_header_html.js"; import generatePageListHtml from "./generate_page_list_html.js"; import generateSidebarHtml from "./generate_sidebar_html.js"; import log from "./log.js"; import parseDocConfigs from "./parse_doc_configs.js"; import { SiteMapCreator } from "./site_map_creator.js"; import { mkdirParent, toUriCompatibleRelativePath } from "./utils.js"; const currentDir = path.dirname(fileURLToPath(import.meta.url)); /** * Create documentation for the directory given into the ouput directory given. * @param {string} baseInDir - The input directory where markdown documentation * is read. * @param {string} baseOutDir - The output directory where HTML files are * generated. * @param {Object} [options={}] - Various documentation options. * @param {Function} [options.getPageTitle] - Callback returning the name of the * page, based on the name of a single markdown document. * If not set, the page title will just be the corresponding markdown's title. * @param {Array.<string>} [options.css] - Optional CSS files which will be * linked to each generated page. * Should be the path to each of those. * @param {string|undefined} [options.version] - String indicating the current * version of the documented application. * @returns {Promise} - Resolve when done */ export default async function createDocumentation(baseInDir, baseOutDir, options = {}) { var _a, _b, _c, _d, _e, _f; if (options.clean === true) { rimrafSync(baseOutDir); } // Parse global configuration as well as local configurations const config = await parseDocConfigs(baseInDir, baseOutDir); // Copy global files to output directory const cssOutputPaths = await copyCssFiles(baseOutDir); const scriptOutputPaths = await copyJavaScriptFiles(baseOutDir); if (typeof ((_a = config.favicon) === null || _a === void 0 ? void 0 : _a.srcPath) === "string") { await copyFileToOutputDir(config.favicon.srcPath, baseInDir, baseOutDir); } if (typeof ((_b = config.logo) === null || _b === void 0 ? void 0 : _b.srcPath) === "string") { await copyFileToOutputDir(config.logo.srcPath, baseInDir, baseOutDir); } /** * Will be used to check the validity of anchor links between documentation * pages (and relative to the same page). */ const anchorChecker = new AnchorChecker(); /** * Will be used to generate the site map file for search engine. */ const siteMapCreator = new SiteMapCreator(); /** * Map where the key is the input Markdown file and the value the * corresponding output HTML file. */ const fileMap = constructFileMap(config.links); /** Object filled progressively to construct our final search index. */ const searchIndex = []; const version = options.version; // Create documentation pages for (let linkIndexInConfig = 0; linkIndexInConfig < config.links.length; linkIndexInConfig++) { const currentLink = config.links[linkIndexInConfig]; if (currentLink.type !== "local-doc") { continue; } for (let pageIdx = 0; pageIdx < currentLink.pages.length; pageIdx++) { const currentPage = currentLink.pages[pageIdx]; if (!currentPage.isPageGroup) { const { inputFile, outputFile } = currentPage; await prepareAndCreateDocumentationPage({ inputFile, outputFile, linkIndexInConfig, pageIndexesInLink: [pageIdx], pageTitle: currentPage.displayName, }); } else if (Array.isArray(currentPage.pages)) { for (let subPageIdx = 0; subPageIdx < currentPage.pages.length; subPageIdx++) { const currentSubPage = currentPage.pages[subPageIdx]; const { inputFile, outputFile } = currentSubPage; await prepareAndCreateDocumentationPage({ inputFile, outputFile, linkIndexInConfig, pageIndexesInLink: [pageIdx, subPageIdx], pageTitle: currentSubPage.displayName, }); } } } } const anchorCheckErrors = anchorChecker.checkAllAnchors(); if (anchorCheckErrors.length > 0) { for (const check of anchorCheckErrors) { if (check.validity === 2 /* AnchorValidity.AnchorNotFound */) { let message = `A referenced anchor link was not found. File with link: ${check.inputFileWithLink} Linked file: ${check.inputFileLinkDestination} Anchor: ${check.anchor} `; const availableAnchors = anchorChecker.getAnchorsForInputFile(check.inputFileLinkDestination); if (availableAnchors !== undefined && availableAnchors.length > 0) { message += " Available Anchors: " + availableAnchors.join(", ") + "\n"; } log("WARNING", message); } } } if (config.siteMapRoot !== undefined) { const xml = siteMapCreator.generateSiteMapXML(); try { const sitemapLoc = path.join(path.resolve(baseOutDir), "sitemap.xml"); await promisify(fs.writeFile)(sitemapLoc, xml); } catch (err) { const srcMessage = (_d = ((_c = err) !== null && _c !== void 0 ? _c : {}).message) !== null && _d !== void 0 ? _d : "Unknown error"; log("WARNING", `Could not create sitemap file: ${srcMessage}`); } } try { const searchIndexLoc = path.join(path.resolve(baseOutDir), "searchIndex.json"); await promisify(fs.writeFile)(searchIndexLoc, JSON.stringify(searchIndex)); } catch (err) { const srcMessage = (_f = ((_e = err) !== null && _e !== void 0 ? _e : {}).message) !== null && _f !== void 0 ? _f : "Unknown error"; log("WARNING", `Could not create search index file: ${srcMessage}`); } return; async function prepareAndCreateDocumentationPage({ inputFile, outputFile, linkIndexInConfig, pageIndexesInLink, pageTitle, }) { var _a, _b, _c; const link = config.links[linkIndexInConfig]; if (link.type !== "local-doc") { return; } // Create output directory if it does not exist const outDir = path.dirname(outputFile); await createDirIfDoesntExist(outDir); let logoInfo = null; if (config.logo !== undefined) { logoInfo = {}; if (config.logo !== undefined && typeof config.logo.link === "string") { logoInfo.link = config.logo.link; } if (config.logo !== undefined && typeof config.logo.srcPath === "string") { const fullPath = path.join(baseOutDir, config.logo.srcPath); logoInfo.url = toUriCompatibleRelativePath(fullPath, outDir); } } let faviconUrl = null; if (config.favicon !== undefined && typeof config.favicon.srcPath === "string") { const fullPath = path.join(baseOutDir, config.favicon.srcPath); faviconUrl = toUriCompatibleRelativePath(fullPath, outDir); } const pageListHtml = generatePageListHtml(config.links, linkIndexInConfig, pageIndexesInLink, outputFile); const navBarHtml = generateHeaderHtml(config, linkIndexInConfig, outputFile, logoInfo, version); const pages = link.pages; const sidebarHtml = generateSidebarHtml(pages, pageIndexesInLink, outputFile, logoInfo); const firstLevelPage = pages[pageIndexesInLink[0]]; let prevPageConfig = null; let nextPageConfig = null; if (firstLevelPage.isPageGroup && pageIndexesInLink.length > 1 && pageIndexesInLink[1] > 0) { prevPageConfig = (_a = firstLevelPage.pages[pageIndexesInLink[1] - 1]) !== null && _a !== void 0 ? _a : null; } else if (pageIndexesInLink[0] > 0) { const prevFirstLevelPage = pages[pageIndexesInLink[0] - 1]; if (prevFirstLevelPage !== undefined) { prevPageConfig = prevFirstLevelPage.isPageGroup ? prevFirstLevelPage.pages[0] : prevFirstLevelPage; } } if (firstLevelPage.isPageGroup && pageIndexesInLink.length > 1 && pageIndexesInLink[1] < firstLevelPage.pages.length - 1) { nextPageConfig = firstLevelPage.pages[pageIndexesInLink[1] + 1]; } else if (pageIndexesInLink[0] < pages.length - 1) { const nextFirstLevelPage = pages[pageIndexesInLink[0] + 1]; if (nextFirstLevelPage !== undefined) { nextPageConfig = nextFirstLevelPage.isPageGroup ? nextFirstLevelPage.pages[0] : nextFirstLevelPage; } } const prevPageInfo = prevPageConfig === null ? null : getRelativePageInfo(prevPageConfig, outputFile); const nextPageInfo = nextPageConfig === null ? null : getRelativePageInfo(nextPageConfig, outputFile); const cssUrls = cssOutputPaths.map((cssOutput) => toUriCompatibleRelativePath(cssOutput, outDir)); const scriptUrls = scriptOutputPaths.map((s) => toUriCompatibleRelativePath(s, outDir)); // add link translation to options const linkTranslator = linkTranslatorFactory(inputFile, outputFile, fileMap, anchorChecker); const { anchors } = await createDocumentationPage({ baseOutDir, cssUrls, faviconUrl, inputFile, linkTranslator, navBarHtml, nextPageInfo, outputFile, pageListHtml, pageTitle, prevPageInfo, scriptUrls, searchIndex, sidebarHtml, }); anchorChecker.addAnchorsForFile(inputFile, anchors); const outputUrlFromRoot = toUriCompatibleRelativePath(outputFile, baseOutDir); const root = config.siteMapRoot; if (config.siteMapRoot !== undefined) { try { // it is recommended that sitemap should only use absolute URL const absoluteURL = new URL(outputUrlFromRoot, root).href; siteMapCreator.addToSiteMap(absoluteURL); } catch (err) { const srcMessage = (_c = ((_b = err) !== null && _b !== void 0 ? _b : {}).message) !== null && _c !== void 0 ? _c : "Unknown error"; throw new Error(`Could not create siteMap url "${outputUrlFromRoot}", the root ${root} may be incorrect : ${srcMessage}`); } } } } /** * Generate linkTranslator functions * @param {string} inputFile * @param {string} outputFile * @param {Array.<Object>} fileMap * @param {Object} anchorChecker * @returns {Function} */ function linkTranslatorFactory(inputFile, outputFile, fileMap, anchorChecker) { const outputDir = path.dirname(outputFile); /** * Convert links to files that will be converted to the links of the * corresponding converted output files. * @param {string} link * @returns {string|undefined} */ return (link) => { if (/^(?:[a-z]+:\/\/)/.test(link)) { return; } if (link[0] === "#") { anchorChecker.addAnchorReference(inputFile, inputFile, link.substring(1)); return; } const extname = path.extname(link); const indexOfAnchor = extname.indexOf("#"); const anchor = indexOfAnchor > 0 ? extname.substring(indexOfAnchor) : ""; const linkWithoutAnchor = link.substring(0, link.length - anchor.length); const completeLink = path.join(path.dirname(inputFile), linkWithoutAnchor); const normalizedLink = path.normalize(path.resolve(completeLink)); const translation = fileMap.get(normalizedLink); if (translation === undefined) { log("WARNING", `A referenced link was not found. File: ${inputFile} Link: ${link} `); } else if (anchor.length > 1) { anchorChecker.addAnchorReference(inputFile, normalizedLink, anchor.substring(1)); } return translation !== undefined ? toUriCompatibleRelativePath(translation, outputDir) + anchor : undefined; }; } function getRelativePageInfo(pageConfig, currentPath) { const { displayName: pDisplayName, outputFile: pOutputFile } = pageConfig; const relativeHref = toUriCompatibleRelativePath(pOutputFile, path.dirname(currentPath)); return { name: pDisplayName, link: relativeHref }; } async function copyFileToOutputDir(filePathFromInputDir, inputDir, outputDir) { var _a, _b; const inputPath = path.join(inputDir, filePathFromInputDir); const outputPath = path.join(outputDir, filePathFromInputDir); const relativeDir = path.relative(inputDir, inputPath); const isSubdir = !relativeDir.startsWith(".."); if (!isSubdir) { throw new Error("You're trying to copy a media asset outside of your root directory (" + filePathFromInputDir + "). This is for forbidden for now."); } const doesOutDirExists = await promisify(fs.exists)(path.dirname(outputPath)); if (!doesOutDirExists) { try { await mkdirParent(path.dirname(outputPath)); } catch (err) { const srcMessage = (_b = ((_a = err) !== null && _a !== void 0 ? _a : {}).message) !== null && _b !== void 0 ? _b : "Unknown error"; throw new Error(`Could not create "${outputPath}" directory: ${srcMessage}`); } } const doesOutFileExist = await promisify(fs.exists)(outputPath); if (!doesOutFileExist) { await promisify(fs.copyFile)(inputPath, outputPath); } } async function createDirIfDoesntExist(dir) { var _a, _b; const doesCSSOutDirExists = await promisify(fs.exists)(dir); if (!doesCSSOutDirExists) { try { await mkdirParent(dir); } catch (err) { const srcMessage = (_b = ((_a = err) !== null && _a !== void 0 ? _a : {}).message) !== null && _b !== void 0 ? _b : "Unknown error"; throw new Error(`Could not create "${dir}" directory: ${srcMessage}`); } } } async function copyCssFiles(baseOutDir) { const cssOutputDir = path.join(path.resolve(baseOutDir), "styles"); const cssFiles = [ path.join(currentDir, "styles/style.css"), path.join(currentDir, "styles/code.css"), ]; const outputPaths = cssFiles.map((cssFilepath) => { return path.join(cssOutputDir, path.basename(cssFilepath)); }); await createDirIfDoesntExist(cssOutputDir); await Promise.all(cssFiles.map(async (cssInput, i) => { await promisify(fs.copyFile)(cssInput, outputPaths[i]); })); return outputPaths; } async function copyJavaScriptFiles(baseOutDir) { const scriptOutputDir = path.join(path.resolve(baseOutDir), "scripts"); const scripts = [ path.join(currentDir, "scripts/fuse.js"), path.join(currentDir, "scripts/script.js"), ]; const outputPaths = scripts.map((s) => path.join(scriptOutputDir, path.basename(s))); await createDirIfDoesntExist(scriptOutputDir); await Promise.all(scripts.map(async (s, i) => { await promisify(fs.copyFile)(s, outputPaths[i]); })); return outputPaths; } /** * Construct a Map of markdown files path to the corresponding output file path. * This can be useful to redirect links to other converted markdowns. * @param {Array.<Object>} links * @returns {Map.<string, string>} */ function constructFileMap(links) { const fileMap = new Map(); for (const link of links) { if (link.type === "local-doc") { for (const pageInfo of link.pages) { if (pageInfo.isPageGroup) { for (const subPageInfo of pageInfo.pages) { fileMap.set(subPageInfo.inputFile, subPageInfo.outputFile); } } else { fileMap.set(pageInfo.inputFile, pageInfo.outputFile); } } } } return fileMap; }