Hi, I’m starting to write my math notes in Typst, and I’m currently using dvdtyp which uses showybox to define theorem boxes. However, there is a known issue of empty box for theorems that break between pages Breakable box empty before new page · Issue #25 · Pablo-Gonzalez-Calderon/showybox-package · GitHub. I’m wondering is there any math theorem package without this issue?
Hi. I think the issue is
So you will face it by using the same box structure, no matter the package.
To find other packages, just search for keywords: Search — Typst: Universe.
To add a bit more details to your answer @Andrew, I agree that it’s best to wait for upstream to fix the issue, or alternatively find another theorems package that does not use the block design.
in showybox, the content is wrapped with a block with the same spacing/above/below properties as the content block (plus align).
@jbirnick does suggest not wrapping with another block and setting the properties directly as a workaround:
but in the case of showybox, that wouldn’t work because the final block is used to create the “box”.
Determining whether or not there is enough space for the content before a pagebreak is pretty difficult in Typst itself. @hongjr03 does propose a fix based on whether the content would fit within the the remaining page space, but it does depend heavily on what the base unit of your content is. It is not a perfect solution. The 2em limit wouldn’t work for raw content, and it wouldn’t work for math.equations either.
Here is another workaround, because I think it’s an interesting one.
I ran into this with some “admonitions” where I use nested blocks to draw double rounded borders.
If you set sticky: true
on the inner block, then it resolves the issue.
It also works on the original issue, and it does not make the total result sticky - it’s only the nested block that has the sticky marker:
#show raw.where(block: true): it => [
#block(fill: luma(230), inset: 10pt, width: 100%)[
#set block(sticky: true)
#it
]
]
#lorem(630)
```rust
fn main() {
println!("Hello, world!");
}
```
What a find! I forgot that option even existed.
EDIT: I submitted a patch upstream for showybox ;) I was too hasty! It doesn’t work.
Well, this is weird. I didn’t find any block element for each RawLine
, but it sure does look like it just glues together all lines as if they are all blocks with sticky flag:
#show raw.where(block: true): it => {
// set block(sticky: true)
block(fill: luma(230), inset: 10pt, width: 100%)[
#set block(sticky: true)
#it
]
}
#lorem(550)
```rust
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
```
Basically breakable
set permanently to false
. And the outer set rule behaves the same way, so at this point, “it’s just breakable: false
with extra steps”.
set block(sticky: true)
should work for the single block inside raw element, but it behaves not how I expected.
The Draft: Make sbox and showy-shadow block sticky to avoid orphan block on pagebreak by quachpas · Pull Request #39 · Pablo-Gonzalez-Calderon/showybox-package · GitHub doesn’t fix anything, as I tested it and without explicit breakable: true
it works the same way as normal, but with it, it still gets orphan top stroke.
Code
#import "@local/showybox:2.0.4": showybox
// #import "@preview/showybox:2.0.4": showybox
#lorem(630)
#showybox(breakable: true)[```rust
fn main() {
println!("Hello, world!");
}
```]
I don’t know much about the raw block use case since I used it to fix a different case of #block[#block[]]
. I could confirm though that the block still does break at page breaks.
For which code snippet?
I think your example shows a problem of the workaround - the block doesn’t want to break easily.
If you take your example and make the rust code longer than 1 page, it does pagebreak - and that is different behaviour than the breakable setting.
This is because it breaks in different place, but the flow is still incorrect like in example above. In both cases the code must start on the first page, but with sticky it doesn’t. So it’s not desired output, at least for raw element, which is a “block inside block” situation.
Code
#show raw.where(block: true): it => {
// set block(sticky: true)
block(fill: luma(230), inset: 10pt, width: 100%)[
#set block(sticky: true)
#it
]
}
#lorem(550)
```rust
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
fn main() {
println!("Hello, world!");
}
```
Well I agree that at this point it’s like a soft breakable setting and it’s not good enough.
That’s just breakable: true
. It’s more of “unbreakable, unless you really insist on breaking by introducing ‘an impossible to contain on one page’ situation”. So it will try to be unbreakable, but with the example where the block is longer than one page, it will have to break it, but not necessarily at the desired place.
But it’s still weird that a single raw sticky block makes all lines stick together, that are just TextElem
.
It’s literally not the same as this setting. You don’t want to agree on anything, do you?
I was referring to your “soft breakable” naming. This word combination doesn’t make sense to me. It’s more of “soft unbreakable”, because it’s like unbreakable, but a bit softer.
New iteration. This workaround works for my admonition use case - it adds a single nested invisible sticky block before the rest of the content.
No workaround:
With workaround:
#set page(height: 10cm, width: 10cm)
#let use-workaround = true
#let admonition(stroke: 0.5pt, radius: 0pt, fill: none, title: none, icon: none, body) = {
let args = (inset: 0.5em, stroke: 0.5pt, fill: fill)
let separation = 2pt
let outer-radius = if radius > 0pt { radius + separation } else { radius }
[#block(..args, radius: outer-radius, inset: separation,
{
if use-workaround {
block(sticky: true, height: 0pt, spacing: 0pt)[]
}
block(..args, radius: radius, {
[*#icon #underline[#title]*<admon-title>]
linebreak()
body
})
}
)<admon-block>]
}
#let info-admon(body) = {
admonition(title: [INFO], icon: emoji.book, radius: 3pt, body)
}
// This case has an empty border before pagebreak - if we don't apply the fix
#lorem(90)
#info-admon[
#lorem(100)
]
#pagebreak()
// This case shows the admon is normally breakable
#lorem(50)
#info-admon[
#lorem(100)
]
Speculative aggressive fix: Shouldn’t every top of block be glued to its content? Why ever page break between top border and the rest of the block… We could even try something like this
#show block.where(sticky: false): it => {
block(sticky: true, height: 0pt, below: 0pt)[]
it
}
(Potentially copy above: it.above
too. Either way, it’s not 100% a drop-in fix).
Now I’m thinking mostly about what a bugfix in typst itself would look like. Maybe there’s a good reason this can’t be done (not with a block, but the same mechanics)
Yea, I thought I have something that could converge within 5 iterations, but turns out it is more tricky than I thought. I created the PR too fast.
This is the final iteration I was at locally
[...]
let alignwrap(content) = layout(size => context {
let el = body.pos().first()
el = if el.has("children") {
let ch = el.children.map(l => measure(l).height)
el.children.find(l => ch.all(h => measure(l).height >= h))
} else if el.has("lines") {
let ch = el.lines.map(l => measure(l).height)
el.lines.find(l => ch.all(h => measure(l).height >= h))
} // TODO: other elements
let line-height = measure(el).height
let bottom-margin = if page.margin == auto {
let small-side = calc.min(page.height, page.width)
(2.5 / 21) * small-side // According to docs, this is the 'auto' margin
} else {
measure(el(length: page.margin.bottom)).width
}
// panic((size.height , here().position().y, bottom-margin))
// panic(here().position().y)
let page-remains = size.height - here().position().y - bottom-margin
let stick = page-remains < line-height
block(
..alignprops,
breakable: breakable,
sticky: stick,
width: 100%,
if "align" in body.named() and body.named().align != none {
align(body.named().align, content)
} else {
(
content
+ repr(page-remains)
+ repr(line-height)
+ repr(page.height)
)
},
)
})
[...]
Test
#import "@local/showybox:2.0.4": showybox
#set page(paper: "iso-b8")
#let defi(definition, content) = {
showybox(
breakable: true,
title-style: (color: black, sep-thickness: 0pt, align: left),
frame: (
title-color: white,
border-color: black.darken(40%),
thickness: 1pt,
),
title: [*Definition*: #definition],
content,
)
}
// Test 1
#lorem(40)
#defi("Definition 1")[
This is the first definition.
With multiples lines
With multiples lines
]
#pagebreak()
// Test 2
#show raw.where(block: true): it => {
showybox(fill: luma(230), breakable: true, above: 1%, inset: 10pt, width: 100%, it)
}
#lorem(36)
```rust
fn main() {
println!("Hello, world!");
}
I am wondering whether I could have convergence by tricking Typst with a boolean combination of breakable
and stick
, but I haven’t found one that works . At least, there’s nothing that seems to preserve both breakability within the document, and stickiness when too close to a page’s end.
Hmm, my guess is that this exploits some logic inside that fixes the problem.
From what I’ve tested, it does break and flows correctly, and fixes the orphaned top stroke that otherwise is present.
Code
#lorem(630)
#let use-workaround = false
#let use-workaround = true
#let code(body) = block(stroke: 1pt, width: 100%, inset: 5pt, {
if use-workaround { block(sticky: true, spacing: 0pt) }
body
})
#code(```rust
fn main() {
println!("Hello, world!");
}
```)
I documented this here.
! That does look like a strong fix.
I’m wondering how it would work with showybox though. Since blocks are heavily nested. It doesn’t seem trivial to find the level at which one should literrally “stick” the empty block. At least, I haven’t found it
I don’t know showybox, just looking at it now. It’s a separate issue, but don’t you always want sticky: true
on the title, if you have a title row? It’s the same case as default headings, which are sticky. Maybe you already have this.
I would use this logic: Any block you have that has a fill or stroke, it should start with the empty sticky block in its body. Maybe that works.
Thanks for everyone’s reply! Just went through the conversation and I feel like I understand Typst a bit more now . I added
#block(sticky:true, spacing: 0pt)
just before “Title of the showybox” comment in showy.typ
and it seems to be working (need to test it more).