myst-cli
Version:
Command line tools for MyST
269 lines (268 loc) • 12.6 kB
JavaScript
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');
}