├── .cargo └── config.toml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── based ├── LICENSE ├── README.md ├── assets │ ├── example.svg │ └── example.typ ├── src │ ├── base16.typ │ ├── base32.typ │ ├── base64.typ │ ├── coder.typ │ └── lib.typ ├── tests │ ├── .gitignore │ ├── .ignore │ ├── decode │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── encode │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ └── template.typ └── typst.toml ├── droplet ├── LICENSE ├── README.md ├── assets │ ├── example-transform.svg │ ├── example-transform.typ │ ├── example.svg │ └── example.typ ├── src │ ├── droplet.typ │ ├── extract.typ │ ├── lib.typ │ ├── split.typ │ └── util.typ ├── tests │ ├── .gitignore │ ├── .ignore │ ├── basic │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── blocks │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── customize │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── explicit │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── extract │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── justify │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── split │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ └── template.typ └── typst.toml ├── equate ├── LICENSE ├── README.md ├── assets │ ├── example-1.svg │ ├── example-2.svg │ ├── example-local.svg │ ├── example-local.typ │ └── example.typ ├── src │ ├── equate.typ │ └── lib.typ ├── tests │ ├── .gitignore │ ├── .ignore │ ├── align-points │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── alignment │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── boxed │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── break │ │ ├── ref │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ └── 8.png │ │ └── test.typ │ ├── local │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── margin │ │ ├── ref │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ └── 7.png │ │ └── test.typ │ ├── number-mode │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── numbering │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ ├── reference │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ └── template.typ └── typst.toml ├── hash ├── LICENSE ├── README.md ├── assets │ ├── example.svg │ └── example.typ ├── bin │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── src │ ├── hash.typ │ ├── hash.wasm │ └── lib.typ └── typst.toml ├── outex ├── LICENSE ├── README.md ├── assets │ ├── example.svg │ └── example.typ ├── src │ ├── lib.typ │ └── outex.typ └── typst.toml ├── qr ├── LICENSE ├── README.md ├── assets │ ├── example.svg │ └── example.typ ├── bin │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── src │ ├── lib.typ │ ├── qr.typ │ └── qr.wasm └── typst.toml ├── quick-maths ├── LICENSE ├── README.md ├── assets │ ├── example.svg │ └── example.typ ├── src │ ├── lib.typ │ └── quick-maths.typ ├── tests │ ├── .gitignore │ ├── .ignore │ ├── shorthands │ │ ├── ref │ │ │ └── 1.png │ │ └── test.typ │ └── template.typ └── typst.toml └── united ├── LICENSE ├── README.md ├── assets ├── postfixes.csv ├── prefixes.csv └── units.csv ├── examples ├── numbers.svg ├── numbers.typ ├── quantities.svg ├── quantities.typ ├── ranges.svg ├── ranges.typ ├── units.svg └── units.typ ├── src ├── data.typ ├── lib.typ ├── number.typ ├── unit.typ └── united.typ └── typst.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | ci: 13 | name: Test "${{ matrix.package }}" 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | package: 19 | - based 20 | - droplet 21 | - equate 22 | - quick-maths 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Probe runner package cache 29 | uses: awalsh128/cache-apt-pkgs-action@latest 30 | with: 31 | packages: cargo 32 | version: 1.0 33 | 34 | - name: Install typst-test from GitHub 35 | uses: baptiste0928/cargo-install@v3 36 | with: 37 | crate: typst-test 38 | git: https://github.com/tingerrr/typst-test.git 39 | branch: ci-semi-stable 40 | 41 | - name: Setup typst 42 | uses: typst-community/setup-typst@v3 43 | 44 | - name: Run test suite 45 | working-directory: ${{ matrix.package }} 46 | run: typst-test run 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # VS Code 13 | .vscode/ 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "arrayref" 13 | version = "0.3.7" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" 16 | 17 | [[package]] 18 | name = "arrayvec" 19 | version = "0.7.4" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 22 | 23 | [[package]] 24 | name = "base64" 25 | version = "0.13.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 28 | 29 | [[package]] 30 | name = "bitflags" 31 | version = "1.3.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 34 | 35 | [[package]] 36 | name = "blake2" 37 | version = "0.10.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" 40 | dependencies = [ 41 | "digest", 42 | ] 43 | 44 | [[package]] 45 | name = "block-buffer" 46 | version = "0.10.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 49 | dependencies = [ 50 | "generic-array", 51 | ] 52 | 53 | [[package]] 54 | name = "bytemuck" 55 | version = "1.14.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" 58 | 59 | [[package]] 60 | name = "cfg-if" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 64 | 65 | [[package]] 66 | name = "color_quant" 67 | version = "1.1.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 70 | 71 | [[package]] 72 | name = "cpufeatures" 73 | version = "0.2.9" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" 76 | dependencies = [ 77 | "libc", 78 | ] 79 | 80 | [[package]] 81 | name = "crc32fast" 82 | version = "1.3.2" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 85 | dependencies = [ 86 | "cfg-if", 87 | ] 88 | 89 | [[package]] 90 | name = "crypto-common" 91 | version = "0.1.6" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 94 | dependencies = [ 95 | "generic-array", 96 | "typenum", 97 | ] 98 | 99 | [[package]] 100 | name = "data-url" 101 | version = "0.2.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" 104 | 105 | [[package]] 106 | name = "digest" 107 | version = "0.10.7" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 110 | dependencies = [ 111 | "block-buffer", 112 | "crypto-common", 113 | "subtle", 114 | ] 115 | 116 | [[package]] 117 | name = "fast_qr" 118 | version = "0.10.2" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "02b09ccb5449ffa2c57933337563fb82b4e0875f031b71410c9faac0cea3d30b" 121 | dependencies = [ 122 | "resvg", 123 | ] 124 | 125 | [[package]] 126 | name = "flate2" 127 | version = "1.0.27" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" 130 | dependencies = [ 131 | "crc32fast", 132 | "miniz_oxide 0.7.1", 133 | ] 134 | 135 | [[package]] 136 | name = "float-cmp" 137 | version = "0.9.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 140 | 141 | [[package]] 142 | name = "fontconfig-parser" 143 | version = "0.5.3" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "674e258f4b5d2dcd63888c01c68413c51f565e8af99d2f7701c7b81d79ef41c4" 146 | dependencies = [ 147 | "roxmltree 0.18.0", 148 | ] 149 | 150 | [[package]] 151 | name = "fontdb" 152 | version = "0.10.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "8131752b3f3b876a20f42b3d08233ad177d6e7ec6d18aaa6954489a201071be5" 155 | dependencies = [ 156 | "fontconfig-parser", 157 | "log", 158 | "memmap2", 159 | "ttf-parser", 160 | ] 161 | 162 | [[package]] 163 | name = "generic-array" 164 | version = "0.14.7" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 167 | dependencies = [ 168 | "typenum", 169 | "version_check", 170 | ] 171 | 172 | [[package]] 173 | name = "gif" 174 | version = "0.11.4" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" 177 | dependencies = [ 178 | "color_quant", 179 | "weezl", 180 | ] 181 | 182 | [[package]] 183 | name = "hash" 184 | version = "0.1.0" 185 | dependencies = [ 186 | "blake2", 187 | "digest", 188 | "md-5", 189 | "sha1", 190 | "sha2", 191 | "sha3", 192 | "wasm-minimal-protocol", 193 | ] 194 | 195 | [[package]] 196 | name = "imagesize" 197 | version = "0.10.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "df19da1e92fbfec043ca97d622955381b1f3ee72a180ec999912df31b1ccd951" 200 | 201 | [[package]] 202 | name = "jpeg-decoder" 203 | version = "0.3.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" 206 | 207 | [[package]] 208 | name = "keccak" 209 | version = "0.1.4" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" 212 | dependencies = [ 213 | "cpufeatures", 214 | ] 215 | 216 | [[package]] 217 | name = "kurbo" 218 | version = "0.8.3" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" 221 | dependencies = [ 222 | "arrayvec", 223 | ] 224 | 225 | [[package]] 226 | name = "libc" 227 | version = "0.2.147" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 230 | 231 | [[package]] 232 | name = "log" 233 | version = "0.4.20" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 236 | 237 | [[package]] 238 | name = "md-5" 239 | version = "0.10.5" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" 242 | dependencies = [ 243 | "digest", 244 | ] 245 | 246 | [[package]] 247 | name = "memmap2" 248 | version = "0.5.10" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" 251 | dependencies = [ 252 | "libc", 253 | ] 254 | 255 | [[package]] 256 | name = "miniz_oxide" 257 | version = "0.5.4" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" 260 | dependencies = [ 261 | "adler", 262 | ] 263 | 264 | [[package]] 265 | name = "miniz_oxide" 266 | version = "0.7.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 269 | dependencies = [ 270 | "adler", 271 | ] 272 | 273 | [[package]] 274 | name = "pico-args" 275 | version = "0.5.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 278 | 279 | [[package]] 280 | name = "png" 281 | version = "0.17.6" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" 284 | dependencies = [ 285 | "bitflags", 286 | "crc32fast", 287 | "flate2", 288 | "miniz_oxide 0.5.4", 289 | ] 290 | 291 | [[package]] 292 | name = "proc-macro2" 293 | version = "1.0.66" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 296 | dependencies = [ 297 | "unicode-ident", 298 | ] 299 | 300 | [[package]] 301 | name = "qr" 302 | version = "0.1.0" 303 | dependencies = [ 304 | "fast_qr", 305 | "wasm-minimal-protocol", 306 | ] 307 | 308 | [[package]] 309 | name = "quote" 310 | version = "1.0.33" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 313 | dependencies = [ 314 | "proc-macro2", 315 | ] 316 | 317 | [[package]] 318 | name = "rctree" 319 | version = "0.5.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" 322 | 323 | [[package]] 324 | name = "resvg" 325 | version = "0.28.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "c115863f2d3621999cf187e318bc92b16402dfeff6a48c74df700d77381394c1" 328 | dependencies = [ 329 | "gif", 330 | "jpeg-decoder", 331 | "log", 332 | "pico-args", 333 | "png", 334 | "rgb", 335 | "svgfilters", 336 | "svgtypes", 337 | "tiny-skia", 338 | "usvg", 339 | "usvg-text-layout", 340 | ] 341 | 342 | [[package]] 343 | name = "rgb" 344 | version = "0.8.36" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" 347 | dependencies = [ 348 | "bytemuck", 349 | ] 350 | 351 | [[package]] 352 | name = "roxmltree" 353 | version = "0.15.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "6b9de9831a129b122e7e61f242db509fa9d0838008bf0b29bb0624669edfe48a" 356 | dependencies = [ 357 | "xmlparser", 358 | ] 359 | 360 | [[package]] 361 | name = "roxmltree" 362 | version = "0.18.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8" 365 | dependencies = [ 366 | "xmlparser", 367 | ] 368 | 369 | [[package]] 370 | name = "rustybuzz" 371 | version = "0.6.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "ab9e34ecf6900625412355a61bda0bd68099fe674de707c67e5e4aed2c05e489" 374 | dependencies = [ 375 | "bitflags", 376 | "bytemuck", 377 | "smallvec", 378 | "ttf-parser", 379 | "unicode-bidi-mirroring", 380 | "unicode-ccc", 381 | "unicode-general-category", 382 | "unicode-script", 383 | ] 384 | 385 | [[package]] 386 | name = "sha1" 387 | version = "0.10.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 390 | dependencies = [ 391 | "cfg-if", 392 | "cpufeatures", 393 | "digest", 394 | ] 395 | 396 | [[package]] 397 | name = "sha2" 398 | version = "0.10.7" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" 401 | dependencies = [ 402 | "cfg-if", 403 | "cpufeatures", 404 | "digest", 405 | ] 406 | 407 | [[package]] 408 | name = "sha3" 409 | version = "0.10.8" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" 412 | dependencies = [ 413 | "digest", 414 | "keccak", 415 | ] 416 | 417 | [[package]] 418 | name = "simplecss" 419 | version = "0.2.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" 422 | dependencies = [ 423 | "log", 424 | ] 425 | 426 | [[package]] 427 | name = "siphasher" 428 | version = "0.3.11" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 431 | 432 | [[package]] 433 | name = "smallvec" 434 | version = "1.11.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" 437 | 438 | [[package]] 439 | name = "strict-num" 440 | version = "0.1.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" 443 | dependencies = [ 444 | "float-cmp", 445 | ] 446 | 447 | [[package]] 448 | name = "subtle" 449 | version = "2.5.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 452 | 453 | [[package]] 454 | name = "svgfilters" 455 | version = "0.4.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "639abcebc15fdc2df179f37d6f5463d660c1c79cd552c12343a4600827a04bce" 458 | dependencies = [ 459 | "float-cmp", 460 | "rgb", 461 | ] 462 | 463 | [[package]] 464 | name = "svgtypes" 465 | version = "0.8.2" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "22975e8a2bac6a76bb54f898a6b18764633b00e780330f0b689f65afb3975564" 468 | dependencies = [ 469 | "siphasher", 470 | ] 471 | 472 | [[package]] 473 | name = "tiny-skia" 474 | version = "0.8.4" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" 477 | dependencies = [ 478 | "arrayref", 479 | "arrayvec", 480 | "bytemuck", 481 | "cfg-if", 482 | "png", 483 | "tiny-skia-path", 484 | ] 485 | 486 | [[package]] 487 | name = "tiny-skia-path" 488 | version = "0.8.4" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" 491 | dependencies = [ 492 | "arrayref", 493 | "bytemuck", 494 | "strict-num", 495 | ] 496 | 497 | [[package]] 498 | name = "ttf-parser" 499 | version = "0.17.1" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "375812fa44dab6df41c195cd2f7fecb488f6c09fbaafb62807488cefab642bff" 502 | 503 | [[package]] 504 | name = "typenum" 505 | version = "1.16.0" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 508 | 509 | [[package]] 510 | name = "unicode-bidi" 511 | version = "0.3.13" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 514 | 515 | [[package]] 516 | name = "unicode-bidi-mirroring" 517 | version = "0.1.0" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" 520 | 521 | [[package]] 522 | name = "unicode-ccc" 523 | version = "0.1.2" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" 526 | 527 | [[package]] 528 | name = "unicode-general-category" 529 | version = "0.6.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" 532 | 533 | [[package]] 534 | name = "unicode-ident" 535 | version = "1.0.11" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 538 | 539 | [[package]] 540 | name = "unicode-script" 541 | version = "0.5.5" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" 544 | 545 | [[package]] 546 | name = "unicode-vo" 547 | version = "0.1.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" 550 | 551 | [[package]] 552 | name = "usvg" 553 | version = "0.28.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "8b5b7c2b30845b3348c067ca3d09e20cc6e327c288f0ca4c48698712abf432e9" 556 | dependencies = [ 557 | "base64", 558 | "data-url", 559 | "flate2", 560 | "imagesize", 561 | "kurbo", 562 | "log", 563 | "rctree", 564 | "roxmltree 0.15.1", 565 | "simplecss", 566 | "siphasher", 567 | "strict-num", 568 | "svgtypes", 569 | ] 570 | 571 | [[package]] 572 | name = "usvg-text-layout" 573 | version = "0.28.0" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "4c9550670848028641bf976b06f5c520ffdcd6f00ee7ee7eb0853f78e2c249d7" 576 | dependencies = [ 577 | "fontdb", 578 | "kurbo", 579 | "log", 580 | "rustybuzz", 581 | "unicode-bidi", 582 | "unicode-script", 583 | "unicode-vo", 584 | "usvg", 585 | ] 586 | 587 | [[package]] 588 | name = "venial" 589 | version = "0.5.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "61584a325b16f97b5b25fcc852eb9550843a251057a5e3e5992d2376f3df4bb2" 592 | dependencies = [ 593 | "proc-macro2", 594 | "quote", 595 | ] 596 | 597 | [[package]] 598 | name = "version_check" 599 | version = "0.9.4" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 602 | 603 | [[package]] 604 | name = "wasm-minimal-protocol" 605 | version = "0.1.0" 606 | source = "git+https://github.com/astrale-sharp/wasm-minimal-protocol#73639bf5c2266a72db624e4a29e322afe1742ede" 607 | dependencies = [ 608 | "proc-macro2", 609 | "quote", 610 | "venial", 611 | ] 612 | 613 | [[package]] 614 | name = "weezl" 615 | version = "0.1.7" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" 618 | 619 | [[package]] 620 | name = "xmlparser" 621 | version = "0.13.5" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" 624 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["hash/bin", "qr/bin"] 3 | resolver = "2" 4 | 5 | [workspace.dependencies] 6 | wasm-minimal-protocol = { git = "https://github.com/astrale-sharp/wasm-minimal-protocol" } 7 | 8 | [profile.release] 9 | lto = true 10 | strip = true 11 | opt-level = 'z' 12 | codegen-units = 1 13 | panic = 'abort' 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typst-plugins 2 | 3 | > [!WARNING] 4 | > This repository has been archived. Packages published to the Typst Universe have been moved to their own repositories: 5 | > - [EpicEricEE/typst-based](https://github.com/EpicEricEE/typst-based) 6 | > - [EpicEricEE/typst-droplet](https://github.com/EpicEricEE/typst-droplet) 7 | > - [EpicEricEE/typst-equate](https://github.com/EpicEricEE/typst-equate) 8 | > - [EpicEricEE/typst-quick-maths](https://github.com/EpicEricEE/typst-quick-maths) 9 | > 10 | > The packages are still available in the `@preview` namespace. 11 | 12 | This repository contains my packages for [Typst](https://github.com/typst/typst). More information about each of the packages can be found in their respective directories. 13 | 14 | | Name | Version | Description | 15 | |-----------------------------|----------------|-----------------------------------------------------| 16 | | [based](based/) | 0.1.0 [:link:] | Encoder and decoder for base64, base32, and base16. | 17 | | [droplet](droplet/) | 0.2.0 [:link:] | Customizable dropped capitals. | 18 | | [equate](equate/) | 0.2.0 [:link:] | Breakable equations with improved numbering. | 19 | | [hash](hash/) | 0.1.0 | Implementation of multiple hashing algorithms. | 20 | | [outex](outex/) | 0.1.0 | Outlines styled like in LaTeX. | 21 | | [qr](qr/) | 0.1.0 | Fast QR Code generator. | 22 | | [quick-maths](quick-maths/) | 0.1.0 [:link:] | Custom shorthands for math equations. | 23 | | [united](united/) | 0.1.0 | Easy typesetting of numbers with units. | 24 | 25 | Packages marked with a :link: are available in the `@preview` namespace. 26 | 27 | ## Building 28 | Packages that run on top of WASM need to be built before they can be used. The package source directories already contain the compiled WASM files of the latest state. To build the WASM files yourself, you need to have [Rust](https://www.rust-lang.org/) installed with the `wasm32-unknown-unknown` target. 29 | 30 | - To build all WASM files, run `cargo build -r` 31 | - To only build a specific package, run `cargo build -r -p ` 32 | - After building, the WASM files can be found in `target/wasm32-unknown-unknown/release/` 33 | 34 | The commands are to be run in the root directory of this repository. 35 | 36 | ## License 37 | The license for each package is found in its respective subdirectory. 38 | 39 | [:link:]: https://typst.app/docs/packages 40 | -------------------------------------------------------------------------------- /based/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eric Biedert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 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 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /based/README.md: -------------------------------------------------------------------------------- 1 | # based 2 | A package for encoding and decoding in base64, base32, and base16. 3 | 4 | > [!WARNING] 5 | > This repository has been archived. The package has been moved to the [EpicEricEE/typst-based](https://github.com/EpicEricEE/typst-based) repository. 6 | 7 | ## Usage 8 | The package comes with three submodules: `base64`, `base32`, and `base16`. All of them have an `encode` and `decode` function. The package also provides the function aliases 9 | - `encode64` / `decode64`, 10 | - `encode32` / `decode32`, and 11 | - `encode16` / `decode16`. 12 | 13 | Both base64 and base32 allow you to choose whether to use padding or not (`pad` parameter). It is enabled by default. Base64 also allows you to encode with the URL-safe alphabet (`url` parameter), while base32 allows you to encode or decode with the "extended hex" alphabet (`hex` parameter). Both options are disabled by default. The base16 encoder uses lowercase letters, the decoder is case-insensitive. 14 | 15 | You can encode strings, arrays and bytes. The `encode` function will return a string, while the `decode` function will return bytes. 16 | 17 | ```typ 18 | #import "@preview/based:0.1.0": base64, base32, base16 19 | 20 | #table( 21 | columns: 3, 22 | 23 | table.header[*Base64*][*Base32*][*Base16*], 24 | 25 | raw(base64.encode("Hello world!")), 26 | raw(base32.encode("Hello world!")), 27 | raw(base16.encode("Hello world!")), 28 | 29 | str(base64.decode("SGVsbG8gd29ybGQh")), 30 | str(base32.decode("JBSWY3DPEB3W64TMMQQQ====")), 31 | str(base16.decode("48656C6C6F20776F726C6421")) 32 | ) 33 | ``` 34 | 35 | ![Result of example code.](./assets/example.svg) 36 | -------------------------------------------------------------------------------- /based/assets/example.typ: -------------------------------------------------------------------------------- 1 | #import "../src/lib.typ": base64, base32, base16 2 | 3 | #set text(size: 14pt) 4 | #set page( 5 | width: auto, 6 | height: auto, 7 | margin: 1em, 8 | background: pad(0.5pt, box( 9 | width: 100%, 10 | height: 100%, 11 | radius: 4pt, 12 | fill: white, 13 | stroke: white.darken(10%), 14 | )), 15 | ) 16 | 17 | #table( 18 | columns: 3, 19 | inset: 0.5em, 20 | 21 | table.header[*Base64*][*Base32*][*Base16*], 22 | 23 | raw(base64.encode("Hello world!")), 24 | raw(base32.encode("Hello world!")), 25 | raw(base16.encode("Hello world!")), 26 | 27 | str(base64.decode("SGVsbG8gd29ybGQh")), 28 | str(base32.decode("JBSWY3DPEB3W64TMMQQQ====")), 29 | str(base16.decode("48656C6C6F20776F726C6421")) 30 | ) 31 | -------------------------------------------------------------------------------- /based/src/base16.typ: -------------------------------------------------------------------------------- 1 | /// Encodes the given data as a hex string. 2 | /// 3 | /// Arguments: 4 | /// - data: The data to encode. Must be of type array, bytes, or string. 5 | /// 6 | /// Returns: The encoded string (lowercase). 7 | #let encode(data) = { 8 | if data.len() == 0 { return "" } 9 | 10 | for byte in array(bytes(data)) { 11 | if byte < 16 { "0" } 12 | str(int(byte), base: 16) 13 | } 14 | } 15 | 16 | /// Decodes the given hex string. 17 | /// 18 | /// Arguments: 19 | /// - string: The string to decode (case-insensitive). 20 | /// 21 | /// Returns: The decoded bytes. 22 | #let decode(string) = { 23 | let dec(hex-digit) = { 24 | let code = str.to-unicode(hex-digit) 25 | if code >= 48 and code <= 57 { code - 48 } // 0-9 26 | else if code >= 65 and code <= 70 { code - 55 } // A-F 27 | else if code >= 97 and code <= 102 { code - 87 } // a-f 28 | else { panic("Invalid hex digit: " + hex-digit) } 29 | } 30 | 31 | let array = range(string.len(), step: 2).map(i => { 32 | 16 * dec(string.at(i)) + dec(string.at(i + 1)) 33 | }) 34 | bytes(array) 35 | } 36 | -------------------------------------------------------------------------------- /based/src/base32.typ: -------------------------------------------------------------------------------- 1 | #import "coder.typ" 2 | 3 | #let alphabet-32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" 4 | #let alphabet-32-hex = "0123456789ABCDEFGHIJKLMNOPQRSTUV" 5 | 6 | /// Encodes the given data in base32 format. 7 | /// 8 | /// Arguments: 9 | /// - data: The data to encode. Must be of type array, bytes, or string. 10 | /// - pad: Whether to pad the output with "=" characters. 11 | /// - hex: Whether to use the extended base32hex alphabet. 12 | /// 13 | /// Returns: The encoded string. 14 | #let encode(data, pad: true, hex: false) = { 15 | let alphabet = if hex { alphabet-32-hex } else { alphabet-32 } 16 | coder.encode(data, alphabet, pad: pad) 17 | } 18 | 19 | /// Decodes the given base32 string. 20 | /// 21 | /// Arguments: 22 | /// - string: The string to decode. 23 | /// - hex: Whether to use the extended base32hex alphabet. 24 | /// 25 | /// Returns: The decoded bytes. 26 | #let decode(string, hex: false) = { 27 | let alphabet = if hex { alphabet-32-hex } else { alphabet-32 } 28 | coder.decode(string, alphabet) 29 | } 30 | -------------------------------------------------------------------------------- /based/src/base64.typ: -------------------------------------------------------------------------------- 1 | #import "coder.typ" 2 | 3 | #let alphabet-64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 4 | #let alphabet-64-url = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" 5 | 6 | /// Encodes the given data in base64 format. 7 | /// 8 | /// Arguments: 9 | /// - data: The data to encode. Must be of type array, bytes, or string. 10 | /// - pad: Whether to pad the output with "=" characters. 11 | /// - url: Whether to use the URL safe alphabet. 12 | /// 13 | /// Returns: The encoded string. 14 | #let encode(data, pad: true, url: false) = { 15 | let alphabet = if url { alphabet-64-url } else { alphabet-64 } 16 | coder.encode(data, alphabet, pad: pad) 17 | } 18 | 19 | /// Decodes the given base64 string. 20 | /// 21 | /// URL safe characters are automatically converted to their standard 22 | /// counterparts. Invalid characters are ignored. 23 | /// 24 | /// Arguments: 25 | /// - string: The string to decode. 26 | /// 27 | /// Returns: The decoded bytes. 28 | #let decode(string) = { 29 | string = string.replace("-", "+").replace("_", "/") 30 | coder.decode(string, alphabet-64) 31 | } 32 | -------------------------------------------------------------------------------- /based/src/coder.typ: -------------------------------------------------------------------------------- 1 | /// Convert a number to a binary array and pad it. 2 | /// 3 | /// Arguments: 4 | /// - number: The number to convert. 5 | /// - size: The size of the array. If given, the array will be padded with 0s. 6 | /// 7 | /// Returns: The binary array. 8 | #let bin(number, size: none) = { 9 | let result = while number > 0 { 10 | (calc.rem(number, 2),) 11 | number = calc.floor(number / 2); 12 | } 13 | 14 | if result == none { result = (0,) } 15 | if size != none and result.len() < size { 16 | result.push(((0,) * (size - result.len()))); 17 | } 18 | 19 | return result.rev().flatten(); 20 | } 21 | 22 | /// Convert a binary array to a number. 23 | /// 24 | /// Arguments: 25 | /// - array: The binary array to convert. 26 | /// 27 | /// Returns: The number. 28 | #let dec(array) = { 29 | array.enumerate().fold(0, (acc, (i, bit)) => { 30 | acc + bit * calc.pow(2, (array.len() - i - 1)) 31 | }) 32 | } 33 | 34 | /// Encodes the given data with the given alphabet. 35 | /// 36 | /// Arguments: 37 | /// - data: The data to encode. Must be of type array, bytes, or string. 38 | /// - alphabet: The alphabet to use for encoding. Its size must be a power of 2. 39 | /// - pad: Whether to pad the output with "=" characters. 40 | /// 41 | /// Returns: The encoded string. 42 | #let encode(data, alphabet, pad: true) = { 43 | let chunk-size = calc.log(alphabet.len(), base: 2) 44 | assert.eq(calc.fract(chunk-size), 0, message: "alphabet size must be a power of 2") 45 | chunk-size = int(chunk-size) 46 | 47 | let bytes = array(bytes(data)) 48 | if bytes.len() == 0 { return "" } 49 | 50 | let bits = bytes.map(bin.with(size: 8)).flatten() 51 | 52 | let pad-chunk-amount = calc.rem(chunk-size - calc.rem(bits.len(), chunk-size), chunk-size) 53 | bits += ((0,) * pad-chunk-amount) 54 | 55 | let string = for i in range(0, bits.len(), step: chunk-size) { 56 | let chunk = bits.slice(i, i + chunk-size) 57 | alphabet.at(dec(chunk)) 58 | } 59 | 60 | if pad { 61 | let lcm = calc.lcm(8, chunk-size) 62 | let pad-amount = calc.rem(lcm - calc.rem(bits.len(), lcm), lcm) 63 | string += range(int(pad-amount / chunk-size)).map(_ => "=").join("") 64 | } 65 | 66 | string 67 | } 68 | 69 | /// Decodes the given string with the given alphabet. 70 | /// 71 | /// Arguments: 72 | /// - string: The string to decode. 73 | /// - alphabet: The alphabet to use for decoding. 74 | /// 75 | /// Returns: The decoded bytes. 76 | #let decode(string, alphabet) = { 77 | let chunk-size = calc.log(alphabet.len(), base: 2) 78 | assert.eq(calc.fract(chunk-size), 0, message: "alphabet size must be a power of 2") 79 | chunk-size = int(chunk-size) 80 | 81 | string = string.replace("=", "") 82 | 83 | let bits = string.codepoints() 84 | .map(c => alphabet.position(c)) 85 | .filter(n => n != none) 86 | .map(bin.with(size: chunk-size)) 87 | .flatten() 88 | 89 | let pad-amount = calc.rem(bits.len(), 8) 90 | if pad-amount > 0 { 91 | bits = bits.slice(0, -pad-amount) 92 | } 93 | 94 | let byte-array = range(0, bits.len(), step: 8).map(i => { 95 | let chunk = bits.slice(i, i + 8) 96 | dec(chunk) 97 | }) 98 | 99 | bytes(byte-array) 100 | } 101 | -------------------------------------------------------------------------------- /based/src/lib.typ: -------------------------------------------------------------------------------- 1 | #import "base64.typ" 2 | #import "base32.typ" 3 | #import "base16.typ" 4 | 5 | #let encode64 = base64.encode 6 | #let decode64 = base64.decode 7 | 8 | #let encode32 = base32.encode 9 | #let decode32 = base32.decode 10 | 11 | #let encode16 = base16.encode 12 | #let decode16 = base16.decode 13 | -------------------------------------------------------------------------------- /based/tests/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **/out/ 3 | **/diff/ 4 | -------------------------------------------------------------------------------- /based/tests/.ignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **.png 3 | **.svg 4 | **.pdf 5 | -------------------------------------------------------------------------------- /based/tests/decode/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/based/tests/decode/ref/1.png -------------------------------------------------------------------------------- /based/tests/decode/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": * 2 | 3 | #set page(width: auto, height: auto, margin: 0pt) 4 | 5 | // Test cases from: https://www.rfc-editor.org/rfc/rfc4648#section-10 6 | 7 | #{ 8 | // Test Base64 9 | assert.eq(str(decode64("")), "") 10 | assert.eq(str(decode64("Zg")), "f") 11 | assert.eq(str(decode64("Zm8=")), "fo") 12 | assert.eq(str(decode64("Zm9v")), "foo") 13 | assert.eq(str(decode64("Zm9vYg==")), "foob") 14 | assert.eq(str(decode64("Zm9vYmE")), "fooba") 15 | assert.eq(str(decode64("Zm9vYmFy")), "foobar") 16 | 17 | // Test Base32 18 | assert.eq(str(decode32("")), "") 19 | assert.eq(str(decode32("MY======")), "f") 20 | assert.eq(str(decode32("MZXQ")), "fo") 21 | assert.eq(str(decode32("MZXW6===")), "foo") 22 | assert.eq(str(decode32("MZXW6YQ=")), "foob") 23 | assert.eq(str(decode32("MZXW6YTB")), "fooba") 24 | assert.eq(str(decode32("MZXW6YTBOI")), "foobar") 25 | 26 | // Test Base32 with extended hex alphabet 27 | assert.eq(str(decode32(hex: true, "")), "") 28 | assert.eq(str(decode32(hex: true, "CO======")), "f") 29 | assert.eq(str(decode32(hex: true, "CPNG")), "fo") 30 | assert.eq(str(decode32(hex: true, "CPNMU===")), "foo") 31 | assert.eq(str(decode32(hex: true, "CPNMUOG")), "foob") 32 | assert.eq(str(decode32(hex: true, "CPNMUOJ1")), "fooba") 33 | assert.eq(str(decode32(hex: true, "CPNMUOJ1E8======")), "foobar") 34 | 35 | // Test Base16 36 | assert.eq(str(decode16("")), "") 37 | assert.eq(str(decode16("66")), "f") 38 | assert.eq(str(decode16("666f")), "fo") 39 | assert.eq(str(decode16("666F6F")), "foo") 40 | assert.eq(str(decode16("666f6f62")), "foob") 41 | assert.eq(str(decode16("666F6F6261")), "fooba") 42 | assert.eq(str(decode16("666F6f626172")), "foobar") 43 | } 44 | -------------------------------------------------------------------------------- /based/tests/encode/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/based/tests/encode/ref/1.png -------------------------------------------------------------------------------- /based/tests/encode/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": * 2 | 3 | #set page(width: auto, height: auto, margin: 0pt) 4 | 5 | // Test cases from: https://www.rfc-editor.org/rfc/rfc4648#section-10 6 | 7 | #{ 8 | // Test Base64 9 | assert.eq(encode64(""), "") 10 | assert.eq(encode64("f"), "Zg==") 11 | assert.eq(encode64("fo"), "Zm8=") 12 | assert.eq(encode64("foo"), "Zm9v") 13 | assert.eq(encode64("foob"), "Zm9vYg==") 14 | assert.eq(encode64("fooba"), "Zm9vYmE=") 15 | assert.eq(encode64("foobar"), "Zm9vYmFy") 16 | 17 | // Test Base32 18 | assert.eq(encode32(""), "") 19 | assert.eq(encode32("f"), "MY======") 20 | assert.eq(encode32("fo"), "MZXQ====") 21 | assert.eq(encode32("foo"), "MZXW6===") 22 | assert.eq(encode32("foob"), "MZXW6YQ=") 23 | assert.eq(encode32("fooba"), "MZXW6YTB") 24 | assert.eq(encode32("foobar"), "MZXW6YTBOI======") 25 | 26 | // Test Base64 with extended hex alphabet 27 | assert.eq(encode32(hex: true, ""), "") 28 | assert.eq(encode32(hex: true, "f"), "CO======") 29 | assert.eq(encode32(hex: true, "fo"), "CPNG====") 30 | assert.eq(encode32(hex: true, "foo"), "CPNMU===") 31 | assert.eq(encode32(hex: true, "foob"), "CPNMUOG=") 32 | assert.eq(encode32(hex: true, "fooba"), "CPNMUOJ1") 33 | assert.eq(encode32(hex: true, "foobar"), "CPNMUOJ1E8======") 34 | 35 | // Test Base16 36 | assert.eq(encode16(""), "") 37 | assert.eq(encode16("f"), "66") 38 | assert.eq(encode16("fo"), "666f") 39 | assert.eq(encode16("foo"), "666f6f") 40 | assert.eq(encode16("foob"), "666f6f62") 41 | assert.eq(encode16("fooba"), "666f6f6261") 42 | assert.eq(encode16("foobar"), "666f6f626172") 43 | } 44 | -------------------------------------------------------------------------------- /based/tests/template.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": * 2 | 3 | #set page(width: auto, height: auto, margin: 0pt) 4 | 5 | #{ 6 | 7 | } 8 | -------------------------------------------------------------------------------- /based/typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "based" 3 | version = "0.1.0" 4 | entrypoint = "src/lib.typ" 5 | authors = ["Eric Biedert"] 6 | license = "MIT" 7 | description = "Encoder and decoder for base64, base32, and base16." 8 | repository = "https://github.com/EpicEricEE/typst-plugins" 9 | exclude = ["README.md", "assets", "tests"] 10 | categories = ["scripting"] 11 | -------------------------------------------------------------------------------- /droplet/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eric Biedert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 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 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /droplet/README.md: -------------------------------------------------------------------------------- 1 | # droplet 2 | A package for creating dropped capitals in typst. 3 | 4 | > [!WARNING] 5 | > This repository has been archived. The package has been moved to the [EpicEricEE/typst-droplet](https://github.com/EpicEricEE/typst-droplet) repository. 6 | 7 | ## Usage 8 | The package comes with a single `dropcap` function that takes content and a few optional parameters. The first letter can either be passed as a positional parameter, or is automatically extracted from the passed body. The rest of the content will be wrapped around the dropped capital by splitting it into two paragraphs. The parameters are as follows: 9 | 10 | | Parameter | Description | Default | 11 | |------------------|-------------------------------------------------------------------|---------| 12 | | `height` | The height of the dropped capital in lines or as length. | `2` | 13 | | `justify` | Whether the text should be justified. | `auto` | 14 | | `gap` | The space between the first letter and the text. | `0pt` | 15 | | `hanging-indent` | The indent of lines after the first. | `0pt` | 16 | | `overhang` | The amount by which the first letter should hang into the margin. | `0pt` | 17 | | `depth` | The space below the dropped capital in lines or as length. | `0pt` | 18 | | `transform` | A function to be applied to the first letter. | `none` | 19 | | `..text-args` | Named arguments to be passed to the text function. | `(:)` | 20 | 21 | ```typ 22 | #import "@preview/droplet:0.2.0": dropcap 23 | 24 | #set par(justify: true) 25 | 26 | #dropcap( 27 | height: 3, 28 | gap: 4pt, 29 | hanging-indent: 1em, 30 | overhang: 8pt, 31 | font: "Curlz MT", 32 | )[ 33 | *Typst* is a new markup-based typesetting system that is designed to be as 34 | _powerful_ as LaTeX while being _much easier_ to learn and use. Typst has: 35 | 36 | - Built-in markup for the most common formatting tasks 37 | - Flexible functions for everything else 38 | - A tightly integrated scripting system 39 | - Math typesetting, bibliography management, and more 40 | - Fast compile times thanks to incremental compilation 41 | - Friendly error messages in case something goes wrong 42 | ] 43 | ``` 44 | 45 | ![Result of example code.](assets/example.svg) 46 | 47 | ## Extended Customization 48 | To further customize the appearance of the dropped capital, you can apply a `transform` function, which takes the first letter as a string and returns the content to be shown. The font size of the letter is then scaled so that the height of the transformed content matches the given height. 49 | 50 | ```typ 51 | #import "@preview/droplet:0.2.0": dropcap 52 | 53 | #dropcap( 54 | height: 2, 55 | justify: true, 56 | gap: 6pt, 57 | transform: letter => style(styles => { 58 | let height = measure(letter, styles).height 59 | 60 | grid(columns: 2, gutter: 6pt, 61 | align(center + horizon, text(blue, letter)), 62 | // Use "place" to ignore the line's height when 63 | // the font size is calculated later on. 64 | place(horizon, line( 65 | angle: 90deg, 66 | length: height + 6pt, 67 | stroke: blue.lighten(40%) + 1pt 68 | )), 69 | ) 70 | }), 71 | lorem(21) 72 | ) 73 | 74 | ``` 75 | 76 | ![Result of example code.](assets/example-transform.svg) 77 | -------------------------------------------------------------------------------- /droplet/assets/example-transform.typ: -------------------------------------------------------------------------------- 1 | #import "../src/lib.typ": dropcap 2 | 3 | #set text(size: 14pt) 4 | #set page( 5 | width: 8cm, 6 | height: auto, 7 | margin: 1em, 8 | background: pad(0.5pt, box( 9 | width: 100%, 10 | height: 100%, 11 | radius: 4pt, 12 | fill: white, 13 | stroke: white.darken(10%), 14 | )), 15 | ) 16 | 17 | #dropcap( 18 | height: 2, 19 | justify: true, 20 | gap: 6pt, 21 | transform: letter => style(styles => { 22 | let height = measure(letter, styles).height 23 | 24 | grid(columns: 2, gutter: 6pt, 25 | align(center + horizon, text(blue, letter)), 26 | // Use "place" to ignore the line's height when 27 | // the font size is calculated later on. 28 | place(horizon, line( 29 | angle: 90deg, 30 | length: height + 6pt, 31 | stroke: blue.lighten(40%) + 1pt 32 | )), 33 | ) 34 | }), 35 | lorem(21) 36 | ) 37 | -------------------------------------------------------------------------------- /droplet/assets/example.typ: -------------------------------------------------------------------------------- 1 | #import "../src/lib.typ": dropcap 2 | 3 | #set par(justify: true) 4 | #set text(size: 14pt) 5 | #set page( 6 | width: 11cm + 16pt, 7 | height: auto, 8 | margin: (x: 1em + 8pt, y: 1em), 9 | background: pad(0.5pt, box( 10 | width: 100%, 11 | height: 100%, 12 | radius: 4pt, 13 | fill: white, 14 | stroke: white.darken(10%), 15 | )), 16 | ) 17 | 18 | #dropcap( 19 | height: 3, 20 | gap: 4pt, 21 | hanging-indent: 1em, 22 | overhang: 8pt, 23 | font: "Curlz MT", 24 | )[ 25 | *Typst* is a new markup-based typesetting system that is designed to be as _powerful_ as LaTeX while being _much easier_ to learn and use. Typst has: 26 | 27 | - Built-in markup for the most common formatting tasks 28 | - Flexible functions for everything else 29 | - A tightly integrated scripting system 30 | - Math typesetting, bibliography management, and more 31 | - Fast compile times thanks to incremental compilation 32 | - Friendly error messages in case something goes wrong 33 | ] 34 | -------------------------------------------------------------------------------- /droplet/src/droplet.typ: -------------------------------------------------------------------------------- 1 | #import "extract.typ": extract 2 | #import "split.typ": split 3 | #import "util.typ": inline 4 | 5 | // Sets the font size so the resulting text height matches the given height. 6 | // 7 | // If not specified otherwise in "text-args", the top and bottom edge of the 8 | // resulting text element will be set to "bounds". If the given body does not 9 | // contain any text, the original body is returned with only the given 10 | // arguments applied. 11 | // 12 | // Parameters: 13 | // - height: The target height of the resulting text. 14 | // - threshold: The maximum difference between target and actual height. 15 | // - text-args: Arguments to be passed to the underlying text element. 16 | // - body: The content of the text element. 17 | // 18 | // Returns: The text with the set font size. 19 | #let sized(height, ..text-args, threshold: 0.1pt, body) = context { 20 | let styled-text = text.with( 21 | top-edge: "bounds", 22 | bottom-edge: "bounds", 23 | ..text-args.named(), 24 | body 25 | ) 26 | 27 | let size = height 28 | let font-height = measure(styled-text(size: size)).height 29 | 30 | // This should only take one iteration, but just in case... 31 | let i = 0 32 | while font-height > 0pt and i < 100 and calc.abs(font-height - height) > threshold { 33 | size *= 1 + (height - font-height) / font-height 34 | font-height = measure(styled-text(size: size)).height 35 | i += 1 36 | } 37 | 38 | return if i < 100 { 39 | styled-text(size: size) 40 | } else { 41 | // Font size calculation did not converge, as there is probably no text 42 | // that can be set to the given height. Return the original text instead, 43 | // with only the given arguments applied. 44 | text(..text-args.named(), body) 45 | } 46 | } 47 | 48 | // Resolves the given height to an absolute length. 49 | // 50 | // Height can be given as an integer, which is interpreted as the number of 51 | // lines, or as a length. 52 | // 53 | // Requires context. 54 | #let resolve-height(height) = { 55 | if type(height) == int { 56 | // Create dummy content to convert line count to height. 57 | let sample-lines = range(height).map(_ => [x]).join(linebreak()) 58 | measure(sample-lines).height 59 | } else { 60 | height.to-absolute() 61 | } 62 | 63 | } 64 | 65 | // Shows the first letter of the given content in a larger font. 66 | // 67 | // If the first letter is not given as a positional argument, it is extracted 68 | // from the content. The rest of the content is split into two pieces, where 69 | // one is positioned next to the dropped capital, and the other below it. 70 | // 71 | // Parameters: 72 | // - height: The height of the first letter. Can be given as the number of 73 | // lines (integer) or as a length. 74 | // - justify: Whether to justify the text next to the first letter. 75 | // - gap: The space between the first letter and the text. 76 | // - hanging-indent: The indent of lines after the first line. 77 | // - overhang: The amount by which the first letter should overhang into the 78 | // margin. Ratios are relative to the width of the first letter. 79 | // - depth: The minimum space below the first letter. Can be given as the 80 | // number of lines (integer) or as a length. 81 | // - transform: A function to be applied to the first letter. 82 | // - text-args: Named arguments to be passed to the underlying text element. 83 | // - body: The content to be shown. 84 | // 85 | // Returns: The content with the first letter shown in a larger font. 86 | #let dropcap( 87 | height: 2, 88 | justify: auto, 89 | gap: 0pt, 90 | hanging-indent: 0pt, 91 | overhang: 0pt, 92 | depth: 0pt, 93 | transform: none, 94 | ..text-args, 95 | body 96 | ) = layout(bounds => context { 97 | let (letter, rest) = if text-args.pos() == () { 98 | extract(body) 99 | } else { 100 | // First letter already given. 101 | (text-args.pos().first(), body) 102 | } 103 | 104 | if transform != none { 105 | letter = transform(letter) 106 | } 107 | 108 | let letter-height = resolve-height(height) 109 | let depth = resolve-height(depth) 110 | 111 | // Create dropcap with the height of sample content. 112 | let letter = box( 113 | height: letter-height + depth, 114 | sized(letter-height, letter, ..text-args.named()) 115 | ) 116 | let letter-width = measure(letter).width 117 | 118 | // Resolve overhang if given as percentage. 119 | let overhang = if type(overhang) == ratio { 120 | letter-width * overhang 121 | } else if type(overhang) == relative { 122 | letter-width * overhang.ratio + overhang.length 123 | } else { 124 | overhang 125 | } 126 | 127 | // Resolve justify if given as auto. 128 | let justify = if justify == auto { par.justify } else { justify } 129 | 130 | // Try to justify as many words as possible next to dropcap. 131 | let bounded = box.with(width: bounds.width - letter-width - gap + overhang) 132 | 133 | let index = 1 134 | let top-position = 0pt 135 | let prev-height = 0pt 136 | let (first, second) = while true { 137 | let (first, second) = split(rest, index) 138 | let first = { 139 | set par(hanging-indent: hanging-indent, justify: justify) 140 | first 141 | } 142 | 143 | let height = measure(bounded(first)).height 144 | let new = split(first, -1).at(1) 145 | top-position = calc.max( 146 | top-position, 147 | height - measure(new).height - par.leading.to-absolute() 148 | ) 149 | 150 | if top-position >= letter-height + depth and height > prev-height { 151 | // Limit reached, new element doesn't fit anymore 152 | split(rest, index - 1) 153 | break 154 | } 155 | 156 | if second == none { 157 | // All content fits next to dropcap. 158 | (first, none) 159 | break 160 | } 161 | 162 | index += 1 163 | prev-height = height 164 | } 165 | 166 | // Layout dropcap and aside text as grid. 167 | set par(justify: justify) 168 | 169 | let last-of-first-inline = inline(split(first, -1).at(1)) 170 | let first-of-second-inline = second != none and inline(split(second, 1).at(0)) 171 | let func = if last-of-first-inline { box } else { block } 172 | 173 | func(grid( 174 | column-gutter: gap, 175 | columns: (letter-width - overhang, 1fr), 176 | move(dx: -overhang, letter), 177 | { 178 | set par(hanging-indent: hanging-indent) 179 | first 180 | 181 | if last-of-first-inline and first-of-second-inline { 182 | linebreak(justify: justify) 183 | } 184 | } 185 | )) 186 | 187 | if func == box { linebreak() } 188 | second 189 | }) 190 | -------------------------------------------------------------------------------- /droplet/src/extract.typ: -------------------------------------------------------------------------------- 1 | #import "util.typ": attach-label, space, splittable, to-string 2 | 3 | // Regex for valid characters in front of the dropped capital. 4 | #let regex-before = regex({ 5 | "[" 6 | "\"'" // Dumb quotes 7 | "\p{C}" // Control characters 8 | "\p{Pi}" // Initial punctuation 9 | "\p{Ps}" // Opening punctuation 10 | "\p{Z}" // Spaces and separators 11 | "¹²³\u2070-\u209F" // Superscripts and subscripts 12 | "]+" 13 | }) 14 | 15 | // Regex for valid characters behind the dropped capital. 16 | #let regex-after = regex({ 17 | "[" 18 | "\." // Full stop 19 | "\"'" // Dumb quotes / apostrophe 20 | "\p{C}" // Control characters 21 | "\p{Pf}" // Final punctuation 22 | "\p{Pe}" // Closing punctuation 23 | "\p{Z}" // Spaces and separators 24 | "\p{M}" // Combining marks 25 | "¹²³\u2070-\u209F" // Superscripts and subscripts 26 | "]+" 27 | }) 28 | 29 | // Extracts the first letter of the given content. 30 | // 31 | // The first letter may be none if the content does not contain any letters. 32 | // If the first child cannot be split further, that child is returned as the 33 | // first letter. 34 | // 35 | // Returns: A tuple of the first letter and the rest. 36 | #let extract-first-letter(body) = { 37 | // Handle string content. 38 | if type(body) == str { 39 | let letter = body.clusters().at(0, default: none) 40 | if letter == none { 41 | return (none, body) 42 | } 43 | let rest = body.clusters().slice(1).join() 44 | return (letter, rest) 45 | } 46 | 47 | // Handle text content. 48 | if body.has("text") { 49 | let (text, ..fields) = body.fields() 50 | if "label" in fields { fields.remove("label") } 51 | let label = if body.has("label") { body.label } 52 | let func(it) = if it != none { body.func()(..fields, it) } 53 | let (letter, rest) = extract-first-letter(body.text) 54 | return attach-label((letter, func(rest)), label) 55 | } 56 | 57 | // Handle content with "body" field. 58 | if body.func() in splittable { 59 | let (body: text, ..fields) = body.fields() 60 | if "label" in fields { fields.remove("label") } 61 | let label = if body.has("label") { body.label } 62 | let func(it) = if it != none { body.func()(..fields, it) } 63 | let (letter, rest) = extract-first-letter(text) 64 | return attach-label((letter, func(rest)), label) 65 | } 66 | 67 | // Handle styled content. 68 | if body.has("child") { 69 | let (child, styles, ..fields) = body.fields() 70 | if "label" in fields { fields.remove("label") } 71 | let label = if body.has("label") { body.label } 72 | let func(it) = if it != none { body.func()(it, styles) } 73 | let (letter, rest) = extract-first-letter(child) 74 | return attach-label((letter, func(rest)), label) 75 | } 76 | 77 | // Handle enumeration items (interpreted as text, e.g. "5. Body" or "+ Body") 78 | if body.func() == enum.item { 79 | let (body, ..fields) = body.fields() 80 | let number = fields.at("number", default: none) 81 | return if number == none { 82 | ("+", body) 83 | } else if number < 10 { 84 | (str(number), "." + body) 85 | } else { 86 | (str(number).first(), str(number).slice(1) + "." + body) 87 | } 88 | } 89 | 90 | // Handle list items (interpreted as text, e.g. "- Body") 91 | if body.func() == list.item { 92 | return ("-", body.body) 93 | } 94 | 95 | // Handle sequences. 96 | if body.has("children") { 97 | let child-pos = body.children.position(c => { 98 | c.func() not in (space, parbreak) 99 | }) 100 | 101 | if child-pos == none { 102 | // There is no non-empty child, so no letter. 103 | return (none, body) 104 | } 105 | 106 | let child = body.children.at(child-pos) 107 | let (letter, rest) = extract-first-letter(child) 108 | if body.children.len() > child-pos { 109 | rest = (rest, ..body.children.slice(child-pos+1)).join() 110 | } 111 | return (letter, rest) 112 | } 113 | 114 | // Handle unbreakable content. 115 | return (body, none) 116 | } 117 | 118 | // Extracts the dropped capital from the given content. 119 | // 120 | // The dropped capital contains the first real letter (or number) of the 121 | // content, but can be preceded by opening punctuation characters, and followed 122 | // by a sequence of closing punctuation characters. 123 | // 124 | // For example, the dropped capital of "Hello, world!" is "H", and the 125 | // dropped capital of "1. Hello, world!" is "1." including the dot. 126 | // 127 | // Returns: A tuple of the dropped capital and the rest. 128 | #let extract(body) = { 129 | let (letter, rest) = extract-first-letter(body) 130 | if letter == none { 131 | return (none, body) 132 | } 133 | 134 | // We can only append punctuation characters if the first letter can be 135 | // converted to a string, but not if it's e.g. a 'box' or 'image'. 136 | if to-string(letter) != none { 137 | // Append opening punctuation characters until the first "real" letter. 138 | while to-string(letter).last().match(regex-before) != none { 139 | let (next-letter, new-rest) = extract-first-letter(rest) 140 | if next-letter == none { break } 141 | letter += next-letter 142 | rest = new-rest 143 | } 144 | 145 | // Append closing punctuation characters. 146 | let (next-letter, new-rest) = extract-first-letter(rest) 147 | while next-letter != none and to-string(next-letter).match(regex-after) != none { 148 | letter += next-letter 149 | rest = new-rest 150 | (next-letter, new-rest) = extract-first-letter(rest) 151 | } 152 | } 153 | 154 | return (letter, rest) 155 | } 156 | -------------------------------------------------------------------------------- /droplet/src/lib.typ: -------------------------------------------------------------------------------- 1 | #import "droplet.typ": dropcap 2 | -------------------------------------------------------------------------------- /droplet/src/split.typ: -------------------------------------------------------------------------------- 1 | #import "util.typ": attach-label, space, splittable 2 | 3 | // Gets the number of breakpoints in the given content. 4 | // 5 | // A breakpoints must always be at a space. For example, the sequece 6 | // ([Hello], [ ], [my world!]) 7 | // has two breakpoints: 8 | // 1. ([Hello],) - ([my world!],) 9 | // 2. ([Hello my], [ ]) - ([world!],) 10 | // 11 | // Returns: The number of breakpoints. 12 | #let breakpoints(body) = { 13 | if type(body) == str { 14 | body.split(" ").len() - 1 15 | } else if body.has("text") { 16 | breakpoints(body.text) 17 | } else if body.has("child") { 18 | breakpoints(body.child) 19 | } else if body.has("children") { 20 | body.children.map(breakpoints).sum(default: 0) 21 | } else if body.func() in splittable { 22 | breakpoints(body.body) 23 | } else if body.func() in (space, linebreak, parbreak) { 24 | 1 25 | } else { 26 | 0 27 | } 28 | } 29 | 30 | // Splits the given content at a given breakpoint index. 31 | // 32 | // Content is split at spaces. A sequence can be split at any of its childrens' 33 | // breakpoints (spaces), but in general not between children. 34 | // 35 | // Returns: A tuple of the first and second part. 36 | #let split(body, index) = { 37 | // Shortcut for out-of-bounds indices. 38 | if index > breakpoints(body) { 39 | return (body, none) 40 | } 41 | 42 | if index < 0 { 43 | return split(body, calc.max(0, breakpoints(body) + index + 1)) 44 | } 45 | 46 | // Handle string content. 47 | if type(body) == str { 48 | let words = body.split(" ") 49 | let first = words.slice(0, index).join(" ") 50 | let second = words.slice(index).join(" ") 51 | return (first, second) 52 | } 53 | 54 | // Handle text content. 55 | if body.has("text") { 56 | let (text, ..fields) = body.fields() 57 | if "label" in fields { fields.remove("label") } 58 | let label = if body.has("label") { body.label } 59 | let func(it) = if it != none { body.func()(..fields, it) } 60 | let (first, second) = split(text, index) 61 | return attach-label((func(first), func(second)), label) 62 | } 63 | 64 | // Handle content with "body" field. 65 | if body.func() in splittable { 66 | let (body: text, ..fields) = body.fields() 67 | if "label" in fields { fields.remove("label") } 68 | let label = if body.has("label") { body.label } 69 | let func(it) = if it != none { body.func()(..fields, it) } 70 | let (first, second) = split(text, index) 71 | return attach-label((func(first), func(second)), label) 72 | } 73 | 74 | // Handle styled content. 75 | if body.has("child") { 76 | let (child, styles, ..fields) = body.fields() 77 | if "label" in fields { fields.remove("label") } 78 | let label = if body.has("label") { body.label } 79 | let func(it) = if it != none { body.func()(it, styles) } 80 | let (first, second) = split(child, index) 81 | return attach-label((func(first), func(second)), label) 82 | } 83 | 84 | // Handle sequences. 85 | if body.has("children") { 86 | let first = () 87 | let second = () 88 | 89 | // Find child containing the breakpoint and split it. 90 | let sub-index = index 91 | for (i, child) in body.children.enumerate() { 92 | let child-breakpoints = breakpoints(child) 93 | 94 | // Check if current child contains splitting point. 95 | if sub-index <= child-breakpoints { 96 | if child.func() not in (space, linebreak, parbreak) { 97 | // Push split child (skip trailing spaces) 98 | let (child-first, child-second) = split(child, sub-index) 99 | first.push(child-first) 100 | second.push(child-second) 101 | } 102 | 103 | second += body.children.slice(i + 1) 104 | break 105 | } 106 | 107 | sub-index -= child-breakpoints 108 | first.push(child) 109 | } 110 | 111 | return (first.join(), second.join()) 112 | } 113 | 114 | // Handle unbreakable content. 115 | return if index == 0 { (none, body) } else { (body, none) } 116 | } 117 | -------------------------------------------------------------------------------- /droplet/src/util.typ: -------------------------------------------------------------------------------- 1 | // Elements that can be split and have a 'body' field. 2 | #let splittable = (strong, emph, underline, stroke, overline, highlight) 3 | 4 | // Element function of spaces. 5 | #let space = [ ].func() 6 | 7 | // Converts the given content to a string. 8 | #let to-string(body) = { 9 | if type(body) == str { 10 | body 11 | } else if body.has("text") { 12 | to-string(body.text) 13 | } else if body.has("child") { 14 | to-string(body.child) 15 | } else if body.has("children") { 16 | body.children.map(to-string).join() 17 | } else if body.func() in splittable { 18 | to-string(body.body) 19 | } else if body.func() == smartquote { 20 | // Unfortunately, we can only use "dumb" quotes here. 21 | if body.double { "\"" } else { "'" } 22 | } else if body.func() == enum.item { 23 | if body.has("number") { 24 | str(body.number) + ". " + to-string(body.body) 25 | } else { 26 | "+ " + to-string(body.body) 27 | } 28 | } else if body.func() == space { 29 | " " 30 | } else if body.func() == super { 31 | let body-string = to-string(body.body) 32 | if body-string.match(regex("^[0-9+\-=\(\)ni\s]+$")) != none { 33 | body-string 34 | .replace("1", "¹") 35 | .replace("2", "²") 36 | .replace("3", "³") 37 | .replace(regex("[04-9]"), it => str.from-unicode(0x2070 + int(it.text))) 38 | .replace("+", "\u{207A}") 39 | .replace("-", "\u{207B}") 40 | .replace("=", "\u{207C}") 41 | .replace("(", "\u{207D}") 42 | .replace(")", "\u{207E}") 43 | .replace("n", "\u{207F}") 44 | .replace("i", "\u{2071}") 45 | } 46 | } else if body.func() == sub { 47 | let body-string = to-string(body.body) 48 | if body-string.match(regex("^[0-9+\-=\(\)aehk-pstx\s]+$")) != none { 49 | body-string 50 | .replace(regex("[0-9]"), it => str.from-unicode(0x2080 + int(it.text))) 51 | .replace("+", "\u{208A}") 52 | .replace("-", "\u{208B}") 53 | .replace("=", "\u{208C}") 54 | .replace("(", "\u{208D}") 55 | .replace(")", "\u{208E}") 56 | .replace("a", "\u{2090}") 57 | .replace("e", "\u{2091}") 58 | .replace("o", "\u{2092}") 59 | .replace("x", "\u{2093}") 60 | .replace("h", "\u{2095}") 61 | .replace("k", "\u{2096}") 62 | .replace("l", "\u{2097}") 63 | .replace("m", "\u{2098}") 64 | .replace("n", "\u{2099}") 65 | .replace("p", "\u{209A}") 66 | .replace("s", "\u{209B}") 67 | .replace("t", "\u{209C}") 68 | } 69 | } 70 | } 71 | 72 | // Attaches a label after the split elements. 73 | // 74 | // The label is only attached to one of the elements, preferring the second 75 | // one. If both elements are empty, the label is discarded. If the label is 76 | // empty, the elements remain unchanged. 77 | #let attach-label((first, second), label) = { 78 | if label == none { 79 | (first, second) 80 | } else if second != none { 81 | (first, [#second#label]) 82 | } else if first != none { 83 | ([#first#label], second) 84 | } else { 85 | (none, none) 86 | } 87 | } 88 | 89 | // Returns whether the element is displayed inline. 90 | // 91 | // Requires context. 92 | #let inline(element) = { 93 | measure(h(0.1pt) + element).width > measure(element).width 94 | } 95 | -------------------------------------------------------------------------------- /droplet/tests/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **/out/ 3 | **/diff/ 4 | -------------------------------------------------------------------------------- /droplet/tests/.ignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **.png 3 | **.svg 4 | **.pdf 5 | -------------------------------------------------------------------------------- /droplet/tests/basic/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/droplet/tests/basic/ref/1.png -------------------------------------------------------------------------------- /droplet/tests/basic/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | 5 | // Test basic use of dropcap. 6 | 7 | #dropcap(lorem(20)) 8 | -------------------------------------------------------------------------------- /droplet/tests/blocks/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/droplet/tests/blocks/ref/1.png -------------------------------------------------------------------------------- /droplet/tests/blocks/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | 5 | // Test block content within a dropcap. 6 | 7 | #dropcap(height: 1cm, gap: 4pt)[ 8 | Einstein said that mass and energy go like 9 | $ E = m c^2 $ 10 | and it was true (mostly). 11 | ] 12 | 13 | #dropcap(height: 1cm, gap: 3pt)[ 14 | There is a rectangle below 15 | 16 | #align(center, rect()) 17 | 18 | but it's still beside the first letter. 19 | ] 20 | -------------------------------------------------------------------------------- /droplet/tests/customize/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/droplet/tests/customize/ref/1.png -------------------------------------------------------------------------------- /droplet/tests/customize/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | #set par(justify: true) 5 | 6 | // Test arguments for customization. 7 | 8 | #dropcap(height: 3, lorem(14)) 9 | #dropcap(height: 1.1cm, lorem(14)) 10 | #dropcap(depth: 2, lorem(23)) 11 | #dropcap(height: 5mm, depth: 5mm, lorem(16)) 12 | #dropcap(overhang: 1em, lorem(7)) 13 | #dropcap(overhang: 100%, lorem(7)) 14 | #dropcap(overhang: -1em, gap: 1em, lorem(12)) 15 | #dropcap(height: 3, hanging-indent: 1em, lorem(13)) 16 | #dropcap( 17 | gap: 8pt, 18 | fill: white, 19 | font: "New Computer Modern", 20 | style: "italic", 21 | transform: letter => { 22 | h(4pt) + box(fill: blue, letter + h(10pt), outset: 3pt) 23 | }, 24 | lorem(11) 25 | ) 26 | -------------------------------------------------------------------------------- /droplet/tests/explicit/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/droplet/tests/explicit/ref/1.png -------------------------------------------------------------------------------- /droplet/tests/explicit/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | 5 | // Test explicitly passed first letter. 6 | 7 | #dropcap(square(size: 1em), gap: 1em)[A square is a square.] 8 | #dropcap(square(size: 1em), gap: 1em, height: 3, lorem(13)) 9 | #dropcap(square(), height: 14pt, gap: 1em, lorem(10)) 10 | #dropcap[\#1][The winner has won what was to win.] 11 | -------------------------------------------------------------------------------- /droplet/tests/extract/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/droplet/tests/extract/ref/1.png -------------------------------------------------------------------------------- /droplet/tests/extract/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | 5 | // Test that the first letter is extracted correctly. 6 | 7 | #dropcap[First letter] 8 | #dropcap[1. Wash your hands] 9 | #dropcap[“This is a quote,” said someone.] 10 | #dropcap[#super[1] In the beginning...] 11 | #dropcap[H#sub[2] is hydrogen.] 12 | #dropcap[#box[This] is a boxed word.] 13 | -------------------------------------------------------------------------------- /droplet/tests/justify/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/droplet/tests/justify/ref/1.png -------------------------------------------------------------------------------- /droplet/tests/justify/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | 5 | // Test different justify values. 6 | 7 | #dropcap(justify: true, lorem(20)) 8 | 9 | #set par(justify: true) 10 | 11 | #dropcap(justify: auto, lorem(20)) 12 | #dropcap(justify: false, lorem(20)) 13 | -------------------------------------------------------------------------------- /droplet/tests/split/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/droplet/tests/split/ref/1.png -------------------------------------------------------------------------------- /droplet/tests/split/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 4.6cm, height: auto, margin: 1em) 4 | 5 | // Test correct splitting of text. 6 | 7 | #dropcap[ 8 | This test verifies that the package doesn't split words at apostrophes. 9 | ] 10 | 11 | #dropcap(justify: true, gap: 2pt)[ 12 | This test verifies that the package doesn't split words at apostrophes. 13 | ] 14 | 15 | #dropcap(justify: true)[ 16 | Here are two rectangles #box(width: 1em, height: 3em, baseline: 2.34em, fill: red) #box(width: 1em, height: 4em, baseline: 3.34em, fill: red) in red and there is text beside. 17 | ] 18 | 19 | #dropcap(height: 1.1em,)[ 20 | This is an equation $(display(integral F = 0))$ test, which tests stuff. 21 | ] 22 | -------------------------------------------------------------------------------- /droplet/tests/template.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": dropcap 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | -------------------------------------------------------------------------------- /droplet/typst.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "droplet" 3 | version = "0.2.0" 4 | entrypoint = "src/lib.typ" 5 | compiler = "0.11.0" 6 | authors = ["Eric Biedert"] 7 | repository = "https://github.com/EpicEricEE/typst-plugins" 8 | license = "MIT" 9 | description = "Customizable dropped capitals." 10 | exclude = ["README.md", "assets", "tests"] 11 | categories = ["text"] 12 | keywords = [ 13 | "drop", "dropped", "dropcap", "big", "large", "caps", "capital", "capitals", 14 | "initial", "initials", "letter", "letters", "lettrine" 15 | ] 16 | -------------------------------------------------------------------------------- /equate/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Eric Biedert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 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 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /equate/README.md: -------------------------------------------------------------------------------- 1 | # equate 2 | A package for improved layout of equations and mathematical expressions. 3 | 4 | > [!WARNING] 5 | > This repository has been archived. The package has been moved to the [EpicEricEE/typst-equate](https://github.com/EpicEricEE/typst-equate) repository. 6 | 7 | When applied, this package will split up multi-line block equations into multiple elements, so that each line can be assigned a separate number. By default, the equation counter is incremented for each line, but this behavior can be changed by setting the `sub-numbering` argument to `true`. In this case, the equation counter will only be incremented once for the entire block, and each line will be assigned a sub-number like `1a`, `2.1`, or similar, depending on the set equation numbering. You can also set the `number-mode` argument to `"label"` to only number labelled lines. If a label is only applied to the full equation, all lines will be numbered. 8 | 9 | This splitting also makes it possible to spread equations over page boundaries while keeping alignment in place, which can be useful for long derivations or proofs. This can be configured by the `breakable` parameter of the `equate` function, or by setting the `breakable` parameter of `block` for equations via a show-set rule. Additionally, the alignment of the equation number is improved, so that it always matches the baseline of the equation. 10 | 11 | If you want to create a "standard" equation with a single equation number centered across all lines, you can attach the `` label to the equation. This will disable the effect of this package for the current equation. 12 | 13 | ## Usage 14 | The package comes with a single `equate` function that is supposed to be used as a template. It takes two optional arguments for customization: 15 | 16 | | Argument | Type | Description | Default | 17 | | --------------- | ------------------- | ---------------------------------------------------------- | -------- | 18 | | `breakable` | `boolean`, `auto` | Whether to allow the equation to break across pages. | `auto` | 19 | | `sub-numbering` | `boolean` | Whether to assign sub-numbers to each line of an equation. | `false` | 20 | | `number-mode` | `"line"`, `"label"` | Whether to number all lines or only those with a label. | `"line"` | 21 | 22 | To reference a specific line of an equation, include the label at the end of the line, like in the following example: 23 | 24 | ```typ 25 | #import "@preview/equate:0.2.0": equate 26 | 27 | #show: equate.with(breakable: true, sub-numbering: true) 28 | #set math.equation(numbering: "(1.1)") 29 | 30 | The dot product of two vectors $arrow(a)$ and $arrow(b)$ can 31 | be calculated as shown in @dot-product. 32 | 33 | $ 34 | angle.l a, b angle.r &= arrow(a) dot arrow(b) \ 35 | &= a_1 b_1 + a_2 b_2 + ... a_n b_n \ 36 | &= sum_(i=1)^n a_i b_i. # 37 | $ 38 | 39 | The sum notation in @sum is a useful way to express the dot 40 | product of two vectors. 41 | ``` 42 | 43 | ![Result of example code (page 1).](assets/example-1.svg) 44 | ![Result of example code (page 2).](assets/example-2.svg) 45 | 46 | ### Local Usage 47 | If you only want to use the package features on selected equations, you can also apply the `equate` function directly to the equation. This will override the default behavior for the current equation only. Note, that this will require you to use the `equate` function as a show rule for references, as shown in the following example: 48 | 49 | ```typ 50 | #import "@preview/equate:0.2.0": equate 51 | 52 | // Allow references to a line of the equation. 53 | #show ref: equate 54 | 55 | #set math.equation(numbering: "(1.1)", supplement: "Eq.") 56 | 57 | #equate($ 58 | E &= m c^2 # \ 59 | &= sqrt(p^2 c^2 + m^2 c^4) # 60 | $) 61 | 62 | While @short is the famous equation by Einstein, @long is a 63 | more general form of the energy-momentum relation. 64 | ``` 65 | 66 | ![Result of example code.](assets/example-local.svg) 67 | 68 | As an alternative to the show rule, it is also possible to manually wrap each reference in an `equate` function, though this is less convenient and more prone to mistakes. 69 | -------------------------------------------------------------------------------- /equate/assets/example-local.typ: -------------------------------------------------------------------------------- 1 | #import "../src/lib.typ": equate 2 | 3 | #set text(size: 14pt) 4 | #set par(justify: true) 5 | #set page( 6 | width: 11cm, 7 | height: auto, 8 | margin: 1em, 9 | background: pad(0.5pt, box( 10 | width: 100%, 11 | height: 100%, 12 | radius: 4pt, 13 | fill: white, 14 | stroke: white.darken(10%), 15 | )), 16 | ) 17 | 18 | // Allow references to a line of the equation. 19 | #show ref: equate 20 | 21 | #set math.equation(numbering: "(1.1)", supplement: "Eq.") 22 | 23 | #equate($ 24 | E &= m c^2 # \ 25 | &= sqrt(p^2 c^2 + m^2 c^4) # 26 | $) 27 | 28 | While @short is the famous equation by Einstein, @long is a 29 | more general form of the energy-momentum relation. 30 | -------------------------------------------------------------------------------- /equate/assets/example.typ: -------------------------------------------------------------------------------- 1 | #import "../src/lib.typ": equate 2 | 3 | #set text(size: 14pt) 4 | #set par(justify: true) 5 | #set page( 6 | width: 11cm, 7 | height: 4cm, 8 | margin: 1em, 9 | background: pad(0.5pt, box( 10 | width: 100%, 11 | height: 100%, 12 | radius: 4pt, 13 | fill: white, 14 | stroke: white.darken(10%), 15 | )), 16 | ) 17 | 18 | #show: equate.with(breakable: true, sub-numbering: true) 19 | #set math.equation(numbering: "(1.1)") 20 | 21 | The dot product of two vectors $arrow(a)$ and $arrow(b)$ can be 22 | calculated as shown in @dot-product. 23 | 24 | $ 25 | angle.l a, b angle.r &= arrow(a) dot arrow(b) \ 26 | &= a_1 b_1 + a_2 b_2 + ... a_n b_n \ 27 | &= sum_(i=1)^n a_i b_i. # 28 | $ 29 | 30 | The sum notation in @sum is a useful way to express the dot 31 | product of two vectors. 32 | -------------------------------------------------------------------------------- /equate/src/equate.typ: -------------------------------------------------------------------------------- 1 | // Element function for alignment points. 2 | #let align-point = $&$.body.func() 3 | 4 | // Element function for sequences. 5 | #let sequence = $a b$.body.func() 6 | 7 | // Element function for a counter update. 8 | #let counter-update = counter(math.equation).update(1).func() 9 | 10 | // Sub-numbering state. 11 | #let state = state("equate/sub-numbering", false) 12 | 13 | // Show rule necessary for referencing equation lines, as the number is not 14 | // stored in a counter, but as metadata in a figure. 15 | #let equate-ref(it) = { 16 | if it.element == none { return it } 17 | if it.element.func() != figure { return it } 18 | if it.element.kind != math.equation { return it } 19 | if it.element.body == none { return it } 20 | if it.element.body.func() != metadata { return it } 21 | 22 | // Display correct number, depending on whether sub-numbering was enabled. 23 | let nums = if state.at(it.element.location()) { 24 | it.element.body.value 25 | } else { 26 | // (3, 1): 3 + 1 - 1 = 3 27 | // (3, 2): 3 + 2 - 1 = 4 28 | (it.element.body.value.first() + it.element.body.value.slice(1).sum(default: 1) - 1,) 29 | } 30 | 31 | assert( 32 | it.element.numbering != none, 33 | message: "cannot reference equation without numbering." 34 | ) 35 | 36 | let num = numbering( 37 | if type(it.element.numbering) == str { 38 | // Trim numbering pattern of prefix and suffix characters. 39 | let counting-symbols = ("1", "a", "A", "i", "I", "一", "壹", "あ", "い", "ア", "イ", "א", "가", "ㄱ", "*") 40 | let prefix-end = it.element.numbering.codepoints().position(c => c in counting-symbols) 41 | let suffix-start = it.element.numbering.codepoints().rev().position(c => c in counting-symbols) 42 | it.element.numbering.slice(prefix-end, if suffix-start == 0 { none } else { -suffix-start }) 43 | } else { 44 | it.element.numbering 45 | }, 46 | ..nums 47 | ) 48 | 49 | let supplement = if it.supplement == auto { 50 | it.element.supplement 51 | } else if type(it.supplement) == function { 52 | (it.supplement)(it.element) 53 | } else { 54 | it.supplement 55 | } 56 | 57 | link(it.element.location(), if supplement not in ([], none) [#supplement~#num] else [#num]) 58 | } 59 | 60 | // Extract lines and trim spaces. 61 | #let to-lines(equation) = { 62 | let lines = if equation.body.func() == sequence { 63 | equation.body.children.split(linebreak()) 64 | } else { 65 | ((equation.body,),) 66 | } 67 | 68 | // Trim spaces at begin and end of line. 69 | let lines = lines.filter(line => line != ()).map(line => { 70 | if line.first() == [ ] and line.last() == [ ] { 71 | line.slice(1, -1) 72 | } else if line.first() == [ ] { 73 | line.slice(1) 74 | } else if line.last() == [ ] { 75 | line.slice(0, -1) 76 | } else { 77 | line 78 | } 79 | }) 80 | 81 | lines 82 | } 83 | 84 | // Layout a single equation line with the given number. 85 | #let layout-line( 86 | number: none, 87 | number-align: none, 88 | number-width: auto, 89 | text-dir: auto, 90 | line 91 | ) = context { 92 | let equation(body) = [ 93 | #math.equation( 94 | block: true, 95 | numbering: _ => none, 96 | body 97 | ) 98 | ] 99 | 100 | // Short circuit if no number has to be added. 101 | if number == none { 102 | return equation(line.join()) 103 | } 104 | 105 | // Short circuit if number is a counter update. 106 | if type(number) == content and number.func() == counter-update { 107 | return { 108 | number 109 | equation(line.join()) 110 | } 111 | } 112 | 113 | // Start of equation block. 114 | let x-start = here().position().x 115 | 116 | // Resolve number width. 117 | let number-width = if number-width == auto { 118 | measure(number).width 119 | } else { 120 | number-width 121 | } 122 | 123 | // Resolve equation alignment in x-direction. 124 | let equation-align = if align.alignment.x in (left, center, right) { 125 | align.alignment.x 126 | } else if text-dir == ltr { 127 | if align.alignment.x == start { left } else { right } 128 | } else if text-dir == rtl { 129 | if align.alignment.x == start { right } else { left } 130 | } 131 | 132 | // Add numbers to the equation body, so that they are aligned at their 133 | // respective baselines. If the equation is centered, the number is put 134 | // on both sides of the equation to keep the center alignment. 135 | 136 | let num = box(width: number-width, align(number-align, number)) 137 | let line-width = measure(equation(line.join())).width 138 | let gap = 0.5em 139 | 140 | layout(bounds => { 141 | let space = if equation-align == center { 142 | bounds.width - line-width - 2 * number-width 143 | } else { 144 | bounds.width - line-width - number-width 145 | } 146 | 147 | let body = if number-align.x == left { 148 | if equation-align == center { 149 | h(-gap) + num + h(space / 2 + gap) + line.join() + h(space / 2) + hide(num) 150 | } else if equation-align == right { 151 | num + h(space + 2 * gap) + line.join() 152 | } else { 153 | h(-gap) + num + h(gap) + line.join() + h(space + gap) 154 | } 155 | } else { 156 | if equation-align == center { 157 | hide(num) + h(space / 2) + line.join() + h(space / 2 + gap) + num + h(-gap) 158 | } else if equation-align == right { 159 | h(space + gap) + line.join() + h(gap) + num + h(-gap) 160 | } else { 161 | line.join() + h(space + 2 * gap) + num 162 | } 163 | } 164 | 165 | equation(body) 166 | }) 167 | } 168 | 169 | // Replace "fake labels" with a hidden figure that is labelled 170 | // accordingly. 171 | #let replace-labels( 172 | lines, 173 | number-mode, 174 | numbering, 175 | supplement, 176 | has-label 177 | ) = { 178 | // Main equation number. 179 | let main-number = counter(math.equation).get() 180 | 181 | // Indices of lines that contain a label. 182 | let labelled = lines 183 | .enumerate() 184 | .filter(((i, line)) => { 185 | if line.len() == 0 { return false } 186 | if line.last().func() != raw { return false } 187 | if line.last().lang != "typc" { return false } 188 | if line.last().text.match(regex("^<.+>$")) == none { return false } 189 | return true 190 | }) 191 | .map(((i, _)) => i) 192 | 193 | // Indices of lines that are marked not to be numbered. 194 | let revoked = lines 195 | .enumerate() 196 | .filter(((i, line)) => { 197 | if i not in labelled { return false } 198 | return line.last().text == "" 199 | }) 200 | .map(((i, _)) => i) 201 | 202 | // The "revoke" label shall not count as a labelled line. 203 | labelled = labelled.filter(i => i not in revoked) 204 | 205 | // Indices of numbered lines in this equation. 206 | let numbered = if number-mode == "line" { 207 | range(lines.len()).filter(i => i not in revoked) 208 | } else if labelled.len() == 0 and has-label { 209 | // Only outer label, so number all lines. 210 | range(lines.len()).filter(i => i not in revoked) 211 | } else { 212 | labelled 213 | } 214 | 215 | ( 216 | numbered, 217 | lines.enumerate() 218 | .map(((i, line)) => { 219 | if i in revoked { 220 | // Remove "revoke" label and space and return line. 221 | line.remove(-1) 222 | if line.at(-2, default: none) == [ ] { line.remove(-2) } 223 | return line 224 | } 225 | 226 | if i not in labelled { return line } 227 | 228 | // Remove trailing spacing (before label). 229 | if line.at(-2, default: none) == [ ] { line.remove(-2) } 230 | 231 | // Append sub-numbering only if there are multiple numbered lines. 232 | let nums = main-number + if numbered.len() > 1 { 233 | (numbered.position(n => n == i) + 1,) 234 | } 235 | 236 | // We use a figure with kind "equation" to make the sub-equation 237 | // referenceable with the correct supplement. The numbering is stored 238 | // in the figure body as metadata, as a counter would only show a 239 | // single number. 240 | line.at(-1) = [#figure( 241 | metadata(nums), 242 | kind: math.equation, 243 | numbering: numbering, 244 | supplement: supplement 245 | )#label(line.last().text.slice(1, -1))] 246 | 247 | return line 248 | }) 249 | ) 250 | } 251 | 252 | // Splitting an equation into multiple lines breaks the inbuilt alignment 253 | // with alignment points, so it is emulated here by adding spacers manually. 254 | #let realign(lines) = { 255 | // Utility shorthand for unnumbered block equation. 256 | let equation(body) = [ 257 | #math.equation( 258 | block: true, 259 | numbering: none, 260 | body 261 | ) 262 | ] 263 | 264 | // Short-circuit if no alignment points. 265 | if lines.all(line => align-point() not in line) { 266 | return lines 267 | } 268 | 269 | // Store widths of each part between alignment points. 270 | let part-widths = lines.map(line => { 271 | line.split(align-point()) 272 | .map(part => measure(equation(part.join())).width) 273 | }) 274 | 275 | // Get maximum width of each part. 276 | let part-widths = for i in range(calc.max(..part-widths.map(points => points.len()))) { 277 | (calc.max(..part-widths.map(line => line.at(i, default: 0pt))), ) 278 | } 279 | 280 | // Get maximum width of each slice of parts. 281 | let max-slice-widths = array.zip(..lines.map(line => range(part-widths.len()).map(i => { 282 | let parts = line.split(align-point()).map(array.join) 283 | if i >= parts.len() { 284 | 0pt 285 | } else { 286 | let slice = parts.slice(0, i + 1).join() 287 | measure(equation(slice)).width 288 | } 289 | }))).map(widths => calc.max(..widths)) 290 | 291 | // Add spacers for each part, so that the part widths are the same for all lines. 292 | let lines = lines.map(line => { 293 | line.split(align-point()) 294 | .enumerate() 295 | .map(((i, part)) => { 296 | // Add spacer to make part the correct width. 297 | let width-diff = part-widths.at(i) - measure(equation(part.join())).width 298 | let spacing = if width-diff > 0pt { h(0pt) + box(fill: yellow, width: width-diff) + h(0pt) } 299 | 300 | if calc.even(i) { 301 | spacing + part.join() // Right align. 302 | } else { 303 | part.join() + spacing // Left align. 304 | } 305 | }) 306 | .intersperse(align-point()) 307 | }) 308 | 309 | // Update maximum slice widths to include spacers. 310 | let max-slice-widths = array.zip(..lines.map(line => range(part-widths.len()).map(i => { 311 | let parts = line.split(align-point()).map(array.join) 312 | if i >= parts.len() { 313 | 0pt 314 | } else { 315 | let slice = parts.slice(0, i + 1).join() 316 | calc.max(max-slice-widths.at(i), measure(equation(slice)).width) 317 | } 318 | }))).map(widths => calc.max(..widths)) 319 | 320 | // Add spacers between parts to ensure correct spacing with combined parts. 321 | lines = for line in lines { 322 | let parts = line.split(align-point()).map(array.join) 323 | for i in range(max-slice-widths.len()) { 324 | if i >= parts.len() { 325 | break 326 | } 327 | let slice = parts.slice(0, i).join() + h(0pt) + parts.at(i) 328 | let slice-width = measure(equation(slice)).width 329 | if slice-width < max-slice-widths.at(i) { 330 | parts.at(i) = h(0pt) + box(fill: green, width: max-slice-widths.at(i) - slice-width) + h(0pt) + parts.at(i) 331 | } 332 | } 333 | (parts,) 334 | } 335 | 336 | // Append remaining spacers at the end for lines that have less align points. 337 | let line-widths = lines.map(line => measure(equation(line.join())).width) 338 | let max-line-width = calc.max(..line-widths) 339 | lines = lines.zip(line-widths).map(((line, line-width)) => { 340 | if line-width < max-line-width { 341 | line.push(h(0pt) + box(fill: red, width: max-line-width - line-width)) 342 | } 343 | line 344 | }) 345 | 346 | lines 347 | } 348 | 349 | // Applies show rules to the given body, so that block equations can span over 350 | // page boundaries while retaining alignment. The equation number is stepped 351 | // and displayed at every line, optionally with sub-numbering. 352 | // 353 | // Parameters: 354 | // - breakable: Whether to allow page breaks within the equation. 355 | // - sub-numbering: Whether to add sub-numbering to the equation. 356 | // - number-mode: Whether to number all lines or only lines containing a label. 357 | // Must be either "line" or "label". 358 | // - debug: Whether to show alignment spacers for debugging. 359 | // 360 | // Returns: The body with the applied show rules. 361 | #let equate( 362 | breakable: auto, 363 | sub-numbering: false, 364 | number-mode: "line", 365 | debug: false, 366 | body 367 | ) = { 368 | // Validate parameters. 369 | assert( 370 | breakable == auto or type(breakable) == bool, 371 | message: "expected boolean or auto for breakable, found " + repr(breakable) 372 | ) 373 | assert( 374 | type(sub-numbering) == bool, 375 | message: "expected boolean for sub-numbering, found " + repr(sub-numbering) 376 | ) 377 | assert( 378 | number-mode in ("line", "label"), 379 | message: "expected \"line\" or \"label\" for number-mode, found " + repr(number-mode) 380 | ) 381 | assert( 382 | type(debug) == bool, 383 | message: "expected boolean for debug, found " + repr(debug) 384 | ) 385 | 386 | // This function was applied to a reference or label, so apply the reference 387 | // rule instead of the equation rule. 388 | if type(body) == label { 389 | return { 390 | show ref: equate-ref 391 | ref(body) 392 | } 393 | } else if body.func() == ref { 394 | return { 395 | show ref: equate-ref 396 | body 397 | } 398 | } 399 | 400 | show math.equation.where(block: true): set block(breakable: breakable) if type(breakable) == bool 401 | show math.equation.where(block: true): it => { 402 | // Allow a way to make default equations. 403 | if it.has("label") and it.label == { 404 | return it 405 | } 406 | 407 | // Make spacers visible in debug mode. 408 | show box.where(body: none): it => { 409 | if debug { it } else { hide(it) } 410 | } 411 | show box.where(body: none): set box( 412 | height: 0.4em, 413 | stroke: 0.4pt, 414 | ) if debug 415 | 416 | // Prevent show rules on figures from messing with replaced labels. 417 | show figure.where(kind: math.equation): it => { 418 | if it.body == none { return it } 419 | if it.body.func() != metadata { return it } 420 | none 421 | } 422 | 423 | // Main equation number. 424 | let main-number = counter(math.equation).get().first() 425 | 426 | // Resolve text direction. 427 | let text-dir = if text.dir == auto { 428 | if text.lang in ( 429 | "ar", "dv", "fa", "he", "ks", "pa", 430 | "ps", "sd", "ug", "ur", "yi", 431 | ) { rtl } else { ltr } 432 | } else { 433 | text.dir 434 | } 435 | 436 | // Resolve number position in x-direction. 437 | let number-align = if it.number-align.x in (left, right) { 438 | it.number-align.x 439 | } else if text-dir == ltr { 440 | if it.number-align.x == start { left } else { right } 441 | } else if text-dir == rtl { 442 | if it.number-align.x == start { right } else { left } 443 | } 444 | 445 | let (numbered, lines) = replace-labels( 446 | to-lines(it), 447 | number-mode, 448 | it.numbering, 449 | it.supplement, 450 | it.has("label") 451 | ) 452 | 453 | // Short-circuit for single-line equations. 454 | if lines.len() == 1 { 455 | if it.numbering == none { return it } 456 | if numbering(it.numbering, 1) == none { return it } 457 | 458 | let number = if numbered.len() > 0 { 459 | numbering(it.numbering, main-number) 460 | } else { 461 | // Step back counter as this equation should not be counted. 462 | counter(math.equation).update(n => n - 1) 463 | } 464 | 465 | return { 466 | // Update state to allow correct referencing. 467 | state.update(_ => sub-numbering) 468 | 469 | layout-line( 470 | lines.first(), 471 | number: number, 472 | number-align: number-align, 473 | text-dir: text-dir 474 | ) 475 | 476 | // Step back counter as we introducted an additional equation 477 | // that increased the counter by one. 478 | counter(math.equation).update(n => n - 1) 479 | } 480 | } 481 | 482 | // Calculate maximum width of all numberings in this equation. 483 | let max-number-width = if it.numbering == none { 0pt } else { 484 | calc.max(0pt, ..range(numbered.len()).map(i => { 485 | let nums = if sub-numbering and numbered.len() > 1 { 486 | (main-number, i + 1)} 487 | else { 488 | (main-number + i,) 489 | } 490 | measure(numbering(it.numbering, ..nums)).width 491 | })) 492 | } 493 | 494 | // Update state to allow correct referencing. 495 | state.update(_ => sub-numbering) 496 | 497 | // Layout equation as grid to allow page breaks. 498 | block(grid( 499 | columns: 1, 500 | row-gutter: par.leading, 501 | ..realign(lines).enumerate().map(((i, line)) => { 502 | let sub-number = numbered.position(n => n == i) 503 | let number = if it.numbering == none { 504 | none 505 | } else if sub-number == none { 506 | // Step back counter as this equation should not be counted. 507 | counter(math.equation).update(n => n - 1) 508 | } else if sub-numbering and numbered.len() > 1 { 509 | numbering(it.numbering, main-number, sub-number + 1) 510 | } else { 511 | numbering(it.numbering, main-number + sub-number) 512 | } 513 | 514 | layout-line( 515 | line, 516 | number: number, 517 | number-align: number-align, 518 | number-width: max-number-width, 519 | text-dir: text-dir 520 | ) 521 | }) 522 | )) 523 | 524 | // Revert equation counter step(s). 525 | if it.numbering == none { 526 | // We converted a non-numbered equation into multiple empty- 527 | // numbered ones and thus increased the counter at every line. 528 | counter(math.equation).update(n => n - lines.len()) 529 | } else { 530 | // Each line stepped the equation counter, but it should only 531 | // have been stepped once (when using sub-numbering). We also 532 | // always introduced an additional numbered equation that 533 | // stepped the counter. 534 | counter(math.equation).update(n => { 535 | n - if sub-numbering and numbered.len() > 1 { numbered.len() } else { 1 } 536 | }) 537 | } 538 | } 539 | 540 | // Add show rule for referencing equation lines. 541 | show ref: equate-ref 542 | 543 | body 544 | } 545 | -------------------------------------------------------------------------------- /equate/src/lib.typ: -------------------------------------------------------------------------------- 1 | #import "equate.typ": equate 2 | -------------------------------------------------------------------------------- /equate/tests/.gitignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **/out/ 3 | **/diff/ 4 | -------------------------------------------------------------------------------- /equate/tests/.ignore: -------------------------------------------------------------------------------- 1 | # added by typst-test 2 | **.png 3 | **.svg 4 | **.pdf 5 | -------------------------------------------------------------------------------- /equate/tests/align-points/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/align-points/ref/1.png -------------------------------------------------------------------------------- /equate/tests/align-points/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": equate 2 | 3 | #set page(width: 8cm, height: auto, margin: 1em) 4 | #show: equate 5 | 6 | // Test re-implemented alignment algorithm. 7 | 8 | $ a + b &= c \ 9 | &= d + e $ 10 | 11 | $ a + b &= c + d &= e + f \ 12 | g &= & + h $ 13 | 14 | $ a + b &= c + d &&= e + f \ 15 | g & &&= h $ 16 | 17 | $ a + b &= c \ 18 | d & &= e + f $ 19 | 20 | $ "text" & "text" \ 21 | & "text" $ 22 | 23 | // Cases below taken from Typst test suite. 24 | 25 | $ "a" &= c \ 26 | &= c + 1 & "By definition" \ 27 | &= d + 100 + 1000 \ 28 | &= x & & "Even longer" \ 29 | $ 30 | 31 | $ & "right" \ 32 | "a very long line" \ 33 | "left" $ 34 | 35 | $ "right" \ 36 | "a very long line" \ 37 | "left" \ $ 38 | 39 | $ a &= b & quad c &= d \ 40 | e &= f & g &= h $ 41 | -------------------------------------------------------------------------------- /equate/tests/alignment/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/alignment/ref/1.png -------------------------------------------------------------------------------- /equate/tests/alignment/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": equate 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | #show: equate 5 | 6 | // Test correct number position when using `set align`. 7 | 8 | #set math.equation(numbering: "(1)") 9 | 10 | #for number-align in (left, right) { 11 | set math.equation(number-align: number-align) 12 | 13 | $ a + b $ 14 | show math.equation: set align(start) 15 | $ a + b $ 16 | show math.equation: set align(end) 17 | $ a + b $ 18 | } 19 | 20 | // Test alignment points together with `set align`. 21 | 22 | #show math.equation: set align(start) 23 | 24 | $ a + b &= c &+ d = e \ 25 | f &= g & = h $ 26 | -------------------------------------------------------------------------------- /equate/tests/boxed/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/boxed/ref/1.png -------------------------------------------------------------------------------- /equate/tests/boxed/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": equate 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | #show: equate.with(breakable: true) 5 | 6 | // Test equation sizing when given constraints. 7 | 8 | // Unnumbered 9 | #block(width: 50%, fill: yellow, $ a + b $) 10 | #block(width: 50%, fill: yellow, $ c + d \ e + f $) 11 | 12 | #h(1cm) #box(width: 40%, fill: yellow, $ g + h $) 13 | 14 | #h(1cm) #box(fill: yellow, $ g + h $) 15 | 16 | // Numbered 17 | #set math.equation(numbering: "(1)") 18 | 19 | #block(width: 50%, fill: yellow, $ a + b $) 20 | #block(width: 50%, fill: yellow, $ c + d \ e + f $) 21 | 22 | #h(1cm) #box(width: 40%, fill: yellow, $ g + h $) 23 | 24 | #h(1cm) #box(fill: yellow, $ g + h $) 25 | 26 | // Columns 27 | #block(height: 2cm, columns(2)[ 28 | $ a + b \ 29 | c + d \ 30 | e + f \ 31 | g + h \ 32 | i + j $ 33 | ]) 34 | -------------------------------------------------------------------------------- /equate/tests/break/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/1.png -------------------------------------------------------------------------------- /equate/tests/break/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/2.png -------------------------------------------------------------------------------- /equate/tests/break/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/3.png -------------------------------------------------------------------------------- /equate/tests/break/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/4.png -------------------------------------------------------------------------------- /equate/tests/break/ref/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/5.png -------------------------------------------------------------------------------- /equate/tests/break/ref/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/6.png -------------------------------------------------------------------------------- /equate/tests/break/ref/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/7.png -------------------------------------------------------------------------------- /equate/tests/break/ref/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/break/ref/8.png -------------------------------------------------------------------------------- /equate/tests/break/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": equate 2 | 3 | #set page(width: 6cm, height: 2cm, margin: 1em) 4 | #show: equate 5 | 6 | // Test equations breaking across page boundaries. 7 | 8 | #show math.equation: set block(breakable: true) 9 | 10 | $ a + b \ 11 | c - d \ 12 | e + f \ 13 | g = h $ 14 | 15 | $ a &= b \ 16 | &= d \ 17 | &= f \ 18 | g &= h $ 19 | 20 | // Test breakable parameter. 21 | 22 | #equate(breakable: false, $ 23 | a + b \ 24 | c - d \ 25 | e + f \ 26 | g = h 27 | $) 28 | 29 | #show math.equation: set block(breakable: false) 30 | 31 | $ a + b \ 32 | c - d \ 33 | e + f \ 34 | g = h $ 35 | 36 | 37 | #equate(breakable: true, $ 38 | a + b \ 39 | c - d \ 40 | e + f \ 41 | g = h 42 | $) 43 | -------------------------------------------------------------------------------- /equate/tests/local/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/local/ref/1.png -------------------------------------------------------------------------------- /equate/tests/local/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": equate 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | #set math.equation(numbering: "(1)") 5 | 6 | $ a + b \ 7 | c + d $ 8 | 9 | #equate($ 10 | d + e \ 11 | f + g # 12 | $) 13 | 14 | @lbl (wrong) 15 | 16 | #equate() (correct) 17 | 18 | #[ 19 | #show ref: equate 20 | @lbl (correct) 21 | ] 22 | 23 | #[ 24 | #show: equate 25 | @lbl (correct) 26 | ] -------------------------------------------------------------------------------- /equate/tests/margin/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/margin/ref/1.png -------------------------------------------------------------------------------- /equate/tests/margin/ref/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/margin/ref/2.png -------------------------------------------------------------------------------- /equate/tests/margin/ref/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/margin/ref/3.png -------------------------------------------------------------------------------- /equate/tests/margin/ref/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/margin/ref/4.png -------------------------------------------------------------------------------- /equate/tests/margin/ref/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/margin/ref/5.png -------------------------------------------------------------------------------- /equate/tests/margin/ref/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/margin/ref/6.png -------------------------------------------------------------------------------- /equate/tests/margin/ref/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/margin/ref/7.png -------------------------------------------------------------------------------- /equate/tests/margin/test.typ: -------------------------------------------------------------------------------- 1 | #import "/src/lib.typ": equate 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | #show: equate.with(breakable: true) 5 | 6 | // Test number positioning with different page margins. 7 | 8 | #set math.equation(numbering: "(1)") 9 | 10 | #for side in ("left", "right", "x", "inside", "outside") { 11 | page(margin: ((side): 2cm))[ 12 | $ a + b $ 13 | 14 | #set math.equation(number-align: start) 15 | $ a + b $ 16 | 17 | #set text(dir: rtl) 18 | $ a + b $ 19 | ] 20 | } 21 | 22 | // Test break over pages with different margins. 23 | 24 | #set page(margin: (inside: 2cm), height: 2cm) 25 | 26 | $ a + b \ 27 | c + d \ 28 | e + f \ 29 | g + h $ 30 | -------------------------------------------------------------------------------- /equate/tests/number-mode/ref/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpicEricEE/typst-plugins/6b823f5212fe07bb09f71b5eb9cab02d528a5c2a/equate/tests/number-mode/ref/1.png -------------------------------------------------------------------------------- /equate/tests/number-mode/test.typ: -------------------------------------------------------------------------------- 1 | #import "../../src/lib.typ": equate 2 | 3 | #set page(width: 6cm, height: auto, margin: 1em) 4 | #show: equate.with(number-mode: "label") 5 | 6 | // Test correct counter incrementation with number-mode "label". 7 | 8 | #set math.equation(numbering: "(1.1)") 9 | 10 | $ a + b #