UNPKG

myst-cli

Version:
269 lines (268 loc) 12.6 kB
import fs from 'node:fs'; import path from 'node:path'; import { filter } from 'unist-util-filter'; import { remove } from 'unist-util-remove'; import { selectAll } from 'unist-util-select'; import { MystTransformer, SphinxTransformer, } from 'myst-transforms'; import { copyNode, liftChildren, normalizeLabel, fileError, isTargetIdentifierNode, selectMdastNodes, } from 'myst-common'; import { selectFile } from '../process/file.js'; import { watch } from '../store/reducers.js'; import { castSession } from '../session/cache.js'; import { fetchMystLinkData, fetchMystXRefData, nodesFromMystXRefData } from './crossReferences.js'; import { fileFromRelativePath } from './links.js'; function mutateEmbedNode(node, targetNode, opts) { const { url, dataUrl, targetFile, sourceFile } = opts !== null && opts !== void 0 ? opts : {}; if (targetNode && node['remove-output']) { targetNode = filter(targetNode, (n) => { var _a; return n.type !== 'output' && ((_a = n.data) === null || _a === void 0 ? void 0 : _a.type) !== 'output'; }); } if (targetNode && node['remove-input']) { targetNode = filter(targetNode, (n) => { var _a; return n.type !== 'code' || ((_a = n.data) === null || _a === void 0 ? void 0 : _a.type) === 'output'; }); } selectAll('[identifier],[label],[html_id]', targetNode).forEach((idNode) => { // Non-target nodes may keep these properties if (!isTargetIdentifierNode(idNode)) return; delete idNode.identifier; delete idNode.label; delete idNode.html_id; }); selectAll('crossReference', targetNode).forEach((targetXRef) => { if (!targetXRef.remote) { targetXRef.url = url; targetXRef.dataUrl = dataUrl; targetXRef.remote = true; } }); if (targetFile && sourceFile) { // TODO: Other node types that point to a file? selectAll('image', targetNode).forEach((imageNode) => { if (imageNode.url) { const absoluteUrl = path.resolve(path.dirname(sourceFile), imageNode.url); if (fs.existsSync(absoluteUrl)) { imageNode.url = path.relative(path.dirname(targetFile), absoluteUrl); } } if (imageNode.urlSource) { const absoluteUrlSource = path.resolve(path.dirname(sourceFile), imageNode.urlSource); if (fs.existsSync(absoluteUrlSource)) { imageNode.urlSource = path.relative(path.dirname(targetFile), absoluteUrlSource); } } }); } if (!targetNode) { node.children = []; } else if (targetNode.type === 'block') { // Do not nest a single block inside an embed node.children = targetNode.children; } else { node.children = [targetNode]; } } function targetsToTarget(targets) { if (targets.length === 1) return copyNode(targets[0]); return { type: 'block', children: copyNode(targets) }; } /** * This is the {embed} directive, that embeds nodes from elsewhere in a page. */ export async function embedTransform(session, mdast, file, dependencies, state) { const references = Object.values(castSession(session).$externalReferences); const mystTransformer = new MystTransformer(references); const embedNodes = selectAll('embed', mdast); await Promise.all(embedNodes.map(async (node) => { var _a, _b, _c, _d, _e, _f, _g; const vfile = state.vfile; const label = (_a = node.source) === null || _a === void 0 ? void 0 : _a.label; if (!label) { fileError(vfile, 'Embed node does not have a label', { node }); return; } if (label.startsWith('xref:') || label.startsWith('myst:')) { if (!mystTransformer.test(label)) { let note; const sphinxTransformer = new SphinxTransformer(references); if (sphinxTransformer.test(label)) { note = 'Embed target must be a MyST project, not intersphinx.'; } else { note = 'Embed target must be a MyST project and included in your project references.'; } fileError(vfile, `Cannot embed "${label}"`, { node, note }); return; } const referenceLink = { type: 'link', url: label, urlSource: label, children: [], }; const transformed = mystTransformer.transform(referenceLink, vfile); const referenceXRef = referenceLink; if (transformed) { let data; let targetNodes; if (referenceXRef.identifier) { data = await fetchMystXRefData(session, referenceXRef, vfile); if (!data) return; targetNodes = nodesFromMystXRefData(data, referenceXRef.identifier, vfile, { urlSource: label, // TODO: maxNodes - settable via embed directive }); } else { data = await fetchMystLinkData(session, referenceLink, vfile); if (!(data === null || data === void 0 ? void 0 : data.mdast)) return; targetNodes = data.mdast.children; } if (!(targetNodes === null || targetNodes === void 0 ? void 0 : targetNodes.length)) return; const targetNode = targetsToTarget(targetNodes); selectAll('crossReference', targetNode).forEach((targetXRef) => { // If target xref already has remoteBaseUrl, it can be unchanged in the embedded context if (targetXRef.remoteBaseUrl) return; // Otherwise, the remoteBaseUrl is added from the embed context targetXRef.remoteBaseUrl = referenceXRef.remoteBaseUrl; }); selectAll('link', targetNode).forEach((targetLink) => { if (!targetLink.internal) return; targetLink.internal = false; targetLink.url = `${referenceXRef.remoteBaseUrl}${targetLink.url}`; if (targetLink.dataUrl) { targetLink.dataUrl = `${referenceXRef.remoteBaseUrl}${targetLink.dataUrl}`; } }); selectAll('[source]', targetNode).forEach((target) => { if (!target.source) return; target.source.remoteBaseUrl = referenceXRef.remoteBaseUrl; }); mutateEmbedNode(node, targetNode, referenceXRef); // Remote dependency, not added as local dependency const source = { url: referenceXRef.url, remoteBaseUrl: referenceXRef.remoteBaseUrl, label, }; if (data.kind) source.kind = data.kind; if (data.slug) source.slug = data.slug; if (data.location) source.location = data.location; if ((_b = data.frontmatter) === null || _b === void 0 ? void 0 : _b.title) source.title = data.frontmatter.title; if ((_c = data.frontmatter) === null || _c === void 0 ? void 0 : _c.short_title) source.short_title = data.frontmatter.short_title; node.source = source; } return; } let hash = label; let linkFile; if (label.includes('#')) { const linkFileWithTarget = fileFromRelativePath(label, file); if (!linkFileWithTarget) return; linkFile = linkFileWithTarget.split('#')[0]; hash = linkFileWithTarget.slice(linkFile.length + 1); } const { identifier } = (_d = normalizeLabel(hash)) !== null && _d !== void 0 ? _d : {}; if (!identifier) { fileError(vfile, 'Embed node does not have label', { node }); return; } const stateProvider = state.resolveStateProvider(identifier, linkFile); if (!stateProvider) return; const cache = castSession(session); const pageMdast = (_f = (_e = cache.$getMdast(stateProvider.filePath)) === null || _e === void 0 ? void 0 : _e.post) === null || _f === void 0 ? void 0 : _f.mdast; if (!pageMdast) return; let targetNodes; if (stateProvider.getFileTarget(identifier)) { targetNodes = pageMdast.children; } else { targetNodes = selectMdastNodes(pageMdast, identifier).nodes; } if (!(targetNodes === null || targetNodes === void 0 ? void 0 : targetNodes.length)) { fileError(vfile, `Embed target for "${label}" not found`, { node }); return; } const target = targetsToTarget(targetNodes); if (!state.states) { // Single page case - we do not need to update urls/dependencies mutateEmbedNode(node, target); return; } const { url, dataUrl, filePath } = stateProvider; mutateEmbedNode(node, target, { url, dataUrl, targetFile: file, sourceFile: filePath }); if (!url) return; const source = { url, label }; if (filePath) { session.store.dispatch(watch.actions.addLocalDependency({ path: file, dependency: filePath, })); const { kind, slug, frontmatter, location } = (_g = selectFile(session, filePath)) !== null && _g !== void 0 ? _g : {}; if (kind) source.kind = kind; if (slug) source.slug = slug; if (location) source.location = location; if (frontmatter === null || frontmatter === void 0 ? void 0 : frontmatter.title) source.title = frontmatter.title; if (frontmatter === null || frontmatter === void 0 ? void 0 : frontmatter.short_title) source.short_title = frontmatter.short_title; } node.source = source; if (!dependencies.map((dep) => dep.url).includes(url)) dependencies.push(source); })); // If a figure contains a single embed node, move the source info to the figure and lift // the embed children, eliminating the embed node. const containerNodes = selectAll('container', mdast); containerNodes.forEach((node) => { var _a, _b; const containerEmbeds = (_a = node.children) === null || _a === void 0 ? void 0 : _a.filter((child) => child.type === 'embed'); if ((containerEmbeds === null || containerEmbeds === void 0 ? void 0 : containerEmbeds.length) === 1) { node.source = { ...containerEmbeds[0].source }; containerEmbeds[0].type = '_lift'; // If the figure's embedded content is _another_ figure, just lift out the children except caption/legend if (((_b = containerEmbeds[0].children) === null || _b === void 0 ? void 0 : _b.length) === 1 && containerEmbeds[0].children[0].type === 'container') { containerEmbeds[0].children[0].type = '_lift'; // It would be nice to keep these if there is not another caption defined on 'node' but that leads to // issues with the current figure enumeration resolution, since embedding happens after referencing... remove(containerEmbeds[0].children[0], 'caption'); remove(containerEmbeds[0].children[0], 'legend'); } } }); // If embed node contains a single figure, copy the source info to the figure const remainingEmbedNodes = selectAll('embed', mdast); remainingEmbedNodes.forEach((node) => { var _a; if (((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) === 1 && node.children[0].type === 'container') { node.children[0].source = { ...node.source }; node.type = '_lift'; } }); liftChildren(mdast, '_lift'); }