├── 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 | ![Example 1](https://www.github.com/ntjess/wrap-it/raw/v0.1.1/assets/example-1.png) 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 | ![Example 2](https://www.github.com/ntjess/wrap-it/raw/v0.1.1/assets/example-2.png) 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 | ![Example 3](https://www.github.com/ntjess/wrap-it/raw/v0.1.1/assets/example-3.png) 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 | ![Example 4](https://www.github.com/ntjess/wrap-it/raw/v0.1.1/assets/example-4.png) 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 | ![Example 5](https://www.github.com/ntjess/wrap-it/raw/v0.1.1/assets/example-5.png) -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------