From 03fdfef3947c2b680a4ecffccd8e66a26f7855b4 Mon Sep 17 00:00:00 2001 From: Franklin Koch Date: Wed, 18 Dec 2024 00:48:29 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=84=20Updates=20for=20multi-article=20?= =?UTF-8?q?typst=20export=20with=20interactive=20figures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/fifty-cougars-invent.md | 5 +++ .changeset/kind-lies-kick.md | 7 ++++ .changeset/large-countries-cheer.md | 5 +++ .changeset/nasty-pets-smell.md | 5 +++ .changeset/new-birds-bake.md | 5 +++ .changeset/rare-months-rest.md | 5 +++ packages/myst-cli/src/build/typst.ts | 28 +++++++++++++-- packages/myst-cli/src/process/mdast.ts | 5 +++ packages/myst-cli/src/transforms/images.ts | 39 +++++++++++++++++++++ packages/myst-cli/src/transforms/outputs.ts | 3 ++ packages/myst-directives/src/figure.ts | 15 ++++++++ packages/myst-to-typst/src/container.ts | 5 +++ packages/myst-to-typst/src/index.ts | 4 +-- packages/myst-to-typst/src/math.ts | 4 +++ packages/myst-to-typst/src/utils.ts | 7 ++-- packages/myst-transforms/src/containers.ts | 16 +++++++++ 16 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 .changeset/fifty-cougars-invent.md create mode 100644 .changeset/kind-lies-kick.md create mode 100644 .changeset/large-countries-cheer.md create mode 100644 .changeset/nasty-pets-smell.md create mode 100644 .changeset/new-birds-bake.md create mode 100644 .changeset/rare-months-rest.md diff --git a/.changeset/fifty-cougars-invent.md b/.changeset/fifty-cougars-invent.md new file mode 100644 index 000000000..20d63649c --- /dev/null +++ b/.changeset/fifty-cougars-invent.md @@ -0,0 +1,5 @@ +--- +'myst-to-typst': patch +--- + +Support additional greek characters in typst diff --git a/.changeset/kind-lies-kick.md b/.changeset/kind-lies-kick.md new file mode 100644 index 000000000..e924a6529 --- /dev/null +++ b/.changeset/kind-lies-kick.md @@ -0,0 +1,7 @@ +--- +'myst-directives': patch +'myst-transforms': patch +'myst-cli': patch +--- + +Add static figure placeholder for images that should be used only for PDF exports diff --git a/.changeset/large-countries-cheer.md b/.changeset/large-countries-cheer.md new file mode 100644 index 000000000..b68723ff8 --- /dev/null +++ b/.changeset/large-countries-cheer.md @@ -0,0 +1,5 @@ +--- +'myst-cli': patch +--- + +Prevent html outputs that translate to empty images diff --git a/.changeset/nasty-pets-smell.md b/.changeset/nasty-pets-smell.md new file mode 100644 index 000000000..dcd846a8f --- /dev/null +++ b/.changeset/nasty-pets-smell.md @@ -0,0 +1,5 @@ +--- +'myst-to-typst': patch +--- + +Support restarting counter for each typst article in multi-article export diff --git a/.changeset/new-birds-bake.md b/.changeset/new-birds-bake.md new file mode 100644 index 000000000..d1f6bb843 --- /dev/null +++ b/.changeset/new-birds-bake.md @@ -0,0 +1,5 @@ +--- +'myst-to-typst': patch +--- + +Fix typst crossreferences to other pages diff --git a/.changeset/rare-months-rest.md b/.changeset/rare-months-rest.md new file mode 100644 index 000000000..8602b51fe --- /dev/null +++ b/.changeset/rare-months-rest.md @@ -0,0 +1,5 @@ +--- +'myst-cli': patch +--- + +Prioritize project-level parts for project-level typst export diff --git a/packages/myst-cli/src/build/typst.ts b/packages/myst-cli/src/build/typst.ts index 08703462a..02655e8cb 100644 --- a/packages/myst-cli/src/build/typst.ts +++ b/packages/myst-cli/src/build/typst.ts @@ -267,6 +267,31 @@ export async function localArticleToTypstTemplated( macros: [], commands: {}, }; + const state = session.store.getState(); + const projectFrontmatter = selectors.selectLocalProjectConfig(state, projectPath ?? '.') ?? {}; + if (file === selectors.selectCurrentProjectFile(state)) { + // If export is defined at the project level, prioritize project parts over page parts + partDefinitions.forEach((def) => { + const part = extractTypstPart( + session, + { type: 'root', children: [] }, + {}, + def, + projectFrontmatter, + templateYml, + ); + if (Array.isArray(part)) { + // This is the case if def.as_list is true + part.forEach((item) => { + collected = mergeTypstTemplateImports(collected, item); + }); + parts[def.id] = part.map(({ value }) => value); + } else if (part != null) { + collected = mergeTypstTemplateImports(collected, part); + parts[def.id] = part?.value ?? ''; + } + }); + } const hasGlossaries = false; const results = await Promise.all( content.map(async ({ mdast, frontmatter, references }, ind) => { @@ -311,8 +336,7 @@ export async function localArticleToTypstTemplated( frontmatter = content[0].frontmatter; typstContent = results[0].value; } else { - const state = session.store.getState(); - frontmatter = selectors.selectLocalProjectConfig(state, projectPath ?? '.') ?? {}; + frontmatter = projectFrontmatter; const { dir, name, ext } = path.parse(output); typstContent = ''; let fileInd = 0; diff --git a/packages/myst-cli/src/process/mdast.ts b/packages/myst-cli/src/process/mdast.ts index f620767d3..f93603a7e 100644 --- a/packages/myst-cli/src/process/mdast.ts +++ b/packages/myst-cli/src/process/mdast.ts @@ -68,6 +68,8 @@ import { transformFilterOutputStreams, transformLiftCodeBlocksInJupytext, transformMystXRefs, + removeStaticImages, + removeNonStaticImages, } from '../transforms/index.js'; import type { ImageExtensions } from '../utils/resolveExtension.js'; import { logMessagesFromVFile } from '../utils/logging.js'; @@ -379,10 +381,13 @@ export async function finalizeMdast( const vfile = new VFile(); // Collect errors on this file vfile.path = file; if (simplifyFigures) { + removeNonStaticImages(mdast); // Transform output nodes to images / text reduceOutputs(session, mdast, file, imageWriteFolder, { altOutputFolder: simplifyFigures ? undefined : imageAltOutputFolder, }); + } else { + removeStaticImages(mdast); } transformOutputsToFile(session, mdast, imageWriteFolder, { altOutputFolder: simplifyFigures ? undefined : imageAltOutputFolder, diff --git a/packages/myst-cli/src/transforms/images.ts b/packages/myst-cli/src/transforms/images.ts index f91382090..d6c171c61 100644 --- a/packages/myst-cli/src/transforms/images.ts +++ b/packages/myst-cli/src/transforms/images.ts @@ -746,6 +746,45 @@ export function transformPlaceholderImages( remove(mdast, '__remove__'); } +/** + * Remove all static images + * + * This should only be run for web builds + */ +export function removeStaticImages(mdast: GenericParent) { + selectAll('image', mdast) + .filter((image: GenericNode) => image.static) + .forEach((image: GenericNode) => { + image.type = '__remove__'; + }); + remove(mdast, '__remove__'); +} + +/** + * Remove all figure content except static/placeholder, where present + * + * This should only be run for static builds + */ +export function removeNonStaticImages(mdast: GenericParent) { + const containers = selectAll('container', mdast); + containers.forEach((container: GenericNode) => { + const hasStatic = !!container.children?.find((child) => child.static); + const hasPlaceholder = !!container.children?.find((child) => child.placeholder); + if (!hasStatic && !hasPlaceholder) return; + container.children = container.children?.filter((child) => { + if (['caption', 'legend'].includes(child.type)) { + // Always keep caption/legend + return true; + } + if (hasStatic) { + // Prioritize static image over placeholder + return !!child.static; + } + return !!child.placeholder; + }); + }); +} + /** * Trim base64 values for urlSource when they have been replaced by image urls */ diff --git a/packages/myst-cli/src/transforms/outputs.ts b/packages/myst-cli/src/transforms/outputs.ts index ea014c752..a632a6797 100644 --- a/packages/myst-cli/src/transforms/outputs.ts +++ b/packages/myst-cli/src/transforms/outputs.ts @@ -279,6 +279,9 @@ export function reduceOutputs( ], }; htmlTransform(htmlTree); + if ((selectAll('image', htmlTree) as GenericNode[]).find((htmlImage) => !htmlImage.url)) { + return undefined; + } return htmlTree.children; } else if (content_type.startsWith('image/')) { const path = writeCachedOutputToFile(session, hash, cache.$outputs[hash], writeFolder, { diff --git a/packages/myst-directives/src/figure.ts b/packages/myst-directives/src/figure.ts index 406c12d19..273b6b47b 100644 --- a/packages/myst-directives/src/figure.ts +++ b/packages/myst-directives/src/figure.ts @@ -56,6 +56,10 @@ export const figureDirective: DirectiveSpec = { type: String, doc: 'A placeholder image when using a notebook cell as the figure contents. This will be shown in place of the Jupyter output until an execution environment is attached. It will also be used in static outputs, such as a PDF output.', }, + static: { + type: String, + doc: 'An image only used in static outputs. This image will always take priority for static outputs, such as PDFs, and will never be used in dynamic outputs, such as web builds.', + }, 'no-subfigures': { type: Boolean, doc: 'Disallow implicit subfigure creation from child nodes', @@ -96,6 +100,17 @@ export const figureDirective: DirectiveSpec = { align: data.options?.align as Image['align'], }); } + if (data.options?.static) { + children.push({ + type: 'image', + static: true, + url: data.options.static as string, + alt: data.options?.alt as string, + width: data.options?.width as string, + height: data.options?.height as string, + align: data.options?.align as Image['align'], + }); + } if (data.body) { children.push(...(data.body as GenericNode[])); } diff --git a/packages/myst-to-typst/src/container.ts b/packages/myst-to-typst/src/container.ts index 4634c1766..36320a8ab 100644 --- a/packages/myst-to-typst/src/container.ts +++ b/packages/myst-to-typst/src/container.ts @@ -104,6 +104,11 @@ export const containerHandler: Handler = (node, state) => { return; } + if (node.enumerator?.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: GenericNode) => item.type === 'container').length === diff --git a/packages/myst-to-typst/src/index.ts b/packages/myst-to-typst/src/index.ts index 35dcfad6d..b98f42a11 100644 --- a/packages/myst-to-typst/src/index.ts +++ b/packages/myst-to-typst/src/index.ts @@ -363,10 +363,10 @@ const handlers: Record = { legend: captionHandler, captionNumber: () => undefined, crossReference(node: CrossReference, state, parent) { - if (node.remote) { + if (node.remoteBaseUrl) { // We don't want to handle remote references, treat them as links const url = - (node.remoteBaseUrl ?? '') + + node.remoteBaseUrl + (node.url === '/' ? '' : node.url ?? '') + (node.html_id ? `#${node.html_id}` : ''); linkHandler({ ...node, url: url }, state); diff --git a/packages/myst-to-typst/src/math.ts b/packages/myst-to-typst/src/math.ts index 4b1a27dc1..0bc881faa 100644 --- a/packages/myst-to-typst/src/math.ts +++ b/packages/myst-to-typst/src/math.ts @@ -67,6 +67,10 @@ const math: Handler = (node, state) => { const { identifier: label } = normalizeLabel(node.label) ?? {}; addMacrosToState(value, state); state.ensureNewLine(); + if (node.enumerator?.endsWith('.1')) { + state.write(`#set math.equation(numbering: "(${node.enumerator})")\n`); + state.write(`#counter(math.equation).update(0)\n\n`); + } // Note: must have spaces $ math $ for the block! state.write(`$ ${value} $${label ? ` <${label}>` : ''}\n\n`); state.ensureNewLine(true); diff --git a/packages/myst-to-typst/src/utils.ts b/packages/myst-to-typst/src/utils.ts index 36fad28df..2e21317fe 100644 --- a/packages/myst-to-typst/src/utils.ts +++ b/packages/myst-to-typst/src/utils.ts @@ -49,8 +49,8 @@ const textOnlyReplacements: Record = { '©': '#emoji.copyright ', '®': '#emoji.reg ', '™': '#emoji.tm ', - '<': '\\< ', - '>': '\\> ', + '<': '\\<', + '>': '\\>', ' ': '~', ' ': '~', // eslint-disable-next-line no-irregular-whitespace @@ -105,9 +105,11 @@ const mathReplacements: Record = { '×': 'times', Α: 'A', α: 'alpha', + 𝜶: 'alpha', Β: 'B', β: 'beta', ß: 'beta', + 𝜷: 'beta', Γ: 'Gamma', γ: 'gamma', Δ: 'Delta', @@ -115,6 +117,7 @@ const mathReplacements: Record = { δ: 'delta', Ε: 'E', ε: 'epsilon', + 𝝴: 'epsilon', Ζ: 'Z', ζ: 'zeta', Η: 'H', diff --git a/packages/myst-transforms/src/containers.ts b/packages/myst-transforms/src/containers.ts index f9ff46f48..fb82f5546 100644 --- a/packages/myst-transforms/src/containers.ts +++ b/packages/myst-transforms/src/containers.ts @@ -44,6 +44,10 @@ function isPlaceholder(node: GenericNode) { return node.type === 'image' && node.placeholder; } +function isStatic(node: GenericNode) { + return node.type === 'image' && node.static; +} + /** Nest node inside container */ function createSubfigure(node: GenericNode, parent: GenericParent): GenericParent { const children = node.type === 'container' && node.children ? node.children : [node]; @@ -130,6 +134,7 @@ export function containerChildrenTransform(tree: GenericParent, vfile: VFile) { hoistContentOutOfParagraphs(container); let subfigures: GenericNode[] = []; let placeholderImage: GenericNode | undefined; + let staticImage: GenericNode | undefined; let caption: GenericNode | undefined; let legend: GenericNode | undefined; const otherNodes: GenericNode[] = []; @@ -161,6 +166,15 @@ export function containerChildrenTransform(tree: GenericParent, vfile: VFile) { } else { placeholderImage = child; } + } else if (isStatic(child)) { + if (staticImage) { + fileError(vfile, 'container has multiple static images', { + node: container, + ruleId: RuleId.containerChildrenValid, + }); + } else { + staticImage = child; + } } else if (SUBFIGURE_TYPES.includes(child.type)) { subfigures.push(child); } else { @@ -186,6 +200,7 @@ export function containerChildrenTransform(tree: GenericParent, vfile: VFile) { caption ? 'caption' : undefined, legend ? 'legend' : undefined, placeholderImage ? 'placeholder image' : undefined, + staticImage ? 'static image' : undefined, ] .filter(Boolean) .join(', '); @@ -206,6 +221,7 @@ export function containerChildrenTransform(tree: GenericParent, vfile: VFile) { } const children: GenericNode[] = [...subfigures]; if (placeholderImage) children.push(placeholderImage); + if (staticImage) children.push(staticImage); // Caption is above tables and below all other figures if (container.kind === 'table') { if (caption) children.unshift(caption);