myst-to-typst
Version:
Export from MyST mdast to Typst
180 lines (179 loc) • 6.72 kB
JavaScript
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
};