UNPKG

myst-to-typst

Version:
180 lines (179 loc) 6.72 kB
import { fileError } from 'myst-common'; export var CaptionKind; (function (CaptionKind) { CaptionKind["fig"] = "fig"; CaptionKind["eq"] = "eq"; CaptionKind["code"] = "code"; CaptionKind["table"] = "table"; })(CaptionKind || (CaptionKind = {})); function switchKind(node) { switch (node.type) { case 'iframe': case 'image': return CaptionKind.fig; case 'table': return CaptionKind.table; case 'code': return CaptionKind.code; case 'math': return CaptionKind.eq; default: return null; } } export function determineCaptionKind(node) { var _a; let kind = switchKind(node); (_a = node.children) === null || _a === void 0 ? void 0 : _a.forEach((n) => { if (!kind) kind = determineCaptionKind(n); }); return kind; } function renderFigureChild(node, state) { var _a; const bracketNode = node.type === 'div' && ((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) === 1 ? node.children[0] : node; const useBrackets = bracketNode.type !== 'image' && bracketNode.type !== 'table'; if (node.type === 'legend') { state.useMacro('#let legendStyle = (fill: black.lighten(20%), size: 8pt, style: "italic")'); state.write('text(..legendStyle)'); node.type = 'paragraph'; } if (useBrackets) state.write('[\n'); else state.write('\n '); state.renderChildren({ children: [node] }); if (useBrackets) state.write('\n]'); } export function getDefaultCaptionSupplement(kind) { if (kind === 'code') kind = 'program'; const domain = kind.includes(':') ? kind.split(':')[1] : kind; return `${domain.slice(0, 1).toUpperCase()}${domain.slice(1)}`; } export const containerHandler = (node, state) => { var _a, _b, _c; if (state.data.isInTable) { fileError(state.file, 'Unable to render typst figure inside table', { node, source: 'myst-to-typst', }); return; } state.ensureNewLine(); const prevState = state.data.isInFigure; state.data.isInFigure = true; const { identifier, kind } = node; let label = identifier; const captionTypes = node.kind === 'table' ? ['caption'] : ['caption', 'legend']; const captions = (_a = node.children) === null || _a === void 0 ? void 0 : _a.filter((child) => { return captionTypes.includes(child.type); }); let nonCaptions = (_b = node.children) === null || _b === void 0 ? void 0 : _b.filter((child) => { return !captionTypes.includes(child.type); }); nonCaptions = [ ...nonCaptions.filter((child) => child.type !== 'legend'), ...nonCaptions.filter((child) => child.type === 'legend'), ]; if (!nonCaptions || nonCaptions.length === 0) { fileError(state.file, `Figure with no non-caption content: ${label}`, { node, source: 'myst-to-typst', }); } const flatCaptions = captions .map((cap) => cap.children) .filter(Boolean) .flat(); if (node.kind === 'quote') { const prevIsInBlockquote = state.data.isInBlockquote; state.data.isInBlockquote = true; state.write('#quote(block: true'); if (flatCaptions.length > 0) { state.write(', attribution: ['); state.renderChildren(flatCaptions); state.write('])['); } else { state.write(')['); } state.renderChildren(nonCaptions); state.write(']'); state.data.isInBlockquote = prevIsInBlockquote; return; } // This resets the typst counter to match MyST numbering. // However, it is dependent on the resolved enumerator value. This will work given // default enumerators, but if the user sets numbering 'template' it will not work. // TODO: persist `numbering` metadata in a way that typst can reset based on that. if ((_c = node.enumerator) === null || _c === void 0 ? void 0 : _c.endsWith('.1')) { state.write(`#set figure(numbering: "${node.enumerator}")\n`); state.write(`#counter(figure.where(kind: "${kind}")).update(0)\n\n`); } if (nonCaptions && nonCaptions.length > 1) { const allSubFigs = nonCaptions.filter((item) => item.type === 'container').length === nonCaptions.length; state.useMacro('#import "@preview/subpar:0.1.1"'); state.useMacro('#let breakableDefault = true'); state.write(`#show figure: set block(breakable: ${allSubFigs ? 'false' : 'breakableDefault'})\n`); state.write('#subpar.grid('); let columns = nonCaptions.length <= 3 ? nonCaptions.length : 2; // TODO: allow this to be customized nonCaptions.forEach((item) => { if (item.type === 'container') { state.write('figure(\n'); state.renderChildren(item); state.write('\n, caption: []),'); // TODO: add sub-captions if (item.identifier) { state.write(` <${item.identifier}>,`); } state.write('\n'); } else { renderFigureChild(item, state); state.write(',\n'); columns = 1; } }); state.write(`columns: ${columns},\n`); if (label) { state.write(`label: <${label}>,`); label = undefined; } } else if (nonCaptions && nonCaptions.length === 1) { state.useMacro('#let breakableDefault = true'); state.write('#show figure: set block(breakable: breakableDefault)\n'); state.write('#figure('); renderFigureChild(nonCaptions[0], state); state.write(','); } else { state.useMacro('#let breakableDefault = true'); state.write('#show figure: set block(breakable: breakableDefault)\n'); state.write('#figure([\n '); state.renderChildren(node, 1); state.write('],'); } if (captions === null || captions === void 0 ? void 0 : captions.length) { state.write('\n caption: [\n'); state.renderChildren(flatCaptions); state.write('\n],'); } if (kind) { const supplement = getDefaultCaptionSupplement(kind); state.write(`\n kind: "${kind}",`); state.write(`\n supplement: [${supplement}],`); } state.write('\n)'); if (label) state.write(` <${label}>`); state.ensureNewLine(true); state.addNewLine(); state.data.isInFigure = prevState; }; export const captionHandler = () => { // blank };