├── LICENSE
├── README.md
├── assets
├── example-1.png
├── example-2.png
├── example-3.png
├── example-4.png
└── example-5.png
├── docs
├── make-example-images.typ
├── manual.pdf
├── manual.typ
├── readme.pdf
└── readme.typ
├── typst.toml
├── update_readme.py
└── wrap-it.typ
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wrap-It: Wrapping text around figures & content
2 |
3 | Until is
4 | resolved, `typst` doesn’t natively support wrapping text around figures
5 | or other content. However, you can use `wrap-it` to mimic much of this
6 | functionality:
7 |
8 | - Wrapping images left or right of their text
9 |
10 | - Specifying margins
11 |
12 | - And more
13 |
14 | Detailed descriptions of each parameter are available in the
15 | [wrap-it
16 | documentation](https://github.com/ntjess/wrap-it/blob/main/docs/manual.pdf).
17 |
18 | # Installation
19 |
20 | The easiest method is to import `wrap-it: wrap-content` from the
21 | `@preview` package:
22 |
23 | `#import "@preview/wrap-it:0.1.1": wrap-content`
24 |
25 | # Sample use:
26 |
27 | ## Vanilla
28 |
29 | ``` typst
30 | #let fig = figure(
31 | rect(fill: teal, radius: 0.5em, width: 8em),
32 | caption: [A figure],
33 | )
34 | #let body = lorem(30)
35 | #wrap-content(fig, body)
36 | ```
37 | 
38 |
39 | ## Changing alignment and margin
40 |
41 | ``` typst
42 | #wrap-content(
43 | fig,
44 | body,
45 | align: bottom + right,
46 | column-gutter: 2em
47 | )
48 | ```
49 | 
50 |
51 | ## Uniform margin around the image
52 |
53 | The easiest way to get a uniform, highly-customizable margin is through
54 | boxing your image:
55 |
56 | ``` typst
57 | #let boxed = box(fig, inset: 0.25em)
58 | #wrap-content(boxed)[
59 | #lorem(30)
60 | ]
61 | ```
62 | 
63 |
64 | ## Wrapping two images in the same paragraph
65 |
66 | Note that for longer captions (as is the case in the bottom figure
67 | below), providing an explicit `columns` parameter is necessary to inform
68 | caption text of where to wrap.
69 |
70 | ``` typst
71 | #let fig2 = figure(
72 | rect(fill: lime, radius: 0.5em),
73 | caption: [#lorem(10)],
74 | )
75 | #wrap-top-bottom(
76 | bottom-kwargs: (columns: (1fr, 2fr)),
77 | box(fig, inset: 0.25em),
78 | fig2,
79 | lorem(50),
80 | )
81 | ```
82 | 
83 |
84 | ## Adding a label to a wrapped figure
85 |
86 | Typst can only append labels to figures in content mode. So, when
87 | wrapping text around a figure that needs a label, you must first place
88 | your figure in a content block with its label, then wrap it:
89 |
90 | ``` typst
91 | #show ref: it => underline(text(blue, it))
92 | #let fig = [
93 | #figure(
94 | rect(fill: red, radius: 0.5em, width: 8em),
95 | caption:[Labeled]
96 | )
97 | ]
98 | #wrap-content(fig, [Fortunately, @fig:lbl's label can be referenced within the wrapped text. #lorem(15)])
99 | ```
100 | 
--------------------------------------------------------------------------------
/assets/example-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntjess/wrap-it/fb225204a6dde4f607965c041829c56ec0ec6e39/assets/example-1.png
--------------------------------------------------------------------------------
/assets/example-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntjess/wrap-it/fb225204a6dde4f607965c041829c56ec0ec6e39/assets/example-2.png
--------------------------------------------------------------------------------
/assets/example-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntjess/wrap-it/fb225204a6dde4f607965c041829c56ec0ec6e39/assets/example-3.png
--------------------------------------------------------------------------------
/assets/example-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntjess/wrap-it/fb225204a6dde4f607965c041829c56ec0ec6e39/assets/example-4.png
--------------------------------------------------------------------------------
/assets/example-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntjess/wrap-it/fb225204a6dde4f607965c041829c56ec0ec6e39/assets/example-5.png
--------------------------------------------------------------------------------
/docs/make-example-images.typ:
--------------------------------------------------------------------------------
1 | #import "readme.typ": eval-kwargs, showman-config
2 | #import "@preview/showman:0.1.2": runner
3 | #show raw.where(lang: "example"): it => it
4 |
5 |
6 | #let get-example-blocks(body) = {
7 | let example-blocks = ()
8 | let example-blocks-regex = regex("```example\n([\s\S]+?)\n```")
9 | for match in body.matches(example-blocks-regex) {
10 | example-blocks.push(match.captures.at(0))
11 | }
12 | example-blocks
13 | }
14 |
15 | #let all-blocks = get-example-blocks(read("readme.typ"))
16 | #set page(margin: 1em, ..showman-config.page-size)
17 | // Dummy first page to match showman expectations
18 | #pagebreak()
19 | #for (ii, example) in all-blocks.enumerate() {
20 | eval(
21 | mode: "markup",
22 | "#let output(body) = body;\n" + eval-kwargs.eval-prefix + example,
23 | scope: eval-kwargs.scope,
24 | )
25 | if ii != all-blocks.len() - 1 {
26 | pagebreak()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/docs/manual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntjess/wrap-it/fb225204a6dde4f607965c041829c56ec0ec6e39/docs/manual.pdf
--------------------------------------------------------------------------------
/docs/manual.typ:
--------------------------------------------------------------------------------
1 | #import "@preview/tidy:0.2.0"
2 | #import "@preview/showman:0.1.2"
3 | #import "../wrap-it.typ"
4 | #show raw.where(block: true, lang: "typ"): showman.formatter.format-raw.with(width: 100%)
5 | #show raw.where(lang: "typ"): showman.runner.global-example.with(
6 | unpack-modules: true,
7 | scope: (wrap-it: wrap-it),
8 | eval-prefix: "#let wrap-content(..args) = output(wrap-it.wrap-content(..args))",
9 | )
10 | #show : set text(font: "New Computer Modern")
11 | #let module = tidy.parse-module(read("../wrap-it.typ"))
12 | #tidy.show-module(
13 | module,
14 | style: tidy.styles.default,
15 | first-heading-level: 1,
16 | show-outline: false,
17 | break-param-descriptions: true,
18 | )
19 |
--------------------------------------------------------------------------------
/docs/readme.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntjess/wrap-it/fb225204a6dde4f607965c041829c56ec0ec6e39/docs/readme.pdf
--------------------------------------------------------------------------------
/docs/readme.typ:
--------------------------------------------------------------------------------
1 | #import "@preview/showman:0.1.2"
2 | #import "/wrap-it.typ"
3 |
4 | #let eval-kwargs = (
5 | eval-prefix: "
6 | #set par(justify: true)
7 | #let fig = figure(
8 | rect(fill: teal, radius: 0.5em, width: 8em),
9 | caption: [A figure],
10 | )
11 | #let body = lorem(30)
12 |
13 | #let wrap-content(..args) = output(wrap-it.wrap-content(..args))
14 | #let wrap-top-bottom(..args) = output(wrap-it.wrap-top-bottom(..args))
15 | ",
16 | scope: (wrap-it: wrap-it),
17 | )
18 | // TODO: Can't find how to tell pandoc the --root is at /, so it doesn't have access to typst.toml
19 | #let pkg-version = "0.1.1"
20 | // #let pkg-version = toml("/typst.toml").at("package").at("version")
21 |
22 |
23 | #show: showman.formatter.template.with(eval-kwargs: (eval-kwargs))
24 |
25 | #let showman-config = (
26 | page-size: (width: 4.1in, height: auto),
27 | eval-kwargs: eval-kwargs,
28 | )
29 | #show : set text(font: "Libertinus Serif", size: 10pt)
30 | #show link: it => {
31 | set text(fill: blue)
32 | underline(it)
33 | }
34 | #set page(height: auto)
35 | = Wrap-It: Wrapping text around figures & content
36 | Until https://github.com/typst/typst/issues/553 is resolved, `typst` doesn't natively support wrapping text around figures or other content. However, you can use `wrap-it` to mimic much of this functionality:
37 | - Wrapping images left or right of their text
38 | - Specifying margins
39 | - And more
40 |
41 | Detailed descriptions of each parameter are available in the #link("https://github.com/ntjess/wrap-it/blob/main/docs/manual.pdf")[wrap-it documentation].
42 |
43 | = Installation
44 | The easiest method is to import `wrap-it: wrap-content` from the `@preview` package:
45 | #let raw-string = "#import \"@preview/wrap-it:" + pkg-version + "\": wrap-content"
46 |
47 | #raw(raw-string, lang: "typ")
48 |
49 | = Sample use:
50 | == Vanilla
51 |
52 | ```example
53 | #let fig = figure(
54 | rect(fill: teal, radius: 0.5em, width: 8em),
55 | caption: [A figure],
56 | )
57 | #let body = lorem(30)
58 | #wrap-content(fig, body)
59 | ```
60 |
61 | == Changing alignment and margin
62 | ```example
63 | #wrap-content(
64 | fig,
65 | body,
66 | align: bottom + right,
67 | column-gutter: 2em
68 | )
69 | ```
70 |
71 | == Uniform margin around the image
72 | The easiest way to get a uniform, highly-customizable margin is through boxing your image:
73 | ```example
74 | #let boxed = box(fig, inset: 0.25em)
75 | #wrap-content(boxed)[
76 | #lorem(30)
77 | ]
78 | ```
79 | == Wrapping two images in the same paragraph
80 | Note that for longer captions (as is the case in the bottom figure below), providing an explicit `columns` parameter is necessary to inform caption text of where to wrap.
81 | ```example
82 | #let fig2 = figure(
83 | rect(fill: lime, radius: 0.5em),
84 | caption: [#lorem(10)],
85 | )
86 | #wrap-top-bottom(
87 | bottom-kwargs: (columns: (1fr, 2fr)),
88 | box(fig, inset: 0.25em),
89 | fig2,
90 | lorem(50),
91 | )
92 | ```
93 |
94 | == Adding a label to a wrapped figure
95 | Typst can only append labels to figures in content mode. So, when wrapping text around a figure that needs a label, you must first place your figure in a content block with its label, then wrap it:
96 |
97 | ```example
98 | #show ref: it => underline(text(blue, it))
99 | #let fig = [
100 | #figure(
101 | rect(fill: red, radius: 0.5em, width: 8em),
102 | caption:[Labeled]
103 | )
104 | ]
105 | #wrap-content(fig, [Fortunately, @fig:lbl's label can be referenced within the wrapped text. #lorem(15)])
106 | ```
107 |
--------------------------------------------------------------------------------
/typst.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wrap-it"
3 | version = "0.1.1"
4 | entrypoint = "wrap-it.typ"
5 | authors = ["Nathan Jessurun"]
6 | license = "Unlicense"
7 | description = "Wrap text around figures and content"
8 | repository = "https://github.com/ntjess/wrap-it"
9 | keywords = ["wrapfig", "wrap", "align", "image"]
10 | exclude = ["manual.pdf"]
11 |
12 |
13 | [tool.packager]
14 | paths = [
15 | "wrap-it.typ",
16 | "LICENSE",
17 | "README.md",
18 | { from = "docs/manual.pdf", to = "manual.pdf" },
19 | ]
20 |
--------------------------------------------------------------------------------
/update_readme.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | import re
3 | from pathlib import Path
4 |
5 | from showman.converter import Converter
6 |
7 | toml_text = Path("typst.toml").read_text()
8 | groups = re.match(r".*version = \"(.*?)\"", toml_text, re.DOTALL)
9 | assert groups
10 | version = groups.group(1)
11 |
12 |
13 | class WrapItConverter(Converter):
14 | def _setup_build_folder(self, persist=False):
15 | return (
16 | self.typst_file.parent,
17 | self.typst_file.parent / "make-example-images.typ",
18 | )
19 |
20 | def _get_runnable_langs(self):
21 | return ["example"]
22 |
23 | def __del__(self):
24 | # No need to clean up build directory
25 | pass
26 |
27 |
28 | WrapItConverter(
29 | "docs/readme.typ",
30 | assets_dir="assets",
31 | root_dir=".",
32 | showable_labels=["example"],
33 | # log_level="DEBUG",
34 | ).save(
35 | out_path="README.md",
36 | remote_url=f"https://www.github.com/ntjess/wrap-it/v{version}/",
37 | force=True,
38 | )
39 |
--------------------------------------------------------------------------------
/wrap-it.typ:
--------------------------------------------------------------------------------
1 | #let styled = text(red)[lorem].func()
2 |
3 | #let _gridded(dir, fixed, to-wrap, ..kwargs) = {
4 | let dir-kwargs = (:)
5 | if dir not in (ltr, rtl) {
6 | panic("Specify either `rtl` or `ltr` as the wrap direction")
7 | }
8 | let args = if dir == rtl {
9 | (to-wrap, fixed)
10 | } else {
11 | (fixed, to-wrap)
12 | }
13 | grid(..args, columns: 2, rows: 2, column-gutter: 1em, ..kwargs)
14 | }
15 |
16 | #let _grid-height(content, container-size) = {
17 | measure(box(width: container-size.width, content)).height
18 | }
19 |
20 | #let _get-chunk(words, end, reverse, start: 0) = {
21 | if end < 0 {
22 | return words.join(" ")
23 | }
24 | if reverse {
25 | words = words.rev()
26 | }
27 | let subset = words.slice(start, end)
28 | if reverse {
29 | subset = subset.rev()
30 | }
31 | subset.join(" ")
32 | }
33 |
34 | #let _get-wrap-index(height-func, words, goal-height, reverse) = {
35 | for index in range(1, words.len(), step: 1) {
36 | let cur-height = height-func(_get-chunk(words, index, reverse))
37 | if cur-height > goal-height {
38 | return index - 1
39 | }
40 | }
41 | return -1
42 | }
43 |
44 | #let _rewrap(element, new-content) = {
45 | let fields = element.fields()
46 | for key in ("body", "text", "children", "child") {
47 | if key in fields {
48 | let _ = fields.remove(key)
49 | }
50 | }
51 | let positional = (new-content,)
52 | if "styles" in fields {
53 | positional.push(fields.remove("styles"))
54 | }
55 | element.func()(..fields, ..positional)
56 | }
57 |
58 | #let split-other(body, height-func, goal-height, align, splitter-func) = {
59 | (wrapped: none, rest: body)
60 | }
61 |
62 | #let split-has-text(body, height-func, goal-height, align, splitter-func) = {
63 | let words = body.text.split(" ")
64 | let reverse = align.y == bottom
65 | let wrap-index = _get-wrap-index(height-func, words, goal-height, reverse)
66 | let _rewrap = _rewrap.with(body)
67 | if wrap-index > 0 {
68 | let chunk = _rewrap(_get-chunk(words, wrap-index, reverse))
69 | let end-chunk = _rewrap(_get-chunk(words, words.len(), reverse, start: wrap-index))
70 | (
71 | wrapped: context {
72 | chunk
73 | linebreak(justify: par.justify)
74 | },
75 | rest: end-chunk,
76 | )
77 | } else {
78 | (wrapped: none, rest: body)
79 | }
80 | }
81 |
82 | #let split-has-children(body, height-func, goal-height, align, splitter-func) = {
83 | let reverse = align.y == bottom
84 | let children = if reverse {
85 | body.children.rev()
86 | } else {
87 | body.children
88 | }
89 | for (ii, child) in children.enumerate() {
90 | let prev-children = children.slice(0, ii).join()
91 | let new-height-func(child) = {
92 | height-func((prev-children, child).join())
93 | }
94 | let height = new-height-func(child)
95 | if height <= goal-height {
96 | continue
97 | }
98 | // height func calculator should now account for prior children
99 | let split = splitter-func(child, new-height-func, goal-height, align)
100 | let new-children = (..children.slice(0, ii), split.wrapped)
101 | let new-rest = children.slice(ii + 1)
102 | if split.rest != none {
103 | new-rest.insert(0, split.rest)
104 | }
105 | if reverse {
106 | new-children = new-children.rev()
107 | new-rest = new-rest.rev()
108 | }
109 | return (
110 | wrapped: _rewrap(body, new-children),
111 | rest: _rewrap(body, new-rest),
112 | )
113 | }
114 | panic("This function should only be called if the seq child should be split")
115 | }
116 |
117 | #let split-has-body(body, height-func, goal-height, align, splitter-func) = {
118 | // Elements that can be split and have a 'body' field.
119 | let splittable = (strong, emph, underline, stroke, overline, highlight, list.item, styled)
120 |
121 | let new-height-func(content) = {
122 | height-func(_rewrap(body, content))
123 | }
124 | let args = (new-height-func, goal-height, align, splitter-func)
125 | let body-text = body.at("body", default: body.at("child", default: none))
126 | if body.func() in splittable {
127 | let result = splitter-func(body-text, new-height-func, goal-height, align)
128 | if result.wrapped != none {
129 | return (wrapped: _rewrap(body, result.wrapped), rest: _rewrap(body, result.rest))
130 | } else {
131 | return split-other(body, ..args)
132 | }
133 | }
134 | // Shape doesn't split nicely, so treat it as unwrappable
135 | return split-other(body, ..args)
136 | }
137 |
138 | #let splitter(body, height-func, goal-height, align) = {
139 | let self-height = height-func(body)
140 | if self-height <= goal-height {
141 | return (wrapped: body, rest: none)
142 | }
143 | if type(body) == str {
144 | body = text(body)
145 | }
146 | let body-splitter = if body.has("text") {
147 | split-has-text
148 | } else if body.has("body") or body.has("child") {
149 | split-has-body
150 | } else if body.has("children") {
151 | split-has-children
152 | } else {
153 | split-other
154 | }
155 | return body-splitter(body, height-func, goal-height, align, splitter)
156 | }
157 |
158 | #let _inner-wrap-content(to-wrap, y-align, grid-func, container-size, ..grid-kwargs) = {
159 | let height-func(txt) = _grid-height(grid-func(txt), container-size)
160 | let goal-height = height-func([])
161 | if y-align == top {
162 | goal-height += measure(v(1em)).height
163 | }
164 | let result = splitter(to-wrap, height-func, goal-height, y-align)
165 | if y-align == top {
166 | grid-func(result.wrapped)
167 | result.rest
168 | } else {
169 | result.rest
170 | grid-func(result.wrapped)
171 | }
172 | }
173 |
174 | /// Places `to-wrap` next to `fixed`, wrapping `to-wrap` as its height overflows `fixed`.
175 | ///
176 | /// *Basic Use:*
177 | /// ```typ
178 | /// #let body = lorem(40)
179 | /// #wrap-content(rect(fill: teal), body)
180 | /// ```
181 | ///
182 | /// *Something More Fun:*
183 | /// ```typ
184 | /// #set par(justify: true)
185 | /// // Helpers; not required
186 | /// #let grad(map) = {
187 | /// gradient.linear(
188 | /// ..eval("color.map." + map)
189 | /// )
190 | /// }
191 | /// #let make-fig(fill) = {
192 | /// set figure.caption(separator: "")
193 | /// fill = grad(fill)
194 | /// figure(
195 | /// rect(fill: fill, radius: 0.5em),
196 | /// caption: [],
197 | /// )
198 | /// }
199 | /// #let (fig1, fig2) = {
200 | /// ("viridis", "plasma").map(make-fig)
201 | /// }
202 | /// #wrap-content(fig1, body, align: right)
203 | /// #wrap-content(fig2, [#body #body], align: bottom)
204 | /// ```
205 | ///
206 | /// Note that you can increase the distance between a figure's bottom and the wrapped
207 | /// text by boxing it with an inset:
208 | /// ```typ
209 | /// #let spaced = box(
210 | /// make-fig("rocket"),
211 | /// inset: (bottom: 0.3em)
212 | /// )
213 | /// #wrap-content(spaced, body)
214 | /// ```
215 | ///
216 | /// - fixed (content): Content that will not be wrapped, (i.e., a figure).
217 | ///
218 | /// - to-wrap (content): Content that will be wrapped, (i.e., text). Currently, logic
219 | /// works best with pure-text content, but hypothetically will work with any `content`.
220 | ///
221 | /// - align (alignment): Alignment of `fixed` relative to `to-wrap`. `top` will align
222 | /// the top of `fixed` with the top of `to-wrap`, and `bottom` will align the bottom of
223 | /// `fixed` with the bottom of `to-wrap`. `left` and `right` alignments determine
224 | /// horizontal alignment of `fixed` relative to `to-wrap`. Alignments can be combined,
225 | /// i.e., `bottom + right` will align the bottom-right corner of `fixed` with the
226 | /// bottom-right corner of `to-wrap`.
227 | /// ```typ
228 | /// #wrap-content(
229 | /// make-fig("turbo"),
230 | /// body,
231 | /// align: bottom + right
232 | /// )
233 | /// ```
234 | ///
235 | /// - size (size, auto): Size of the wrapping container. If `auto`, this will be set to
236 | /// the current container size. Otherwise, wrapping logic will attempt to stay within
237 | /// the provided constraints.
238 | ///
239 | /// - ..grid-kwargs (any): Keyword arguments to pass to the underlying `grid` function.
240 | /// Of note:
241 | /// - `column-gutter` controls horizontal margin between `fixed` and `to-wrap`. Or,
242 | /// you can surround the fixed content in a box with `(inset: ...)` for more
243 | /// fine-grained control.
244 | /// - `columns` can be set to force sizing of `fixed` and `to-wrap`. For instance,
245 | /// `columns: (50%, 50%)` will force `fixed` and `to-wrap` to each take up half
246 | /// of the available space. If content isn't this big, the fill will be blank
247 | /// margin.
248 | /// ```typ
249 | /// #let spaced = box(
250 | /// make-fig("mako"), inset: 0.5em
251 | /// )
252 | /// #wrap-content(spaced, body)
253 | /// ```
254 | /// ```typ
255 | /// #wrap-content(
256 | /// make-fig("spectral"),
257 | /// body,
258 | /// align: bottom,
259 | /// columns: (50%, 50%),
260 | /// )
261 | /// ```
262 | ///
263 | #let wrap-content(
264 | fixed,
265 | to-wrap,
266 | align: top + left,
267 | size: auto,
268 | ..grid-kwargs,
269 | ) = {
270 | if center in (align.x, align.y) {
271 | panic("Center alignment is not supported")
272 | }
273 |
274 | // "none" x alignment defaults to left
275 | let dir = if align.x == right {
276 | rtl
277 | } else {
278 | ltr
279 | }
280 | let gridded(..args) = box(_gridded(dir, fixed, ..grid-kwargs, ..args))
281 | // "none" y alignment defaults to top
282 | let y-align = if align.y == bottom {
283 | bottom
284 | } else {
285 | top
286 | }
287 |
288 | if size != auto {
289 | _inner-wrap-content(to-wrap, y-align, gridded, size, ..grid-kwargs)
290 | } else {
291 | layout(container-size => {
292 | _inner-wrap-content(to-wrap, y-align, gridded, container-size, ..grid-kwargs)
293 | })
294 | }
295 | }
296 |
297 | /// Wrap a body of text around two pieces of content. The logic only works if enough text
298 | /// exists to overflow both the top and bottom content. Use this instead of 2 separate
299 | /// `wrap-content` calls if you want to avoid a paragraph break between the top and bottom
300 | /// content.
301 | ///
302 | /// *Example:*
303 | /// ```typ
304 | /// #let fig1 = make-fig("inferno")
305 | /// #let fig2 = make-fig("rainbow")
306 | /// #wrap-top-bottom(fig1, fig2, lorem(60))
307 | /// ```
308 | /// - top-fixed (content): Content that will not be wrapped, (i.e., a figure).
309 | /// - bottom-fixed (content): Content that will not be wrapped, (i.e., a figure).
310 | /// - body (content): Content that will be wrapped, (i.e., text)
311 | /// - top-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function
312 | /// for the top content. `x` alignment is kept (left/right), but `y` alignment is
313 | /// overridden to `top`.
314 | /// - bottom-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function
315 | /// for the bottom content. `x` alignment is kept (left/right), but `y` alignment is
316 | /// overridden to `bottom`.
317 | ///
318 | #let wrap-top-bottom(
319 | top-fixed,
320 | bottom-fixed,
321 | body,
322 | top-kwargs: (:),
323 | bottom-kwargs: (:),
324 | ) = {
325 | top-kwargs = top-kwargs + (
326 | align: top-kwargs.at("align", default: top + left).x + top,
327 | )
328 | bottom-kwargs = bottom-kwargs + (
329 | align: bottom-kwargs.at("align", default: bottom + right).x + bottom,
330 | )
331 | layout(size => {
332 | let wrapfig(..args) = wrap-content(size: size, ..args)
333 | wrapfig(top-fixed, ..top-kwargs)[
334 | #wrapfig(bottom-fixed, ..bottom-kwargs)[
335 | #body
336 | ]
337 | ]
338 | })
339 | }
340 |
--------------------------------------------------------------------------------