Foreword
This is a long as hell explanation, and very XY problem-y and I’m quite annoyed at how long I’ve burnt on this, so I’m just going to compile all I’ve written up and dump it here. Feel free to rename the title or answer the underlying problem in another post or however it works around here.
Underlying intent
I want to implement CSS-only, accessible side notes. I am following Making semantic sidenotes without JavaScript | Koos Looijesteijn with the corresponding full demo code at GitHub - kslstn/sidenotes: Semantic sidenotes for the web without JavaScript. I’m running into issues with the responsive version for thin (e.g. mobile) screens—we want to do some shenanigans that you can see demonstrated at the first link after procuring a thin viewport.
What I have
The function that generates side notes
#let sidenote(body) = {
counter(footnote).step()
context {
let n = counter(footnote).get().first()
html.span(class: "sidenote")[
#html.input(
aria-label: "Show sidenote",
type: "checkbox",
id: "sidenote__checkbox--" + str(n),
class: "sidenote__checkbox",
)
#html.elem("label", attrs: (
tabindex: "0",
// title: str(body), // can't convert content to string
aria-describedby: "sidenote-" + str(n),
"for": "sidenote__checkbox--" + str(n),
class: "sidenote__button",
))[
#super[#n]
]
#html.small(
id: "sidenote-" + str(n),
class: "sidenote__content sidenote__content--number-" + str(n),
)[
#html.span(class: "sidenote__content-parenthesis")[(sidenote: ]
#super[#n]#body#html.span(class: "sidenote__content-parenthesis")[)]
]
]
}
}
The HTML it generated
<span class="sidenote"
><input
aria-label="Show sidenote"
type="checkbox"
id="sidenote__checkbox--1"
class="sidenote__checkbox"
/><label
tabindex="0"
aria-describedby="sidenote-1"
for="sidenote__checkbox--1"
class="sidenote__button"
><sup>1</sup></label
>
<p>
<small id="sidenote-1" class="sidenote__content sidenote__content--number-1"
><span class="sidenote__content-parenthesis">(sidenote:</span>
<sup>1</sup>Subframe LSP responses are great for coding. Stuff like bevy
projects or
<a href="https://graphite.rs/">https://graphite.rs/</a>
contributions can get a little absurd, again, relative to what I’m used
to. Mumble mumble metaprogramming is cool but dangerous.<span
class="sidenote__content-parenthesis"
>)</span
></small
>
</p></span
>
The HTML I want it to generate
Literally just the above but without the <p> and without the </p>.
Current workaround
I just regex it out in post-processing :/ Would love to do it in native Typst, though.
From Discord #quick-questions
Initial message by me
Been trying to implement Making semantic sidenotes without JavaScript | Koos Looijesteijn for the past, uh, hours. But it doesn’t seem possible to put an input, label, and small together on the same level without Typst trying to wrap small in a <p>?
#let sidenote(body) = {
html.span[
#html.input()
#html.label()
#html.small[aaaaa]
]
}
Word.#sidenote[Says who?] Anyways, check this out.
outputs
<p>
Word.<span
><input /><label></label>
<p><small>aaaaa</small></p></span
><span style="white-space: pre-wrap"> </span>Anyways, check this out.
</p>
Which borks the CSS selectors. And if I wrap the input and label into another span, that gets rid of the <p>, but also borks CSS selectors.
I’m losing track of what does what, but the input is a checkbox that toggles the span visible and invisible, and thus needs to select it
i.e. .sidenote__checkbox:checked ~ .sidenote__content and such
Reply by SillyFreak
hmm… the magic seems to be that the .sidenote__content element is next to the .sidenote__checkbox element, not that the content element is a small element. can you just wrap the small inside an explicit html.p and put the class there? Typst is probably adding the <p> since it thinks (rightfully?
) that there needs to be a block level element, so if you add one explicitly it should wrap it in another.
Re-reply by me
too tired to make it work, so I’ll just leave my post-processing regex hack as the solution for now. But that doesn’t work either because sidenotes, generated via #sidenote[...] are inside a paragraph already anyways. So by doing that, having a p inside a p, apparently HTML renderers resolve this by splitting it up? It’s all confusing and diffucult to find learning resources.
basically, if you put
<p>outer<p>inner</p>other side</p>
in a file and open it in the browser, you get
<p>outer</p><p>inner</p>other side<p></p>
even though by looking at view-source:localhost:8000 or whatever, nothing changed.
Also,
<p>outer<div>inner</div>other side</p>
turns into
<p>outer</p><div>inner</div>other side<p></p>
I think it’s a block v inline thing. And I think it happens render-time before css is applied? Wait I should check that
<style>div {display:inline}</style><p>one<div>two</div>three</p>
turns to
<p>one</p><div>two</div>three<p></p>
So, still bad.
Closing thoughts
Still confused what mechanism Typst uses to decide when to insert a paragraph tag. Possibly something to do with block level v inline level tags. Not sure, and I’ve ran out of energy to experiment.
![]()
