@alauda/doom
Version:
Doctor Doom making docs.
216 lines (215 loc) • 9.29 kB
JavaScript
import fs from 'node:fs/promises';
import path from 'node:path';
import { generatePdf, } from '@alauda/doom-export';
import { logger, serve } from '@rspress/core';
import { removeLeadingSlash } from '@rspress/shared';
import { Command } from 'commander';
import { groupBy } from 'es-toolkit';
import { cyan, yellow } from 'yoctocolors';
import { autoSidebar, } from '../plugins/index.js';
import { getPdfName } from '../shared/index.js';
import { pathExists, setNodeEnv } from '../utils/index.js';
import { loadConfig } from './load-config.js';
const collectPages = (sidebarItems, base) => {
const pages = new Map();
for (const item of sidebarItems) {
if ('link' in item && item.link) {
const link = removeLeadingSlash(item.link);
pages.set(link, {
key: link,
path: base + link + '.html?print',
title: item.text,
});
}
if ('items' in item) {
for (const page of collectPages(item.items, base)) {
if (!pages.has(page.key)) {
pages.set(page.key, page);
}
}
}
}
return [...pages.values()];
};
export const exportCommand = new Command('export')
.description('Export the documentation as PDF, `apis/**` and `*/apis/**` routes will be ignored automatically')
.argument('[root]', 'Root directory of the documentation')
.option('-H, --host [host]', 'Serve host name', 'localhost')
.option('-P, --port [port]', 'Serve port number', '4173')
.action(async function (root) {
setNodeEnv('production');
const { port, host, ...globalOptions } = this.optsWithGlobals();
let { config, configFilePath } = await loadConfig(root, {
...globalOptions,
export: true,
});
const outDir = config.outDir;
if (!(await pathExists(path.resolve(outDir, 'index.html'), 'file'))) {
logger.error('Please build the documentation first');
process.exit(1);
}
config = await autoSidebar(config, {
ignore: globalOptions.ignore,
export: true,
});
config.builderConfig.server.open = false;
// make sure it won't be overridden by `serve`
const themeConfig = { ...config.themeConfig };
logger.start('Serving...');
await serve({ config, configFilePath, host, port });
const tempDir = path.resolve(outDir, '.doom');
const commonOptions = {
tempDir,
port: port,
host: host,
outDir,
pdfOptions: {
margin: {
top: 40,
right: 40,
bottom: 40,
left: 40,
},
printBackground: true,
headerTemplate: /* HTML */ `<div
style="margin-top: -0.4cm; height: 70%; width: 100%; display: flex; justify-content: center; align-items: center; color: lightgray; border-bottom: solid lightgray 1px; font-size: 10px;"
>
<span class="title"></span>
</div>`,
footerTemplate: /* HTML */ `<div
style="margin-bottom: -0.4cm; height: 70%; width: 100%; display: flex; justify-content: flex-start; align-items: center; color: lightgray; border-top: solid lightgray 1px; font-size: 10px;"
></div>`,
},
printerOptions: {
ignoreHTTPSErrors: true,
initScripts: [
// eslint-disable-next-line @typescript-eslint/require-await
async () => {
localStorage.setItem('rspress-theme-appearance', 'light');
localStorage.setItem('rspress-visited', '1');
},
],
outlineContainerSelector: '.rp-doc',
outlineExcludeSelector: '.rp-toc-exclude *',
},
};
const exportPdf = async (sidebarItems, lang = config.lang, { includeApisPages, overrideOutlines, ...options } = {}) => {
let pages = collectPages(sidebarItems, config.base);
if (!includeApisPages) {
pages = pages.filter((page) => themeConfig.locales?.length
? !themeConfig.locales.some((l) => page.key.startsWith(l.lang === config.lang ? 'apis/' : `${l.lang}/apis/`))
: !page.key.startsWith('apis/'));
}
const pdfOptions = {
pages,
outFile: getPdfName(lang, config.userBase, config.title),
cleanupTempDir: false,
customOutlines: overrideOutlines
? (pages) => {
const pageMap = new Map();
let pageIndex = 0;
for (const page of pages) {
pageMap.set(page.url.split('?')[0], {
title: page.title,
pageIndex,
});
pageIndex += page.count;
}
const generateOutlinesFromSidebar = (sidebarItems) => {
const result = [];
for (const item of sidebarItems) {
let children;
if ('items' in item) {
children = generateOutlinesFromSidebar(item.items);
}
if ('link' in item && item.link) {
const page = pageMap.get(`${config.base}${removeLeadingSlash(item.link)}.html`);
if (page != null) {
result.push({
title: page.title || item.text,
to: page.pageIndex,
children,
});
}
}
}
return result;
};
return generateOutlinesFromSidebar(sidebarItems);
}
: undefined,
...commonOptions,
...options,
};
logger.start(`Exporting ${lang} language documents with ${pages.length} pages into ${yellow(pdfOptions.outFile)}...`);
return generatePdf(pdfOptions);
};
const findScopeFactory = (scope) => function findScope(sidebarItems) {
const found = new Set();
for (const item of sidebarItems) {
if (!('_fileKey' in item) || !item._fileKey) {
continue;
}
if (scope.includes(item._fileKey)) {
found.add(item);
}
if ('items' in item) {
const found_ = findScope(item.items);
if (found_?.size) {
found_.forEach((i) => found.add(i));
}
}
}
return found;
};
const exportItems = config.export || [];
// eslint-disable-next-line @typescript-eslint/no-useless-default-assignment
const { apis = [], nonApis = [] } = groupBy(exportItems, (item) => item.scope.some((s) => s.startsWith('apis/') || s.startsWith('*/apis/'))
? 'apis'
: 'nonApis');
const exportEntries = async (sidebarItems, allOutlines, lang = config.lang) => {
for (const item of nonApis) {
const found = findScopeFactory(item.flattenScope)(sidebarItems);
if (!found?.size) {
logger.warn(`Cannot find matched scope \`${cyan(item.scope.join(', '))}\` for lang ${lang}, skip exporting`);
continue;
}
const foundSidebarItems = [...found];
await exportPdf(foundSidebarItems, lang, {
allOutlines,
outFile: `${item.name || (foundSidebarItems.find((it) => it.link?.endsWith('/index')) ?? foundSidebarItems[0]).text}-${lang}.pdf`,
});
}
for (const item of apis) {
const found = findScopeFactory(item.flattenScope)(sidebarItems);
if (!found?.size) {
logger.warn(`Cannot find matched scope \`${cyan(item.scope.join(', '))}\` for lang ${lang}, skip exporting`);
continue;
}
const foundSidebarItems = [...found];
await exportPdf(foundSidebarItems, lang, {
includeApisPages: true,
outFile: `${item.name || (foundSidebarItems.find((it) => it.link?.endsWith('/index')) ?? foundSidebarItems[0]).text}-${lang}.pdf`,
});
}
};
if (themeConfig.locales?.length) {
for (const { lang, sidebar } of themeConfig.locales) {
const sidebarItems = sidebar[config.lang === lang ? '/' : `/${lang}`];
const { allOutlines } = await exportPdf(sidebarItems, lang, {
overrideOutlines: true,
});
await exportEntries(sidebarItems, allOutlines, lang);
}
}
else {
const sidebarItems = themeConfig.sidebar['/'];
const { allOutlines } = await exportPdf(sidebarItems, undefined, {
overrideOutlines: true,
});
await exportEntries(sidebarItems, allOutlines);
}
await fs.rm(tempDir, { force: true, recursive: true });
logger.ready('Closing the server');
process.exit(0);
});