From 49d5c8bc216aae84aff5bb256d0a55c934d0c3e2 Mon Sep 17 00:00:00 2001 From: Rowan Cockett <rowanc1@gmail.com> Date: Tue, 29 Oct 2024 21:33:21 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B1=20Update=20proof=20and=20blockquot?= =?UTF-8?q?e=20for=20typst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/silent-badgers-begin.md | 5 ++ packages/myst-to-typst/src/container.ts | 32 +++++++++--- packages/myst-to-typst/src/index.ts | 67 ++++++++++++++++++++----- packages/myst-to-typst/src/types.ts | 1 + 4 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 .changeset/silent-badgers-begin.md diff --git a/.changeset/silent-badgers-begin.md b/.changeset/silent-badgers-begin.md new file mode 100644 index 000000000..23aa1bbd5 --- /dev/null +++ b/.changeset/silent-badgers-begin.md @@ -0,0 +1,5 @@ +--- +"myst-to-typst": patch +--- + +add proof and change blockquote diff --git a/packages/myst-to-typst/src/container.ts b/packages/myst-to-typst/src/container.ts index 2ca9b7f31..e04a49eec 100644 --- a/packages/myst-to-typst/src/container.ts +++ b/packages/myst-to-typst/src/container.ts @@ -41,7 +41,7 @@ function renderFigureChild(node: GenericNode, state: ITypstSerializer) { if (useBrackets) state.write('\n]'); } -function getDefaultCaptionSupplement(kind: CaptionKind | string) { +export function getDefaultCaptionSupplement(kind: CaptionKind | string) { if (kind === 'code') kind = 'program'; const domain = kind.includes(':') ? kind.split(':')[1] : kind; return `${domain.slice(0, 1).toUpperCase()}${domain.slice(1)}`; @@ -55,7 +55,6 @@ export const containerHandler: Handler = (node, state) => { }); return; } - state.ensureNewLine(); const prevState = state.data.isInFigure; state.data.isInFigure = true; @@ -73,6 +72,28 @@ export const containerHandler: Handler = (node, state) => { source: 'myst-to-typst', }); } + const flatCaptions = captions + .map((cap: GenericNode) => 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; + } + if (nonCaptions && nonCaptions.length > 1) { const allSubFigs = nonCaptions.filter((item: GenericNode) => item.type === 'container').length === @@ -114,12 +135,7 @@ export const containerHandler: Handler = (node, state) => { } if (captions?.length) { state.write('\n caption: [\n'); - state.renderChildren({ - children: captions - .map((cap: GenericNode) => cap.children) - .filter(Boolean) - .flat(), - }); + state.renderChildren(flatCaptions); state.write('\n],'); } if (kind) { diff --git a/packages/myst-to-typst/src/index.ts b/packages/myst-to-typst/src/index.ts index 419d6f5cf..b86e23373 100644 --- a/packages/myst-to-typst/src/index.ts +++ b/packages/myst-to-typst/src/index.ts @@ -3,7 +3,7 @@ import type { Plugin } from 'unified'; import type { VFile } from 'vfile'; import type { GenericNode } from 'myst-common'; import { fileError, fileWarn, toText, getMetadataTags } from 'myst-common'; -import { captionHandler, containerHandler } from './container.js'; +import { captionHandler, containerHandler, getDefaultCaptionSupplement } from './container.js'; import type { Handler, ITypstSerializer, @@ -57,12 +57,6 @@ const admonitionMacros = { '#let warningBlock(body, heading: [Warning]) = admonition(body, heading: heading, color: yellow)', }; -const blockquote = `#let blockquote(node, color: gray) = { - let stroke = (left: 2pt + color.darken(20%)) - set text(fill: black.lighten(40%), style: "oblique") - block(width: 100%, inset: 8pt, stroke: stroke)[#node] -}`; - const tabSet = ` #let tabSet(body) = { block(width: 100%, stroke: luma(240), [#body]) @@ -79,6 +73,26 @@ const tabItem = ` ]) }`; +const proof = ` +#let proof(body, heading: none, kind: "proof", supplement: "Proof", labelName: none, color: blue, float: true) = { + let stroke = 1pt + color.lighten(90%) + let fill = color.lighten(90%) + let title + set figure.caption(position: top) + set figure(placement: none) + show figure.caption.where(body: heading): (it) => { + block(width: 100%, stroke: stroke, fill: fill, inset: 8pt, it) + } + place(auto, float: float, block(width: 100%, [ + #figure(kind: kind, supplement: supplement, gap: 0pt, [ + #set align(left); + #set figure.caption(position: bottom) + #block(width: 100%, fill: luma(253), stroke: stroke, inset: 8pt)[#body] + ], caption: heading) + #if(labelName != none){label(labelName)} + ])) +}`; + const INDENT = ' '; const linkHandler = (node: any, state: ITypstSerializer) => { @@ -128,8 +142,13 @@ const handlers: Record<string, Handler> = { state.renderChildren(node, 2); }, blockquote(node, state) { - state.useMacro(blockquote); - state.renderEnvironment(node, 'blockquote'); + if (state.data.isInBlockquote) { + state.renderChildren(node); + return; + } + state.write('#quote(block: true)['); + state.renderChildren(node); + state.write(']'); }, definitionList(node, state) { let dedent = false; @@ -282,7 +301,7 @@ const handlers: Record<string, Handler> = { } state.useMacro(admonitionMacros[node.kind]); state.write(`#${node.kind}Block`); - if (title && toText(title).toLowerCase().replace(' ', '') !== node.kind) { + if (title && toText(title).toLowerCase().replaceAll(' ', '') !== node.kind) { state.write('(heading: ['); state.renderChildren(title); state.write('])'); @@ -416,6 +435,25 @@ const handlers: Record<string, Handler> = { state.renderChildren(node); state.write('\n]\n\n'); }, + proof(node: GenericNode, state) { + state.useMacro(proof); + const title = select('admonitionTitle', node); + const kind = node.kind || 'proof'; + const supplement = getDefaultCaptionSupplement(kind); + state.write( + `#proof(kind: "${kind}", supplement: "${supplement}", labelName: ${node.identifier ? `"${node.identifier}"` : 'none'}`, + ); + if (title) { + state.write(', heading: ['); + state.renderChildren(title); + state.write('])['); + } else { + state.write(')['); + } + state.renderChildren(node); + state.write(']'); + state.ensureNewLine(); + }, }; class TypstSerializer implements ITypstSerializer { @@ -474,10 +512,15 @@ class TypstSerializer implements ITypstSerializer { } renderChildren( - node: Partial<Parent>, + node: Partial<Parent> | Parent[], trailingNewLines = 0, - { delim = '', trimEnd = true }: RenderChildrenOptions = {}, + opts: RenderChildrenOptions = {}, ) { + if (Array.isArray(node)) { + this.renderChildren({ children: node }, trailingNewLines, opts); + return; + } + const { delim = '', trimEnd = true } = opts; const numChildren = node.children?.length ?? 0; node.children?.forEach((child, index) => { if (!child) return; diff --git a/packages/myst-to-typst/src/types.ts b/packages/myst-to-typst/src/types.ts index c27c8e70a..0b740c9a4 100644 --- a/packages/myst-to-typst/src/types.ts +++ b/packages/myst-to-typst/src/types.ts @@ -25,6 +25,7 @@ export type Options = { export type StateData = { tableColumns?: number; isInFigure?: boolean; + isInBlockquote?: boolean; isInTable?: boolean; longFigure?: boolean; definitionIndent?: number;