UNPKG

myst-cli

Version:
136 lines (135 loc) 5.52 kB
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`)); }