myst-cli
Version:
Command line tools for MyST
233 lines (232 loc) • 10.3 kB
JavaScript
import path from 'node:path';
import chalk from 'chalk';
import { EXT_TO_FORMAT, ExportFormats } from 'myst-frontmatter';
import { findCurrentProjectAndLoad } from '../config.js';
import { filterPages, loadProjectFromDisk } from '../project/load.js';
import { selectors } from '../store/index.js';
import { writeJsonLogs } from '../utils/logging.js';
import { uniqueArray } from '../utils/uniqueArray.js';
import { buildSite } from './site/prepare.js';
import { localArticleExport } from './utils/localArticleExport.js';
import { collectExportOptions, resolveExportListArticles } from './utils/collectExportOptions.js';
import { buildHtml } from './html/index.js';
import { binaryName, readableName } from '../utils/whiteLabelling.js';
export function hasAnyExplicitExportFormat(opts) {
const { docx, pdf, tex, typst, xml, md, meca, cff } = opts;
return docx || pdf || tex || typst || xml || md || meca || cff || false;
}
/**
* Determine all allowed export formats based on CLI options and arguments
*
* @param opts.docx docx export explicitly requested with --docx option
* @param opts.pdf pdf export explicitly requested with --pdf option
* @param opts.tex (all formats follow as above)
* @param opts.typst
* @param opts.xml
* @param opts.md
* @param opts.meca
* @param opts.all all exports requested with --all option
* @param opts.explicit explicit input file was provided
*/
export function getAllowedExportFormats(opts) {
const { docx, pdf, tex, typst, xml, md, meca, cff, all, explicit } = opts;
const formats = [];
const any = hasAnyExplicitExportFormat(opts);
const override = all || (!any && explicit);
if (docx || override)
formats.push(ExportFormats.docx);
if (pdf || override) {
formats.push(ExportFormats.pdf, ExportFormats.pdftex, ExportFormats.typst);
}
if (tex || override) {
formats.push(ExportFormats.tex, ExportFormats.pdftex);
}
if (typst || override)
formats.push(ExportFormats.typst);
if (xml || override)
formats.push(ExportFormats.xml);
if (md || override)
formats.push(ExportFormats.md);
if (meca || override)
formats.push(ExportFormats.meca);
if (cff || override)
formats.push(ExportFormats.cff);
return [...new Set(formats)];
}
/**
* Return requested formats from CLI options
*/
export function getRequestedExportFormats(opts) {
const { docx, pdf, tex, typst, xml, md, meca, cff } = opts;
const formats = [];
if (docx)
formats.push(ExportFormats.docx);
if (pdf)
formats.push(ExportFormats.pdf);
if (tex)
formats.push(ExportFormats.tex);
if (typst)
formats.push(ExportFormats.typst);
if (xml)
formats.push(ExportFormats.xml);
if (md)
formats.push(ExportFormats.md);
if (meca)
formats.push(ExportFormats.meca);
if (cff)
formats.push(ExportFormats.cff);
return formats;
}
export function exportSite(session, opts) {
const { force, site, html, all } = opts;
const siteConfig = selectors.selectCurrentSiteConfig(session.store.getState());
return site || html || all || (siteConfig && !force && !hasAnyExplicitExportFormat(opts));
}
export function getProjectPaths(session) {
var _a, _b, _c;
const siteConfig = selectors.selectCurrentSiteConfig(session.store.getState());
const projectPaths = [
(_a = selectors.selectCurrentProjectPath(session.store.getState())) !== null && _a !== void 0 ? _a : path.resolve('.'),
...((_c = (_b = siteConfig === null || siteConfig === void 0 ? void 0 : siteConfig.projects) === null || _b === void 0 ? void 0 : _b.map((proj) => proj.path).filter((projectPath) => !!projectPath)) !== null && _c !== void 0 ? _c : []),
];
return uniqueArray(projectPaths);
}
/**
* Gather a list of export objects with resolved format/input/output values
*
* @param files list of files to export; if empty, current project will be loaded
* and all project files (including the config file) will be searched for exports
* @param opts user-provided options, including desired export formats and output filename
*/
export async function collectAllBuildExportOptions(session, files, opts) {
var _a;
const { output } = opts;
files = files.map((file) => path.resolve(file));
if (output && files.length !== 1) {
throw new Error('When specifying a named output for export, you must list exactly one file.');
}
const requestedFormats = getRequestedExportFormats(opts);
if (output && requestedFormats.length > 1) {
throw new Error(`When specifying output, you can only request one format`);
}
let exportOptionsList;
const projectPath = await findCurrentProjectAndLoad(session, files[0] ? path.dirname(files[0]) : '.');
if (projectPath)
await loadProjectFromDisk(session, projectPath);
if (output) {
session.log.debug(`Exporting formats: "${requestedFormats.join('", "')}"`);
const format = (_a = requestedFormats[0]) !== null && _a !== void 0 ? _a : EXT_TO_FORMAT[path.extname(output)];
if (!format) {
throw new Error(`Cannot specify format from output "${output}" - please specify format option, e.g. --pdf`);
}
exportOptionsList = resolveExportListArticles(session, files[0], [{ format, output: path.join(path.resolve('.'), output) }], projectPath, opts).map((exp) => {
return { ...exp, $file: files[0], $project: projectPath };
});
}
else if (files.length) {
const allowedFormats = getAllowedExportFormats({
...opts,
explicit: true,
});
session.log.debug(`Exporting formats: "${allowedFormats.join('", "')}"`);
exportOptionsList = (await Promise.all(files.map(async (file) => {
const fileExportOptionsList = await collectExportOptions(session, [file], allowedFormats, opts);
// If requested exports were defined in file frontmatter, return those.
if (fileExportOptionsList.length > 0) {
return fileExportOptionsList;
}
// If no requested exports were found, force them to build.
return resolveExportListArticles(session, file, requestedFormats.map((format) => {
return { format };
}), projectPath, opts).map((exp) => {
return { ...exp, $file: files[0], $project: projectPath };
});
}))).flat();
}
else {
const allowedFormats = getAllowedExportFormats(opts);
session.log.debug(`Exporting formats: "${allowedFormats.join('", "')}"`);
const projectPaths = getProjectPaths(session);
exportOptionsList = (await Promise.all(projectPaths.map(async (projPath) => {
try {
const project = await loadProjectFromDisk(session, projPath);
files = filterPages(project).map((page) => page.file);
}
catch (err) {
session.log.debug(`Unable to load any content from project at: ${projPath}\n${err}`);
return [];
}
const exportOptions = await collectExportOptions(session, files, allowedFormats, {
...opts,
projectPath: projPath,
});
return exportOptions;
}))).flat();
}
return exportOptionsList;
}
function extToKind(ext) {
// We promote `jats` in the docs, even though extension is `.xml`
if (ext === 'xml')
return 'jats';
return ext;
}
export async function build(session, files, opts) {
const { site, all, watch, writeDOIBib } = opts;
const performSiteBuild = all || (files.length === 0 && exportSite(session, opts)) || writeDOIBib;
const exportOptionsList = await collectAllBuildExportOptions(session, files, opts);
// TODO: generalize and pull this out!
const buildLog = {
input: {
files: files,
opts: opts,
performSiteBuild,
},
exports: exportOptionsList,
};
const exportLogList = exportOptionsList.map((exportOptions) => {
return `${path.relative('.', exportOptions.$file)} -> ${exportOptions.output}`;
});
if (exportLogList.length === 0) {
if (!(site || performSiteBuild)) {
// Print out the kinds that are filtered
const kinds = Object.entries(opts)
.filter(([k, v]) => ['docx', 'pdf', 'tex', 'typst', 'xml', 'md', 'meca', 'cff'].includes(k) && v)
.map(([k]) => k);
session.log.info(`📭 No file exports${kinds.length > 0 ? ` with kind "${kinds.map(extToKind).join('", "')}"` : ''} found.`);
if (kinds.length) {
session.log.info(chalk.dim(`You may need to add an 'exports' field to the frontmatter of the file(s) you wish to export:\n\n---\nexports:\n - format: ${extToKind(kinds[0])}\n---`));
}
else {
session.log.info(chalk.dim(`You may need to specify either:\n - an export format, e.g. \`${binaryName()} build --pdf\`\n - a file to export, e.g. \`${binaryName()} build my-file.md\``));
}
}
}
else {
session.log.info(`📬 Performing exports:\n ${exportLogList.join('\n ')}`);
await localArticleExport(session, exportOptionsList, opts);
}
if (performSiteBuild) {
const siteConfig = selectors.selectCurrentSiteConfig(session.store.getState());
if (!siteConfig) {
session.log.info('🌎 No site configuration found.');
session.log.debug(`To build a site, first run '${binaryName()} init --site'`);
}
else {
session.log.info(`🌎 Building ${readableName()} site`);
if (watch) {
session.log.warn(`Site content will not be watched and updated; use '${binaryName()} start' instead`);
}
if (opts.html) {
buildLog.buildHtml = true;
await buildHtml(session, opts);
}
else {
buildLog.buildSite = true;
await buildSite(session, opts);
}
}
}
writeJsonLogs(session, 'myst.build.json', buildLog);
session.dispose();
}