UNPKG

myst-cli

Version:
218 lines (217 loc) 10.1 kB
import fs from 'node:fs'; import path from 'node:path'; import util from 'util'; import chalk from 'chalk'; import { pdfTexExportCommand, texMakeGlossariesCommand } from 'jtex'; import { exec, tic } from 'myst-cli-utils'; import { RuleId, TemplateKind, fileError, fileWarn } from 'myst-common'; import MystTemplate from 'myst-templates'; import { VFile } from 'vfile'; import { docLinks } from '../../docs.js'; import { uniqueArray } from '../../utils/uniqueArray.js'; import { logMessagesFromVFile } from '../../utils/logging.js'; import { createTempFolder } from '../../utils/createTempFolder.js'; import { cleanOutput } from '../utils/cleanOutput.js'; import { isLatexmkAvailable, isMakeglossariesAvailable } from './utils.js'; const copyFile = util.promisify(fs.copyFile); function copyContents(srcFolder, destFolder) { fs.readdirSync(srcFolder).forEach((item) => { const srcItemPath = path.join(srcFolder, item); const destItemPath = path.join(destFolder, item); if (fs.lstatSync(srcItemPath).isDirectory()) { fs.mkdirSync(destItemPath); copyContents(srcItemPath, destItemPath); } else { fs.copyFileSync(srcItemPath, destItemPath); } }); } export function getLogOutputFolder(pdfOutput) { const pdfBasename = path.basename(pdfOutput, path.extname(pdfOutput)); return path.join(path.dirname(pdfOutput), `${pdfBasename}_pdf_logs`); } export function getTexOutputFolder(pdfOutput) { const pdfBasename = path.basename(pdfOutput, path.extname(pdfOutput)); return path.join(path.dirname(pdfOutput), `${pdfBasename}_pdf_tex`); } export async function createPdfGivenTexExport(session, texExportOptions, pdfOutput, copyLogs, clean, glossaries) { if (clean) cleanOutput(session, pdfOutput); const vfile = new VFile(); vfile.path = pdfOutput; const { output: texOutput, template } = texExportOptions; const templateLogString = `(${template !== null && template !== void 0 ? template : 'default'})`; const { buildPath, texFile, texLogFile, pdfBuild, logBuild, texLogBuild, logOutputFolder, logOutput, texLogOutput, } = ensurePaths(session, texOutput, pdfOutput); if (clean) cleanOutput(session, logOutputFolder); const { buildError, toc } = await runCommands(session, template, texFile, texLogFile, templateLogString, pdfBuild, buildPath, vfile, glossaries); const logs = uniqueArray(fs .readFileSync(logBuild) .toString() .split('\n') .filter((line) => line.includes('WARN ') || line.includes('LaTeX Error') || line.includes('LaTeX Warning')), (line) => (line.indexOf('WARN') > -1 ? line.slice(line.indexOf('WARN')) : line)).filter((line) => !line.includes('Unused global option')); // Remove the trivial errors const packageErrors = logs .map((line) => { var _a; return (line.includes('LaTeX Error') ? (_a = line.match(/`(.*)\.sty'/)) === null || _a === void 0 ? void 0 : _a[1] : undefined); }) .filter((sty) => !!sty) .map((p) => `${p}.sty`); session.log.debug(`Unknown style files: "${packageErrors.join('", "')}"`); // Here we could search for packages and install: // const packages = await Promise.all( // packageErrors.map((sty) => searchForPackage(session, sty, { cwd: buildPath })), // ); if (logs.length > 0) { if (buildError) { if (fs.existsSync(pdfBuild)) { session.log.info('\n Please check your PDF, it may actually be fine? 🤷'); } else { session.log.info('\n The PDF did not compile, here are the logs:'); } } else { session.log.info(`\n LaTeX ${chalk.green('succeeded')} with warnings:`); } session.log.info(chalk.dim(`\n Console: ${texLogBuild}\n Detailed logs: ${logBuild}\n Build path: ${buildPath}\n`)); session.log.info(`\n ${chalk.dim('Preview:')}\n ${logs .map((line) => (line.includes('LaTeX Error') ? chalk.red(line) : chalk.yellowBright(line))) .join('\n ')}\n`); } const pdfBuildExists = fs.existsSync(pdfBuild); const logBuildExists = fs.existsSync(logBuild); const texLogBuildExists = fs.existsSync(texLogBuild); if (pdfBuildExists && !fs.existsSync(path.dirname(pdfOutput))) { fs.mkdirSync(path.dirname(pdfOutput), { recursive: true }); } if (pdfBuildExists) { session.log.info(toc(`📄 Exported PDF ${templateLogString} in %s, copying to ${pdfOutput}`)); await copyFile(pdfBuild, pdfOutput); session.log.debug(`Copied PDF file to ${pdfOutput}`); } else { fileError(vfile, `Could not find ${pdfBuild} as expected`, { ruleId: RuleId.pdfBuilds }); } if (copyLogs) { if ((logBuildExists || texLogBuildExists) && !fs.existsSync(path.dirname(logOutput))) { fs.mkdirSync(path.dirname(logOutput), { recursive: true }); } if (logBuildExists) { session.log.debug(`Copying log file: ${logOutput}`); await copyFile(logBuild, logOutput); } if (texLogBuildExists) { session.log.debug(`Copying log file: ${texLogOutput}`); await copyFile(texLogBuild, texLogOutput); } } logMessagesFromVFile(session, vfile); const logFiles = copyLogs ? [logOutput, texLogOutput] : [logBuild, texLogBuild]; if (!fs.existsSync(pdfOutput)) { const err = Error(`Error exporting: ${pdfOutput}`); err.logFiles = logFiles; throw err; } return { logFiles, tempFolders: [buildPath] }; } function ensurePaths(session, texOutput, pdfOutput) { const buildPath = createTempFolder(session); const texFile = path.basename(texOutput); const texBuild = path.join(buildPath, texFile); copyContents(path.dirname(texOutput), buildPath); if (!fs.existsSync(texBuild)) { throw Error(`Error exporting: ${pdfOutput}\nCould not find tex file: ${texOutput}`); } const pdfBasename = path.basename(pdfOutput, path.extname(pdfOutput)); const pdfFile = `${pdfBasename}.pdf`; const pdfBuild = path.join(buildPath, pdfFile); const logFile = `${pdfBasename}.log`; const texLogFile = `${pdfBasename}.shell.log`; // Temporary log file locations const logBuild = path.join(buildPath, logFile); const texLogBuild = path.join(buildPath, texLogFile); // Log file location saved alongside pdf const logOutputFolder = getLogOutputFolder(pdfOutput); const logOutput = path.join(logOutputFolder, logFile); const texLogOutput = path.join(logOutputFolder, texLogFile); return { buildPath, texFile, texLogFile, pdfBuild, logBuild, texLogBuild, logOutputFolder, logOutput, texLogOutput, }; } async function runCommands(session, template, texFile, texLogFile, templateLogString, pdfBuild, buildPath, vfile, glossaries) { const toc = tic(); let buildError = await runPdfBuildCommand(session, texFile, texLogFile, templateLogString, template, pdfBuild, buildPath, vfile); if (buildError || !glossaries) { return { buildError, toc }; } // Glossaries require two more commands to be run buildError = await runGlossariesBuildCommand(session, texFile, texLogFile, buildPath); if (buildError) { return { buildError, toc }; } buildError = await runPdfBuildCommand(session, texFile, texLogFile, templateLogString, template, pdfBuild, buildPath, vfile, true); return { buildError, toc }; } async function runPdfBuildCommand(session, texFile, texLogFile, templateLogString, template, pdfBuild, buildPath, vfile, debugLogsOnly) { if (!isLatexmkAvailable()) { fileError(vfile, `⚠️ The "latexmk" command is not available. See documentation on installing LaTeX:\n\n${docLinks.installLatex}`, { ruleId: RuleId.pdfBuildCommandsAvailable }); } let buildCommand; if (!template) { buildCommand = pdfTexExportCommand(texFile, texLogFile); } else { const mystTemplate = new MystTemplate(session, { kind: TemplateKind.tex, template: template || undefined, buildDir: session.buildPath(), errorLogFn: (message) => { fileError(vfile, message, { ruleId: RuleId.pdfBuilds }); }, warningLogFn: (message) => { fileWarn(vfile, message, { ruleId: RuleId.pdfBuilds }); }, }); buildCommand = pdfTexExportCommand(texFile, texLogFile, mystTemplate); } let buildError = false; try { if (!debugLogsOnly) session.log.info(`🖨 Rendering PDF ${templateLogString} to ${pdfBuild}`); session.log.debug(`Running command:\n> ${buildCommand}`); await exec(buildCommand, { cwd: buildPath }); session.log.debug(`Done building LaTeX.`); } catch (err) { session.log.debug(err.stack); fileError(vfile, `LaTeX reported an error building your PDF ${templateLogString} for ${texFile}`, { ruleId: RuleId.pdfBuildsWithoutErrors }); buildError = true; } return buildError; } async function runGlossariesBuildCommand(session, texFile, texLogFile, buildPath) { if (!isMakeglossariesAvailable()) { session.log.error(`⚠️ The "makeglossaries" command is not available. See documentation on installing LaTeX:\n\n${docLinks.installLatex}`); } const buildCommand = texMakeGlossariesCommand(texFile, texLogFile); let buildError = false; try { session.log.info(`🔖 Creating glossaries in ${buildPath}`); session.log.debug(`Running command:\n> ${buildCommand}`); await exec(buildCommand, { cwd: buildPath }); session.log.debug(`Done building glossaries.`); } catch (err) { session.log.debug(err.stack); session.log.error(`🛑 Command makeglossaries reported an error for ${texFile}`); buildError = true; } return buildError; }