I wanted to document here a design issue that I encountered with the HTML output, in case it helps other people. I think that it should be explained clearly in the documentation, because it really looks like a confusing bug at first.
The following works as intended when you compile to HTML:
= Heading level 1
Foo
== Heading level 2
it gives you the following output body:
<h2>Heading level 1</h2>
<p>Foo</p>
<h3>Heading level 2</h3>
On the other hand, suppose you have
#show heading: it =>
block(counter(heading).display(it.numbering) + h(10pt) + it.body)
if (it.level == 1) v(8pt)
= Heading level 1
Foo
== Heading level 2
Then you are going to see the following output:
<div>1Heading level 1</div>
<p>Foo</p>
<div>1.1Heading level 2</div>
This looks like a bug: the heading levels are gone from the HTML output!
In this synthetic repro case, it is obvious what is going on: the function show rule has replaced the Typst heading element with an expansion that does not include the heading element anymore, and so the HTML output does not contain headings.
But in practice this will trick you, because the #show rule will not be in the document as in this minimal repro case, but in some template written by someone else that you are reusing. The template uses function show rules to render titles exactly as its authors intended… but in fact it completely breaks the HTML output.
Current solutions?
Currently I don’t know of agood fix for this problem, but I know of an ugly fix: the template author has to do a conditional check on the target to detect that HTML rendering is used, and then protect every function show rule against this issue by manually recreating the intended elements.
#set heading(numbering: "1.1")
#show heading: it => context {
if target() == "html" { html.elem("h" + str(it.level), it.body) }
else {
block(counter(heading).display(it.numbering) + h(10pt) + it.body)
if (it.level == 1) { v(8pt) }
}
}
Notice the context marker, because target() is contextual.
I think that this solution is pretty bad because it means that template authors have to be extra careful to un-break the HTML output when they use function show rules, and this is extra work. Most template authors are not going to test or care about their HTML output, and users will suffer.
Ideas for solutions?
It would be much nicer to be able to selectively enable/disable rules depending on the target. For example informed template authors could use:
#show heading when target() != "html": it => ...
But this is not enough, because it does not let users fix a broken function show rule from the template – to my knowledge it is not currently possible to override/uninstall a function show rule.
Another issue with the workaround of using contextual { if backend () == "html" { ... } else { ... }} in the template definition is that it is rejected by typst unless --features html is passed. In other words, if the template author uses this to make life easier for users interested in HTML output, it requires changing the build configuration for all users, even those that are not using the HTML output.