How to render math equations inline and in CeTZ in html export?

Hi! I have two questions about HTML rendering:

  1. When rendering inline math equations, the left and right content will be separated into different <p> s. Is this expected? Is there a way to output it in the same <p> (or simply not wrap the <p> )?,
  2. When using cetz, the math equations in the content disappears. How can I make it display?
#import "@preview/cetz:0.3.4"
#show math.equation: html.frame
#let canvas(..args) = html.frame(cetz.canvas(..args))
#import cetz.draw: *

a$x$b // display as <p>a</p><svg>..</svg><p>b</p>

// cetz example
#let data = (
  [A$x$b], ([B], [C], [D]), ([E], [F])
)

#canvas({
  import cetz.draw: *

  set-style(content: (padding: .2),
    fill: gray.lighten(70%),
    stroke: gray.lighten(70%))

  cetz.tree.tree(data, spread: 2.5, grow: 1.5, draw-node: (node, ..) => {
    circle((), radius: .45, stroke: none)
    content((), node.content)
  }, draw-edge: (from, to, ..) => {
    line((a: from, number: .6, b: to),
         (a: to, number: .6, b: from), mark: (end: ">"))
  }, name: "tree")

  // Draw a "custom" connection between two nodes
  let (a, b) = ("tree.0-0-1", "tree.0-1-0",)
  line((a, .6, b), (b, .6, a), mark: (end: ">", start: ">"))
})

Hey @shigma, welcome to the forum! I’ve changed your question post’s title to better fit our guidelines: How to post in the Questions category

Make sure your title is a question you’d ask to a friend about Typst. :wink:

1 Like

This is likely because html.frame isn’t an inline element (only text decoration elements and horizontal spacing are inline), so it interrupts paragraphs, similarly to e.g. image outside of html export. You’ll need to wrap them in a box for inline equations:

#show math.equation: html.frame
#show math.equation.where(block: false): box

That’s because you’re wrapping the canvas in a frame, causing the canvas to be exported as SVG, and then the show rule wrapping equations in html.frame is being applied, which has no effect in SVG export (only in HTML export).

You’ll need to ensure the show rule is not applied inside a CeTZ canvas. One way to do this involves state:

#let in-canvas = state("in-cetz-canvas", false)
#show math.equation: it => context {
  if in-canvas.get() { it } else { html.frame(it) }
}
#show math.equation.where(block: false): box
#let canvas(..args) = {
  in-canvas.update(true)
  html.frame(cetz.canvas(..args))
  in-canvas.update(false)
  // NOTE: this simplified version assumes you won't
  // create a canvas inside a canvas.
}
2 Likes

Thank you very much for your answer! All the math equations now display as expected! I feel a little more understanding of typst’s #show rule now.

1 Like

Here’s a more flexible solution which ought to work in more situations (not only in a CeTZ canvas):

#show math.equation: it => context {
  // only wrap in frame on html export
  if target() == "html" {
    // wrap frames of inline equations in a box
    // so they don't interrupt the paragraph
    show: if it.block { it => it } else { box }
    html.frame(it)
  } else {
    it
  }
}

Replacing the original math equation show rule in your post’s example code with the code above is enough to fix both problems.

1 Like

Another question is that when generating HTML, we often have to write code like this:

#html.frame(cetz.canvas(..))
#html.frame(fletcher.diagram(..))

Is there a way, similar to #show math.equation, to omit the html.frame here?

Yes:

#let canvas(..args) = html.frame(cetz.canvas(..args))

// Usage:
#canvas({
  import cetz.draw: *
  // and so on
})
1 Like

Well, I know this way of writing, but similar encapsulation is needed for every packages. What I really want is a piece of “prelude” code that can be applied to any HTML rendering scenario and is no different in use from other environments. The #show math.equation above achieves this perfectly :+1: , but the #let canvas here doesn’t (because I can’t include every package, and I can’t prevent users from using the cetz.canvas directly).

Background: I’m developing GitHub - shigma/slidev-addon-typst: Typst addon for Slidev, and as a library I don’t want the development experience to be interrupted by html.frame.

I’m not sure if this is currently possible to not use html module inline for HTML to work. I can only think of

// https://forum.typst.app/t/3701/2
#let target = dictionary(std).at("target", default: () => "paged")

#let some-wrapper(it) = context {
    if target() == "paged" { it } else { html.frame(it) }
}

Maybe you can do show box/block: html.frame, but that is probably very bad.

1 Like