UNPKG

myst-cli

Version:
239 lines (238 loc) 11.5 kB
import fs from 'node:fs'; import path from 'node:path'; import AdmZip from 'adm-zip'; import { glob } from 'glob'; import mime from 'mime-types'; import { copyFileMaintainPath, copyFileToFolder, isDirectory, tic } from 'myst-cli-utils'; import { RuleId, fileError, fileWarn } from 'myst-common'; import { ExportFormats } from 'myst-frontmatter'; import { selectAll } from 'unist-util-select'; import { VFile } from 'vfile'; import { createManifestXml } from 'meca'; import { runJatsExport } from '../jats/single.js'; import { loadFile } from '../../process/file.js'; import { writeTOCToConfigFile } from '../../project/toTOC.js'; import { castSession } from '../../session/cache.js'; import { selectors } from '../../store/index.js'; import { createTempFolder } from '../../utils/createTempFolder.js'; import { logMessagesFromVFile } from '../../utils/logging.js'; import { cleanOutput } from '../utils/cleanOutput.js'; import { collectExportOptions } from '../utils/collectExportOptions.js'; function mediaTypeFromFile(file) { const mediaType = mime.lookup(file); if (mediaType) return mediaType; const ext = path.extname(file); // A couple common extras not included with 'mime-types' if (ext === '.ipynb') return 'application/x-ipynb+json'; if (ext === '.bib') return 'application/x-bibtex'; return ''; } function addManifestItem(manifestItems, itemType, mecaFolder, file, mediaType) { if (!file) return; const hrefPath = path.relative(mecaFolder, file).split(path.sep).join('/'); const hrefTrail = isDirectory(file) ? '/' : ''; manifestItems.push({ href: `${hrefPath}${hrefTrail}`, itemType, mediaType: mediaType ? mediaType : mediaTypeFromFile(file), }); } function bundleFolder(folder) { return path.join(folder, 'bundle'); } async function copyFilesFromConfig(session, projectPath, mecaFolder, manifestItems, errorLogFn) { var _a, _b; const projConfig = selectors.selectLocalProjectConfig(session.store.getState(), projectPath); const entries = [ ...((_a = projConfig === null || projConfig === void 0 ? void 0 : projConfig.requirements) !== null && _a !== void 0 ? _a : []).map((entry) => { return { itemType: 'article-source-environment', entry }; }), ...((_b = projConfig === null || projConfig === void 0 ? void 0 : projConfig.resources) !== null && _b !== void 0 ? _b : []).map((entry) => { return { itemType: 'article-source', entry }; }), ]; if (entries.length) { await Promise.all(entries.map(async ({ itemType, entry }) => { const resolvedEntry = [...projectPath.split(path.sep), entry].join('/'); const matches = await glob(resolvedEntry); matches .map((match) => match.split('/').join(path.sep)) .filter((match) => !isDirectory(match)) .forEach((match) => { const destination = copyFileMaintainPath(session, match, projectPath, bundleFolder(mecaFolder), errorLogFn); addManifestItem(manifestItems, itemType, mecaFolder, destination); }); })); } } /** * Copy files referenced from image, link, and include nodes, as well as thumbnail */ async function copyDependentFiles(session, sourceFile, projectPath, mecaFolder, manifestItems, errorLogFn) { var _a; const cache = castSession(session); if (!cache.$getMdast(sourceFile)) { await loadFile(session, sourceFile, projectPath); } const pre = (_a = cache.$getMdast(sourceFile)) === null || _a === void 0 ? void 0 : _a.pre; if (!pre) return; const { mdast, frontmatter } = pre; const urlNodes = selectAll('image,link', mdast); const fileNodes = selectAll('include', mdast); const filesToCopy = [ ...urlNodes.map((node) => node.url), ...fileNodes.map((node) => node.file), frontmatter === null || frontmatter === void 0 ? void 0 : frontmatter.thumbnail, ] .filter((file) => !!file) .map((file) => path.resolve(path.dirname(sourceFile), file)) .filter((file) => fs.existsSync(file)); filesToCopy.forEach((file) => { const dependency = copyFileMaintainPath(session, file, projectPath, bundleFolder(mecaFolder), errorLogFn); addManifestItem(manifestItems, 'article-source', mecaFolder, dependency); }); } function writeMecaManifest(manifestItems, mecaFolder) { const manifest = createManifestXml(manifestItems); fs.writeFileSync(path.join(mecaFolder, 'manifest.xml'), manifest); } /** * Meca export collects into a folder and zips: JATS (and dependent files), pdf/docx exports, * source markdown/ipynb/etc (and dependent files), table of contents, config yaml file, * and any resources/requirements defined in the config file. It also * writes an xml manifest file describing all the contents. */ export async function runMecaExport(session, sourceFile, exportOptions, opts) { var _a; const toc = tic(); const { output, articles } = exportOptions; const { projectPath, clean, extraLinkTransformers } = opts !== null && opts !== void 0 ? opts : {}; // At this point, export options are resolved to contain zero or one articles const articleFile = (_a = articles === null || articles === void 0 ? void 0 : articles[0]) === null || _a === void 0 ? void 0 : _a.file; const vfile = new VFile(); vfile.path = output; const fileCopyErrorLogFn = (m) => { fileError(vfile, m, { ruleId: RuleId.mecaFilesCopied }); }; if (clean) cleanOutput(session, output); const mecaFolder = createTempFolder(session); const manifestItems = []; const jatsExports = await collectExportOptions(session, articleFile ? [articleFile] : [], [ExportFormats.xml], { projectPath, }); if (jatsExports.length === 0 && articleFile) { // If no JATS export is defined but MECA specifies an article, build JATS implicitly from that article const jatsOutput = path.join(mecaFolder, 'article.xml'); await runJatsExport(session, sourceFile, { ...exportOptions, output: jatsOutput }, { projectPath, clean, extraLinkTransformers }); addManifestItem(manifestItems, 'article-metadata', mecaFolder, jatsOutput); const jatsFiles = path.join(mecaFolder, 'files'); if (fs.existsSync(jatsFiles)) { fs.readdirSync(jatsFiles).forEach((file) => { addManifestItem(manifestItems, 'article-supporting-file', mecaFolder, path.join(mecaFolder, 'files', file)); }); } } else if (jatsExports.length === 0) { fileWarn(vfile, `No JATS export found for inclusion with MECA bundle`, { ruleId: RuleId.mecaIncludesJats, }); } else if (jatsExports.length > 1) { fileWarn(vfile, `Multiple JATS exports found for inclusion with MECA bundle\nConventionally, MECA should only have one JATS article`, { ruleId: RuleId.mecaIncludesJats, }); } // Copy any existing JATS exports (and dependent files) jatsExports.forEach(({ output: jatsOutput }) => { if (!fs.existsSync(jatsOutput)) { fileWarn(vfile, `Other exports must be built prior to building MECA bundle\nTo resolve this, run: myst build --all`, { ruleId: RuleId.mecaExportsBuilt, }); } const jatsDest = copyFileToFolder(session, jatsOutput, mecaFolder, fileCopyErrorLogFn); if (jatsDest) { addManifestItem(manifestItems, 'article-metadata', mecaFolder, jatsDest); } const jatsFiles = path.join(path.dirname(jatsOutput), 'files'); if (fs.existsSync(jatsFiles)) { fs.readdirSync(jatsFiles).forEach((file) => { const src = path.join(jatsFiles, file); const fileDest = copyFileToFolder(session, src, path.join(mecaFolder, 'files'), fileCopyErrorLogFn); addManifestItem(manifestItems, 'article-supporting-file', mecaFolder, fileDest); }); } }); // Copy any existing pdf/docx/tex-zip exports const manuscriptExports = (await collectExportOptions(session, articleFile ? [articleFile] : [], [ExportFormats.docx, ExportFormats.pdf, ExportFormats.tex], { projectPath, })).filter((exp) => { // Do not copy unzipped tex exports return exp.format !== ExportFormats.tex || path.extname(exp.output) === '.zip'; }); manuscriptExports.forEach(({ output: manuscriptOutput }) => { if (!fs.existsSync(manuscriptOutput)) { fileWarn(vfile, `Other exports must be built prior to building MECA bundle\nTo resolve this, run: myst build --all`, { ruleId: RuleId.mecaExportsBuilt, }); } const manuscriptDest = copyFileToFolder(session, manuscriptOutput, mecaFolder, fileCopyErrorLogFn); addManifestItem(manifestItems, 'manuscript', mecaFolder, manuscriptDest); }); const project = projectPath ? selectors.selectLocalProject(session.store.getState(), projectPath) : undefined; const bundle = bundleFolder(mecaFolder); if (projectPath && project) { // Copy myst.yml const configFile = selectors.selectLocalConfigFile(session.store.getState(), projectPath); if (configFile) { const configDest = copyFileMaintainPath(session, configFile, projectPath, bundle, fileCopyErrorLogFn); // Ensure that an explicit TOC is present if (configDest) { await writeTOCToConfigFile(project, configFile, configDest); } addManifestItem(manifestItems, 'article-source', mecaFolder, configDest); // Copy requirements and resources await copyFilesFromConfig(session, projectPath, mecaFolder, manifestItems, fileCopyErrorLogFn); } // Write all source markdown/ipynb/etc files const projectPages = [ { page: project.file, itemType: 'article-source' }, ...project.pages .filter((page) => { return !!page.file; }) .map((page) => { return { page: page.file, itemType: 'article-source' }; }), ...project.bibliography.map((bib) => { return { page: bib, itemType: 'article-source' }; }), ]; await Promise.all(projectPages.map(async ({ page, itemType }) => { const pageDest = copyFileMaintainPath(session, page, projectPath, bundle, fileCopyErrorLogFn); addManifestItem(manifestItems, itemType, mecaFolder, pageDest); await copyDependentFiles(session, page, projectPath, mecaFolder, manifestItems, fileCopyErrorLogFn); })); } else if (articleFile) { const articleDest = copyFileToFolder(session, articleFile, bundle, fileCopyErrorLogFn); addManifestItem(manifestItems, 'article-source', mecaFolder, articleDest); } if (fs.existsSync(bundle)) { addManifestItem(manifestItems, 'article-source-directory', mecaFolder, path.join(bundle, ''), 'application/x-directory'); } writeMecaManifest(manifestItems, mecaFolder); const zip = new AdmZip(); zip.addLocalFolder(mecaFolder); zip.writeZip(output); logMessagesFromVFile(session, vfile); session.log.info(toc(`🤐 MECA output copied and zipped to ${output} in %s`)); return { tempFolders: [] }; }