@canalplus/readme.doc
Version:
Readme's an Extremely Accessible Documentation MakEr
321 lines (320 loc) • 14.1 kB
JavaScript
import * as fs from "fs";
import * as path from "path";
import { promisify } from "util";
export default async function parseDocConfigs(baseInDir, baseOutDir) {
var _a, _b;
const rootConfigFileName = path.join(baseInDir, ".docConfig.json");
const rootConfig = await parseAndCheckRootConfigFile(rootConfigFileName);
const ret = {
logo: undefined,
favicon: undefined,
links: [],
linksRightIndex: -1,
siteMapRoot: undefined,
};
if (typeof rootConfig.logo === "object") {
ret.logo = rootConfig.logo;
}
if (typeof rootConfig.favicon === "object") {
ret.favicon = rootConfig.favicon;
}
const linksLeft = (_a = rootConfig.linksLeft) !== null && _a !== void 0 ? _a : [];
const linksRight = (_b = rootConfig.linksRight) !== null && _b !== void 0 ? _b : [];
const categoryLinks = linksLeft.concat(linksRight);
if (linksRight.length > 0) {
ret.linksRightIndex = linksLeft.length;
}
for (let i = 0; i < categoryLinks.length; i++) {
const category = categoryLinks[i];
if (typeof category !== "object" || category === null) {
if (i < linksLeft.length) {
throw new Error(`Invalid element in \`linksLeft\` at index ${i}`);
}
else {
throw new Error(`Invalid element in \`linksRight\` at index ${i - linksLeft.length}`);
}
}
if (!("type" in category) || typeof category.type !== "string") {
if (i < linksLeft.length) {
throw new Error(`Element in \`linksLeft\` at index ${i} is missing its \`type\` property`);
}
else {
throw new Error(`Element in \`linksRight\` at index ${i - linksLeft.length} is missing its \`type\` property`);
}
}
let parsedCategory;
switch (category.type) {
case "local-doc":
if (!("path" in category) || typeof category.path !== "string") {
throw new Error(`A "local-doc" element is missing its "path" property.`);
}
if (!("displayName" in category) ||
typeof category.displayName !== "string") {
throw new Error(`A "local-doc" element is missing its "path" property.`);
}
parsedCategory = await parseLocalDocCategory({ displayName: category.displayName, path: category.path }, baseInDir, baseOutDir);
break;
case "version":
let link;
if ("link" in category) {
if (typeof category.link !== "string") {
throw new Error(`A version" element has a "link" property in the wrong format.` +
" Should be a string.");
}
link = category.link;
}
parsedCategory = { type: "version", link };
break;
case "link":
if (!("link" in category) || typeof category.link !== "string") {
throw new Error(`A "link" element is missing its "link" property.`);
}
if (!("displayName" in category) ||
typeof category.displayName !== "string") {
throw new Error(`A "link" element is missing its "path" property.`);
}
parsedCategory = {
type: "external-link",
link: category.link,
displayName: category.displayName,
};
break;
case "github-link":
if (!("link" in category) || typeof category.link !== "string") {
throw new Error(`A "github-link" element is missing its "link" property.`);
}
parsedCategory = {
type: "github-link",
link: category.link,
};
break;
case "search":
parsedCategory = { type: "search" };
break;
}
if (parsedCategory !== undefined) {
ret.links.push(parsedCategory);
}
}
if (typeof rootConfig.siteMapRoot === "string") {
ret.siteMapRoot = rootConfig.siteMapRoot;
}
return ret;
}
async function parseLocalDocCategory(category, baseInDir, baseOutDir) {
var _a, _b, _c, _d;
const categoryPath = path.join(baseInDir, category.path);
const categoryOutPath = path.join(baseOutDir, category.path);
const parsedCategory = {
type: "local-doc",
displayName: category.displayName,
firstPage: "",
pages: [],
};
const catCfgFileName = path.join(categoryPath, ".docConfig.json");
const categoryConfig = await parseAndCheckSubConfigFile(catCfgFileName);
for (const page of categoryConfig.pages) {
const pagePath = path.join(categoryPath, page.path);
let pageStat;
try {
pageStat = await promisify(fs.stat)(pagePath);
}
catch (err) {
const srcMessage = (_b = ((_a = err) !== null && _a !== void 0 ? _a : {}).message) !== null && _b !== void 0 ? _b : "Unknown error";
throw new Error(`Cannot run stat on "${pagePath}": ${srcMessage}`);
}
if (pageStat.isDirectory()) {
const parsedPage = {
isPageGroup: true,
defaultOpen: page.defaultOpen === true,
displayName: page.displayName,
pages: [],
};
parsedCategory.pages.push(parsedPage);
const pageCfgFileName = path.join(pagePath, ".docConfig.json");
const pageGroupConfig = await parseAndCheckSubConfigFile(pageCfgFileName);
for (const subPage of pageGroupConfig.pages) {
const subPagePath = path.join(pagePath, subPage.path);
const subPageOutPath = path.join(categoryOutPath, page.path, subPage.path);
let subPageState;
try {
subPageState = await promisify(fs.stat)(subPagePath);
}
catch (err) {
const srcMessage = (_d = ((_c = err) !== null && _c !== void 0 ? _c : {}).message) !== null && _d !== void 0 ? _d : "Unknown error";
throw new Error(`Cannot run stat on "${subPagePath}": ${srcMessage}`);
}
if (subPageState.isDirectory()) {
throw new Error('Category page depth cannot exceed 2 yet "' +
subPagePath +
'" is a directory.');
}
const outputFile = path.join(path.dirname(subPageOutPath), path.basename(subPagePath, ".md") + ".html");
if (parsedPage.pages === undefined) {
parsedPage.pages = [];
}
parsedPage.pages.push({
isPageGroup: false,
displayName: subPage.displayName,
inputFile: path.normalize(path.resolve(subPagePath)),
outputFile: path.normalize(path.resolve(outputFile)),
});
}
}
else if (pageStat.isFile()) {
const outputFile = path.join(categoryOutPath, path.basename(pagePath, ".md") + ".html");
parsedCategory.pages.push({
isPageGroup: false,
displayName: page.displayName,
inputFile: path.normalize(path.resolve(pagePath)),
outputFile: path.normalize(path.resolve(outputFile)),
});
}
}
// Retrieve landing page for category == first page available
if (parsedCategory.pages.length > 0) {
if (!parsedCategory.pages[0].isPageGroup) {
parsedCategory.firstPage = parsedCategory.pages[0].outputFile;
}
else if (parsedCategory.pages[0].pages !== undefined &&
parsedCategory.pages[0].pages.length > 0) {
parsedCategory.firstPage = parsedCategory.pages[0].pages[0].outputFile;
}
}
return parsedCategory;
}
/**
* Parse root config file into a JS object and check that all properties are in
* the right format.
* Exit with a meaningful error if that's not the case.
* @param {string} rootConfigFileName
* @returns {Promise.<Object>}
*/
async function parseAndCheckRootConfigFile(rootConfigFileName) {
var _a, _b, _c, _d;
let configStr;
try {
configStr = await promisify(fs.readFile)(rootConfigFileName, "utf8");
}
catch (err) {
const srcMessage = (_b = ((_a = err) !== null && _a !== void 0 ? _a : {}).message) !== null && _b !== void 0 ? _b : "Unknown error";
throw new Error('Impossible to read Root .docConfig.json file ("' +
rootConfigFileName +
'"): ' +
srcMessage);
}
let config;
try {
config = JSON.parse(configStr);
}
catch (err) {
const srcMessage = (_d = ((_c = err) !== null && _c !== void 0 ? _c : {}).message) !== null && _d !== void 0 ? _d : "Unknown error";
exitWithInvalidRootConfig(srcMessage);
}
if (typeof config !== "object" || config === null) {
exitWithInvalidRootConfig("Should be under an object form.");
}
if ("logo" in config) {
if (typeof config.logo !== "object" || config.logo === null) {
exitWithInvalidRootConfig(`The "logo" property, if defined, should contain an object.`);
}
if (!("srcPath" in config.logo) ||
typeof config.logo.srcPath !== "string") {
exitWithInvalidRootConfig(`The "logo" property, if defined, should contain a "srcPath" ` +
"property set as a string.");
}
if ("link" in config.logo && typeof config.logo.link !== "string") {
exitWithInvalidRootConfig(`The "logo.link" property, if defined, should be set as a string.`);
}
}
if ("favicon" in config) {
if (typeof config.favicon !== "object" || config.favicon === null) {
exitWithInvalidRootConfig(`The "favicon" property, if defined, should contain an object.`);
}
if (!("srcPath" in config.favicon) ||
typeof config.favicon.srcPath !== "string") {
exitWithInvalidRootConfig(`The "favicon" property, if defined, should contain a "srcPath" ` +
"property set as a string.");
}
}
if ("linksLeft" in config && !Array.isArray(config.linksLeft)) {
exitWithInvalidRootConfig(`The "linksLeft" property, if defined, should be set as an Array.`);
}
if ("linksRight" in config && !Array.isArray(config.linksRight)) {
exitWithInvalidRootConfig(`The "linksRight" property, if defined, should be set as an Array.`);
}
if ("siteMapRoot" in config && typeof config.siteMapRoot !== "string") {
exitWithInvalidRootConfig(`The "siteMapRoot" property, if defined, should be a string.`);
}
return config;
function exitWithInvalidRootConfig(reason) {
throw new Error(`Root .docConfig.json file ("${rootConfigFileName}") is invalid: ${reason}`);
}
}
/**
* @param {string} filename
* @returns {Promise.<Object>}
*/
async function parseAndCheckSubConfigFile(filename) {
var _a, _b, _c, _d;
let configStr;
try {
configStr = await promisify(fs.readFile)(filename, "utf8");
}
catch (err) {
const srcMessage = (_b = ((_a = err) !== null && _a !== void 0 ? _a : {}).message) !== null && _b !== void 0 ? _b : "Unknown error";
throw new Error(`Impossible to read "${filename}" config file: ${srcMessage}`);
}
let config;
try {
config = JSON.parse(configStr);
}
catch (err) {
const srcMessage = (_d = ((_c = err) !== null && _c !== void 0 ? _c : {}).message) !== null && _d !== void 0 ? _d : "Unknown error";
throw new Error(`"${filename}" config file is invalid: ${srcMessage}`);
}
if (typeof config !== "object" || config === null) {
throw new Error(`"${filename}" config file is invalid: ` +
`Should be under an object form.`);
}
if (!("pages" in config) ||
!Array.isArray(config.pages) ||
config.pages.length === 0) {
throw new Error(`"${filename}" config file is invalid: ` +
`Should have a "pages" property with at least one entry.`);
}
const outputPages = [];
for (let i = 0; i < config.pages.length; i++) {
const page = config.pages[i];
if (typeof page !== "object" || page === null) {
throw new Error(`"${filename}" config file is invalid: ` +
`One of the element in "pages" has an invalid format ` +
"(should be an object).");
}
if (!("path" in page) || typeof page.path !== "string") {
throw new Error(`"${filename}" config file is invalid: ` +
`One of the element in "pages" has an invalid "path" property ` +
"(should be a string).");
}
if (!("displayName" in page) || typeof page.displayName !== "string") {
throw new Error(`"${filename}" config file is invalid: ` +
`One of the element in "pages" has an invalid "displayName" property ` +
"(should be a string).");
}
const pushedPage = {
path: page.path,
displayName: page.displayName,
};
if ("defaultOpen" in page) {
if (typeof page.defaultOpen !== "boolean") {
throw new Error(`"${filename}" config file is invalid: ` +
`One of the element in "pages" has an invalid "defaultOpen" property ` +
"(should be a boolean or not defined).");
}
pushedPage.defaultOpen = page.defaultOpen;
}
outputPages.push(pushedPage);
}
// TODO make TypeScript understand our checks here
return { pages: outputPages };
}