myst-cli
Version:
Command line tools for MyST
136 lines (135 loc) • 5.52 kB
JavaScript
import { selectAll } from 'unist-util-select';
import { RuleId, fileWarn, plural, selectMdastNodes } from 'myst-common';
import { computeHash, tic } from 'myst-cli-utils';
import { addChildrenFromTargetNode } from 'myst-transforms';
import { loadFromCache, writeToCache } from '../session/cache.js';
import { migrate } from 'myst-migrate';
import { SPEC_VERSION } from '../spec-version.js';
export const XREF_MAX_AGE = 1; // in days
function mystDataFilename(dataUrl) {
return `myst-${computeHash(dataUrl)}.json`;
}
async function fetchMystData(session, dataUrl, urlSource, vfile) {
const onError = (note) => {
fileWarn(vfile, `Unable to resolve link text from external MyST reference: ${urlSource ?? dataUrl ?? ''}`, {
ruleId: RuleId.mystLinkValid,
note,
});
};
if (!dataUrl) {
return onError('Data source URL unavailable');
}
const filename = mystDataFilename(dataUrl);
const cacheData = loadFromCache(session, filename, { maxAge: XREF_MAX_AGE });
if (cacheData) {
return JSON.parse(cacheData);
}
let data;
try {
const resp = await session.fetch(dataUrl);
if (!resp.ok) {
return onError('Could not fetch data from URL');
}
data = (await resp.json());
}
catch {
return onError('Could not load fetched data');
}
// Migrate external content if version differs from current
if (data.mdast && data.version !== undefined && data.version !== SPEC_VERSION) {
session.log.info(`Migrating external content from v${data.version} to v${SPEC_VERSION} for ${urlSource}`);
try {
const migrateFile = {
version: data.version,
mdast: data.mdast,
};
const migrated = await migrate(migrateFile, {
to: SPEC_VERSION,
log: session.log,
});
data.mdast = migrated.mdast;
data.version = migrated.version;
session.log.debug(`Migrated external content from v${migrateFile.version} to v${SPEC_VERSION} for ${urlSource}`);
}
catch (error) {
session.log.warn(`Failed to migrate external content from v${data.version} to v${SPEC_VERSION}: ${error}`);
// Continue with original content if migration fails
}
}
writeToCache(session, filename, JSON.stringify(data));
return data;
}
export async function fetchMystLinkData(session, node, vfile) {
return fetchMystData(session, node.dataUrl, node.urlSource, vfile);
}
export async function fetchMystXRefData(session, node, vfile) {
let dataUrl;
if (node.remoteBaseUrl && node.dataUrl) {
dataUrl = `${node.remoteBaseUrl}${node.dataUrl}`;
}
return fetchMystData(session, dataUrl, node.urlSource, vfile);
}
export function nodesFromMystXRefData(data, identifier, vfile, opts) {
let targetNodes;
[data, ...Object.values(data.frontmatter?.parts ?? {})].forEach(({ mdast }) => {
if (!mdast || targetNodes?.length)
return;
targetNodes = selectMdastNodes(mdast, identifier, opts?.maxNodes).nodes;
});
if (!targetNodes?.length) {
fileWarn(vfile, `Unable to resolve content from external MyST reference: ${opts?.urlSource}`, {
ruleId: RuleId.mystLinkValid,
note: `Could not locate identifier ${identifier} in page content`,
});
return;
}
return targetNodes;
}
/**
* Load external MyST project data to update link text for MyST xrefs
*/
export async function transformMystXRefs(session, vfile, mdast, frontmatter) {
const toc = tic();
const nodes = selectAll('link,crossReference', mdast)
.filter((node) => {
// Only handle MyST xrefs
return node.protocol === 'xref:myst' && node.dataUrl;
})
.filter((node) => {
// If no link text, load the target to compute children
if (!node.children?.length)
return true;
// If `identifier` is present, we may need to update existing children (e.g. enumerator)
return !!node.identifier;
});
if (nodes.length === 0)
return;
session.log.debug(`Updating link text for ${plural('%s external MyST reference(s)', nodes)}`);
let number = 0;
await Promise.all([
...nodes.map(async (node) => {
if (!node.identifier) {
// Page references without specific node identifier
const data = await fetchMystLinkData(session, node, vfile);
if (!data)
return;
node.children = [{ type: 'text', value: data.frontmatter?.title ?? data.slug ?? '' }];
}
else {
const data = await fetchMystXRefData(session, node, vfile);
if (!data)
return;
const targets = nodesFromMystXRefData(data, node.identifier, vfile, {
urlSource: node.urlSource,
maxNodes: 1,
});
if (targets?.length) {
addChildrenFromTargetNode(node, targets[0], frontmatter.numbering, vfile, frontmatter.numbering?.title?.offset);
}
}
number += 1;
}),
]);
const denominator = number === nodes.length ? '' : `/${nodes.length}`;
session.log.info(toc(`🪄 Updated link text for ${plural(`%s${denominator} external MyST reference(s)`, number)} in %s`));
}