Hi!
I’m looking for a way to “automatically” apply a two-column layout only to paragraphs (and not to headings, for example). Ideally, I should be able to write something like:
= Report Overview
<report-overview>
lorem ipsum lorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
lorem ipsum lorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
= Key Takeaways
<key-takeaways>
lorem ipsum lorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
lorem ipsum lorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsumlorem ipsumlorem ipsumlorem
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
ipsumlorem ipsum ipsumlorem ipsum ipsumlorem ipsum
I’m basically looking for the best way to do something like this, ideally (I don’t know if it’s possible) without always having to specify it with a grid() or equivalent.
Hi @JosephBarbier! I have changed your post’s title to bring it in line with the question guidelines and thus make it easier to understand from the title:
Good titles are questions you would ask your friend about Typst.
I also added the layout and layout-containers (for columns) tags, as it makes your question easier to find.
If you think I didn’t hit the mark with the new title, feel free to edit it again but keep it a question please
We can work around this by manually calculating what the height of the columns should be, and using a show rule on par to apply this balanced layout automatically:
// Reference:
// https://typst.app/docs/reference/layout/page/#parameters-margin
// Source:
// https://forum.typst.app/t/how-to-draw-at-the-margin-of-a-page/1708/2
#let calc-margin(margin, shape) = if margin == auto {
2.5 / 21 * calc.min(..shape)
} else {
margin
}
#show par: it => context {
// balanced columns
let num-cols = 2
let gutter = columns.gutter // change this as needed
let width = page.width - 2 * calc-margin(page.margin, (page.width, page.height))
width = width * (100% - gutter.ratio * (num-cols - 1))
width = width - gutter.length * (num-cols - 1)
width = width / num-cols
let height = measure(it, width: width).height
let buffer = if num-cols == 2 {0pt} else {1em}
// The last column may overflow due to rounding,
// so we might need to add some extra space.
// More likely if there are more columns
block(
height: height / num-cols + buffer,
columns(num-cols, gutter: gutter, it)
)
}
Note about the answer: I am not 100% sure if I calculate the width properly. After testing, it seems fine, but it could be off in some edge cases.
Instead of adding a buffer, we can also measure again:
#show par: it => layout(size => {
let column-count = 2
let columnized = columns(column-count, it)
let textheight = measure(columnized, width: size.width).height / column-count
let height = measure(columnized, height: textheight + 0.9em, width: size.width).height
let height = measure(columnized, height: height, width: size.width).height
block(height: height, width: size.width, columnized)
})
The third measure is there because I’ve found that it sometimes has extra whitespace otherwise (in particular when used for outlines or whenever the body contains non-text elements, it might be fine to remove it for this usecase)
We find the width by using layout – this means this also works if the paragraph is inside some container
EDIT: Looking at the example you give, it seems like you want all paragraphs between headings to be combined into one columns. I don’t think that that is possible automatically[1], you probably need to wrap each section into a #balance(columns(2, [...])) using the balance function given in “See code taking a columns” EDIT EDIT: See new answer below
Addendum: You could probably write a document show rule (#show: document=> {}) that automatically groups paragraphs together into columns, but i haven’t gotten the energy to figure out how rn ↩︎
Change the line with if child.func() ... to filter for whatever elements you want to have break the columns. The example only has forst-level headings break the columns, but you could put if child.func() in (heading, pagebreak, ) { to have all headings break the columns etc.
You mustn’t have pagebreaks inside the columns, as this is not allowed.
Additional limitation: you can’t have set or show rules in your document flow, since that will put the rest of the content into a styled element. So that would be detected as a single thing, instead of a sequence of paragraphs and other elements.
Generally speaking, you would just make a styled(styles: element.styles, element.child) wrapper and mutate the child how you want. I did not read the code, might not work here.
I always get styled with let styled = text(red)[].func().
Yeah, it was mostly meant as a general remark about the limits of “inspecting elements directly approaches”
here, mutating the styled child doesn’t fully work, because the element hierarchy and document structure are separate. You would need to effectively split
#set ...
abc
= Def
into
#[
#set ...
abc
]
#[
#set ...
= Def
]
to be able to treat the paragraphs separately from the heading, and that can probably have unintended consequences.