From be7cfc85d08c545abfac08098b7b33b4bd71f37e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 27 Oct 2024 19:04:55 +0100 Subject: Split out four new crates (#5302) --- crates/typst-library/Cargo.toml | 72 + crates/typst-library/src/diag.rs | 580 ++++++ crates/typst-library/src/engine.rs | 387 ++++ crates/typst-library/src/foundations/args.rs | 453 +++++ crates/typst-library/src/foundations/array.rs | 1133 +++++++++++ crates/typst-library/src/foundations/auto.rs | 267 +++ crates/typst-library/src/foundations/bool.rs | 26 + crates/typst-library/src/foundations/bytes.rs | 262 +++ crates/typst-library/src/foundations/calc.rs | 1211 ++++++++++++ crates/typst-library/src/foundations/cast.rs | 499 +++++ crates/typst-library/src/foundations/content.rs | 1007 ++++++++++ crates/typst-library/src/foundations/context.rs | 85 + crates/typst-library/src/foundations/datetime.rs | 580 ++++++ crates/typst-library/src/foundations/decimal.rs | 499 +++++ crates/typst-library/src/foundations/dict.rs | 395 ++++ crates/typst-library/src/foundations/duration.rs | 215 +++ crates/typst-library/src/foundations/element.rs | 333 ++++ crates/typst-library/src/foundations/fields.rs | 91 + crates/typst-library/src/foundations/float.rs | 190 ++ crates/typst-library/src/foundations/func.rs | 541 ++++++ crates/typst-library/src/foundations/int.rs | 478 +++++ crates/typst-library/src/foundations/label.rs | 93 + crates/typst-library/src/foundations/mod.rs | 302 +++ crates/typst-library/src/foundations/module.rs | 142 ++ crates/typst-library/src/foundations/none.rs | 114 ++ crates/typst-library/src/foundations/ops.rs | 583 ++++++ crates/typst-library/src/foundations/plugin.rs | 365 ++++ crates/typst-library/src/foundations/repr.rs | 235 +++ crates/typst-library/src/foundations/scope.rs | 416 ++++ crates/typst-library/src/foundations/selector.rs | 449 +++++ crates/typst-library/src/foundations/str.rs | 969 ++++++++++ crates/typst-library/src/foundations/styles.rs | 1034 ++++++++++ crates/typst-library/src/foundations/symbol.rs | 356 ++++ crates/typst-library/src/foundations/sys.rs | 18 + crates/typst-library/src/foundations/ty.rs | 223 +++ crates/typst-library/src/foundations/value.rs | 761 ++++++++ crates/typst-library/src/foundations/version.rs | 202 ++ crates/typst-library/src/introspection/counter.rs | 856 +++++++++ crates/typst-library/src/introspection/here.rs | 52 + .../src/introspection/introspector.rs | 453 +++++ crates/typst-library/src/introspection/locate.rs | 119 ++ crates/typst-library/src/introspection/location.rs | 113 ++ crates/typst-library/src/introspection/locator.rs | 350 ++++ crates/typst-library/src/introspection/metadata.rs | 38 + crates/typst-library/src/introspection/mod.rs | 53 + crates/typst-library/src/introspection/query.rs | 171 ++ crates/typst-library/src/introspection/state.rs | 476 +++++ crates/typst-library/src/introspection/tag.rs | 73 + crates/typst-library/src/layout/abs.rs | 279 +++ crates/typst-library/src/layout/align.rs | 748 ++++++++ crates/typst-library/src/layout/angle.rs | 244 +++ crates/typst-library/src/layout/axes.rs | 320 ++++ crates/typst-library/src/layout/columns.rs | 98 + crates/typst-library/src/layout/container.rs | 563 ++++++ crates/typst-library/src/layout/corners.rs | 315 +++ crates/typst-library/src/layout/dir.rs | 132 ++ crates/typst-library/src/layout/em.rs | 168 ++ crates/typst-library/src/layout/fr.rs | 145 ++ crates/typst-library/src/layout/fragment.rs | 94 + crates/typst-library/src/layout/frame.rs | 606 ++++++ crates/typst-library/src/layout/grid.rs | 924 +++++++++ crates/typst-library/src/layout/hide.rs | 34 + crates/typst-library/src/layout/layout.rs | 107 ++ crates/typst-library/src/layout/length.rs | 276 +++ crates/typst-library/src/layout/measure.rs | 115 ++ crates/typst-library/src/layout/mod.rs | 106 ++ crates/typst-library/src/layout/pad.rs | 65 + crates/typst-library/src/layout/page.rs | 924 +++++++++ crates/typst-library/src/layout/place.rs | 208 ++ crates/typst-library/src/layout/point.rs | 168 ++ crates/typst-library/src/layout/ratio.rs | 153 ++ crates/typst-library/src/layout/regions.rs | 159 ++ crates/typst-library/src/layout/rel.rs | 276 +++ crates/typst-library/src/layout/repeat.rs | 49 + crates/typst-library/src/layout/sides.rs | 343 ++++ crates/typst-library/src/layout/size.rs | 87 + crates/typst-library/src/layout/spacing.rs | 193 ++ crates/typst-library/src/layout/stack.rs | 84 + crates/typst-library/src/layout/transform.rs | 408 ++++ crates/typst-library/src/lib.rs | 264 +++ crates/typst-library/src/loading/cbor.rs | 62 + crates/typst-library/src/loading/csv.rs | 191 ++ crates/typst-library/src/loading/json.rs | 101 + crates/typst-library/src/loading/mod.rs | 89 + crates/typst-library/src/loading/read.rs | 59 + crates/typst-library/src/loading/toml.rs | 92 + crates/typst-library/src/loading/xml.rs | 123 ++ crates/typst-library/src/loading/yaml.rs | 83 + crates/typst-library/src/math/accent.rs | 147 ++ crates/typst-library/src/math/attach.rs | 156 ++ crates/typst-library/src/math/cancel.rs | 116 ++ crates/typst-library/src/math/equation.rs | 256 +++ crates/typst-library/src/math/frac.rs | 56 + crates/typst-library/src/math/lr.rs | 135 ++ crates/typst-library/src/math/matrix.rs | 448 +++++ crates/typst-library/src/math/mod.rs | 257 +++ crates/typst-library/src/math/op.rs | 103 + crates/typst-library/src/math/root.rs | 35 + crates/typst-library/src/math/style.rs | 254 +++ crates/typst-library/src/math/underover.rs | 156 ++ crates/typst-library/src/model/bibliography.rs | 1101 +++++++++++ crates/typst-library/src/model/cite.rs | 175 ++ crates/typst-library/src/model/document.rs | 145 ++ crates/typst-library/src/model/emph.rs | 41 + crates/typst-library/src/model/enum.rs | 271 +++ crates/typst-library/src/model/figure.rs | 641 +++++++ crates/typst-library/src/model/footnote.rs | 326 ++++ crates/typst-library/src/model/heading.rs | 351 ++++ crates/typst-library/src/model/link.rs | 212 +++ crates/typst-library/src/model/list.rs | 246 +++ crates/typst-library/src/model/mod.rs | 75 + crates/typst-library/src/model/numbering.rs | 658 +++++++ crates/typst-library/src/model/outline.rs | 547 ++++++ crates/typst-library/src/model/par.rs | 431 +++++ crates/typst-library/src/model/quote.rs | 237 +++ crates/typst-library/src/model/reference.rs | 307 +++ crates/typst-library/src/model/strong.rs | 48 + crates/typst-library/src/model/table.rs | 724 +++++++ crates/typst-library/src/model/terms.rs | 204 ++ crates/typst-library/src/routines.rs | 368 ++++ crates/typst-library/src/symbols/emoji.rs | 1363 +++++++++++++ crates/typst-library/src/symbols/mod.rs | 24 + crates/typst-library/src/symbols/sym.rs | 995 ++++++++++ crates/typst-library/src/text/case.rs | 79 + crates/typst-library/src/text/deco.rs | 401 ++++ crates/typst-library/src/text/font/book.rs | 546 ++++++ crates/typst-library/src/text/font/color.rs | 627 ++++++ crates/typst-library/src/text/font/exceptions.rs | 337 ++++ crates/typst-library/src/text/font/mod.rs | 306 +++ crates/typst-library/src/text/font/variant.rs | 320 ++++ crates/typst-library/src/text/item.rs | 118 ++ crates/typst-library/src/text/lang.rs | 317 +++ crates/typst-library/src/text/linebreak.rs | 46 + crates/typst-library/src/text/lorem.rs | 24 + crates/typst-library/src/text/mod.rs | 1318 +++++++++++++ crates/typst-library/src/text/raw.rs | 926 +++++++++ crates/typst-library/src/text/shift.rs | 210 ++ crates/typst-library/src/text/smallcaps.rs | 58 + crates/typst-library/src/text/smartquote.rs | 387 ++++ crates/typst-library/src/text/space.rs | 31 + crates/typst-library/src/visualize/color.rs | 2013 ++++++++++++++++++++ crates/typst-library/src/visualize/gradient.rs | 1260 ++++++++++++ crates/typst-library/src/visualize/image/mod.rs | 360 ++++ crates/typst-library/src/visualize/image/raster.rs | 286 +++ crates/typst-library/src/visualize/image/svg.rs | 289 +++ crates/typst-library/src/visualize/line.rs | 64 + crates/typst-library/src/visualize/mod.rs | 50 + crates/typst-library/src/visualize/paint.rs | 102 + crates/typst-library/src/visualize/path.rs | 276 +++ crates/typst-library/src/visualize/pattern.rs | 285 +++ crates/typst-library/src/visualize/polygon.rs | 135 ++ crates/typst-library/src/visualize/shape.rs | 448 +++++ crates/typst-library/src/visualize/stroke.rs | 617 ++++++ crates/typst-library/translations/ar.txt | 7 + crates/typst-library/translations/ca.txt | 7 + crates/typst-library/translations/cs.txt | 7 + crates/typst-library/translations/da.txt | 7 + crates/typst-library/translations/de.txt | 7 + crates/typst-library/translations/en.txt | 7 + crates/typst-library/translations/es.txt | 7 + crates/typst-library/translations/et.txt | 7 + crates/typst-library/translations/fi.txt | 7 + crates/typst-library/translations/fr.txt | 7 + crates/typst-library/translations/gl.txt | 7 + crates/typst-library/translations/gr.txt | 7 + crates/typst-library/translations/he.txt | 7 + crates/typst-library/translations/hu.txt | 7 + crates/typst-library/translations/is.txt | 7 + crates/typst-library/translations/it.txt | 7 + crates/typst-library/translations/ja.txt | 7 + crates/typst-library/translations/la.txt | 7 + crates/typst-library/translations/nb.txt | 7 + crates/typst-library/translations/nl.txt | 7 + crates/typst-library/translations/nn.txt | 7 + crates/typst-library/translations/pl.txt | 7 + crates/typst-library/translations/pt-PT.txt | 7 + crates/typst-library/translations/pt.txt | 7 + crates/typst-library/translations/ro.txt | 8 + crates/typst-library/translations/ru.txt | 7 + crates/typst-library/translations/sl.txt | 7 + crates/typst-library/translations/sq.txt | 7 + crates/typst-library/translations/sr.txt | 7 + crates/typst-library/translations/sv.txt | 7 + crates/typst-library/translations/tl.txt | 7 + crates/typst-library/translations/tr.txt | 7 + crates/typst-library/translations/ua.txt | 7 + crates/typst-library/translations/vi.txt | 8 + crates/typst-library/translations/zh-TW.txt | 7 + crates/typst-library/translations/zh.txt | 7 + 189 files changed, 51874 insertions(+) create mode 100644 crates/typst-library/Cargo.toml create mode 100644 crates/typst-library/src/diag.rs create mode 100644 crates/typst-library/src/engine.rs create mode 100644 crates/typst-library/src/foundations/args.rs create mode 100644 crates/typst-library/src/foundations/array.rs create mode 100644 crates/typst-library/src/foundations/auto.rs create mode 100644 crates/typst-library/src/foundations/bool.rs create mode 100644 crates/typst-library/src/foundations/bytes.rs create mode 100644 crates/typst-library/src/foundations/calc.rs create mode 100644 crates/typst-library/src/foundations/cast.rs create mode 100644 crates/typst-library/src/foundations/content.rs create mode 100644 crates/typst-library/src/foundations/context.rs create mode 100644 crates/typst-library/src/foundations/datetime.rs create mode 100644 crates/typst-library/src/foundations/decimal.rs create mode 100644 crates/typst-library/src/foundations/dict.rs create mode 100644 crates/typst-library/src/foundations/duration.rs create mode 100644 crates/typst-library/src/foundations/element.rs create mode 100644 crates/typst-library/src/foundations/fields.rs create mode 100644 crates/typst-library/src/foundations/float.rs create mode 100644 crates/typst-library/src/foundations/func.rs create mode 100644 crates/typst-library/src/foundations/int.rs create mode 100644 crates/typst-library/src/foundations/label.rs create mode 100644 crates/typst-library/src/foundations/mod.rs create mode 100644 crates/typst-library/src/foundations/module.rs create mode 100644 crates/typst-library/src/foundations/none.rs create mode 100644 crates/typst-library/src/foundations/ops.rs create mode 100644 crates/typst-library/src/foundations/plugin.rs create mode 100644 crates/typst-library/src/foundations/repr.rs create mode 100644 crates/typst-library/src/foundations/scope.rs create mode 100644 crates/typst-library/src/foundations/selector.rs create mode 100644 crates/typst-library/src/foundations/str.rs create mode 100644 crates/typst-library/src/foundations/styles.rs create mode 100644 crates/typst-library/src/foundations/symbol.rs create mode 100644 crates/typst-library/src/foundations/sys.rs create mode 100644 crates/typst-library/src/foundations/ty.rs create mode 100644 crates/typst-library/src/foundations/value.rs create mode 100644 crates/typst-library/src/foundations/version.rs create mode 100644 crates/typst-library/src/introspection/counter.rs create mode 100644 crates/typst-library/src/introspection/here.rs create mode 100644 crates/typst-library/src/introspection/introspector.rs create mode 100644 crates/typst-library/src/introspection/locate.rs create mode 100644 crates/typst-library/src/introspection/location.rs create mode 100644 crates/typst-library/src/introspection/locator.rs create mode 100644 crates/typst-library/src/introspection/metadata.rs create mode 100644 crates/typst-library/src/introspection/mod.rs create mode 100644 crates/typst-library/src/introspection/query.rs create mode 100644 crates/typst-library/src/introspection/state.rs create mode 100644 crates/typst-library/src/introspection/tag.rs create mode 100644 crates/typst-library/src/layout/abs.rs create mode 100644 crates/typst-library/src/layout/align.rs create mode 100644 crates/typst-library/src/layout/angle.rs create mode 100644 crates/typst-library/src/layout/axes.rs create mode 100644 crates/typst-library/src/layout/columns.rs create mode 100644 crates/typst-library/src/layout/container.rs create mode 100644 crates/typst-library/src/layout/corners.rs create mode 100644 crates/typst-library/src/layout/dir.rs create mode 100644 crates/typst-library/src/layout/em.rs create mode 100644 crates/typst-library/src/layout/fr.rs create mode 100644 crates/typst-library/src/layout/fragment.rs create mode 100644 crates/typst-library/src/layout/frame.rs create mode 100644 crates/typst-library/src/layout/grid.rs create mode 100644 crates/typst-library/src/layout/hide.rs create mode 100644 crates/typst-library/src/layout/layout.rs create mode 100644 crates/typst-library/src/layout/length.rs create mode 100644 crates/typst-library/src/layout/measure.rs create mode 100644 crates/typst-library/src/layout/mod.rs create mode 100644 crates/typst-library/src/layout/pad.rs create mode 100644 crates/typst-library/src/layout/page.rs create mode 100644 crates/typst-library/src/layout/place.rs create mode 100644 crates/typst-library/src/layout/point.rs create mode 100644 crates/typst-library/src/layout/ratio.rs create mode 100644 crates/typst-library/src/layout/regions.rs create mode 100644 crates/typst-library/src/layout/rel.rs create mode 100644 crates/typst-library/src/layout/repeat.rs create mode 100644 crates/typst-library/src/layout/sides.rs create mode 100644 crates/typst-library/src/layout/size.rs create mode 100644 crates/typst-library/src/layout/spacing.rs create mode 100644 crates/typst-library/src/layout/stack.rs create mode 100644 crates/typst-library/src/layout/transform.rs create mode 100644 crates/typst-library/src/lib.rs create mode 100644 crates/typst-library/src/loading/cbor.rs create mode 100644 crates/typst-library/src/loading/csv.rs create mode 100644 crates/typst-library/src/loading/json.rs create mode 100644 crates/typst-library/src/loading/mod.rs create mode 100644 crates/typst-library/src/loading/read.rs create mode 100644 crates/typst-library/src/loading/toml.rs create mode 100644 crates/typst-library/src/loading/xml.rs create mode 100644 crates/typst-library/src/loading/yaml.rs create mode 100644 crates/typst-library/src/math/accent.rs create mode 100644 crates/typst-library/src/math/attach.rs create mode 100644 crates/typst-library/src/math/cancel.rs create mode 100644 crates/typst-library/src/math/equation.rs create mode 100644 crates/typst-library/src/math/frac.rs create mode 100644 crates/typst-library/src/math/lr.rs create mode 100644 crates/typst-library/src/math/matrix.rs create mode 100644 crates/typst-library/src/math/mod.rs create mode 100644 crates/typst-library/src/math/op.rs create mode 100644 crates/typst-library/src/math/root.rs create mode 100644 crates/typst-library/src/math/style.rs create mode 100644 crates/typst-library/src/math/underover.rs create mode 100644 crates/typst-library/src/model/bibliography.rs create mode 100644 crates/typst-library/src/model/cite.rs create mode 100644 crates/typst-library/src/model/document.rs create mode 100644 crates/typst-library/src/model/emph.rs create mode 100644 crates/typst-library/src/model/enum.rs create mode 100644 crates/typst-library/src/model/figure.rs create mode 100644 crates/typst-library/src/model/footnote.rs create mode 100644 crates/typst-library/src/model/heading.rs create mode 100644 crates/typst-library/src/model/link.rs create mode 100644 crates/typst-library/src/model/list.rs create mode 100644 crates/typst-library/src/model/mod.rs create mode 100644 crates/typst-library/src/model/numbering.rs create mode 100644 crates/typst-library/src/model/outline.rs create mode 100644 crates/typst-library/src/model/par.rs create mode 100644 crates/typst-library/src/model/quote.rs create mode 100644 crates/typst-library/src/model/reference.rs create mode 100644 crates/typst-library/src/model/strong.rs create mode 100644 crates/typst-library/src/model/table.rs create mode 100644 crates/typst-library/src/model/terms.rs create mode 100644 crates/typst-library/src/routines.rs create mode 100644 crates/typst-library/src/symbols/emoji.rs create mode 100644 crates/typst-library/src/symbols/mod.rs create mode 100644 crates/typst-library/src/symbols/sym.rs create mode 100644 crates/typst-library/src/text/case.rs create mode 100644 crates/typst-library/src/text/deco.rs create mode 100644 crates/typst-library/src/text/font/book.rs create mode 100644 crates/typst-library/src/text/font/color.rs create mode 100644 crates/typst-library/src/text/font/exceptions.rs create mode 100644 crates/typst-library/src/text/font/mod.rs create mode 100644 crates/typst-library/src/text/font/variant.rs create mode 100644 crates/typst-library/src/text/item.rs create mode 100644 crates/typst-library/src/text/lang.rs create mode 100644 crates/typst-library/src/text/linebreak.rs create mode 100644 crates/typst-library/src/text/lorem.rs create mode 100644 crates/typst-library/src/text/mod.rs create mode 100644 crates/typst-library/src/text/raw.rs create mode 100644 crates/typst-library/src/text/shift.rs create mode 100644 crates/typst-library/src/text/smallcaps.rs create mode 100644 crates/typst-library/src/text/smartquote.rs create mode 100644 crates/typst-library/src/text/space.rs create mode 100644 crates/typst-library/src/visualize/color.rs create mode 100644 crates/typst-library/src/visualize/gradient.rs create mode 100644 crates/typst-library/src/visualize/image/mod.rs create mode 100644 crates/typst-library/src/visualize/image/raster.rs create mode 100644 crates/typst-library/src/visualize/image/svg.rs create mode 100644 crates/typst-library/src/visualize/line.rs create mode 100644 crates/typst-library/src/visualize/mod.rs create mode 100644 crates/typst-library/src/visualize/paint.rs create mode 100644 crates/typst-library/src/visualize/path.rs create mode 100644 crates/typst-library/src/visualize/pattern.rs create mode 100644 crates/typst-library/src/visualize/polygon.rs create mode 100644 crates/typst-library/src/visualize/shape.rs create mode 100644 crates/typst-library/src/visualize/stroke.rs create mode 100644 crates/typst-library/translations/ar.txt create mode 100644 crates/typst-library/translations/ca.txt create mode 100644 crates/typst-library/translations/cs.txt create mode 100644 crates/typst-library/translations/da.txt create mode 100644 crates/typst-library/translations/de.txt create mode 100644 crates/typst-library/translations/en.txt create mode 100644 crates/typst-library/translations/es.txt create mode 100644 crates/typst-library/translations/et.txt create mode 100644 crates/typst-library/translations/fi.txt create mode 100644 crates/typst-library/translations/fr.txt create mode 100644 crates/typst-library/translations/gl.txt create mode 100644 crates/typst-library/translations/gr.txt create mode 100644 crates/typst-library/translations/he.txt create mode 100644 crates/typst-library/translations/hu.txt create mode 100644 crates/typst-library/translations/is.txt create mode 100644 crates/typst-library/translations/it.txt create mode 100644 crates/typst-library/translations/ja.txt create mode 100644 crates/typst-library/translations/la.txt create mode 100644 crates/typst-library/translations/nb.txt create mode 100644 crates/typst-library/translations/nl.txt create mode 100644 crates/typst-library/translations/nn.txt create mode 100644 crates/typst-library/translations/pl.txt create mode 100644 crates/typst-library/translations/pt-PT.txt create mode 100644 crates/typst-library/translations/pt.txt create mode 100644 crates/typst-library/translations/ro.txt create mode 100644 crates/typst-library/translations/ru.txt create mode 100644 crates/typst-library/translations/sl.txt create mode 100644 crates/typst-library/translations/sq.txt create mode 100644 crates/typst-library/translations/sr.txt create mode 100644 crates/typst-library/translations/sv.txt create mode 100644 crates/typst-library/translations/tl.txt create mode 100644 crates/typst-library/translations/tr.txt create mode 100644 crates/typst-library/translations/ua.txt create mode 100644 crates/typst-library/translations/vi.txt create mode 100644 crates/typst-library/translations/zh-TW.txt create mode 100644 crates/typst-library/translations/zh.txt (limited to 'crates/typst-library') diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml new file mode 100644 index 00000000..de28f001 --- /dev/null +++ b/crates/typst-library/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "typst-library" +description = "Typst's standard library." +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +categories = { workspace = true } +keywords = { workspace = true } +readme = { workspace = true } + +[dependencies] +typst-assets = { workspace = true } +typst-macros = { workspace = true } +typst-syntax = { workspace = true } +typst-timing = { workspace = true } +typst-utils = { workspace = true } +az = { workspace = true } +bitflags = { workspace = true } +bumpalo = { workspace = true } +chinese-number = { workspace = true } +ciborium = { workspace = true } +comemo = { workspace = true } +csv = { workspace = true } +ecow = { workspace = true } +flate2 = { workspace = true } +fontdb = { workspace = true } +hayagriva = { workspace = true } +icu_properties = { workspace = true } +icu_provider = { workspace = true } +icu_provider_blob = { workspace = true } +image = { workspace = true } +indexmap = { workspace = true } +kamadak-exif = { workspace = true } +kurbo = { workspace = true } +lipsum = { workspace = true } +once_cell = { workspace = true } +palette = { workspace = true } +phf = { workspace = true } +png = { workspace = true } +qcms = { workspace = true } +rayon = { workspace = true } +regex = { workspace = true } +roxmltree = { workspace = true } +rust_decimal = { workspace = true } +rustybuzz = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +siphasher = { workspace = true } +smallvec = { workspace = true } +syntect = { workspace = true } +time = { workspace = true } +toml = { workspace = true } +ttf-parser = { workspace = true } +two-face = { workspace = true } +typed-arena = { workspace = true } +unicode-math-class = { workspace = true } +unicode-segmentation = { workspace = true } +unscanny = { workspace = true } +usvg = { workspace = true } +wasmi = { workspace = true } +xmlwriter = { workspace = true } + +[dev-dependencies] +typst-dev-assets = { workspace = true } + +[lints] +workspace = true diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs new file mode 100644 index 00000000..bd4c90a1 --- /dev/null +++ b/crates/typst-library/src/diag.rs @@ -0,0 +1,580 @@ +//! Diagnostics. + +use std::fmt::{self, Display, Formatter}; +use std::io; +use std::path::{Path, PathBuf}; +use std::str::Utf8Error; +use std::string::FromUtf8Error; + +use comemo::Tracked; +use ecow::{eco_vec, EcoVec}; +use typst_syntax::package::{PackageSpec, PackageVersion}; +use typst_syntax::{Span, Spanned, SyntaxError}; + +use crate::{World, WorldExt}; + +/// Early-return with a [`StrResult`] or [`SourceResult`]. +/// +/// If called with just a string and format args, returns with a +/// `StrResult`. If called with a span, a string and format args, returns +/// a `SourceResult`. +/// +/// You can also emit hints with the `; hint: "..."` syntax. +/// +/// ```ignore +/// bail!("bailing with a {}", "string result"); +/// bail!(span, "bailing with a {}", "source result"); +/// bail!( +/// span, "bailing with a {}", "source result"; +/// hint: "hint 1" +/// ); +/// bail!( +/// span, "bailing with a {}", "source result"; +/// hint: "hint 1"; +/// hint: "hint 2"; +/// ); +/// ``` +#[macro_export] +#[doc(hidden)] +macro_rules! __bail { + // For bail!("just a {}", "string") + ( + $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + return Err($crate::diag::error!( + $fmt $(, $arg)* + $(; hint: $hint $(, $hint_arg)*)* + )) + }; + + // For bail!(error!(..)) + ($error:expr) => { + return Err(::ecow::eco_vec![$error]) + }; + + // For bail(span, ...) + ($($tts:tt)*) => { + return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)]) + }; +} + +/// Construct an [`EcoString`], [`HintedString`] or [`SourceDiagnostic`] with +/// severity `Error`. +#[macro_export] +#[doc(hidden)] +macro_rules! __error { + // For bail!("just a {}", "string"). + ($fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::eco_format!($fmt, $($arg),*).into() + }; + + // For bail!("a hinted {}", "string"; hint: "some hint"; hint: "...") + ( + $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + $crate::diag::HintedString::new( + $crate::diag::eco_format!($fmt, $($arg),*) + ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))* + }; + + // For bail!(span, ...) + ( + $span:expr, $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + $crate::diag::SourceDiagnostic::error( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))* + }; +} + +/// Construct a [`SourceDiagnostic`] with severity `Warning`. +/// +/// You can also emit hints with the `; hint: "..."` syntax. +/// +/// ```ignore +/// warning!(span, "warning with a {}", "source result"); +/// warning!( +/// span, "warning with a {}", "source result"; +/// hint: "hint 1" +/// ); +/// warning!( +/// span, "warning with a {}", "source result"; +/// hint: "hint 1"; +/// hint: "hint 2"; +/// ); +/// ``` +#[macro_export] +#[doc(hidden)] +macro_rules! __warning { + ( + $span:expr, + $fmt:literal $(, $arg:expr)* + $(; hint: $hint:literal $(, $hint_arg:expr)*)* + $(,)? + ) => { + $crate::diag::SourceDiagnostic::warning( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))* + }; +} + +#[rustfmt::skip] +#[doc(inline)] +pub use { + crate::__bail as bail, + crate::__error as error, + crate::__warning as warning, + ecow::{eco_format, EcoString}, +}; + +/// A result that can carry multiple source errors. +pub type SourceResult = Result>; + +/// An output alongside warnings generated while producing it. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Warned { + /// The produced output. + pub output: T, + /// Warnings generated while producing the output. + pub warnings: EcoVec, +} + +/// An error or warning in a source file. +/// +/// The contained spans will only be detached if any of the input source files +/// were detached. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct SourceDiagnostic { + /// Whether the diagnostic is an error or a warning. + pub severity: Severity, + /// The span of the relevant node in the source code. + pub span: Span, + /// A diagnostic message describing the problem. + pub message: EcoString, + /// The trace of function calls leading to the problem. + pub trace: EcoVec>, + /// Additional hints to the user, indicating how this problem could be avoided + /// or worked around. + pub hints: EcoVec, +} + +/// The severity of a [`SourceDiagnostic`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Severity { + /// A fatal error. + Error, + /// A non-fatal warning. + Warning, +} + +impl SourceDiagnostic { + /// Create a new, bare error. + pub fn error(span: Span, message: impl Into) -> Self { + Self { + severity: Severity::Error, + span, + trace: eco_vec![], + message: message.into(), + hints: eco_vec![], + } + } + + /// Create a new, bare warning. + pub fn warning(span: Span, message: impl Into) -> Self { + Self { + severity: Severity::Warning, + span, + trace: eco_vec![], + message: message.into(), + hints: eco_vec![], + } + } + + /// Adds a single hint to the diagnostic. + pub fn hint(&mut self, hint: impl Into) { + self.hints.push(hint.into()); + } + + /// Adds a single hint to the diagnostic. + pub fn with_hint(mut self, hint: impl Into) -> Self { + self.hint(hint); + self + } + + /// Adds user-facing hints to the diagnostic. + pub fn with_hints(mut self, hints: impl IntoIterator) -> Self { + self.hints.extend(hints); + self + } +} + +impl From for SourceDiagnostic { + fn from(error: SyntaxError) -> Self { + Self { + severity: Severity::Error, + span: error.span, + message: error.message, + trace: eco_vec![], + hints: error.hints, + } + } +} + +/// A part of a diagnostic's [trace](SourceDiagnostic::trace). +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Tracepoint { + /// A function call. + Call(Option), + /// A show rule application. + Show(EcoString), + /// A module import. + Import, +} + +impl Display for Tracepoint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Tracepoint::Call(Some(name)) => { + write!(f, "error occurred in this call of function `{name}`") + } + Tracepoint::Call(None) => { + write!(f, "error occurred in this function call") + } + Tracepoint::Show(name) => { + write!(f, "error occurred while applying show rule to this {name}") + } + Tracepoint::Import => { + write!(f, "error occurred while importing this module") + } + } + } +} + +/// Enrich a [`SourceResult`] with a tracepoint. +pub trait Trace { + /// Add the tracepoint to all errors that lie outside the `span`. + fn trace(self, world: Tracked, make_point: F, span: Span) -> Self + where + F: Fn() -> Tracepoint; +} + +impl Trace for SourceResult { + fn trace(self, world: Tracked, make_point: F, span: Span) -> Self + where + F: Fn() -> Tracepoint, + { + self.map_err(|mut errors| { + let Some(trace_range) = world.range(span) else { return errors }; + for error in errors.make_mut().iter_mut() { + // Skip traces that surround the error. + if let Some(error_range) = world.range(error.span) { + if error.span.id() == span.id() + && trace_range.start <= error_range.start + && trace_range.end >= error_range.end + { + continue; + } + } + + error.trace.push(Spanned::new(make_point(), span)); + } + errors + }) + } +} + +/// A result type with a string error message. +pub type StrResult = Result; + +/// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by +/// adding span information. +pub trait At { + /// Add the span information. + fn at(self, span: Span) -> SourceResult; +} + +impl At for Result +where + S: Into, +{ + fn at(self, span: Span) -> SourceResult { + self.map_err(|message| { + let mut diagnostic = SourceDiagnostic::error(span, message); + if diagnostic.message.contains("(access denied)") { + diagnostic.hint("cannot read file outside of project root"); + diagnostic + .hint("you can adjust the project root with the --root argument"); + } + eco_vec![diagnostic] + }) + } +} + +/// A result type with a string error message and hints. +pub type HintedStrResult = Result; + +/// A string message with hints. +/// +/// This is internally represented by a vector of strings. +/// The first element of the vector contains the message. +/// The remaining elements are the hints. +/// This is done to reduce the size of a HintedString. +/// The vector is guaranteed to not be empty. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct HintedString(EcoVec); + +impl HintedString { + /// Creates a new hinted string with the given message. + pub fn new(message: EcoString) -> Self { + Self(eco_vec![message]) + } + + /// A diagnostic message describing the problem. + pub fn message(&self) -> &EcoString { + self.0.first().unwrap() + } + + /// Additional hints to the user, indicating how this error could be avoided + /// or worked around. + pub fn hints(&self) -> &[EcoString] { + self.0.get(1..).unwrap_or(&[]) + } + + /// Adds a single hint to the hinted string. + pub fn hint(&mut self, hint: impl Into) { + self.0.push(hint.into()); + } + + /// Adds a single hint to the hinted string. + pub fn with_hint(mut self, hint: impl Into) -> Self { + self.hint(hint); + self + } + + /// Adds user-facing hints to the hinted string. + pub fn with_hints(mut self, hints: impl IntoIterator) -> Self { + self.0.extend(hints); + self + } +} + +impl From for HintedString +where + S: Into, +{ + fn from(value: S) -> Self { + Self::new(value.into()) + } +} + +impl At for HintedStrResult { + fn at(self, span: Span) -> SourceResult { + self.map_err(|err| { + let mut components = err.0.into_iter(); + let message = components.next().unwrap(); + let diag = SourceDiagnostic::error(span, message).with_hints(components); + eco_vec![diag] + }) + } +} + +/// Enrich a [`StrResult`] or [`HintedStrResult`] with a hint. +pub trait Hint { + /// Add the hint. + fn hint(self, hint: impl Into) -> HintedStrResult; +} + +impl Hint for Result +where + S: Into, +{ + fn hint(self, hint: impl Into) -> HintedStrResult { + self.map_err(|message| HintedString::new(message.into()).with_hint(hint)) + } +} + +impl Hint for HintedStrResult { + fn hint(self, hint: impl Into) -> HintedStrResult { + self.map_err(|mut error| { + error.hint(hint.into()); + error + }) + } +} + +/// A result type with a file-related error. +pub type FileResult = Result; + +/// An error that occurred while trying to load of a file. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum FileError { + /// A file was not found at this path. + NotFound(PathBuf), + /// A file could not be accessed. + AccessDenied, + /// A directory was found, but a file was expected. + IsDirectory, + /// The file is not a Typst source file, but should have been. + NotSource, + /// The file was not valid UTF-8, but should have been. + InvalidUtf8, + /// The package the file is part of could not be loaded. + Package(PackageError), + /// Another error. + /// + /// The optional string can give more details, if available. + Other(Option), +} + +impl FileError { + /// Create a file error from an I/O error. + pub fn from_io(err: io::Error, path: &Path) -> Self { + match err.kind() { + io::ErrorKind::NotFound => Self::NotFound(path.into()), + io::ErrorKind::PermissionDenied => Self::AccessDenied, + io::ErrorKind::InvalidData + if err.to_string().contains("stream did not contain valid UTF-8") => + { + Self::InvalidUtf8 + } + _ => Self::Other(Some(eco_format!("{err}"))), + } + } +} + +impl std::error::Error for FileError {} + +impl Display for FileError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::NotFound(path) => { + write!(f, "file not found (searched at {})", path.display()) + } + Self::AccessDenied => f.pad("failed to load file (access denied)"), + Self::IsDirectory => f.pad("failed to load file (is a directory)"), + Self::NotSource => f.pad("not a typst source file"), + Self::InvalidUtf8 => f.pad("file is not valid utf-8"), + Self::Package(error) => error.fmt(f), + Self::Other(Some(err)) => write!(f, "failed to load file ({err})"), + Self::Other(None) => f.pad("failed to load file"), + } + } +} + +impl From for FileError { + fn from(_: Utf8Error) -> Self { + Self::InvalidUtf8 + } +} + +impl From for FileError { + fn from(_: FromUtf8Error) -> Self { + Self::InvalidUtf8 + } +} + +impl From for FileError { + fn from(err: PackageError) -> Self { + Self::Package(err) + } +} + +impl From for EcoString { + fn from(err: FileError) -> Self { + eco_format!("{err}") + } +} + +/// A result type with a package-related error. +pub type PackageResult = Result; + +/// An error that occurred while trying to load a package. +/// +/// Some variants have an optional string can give more details, if available. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum PackageError { + /// The specified package does not exist. + NotFound(PackageSpec), + /// The specified package found, but the version does not exist. + VersionNotFound(PackageSpec, PackageVersion), + /// Failed to retrieve the package through the network. + NetworkFailed(Option), + /// The package archive was malformed. + MalformedArchive(Option), + /// Another error. + Other(Option), +} + +impl std::error::Error for PackageError {} + +impl Display for PackageError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::NotFound(spec) => { + write!(f, "package not found (searched for {spec})",) + } + Self::VersionNotFound(spec, latest) => { + write!( + f, + "package found, but version {} does not exist (latest is {})", + spec.version, latest, + ) + } + Self::NetworkFailed(Some(err)) => { + write!(f, "failed to download package ({err})") + } + Self::NetworkFailed(None) => f.pad("failed to download package"), + Self::MalformedArchive(Some(err)) => { + write!(f, "failed to decompress package ({err})") + } + Self::MalformedArchive(None) => { + f.pad("failed to decompress package (archive malformed)") + } + Self::Other(Some(err)) => write!(f, "failed to load package ({err})"), + Self::Other(None) => f.pad("failed to load package"), + } + } +} + +impl From for EcoString { + fn from(err: PackageError) -> Self { + eco_format!("{err}") + } +} + +/// Format a user-facing error message for an XML-like file format. +pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString { + match error { + roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => { + eco_format!( + "failed to parse {format} (found closing tag '{actual}' \ + instead of '{expected}' in line {})", + pos.row + ) + } + roxmltree::Error::UnknownEntityReference(entity, pos) => { + eco_format!( + "failed to parse {format} (unknown entity '{entity}' in line {})", + pos.row + ) + } + roxmltree::Error::DuplicatedAttribute(attr, pos) => { + eco_format!( + "failed to parse {format} (duplicate attribute '{attr}' in line {})", + pos.row + ) + } + roxmltree::Error::NoRootNode => { + eco_format!("failed to parse {format} (missing root node)") + } + err => eco_format!("failed to parse {format} ({err})"), + } +} diff --git a/crates/typst-library/src/engine.rs b/crates/typst-library/src/engine.rs new file mode 100644 index 00000000..e532172e --- /dev/null +++ b/crates/typst-library/src/engine.rs @@ -0,0 +1,387 @@ +//! Definition of the central compilation context. + +use std::collections::HashSet; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use comemo::{Track, Tracked, TrackedMut, Validate}; +use ecow::EcoVec; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; +use typst_syntax::{FileId, Span}; + +use crate::diag::{bail, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; +use crate::foundations::{Styles, Value}; +use crate::introspection::Introspector; +use crate::routines::Routines; +use crate::World; + +/// Holds all data needed during compilation. +pub struct Engine<'a> { + /// Defines implementation of various Typst compiler routines as a table of + /// function pointers. + pub routines: &'a Routines, + /// The compilation environment. + pub world: Tracked<'a, dyn World + 'a>, + /// Provides access to information about the document. + pub introspector: Tracked<'a, Introspector>, + /// May hold a span that is currently under inspection. + pub traced: Tracked<'a, Traced>, + /// A pure sink for warnings, delayed errors, and spans under inspection. + pub sink: TrackedMut<'a, Sink>, + /// The route the engine took during compilation. This is used to detect + /// cyclic imports and excessive nesting. + pub route: Route<'a>, +} + +impl Engine<'_> { + /// Handles a result without immediately terminating execution. Instead, it + /// produces a delayed error that is only promoted to a fatal one if it + /// remains by the end of the introspection loop. + pub fn delay(&mut self, result: SourceResult) -> T { + match result { + Ok(value) => value, + Err(errors) => { + self.sink.delay(errors); + T::default() + } + } + } + + /// Runs tasks on the engine in parallel. + pub fn parallelize(&mut self, iter: P, f: F) -> impl Iterator + where + P: IntoIterator, + I: Iterator, + T: Send, + U: Send, + F: Fn(&mut Engine, T) -> U + Send + Sync, + { + let Engine { + world, introspector, traced, ref route, routines, .. + } = *self; + + // We collect into a vector and then call `into_par_iter` instead of + // using `par_bridge` because it does not retain the ordering. + let work: Vec = iter.into_iter().collect(); + + // Work in parallel. + let mut pairs: Vec<(U, Sink)> = Vec::with_capacity(work.len()); + work.into_par_iter() + .map(|value| { + let mut sink = Sink::new(); + let mut engine = Engine { + world, + introspector, + traced, + sink: sink.track_mut(), + route: route.clone(), + routines, + }; + (f(&mut engine, value), sink) + }) + .collect_into_vec(&mut pairs); + + // Apply the subsinks to the outer sink. + for (_, sink) in &mut pairs { + let sink = std::mem::take(sink); + self.sink.extend(sink.delayed, sink.warnings, sink.values); + } + + pairs.into_iter().map(|(output, _)| output) + } +} + +/// May hold a span that is currently under inspection. +#[derive(Default)] +pub struct Traced(Option); + +impl Traced { + /// Wraps a to-be-traced `Span`. + /// + /// Call `Traced::default()` to trace nothing. + pub fn new(traced: Span) -> Self { + Self(Some(traced)) + } +} + +#[comemo::track] +impl Traced { + /// Returns the traced span _if_ it is part of the given source file or + /// `None` otherwise. + /// + /// We hide the span if it isn't in the given file so that only results for + /// the file with the traced span are invalidated. + pub fn get(&self, id: FileId) -> Option { + if self.0.and_then(Span::id) == Some(id) { + self.0 + } else { + None + } + } +} + +/// A push-only sink for delayed errors, warnings, and traced values. +/// +/// All tracked methods of this type are of the form `(&mut self, ..) -> ()`, so +/// in principle they do not need validation (though that optimization is not +/// yet implemented in comemo). +#[derive(Default, Clone)] +pub struct Sink { + /// Delayed errors: Those are errors that we can ignore until the last + /// iteration. For instance, show rules may throw during earlier iterations + /// because the introspector is not yet ready. We first ignore that and + /// proceed with empty content and only if the error remains by the end + /// of the last iteration, we promote it. + delayed: EcoVec, + /// Warnings emitted during iteration. + warnings: EcoVec, + /// Hashes of all warning's spans and messages for warning deduplication. + warnings_set: HashSet, + /// A sequence of traced values for a span. + values: EcoVec<(Value, Option)>, +} + +impl Sink { + /// The maximum number of traced values. + pub const MAX_VALUES: usize = 10; + + /// Create a new empty sink. + pub fn new() -> Self { + Self::default() + } + + /// Get the stored delayed errors. + pub fn delayed(&mut self) -> EcoVec { + std::mem::take(&mut self.delayed) + } + + /// Get the stored warnings. + pub fn warnings(self) -> EcoVec { + self.warnings + } + + /// Get the values for the traced span. + pub fn values(self) -> EcoVec<(Value, Option)> { + self.values + } + + /// Extend from another sink. + pub fn extend_from_sink(&mut self, other: Sink) { + self.extend(other.delayed, other.warnings, other.values); + } +} + +#[comemo::track] +impl Sink { + /// Push delayed errors. + pub fn delay(&mut self, errors: EcoVec) { + self.delayed.extend(errors); + } + + /// Add a warning. + pub fn warn(&mut self, warning: SourceDiagnostic) { + // Check if warning is a duplicate. + let hash = typst_utils::hash128(&(&warning.span, &warning.message)); + if self.warnings_set.insert(hash) { + self.warnings.push(warning); + } + } + + /// Trace a value and optionally styles for the traced span. + pub fn value(&mut self, value: Value, styles: Option) { + if self.values.len() < Self::MAX_VALUES { + self.values.push((value, styles)); + } + } + + /// Extend from parts of another sink. + fn extend( + &mut self, + delayed: EcoVec, + warnings: EcoVec, + values: EcoVec<(Value, Option)>, + ) { + self.delayed.extend(delayed); + for warning in warnings { + self.warn(warning); + } + if let Some(remaining) = Self::MAX_VALUES.checked_sub(self.values.len()) { + self.values.extend(values.into_iter().take(remaining)); + } + } +} + +/// The route the engine took during compilation. This is used to detect +/// cyclic imports and excessive nesting. +pub struct Route<'a> { + /// The parent route segment, if present. + /// + /// This is used when an engine is created from another engine. + // We need to override the constraint's lifetime here so that `Tracked` is + // covariant over the constraint. If it becomes invariant, we're in for a + // world of lifetime pain. + outer: Option as Validate>::Constraint>>, + /// This is set if this route segment was inserted through the start of a + /// module evaluation. + id: Option, + /// This is set whenever we enter a function, nested layout, or are applying + /// a show rule. The length of this segment plus the lengths of all `outer` + /// route segments make up the length of the route. If the length of the + /// route exceeds `MAX_DEPTH`, then we throw a "maximum ... depth exceeded" + /// error. + len: usize, + /// The upper bound we've established for the parent chain length. + /// + /// We don't know the exact length (that would defeat the whole purpose + /// because it would prevent cache reuse of some computation at different, + /// non-exceeding depths). + upper: AtomicUsize, +} + +impl<'a> Route<'a> { + /// Create a new, empty route. + pub fn root() -> Self { + Self { + id: None, + outer: None, + len: 0, + upper: AtomicUsize::new(0), + } + } + + /// Extend the route with another segment with a default length of 1. + pub fn extend(outer: Tracked<'a, Self>) -> Self { + Route { + outer: Some(outer), + id: None, + len: 1, + upper: AtomicUsize::new(usize::MAX), + } + } + + /// Attach a file id to the route segment. + pub fn with_id(self, id: FileId) -> Self { + Self { id: Some(id), ..self } + } + + /// Set the length of the route segment to zero. + pub fn unnested(self) -> Self { + Self { len: 0, ..self } + } + + /// Start tracking this route. + /// + /// In comparison to [`Track::track`], this method skips this chain link + /// if it does not contribute anything. + pub fn track(&self) -> Tracked<'_, Self> { + match self.outer { + Some(outer) if self.id.is_none() && self.len == 0 => outer, + _ => Track::track(self), + } + } + + /// Increase the nesting depth for this route segment. + pub fn increase(&mut self) { + self.len += 1; + } + + /// Decrease the nesting depth for this route segment. + pub fn decrease(&mut self) { + self.len -= 1; + } +} + +/// The maximum nesting depths. They are different so that even if show rule and +/// call checks are interleaved, for show rule problems we always get the show +/// rule error. The lower the max depth for a kind of error, the higher its +/// precedence compared to the others. +impl Route<'_> { + /// The maximum stack nesting depth. + const MAX_SHOW_RULE_DEPTH: usize = 64; + + /// The maximum layout nesting depth. + const MAX_LAYOUT_DEPTH: usize = 72; + + /// The maximum function call nesting depth. + const MAX_CALL_DEPTH: usize = 80; + + /// Ensures that we are within the maximum show rule depth. + pub fn check_show_depth(&self) -> HintedStrResult<()> { + if !self.within(Route::MAX_SHOW_RULE_DEPTH) { + bail!( + "maximum show rule depth exceeded"; + hint: "check whether the show rule matches its own output" + ); + } + Ok(()) + } + + /// Ensures that we are within the maximum layout depth. + pub fn check_layout_depth(&self) -> HintedStrResult<()> { + if !self.within(Route::MAX_LAYOUT_DEPTH) { + bail!( + "maximum layout depth exceeded"; + hint: "try to reduce the amount of nesting in your layout", + ); + } + Ok(()) + } + + /// Ensures that we are within the maximum function call depth. + pub fn check_call_depth(&self) -> StrResult<()> { + if !self.within(Route::MAX_CALL_DEPTH) { + bail!("maximum function call depth exceeded"); + } + Ok(()) + } +} + +#[comemo::track] +impl<'a> Route<'a> { + /// Whether the given id is part of the route. + pub fn contains(&self, id: FileId) -> bool { + self.id == Some(id) || self.outer.is_some_and(|outer| outer.contains(id)) + } + + /// Whether the route's depth is less than or equal to the given depth. + pub fn within(&self, depth: usize) -> bool { + // We only need atomicity and no synchronization of other operations, so + // `Relaxed` is fine. + use Ordering::Relaxed; + + let upper = self.upper.load(Relaxed); + if upper.saturating_add(self.len) <= depth { + return true; + } + + match self.outer { + Some(_) if depth < self.len => false, + Some(outer) => { + let within = outer.within(depth - self.len); + if within && depth < upper { + // We don't want to accidentally increase the upper bound, + // hence the compare-exchange. + self.upper.compare_exchange(upper, depth, Relaxed, Relaxed).ok(); + } + within + } + None => true, + } + } +} + +impl Default for Route<'_> { + fn default() -> Self { + Self::root() + } +} + +impl Clone for Route<'_> { + fn clone(&self) -> Self { + Self { + outer: self.outer, + id: self.id, + len: self.len, + upper: AtomicUsize::new(self.upper.load(Ordering::Relaxed)), + } + } +} diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs new file mode 100644 index 00000000..ee282a87 --- /dev/null +++ b/crates/typst-library/src/foundations/args.rs @@ -0,0 +1,453 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult, StrResult}; +use crate::foundations::{ + cast, func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value, +}; + +/// Captured arguments to a function. +/// +/// # Argument Sinks +/// Like built-in functions, custom functions can also take a variable number of +/// arguments. You can specify an _argument sink_ which collects all excess +/// arguments as `..sink`. The resulting `sink` value is of the `arguments` +/// type. It exposes methods to access the positional and named arguments. +/// +/// ```example +/// #let format(title, ..authors) = { +/// let by = authors +/// .pos() +/// .join(", ", last: " and ") +/// +/// [*#title* \ _Written by #by;_] +/// } +/// +/// #format("ArtosFlow", "Jane", "Joe") +/// ``` +/// +/// # Spreading +/// Inversely to an argument sink, you can _spread_ arguments, arrays and +/// dictionaries into a function call with the `..spread` operator: +/// +/// ```example +/// #let array = (2, 3, 5) +/// #calc.min(..array) +/// #let dict = (fill: blue) +/// #text(..dict)[Hello] +/// ``` +#[ty(scope, cast, name = "arguments")] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Args { + /// The callsite span for the function. This is not the span of the argument + /// list itself, but of the whole function call. + pub span: Span, + /// The positional and named arguments. + pub items: EcoVec, +} + +impl Args { + /// Create positional arguments from a span and values. + pub fn new(span: Span, values: impl IntoIterator) -> Self { + let items = values + .into_iter() + .map(|value| Arg { + span, + name: None, + value: Spanned::new(value.into_value(), span), + }) + .collect(); + Self { span, items } + } + + /// Attach a span to these arguments if they don't already have one. + pub fn spanned(mut self, span: Span) -> Self { + if self.span.is_detached() { + self.span = span; + } + self + } + + /// Returns the number of remaining positional arguments. + pub fn remaining(&self) -> usize { + self.items.iter().filter(|slot| slot.name.is_none()).count() + } + + /// Insert a positional argument at a specific index. + pub fn insert(&mut self, index: usize, span: Span, value: Value) { + self.items.insert( + index, + Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }, + ) + } + + /// Push a positional argument. + pub fn push(&mut self, span: Span, value: Value) { + self.items.push(Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }) + } + + /// Consume and cast the first positional argument if there is one. + pub fn eat(&mut self) -> SourceResult> + where + T: FromValue>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() { + let value = self.items.remove(i).value; + let span = value.span; + return T::from_value(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Consume n positional arguments if possible. + pub fn consume(&mut self, n: usize) -> SourceResult> { + let mut list = vec![]; + + let mut i = 0; + while i < self.items.len() && list.len() < n { + if self.items[i].name.is_none() { + list.push(self.items.remove(i)); + } else { + i += 1; + } + } + + if list.len() < n { + bail!(self.span, "not enough arguments"); + } + + Ok(list) + } + + /// Consume and cast the first positional argument. + /// + /// Returns a `missing argument: {what}` error if no positional argument is + /// left. + pub fn expect(&mut self, what: &str) -> SourceResult + where + T: FromValue>, + { + match self.eat()? { + Some(v) => Ok(v), + None => bail!(self.missing_argument(what)), + } + } + + /// The error message for missing arguments. + fn missing_argument(&self, what: &str) -> SourceDiagnostic { + for item in &self.items { + let Some(name) = item.name.as_deref() else { continue }; + if name == what { + return error!( + item.span, + "the argument `{what}` is positional"; + hint: "try removing `{}:`", name, + ); + } + } + + error!(self.span, "missing argument: {what}") + } + + /// Find and consume the first castable positional argument. + pub fn find(&mut self) -> SourceResult> + where + T: FromValue>, + { + for (i, slot) in self.items.iter().enumerate() { + if slot.name.is_none() && T::castable(&slot.value.v) { + let value = self.items.remove(i).value; + let span = value.span; + return T::from_value(value).at(span).map(Some); + } + } + Ok(None) + } + + /// Find and consume all castable positional arguments. + pub fn all(&mut self) -> SourceResult> + where + T: FromValue>, + { + let mut list = vec![]; + let mut errors = eco_vec![]; + self.items.retain(|item| { + if item.name.is_some() { + return true; + }; + let span = item.value.span; + let spanned = Spanned::new(std::mem::take(&mut item.value.v), span); + match T::from_value(spanned).at(span) { + Ok(val) => list.push(val), + Err(diags) => errors.extend(diags), + } + false + }); + if !errors.is_empty() { + return Err(errors); + } + Ok(list) + } + + /// Cast and remove the value for the given named argument, returning an + /// error if the conversion fails. + pub fn named(&mut self, name: &str) -> SourceResult> + where + T: FromValue>, + { + // We don't quit once we have a match because when multiple matches + // exist, we want to remove all of them and use the last one. + let mut i = 0; + let mut found = None; + while i < self.items.len() { + if self.items[i].name.as_deref() == Some(name) { + let value = self.items.remove(i).value; + let span = value.span; + found = Some(T::from_value(value).at(span)?); + } else { + i += 1; + } + } + Ok(found) + } + + /// Same as named, but with fallback to find. + pub fn named_or_find(&mut self, name: &str) -> SourceResult> + where + T: FromValue>, + { + match self.named(name)? { + Some(value) => Ok(Some(value)), + None => self.find(), + } + } + + /// Take out all arguments into a new instance. + pub fn take(&mut self) -> Self { + Self { + span: self.span, + items: std::mem::take(&mut self.items), + } + } + + /// Return an "unexpected argument" error if there is any remaining + /// argument. + pub fn finish(self) -> SourceResult<()> { + if let Some(arg) = self.items.first() { + match &arg.name { + Some(name) => bail!(arg.span, "unexpected argument: {name}"), + _ => bail!(arg.span, "unexpected argument"), + } + } + Ok(()) + } +} + +/// A key that can be used to get an argument: either the index of a positional +/// argument, or the name of a named argument. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ArgumentKey { + Index(i64), + Name(Str), +} + +cast! { + ArgumentKey, + v: i64 => Self::Index(v), + v: Str => Self::Name(v), +} + +impl Args { + fn get(&self, key: &ArgumentKey) -> Option<&Value> { + let item = match key { + &ArgumentKey::Index(index) => { + let mut iter = self.items.iter().filter(|item| item.name.is_none()); + if index < 0 { + let index = (-(index + 1)).try_into().ok()?; + iter.nth_back(index) + } else { + let index = index.try_into().ok()?; + iter.nth(index) + } + } + // Accept the last argument with the right name. + ArgumentKey::Name(name) => { + self.items.iter().rfind(|item| item.name.as_ref() == Some(name)) + } + }; + item.map(|item| &item.value.v) + } +} + +#[scope] +impl Args { + /// Construct spreadable arguments in place. + /// + /// This function behaves like `{let args(..sink) = sink}`. + /// + /// ```example + /// #let args = arguments(stroke: red, inset: 1em, [Body]) + /// #box(..args) + /// ``` + #[func(constructor)] + pub fn construct( + /// The real arguments (the other argument is just for the docs). + /// The docs argument cannot be called `args`. + args: &mut Args, + /// The arguments to construct. + #[external] + #[variadic] + arguments: Vec, + ) -> Args { + args.take() + } + + /// Returns the positional argument at the specified index, or the named + /// argument with the specified name. + /// + /// If the key is an [integer]($int), this is equivalent to first calling + /// [`pos`]($arguments.pos) and then [`array.at`]. If it is a [string]($str), + /// this is equivalent to first calling [`named`]($arguments.named) and then + /// [`dictionary.at`]. + #[func] + pub fn at( + &self, + /// The index or name of the argument to get. + key: ArgumentKey, + /// A default value to return if the key is invalid. + #[named] + default: Option, + ) -> StrResult { + self.get(&key) + .cloned() + .or(default) + .ok_or_else(|| missing_key_no_default(key)) + } + + /// Returns the captured positional arguments as an array. + #[func(name = "pos", title = "Positional")] + pub fn to_pos(&self) -> Array { + self.items + .iter() + .filter(|item| item.name.is_none()) + .map(|item| item.value.v.clone()) + .collect() + } + + /// Returns the captured named arguments as a dictionary. + #[func(name = "named")] + pub fn to_named(&self) -> Dict { + self.items + .iter() + .filter_map(|item| item.name.clone().map(|name| (name, item.value.v.clone()))) + .collect() + } +} + +impl Debug for Args { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_list().entries(&self.items).finish() + } +} + +impl Repr for Args { + fn repr(&self) -> EcoString { + let pieces = self.items.iter().map(Arg::repr).collect::>(); + repr::pretty_array_like(&pieces, false).into() + } +} + +impl PartialEq for Args { + fn eq(&self, other: &Self) -> bool { + self.to_pos() == other.to_pos() && self.to_named() == other.to_named() + } +} + +/// An argument to a function call: `12` or `draw: false`. +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Arg { + /// The span of the whole argument. + pub span: Span, + /// The name of the argument (`None` for positional arguments). + pub name: Option, + /// The value of the argument. + pub value: Spanned, +} + +impl Debug for Arg { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Some(name) = &self.name { + name.fmt(f)?; + f.write_str(": ")?; + self.value.v.fmt(f) + } else { + self.value.v.fmt(f) + } + } +} + +impl Repr for Arg { + fn repr(&self) -> EcoString { + if let Some(name) = &self.name { + eco_format!("{}: {}", name, self.value.v.repr()) + } else { + self.value.v.repr() + } + } +} + +impl PartialEq for Arg { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.value.v == other.value.v + } +} + +/// Things that can be used as arguments. +pub trait IntoArgs { + /// Convert into arguments, attaching the `fallback` span in case `Self` + /// doesn't have a span. + fn into_args(self, fallback: Span) -> Args; +} + +impl IntoArgs for Args { + fn into_args(self, fallback: Span) -> Args { + self.spanned(fallback) + } +} + +impl IntoArgs for I +where + I: IntoIterator, + T: IntoValue, +{ + fn into_args(self, fallback: Span) -> Args { + Args::new(fallback, self) + } +} + +/// The missing key access error message when no default was given. +#[cold] +fn missing_key_no_default(key: ArgumentKey) -> EcoString { + eco_format!( + "arguments do not contain key {} \ + and no default value was specified", + match key { + ArgumentKey::Index(i) => i.repr(), + ArgumentKey::Name(name) => name.repr(), + } + ) +} diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs new file mode 100644 index 00000000..9c8aecac --- /dev/null +++ b/crates/typst-library/src/foundations/array.rs @@ -0,0 +1,1133 @@ +use std::cmp::Ordering; +use std::fmt::{Debug, Formatter}; +use std::num::{NonZeroI64, NonZeroUsize}; +use std::ops::{Add, AddAssign}; + +use comemo::Tracked; +use ecow::{eco_format, EcoString, EcoVec}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, + Func, IntoValue, Reflect, Repr, Str, Value, Version, +}; + +/// Create a new [`Array`] from values. +#[macro_export] +#[doc(hidden)] +macro_rules! __array { + ($value:expr; $count:expr) => { + $crate::foundations::Array::from($crate::foundations::eco_vec![ + $crate::foundations::IntoValue::into_value($value); + $count + ]) + }; + + ($($value:expr),* $(,)?) => { + $crate::foundations::Array::from($crate::foundations::eco_vec![$( + $crate::foundations::IntoValue::into_value($value) + ),*]) + }; +} + +#[doc(inline)] +pub use crate::__array as array; + +/// A sequence of values. +/// +/// You can construct an array by enclosing a comma-separated sequence of values +/// in parentheses. The values do not have to be of the same type. +/// +/// You can access and update array items with the `.at()` method. Indices are +/// zero-based and negative indices wrap around to the end of the array. You can +/// iterate over an array using a [for loop]($scripting/#loops). Arrays can be +/// added together with the `+` operator, [joined together]($scripting/#blocks) +/// and multiplied with integers. +/// +/// **Note:** An array of length one needs a trailing comma, as in `{(1,)}`. +/// This is to disambiguate from a simple parenthesized expressions like `{(1 + +/// 2) * 3}`. An empty array is written as `{()}`. +/// +/// # Example +/// ```example +/// #let values = (1, 7, 4, -3, 2) +/// +/// #values.at(0) \ +/// #(values.at(0) = 3) +/// #values.at(-1) \ +/// #values.find(calc.even) \ +/// #values.filter(calc.odd) \ +/// #values.map(calc.abs) \ +/// #values.rev() \ +/// #(1, (2, 3)).flatten() \ +/// #(("A", "B", "C") +/// .join(", ", last: " and ")) +/// ``` +#[ty(scope, cast)] +#[derive(Default, Clone, PartialEq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Array(EcoVec); + +impl Array { + /// Create a new, empty array. + pub fn new() -> Self { + Self::default() + } + + /// Creates a new vec, with a known capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self(EcoVec::with_capacity(capacity)) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Extract a slice of the whole array. + pub fn as_slice(&self) -> &[Value] { + self.0.as_slice() + } + + /// Iterate over references to the contained values. + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } + + /// Mutably borrow the first value in the array. + pub fn first_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().first_mut().ok_or_else(array_is_empty) + } + + /// Mutably borrow the last value in the array. + pub fn last_mut(&mut self) -> StrResult<&mut Value> { + self.0.make_mut().last_mut().ok_or_else(array_is_empty) + } + + /// Mutably borrow the value at the given index. + pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> { + let len = self.len(); + self.locate_opt(index, false) + .and_then(move |i| self.0.make_mut().get_mut(i)) + .ok_or_else(|| out_of_bounds(index, len)) + } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64, end_ok: bool) -> StrResult { + self.locate_opt(index, end_ok) + .ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds. + /// + /// `index == len` is considered in bounds if and only if `end_ok` is true. + fn locate_opt(&self, index: i64, end_ok: bool) -> Option { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v < self.0.len() + end_ok as usize) + } + + /// Repeat this array `n` times. + pub fn repeat(&self, n: usize) -> StrResult { + let count = self + .len() + .checked_mul(n) + .ok_or_else(|| format!("cannot repeat this array {n} times"))?; + + Ok(self.iter().cloned().cycle().take(count).collect()) + } +} + +#[scope] +impl Array { + /// Converts a value to an array. + /// + /// Note that this function is only intended for conversion of a collection-like + /// value to an array, not for creation of an array from individual items. Use + /// the array syntax `(1, 2, 3)` (or `(1,)` for a single-element array) instead. + /// + /// ```example + /// #let hi = "Hello 😃" + /// #array(bytes(hi)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to an array. + value: ToArray, + ) -> Array { + value.0 + } + + /// The number of values in the array. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the first item in the array. May be used on the left-hand side + /// of an assignment. Fails with an error if the array is empty. + #[func] + pub fn first(&self) -> StrResult { + self.0.first().cloned().ok_or_else(array_is_empty) + } + + /// Returns the last item in the array. May be used on the left-hand side of + /// an assignment. Fails with an error if the array is empty. + #[func] + pub fn last(&self) -> StrResult { + self.0.last().cloned().ok_or_else(array_is_empty) + } + + /// Returns the item at the specified index in the array. May be used on the + /// left-hand side of an assignment. Returns the default value if the index + /// is out of bounds or fails with an error if no default value was + /// specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the item. If negative, indexes from + /// the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option, + ) -> StrResult { + self.locate_opt(index, false) + .and_then(|i| self.0.get(i).cloned()) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Adds a value to the end of the array. + #[func] + pub fn push( + &mut self, + /// The value to insert at the end of the array. + value: Value, + ) { + self.0.push(value); + } + + /// Removes the last item from the array and returns it. Fails with an error + /// if the array is empty. + #[func] + pub fn pop(&mut self) -> StrResult { + self.0.pop().ok_or_else(array_is_empty) + } + + /// Inserts a value into the array at the specified index, shifting all + /// subsequent elements to the right. Fails with an error if the index is + /// out of bounds. + /// + /// To replace an element of an array, use [`at`]($array.at). + #[func] + pub fn insert( + &mut self, + /// The index at which to insert the item. If negative, indexes from + /// the back. + index: i64, + /// The value to insert into the array. + value: Value, + ) -> StrResult<()> { + let i = self.locate(index, true)?; + self.0.insert(i, value); + Ok(()) + } + + /// Removes the value at the specified index from the array and return it. + #[func] + pub fn remove( + &mut self, + /// The index at which to remove the item. If negative, indexes from + /// the back. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option, + ) -> StrResult { + self.locate_opt(index, false) + .map(|i| self.0.remove(i)) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Extracts a subslice of the array. Fails with an error if the start or end + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). If negative, indexes from the back. + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// of the array is extracted. If negative, indexes from the back. + #[default] + end: Option, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with `end`. + #[named] + count: Option, + ) -> StrResult { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } + let start = self.locate(start, true)?; + let end = self.locate(end.unwrap_or(self.len() as i64), true)?.max(start); + Ok(self.0[start..end].into()) + } + + /// Whether the array contains the specified value. + /// + /// This method also has dedicated syntax: You can write `{2 in (1, 2, 3)}` + /// instead of `{(1, 2, 3).contains(2)}`. + #[func] + pub fn contains( + &self, + /// The value to search for. + value: Value, + ) -> bool { + self.0.contains(&value) + } + + /// Searches for an item for which the given function returns `{true}` and + /// returns the first match or `{none}` if there is no match. + #[func] + pub fn find( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult> { + for item in self.iter() { + if searcher + .call(engine, context, [item.clone()])? + .cast::() + .at(searcher.span())? + { + return Ok(Some(item.clone())); + } + } + Ok(None) + } + + /// Searches for an item for which the given function returns `{true}` and + /// returns the index of the first match or `{none}` if there is no match. + #[func] + pub fn position( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The function to apply to each item. Must return a boolean. + searcher: Func, + ) -> SourceResult> { + for (i, item) in self.iter().enumerate() { + if searcher + .call(engine, context, [item.clone()])? + .cast::() + .at(searcher.span())? + { + return Ok(Some(i as i64)); + } + } + + Ok(None) + } + + /// Create an array consisting of a sequence of numbers. + /// + /// If you pass just one positional parameter, it is interpreted as the + /// `end` of the range. If you pass two, they describe the `start` and `end` + /// of the range. + /// + /// This function is available both in the array function's scope and + /// globally. + /// + /// ```example + /// #range(5) \ + /// #range(2, 5) \ + /// #range(20, step: 4) \ + /// #range(21, step: 4) \ + /// #range(5, 2, step: -1) + /// ``` + #[func] + pub fn range( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + /// The start of the range (inclusive). + #[external] + #[default] + start: i64, + /// The end of the range (exclusive). + #[external] + end: i64, + /// The distance between the generated numbers. + #[named] + #[default(NonZeroI64::new(1).unwrap())] + step: NonZeroI64, + ) -> SourceResult { + let first = args.expect::("end")?; + let (start, end) = match args.eat::()? { + Some(second) => (first, second), + None => (0, first), + }; + + let step = step.get(); + + let mut x = start; + let mut array = Self::new(); + + while x.cmp(&end) == 0.cmp(&step) { + array.push(x.into_value()); + x += step; + } + + Ok(array) + } + + /// Produces a new array with only the items from the original one for which + /// the given function returns true. + #[func] + pub fn filter( + &self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult { + let mut kept = EcoVec::new(); + for item in self.iter() { + if test + .call(engine, context, [item.clone()])? + .cast::() + .at(test.span())? + { + kept.push(item.clone()) + } + } + Ok(kept.into()) + } + + /// Produces a new array in which all items from the original one were + /// transformed with the given function. + #[func] + pub fn map( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The function to apply to each item. + mapper: Func, + ) -> SourceResult { + self.into_iter() + .map(|item| mapper.call(engine, context, [item])) + .collect() + } + + /// Returns a new array with the values alongside their indices. + /// + /// The returned array consists of `(index, value)` pairs in the form of + /// length-2 arrays. These can be [destructured]($scripting/#bindings) with + /// a let binding or for loop. + #[func] + pub fn enumerate( + self, + /// The index returned for the first pair of the returned list. + #[named] + #[default(0)] + start: i64, + ) -> StrResult { + self.into_iter() + .enumerate() + .map(|(i, value)| { + Ok(array![ + start + .checked_add_unsigned(i as u64) + .ok_or("array index is too large")?, + value + ] + .into_value()) + }) + .collect() + } + + /// Zips the array with other arrays. + /// + /// Returns an array of arrays, where the `i`th inner array contains all the + /// `i`th elements from each original array. + /// + /// If the arrays to be zipped have different lengths, they are zipped up to + /// the last element of the shortest array and all remaining elements are + /// ignored. + /// + /// This function is variadic, meaning that you can zip multiple arrays + /// together at once: `{(1, 2).zip(("A", "B"), (10, 20))}` yields + /// `{((1, "A", 10), (2, "B", 20))}`. + #[func] + pub fn zip( + self, + /// The real arguments (the `others` arguments are just for the docs, this + /// function is a bit involved, so we parse the positional arguments manually). + args: &mut Args, + /// Whether all arrays have to have the same length. + /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an + /// error. + #[named] + #[default(false)] + exact: bool, + /// The arrays to zip with. + #[external] + #[variadic] + others: Vec, + ) -> SourceResult { + let remaining = args.remaining(); + + // Fast path for one array. + if remaining == 0 { + return Ok(self.into_iter().map(|item| array![item].into_value()).collect()); + } + + // Fast path for just two arrays. + if remaining == 1 { + let Spanned { v: other, span: other_span } = + args.expect::>("others")?; + if exact && self.len() != other.len() { + bail!( + other_span, + "second array has different length ({}) from first array ({})", + other.len(), + self.len() + ); + } + return Ok(self + .into_iter() + .zip(other) + .map(|(first, second)| array![first, second].into_value()) + .collect()); + } + + // If there is more than one array, we use the manual method. + let mut out = Self::with_capacity(self.len()); + let arrays = args.all::>()?; + if exact { + let errs = arrays + .iter() + .filter(|sp| sp.v.len() != self.len()) + .map(|Spanned { v, span }| { + SourceDiagnostic::error( + *span, + eco_format!( + "array has different length ({}) from first array ({})", + v.len(), + self.len() + ), + ) + }) + .collect::>(); + if !errs.is_empty() { + return Err(errs); + } + } + + let mut iterators = + arrays.into_iter().map(|i| i.v.into_iter()).collect::>(); + + for this in self { + let mut row = Self::with_capacity(1 + iterators.len()); + row.push(this.clone()); + + for iterator in &mut iterators { + let Some(item) = iterator.next() else { + return Ok(out); + }; + + row.push(item); + } + + out.push(row.into_value()); + } + + Ok(out) + } + + /// Folds all items into a single value using an accumulator function. + #[func] + pub fn fold( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The initial value to start with. + init: Value, + /// The folding function. Must have two parameters: One for the + /// accumulated value and one for an item. + folder: Func, + ) -> SourceResult { + let mut acc = init; + for item in self { + acc = folder.call(engine, context, [acc, item])?; + } + Ok(acc) + } + + /// Sums all items (works for all types that can be added). + #[func] + pub fn sum( + self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option, + ) -> HintedStrResult { + let mut iter = self.into_iter(); + let mut acc = iter + .next() + .or(default) + .ok_or("cannot calculate sum of empty array with no default")?; + for item in iter { + acc = ops::add(acc, item)?; + } + Ok(acc) + } + + /// Calculates the product all items (works for all types that can be + /// multiplied). + #[func] + pub fn product( + self, + /// What to return if the array is empty. Must be set if the array can + /// be empty. + #[named] + default: Option, + ) -> HintedStrResult { + let mut iter = self.into_iter(); + let mut acc = iter + .next() + .or(default) + .ok_or("cannot calculate product of empty array with no default")?; + for item in iter { + acc = ops::mul(acc, item)?; + } + Ok(acc) + } + + /// Whether the given function returns `{true}` for any item in the array. + #[func] + pub fn any( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult { + for item in self { + if test.call(engine, context, [item])?.cast::().at(test.span())? { + return Ok(true); + } + } + + Ok(false) + } + + /// Whether the given function returns `{true}` for all items in the array. + #[func] + pub fn all( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The function to apply to each item. Must return a boolean. + test: Func, + ) -> SourceResult { + for item in self { + if !test.call(engine, context, [item])?.cast::().at(test.span())? { + return Ok(false); + } + } + + Ok(true) + } + + /// Combine all nested arrays into a single flat one. + #[func] + pub fn flatten(self) -> Array { + let mut flat = EcoVec::with_capacity(self.0.len()); + for item in self { + if let Value::Array(nested) = item { + flat.extend(nested.flatten()); + } else { + flat.push(item); + } + } + flat.into() + } + + /// Return a new array with the same items, but in reverse order. + #[func(title = "Reverse")] + pub fn rev(self) -> Array { + self.into_iter().rev().collect() + } + + /// Split the array at occurrences of the specified value. + #[func] + pub fn split( + &self, + /// The value to split at. + at: Value, + ) -> Array { + self.as_slice() + .split(|value| *value == at) + .map(|subslice| Value::Array(subslice.iter().cloned().collect())) + .collect() + } + + /// Combine all items in the array into one. + #[func] + pub fn join( + self, + /// A value to insert between each item of the array. + #[default] + separator: Option, + /// An alternative separator between the last two items. + #[named] + last: Option, + ) -> StrResult { + let len = self.0.len(); + let separator = separator.unwrap_or(Value::None); + + let mut last = last; + let mut result = Value::None; + for (i, value) in self.into_iter().enumerate() { + if i > 0 { + if i + 1 == len && last.is_some() { + result = ops::join(result, last.take().unwrap())?; + } else { + result = ops::join(result, separator.clone())?; + } + } + + result = ops::join(result, value)?; + } + + Ok(result) + } + + /// Returns an array with a copy of the separator value placed between + /// adjacent elements. + #[func] + pub fn intersperse( + self, + /// The value that will be placed between each adjacent element. + separator: Value, + ) -> Array { + // TODO: Use once stabilized: + // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.intersperse + let size = match self.len() { + 0 => return Array::new(), + n => (2 * n) - 1, + }; + let mut vec = EcoVec::with_capacity(size); + let mut iter = self.into_iter(); + + if let Some(first) = iter.next() { + vec.push(first); + } + + for value in iter { + vec.push(separator.clone()); + vec.push(value); + } + + Array(vec) + } + + /// Splits an array into non-overlapping chunks, starting at the beginning, + /// ending with a single remainder chunk. + /// + /// All chunks but the last have `chunk-size` elements. + /// If `exact` is set to `{true}`, the remainder is dropped if it + /// contains less than `chunk-size` elements. + /// + /// ```example + /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) + /// #array.chunks(3) + /// #array.chunks(3, exact: true) + /// ``` + #[func] + pub fn chunks( + self, + /// How many elements each chunk may at most contain. + chunk_size: NonZeroUsize, + /// Whether to keep the remainder if its size is less than `chunk-size`. + #[named] + #[default(false)] + exact: bool, + ) -> Array { + let to_array = |chunk| Array::from(chunk).into_value(); + if exact { + self.0.chunks_exact(chunk_size.get()).map(to_array).collect() + } else { + self.0.chunks(chunk_size.get()).map(to_array).collect() + } + } + + /// Returns sliding windows of `window-size` elements over an array. + /// + /// If the array length is less than `window-size`, this will return an empty array. + /// + /// ```example + /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) + /// #array.windows(5) + /// ``` + #[func] + pub fn windows( + self, + /// How many elements each window will contain. + window_size: NonZeroUsize, + ) -> Array { + self.0 + .windows(window_size.get()) + .map(|window| Array::from(window).into_value()) + .collect() + } + + /// Return a sorted version of this array, optionally by a given key + /// function. The sorting algorithm used is stable. + /// + /// Returns an error if two values could not be compared or if the key + /// function (if given) yields an error. + #[func] + pub fn sorted( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The callsite span. + span: Span, + /// If given, applies this function to the elements in the array to + /// determine the keys to sort by. + #[named] + key: Option, + ) -> SourceResult { + let mut result = Ok(()); + let mut vec = self.0; + let mut key_of = |x: Value| match &key { + // NOTE: We are relying on `comemo`'s memoization of function + // evaluation to not excessively reevaluate the `key`. + Some(f) => f.call(engine, context, [x]), + None => Ok(x), + }; + vec.make_mut().sort_by(|a, b| { + // Until we get `try` blocks :) + match (key_of(a.clone()), key_of(b.clone())) { + (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { + if result.is_ok() { + result = Err(err).at(span); + } + Ordering::Equal + }), + (Err(e), _) | (_, Err(e)) => { + if result.is_ok() { + result = Err(e); + } + Ordering::Equal + } + } + }); + result.map(|_| vec.into()) + } + + /// Deduplicates all items in the array. + /// + /// Returns a new array with all duplicate items removed. Only the first + /// element of each duplicate is kept. + /// + /// ```example + /// #(1, 1, 2, 3, 1).dedup() + /// ``` + #[func(title = "Deduplicate")] + pub fn dedup( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// If given, applies this function to the elements in the array to + /// determine the keys to deduplicate by. + #[named] + key: Option, + ) -> SourceResult { + let mut out = EcoVec::with_capacity(self.0.len()); + let mut key_of = |x: Value| match &key { + // NOTE: We are relying on `comemo`'s memoization of function + // evaluation to not excessively reevaluate the `key`. + Some(f) => f.call(engine, context, [x]), + None => Ok(x), + }; + + // This algorithm is O(N^2) because we cannot rely on `HashSet` since: + // 1. We would like to preserve the order of the elements. + // 2. We cannot hash arbitrary `Value`. + 'outer: for value in self { + let key = key_of(value.clone())?; + if out.is_empty() { + out.push(value); + continue; + } + + for second in out.iter() { + if ops::equal(&key, &key_of(second.clone())?) { + continue 'outer; + } + } + + out.push(value); + } + + Ok(Self(out)) + } + + /// Converts an array of pairs into a dictionary. + /// The first value of each pair is the key, the second the value. + /// + /// If the same key occurs multiple times, the last value is selected. + /// + /// ```example + /// #( + /// ("apples", 2), + /// ("peaches", 3), + /// ("apples", 5), + /// ).to-dict() + /// ``` + #[func] + pub fn to_dict(self) -> StrResult { + self.into_iter() + .map(|value| { + let value_ty = value.ty(); + let pair = value.cast::().map_err(|_| { + eco_format!("expected (str, any) pairs, found {}", value_ty) + })?; + if let [key, value] = pair.as_slice() { + let key = key.clone().cast::().map_err(|_| { + eco_format!("expected key of type str, found {}", value.ty()) + })?; + Ok((key, value.clone())) + } else { + bail!("expected pairs of length 2, found length {}", pair.len()); + } + }) + .collect() + } + + /// Reduces the elements to a single one, by repeatedly applying a reducing + /// operation. + /// + /// If the array is empty, returns `{none}`, otherwise, returns the result + /// of the reduction. + /// + /// The reducing function is a closure with two arguments: an "accumulator", + /// and an element. + /// + /// For arrays with at least one element, this is the same as [`array.fold`] + /// with the first element of the array as the initial accumulator value, + /// folding every subsequent element into it. + #[func] + pub fn reduce( + self, + /// The engine. + engine: &mut Engine, + /// The callsite context. + context: Tracked, + /// The reducing function. Must have two parameters: One for the + /// accumulated value and one for an item. + reducer: Func, + ) -> SourceResult { + let mut iter = self.into_iter(); + let mut acc = iter.next().unwrap_or_default(); + for item in iter { + acc = reducer.call(engine, context, [acc, item])?; + } + Ok(acc) + } +} + +/// A value that can be cast to bytes. +pub struct ToArray(Array); + +cast! { + ToArray, + v: Array => Self(v), + v: Bytes => Self(v.iter().map(|&b| Value::Int(b.into())).collect()), + v: Version => Self(v.values().iter().map(|&v| Value::Int(v as i64)).collect()) +} + +impl Debug for Array { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + f.debug_list().entries(&self.0).finish() + } +} + +impl Repr for Array { + fn repr(&self) -> EcoString { + let max = 40; + let mut pieces: Vec<_> = self + .iter() + .take(max) + .map(|value| eco_format!("{}", value.repr())) + .collect(); + if self.len() > max { + pieces.push(eco_format!(".. ({} items omitted)", self.len() - max)); + } + repr::pretty_array_like(&pieces, self.len() == 1).into() + } +} + +impl Add for Array { + type Output = Self; + + fn add(mut self, rhs: Array) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Array { + fn add_assign(&mut self, rhs: Self) { + self.0.extend(rhs.0); + } +} + +impl Extend for Array { + fn extend>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl FromIterator for Array { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl IntoIterator for Array { + type Item = Value; + type IntoIter = ecow::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a Array { + type Item = &'a Value; + type IntoIter = std::slice::Iter<'a, Value>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl From> for Array { + fn from(v: EcoVec) -> Self { + Array(v) + } +} + +impl From<&[Value]> for Array { + fn from(v: &[Value]) -> Self { + Array(v.into()) + } +} + +impl Reflect for Vec { + fn input() -> CastInfo { + Array::input() + } + + fn output() -> CastInfo { + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl Reflect for SmallVec<[T; N]> { + fn input() -> CastInfo { + Array::input() + } + + fn output() -> CastInfo { + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl IntoValue for Vec { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} + +impl IntoValue for SmallVec<[T; N]> { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} + +impl FromValue for Vec { + fn from_value(value: Value) -> HintedStrResult { + value.cast::()?.into_iter().map(Value::cast).collect() + } +} + +impl FromValue for SmallVec<[T; N]> { + fn from_value(value: Value) -> HintedStrResult { + value.cast::()?.into_iter().map(Value::cast).collect() + } +} + +/// The error message when the array is empty. +#[cold] +fn array_is_empty() -> EcoString { + "array is empty".into() +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("array index out of bounds (index: {index}, len: {len})") +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { + eco_format!( + "array index out of bounds (index: {index}, len: {len}) \ + and no default value was specified", + ) +} diff --git a/crates/typst-library/src/foundations/auto.rs b/crates/typst-library/src/foundations/auto.rs new file mode 100644 index 00000000..8237b450 --- /dev/null +++ b/crates/typst-library/src/foundations/auto.rs @@ -0,0 +1,267 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +use crate::diag::HintedStrResult; +use crate::foundations::{ + ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type, + Value, +}; + +/// A value that indicates a smart default. +/// +/// The auto type has exactly one value: `{auto}`. +/// +/// Parameters that support the `{auto}` value have some smart default or +/// contextual behaviour. A good example is the [text direction]($text.dir) +/// parameter. Setting it to `{auto}` lets Typst automatically determine the +/// direction from the [text language]($text.lang). +#[ty(cast, name = "auto")] +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct AutoValue; + +impl IntoValue for AutoValue { + fn into_value(self) -> Value { + Value::Auto + } +} + +impl FromValue for AutoValue { + fn from_value(value: Value) -> HintedStrResult { + match value { + Value::Auto => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Reflect for AutoValue { + fn input() -> CastInfo { + CastInfo::Type(Type::of::()) + } + + fn output() -> CastInfo { + CastInfo::Type(Type::of::()) + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::Auto) + } +} + +impl Debug for AutoValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Auto") + } +} + +impl Repr for AutoValue { + fn repr(&self) -> EcoString { + "auto".into() + } +} + +/// A value that can be automatically determined. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Smart { + /// The value should be determined smartly based on the circumstances. + Auto, + /// A specific value. + Custom(T), +} + +impl Smart { + /// Whether the value is `Auto`. + pub fn is_auto(&self) -> bool { + matches!(self, Self::Auto) + } + + /// Whether this holds a custom value. + pub fn is_custom(&self) -> bool { + matches!(self, Self::Custom(_)) + } + + /// Whether this is a `Smart::Custom(x)` and `f(x)` is true. + pub fn is_custom_and(self, f: F) -> bool + where + F: Fn(T) -> bool, + { + match self { + Self::Auto => false, + Self::Custom(x) => f(x), + } + } + + /// Returns a `Smart<&T>` borrowing the inner `T`. + pub fn as_ref(&self) -> Smart<&T> { + match self { + Smart::Auto => Smart::Auto, + Smart::Custom(v) => Smart::Custom(v), + } + } + + /// Returns the contained custom value. + /// + /// If the value is [`Smart::Auto`], returns `None`. + /// + /// Equivalently, this just converts `Smart` to `Option`. + pub fn custom(self) -> Option { + match self { + Self::Auto => None, + Self::Custom(x) => Some(x), + } + } + + /// Map the contained custom value with `f`. + pub fn map(self, f: F) -> Smart + where + F: FnOnce(T) -> U, + { + match self { + Self::Auto => Smart::Auto, + Self::Custom(x) => Smart::Custom(f(x)), + } + } + + /// Map the contained custom value with `f` if it contains a custom value, + /// otherwise returns `default`. + pub fn map_or(self, default: U, f: F) -> U + where + F: FnOnce(T) -> U, + { + match self { + Self::Auto => default, + Self::Custom(x) => f(x), + } + } + + /// Keeps `self` if it contains a custom value, otherwise returns `other`. + pub fn or(self, other: Smart) -> Self { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => other, + } + } + + /// Keeps `self` if it contains a custom value, otherwise returns the + /// output of the given function. + pub fn or_else(self, f: F) -> Self + where + F: FnOnce() -> Self, + { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => f(), + } + } + + /// Returns `Auto` if `self` is `Auto`, otherwise calls the provided + /// function on the contained value and returns the result. + pub fn and_then(self, f: F) -> Smart + where + F: FnOnce(T) -> Smart, + { + match self { + Smart::Auto => Smart::Auto, + Smart::Custom(x) => f(x), + } + } + + /// Returns the contained custom value or a provided default value. + pub fn unwrap_or(self, default: T) -> T { + match self { + Self::Auto => default, + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or computes a default value. + pub fn unwrap_or_else(self, f: F) -> T + where + F: FnOnce() -> T, + { + match self { + Self::Auto => f(), + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or the default value. + pub fn unwrap_or_default(self) -> T + where + T: Default, + { + // we want to do this; the Clippy lint is not type-aware + #[allow(clippy::unwrap_or_default)] + self.unwrap_or_else(T::default) + } +} + +impl Smart> { + /// Removes a single level of nesting, returns `Auto` if the inner or outer value is `Auto`. + pub fn flatten(self) -> Smart { + match self { + Smart::Custom(Smart::Auto) | Smart::Auto => Smart::Auto, + Smart::Custom(Smart::Custom(v)) => Smart::Custom(v), + } + } +} + +impl Default for Smart { + fn default() -> Self { + Self::Auto + } +} + +impl Reflect for Smart { + fn input() -> CastInfo { + T::input() + AutoValue::input() + } + + fn output() -> CastInfo { + T::output() + AutoValue::output() + } + + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) + } +} + +impl IntoValue for Smart { + fn into_value(self) -> Value { + match self { + Smart::Custom(v) => v.into_value(), + Smart::Auto => Value::Auto, + } + } +} + +impl FromValue for Smart { + fn from_value(value: Value) -> HintedStrResult { + match value { + Value::Auto => Ok(Self::Auto), + v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} + +impl Resolve for Smart { + type Output = Smart; + + fn resolve(self, styles: StyleChain) -> Self::Output { + self.map(|v| v.resolve(styles)) + } +} + +impl Fold for Smart { + fn fold(self, outer: Self) -> Self { + use Smart::Custom; + match (self, outer) { + (Custom(inner), Custom(outer)) => Custom(inner.fold(outer)), + // An explicit `auto` should be respected, thus we don't do + // `inner.or(outer)`. + (inner, _) => inner, + } + } +} diff --git a/crates/typst-library/src/foundations/bool.rs b/crates/typst-library/src/foundations/bool.rs new file mode 100644 index 00000000..e88c8c6f --- /dev/null +++ b/crates/typst-library/src/foundations/bool.rs @@ -0,0 +1,26 @@ +use ecow::EcoString; + +use crate::foundations::{ty, Repr}; + +/// A type with two states. +/// +/// The boolean type has two values: `{true}` and `{false}`. It denotes whether +/// something is active or enabled. +/// +/// # Example +/// ```example +/// #false \ +/// #true \ +/// #(1 < 2) +/// ``` +#[ty(cast, title = "Boolean")] +type bool; + +impl Repr for bool { + fn repr(&self) -> EcoString { + match self { + true => "true".into(), + false => "false".into(), + } + } +} diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs new file mode 100644 index 00000000..05fe4763 --- /dev/null +++ b/crates/typst-library/src/foundations/bytes.rs @@ -0,0 +1,262 @@ +use std::borrow::Cow; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, AddAssign, Deref}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use typst_utils::LazyHash; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value}; + +/// A sequence of bytes. +/// +/// This is conceptually similar to an array of [integers]($int) between `{0}` +/// and `{255}`, but represented much more efficiently. You can iterate over it +/// using a [for loop]($scripting/#loops). +/// +/// You can convert +/// - a [string]($str) or an [array] of integers to bytes with the [`bytes`] +/// constructor +/// - bytes to a string with the [`str`] constructor, with UTF-8 encoding +/// - bytes to an array of integers with the [`array`] constructor +/// +/// When [reading]($read) data from a file, you can decide whether to load it +/// as a string or as raw bytes. +/// +/// ```example +/// #bytes((123, 160, 22, 0)) \ +/// #bytes("Hello 😃") +/// +/// #let data = read( +/// "rhino.png", +/// encoding: none, +/// ) +/// +/// // Magic bytes. +/// #array(data.slice(0, 4)) \ +/// #str(data.slice(1, 4)) +/// ``` +#[ty(scope, cast)] +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Bytes(Arc>>); + +impl Bytes { + /// Create a buffer from a static byte slice. + pub fn from_static(slice: &'static [u8]) -> Self { + Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) + } + + /// Return `true` if the length is 0. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return a view into the buffer. + pub fn as_slice(&self) -> &[u8] { + self + } + + /// Return a copy of the buffer as a vector. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + /// Resolve an index or throw an out of bounds error. + fn locate(&self, index: i64) -> StrResult { + self.locate_opt(index).ok_or_else(|| out_of_bounds(index, self.len())) + } + + /// Resolve an index, if it is within bounds. + /// + /// `index == len` is considered in bounds. + fn locate_opt(&self, index: i64) -> Option { + let wrapped = + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + + wrapped + .and_then(|v| usize::try_from(v).ok()) + .filter(|&v| v <= self.0.len()) + } +} + +#[scope] +impl Bytes { + /// Converts a value to bytes. + /// + /// - Strings are encoded in UTF-8. + /// - Arrays of integers between `{0}` and `{255}` are converted directly. The + /// dedicated byte representation is much more efficient than the array + /// representation and thus typically used for large byte buffers (e.g. image + /// data). + /// + /// ```example + /// #bytes("Hello 😃") \ + /// #bytes((123, 160, 22, 0)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The value that should be converted to bytes. + value: ToBytes, + ) -> Bytes { + value.0 + } + + /// The length in bytes. + #[func(title = "Length")] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the byte at the specified index. Returns the default value if + /// the index is out of bounds or fails with an error if no default value + /// was specified. + #[func] + pub fn at( + &self, + /// The index at which to retrieve the byte. + index: i64, + /// A default value to return if the index is out of bounds. + #[named] + default: Option, + ) -> StrResult { + self.locate_opt(index) + .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .or(default) + .ok_or_else(|| out_of_bounds_no_default(index, self.len())) + } + + /// Extracts a subslice of the bytes. Fails with an error if the start or end + /// index is out of bounds. + #[func] + pub fn slice( + &self, + /// The start index (inclusive). + start: i64, + /// The end index (exclusive). If omitted, the whole slice until the end + /// is extracted. + #[default] + end: Option, + /// The number of items to extract. This is equivalent to passing + /// `start + count` as the `end` position. Mutually exclusive with + /// `end`. + #[named] + count: Option, + ) -> StrResult { + let mut end = end; + if end.is_none() { + end = count.map(|c: i64| start + c); + } + let start = self.locate(start)?; + let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); + Ok(self.0[start..end].into()) + } +} + +impl Debug for Bytes { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Bytes({})", self.len()) + } +} + +impl Repr for Bytes { + fn repr(&self) -> EcoString { + eco_format!("bytes({})", self.len()) + } +} + +impl Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl From<&[u8]> for Bytes { + fn from(slice: &[u8]) -> Self { + Self(Arc::new(LazyHash::new(slice.to_vec().into()))) + } +} + +impl From> for Bytes { + fn from(vec: Vec) -> Self { + Self(Arc::new(LazyHash::new(vec.into()))) + } +} + +impl Add for Bytes { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self += rhs; + self + } +} + +impl AddAssign for Bytes { + fn add_assign(&mut self, rhs: Self) { + if rhs.is_empty() { + // Nothing to do + } else if self.is_empty() { + *self = rhs; + } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { + Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); + } else { + *self = Self::from([self.as_slice(), rhs.as_slice()].concat()); + } + } +} + +impl Serialize for Bytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&eco_format!("{self:?}")) + } else { + serializer.serialize_bytes(self) + } + } +} + +/// A value that can be cast to bytes. +pub struct ToBytes(Bytes); + +cast! { + ToBytes, + v: Str => Self(v.as_bytes().into()), + v: Array => Self(v.iter() + .map(|item| match item { + Value::Int(byte @ 0..=255) => Ok(*byte as u8), + Value::Int(_) => bail!("number must be between 0 and 255"), + value => Err(::error(value)), + }) + .collect::, _>>()? + .into() + ), + v: Bytes => Self(v), +} + +/// The out of bounds access error message. +#[cold] +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("byte index out of bounds (index: {index}, len: {len})") +} + +/// The out of bounds access error message when no default value was given. +#[cold] +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { + eco_format!( + "byte index out of bounds (index: {index}, len: {len}) \ + and no default value was specified", + ) +} diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs new file mode 100644 index 00000000..f12ca74c --- /dev/null +++ b/crates/typst-library/src/foundations/calc.rs @@ -0,0 +1,1211 @@ +//! Calculations and processing of numeric values. + +use std::cmp; +use std::cmp::Ordering; + +use az::SaturatingAs; +use typst_syntax::{Span, Spanned}; +use typst_utils::{round_int_with_precision, round_with_precision}; + +use crate::diag::{bail, At, HintedString, SourceResult, StrResult}; +use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value}; +use crate::layout::{Angle, Fr, Length, Ratio}; + +/// A module with calculation definitions. +pub fn module() -> Module { + let mut scope = Scope::new(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define_func::(); + scope.define("inf", f64::INFINITY); + scope.define("pi", std::f64::consts::PI); + scope.define("tau", std::f64::consts::TAU); + scope.define("e", std::f64::consts::E); + Module::new("calc", scope) +} + +/// Calculates the absolute value of a numeric value. +/// +/// ```example +/// #calc.abs(-5) \ +/// #calc.abs(5pt - 2cm) \ +/// #calc.abs(2fr) \ +/// #calc.abs(decimal("-342.440")) +/// ``` +#[func(title = "Absolute")] +pub fn abs( + /// The value whose absolute value to calculate. + value: ToAbs, +) -> Value { + value.0 +} + +/// A value of which the absolute value can be taken. +pub struct ToAbs(Value); + +cast! { + ToAbs, + v: i64 => Self(v.abs().into_value()), + v: f64 => Self(v.abs().into_value()), + v: Length => Self(Value::Length(v.try_abs() + .ok_or("cannot take absolute value of this length")?)), + v: Angle => Self(Value::Angle(v.abs())), + v: Ratio => Self(Value::Ratio(v.abs())), + v: Fr => Self(Value::Fraction(v.abs())), + v: Decimal => Self(Value::Decimal(v.abs())) +} + +/// Raises a value to some exponent. +/// +/// ```example +/// #calc.pow(2, 3) \ +/// #calc.pow(decimal("2.5"), 2) +/// ``` +#[func(title = "Power")] +pub fn pow( + /// The callsite span. + span: Span, + /// The base of the power. + /// + /// If this is a [`decimal`], the exponent can only be an [integer]($int). + base: DecNum, + /// The exponent of the power. + exponent: Spanned, +) -> SourceResult { + match exponent.v { + _ if exponent.v.float() == 0.0 && base.is_zero() => { + bail!(span, "zero to the power of zero is undefined") + } + Num::Int(i) if i32::try_from(i).is_err() => { + bail!(exponent.span, "exponent is too large") + } + Num::Float(f) if !f.is_normal() && f != 0.0 => { + bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") + } + _ => {} + }; + + match (base, exponent.v) { + (DecNum::Int(a), Num::Int(b)) if b >= 0 => a + .checked_pow(b as u32) + .map(DecNum::Int) + .ok_or_else(too_large) + .at(span), + (DecNum::Decimal(a), Num::Int(b)) => { + a.checked_powi(b).map(DecNum::Decimal).ok_or_else(too_large).at(span) + } + (a, b) => { + let Some(a) = a.float() else { + return Err(cant_apply_to_decimal_and_float()).at(span); + }; + + let result = if a == std::f64::consts::E { + b.float().exp() + } else if a == 2.0 { + b.float().exp2() + } else if let Num::Int(b) = b { + a.powi(b as i32) + } else { + a.powf(b.float()) + }; + + if result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(DecNum::Float(result)) + } + } +} + +/// Raises a value to some exponent of e. +/// +/// ```example +/// #calc.exp(1) +/// ``` +#[func(title = "Exponential")] +pub fn exp( + /// The callsite span. + span: Span, + /// The exponent of the power. + exponent: Spanned, +) -> SourceResult { + match exponent.v { + Num::Int(i) if i32::try_from(i).is_err() => { + bail!(exponent.span, "exponent is too large") + } + Num::Float(f) if !f.is_normal() && f != 0.0 => { + bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") + } + _ => {} + } + + let result = exponent.v.float().exp(); + if result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(result) +} + +/// Calculates the square root of a number. +/// +/// ```example +/// #calc.sqrt(16) \ +/// #calc.sqrt(2.5) +/// ``` +#[func(title = "Square Root")] +pub fn sqrt( + /// The number whose square root to calculate. Must be non-negative. + value: Spanned, +) -> SourceResult { + if value.v.float() < 0.0 { + bail!(value.span, "cannot take square root of negative number"); + } + Ok(value.v.float().sqrt()) +} + +/// Calculates the real nth root of a number. +/// +/// If the number is negative, then n must be odd. +/// +/// ```example +/// #calc.root(16.0, 4) \ +/// #calc.root(27.0, 3) +/// ``` +#[func] +pub fn root( + /// The expression to take the root of + radicand: f64, + /// Which root of the radicand to take + index: Spanned, +) -> SourceResult { + if index.v == 0 { + bail!(index.span, "cannot take the 0th root of a number"); + } else if radicand < 0.0 { + if index.v % 2 == 0 { + bail!( + index.span, + "negative numbers do not have a real nth root when n is even" + ); + } else { + Ok(-(-radicand).powf(1.0 / index.v as f64)) + } + } else { + Ok(radicand.powf(1.0 / index.v as f64)) + } +} + +/// Calculates the sine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.sin(1.5) \ +/// #calc.sin(90deg) +/// ``` +#[func(title = "Sine")] +pub fn sin( + /// The angle whose sine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.sin(), + AngleLike::Int(n) => (n as f64).sin(), + AngleLike::Float(n) => n.sin(), + } +} + +/// Calculates the cosine of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.cos(1.5) \ +/// #calc.cos(90deg) +/// ``` +#[func(title = "Cosine")] +pub fn cos( + /// The angle whose cosine to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.cos(), + AngleLike::Int(n) => (n as f64).cos(), + AngleLike::Float(n) => n.cos(), + } +} + +/// Calculates the tangent of an angle. +/// +/// When called with an integer or a float, they will be interpreted as +/// radians. +/// +/// ```example +/// #calc.tan(1.5) \ +/// #calc.tan(90deg) +/// ``` +#[func(title = "Tangent")] +pub fn tan( + /// The angle whose tangent to calculate. + angle: AngleLike, +) -> f64 { + match angle { + AngleLike::Angle(a) => a.tan(), + AngleLike::Int(n) => (n as f64).tan(), + AngleLike::Float(n) => n.tan(), + } +} + +/// Calculates the arcsine of a number. +/// +/// ```example +/// #calc.asin(0) \ +/// #calc.asin(1) +/// ``` +#[func(title = "Arcsine")] +pub fn asin( + /// The number whose arcsine to calculate. Must be between -1 and 1. + value: Spanned, +) -> SourceResult { + let val = value.v.float(); + if val < -1.0 || val > 1.0 { + bail!(value.span, "value must be between -1 and 1"); + } + Ok(Angle::rad(val.asin())) +} + +/// Calculates the arccosine of a number. +/// +/// ```example +/// #calc.acos(0) \ +/// #calc.acos(1) +/// ``` +#[func(title = "Arccosine")] +pub fn acos( + /// The number whose arcsine to calculate. Must be between -1 and 1. + value: Spanned, +) -> SourceResult { + let val = value.v.float(); + if val < -1.0 || val > 1.0 { + bail!(value.span, "value must be between -1 and 1"); + } + Ok(Angle::rad(val.acos())) +} + +/// Calculates the arctangent of a number. +/// +/// ```example +/// #calc.atan(0) \ +/// #calc.atan(1) +/// ``` +#[func(title = "Arctangent")] +pub fn atan( + /// The number whose arctangent to calculate. + value: Num, +) -> Angle { + Angle::rad(value.float().atan()) +} + +/// Calculates the four-quadrant arctangent of a coordinate. +/// +/// The arguments are `(x, y)`, not `(y, x)`. +/// +/// ```example +/// #calc.atan2(1, 1) \ +/// #calc.atan2(-2, -3) +/// ``` +#[func(title = "Four-quadrant Arctangent")] +pub fn atan2( + /// The X coordinate. + x: Num, + /// The Y coordinate. + y: Num, +) -> Angle { + Angle::rad(f64::atan2(y.float(), x.float())) +} + +/// Calculates the hyperbolic sine of a hyperbolic angle. +/// +/// ```example +/// #calc.sinh(0) \ +/// #calc.sinh(1.5) +/// ``` +#[func(title = "Hyperbolic Sine")] +pub fn sinh( + /// The hyperbolic angle whose hyperbolic sine to calculate. + value: f64, +) -> f64 { + value.sinh() +} + +/// Calculates the hyperbolic cosine of a hyperbolic angle. +/// +/// ```example +/// #calc.cosh(0) \ +/// #calc.cosh(1.5) +/// ``` +#[func(title = "Hyperbolic Cosine")] +pub fn cosh( + /// The hyperbolic angle whose hyperbolic cosine to calculate. + value: f64, +) -> f64 { + value.cosh() +} + +/// Calculates the hyperbolic tangent of an hyperbolic angle. +/// +/// ```example +/// #calc.tanh(0) \ +/// #calc.tanh(1.5) +/// ``` +#[func(title = "Hyperbolic Tangent")] +pub fn tanh( + /// The hyperbolic angle whose hyperbolic tangent to calculate. + value: f64, +) -> f64 { + value.tanh() +} + +/// Calculates the logarithm of a number. +/// +/// If the base is not specified, the logarithm is calculated in base 10. +/// +/// ```example +/// #calc.log(100) +/// ``` +#[func(title = "Logarithm")] +pub fn log( + /// The callsite span. + span: Span, + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned, + /// The base of the logarithm. May not be zero. + #[named] + #[default(Spanned::new(10.0, Span::detached()))] + base: Spanned, +) -> SourceResult { + let number = value.v.float(); + if number <= 0.0 { + bail!(value.span, "value must be strictly positive") + } + + if !base.v.is_normal() { + bail!(base.span, "base may not be zero, NaN, infinite, or subnormal") + } + + let result = if base.v == std::f64::consts::E { + number.ln() + } else if base.v == 2.0 { + number.log2() + } else if base.v == 10.0 { + number.log10() + } else { + number.log(base.v) + }; + + if result.is_infinite() || result.is_nan() { + bail!(span, "the result is not a real number") + } + + Ok(result) +} + +/// Calculates the natural logarithm of a number. +/// +/// ```example +/// #calc.ln(calc.e) +/// ``` +#[func(title = "Natural Logarithm")] +pub fn ln( + /// The callsite span. + span: Span, + /// The number whose logarithm to calculate. Must be strictly positive. + value: Spanned, +) -> SourceResult { + let number = value.v.float(); + if number <= 0.0 { + bail!(value.span, "value must be strictly positive") + } + + let result = number.ln(); + if result.is_infinite() { + bail!(span, "result close to -inf") + } + + Ok(result) +} + +/// Calculates the factorial of a number. +/// +/// ```example +/// #calc.fact(5) +/// ``` +#[func(title = "Factorial")] +pub fn fact( + /// The number whose factorial to calculate. Must be non-negative. + number: u64, +) -> StrResult { + Ok(fact_impl(1, number).ok_or_else(too_large)?) +} + +/// Calculates a permutation. +/// +/// Returns the `k`-permutation of `n`, or the number of ways to choose `k` +/// items from a set of `n` with regard to order. +/// +/// ```example +/// $ "perm"(n, k) &= n!/((n - k)!) \ +/// "perm"(5, 3) &= #calc.perm(5, 3) $ +/// ``` +#[func(title = "Permutation")] +pub fn perm( + /// The base number. Must be non-negative. + base: u64, + /// The number of permutations. Must be non-negative. + numbers: u64, +) -> StrResult { + // By convention. + if base < numbers { + return Ok(0); + } + + Ok(fact_impl(base - numbers + 1, base).ok_or_else(too_large)?) +} + +/// Calculates the product of a range of numbers. Used to calculate +/// permutations. Returns None if the result is larger than `i64::MAX` +fn fact_impl(start: u64, end: u64) -> Option { + // By convention + if end + 1 < start { + return Some(0); + } + + let real_start: u64 = cmp::max(1, start); + let mut count: u64 = 1; + for i in real_start..=end { + count = count.checked_mul(i)?; + } + + count.try_into().ok() +} + +/// Calculates a binomial coefficient. +/// +/// Returns the `k`-combination of `n`, or the number of ways to choose `k` +/// items from a set of `n` without regard to order. +/// +/// ```example +/// #calc.binom(10, 5) +/// ``` +#[func(title = "Binomial")] +pub fn binom( + /// The upper coefficient. Must be non-negative. + n: u64, + /// The lower coefficient. Must be non-negative. + k: u64, +) -> StrResult { + Ok(binom_impl(n, k).ok_or_else(too_large)?) +} + +/// Calculates a binomial coefficient, with `n` the upper coefficient and `k` +/// the lower coefficient. Returns `None` if the result is larger than +/// `i64::MAX` +fn binom_impl(n: u64, k: u64) -> Option { + if k > n { + return Some(0); + } + + // By symmetry + let real_k = cmp::min(n - k, k); + if real_k == 0 { + return Some(1); + } + + let mut result: u64 = 1; + for i in 0..real_k { + result = result.checked_mul(n - i)?.checked_div(i + 1)?; + } + + result.try_into().ok() +} + +/// Calculates the greatest common divisor of two integers. +/// +/// ```example +/// #calc.gcd(7, 42) +/// ``` +#[func(title = "Greatest Common Divisor")] +pub fn gcd( + /// The first integer. + a: i64, + /// The second integer. + b: i64, +) -> i64 { + let (mut a, mut b) = (a, b); + while b != 0 { + let temp = b; + b = a % b; + a = temp; + } + + a.abs() +} + +/// Calculates the least common multiple of two integers. +/// +/// ```example +/// #calc.lcm(96, 13) +/// ``` +#[func(title = "Least Common Multiple")] +pub fn lcm( + /// The first integer. + a: i64, + /// The second integer. + b: i64, +) -> StrResult { + if a == b { + return Ok(a.abs()); + } + + Ok(a.checked_div(gcd(a, b)) + .and_then(|gcd| gcd.checked_mul(b)) + .map(|v| v.abs()) + .ok_or_else(too_large)?) +} + +/// Rounds a number down to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.floor(500.1) +/// #assert(calc.floor(3) == 3) +/// #assert(calc.floor(3.14) == 3) +/// #assert(calc.floor(decimal("-3.14")) == -4) +/// ``` +#[func] +pub fn floor( + /// The number to round down. + value: DecNum, +) -> StrResult { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.floor()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.floor()).map_err(|_| too_large())?), + } +} + +/// Rounds a number up to the nearest integer. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.ceil(500.1) +/// #assert(calc.ceil(3) == 3) +/// #assert(calc.ceil(3.14) == 4) +/// #assert(calc.ceil(decimal("-3.14")) == -3) +/// ``` +#[func] +pub fn ceil( + /// The number to round up. + value: DecNum, +) -> StrResult { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.ceil()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.ceil()).map_err(|_| too_large())?), + } +} + +/// Returns the integer part of a number. +/// +/// If the number is already an integer, it is returned unchanged. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// #calc.trunc(15.9) +/// #assert(calc.trunc(3) == 3) +/// #assert(calc.trunc(-3.7) == -3) +/// #assert(calc.trunc(decimal("8493.12949582390")) == 8493) +/// ``` +#[func(title = "Truncate")] +pub fn trunc( + /// The number to truncate. + value: DecNum, +) -> StrResult { + match value { + DecNum::Int(n) => Ok(n), + DecNum::Float(n) => Ok(crate::foundations::convert_float_to_int(n.trunc()) + .map_err(|_| too_large())?), + DecNum::Decimal(n) => Ok(i64::try_from(n.trunc()).map_err(|_| too_large())?), + } +} + +/// Returns the fractional part of a number. +/// +/// If the number is an integer, returns `0`. +/// +/// ```example +/// #calc.fract(-3.1) +/// #assert(calc.fract(3) == 0) +/// #assert(calc.fract(decimal("234.23949211")) == decimal("0.23949211")) +/// ``` +#[func(title = "Fractional")] +pub fn fract( + /// The number to truncate. + value: DecNum, +) -> DecNum { + match value { + DecNum::Int(_) => DecNum::Int(0), + DecNum::Float(n) => DecNum::Float(n.fract()), + DecNum::Decimal(n) => DecNum::Decimal(n.fract()), + } +} + +/// Rounds a number to the nearest integer away from zero. +/// +/// Optionally, a number of decimal places can be specified. +/// +/// If the number of digits is negative, its absolute value will indicate the +/// amount of significant integer digits to remove before the decimal point. +/// +/// Note that this function will return the same type as the operand. That is, +/// applying `round` to a [`float`] will return a `float`, and to a [`decimal`], +/// another `decimal`. You may explicitly convert the output of this function to +/// an integer with [`int`], but note that such a conversion will error if the +/// `float` or `decimal` is larger than the maximum 64-bit signed integer or +/// smaller than the minimum integer. +/// +/// In addition, this function can error if there is an attempt to round beyond +/// the maximum or minimum integer or `decimal`. If the number is a `float`, +/// such an attempt will cause `{float.inf}` or `{-float.inf}` to be returned +/// for maximum and minimum respectively. +/// +/// ```example +/// #calc.round(3.1415, digits: 2) +/// #assert(calc.round(3) == 3) +/// #assert(calc.round(3.14) == 3) +/// #assert(calc.round(3.5) == 4.0) +/// #assert(calc.round(3333.45, digits: -2) == 3300.0) +/// #assert(calc.round(-48953.45, digits: -3) == -49000.0) +/// #assert(calc.round(3333, digits: -2) == 3300) +/// #assert(calc.round(-48953, digits: -3) == -49000) +/// #assert(calc.round(decimal("-6.5")) == decimal("-7")) +/// #assert(calc.round(decimal("7.123456789"), digits: 6) == decimal("7.123457")) +/// #assert(calc.round(decimal("3333.45"), digits: -2) == decimal("3300")) +/// #assert(calc.round(decimal("-48953.45"), digits: -3) == decimal("-49000")) +/// ``` +#[func] +pub fn round( + /// The number to round. + value: DecNum, + /// If positive, the number of decimal places. + /// + /// If negative, the number of significant integer digits that should be + /// removed before the decimal point. + #[named] + #[default(0)] + digits: i64, +) -> StrResult { + match value { + DecNum::Int(n) => Ok(DecNum::Int( + round_int_with_precision(n, digits.saturating_as::()) + .ok_or_else(too_large)?, + )), + DecNum::Float(n) => { + Ok(DecNum::Float(round_with_precision(n, digits.saturating_as::()))) + } + DecNum::Decimal(n) => Ok(DecNum::Decimal( + n.round(digits.saturating_as::()).ok_or_else(too_large)?, + )), + } +} + +/// Clamps a number between a minimum and maximum value. +/// +/// ```example +/// #calc.clamp(5, 0, 4) +/// #assert(calc.clamp(5, 0, 10) == 5) +/// #assert(calc.clamp(5, 6, 10) == 6) +/// #assert(calc.clamp(decimal("5.45"), 2, decimal("45.9")) == decimal("5.45")) +/// #assert(calc.clamp(decimal("5.45"), decimal("6.75"), 12) == decimal("6.75")) +/// ``` +#[func] +pub fn clamp( + /// The callsite span. + span: Span, + /// The number to clamp. + value: DecNum, + /// The inclusive minimum value. + min: DecNum, + /// The inclusive maximum value. + max: Spanned, +) -> SourceResult { + // Ignore if there are incompatible types (decimal and float) since that + // will cause `apply3` below to error before calling clamp, avoiding a + // panic. + if min + .apply2(max.v, |min, max| max < min, |min, max| max < min, |min, max| max < min) + .unwrap_or(false) + { + bail!(max.span, "max must be greater than or equal to min") + } + + value + .apply3(min, max.v, i64::clamp, f64::clamp, Decimal::clamp) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span) +} + +/// Determines the minimum of a sequence of values. +/// +/// ```example +/// #calc.min(1, -3, -5, 20, 3, 6) \ +/// #calc.min("typst", "is", "cool") +/// ``` +#[func(title = "Minimum")] +pub fn min( + /// The callsite span. + span: Span, + /// The sequence of values from which to extract the minimum. + /// Must not be empty. + #[variadic] + values: Vec>, +) -> SourceResult { + minmax(span, values, Ordering::Less) +} + +/// Determines the maximum of a sequence of values. +/// +/// ```example +/// #calc.max(1, -3, -5, 20, 3, 6) \ +/// #calc.max("typst", "is", "cool") +/// ``` +#[func(title = "Maximum")] +pub fn max( + /// The callsite span. + span: Span, + /// The sequence of values from which to extract the maximum. + /// Must not be empty. + #[variadic] + values: Vec>, +) -> SourceResult { + minmax(span, values, Ordering::Greater) +} + +/// Find the minimum or maximum of a sequence of values. +fn minmax( + span: Span, + values: Vec>, + goal: Ordering, +) -> SourceResult { + let mut iter = values.into_iter(); + let Some(Spanned { v: mut extremum, .. }) = iter.next() else { + bail!(span, "expected at least one value"); + }; + + for Spanned { v, span } in iter { + let ordering = ops::compare(&v, &extremum).at(span)?; + if ordering == goal { + extremum = v; + } + } + + Ok(extremum) +} + +/// Determines whether an integer is even. +/// +/// ```example +/// #calc.even(4) \ +/// #calc.even(5) \ +/// #range(10).filter(calc.even) +/// ``` +#[func] +pub fn even( + /// The number to check for evenness. + value: i64, +) -> bool { + value % 2 == 0 +} + +/// Determines whether an integer is odd. +/// +/// ```example +/// #calc.odd(4) \ +/// #calc.odd(5) \ +/// #range(10).filter(calc.odd) +/// ``` +#[func] +pub fn odd( + /// The number to check for oddness. + value: i64, +) -> bool { + value % 2 != 0 +} + +/// Calculates the remainder of two numbers. +/// +/// The value `calc.rem(x, y)` always has the same sign as `x`, and is smaller +/// in magnitude than `y`. +/// +/// This can error if given a [`decimal`] input and the dividend is too small in +/// magnitude compared to the divisor. +/// +/// ```example +/// #calc.rem(7, 3) \ +/// #calc.rem(7, -3) \ +/// #calc.rem(-7, 3) \ +/// #calc.rem(-7, -3) \ +/// #calc.rem(1.75, 0.5) +/// ``` +#[func(title = "Remainder")] +pub fn rem( + /// The span of the function call. + span: Span, + /// The dividend of the remainder. + dividend: DecNum, + /// The divisor of the remainder. + divisor: Spanned, +) -> SourceResult { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a % b)), + |a, b| Some(DecNum::Float(a % b)), + |a, b| a.checked_rem(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or("dividend too small compared to divisor") + .at(span) +} + +/// Performs euclidean division of two numbers. +/// +/// The result of this computation is that of a division rounded to the integer +/// `{n}` such that the dividend is greater than or equal to `{n}` times the divisor. +/// +/// ```example +/// #calc.div-euclid(7, 3) \ +/// #calc.div-euclid(7, -3) \ +/// #calc.div-euclid(-7, 3) \ +/// #calc.div-euclid(-7, -3) \ +/// #calc.div-euclid(1.75, 0.5) \ +/// #calc.div-euclid(decimal("1.75"), decimal("0.5")) +/// ``` +#[func(title = "Euclidean Division")] +pub fn div_euclid( + /// The callsite span. + span: Span, + /// The dividend of the division. + dividend: DecNum, + /// The divisor of the division. + divisor: Spanned, +) -> SourceResult { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a.div_euclid(b))), + |a, b| Some(DecNum::Float(a.div_euclid(b))), + |a, b| a.checked_div_euclid(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or_else(too_large) + .at(span) +} + +/// This calculates the least nonnegative remainder of a division. +/// +/// Warning: Due to a floating point round-off error, the remainder may equal +/// the absolute value of the divisor if the dividend is much smaller in +/// magnitude than the divisor and the dividend is negative. This only applies +/// for floating point inputs. +/// +/// In addition, this can error if given a [`decimal`] input and the dividend is +/// too small in magnitude compared to the divisor. +/// +/// ```example +/// #calc.rem-euclid(7, 3) \ +/// #calc.rem-euclid(7, -3) \ +/// #calc.rem-euclid(-7, 3) \ +/// #calc.rem-euclid(-7, -3) \ +/// #calc.rem-euclid(1.75, 0.5) \ +/// #calc.rem-euclid(decimal("1.75"), decimal("0.5")) +/// ``` +#[func(title = "Euclidean Remainder")] +pub fn rem_euclid( + /// The callsite span. + span: Span, + /// The dividend of the remainder. + dividend: DecNum, + /// The divisor of the remainder. + divisor: Spanned, +) -> SourceResult { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a.rem_euclid(b))), + |a, b| Some(DecNum::Float(a.rem_euclid(b))), + |a, b| a.checked_rem_euclid(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or("dividend too small compared to divisor") + .at(span) +} + +/// Calculates the quotient (floored division) of two numbers. +/// +/// Note that this function will always return an [integer]($int), and will +/// error if the resulting [`float`] or [`decimal`] is larger than the maximum +/// 64-bit signed integer or smaller than the minimum for that type. +/// +/// ```example +/// $ "quo"(a, b) &= floor(a/b) \ +/// "quo"(14, 5) &= #calc.quo(14, 5) \ +/// "quo"(3.46, 0.5) &= #calc.quo(3.46, 0.5) $ +/// ``` +#[func(title = "Quotient")] +pub fn quo( + /// The span of the function call. + span: Span, + /// The dividend of the quotient. + dividend: DecNum, + /// The divisor of the quotient. + divisor: Spanned, +) -> SourceResult { + if divisor.v.is_zero() { + bail!(divisor.span, "divisor must not be zero"); + } + + let divided = dividend + .apply2( + divisor.v, + |a, b| Some(DecNum::Int(a / b)), + |a, b| Some(DecNum::Float(a / b)), + |a, b| a.checked_div(b).map(DecNum::Decimal), + ) + .ok_or_else(cant_apply_to_decimal_and_float) + .at(span)? + .ok_or_else(too_large) + .at(span)?; + + floor(divided).at(span) +} + +/// A value which can be passed to functions that work with integers and floats. +#[derive(Debug, Copy, Clone)] +pub enum Num { + Int(i64), + Float(f64), +} + +impl Num { + fn float(self) -> f64 { + match self { + Self::Int(v) => v as f64, + Self::Float(v) => v, + } + } +} + +cast! { + Num, + self => match self { + Self::Int(v) => v.into_value(), + Self::Float(v) => v.into_value(), + }, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), +} + +/// A value which can be passed to functions that work with integers, floats, +/// and decimals. +#[derive(Debug, Copy, Clone)] +pub enum DecNum { + Int(i64), + Float(f64), + Decimal(Decimal), +} + +impl DecNum { + /// Checks if this number is equivalent to zero. + fn is_zero(self) -> bool { + match self { + Self::Int(i) => i == 0, + Self::Float(f) => f == 0.0, + Self::Decimal(d) => d.is_zero(), + } + } + + /// If this `DecNum` holds an integer or float, returns a float. + /// Otherwise, returns `None`. + fn float(self) -> Option { + match self { + Self::Int(i) => Some(i as f64), + Self::Float(f) => Some(f), + Self::Decimal(_) => None, + } + } + + /// If this `DecNum` holds an integer or decimal, returns a decimal. + /// Otherwise, returns `None`. + fn decimal(self) -> Option { + match self { + Self::Int(i) => Some(Decimal::from(i)), + Self::Float(_) => None, + Self::Decimal(d) => Some(d), + } + } + + /// Tries to apply a function to two decimal or numeric arguments. + /// + /// Fails with `None` if one is a float and the other is a decimal. + fn apply2( + self, + other: Self, + int: impl FnOnce(i64, i64) -> T, + float: impl FnOnce(f64, f64) -> T, + decimal: impl FnOnce(Decimal, Decimal) -> T, + ) -> Option { + match (self, other) { + (Self::Int(a), Self::Int(b)) => Some(int(a, b)), + (Self::Decimal(a), Self::Decimal(b)) => Some(decimal(a, b)), + (Self::Decimal(a), Self::Int(b)) => Some(decimal(a, Decimal::from(b))), + (Self::Int(a), Self::Decimal(b)) => Some(decimal(Decimal::from(a), b)), + (a, b) => Some(float(a.float()?, b.float()?)), + } + } + + /// Tries to apply a function to three decimal or numeric arguments. + /// + /// Fails with `None` if one is a float and the other is a decimal. + fn apply3( + self, + other: Self, + third: Self, + int: impl FnOnce(i64, i64, i64) -> i64, + float: impl FnOnce(f64, f64, f64) -> f64, + decimal: impl FnOnce(Decimal, Decimal, Decimal) -> Decimal, + ) -> Option { + match (self, other, third) { + (Self::Int(a), Self::Int(b), Self::Int(c)) => Some(Self::Int(int(a, b, c))), + (Self::Decimal(a), b, c) => { + Some(Self::Decimal(decimal(a, b.decimal()?, c.decimal()?))) + } + (a, Self::Decimal(b), c) => { + Some(Self::Decimal(decimal(a.decimal()?, b, c.decimal()?))) + } + (a, b, Self::Decimal(c)) => { + Some(Self::Decimal(decimal(a.decimal()?, b.decimal()?, c))) + } + (a, b, c) => Some(Self::Float(float(a.float()?, b.float()?, c.float()?))), + } + } +} + +cast! { + DecNum, + self => match self { + Self::Int(v) => v.into_value(), + Self::Float(v) => v.into_value(), + Self::Decimal(v) => v.into_value(), + }, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Decimal => Self::Decimal(v), +} + +/// A value that can be passed to a trigonometric function. +pub enum AngleLike { + Int(i64), + Float(f64), + Angle(Angle), +} + +cast! { + AngleLike, + v: i64 => Self::Int(v), + v: f64 => Self::Float(v), + v: Angle => Self::Angle(v), +} + +/// The error message when the result is too large to be represented. +#[cold] +fn too_large() -> &'static str { + "the result is too large" +} + +/// The hinted error message when trying to apply an operation to decimal and +/// float operands. +#[cold] +fn cant_apply_to_decimal_and_float() -> HintedString { + HintedString::new("cannot apply this operation to a decimal and a float".into()) + .with_hint( + "if loss of precision is acceptable, explicitly cast the \ + decimal to a float with `float(value)`", + ) +} diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs new file mode 100644 index 00000000..84f38f36 --- /dev/null +++ b/crates/typst-library/src/foundations/cast.rs @@ -0,0 +1,499 @@ +#[rustfmt::skip] +#[doc(inline)] +pub use typst_macros::{cast, Cast}; + +use std::borrow::Cow; +use std::fmt::Write; +use std::hash::Hash; +use std::ops::Add; + +use ecow::eco_format; +use smallvec::SmallVec; +use typst_syntax::{Span, Spanned}; +use unicode_math_class::MathClass; + +use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; +use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; + +/// Determine details of a type. +/// +/// Type casting works as follows: +/// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` +/// (for documentation and autocomplete). +/// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` +/// (infallible) +/// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` +/// (fallible). +/// +/// We can't use `TryFrom` due to conflicting impls. We could use +/// `From for Value`, but that inverses the impl and leads to tons of +/// `.into()` all over the place that become hard to decipher. +pub trait Reflect { + /// Describe what can be cast into this value. + fn input() -> CastInfo; + + /// Describe what this value can be cast into. + fn output() -> CastInfo; + + /// Whether the given value can be converted to `T`. + /// + /// This exists for performance. The check could also be done through the + /// [`CastInfo`], but it would be much more expensive (heap allocation + + /// dynamic checks instead of optimized machine code for each type). + fn castable(value: &Value) -> bool; + + /// Produce an error message for an unacceptable value type. + /// + /// ```ignore + /// assert_eq!( + /// ::error(&Value::None), + /// "expected integer, found none", + /// ); + /// ``` + fn error(found: &Value) -> HintedString { + Self::input().error(found) + } +} + +impl Reflect for Value { + fn input() -> CastInfo { + CastInfo::Any + } + + fn output() -> CastInfo { + CastInfo::Any + } + + fn castable(_: &Value) -> bool { + true + } +} + +impl Reflect for Spanned { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl Reflect for Packed { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl Reflect for StrResult { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl Reflect for HintedStrResult { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl Reflect for SourceResult { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl Reflect for &T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +impl Reflect for &mut T { + fn input() -> CastInfo { + T::input() + } + + fn output() -> CastInfo { + T::output() + } + + fn castable(value: &Value) -> bool { + T::castable(value) + } +} + +/// Cast a Rust type into a Typst [`Value`]. +/// +/// See also: [`Reflect`]. +pub trait IntoValue { + /// Cast this type into a value. + fn into_value(self) -> Value; +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} + +impl IntoValue for (&Str, &Value) { + fn into_value(self) -> Value { + Value::Array(array![self.0.clone(), self.1.clone()]) + } +} + +impl IntoValue for Cow<'_, T> { + fn into_value(self) -> Value { + self.into_owned().into_value() + } +} + +impl IntoValue for Packed { + fn into_value(self) -> Value { + Value::Content(self.pack()) + } +} + +impl IntoValue for Spanned { + fn into_value(self) -> Value { + self.v.into_value() + } +} + +/// Cast a Rust type or result into a [`SourceResult`]. +/// +/// Converts `T`, [`StrResult`], or [`SourceResult`] into +/// [`SourceResult`] by `Ok`-wrapping or adding span information. +pub trait IntoResult { + /// Cast this type into a value. + fn into_result(self, span: Span) -> SourceResult; +} + +impl IntoResult for T { + fn into_result(self, _: Span) -> SourceResult { + Ok(self.into_value()) + } +} + +impl IntoResult for StrResult { + fn into_result(self, span: Span) -> SourceResult { + self.map(IntoValue::into_value).at(span) + } +} + +impl IntoResult for HintedStrResult { + fn into_result(self, span: Span) -> SourceResult { + self.map(IntoValue::into_value).at(span) + } +} + +impl IntoResult for SourceResult { + fn into_result(self, _: Span) -> SourceResult { + self.map(IntoValue::into_value) + } +} + +impl IntoValue for fn() -> T { + fn into_value(self) -> Value { + self().into_value() + } +} + +/// Try to cast a Typst [`Value`] into a Rust type. +/// +/// See also: [`Reflect`]. +pub trait FromValue: Sized + Reflect { + /// Try to cast the value into an instance of `Self`. + fn from_value(value: V) -> HintedStrResult; +} + +impl FromValue for Value { + fn from_value(value: Value) -> HintedStrResult { + Ok(value) + } +} + +impl FromValue for Packed { + fn from_value(mut value: Value) -> HintedStrResult { + if let Value::Content(content) = value { + match content.into_packed::() { + Ok(packed) => return Ok(packed), + Err(content) => value = Value::Content(content), + } + } + let val = T::from_value(value)?; + Ok(Packed::new(val)) + } +} + +impl FromValue> for T { + fn from_value(value: Spanned) -> HintedStrResult { + T::from_value(value.v) + } +} + +impl FromValue> for Spanned { + fn from_value(value: Spanned) -> HintedStrResult { + let span = value.span; + T::from_value(value.v).map(|t| Spanned::new(t, span)) + } +} + +/// Describes a possible value for a cast. +#[derive(Debug, Clone, PartialEq, Hash, PartialOrd)] +pub enum CastInfo { + /// Any value is okay. + Any, + /// A specific value, plus short documentation for that value. + Value(Value, &'static str), + /// Any value of a type. + Type(Type), + /// Multiple alternatives. + Union(Vec), +} + +impl CastInfo { + /// Produce an error message describing what was expected and what was + /// found. + pub fn error(&self, found: &Value) -> HintedString { + let mut matching_type = false; + let mut parts = vec![]; + + self.walk(|info| match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr()); + if value.ty() == found.ty() { + matching_type = true; + } + } + CastInfo::Type(ty) => parts.push(eco_format!("{ty}")), + CastInfo::Union(_) => {} + }); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); + } + + msg.push_str(&repr::separated_list(&parts, "or")); + + if !matching_type { + msg.push_str(", found "); + write!(msg, "{}", found.ty()).unwrap(); + } + + let mut msg: HintedString = msg.into(); + + if let Value::Int(i) = found { + if !matching_type && parts.iter().any(|p| p == "length") { + msg.hint(eco_format!("a length needs a unit - did you mean {i}pt?")); + } + } else if let Value::Str(s) = found { + if !matching_type && parts.iter().any(|p| p == "label") { + if typst_syntax::is_valid_label_literal_id(s) { + msg.hint(eco_format!( + "use `<{s}>` or `label({})` to create a label", + s.repr() + )); + } else { + msg.hint(eco_format!("use `label({})` to create a label", s.repr())); + } + } + } else if let Value::Decimal(_) = found { + if !matching_type && parts.iter().any(|p| p == "float") { + msg.hint(eco_format!( + "if loss of precision is acceptable, explicitly cast the \ + decimal to a float with `float(value)`" + )); + } + } + + msg + } + + /// Walk all contained non-union infos. + pub fn walk(&self, mut f: F) + where + F: FnMut(&Self), + { + fn inner(info: &CastInfo, f: &mut F) + where + F: FnMut(&CastInfo), + { + if let CastInfo::Union(infos) = info { + for child in infos { + inner(child, f); + } + } else { + f(info); + } + } + + inner(self, &mut f) + } +} + +impl Add for CastInfo { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + for cast in rhs { + if !lhs.contains(&cast) { + lhs.push(cast); + } + } + lhs + } + (Self::Union(mut lhs), rhs) => { + if !lhs.contains(&rhs) { + lhs.push(rhs); + } + lhs + } + (lhs, Self::Union(mut rhs)) => { + if !rhs.contains(&lhs) { + rhs.insert(0, lhs); + } + rhs + } + (lhs, rhs) => vec![lhs, rhs], + }) + } +} + +/// A container for an argument. +pub trait Container { + /// The contained type. + type Inner; +} + +impl Container for Option { + type Inner = T; +} + +impl Container for Vec { + type Inner = T; +} + +impl Container for SmallVec<[T; N]> { + type Inner = T; +} + +/// An uninhabitable type. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Never {} + +impl Reflect for Never { + fn input() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn output() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn castable(_: &Value) -> bool { + false + } +} + +impl IntoValue for Never { + fn into_value(self) -> Value { + match self {} + } +} + +impl FromValue for Never { + fn from_value(value: Value) -> HintedStrResult { + Err(Self::error(&value)) + } +} + +cast! { + MathClass, + self => IntoValue::into_value(match self { + MathClass::Normal => "normal", + MathClass::Alphabetic => "alphabetic", + MathClass::Binary => "binary", + MathClass::Closing => "closing", + MathClass::Diacritic => "diacritic", + MathClass::Fence => "fence", + MathClass::GlyphPart => "glyph-part", + MathClass::Large => "large", + MathClass::Opening => "opening", + MathClass::Punctuation => "punctuation", + MathClass::Relation => "relation", + MathClass::Space => "space", + MathClass::Unary => "unary", + MathClass::Vary => "vary", + MathClass::Special => "special", + }), + /// The default class for non-special things. + "normal" => MathClass::Normal, + /// Punctuation, e.g. a comma. + "punctuation" => MathClass::Punctuation, + /// An opening delimiter, e.g. `(`. + "opening" => MathClass::Opening, + /// A closing delimiter, e.g. `)`. + "closing" => MathClass::Closing, + /// A delimiter that is the same on both sides, e.g. `|`. + "fence" => MathClass::Fence, + /// A large operator like `sum`. + "large" => MathClass::Large, + /// A relation like `=` or `prec`. + "relation" => MathClass::Relation, + /// A unary operator like `not`. + "unary" => MathClass::Unary, + /// A binary operator like `times`. + "binary" => MathClass::Binary, + /// An operator that can be both unary or binary like `+`. + "vary" => MathClass::Vary, +} diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs new file mode 100644 index 00000000..a274b8bf --- /dev/null +++ b/crates/typst-library/src/foundations/content.rs @@ -0,0 +1,1007 @@ +use std::any::TypeId; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::iter::{self, Sum}; +use std::marker::PhantomData; +use std::ops::{Add, AddAssign, Deref, DerefMut}; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::{eco_format, EcoString}; +use serde::{Serialize, Serializer}; +use smallvec::smallvec; +use typst_syntax::Span; +use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; + +use crate::diag::{SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label, + NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, + Value, +}; +use crate::introspection::Location; +use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; +use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; +use crate::text::UnderlineElem; + +/// A piece of document content. +/// +/// This type is at the heart of Typst. All markup you write and most +/// [functions]($function) you call produce content values. You can create a +/// content value by enclosing markup in square brackets. This is also how you +/// pass content to functions. +/// +/// # Example +/// ```example +/// Type of *Hello!* is +/// #type([*Hello!*]) +/// ``` +/// +/// Content can be added with the `+` operator, +/// [joined together]($scripting/#blocks) and multiplied with integers. Wherever +/// content is expected, you can also pass a [string]($str) or `{none}`. +/// +/// # Representation +/// Content consists of elements with fields. When constructing an element with +/// its _element function,_ you provide these fields as arguments and when you +/// have a content value, you can access its fields with [field access +/// syntax]($scripting/#field-access). +/// +/// Some fields are required: These must be provided when constructing an +/// element and as a consequence, they are always available through field access +/// on content of that type. Required fields are marked as such in the +/// documentation. +/// +/// Most fields are optional: Like required fields, they can be passed to the +/// element function to configure them for a single element. However, these can +/// also be configured with [set rules]($styling/#set-rules) to apply them to +/// all elements within a scope. Optional fields are only available with field +/// access syntax when they were explicitly passed to the element function, not +/// when they result from a set rule. +/// +/// Each element has a default appearance. However, you can also completely +/// customize its appearance with a [show rule]($styling/#show-rules). The show +/// rule is passed the element. It can access the element's field and produce +/// arbitrary content from it. +/// +/// In the web app, you can hover over a content variable to see exactly which +/// elements the content is composed of and what fields they have. +/// Alternatively, you can inspect the output of the [`repr`] function. +#[ty(scope, cast)] +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Content { + /// The partially element-dependent inner data. + inner: Arc>, + /// The element's source code location. + span: Span, +} + +/// The inner representation behind the `Arc`. +#[derive(Hash)] +struct Inner { + /// An optional label attached to the element. + label: Option