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;