diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
| -rw-r--r-- | Cargo.lock | 207 | ||||
| -rw-r--r-- | Cargo.toml | 7 | ||||
| -rw-r--r-- | crates/typst-cli/Cargo.toml | 4 | ||||
| -rw-r--r-- | crates/typst-cli/src/main.rs | 3 | ||||
| -rw-r--r-- | crates/typst-cli/src/query.rs | 3 | ||||
| -rw-r--r-- | crates/typst-cli/src/timings.rs | 3 | ||||
| -rw-r--r-- | crates/typst-cli/src/world.rs | 4 | ||||
| -rw-r--r-- | crates/typst-eval/Cargo.toml | 32 | ||||
| -rw-r--r-- | crates/typst-eval/src/access.rs (renamed from crates/typst/src/eval/access.rs) | 10 | ||||
| -rw-r--r-- | crates/typst-eval/src/binding.rs (renamed from crates/typst/src/eval/binding.rs) | 8 | ||||
| -rw-r--r-- | crates/typst-eval/src/call.rs (renamed from crates/typst/src/eval/call.rs) | 38 | ||||
| -rw-r--r-- | crates/typst-eval/src/code.rs (renamed from crates/typst/src/eval/code.rs) | 13 | ||||
| -rw-r--r-- | crates/typst-eval/src/flow.rs (renamed from crates/typst/src/eval/flow.rs) | 10 | ||||
| -rw-r--r-- | crates/typst-eval/src/import.rs (renamed from crates/typst/src/eval/import.rs) | 29 | ||||
| -rw-r--r-- | crates/typst-eval/src/lib.rs (renamed from crates/typst/src/eval/mod.rs) | 42 | ||||
| -rw-r--r-- | crates/typst-eval/src/markup.rs (renamed from crates/typst/src/eval/markup.rs) | 18 | ||||
| -rw-r--r-- | crates/typst-eval/src/math.rs (renamed from crates/typst/src/eval/math.rs) | 17 | ||||
| -rw-r--r-- | crates/typst-eval/src/methods.rs (renamed from crates/typst/src/foundations/methods.rs) | 6 | ||||
| -rw-r--r-- | crates/typst-eval/src/ops.rs | 91 | ||||
| -rw-r--r-- | crates/typst-eval/src/rules.rs (renamed from crates/typst/src/eval/rules.rs) | 13 | ||||
| -rw-r--r-- | crates/typst-eval/src/vm.rs (renamed from crates/typst/src/eval/vm.rs) | 14 | ||||
| -rw-r--r-- | crates/typst-ide/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/typst-ide/src/analyze.rs | 5 | ||||
| -rw-r--r-- | crates/typst-ide/src/complete.rs | 1 | ||||
| -rw-r--r-- | crates/typst-ide/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/typst-ide/src/tooltip.rs | 2 | ||||
| -rw-r--r-- | crates/typst-kit/Cargo.toml | 5 | ||||
| -rw-r--r-- | crates/typst-kit/src/fonts.rs | 10 | ||||
| -rw-r--r-- | crates/typst-kit/src/package.rs | 4 | ||||
| -rw-r--r-- | crates/typst-layout/Cargo.toml | 43 | ||||
| -rw-r--r-- | crates/typst-layout/src/flow/block.rs | 401 | ||||
| -rw-r--r-- | crates/typst-layout/src/flow/collect.rs (renamed from crates/typst/src/layout/flow/collect.rs) | 54 | ||||
| -rw-r--r-- | crates/typst-layout/src/flow/compose.rs (renamed from crates/typst/src/layout/flow/compose.rs) | 31 | ||||
| -rw-r--r-- | crates/typst-layout/src/flow/distribute.rs (renamed from crates/typst/src/layout/flow/distribute.rs) | 11 | ||||
| -rw-r--r-- | crates/typst-layout/src/flow/mod.rs (renamed from crates/typst/src/layout/flow/mod.rs) | 78 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/cells.rs (renamed from crates/typst/src/layout/grid/cells.rs) | 332 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/layouter.rs (renamed from crates/typst/src/layout/grid/layout.rs) | 29 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/lines.rs (renamed from crates/typst/src/layout/grid/lines.rs) | 57 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/mod.rs | 416 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/repeated.rs (renamed from crates/typst/src/layout/grid/repeated.rs) | 32 | ||||
| -rw-r--r-- | crates/typst-layout/src/grid/rowspans.rs (renamed from crates/typst/src/layout/grid/rowspans.rs) | 80 | ||||
| -rw-r--r-- | crates/typst-layout/src/image.rs | 142 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/box.rs | 87 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/collect.rs (renamed from crates/typst/src/layout/inline/collect.rs) | 19 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/deco.rs | 213 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/finalize.rs (renamed from crates/typst/src/layout/inline/finalize.rs) | 5 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/line.rs (renamed from crates/typst/src/layout/inline/line.rs) | 19 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/linebreak.rs (renamed from crates/typst/src/layout/inline/linebreak.rs) | 10 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/mod.rs (renamed from crates/typst/src/layout/inline/mod.rs) | 26 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/prepare.rs (renamed from crates/typst/src/layout/inline/prepare.rs) | 8 | ||||
| -rw-r--r-- | crates/typst-layout/src/inline/shaping.rs (renamed from crates/typst/src/layout/inline/shaping.rs) | 20 | ||||
| -rw-r--r-- | crates/typst-layout/src/lib.rs | 30 | ||||
| -rw-r--r-- | crates/typst-layout/src/lists.rs | 146 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/accent.rs | 75 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/attach.rs (renamed from crates/typst/src/math/attach.rs) | 427 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/cancel.rs | 144 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/frac.rs (renamed from crates/typst/src/math/frac.rs) | 111 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/fragment.rs (renamed from crates/typst/src/math/fragment.rs) | 150 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/lr.rs | 135 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/mat.rs | 333 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/mod.rs | 703 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/root.rs (renamed from crates/typst/src/math/root.rs) | 68 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/run.rs (renamed from crates/typst/src/math/row.rs) | 60 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/shared.rs | 207 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/stretch.rs (renamed from crates/typst/src/math/stretch.rs) | 171 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/text.rs | 344 | ||||
| -rw-r--r-- | crates/typst-layout/src/math/underover.rs | 327 | ||||
| -rw-r--r-- | crates/typst-layout/src/pad.rs (renamed from crates/typst/src/layout/pad.rs) | 86 | ||||
| -rw-r--r-- | crates/typst-layout/src/pages/collect.rs (renamed from crates/typst/src/layout/pages/collect.rs) | 8 | ||||
| -rw-r--r-- | crates/typst-layout/src/pages/finalize.rs (renamed from crates/typst/src/layout/pages/finalize.rs) | 9 | ||||
| -rw-r--r-- | crates/typst-layout/src/pages/mod.rs (renamed from crates/typst/src/layout/pages/mod.rs) | 28 | ||||
| -rw-r--r-- | crates/typst-layout/src/pages/run.rs (renamed from crates/typst/src/layout/pages/run.rs) | 38 | ||||
| -rw-r--r-- | crates/typst-layout/src/repeat.rs | 60 | ||||
| -rw-r--r-- | crates/typst-layout/src/shapes.rs (renamed from crates/typst/src/visualize/shape.rs) | 897 | ||||
| -rw-r--r-- | crates/typst-layout/src/stack.rs (renamed from crates/typst/src/layout/stack.rs) | 104 | ||||
| -rw-r--r-- | crates/typst-layout/src/transforms.rs | 246 | ||||
| -rw-r--r-- | crates/typst-library/Cargo.toml | 72 | ||||
| -rw-r--r-- | crates/typst-library/src/diag.rs (renamed from crates/typst/src/diag.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/engine.rs (renamed from crates/typst/src/engine.rs) | 13 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/args.rs (renamed from crates/typst/src/foundations/args.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/array.rs (renamed from crates/typst/src/foundations/array.rs) | 7 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/auto.rs (renamed from crates/typst/src/foundations/auto.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/bool.rs (renamed from crates/typst/src/foundations/bool.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/bytes.rs (renamed from crates/typst/src/foundations/bytes.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/calc.rs (renamed from crates/typst/src/foundations/calc.rs) | 7 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/cast.rs (renamed from crates/typst/src/foundations/cast.rs) | 10 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/content.rs (renamed from crates/typst/src/foundations/content.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/context.rs (renamed from crates/typst/src/foundations/context.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/datetime.rs (renamed from crates/typst/src/foundations/datetime.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/decimal.rs (renamed from crates/typst/src/foundations/decimal.rs) | 8 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/dict.rs (renamed from crates/typst/src/foundations/dict.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/duration.rs (renamed from crates/typst/src/foundations/duration.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/element.rs (renamed from crates/typst/src/foundations/element.rs) | 7 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/fields.rs (renamed from crates/typst/src/foundations/fields.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/float.rs (renamed from crates/typst/src/foundations/float.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/func.rs (renamed from crates/typst/src/foundations/func.rs) | 13 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/int.rs (renamed from crates/typst/src/foundations/int.rs) | 21 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/label.rs (renamed from crates/typst/src/foundations/label.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/mod.rs (renamed from crates/typst/src/foundations/mod.rs) | 15 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/module.rs (renamed from crates/typst/src/foundations/module.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/none.rs (renamed from crates/typst/src/foundations/none.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/ops.rs (renamed from crates/typst/src/eval/ops.rs) | 92 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/plugin.rs (renamed from crates/typst/src/foundations/plugin.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/repr.rs (renamed from crates/typst/src/foundations/repr.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/scope.rs (renamed from crates/typst/src/foundations/scope.rs) | 12 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/selector.rs (renamed from crates/typst/src/foundations/selector.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/str.rs (renamed from crates/typst/src/foundations/str.rs) | 10 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/styles.rs (renamed from crates/typst/src/foundations/styles.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/symbol.rs (renamed from crates/typst/src/symbols/symbol.rs) | 8 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/sys.rs (renamed from crates/typst/src/foundations/sys.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/ty.rs (renamed from crates/typst/src/foundations/ty.rs) | 9 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/value.rs (renamed from crates/typst/src/foundations/value.rs) | 14 | ||||
| -rw-r--r-- | crates/typst-library/src/foundations/version.rs (renamed from crates/typst/src/foundations/version.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/counter.rs (renamed from crates/typst/src/introspection/counter.rs) | 8 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/here.rs (renamed from crates/typst/src/introspection/here.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/introspector.rs (renamed from crates/typst/src/introspection/introspector.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/locate.rs (renamed from crates/typst/src/introspection/locate.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/location.rs (renamed from crates/typst/src/introspection/location.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/locator.rs (renamed from crates/typst/src/introspection/locator.rs) | 6 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/metadata.rs (renamed from crates/typst/src/introspection/metadata.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/mod.rs (renamed from crates/typst/src/introspection/mod.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/query.rs (renamed from crates/typst/src/introspection/query.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/state.rs (renamed from crates/typst/src/introspection/state.rs) | 6 | ||||
| -rw-r--r-- | crates/typst-library/src/introspection/tag.rs (renamed from crates/typst/src/introspection/tag.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/abs.rs (renamed from crates/typst/src/layout/abs.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/align.rs (renamed from crates/typst/src/layout/align.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/angle.rs (renamed from crates/typst/src/layout/angle.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/axes.rs (renamed from crates/typst/src/layout/axes.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/columns.rs (renamed from crates/typst/src/layout/columns.rs) | 29 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/container.rs | 563 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/corners.rs (renamed from crates/typst/src/layout/corners.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/dir.rs (renamed from crates/typst/src/layout/dir.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/em.rs (renamed from crates/typst/src/layout/em.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/fr.rs (renamed from crates/typst/src/layout/fr.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/fragment.rs (renamed from crates/typst/src/layout/fragment.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/frame.rs (renamed from crates/typst/src/layout/frame.rs) | 37 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/grid.rs (renamed from crates/typst/src/layout/grid/mod.rs) | 491 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/hide.rs (renamed from crates/typst/src/layout/hide.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/layout.rs (renamed from crates/typst/src/layout/layout.rs) | 8 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/length.rs (renamed from crates/typst/src/layout/length.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/measure.rs (renamed from crates/typst/src/layout/measure.rs) | 6 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/mod.rs (renamed from crates/typst/src/layout/mod.rs) | 7 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/pad.rs | 65 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/page.rs (renamed from crates/typst/src/layout/page.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/place.rs (renamed from crates/typst/src/layout/place.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/point.rs (renamed from crates/typst/src/layout/point.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/ratio.rs (renamed from crates/typst/src/layout/ratio.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/regions.rs (renamed from crates/typst/src/layout/regions.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/rel.rs (renamed from crates/typst/src/layout/rel.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/repeat.rs | 49 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/sides.rs (renamed from crates/typst/src/layout/sides.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/size.rs (renamed from crates/typst/src/layout/size.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/spacing.rs (renamed from crates/typst/src/layout/spacing.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/stack.rs | 84 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/transform.rs (renamed from crates/typst/src/layout/transform.rs) | 268 | ||||
| -rw-r--r-- | crates/typst-library/src/lib.rs | 264 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/cbor.rs (renamed from crates/typst/src/loading/cbor.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/csv.rs (renamed from crates/typst/src/loading/csv.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/json.rs (renamed from crates/typst/src/loading/json.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/mod.rs (renamed from crates/typst/src/loading/mod.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/read.rs (renamed from crates/typst/src/loading/read.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/toml.rs (renamed from crates/typst/src/loading/toml.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/xml.rs (renamed from crates/typst/src/loading/xml.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/loading/yaml.rs (renamed from crates/typst/src/loading/yaml.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/math/accent.rs (renamed from crates/typst/src/math/accent.rs) | 174 | ||||
| -rw-r--r-- | crates/typst-library/src/math/attach.rs | 156 | ||||
| -rw-r--r-- | crates/typst-library/src/math/cancel.rs | 116 | ||||
| -rw-r--r-- | crates/typst-library/src/math/equation.rs | 256 | ||||
| -rw-r--r-- | crates/typst-library/src/math/frac.rs | 56 | ||||
| -rw-r--r-- | crates/typst-library/src/math/lr.rs | 135 | ||||
| -rw-r--r-- | crates/typst-library/src/math/matrix.rs (renamed from crates/typst/src/math/matrix.rs) | 358 | ||||
| -rw-r--r-- | crates/typst-library/src/math/mod.rs (renamed from crates/typst/src/math/mod.rs) | 89 | ||||
| -rw-r--r-- | crates/typst-library/src/math/op.rs (renamed from crates/typst/src/math/op.rs) | 32 | ||||
| -rw-r--r-- | crates/typst-library/src/math/root.rs | 35 | ||||
| -rw-r--r-- | crates/typst-library/src/math/style.rs | 254 | ||||
| -rw-r--r-- | crates/typst-library/src/math/underover.rs | 156 | ||||
| -rw-r--r-- | crates/typst-library/src/model/bibliography.rs (renamed from crates/typst/src/model/bibliography.rs) | 42 | ||||
| -rw-r--r-- | crates/typst-library/src/model/cite.rs (renamed from crates/typst/src/model/cite.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/model/document.rs (renamed from crates/typst/src/model/document.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/emph.rs (renamed from crates/typst/src/model/emph.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/enum.rs (renamed from crates/typst/src/model/enum.rs) | 106 | ||||
| -rw-r--r-- | crates/typst-library/src/model/figure.rs (renamed from crates/typst/src/model/figure.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/model/footnote.rs (renamed from crates/typst/src/model/footnote.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/model/heading.rs (renamed from crates/typst/src/model/heading.rs) | 18 | ||||
| -rw-r--r-- | crates/typst-library/src/model/link.rs (renamed from crates/typst/src/model/link.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/list.rs (renamed from crates/typst/src/model/list.rs) | 73 | ||||
| -rw-r--r-- | crates/typst-library/src/model/mod.rs (renamed from crates/typst/src/model/mod.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/numbering.rs (renamed from crates/typst/src/model/numbering.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/outline.rs (renamed from crates/typst/src/model/outline.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/model/par.rs (renamed from crates/typst/src/model/par.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/model/quote.rs (renamed from crates/typst/src/model/quote.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/reference.rs (renamed from crates/typst/src/model/reference.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/strong.rs (renamed from crates/typst/src/model/strong.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/model/table.rs (renamed from crates/typst/src/model/table.rs) | 224 | ||||
| -rw-r--r-- | crates/typst-library/src/model/terms.rs (renamed from crates/typst/src/model/terms.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/routines.rs | 368 | ||||
| -rw-r--r-- | crates/typst-library/src/symbols/emoji.rs (renamed from crates/typst/src/symbols/emoji.rs) | 5 | ||||
| -rw-r--r-- | crates/typst-library/src/symbols/mod.rs (renamed from crates/typst/src/symbols/mod.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/symbols/sym.rs (renamed from crates/typst/src/symbols/sym.rs) | 5 | ||||
| -rw-r--r-- | crates/typst-library/src/text/case.rs (renamed from crates/typst/src/text/case.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/text/deco.rs (renamed from crates/typst/src/text/deco.rs) | 223 | ||||
| -rw-r--r-- | crates/typst-library/src/text/font/book.rs (renamed from crates/typst/src/text/font/book.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/text/font/color.rs (renamed from crates/typst/src/text/font/color.rs) | 28 | ||||
| -rw-r--r-- | crates/typst-library/src/text/font/exceptions.rs (renamed from crates/typst/src/text/font/exceptions.rs) | 10 | ||||
| -rw-r--r-- | crates/typst-library/src/text/font/mod.rs (renamed from crates/typst/src/text/font/mod.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/text/font/variant.rs (renamed from crates/typst/src/text/font/variant.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/text/item.rs (renamed from crates/typst/src/text/item.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/text/lang.rs (renamed from crates/typst/src/text/lang.rs) | 5 | ||||
| -rw-r--r-- | crates/typst-library/src/text/linebreak.rs (renamed from crates/typst/src/text/linebreak.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/text/lorem.rs (renamed from crates/typst/src/text/lorem.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/text/mod.rs (renamed from crates/typst/src/text/mod.rs) | 10 | ||||
| -rw-r--r-- | crates/typst-library/src/text/raw.rs (renamed from crates/typst/src/text/raw.rs) | 12 | ||||
| -rw-r--r-- | crates/typst-library/src/text/shift.rs (renamed from crates/typst/src/text/shift.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/text/smallcaps.rs (renamed from crates/typst/src/text/smallcaps.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/text/smartquote.rs (renamed from crates/typst/src/text/smartquote.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/text/space.rs (renamed from crates/typst/src/text/space.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/color.rs (renamed from crates/typst/src/visualize/color.rs) | 4 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/gradient.rs (renamed from crates/typst/src/visualize/gradient.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/image/mod.rs (renamed from crates/typst/src/visualize/image/mod.rs) | 148 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/image/raster.rs (renamed from crates/typst/src/visualize/image/raster.rs) | 3 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/image/svg.rs (renamed from crates/typst/src/visualize/image/svg.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/line.rs (renamed from crates/typst/src/visualize/line.rs) | 46 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/mod.rs (renamed from crates/typst/src/visualize/mod.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/paint.rs (renamed from crates/typst/src/visualize/paint.rs) | 0 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/path.rs (renamed from crates/typst/src/visualize/path.rs) | 126 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/pattern.rs (renamed from crates/typst/src/visualize/pattern.rs) | 9 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/polygon.rs (renamed from crates/typst/src/visualize/polygon.rs) | 71 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/shape.rs | 448 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/stroke.rs (renamed from crates/typst/src/visualize/stroke.rs) | 2 | ||||
| -rw-r--r-- | crates/typst-library/translations/ar.txt (renamed from crates/typst/translations/ar.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/ca.txt (renamed from crates/typst/translations/ca.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/cs.txt (renamed from crates/typst/translations/cs.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/da.txt (renamed from crates/typst/translations/da.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/de.txt (renamed from crates/typst/translations/de.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/en.txt (renamed from crates/typst/translations/en.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/es.txt (renamed from crates/typst/translations/es.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/et.txt (renamed from crates/typst/translations/et.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/fi.txt (renamed from crates/typst/translations/fi.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/fr.txt (renamed from crates/typst/translations/fr.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/gl.txt (renamed from crates/typst/translations/gl.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/gr.txt (renamed from crates/typst/translations/gr.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/he.txt (renamed from crates/typst/translations/he.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/hu.txt (renamed from crates/typst/translations/hu.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/is.txt (renamed from crates/typst/translations/is.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/it.txt (renamed from crates/typst/translations/it.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/ja.txt (renamed from crates/typst/translations/ja.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/la.txt (renamed from crates/typst/translations/la.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/nb.txt (renamed from crates/typst/translations/nb.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/nl.txt (renamed from crates/typst/translations/nl.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/nn.txt (renamed from crates/typst/translations/nn.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/pl.txt (renamed from crates/typst/translations/pl.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/pt-PT.txt (renamed from crates/typst/translations/pt-PT.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/pt.txt (renamed from crates/typst/translations/pt.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/ro.txt (renamed from crates/typst/translations/ro.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/ru.txt (renamed from crates/typst/translations/ru.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/sl.txt (renamed from crates/typst/translations/sl.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/sq.txt (renamed from crates/typst/translations/sq.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/sr.txt (renamed from crates/typst/translations/sr.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/sv.txt (renamed from crates/typst/translations/sv.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/tl.txt (renamed from crates/typst/translations/tl.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/tr.txt (renamed from crates/typst/translations/tr.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/ua.txt (renamed from crates/typst/translations/ua.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/vi.txt (renamed from crates/typst/translations/vi.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/zh-TW.txt (renamed from crates/typst/translations/zh-TW.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-library/translations/zh.txt (renamed from crates/typst/translations/zh.txt) | 0 | ||||
| -rw-r--r-- | crates/typst-macros/src/cast.rs | 2 | ||||
| -rw-r--r-- | crates/typst-macros/src/elem.rs | 22 | ||||
| -rw-r--r-- | crates/typst-macros/src/func.rs | 1 | ||||
| -rw-r--r-- | crates/typst-macros/src/symbols.rs | 12 | ||||
| -rw-r--r-- | crates/typst-macros/src/time.rs | 12 | ||||
| -rw-r--r-- | crates/typst-macros/src/util.rs | 4 | ||||
| -rw-r--r-- | crates/typst-pdf/Cargo.toml | 7 | ||||
| -rw-r--r-- | crates/typst-pdf/src/catalog.rs | 10 | ||||
| -rw-r--r-- | crates/typst-pdf/src/color.rs | 6 | ||||
| -rw-r--r-- | crates/typst-pdf/src/color_font.rs | 13 | ||||
| -rw-r--r-- | crates/typst-pdf/src/content.rs | 18 | ||||
| -rw-r--r-- | crates/typst-pdf/src/extg.rs | 2 | ||||
| -rw-r--r-- | crates/typst-pdf/src/font.rs | 10 | ||||
| -rw-r--r-- | crates/typst-pdf/src/gradient.rs | 11 | ||||
| -rw-r--r-- | crates/typst-pdf/src/image.rs | 6 | ||||
| -rw-r--r-- | crates/typst-pdf/src/lib.rs | 18 | ||||
| -rw-r--r-- | crates/typst-pdf/src/named_destination.rs | 10 | ||||
| -rw-r--r-- | crates/typst-pdf/src/outline.rs | 6 | ||||
| -rw-r--r-- | crates/typst-pdf/src/page.rs | 17 | ||||
| -rw-r--r-- | crates/typst-pdf/src/pattern.rs | 11 | ||||
| -rw-r--r-- | crates/typst-pdf/src/resources.rs | 10 | ||||
| -rw-r--r-- | crates/typst-realize/Cargo.toml | 29 | ||||
| -rw-r--r-- | crates/typst-realize/src/lib.rs (renamed from crates/typst/src/realize.rs) | 64 | ||||
| -rw-r--r-- | crates/typst-render/Cargo.toml | 4 | ||||
| -rw-r--r-- | crates/typst-render/src/image.rs | 4 | ||||
| -rw-r--r-- | crates/typst-render/src/lib.rs | 6 | ||||
| -rw-r--r-- | crates/typst-render/src/paint.rs | 4 | ||||
| -rw-r--r-- | crates/typst-render/src/shape.rs | 4 | ||||
| -rw-r--r-- | crates/typst-render/src/text.rs | 8 | ||||
| -rw-r--r-- | crates/typst-svg/Cargo.toml | 3 | ||||
| -rw-r--r-- | crates/typst-svg/src/image.rs | 4 | ||||
| -rw-r--r-- | crates/typst-svg/src/lib.rs | 8 | ||||
| -rw-r--r-- | crates/typst-svg/src/paint.rs | 8 | ||||
| -rw-r--r-- | crates/typst-svg/src/shape.rs | 4 | ||||
| -rw-r--r-- | crates/typst-svg/src/text.rs | 8 | ||||
| -rw-r--r-- | crates/typst-syntax/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/typst-syntax/src/file.rs | 8 | ||||
| -rw-r--r-- | crates/typst-syntax/src/highlight.rs | 3 | ||||
| -rw-r--r-- | crates/typst-syntax/src/parser.rs | 3 | ||||
| -rw-r--r-- | crates/typst-syntax/src/source.rs | 2 | ||||
| -rw-r--r-- | crates/typst-syntax/src/span.rs | 14 | ||||
| -rw-r--r-- | crates/typst-timing/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/typst-timing/src/lib.rs | 41 | ||||
| -rw-r--r-- | crates/typst-utils/src/lib.rs | 6 | ||||
| -rw-r--r-- | crates/typst/Cargo.toml | 64 | ||||
| -rw-r--r-- | crates/typst/src/layout/container.rs | 1046 | ||||
| -rw-r--r-- | crates/typst/src/layout/repeat.rs | 107 | ||||
| -rw-r--r-- | crates/typst/src/lib.rs | 345 | ||||
| -rw-r--r-- | crates/typst/src/math/align.rs | 70 | ||||
| -rw-r--r-- | crates/typst/src/math/cancel.rs | 254 | ||||
| -rw-r--r-- | crates/typst/src/math/class.rs | 46 | ||||
| -rw-r--r-- | crates/typst/src/math/ctx.rs | 469 | ||||
| -rw-r--r-- | crates/typst/src/math/equation.rs | 576 | ||||
| -rw-r--r-- | crates/typst/src/math/lr.rs | 272 | ||||
| -rw-r--r-- | crates/typst/src/math/spacing.rs | 66 | ||||
| -rw-r--r-- | crates/typst/src/math/style.rs | 518 | ||||
| -rw-r--r-- | crates/typst/src/math/underover.rs | 496 | ||||
| -rw-r--r-- | docs/Cargo.toml | 7 | ||||
| -rw-r--r-- | docs/src/model.rs | 1 | ||||
| -rw-r--r-- | tests/Cargo.toml | 2 |
325 files changed, 9751 insertions, 8800 deletions
@@ -2668,68 +2668,16 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" name = "typst" version = "0.12.0" dependencies = [ - "arrayvec", - "az", - "bitflags 2.6.0", - "bumpalo", - "chinese-number", - "ciborium", "comemo", - "csv", "ecow", - "flate2", - "fontdb", - "hayagriva", - "hypher", - "icu_properties", - "icu_provider", - "icu_provider_adapters", - "icu_provider_blob", - "icu_segmenter", - "if_chain", - "image", - "indexmap 2.6.0", - "kamadak-exif", - "kurbo", - "lipsum", - "log", - "once_cell", - "palette", - "phf", - "png", - "portable-atomic", - "qcms", - "rayon", - "regex", - "roxmltree", - "rust_decimal", - "rustybuzz", - "serde", - "serde_json", - "serde_yaml 0.9.34+deprecated", - "siphasher 1.0.1", - "smallvec", - "stacker", - "syntect", - "time", - "toml", - "ttf-parser", - "two-face", - "typed-arena", - "typst-assets", - "typst-dev-assets", + "typst-eval", + "typst-layout", + "typst-library", "typst-macros", + "typst-realize", "typst-syntax", "typst-timing", "typst-utils", - "unicode-bidi", - "unicode-math-class", - "unicode-script", - "unicode-segmentation", - "unscanny", - "usvg", - "wasmi", - "xmlwriter", ] [[package]] @@ -2769,7 +2717,7 @@ dependencies = [ "tempfile", "toml", "typst", - "typst-assets", + "typst-eval", "typst-kit", "typst-macros", "typst-pdf", @@ -2791,7 +2739,6 @@ name = "typst-docs" version = "0.12.0" dependencies = [ "clap", - "comemo", "ecow", "heck", "once_cell", @@ -2810,6 +2757,24 @@ dependencies = [ ] [[package]] +name = "typst-eval" +version = "0.12.0" +dependencies = [ + "comemo", + "ecow", + "if_chain", + "indexmap 2.6.0", + "stacker", + "toml", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-segmentation", +] + +[[package]] name = "typst-fuzz" version = "0.12.0" dependencies = [ @@ -2834,6 +2799,7 @@ dependencies = [ "typst", "typst-assets", "typst-dev-assets", + "typst-eval", "unscanny", ] @@ -2850,14 +2816,104 @@ dependencies = [ "once_cell", "openssl", "tar", - "typst", "typst-assets", + "typst-library", + "typst-syntax", "typst-timing", "typst-utils", "ureq", ] [[package]] +name = "typst-layout" +version = "0.12.0" +dependencies = [ + "az", + "bumpalo", + "comemo", + "ecow", + "hypher", + "icu_properties", + "icu_provider", + "icu_provider_adapters", + "icu_provider_blob", + "icu_segmenter", + "kurbo", + "once_cell", + "rustybuzz", + "smallvec", + "ttf-parser", + "typst-assets", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-bidi", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "typst-library" +version = "0.12.0" +dependencies = [ + "az", + "bitflags 2.6.0", + "bumpalo", + "chinese-number", + "ciborium", + "comemo", + "csv", + "ecow", + "flate2", + "fontdb", + "hayagriva", + "icu_properties", + "icu_provider", + "icu_provider_blob", + "image", + "indexmap 2.6.0", + "kamadak-exif", + "kurbo", + "lipsum", + "once_cell", + "palette", + "phf", + "png", + "qcms", + "rayon", + "regex", + "roxmltree", + "rust_decimal", + "rustybuzz", + "serde", + "serde_json", + "serde_yaml 0.9.34+deprecated", + "siphasher 1.0.1", + "smallvec", + "syntect", + "time", + "toml", + "ttf-parser", + "two-face", + "typed-arena", + "typst-assets", + "typst-dev-assets", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-math-class", + "unicode-segmentation", + "unscanny", + "usvg", + "wasmi", + "xmlwriter", +] + +[[package]] name = "typst-macros" version = "0.12.0" dependencies = [ @@ -2885,15 +2941,33 @@ dependencies = [ "subsetter", "svg2pdf", "ttf-parser", - "typst", "typst-assets", + "typst-library", "typst-macros", + "typst-syntax", "typst-timing", - "unscanny", + "typst-utils", "xmp-writer", ] [[package]] +name = "typst-realize" +version = "0.12.0" +dependencies = [ + "arrayvec", + "bumpalo", + "comemo", + "ecow", + "once_cell", + "regex", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", +] + +[[package]] name = "typst-render" version = "0.12.0" dependencies = [ @@ -2902,13 +2976,11 @@ dependencies = [ "image", "pixglyph", "resvg", - "roxmltree", "tiny-skia", "ttf-parser", - "typst", + "typst-library", "typst-macros", "typst-timing", - "usvg", ] [[package]] @@ -2920,9 +2992,10 @@ dependencies = [ "ecow", "flate2", "ttf-parser", - "typst", + "typst-library", "typst-macros", "typst-timing", + "typst-utils", "xmlparser", "xmlwriter", ] @@ -2935,6 +3008,7 @@ dependencies = [ "once_cell", "serde", "toml", + "typst-timing", "typst-utils", "unicode-ident", "unicode-math-class", @@ -2956,10 +3030,10 @@ dependencies = [ "rayon", "regex", "tiny-skia", - "ttf-parser", "typst", "typst-assets", "typst-dev-assets", + "typst-library", "typst-pdf", "typst-render", "typst-svg", @@ -2974,7 +3048,6 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "typst-syntax", ] [[package]] @@ -18,10 +18,14 @@ readme = "README.md" [workspace.dependencies] typst = { path = "crates/typst", version = "0.12.0" } typst-cli = { path = "crates/typst-cli", version = "0.12.0" } +typst-eval = { path = "crates/typst-eval", version = "0.12.0" } typst-ide = { path = "crates/typst-ide", version = "0.12.0" } typst-kit = { path = "crates/typst-kit", version = "0.12.0" } +typst-layout = { path = "crates/typst-layout", version = "0.12.0" } +typst-library = { path = "crates/typst-library", version = "0.12.0" } typst-macros = { path = "crates/typst-macros", version = "0.12.0" } typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" } +typst-realize = { path = "crates/typst-realize", version = "0.12.0" } typst-render = { path = "crates/typst-render", version = "0.12.0" } typst-svg = { path = "crates/typst-svg", version = "0.12.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } @@ -145,5 +149,8 @@ strip = true [workspace.lints.clippy] blocks_in_conditions = "allow" +comparison_chain = "allow" +manual_range_contains = "allow" mutable_key_type = "allow" uninlined_format_args = "warn" +wildcard_in_or_patterns = "allow" diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 1c622d1e..855d7bee 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -19,7 +19,7 @@ doc = false [dependencies] typst = { workspace = true } -typst-assets = { workspace = true, features = ["fonts"] } +typst-eval = { workspace = true } typst-kit = { workspace = true } typst-macros = { workspace = true } typst-pdf = { workspace = true } @@ -28,8 +28,8 @@ typst-svg = { workspace = true } typst-timing = { workspace = true } chrono = { workspace = true } clap = { workspace = true } -color-print = { workspace = true } codespan-reporting = { workspace = true } +color-print = { workspace = true } comemo = { workspace = true } dirs = { workspace = true } ecow = { workspace = true } diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index b14e687e..464ad624 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -94,9 +94,10 @@ fn print_error(msg: &str) -> io::Result<()> { #[cfg(not(feature = "self-update"))] mod update { - use crate::args::UpdateCommand; use typst::diag::{bail, StrResult}; + use crate::args::UpdateCommand; + pub fn update(_: &UpdateCommand) -> StrResult<()> { bail!( "self-updating is not enabled for this executable, \ diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs index ef3c7951..90d99a5a 100644 --- a/crates/typst-cli/src/query.rs +++ b/crates/typst-cli/src/query.rs @@ -2,11 +2,11 @@ use comemo::Track; use ecow::{eco_format, EcoString}; use serde::Serialize; use typst::diag::{bail, HintedStrResult, StrResult, Warned}; -use typst::eval::{eval_string, EvalMode}; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::model::Document; use typst::syntax::Span; use typst::World; +use typst_eval::{eval_string, EvalMode}; use crate::args::{QueryCommand, SerializationFormat}; use crate::compile::print_diagnostics; @@ -56,6 +56,7 @@ fn retrieve( document: &Document, ) -> HintedStrResult<Vec<Content>> { let selector = eval_string( + &typst::ROUTINES, world.track(), &command.selector, Span::detached(), diff --git a/crates/typst-cli/src/timings.rs b/crates/typst-cli/src/timings.rs index df7665b7..4446bbf9 100644 --- a/crates/typst-cli/src/timings.rs +++ b/crates/typst-cli/src/timings.rs @@ -72,7 +72,8 @@ impl Timer { let writer = BufWriter::with_capacity(1 << 20, file); typst_timing::export_json(writer, |span| { - resolve_span(world, span).unwrap_or_else(|| ("unknown".to_string(), 0)) + resolve_span(world, Span::from_raw(span)) + .unwrap_or_else(|| ("unknown".to_string(), 0)) })?; Ok(output) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index bb491c02..b790f41a 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -16,7 +16,7 @@ use typst::utils::LazyHash; use typst::{Library, World}; use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::package::PackageStorage; -use typst_timing::{timed, TimingScope}; +use typst_timing::timed; use crate::args::{Input, SharedArgs}; use crate::compile::ExportCache; @@ -285,8 +285,6 @@ impl FileSlot { self.source.get_or_init( || read(self.id, project_root, package_storage), |data, prev| { - let name = if prev.is_some() { "reparsing file" } else { "parsing file" }; - let _scope = TimingScope::new(name, None); let text = decode_utf8(&data)?; if let Some(mut prev) = prev { prev.replace(text); diff --git a/crates/typst-eval/Cargo.toml b/crates/typst-eval/Cargo.toml new file mode 100644 index 00000000..12a6a6a4 --- /dev/null +++ b/crates/typst-eval/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "typst-eval" +description = "Typst's code interpreter." +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-library = { workspace = true } +typst-macros = { workspace = true } +typst-syntax = { workspace = true } +typst-timing = { workspace = true } +typst-utils = { workspace = true } +comemo = { workspace = true } +ecow = { workspace = true } +if_chain = { workspace = true } +indexmap = { workspace = true } +toml = { workspace = true } +unicode-segmentation = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +stacker = { workspace = true } + +[lints] +workspace = true diff --git a/crates/typst/src/eval/access.rs b/crates/typst-eval/src/access.rs index ab0a6491..9bcac4d6 100644 --- a/crates/typst/src/eval/access.rs +++ b/crates/typst-eval/src/access.rs @@ -1,9 +1,9 @@ use ecow::eco_format; +use typst_library::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint}; +use typst_library::foundations::{Dict, Value}; +use typst_syntax::ast::{self, AstNode}; -use crate::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint}; -use crate::eval::{Eval, Vm}; -use crate::foundations::{call_method_access, is_accessor_method, Dict, Value}; -use crate::syntax::ast::{self, AstNode}; +use crate::{call_method_access, is_accessor_method, Eval, Vm}; /// Access an expression mutably. pub(crate) trait Access { @@ -85,7 +85,7 @@ pub(crate) fn access_dict<'a>( Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_) ) { bail!(span, "cannot mutate fields on {ty}"); - } else if crate::foundations::fields_on(ty).is_empty() { + } else if typst_library::foundations::fields_on(ty).is_empty() { bail!(span, "{ty} does not have accessible fields"); } else { // type supports static fields, which don't yet have diff --git a/crates/typst/src/eval/binding.rs b/crates/typst-eval/src/binding.rs index 18468b9e..f3802f07 100644 --- a/crates/typst/src/eval/binding.rs +++ b/crates/typst-eval/src/binding.rs @@ -1,11 +1,11 @@ use std::collections::HashSet; use ecow::eco_format; +use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult}; +use typst_library::foundations::{Array, Dict, Value}; +use typst_syntax::ast::{self, AstNode}; -use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult}; -use crate::eval::{Access, Eval, Vm}; -use crate::foundations::{Array, Dict, Value}; -use crate::syntax::ast::{self, AstNode}; +use crate::{Access, Eval, Vm}; impl Eval for ast::LetBinding<'_> { type Output = Value; diff --git a/crates/typst/src/eval/call.rs b/crates/typst-eval/src/call.rs index 4331e187..9dfb7693 100644 --- a/crates/typst/src/eval/call.rs +++ b/crates/typst-eval/src/call.rs @@ -1,23 +1,24 @@ use comemo::{Tracked, TrackedMut}; use ecow::{eco_format, EcoString, EcoVec}; - -use crate::diag::{ +use typst_library::diag::{ bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult, Trace, Tracepoint, }; -use crate::engine::{Engine, Sink, Traced}; -use crate::eval::{Access, Eval, FlowEvent, Route, Vm}; -use crate::foundations::{ - call_method_mut, is_mutating_method, Arg, Args, Bytes, Capturer, Closure, Content, - Context, Func, IntoValue, NativeElement, Scope, Scopes, Value, +use typst_library::engine::{Engine, Sink, Traced}; +use typst_library::foundations::{ + Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, + NativeElement, Scope, Scopes, Value, }; -use crate::introspection::Introspector; -use crate::math::LrElem; -use crate::syntax::ast::{self, AstNode, Ident}; -use crate::syntax::{Span, Spanned, SyntaxNode}; -use crate::text::TextElem; -use crate::utils::LazyHash; -use crate::World; +use typst_library::introspection::Introspector; +use typst_library::math::LrElem; +use typst_library::routines::Routines; +use typst_library::text::TextElem; +use typst_library::World; +use typst_syntax::ast::{self, AstNode, Ident}; +use typst_syntax::{Span, Spanned, SyntaxNode}; +use typst_utils::LazyHash; + +use crate::{call_method_mut, is_mutating_method, Access, Eval, FlowEvent, Route, Vm}; impl Eval for ast::FuncCall<'_> { type Output = Value; @@ -159,9 +160,10 @@ impl Eval for ast::Closure<'_> { /// Call the function in the context with the arguments. #[comemo::memoize] #[allow(clippy::too_many_arguments)] -pub(crate) fn call_closure( +pub fn eval_closure( func: &Func, closure: &LazyHash<Closure>, + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -182,6 +184,7 @@ pub(crate) fn call_closure( // Prepare the engine. let engine = Engine { + routines, world, introspector, traced, @@ -210,7 +213,7 @@ pub(crate) fn call_closure( vm.define(ident, args.expect::<Value>(&ident)?) } pattern => { - crate::eval::destructure( + crate::destructure( &mut vm, pattern, args.expect::<Value>("pattern parameter")?, @@ -583,8 +586,9 @@ impl<'a> CapturesVisitor<'a> { #[cfg(test)] mod tests { + use typst_syntax::parse; + use super::*; - use crate::syntax::parse; #[track_caller] fn test(text: &str, result: &[&str]) { diff --git a/crates/typst/src/eval/code.rs b/crates/typst-eval/src/code.rs index 7cfb7f59..918d9d2a 100644 --- a/crates/typst/src/eval/code.rs +++ b/crates/typst-eval/src/code.rs @@ -1,11 +1,12 @@ use ecow::{eco_vec, EcoVec}; - -use crate::diag::{bail, error, At, SourceResult}; -use crate::eval::{ops, CapturesVisitor, Eval, Vm}; -use crate::foundations::{ - Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, Value, +use typst_library::diag::{bail, error, At, SourceResult}; +use typst_library::foundations::{ + ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, + Value, }; -use crate::syntax::ast::{self, AstNode}; +use typst_syntax::ast::{self, AstNode}; + +use crate::{CapturesVisitor, Eval, Vm}; impl Eval for ast::Code<'_> { type Output = Value; diff --git a/crates/typst/src/eval/flow.rs b/crates/typst-eval/src/flow.rs index a68be95b..5c9d2a00 100644 --- a/crates/typst/src/eval/flow.rs +++ b/crates/typst-eval/src/flow.rs @@ -1,10 +1,10 @@ +use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult}; +use typst_library::foundations::{ops, IntoValue, Value}; +use typst_syntax::ast::{self, AstNode}; +use typst_syntax::{Span, SyntaxKind, SyntaxNode}; use unicode_segmentation::UnicodeSegmentation; -use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult}; -use crate::eval::{destructure, ops, Eval, Vm}; -use crate::foundations::{IntoValue, Value}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::{Span, SyntaxKind, SyntaxNode}; +use crate::{destructure, Eval, Vm}; /// The maximum number of loop iterations. const MAX_ITERATIONS: usize = 10_000; diff --git a/crates/typst/src/eval/import.rs b/crates/typst-eval/src/import.rs index d02938cc..316fbf87 100644 --- a/crates/typst/src/eval/import.rs +++ b/crates/typst-eval/src/import.rs @@ -1,13 +1,15 @@ use comemo::TrackedMut; use ecow::{eco_format, eco_vec, EcoString}; +use typst_library::diag::{ + bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint, +}; +use typst_library::foundations::{Content, Module, Value}; +use typst_library::World; +use typst_syntax::ast::{self, AstNode}; +use typst_syntax::package::{PackageManifest, PackageSpec}; +use typst_syntax::{FileId, Span, VirtualPath}; -use crate::diag::{bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint}; -use crate::eval::{eval, Eval, Vm}; -use crate::foundations::{Content, Module, Value}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::package::{PackageManifest, PackageSpec}; -use crate::syntax::{FileId, Span, VirtualPath}; -use crate::World; +use crate::{eval, Eval, Vm}; impl Eval for ast::ModuleImport<'_> { type Output = Value; @@ -171,8 +173,9 @@ pub fn import( /// Import an external package. fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> { // Evaluate the manifest. + let world = vm.world(); let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); - let bytes = vm.world().file(manifest_id).at(span)?; + let bytes = world.file(manifest_id).at(span)?; let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; let manifest: PackageManifest = toml::from_str(string) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) @@ -181,7 +184,7 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo // Evaluate the entry point. let entrypoint_id = manifest_id.join(&manifest.package.entrypoint); - let source = vm.world().source(entrypoint_id).at(span)?; + let source = world.source(entrypoint_id).at(span)?; // Prevent cyclic importing. if vm.engine.route.contains(source.id()) { @@ -190,13 +193,14 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo let point = || Tracepoint::Import; Ok(eval( - vm.world(), + vm.engine.routines, + vm.engine.world, vm.engine.traced, TrackedMut::reborrow_mut(&mut vm.engine.sink), vm.engine.route.track(), &source, ) - .trace(vm.world(), point, span)? + .trace(world, point, span)? .with_name(manifest.package.name)) } @@ -215,7 +219,8 @@ fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> { // Evaluate the file. let point = || Tracepoint::Import; eval( - world, + vm.engine.routines, + vm.engine.world, vm.engine.traced, TrackedMut::reborrow_mut(&mut vm.engine.sink), vm.engine.route.track(), diff --git a/crates/typst/src/eval/mod.rs b/crates/typst-eval/src/lib.rs index dc8a1802..a5c0c7e3 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst-eval/src/lib.rs @@ -1,4 +1,4 @@ -//! Evaluation of markup and code. +//! Typst's code interpreter. pub(crate) mod ops; @@ -10,31 +10,35 @@ mod flow; mod import; mod markup; mod math; +mod methods; mod rules; mod vm; pub use self::call::*; pub use self::import::*; pub use self::vm::*; +pub use typst_library::routines::EvalMode; -pub(crate) use self::access::*; -pub(crate) use self::binding::*; -pub(crate) use self::flow::*; +use self::access::*; +use self::binding::*; +use self::flow::*; +use self::methods::*; use comemo::{Track, Tracked, TrackedMut}; - -use crate::diag::{bail, SourceResult}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Cast, Context, Module, NativeElement, Scope, Scopes, Value}; -use crate::introspection::Introspector; -use crate::math::EquationElem; -use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span}; -use crate::World; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Context, Module, NativeElement, Scope, Scopes, Value}; +use typst_library::introspection::Introspector; +use typst_library::math::EquationElem; +use typst_library::routines::Routines; +use typst_library::World; +use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span}; /// Evaluate a source file and return the resulting module. #[comemo::memoize] #[typst_macros::time(name = "eval", span = source.root().span())] pub fn eval( + routines: &Routines, world: Tracked<dyn World + '_>, traced: Tracked<Traced>, sink: TrackedMut<Sink>, @@ -50,6 +54,7 @@ pub fn eval( // Prepare the engine. let introspector = Introspector::default(); let engine = Engine { + routines, world, introspector: introspector.track(), traced, @@ -94,6 +99,7 @@ pub fn eval( /// Everything in the output is associated with the given `span`. #[comemo::memoize] pub fn eval_string( + routines: &Routines, world: Tracked<dyn World + '_>, string: &str, span: Span, @@ -119,6 +125,7 @@ pub fn eval_string( let introspector = Introspector::default(); let traced = Traced::default(); let engine = Engine { + routines, world, introspector: introspector.track(), traced: traced.track(), @@ -153,17 +160,6 @@ pub fn eval_string( Ok(output) } -/// In which mode to evaluate a string. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum EvalMode { - /// Evaluate as code, as after a hash. - Code, - /// Evaluate as markup, like in a Typst file. - Markup, - /// Evaluate as math, as in an equation. - Math, -} - /// Evaluate an expression. pub trait Eval { /// The output of evaluating the expression. diff --git a/crates/typst/src/eval/markup.rs b/crates/typst-eval/src/markup.rs index 42fede1c..e28eb9dd 100644 --- a/crates/typst/src/eval/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -1,18 +1,18 @@ -use crate::diag::{warning, At, SourceResult}; -use crate::eval::{Eval, Vm}; -use crate::foundations::{ - Content, Label, NativeElement, Repr, Smart, Unlabellable, Value, +use typst_library::diag::{warning, At, SourceResult}; +use typst_library::foundations::{ + Content, Label, NativeElement, Repr, Smart, Symbol, Unlabellable, Value, }; -use crate::math::EquationElem; -use crate::model::{ +use typst_library::math::EquationElem; +use typst_library::model::{ EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem, StrongElem, Supplement, TermItem, Url, }; -use crate::symbols::Symbol; -use crate::syntax::ast::{self, AstNode}; -use crate::text::{ +use typst_library::text::{ LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem, }; +use typst_syntax::ast::{self, AstNode}; + +use crate::{Eval, Vm}; impl Eval for ast::Markup<'_> { type Output = Content; diff --git a/crates/typst/src/eval/math.rs b/crates/typst-eval/src/math.rs index d1a667a5..c61a3251 100644 --- a/crates/typst/src/eval/math.rs +++ b/crates/typst-eval/src/math.rs @@ -1,12 +1,13 @@ use ecow::eco_format; - -use crate::diag::{At, SourceResult}; -use crate::eval::{Eval, Vm}; -use crate::foundations::{Content, NativeElement, Value}; -use crate::math::{AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem}; -use crate::symbols::Symbol; -use crate::syntax::ast::{self, AstNode}; -use crate::text::TextElem; +use typst_library::diag::{At, SourceResult}; +use typst_library::foundations::{Content, NativeElement, Symbol, Value}; +use typst_library::math::{ + AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, +}; +use typst_library::text::TextElem; +use typst_syntax::ast::{self, AstNode}; + +use crate::{Eval, Vm}; impl Eval for ast::Math<'_> { type Output = Content; diff --git a/crates/typst/src/foundations/methods.rs b/crates/typst-eval/src/methods.rs index 945b7c50..7cb36a00 100644 --- a/crates/typst/src/foundations/methods.rs +++ b/crates/typst-eval/src/methods.rs @@ -1,8 +1,8 @@ //! Handles special built-in methods on values. -use crate::diag::{At, SourceResult}; -use crate::foundations::{Args, Str, Type, Value}; -use crate::syntax::Span; +use typst_library::diag::{At, SourceResult}; +use typst_library::foundations::{Args, Str, Type, Value}; +use typst_syntax::Span; /// Whether a specific method is mutating. pub(crate) fn is_mutating_method(method: &str) -> bool { diff --git a/crates/typst-eval/src/ops.rs b/crates/typst-eval/src/ops.rs new file mode 100644 index 00000000..ebbd6743 --- /dev/null +++ b/crates/typst-eval/src/ops.rs @@ -0,0 +1,91 @@ +use typst_library::diag::{At, HintedStrResult, SourceResult}; +use typst_library::foundations::{ops, IntoValue, Value}; +use typst_syntax::ast::{self, AstNode}; + +use crate::{access_dict, Access, Eval, Vm}; + +impl Eval for ast::Unary<'_> { + type Output = Value; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.expr().eval(vm)?; + let result = match self.op() { + ast::UnOp::Pos => ops::pos(value), + ast::UnOp::Neg => ops::neg(value), + ast::UnOp::Not => ops::not(value), + }; + result.at(self.span()) + } +} + +impl Eval for ast::Binary<'_> { + type Output = Value; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + match self.op() { + ast::BinOp::Add => apply_binary(self, vm, ops::add), + ast::BinOp::Sub => apply_binary(self, vm, ops::sub), + ast::BinOp::Mul => apply_binary(self, vm, ops::mul), + ast::BinOp::Div => apply_binary(self, vm, ops::div), + ast::BinOp::And => apply_binary(self, vm, ops::and), + ast::BinOp::Or => apply_binary(self, vm, ops::or), + ast::BinOp::Eq => apply_binary(self, vm, ops::eq), + ast::BinOp::Neq => apply_binary(self, vm, ops::neq), + ast::BinOp::Lt => apply_binary(self, vm, ops::lt), + ast::BinOp::Leq => apply_binary(self, vm, ops::leq), + ast::BinOp::Gt => apply_binary(self, vm, ops::gt), + ast::BinOp::Geq => apply_binary(self, vm, ops::geq), + ast::BinOp::In => apply_binary(self, vm, ops::in_), + ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in), + ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)), + ast::BinOp::AddAssign => apply_assignment(self, vm, ops::add), + ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub), + ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul), + ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div), + } + } +} + +/// Apply a basic binary operation. +fn apply_binary( + binary: ast::Binary, + vm: &mut Vm, + op: fn(Value, Value) -> HintedStrResult<Value>, +) -> SourceResult<Value> { + let lhs = binary.lhs().eval(vm)?; + + // Short-circuit boolean operations. + if (binary.op() == ast::BinOp::And && lhs == false.into_value()) + || (binary.op() == ast::BinOp::Or && lhs == true.into_value()) + { + return Ok(lhs); + } + + let rhs = binary.rhs().eval(vm)?; + op(lhs, rhs).at(binary.span()) +} + +/// Apply an assignment operation. +fn apply_assignment( + binary: ast::Binary, + vm: &mut Vm, + op: fn(Value, Value) -> HintedStrResult<Value>, +) -> SourceResult<Value> { + let rhs = binary.rhs().eval(vm)?; + let lhs = binary.lhs(); + + // An assignment to a dictionary field is different from a normal access + // since it can create the field instead of just modifying it. + if binary.op() == ast::BinOp::Assign { + if let ast::Expr::FieldAccess(access) = lhs { + let dict = access_dict(vm, access)?; + dict.insert(access.field().get().clone().into(), rhs); + return Ok(Value::None); + } + } + + let location = binary.lhs().access(vm)?; + let lhs = std::mem::take(&mut *location); + *location = op(lhs, rhs).at(binary.span())?; + Ok(Value::None) +} diff --git a/crates/typst/src/eval/rules.rs b/crates/typst-eval/src/rules.rs index 1748bbd7..646354d4 100644 --- a/crates/typst/src/eval/rules.rs +++ b/crates/typst-eval/src/rules.rs @@ -1,11 +1,12 @@ -use crate::diag::{warning, At, SourceResult}; -use crate::eval::{Eval, Vm}; -use crate::foundations::{ +use typst_library::diag::{warning, At, SourceResult}; +use typst_library::foundations::{ Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation, }; -use crate::layout::BlockElem; -use crate::model::ParElem; -use crate::syntax::ast::{self, AstNode}; +use typst_library::layout::BlockElem; +use typst_library::model::ParElem; +use typst_syntax::ast::{self, AstNode}; + +use crate::{Eval, Vm}; impl Eval for ast::SetRule<'_> { type Output = Styles; diff --git a/crates/typst/src/eval/vm.rs b/crates/typst-eval/src/vm.rs index 4d346870..1c8331b6 100644 --- a/crates/typst/src/eval/vm.rs +++ b/crates/typst-eval/src/vm.rs @@ -1,15 +1,15 @@ use comemo::Tracked; +use typst_library::engine::Engine; +use typst_library::foundations::{Context, IntoValue, Scopes, Value}; +use typst_library::World; +use typst_syntax::ast::{self, AstNode}; +use typst_syntax::Span; -use crate::engine::Engine; -use crate::eval::FlowEvent; -use crate::foundations::{Context, IntoValue, Scopes, Value}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::Span; -use crate::World; +use crate::FlowEvent; /// A virtual machine. /// -/// Holds the state needed to [evaluate](crate::eval::eval()) Typst sources. A +/// Holds the state needed to [evaluate](crate::eval()) Typst sources. A /// new virtual machine is created for each module evaluation and function call. pub struct Vm<'a> { /// The underlying virtual typesetter. diff --git a/crates/typst-ide/Cargo.toml b/crates/typst-ide/Cargo.toml index 4e87f99b..3c98e9b8 100644 --- a/crates/typst-ide/Cargo.toml +++ b/crates/typst-ide/Cargo.toml @@ -14,6 +14,7 @@ readme = { workspace = true } [dependencies] typst = { workspace = true } +typst-eval = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } if_chain = { workspace = true } diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index c3779556..75ffaede 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -1,12 +1,12 @@ use comemo::Track; use ecow::{eco_vec, EcoString, EcoVec}; use typst::engine::{Engine, Route, Sink, Traced}; -use typst::eval::Vm; use typst::foundations::{Context, Label, Scopes, Styles, Value}; use typst::introspection::Introspector; use typst::model::{BibliographyElem, Document}; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use typst::World; +use typst_eval::Vm; /// Try to determine a set of possible values for an expression. pub fn analyze_expr( @@ -58,6 +58,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> { let traced = Traced::default(); let mut sink = Sink::new(); let engine = Engine { + routines: &typst::ROUTINES, world: world.track(), introspector: introspector.track(), traced: traced.track(), @@ -73,7 +74,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> { Span::detached(), ); - typst::eval::import(&mut vm, source, source_span, true) + typst_eval::import(&mut vm, source, source_span, true) .ok() .map(Value::Module) } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index d534f55c..cde4d1e3 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1353,7 +1353,6 @@ impl<'a> CompletionContext<'a> { #[cfg(test)] mod tests { - use super::autocomplete; use crate::tests::TestWorld; diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs index 63ba6f75..f09e6ac5 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -191,7 +191,7 @@ mod tests { // Set page width to 120pt with 10pt margins, so that the inner page is // exactly 100pt wide. Page height is unbounded and font size is 10pt so // that it multiplies to nice round numbers. - let mut lib = Library::default(); + let mut lib = typst::Library::default(); lib.styles .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); lib.styles.set(PageElem::set_height(Smart::Auto)); diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 532cda39..df93d1dc 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -3,13 +3,13 @@ use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use typst::engine::Sink; -use typst::eval::CapturesVisitor; use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; use typst::layout::Length; use typst::model::Document; use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; use typst::utils::{round_with_precision, Numeric}; use typst::World; +use typst_eval::CapturesVisitor; use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family}; diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 13d0a34b..266eba0b 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -11,13 +11,14 @@ license = { workspace = true } readme = { workspace = true } [dependencies] -typst = { workspace = true } typst-assets = { workspace = true, optional = true } +typst-library = { workspace = true } +typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } +dirs = { workspace = true, optional = true } ecow = { workspace = true } env_proxy = { workspace = true, optional = true } -dirs = { workspace = true, optional = true } flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } diff --git a/crates/typst-kit/src/fonts.rs b/crates/typst-kit/src/fonts.rs index 8c8981a1..83e13fd8 100644 --- a/crates/typst-kit/src/fonts.rs +++ b/crates/typst-kit/src/fonts.rs @@ -8,12 +8,12 @@ //! - For math: New Computer Modern Math //! - For code: Deja Vu Sans Mono -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; use std::sync::OnceLock; -use std::{fs, path::Path}; use fontdb::{Database, Source}; -use typst::text::{Font, FontBook, FontInfo}; +use typst_library::text::{Font, FontBook, FontInfo}; use typst_timing::TimingScope; /// Holds details about the location of a font and lazily the font itself. @@ -46,7 +46,7 @@ impl FontSlot { pub fn get(&self) -> Option<Font> { self.font .get_or_init(|| { - let _scope = TimingScope::new("load font", None); + let _scope = TimingScope::new("load font"); let data = fs::read( self.path .as_ref() @@ -196,7 +196,7 @@ impl FontSearcher { #[cfg(feature = "embed-fonts")] fn add_embedded(&mut self) { for data in typst_assets::fonts() { - let buffer = typst::foundations::Bytes::from_static(data); + let buffer = typst_library::foundations::Bytes::from_static(data); for (i, font) in Font::iter(buffer).enumerate() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index 83978010..412c7982 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -5,8 +5,8 @@ use std::path::{Path, PathBuf}; use ecow::eco_format; use once_cell::sync::OnceCell; -use typst::diag::{bail, PackageError, PackageResult, StrResult}; -use typst::syntax::package::{ +use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; +use typst_syntax::package::{ PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec, }; diff --git a/crates/typst-layout/Cargo.toml b/crates/typst-layout/Cargo.toml new file mode 100644 index 00000000..4c133a4e --- /dev/null +++ b/crates/typst-layout/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "typst-layout" +description = "Typst's layout engine." +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-library = { workspace = true } +typst-macros = { workspace = true } +typst-syntax = { workspace = true } +typst-timing = { workspace = true } +typst-utils = { workspace = true } +az = { workspace = true } +bumpalo = { workspace = true } +comemo = { workspace = true } +ecow = { workspace = true } +hypher = { workspace = true } +icu_properties = { workspace = true } +icu_provider = { workspace = true } +icu_provider_adapters = { workspace = true } +icu_provider_blob = { workspace = true } +icu_segmenter = { workspace = true } +kurbo = { workspace = true } +once_cell = { workspace = true } +rustybuzz = { workspace = true } +smallvec = { workspace = true } +ttf-parser = { workspace = true } +unicode-bidi = { workspace = true } +unicode-math-class = { workspace = true } +unicode-script = { workspace = true } +unicode-segmentation = { workspace = true } + +[lints] +workspace = true diff --git a/crates/typst-layout/src/flow/block.rs b/crates/typst-layout/src/flow/block.rs new file mode 100644 index 00000000..1dd98812 --- /dev/null +++ b/crates/typst-layout/src/flow/block.rs @@ -0,0 +1,401 @@ +use once_cell::unsync::Lazy; +use smallvec::SmallVec; +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::{Packed, Resolve, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Axes, BlockBody, BlockElem, Fragment, Frame, FrameKind, Region, Regions, Rel, + Sides, Size, Sizing, +}; +use typst_library::visualize::Stroke; +use typst_utils::Numeric; + +use crate::shapes::{clip_rect, fill_and_stroke}; + +/// Lay this out as an unbreakable block. +#[typst_macros::time(name = "block", span = elem.span())] +pub fn layout_single_block( + elem: &Packed<BlockElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + // Fetch sizing properties. + let width = elem.width(styles); + let height = elem.height(styles); + let inset = elem.inset(styles).unwrap_or_default(); + + // Build the pod regions. + let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size); + + // Layout the body. + let body = elem.body(styles); + let mut frame = match body { + // If we have no body, just create one frame. Its size will be + // adjusted below. + None => Frame::hard(Size::zero()), + + // If we have content as our body, just layout it. + Some(BlockBody::Content(body)) => { + crate::layout_frame(engine, body, locator.relayout(), styles, pod)? + } + + // If we have a child that wants to layout with just access to the + // base region, give it that. + Some(BlockBody::SingleLayouter(callback)) => { + callback.call(engine, locator, styles, pod)? + } + + // If we have a child that wants to layout with full region access, + // we layout it. + Some(BlockBody::MultiLayouter(callback)) => { + let expand = (pod.expand | region.expand) & pod.size.map(Abs::is_finite); + let pod = Region { expand, ..pod }; + callback.call(engine, locator, styles, pod.into())?.into_frame() + } + }; + + // Explicit blocks are boundaries for gradient relativeness. + if matches!(body, None | Some(BlockBody::Content(_))) { + frame.set_kind(FrameKind::Hard); + } + + // Enforce a correct frame size on the expanded axes. Do this before + // applying the inset, since the pod shrunk. + frame.set_size(pod.expand.select(pod.size, frame.size())); + + // Apply the inset. + if !inset.is_zero() { + crate::pad::grow(&mut frame, &inset); + } + + // Prepare fill and stroke. + let fill = elem.fill(styles); + let stroke = elem + .stroke(styles) + .unwrap_or_default() + .map(|s| s.map(Stroke::unwrap_or_default)); + + // Only fetch these if necessary (for clipping or filling/stroking). + let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default()); + let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default()); + + // Clip the contents, if requested. + if elem.clip(styles) { + let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); + frame.clip(clip_rect(size, &radius, &stroke)); + } + + // Add fill and/or stroke. + if fill.is_some() || stroke.iter().any(Option::is_some) { + fill_and_stroke(&mut frame, fill, &stroke, &outset, &radius, elem.span()); + } + + // Assign label to each frame in the fragment. + if let Some(label) = elem.label() { + frame.label(label); + } + + Ok(frame) +} + +/// Lay this out as a breakable block. +#[typst_macros::time(name = "block", span = elem.span())] +pub fn layout_multi_block( + elem: &Packed<BlockElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + // Fetch sizing properties. + let width = elem.width(styles); + let height = elem.height(styles); + let inset = elem.inset(styles).unwrap_or_default(); + + // Allocate a small vector for backlogs. + let mut buf = SmallVec::<[Abs; 2]>::new(); + + // Build the pod regions. + let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf); + + // Layout the body. + let body = elem.body(styles); + let mut fragment = match body { + // If we have no body, just create one frame plus one per backlog + // region. We create them zero-sized; if necessary, their size will + // be adjusted below. + None => { + let mut frames = vec![]; + frames.push(Frame::hard(Size::zero())); + if pod.expand.y { + let mut iter = pod; + while !iter.backlog.is_empty() { + frames.push(Frame::hard(Size::zero())); + iter.next(); + } + } + Fragment::frames(frames) + } + + // If we have content as our body, just layout it. + Some(BlockBody::Content(body)) => { + let mut fragment = + crate::layout_fragment(engine, body, locator.relayout(), styles, pod)?; + + // If the body is automatically sized and produced more than one + // fragment, ensure that the width was consistent across all + // regions. If it wasn't, we need to relayout with expansion. + if !pod.expand.x + && fragment + .as_slice() + .windows(2) + .any(|w| !w[0].width().approx_eq(w[1].width())) + { + let max_width = + fragment.iter().map(|frame| frame.width()).max().unwrap_or_default(); + let pod = Regions { + size: Size::new(max_width, pod.size.y), + expand: Axes::new(true, pod.expand.y), + ..pod + }; + fragment = crate::layout_fragment(engine, body, locator, styles, pod)?; + } + + fragment + } + + // If we have a child that wants to layout with just access to the + // base region, give it that. + Some(BlockBody::SingleLayouter(callback)) => { + let pod = Region::new(pod.base(), pod.expand); + callback.call(engine, locator, styles, pod).map(Fragment::frame)? + } + + // If we have a child that wants to layout with full region access, + // we layout it. + // + // For auto-sized multi-layouters, we propagate the outer expansion + // so that they can decide for themselves. We also ensure again to + // only expand if the size is finite. + Some(BlockBody::MultiLayouter(callback)) => { + let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite); + let pod = Regions { expand, ..pod }; + callback.call(engine, locator, styles, pod)? + } + }; + + // Prepare fill and stroke. + let fill = elem.fill(styles); + let stroke = elem + .stroke(styles) + .unwrap_or_default() + .map(|s| s.map(Stroke::unwrap_or_default)); + + // Only fetch these if necessary (for clipping or filling/stroking). + let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default()); + let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default()); + + // Fetch/compute these outside of the loop. + let clip = elem.clip(styles); + let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some); + let has_inset = !inset.is_zero(); + let is_explicit = matches!(body, None | Some(BlockBody::Content(_))); + + // Skip filling/stroking the first frame if it is empty and a non-empty + // one follows. + let mut skip_first = false; + if let [first, rest @ ..] = fragment.as_slice() { + skip_first = has_fill_or_stroke + && first.is_empty() + && rest.iter().any(|frame| !frame.is_empty()); + } + + // Post-process to apply insets, clipping, fills, and strokes. + for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() { + // Explicit blocks are boundaries for gradient relativeness. + if is_explicit { + frame.set_kind(FrameKind::Hard); + } + + // Enforce a correct frame size on the expanded axes. Do this before + // applying the inset, since the pod shrunk. + frame.set_size(pod.expand.select(region, frame.size())); + + // Apply the inset. + if has_inset { + crate::pad::grow(frame, &inset); + } + + // Clip the contents, if requested. + if clip { + let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); + frame.clip(clip_rect(size, &radius, &stroke)); + } + + // Add fill and/or stroke. + if has_fill_or_stroke && (i > 0 || !skip_first) { + fill_and_stroke(frame, fill.clone(), &stroke, &outset, &radius, elem.span()); + } + } + + // Assign label to each frame in the fragment. + if let Some(label) = elem.label() { + for frame in fragment.iter_mut() { + frame.label(label); + } + } + + Ok(fragment) +} + +/// Builds the pod region for an unbreakable sized container. +pub(crate) fn unbreakable_pod( + width: &Sizing, + height: &Sizing, + inset: &Sides<Rel<Abs>>, + styles: StyleChain, + base: Size, +) -> Region { + // Resolve the size. + let mut size = Size::new( + match width { + // - For auto, the whole region is available. + // - Fr is handled outside and already factored into the `region`, + // so we can treat it equivalently to 100%. + Sizing::Auto | Sizing::Fr(_) => base.x, + // Resolve the relative sizing. + Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), + }, + match height { + Sizing::Auto | Sizing::Fr(_) => base.y, + Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y), + }, + ); + + // Take the inset, if any, into account. + if !inset.is_zero() { + size = crate::pad::shrink(size, inset); + } + + // If the child is manually, the size is forced and we should enable + // expansion. + let expand = Axes::new( + *width != Sizing::Auto && size.x.is_finite(), + *height != Sizing::Auto && size.y.is_finite(), + ); + + Region::new(size, expand) +} + +/// Builds the pod regions for a breakable sized container. +fn breakable_pod<'a>( + width: &Sizing, + height: &Sizing, + inset: &Sides<Rel<Abs>>, + styles: StyleChain, + regions: Regions, + buf: &'a mut SmallVec<[Abs; 2]>, +) -> Regions<'a> { + let base = regions.base(); + + // The vertical region sizes we're about to build. + let first; + let full; + let backlog: &mut [Abs]; + let last; + + // If the block has a fixed height, things are very different, so we + // handle that case completely separately. + match height { + Sizing::Auto | Sizing::Fr(_) => { + // If the block is automatically sized, we can just inherit the + // regions. + first = regions.size.y; + full = regions.full; + buf.extend_from_slice(regions.backlog); + backlog = buf; + last = regions.last; + } + + Sizing::Rel(rel) => { + // Resolve the sizing to a concrete size. + let resolved = rel.resolve(styles).relative_to(base.y); + + // Since we're manually sized, the resolved size is the base height. + full = resolved; + + // Distribute the fixed height across a start region and a backlog. + (first, backlog) = distribute(resolved, regions, buf); + + // If the height is manually sized, we don't want a final repeatable + // region. + last = None; + } + }; + + // Resolve the horizontal sizing to a concrete width and combine + // `width` and `first` into `size`. + let mut size = Size::new( + match width { + Sizing::Auto | Sizing::Fr(_) => regions.size.x, + Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), + }, + first, + ); + + // Take the inset, if any, into account, applying it to the + // individual region components. + let (mut full, mut last) = (full, last); + if !inset.is_zero() { + crate::pad::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset); + } + + // If the child is manually, the size is forced and we should enable + // expansion. + let expand = Axes::new( + *width != Sizing::Auto && size.x.is_finite(), + *height != Sizing::Auto && size.y.is_finite(), + ); + + Regions { size, full, backlog, last, expand } +} + +/// Distribute a fixed height spread over existing regions into a new first +/// height and a new backlog. +fn distribute<'a>( + height: Abs, + mut regions: Regions, + buf: &'a mut SmallVec<[Abs; 2]>, +) -> (Abs, &'a mut [Abs]) { + // Build new region heights from old regions. + let mut remaining = height; + loop { + let limited = regions.size.y.clamp(Abs::zero(), remaining); + buf.push(limited); + remaining -= limited; + if remaining.approx_empty() + || !regions.may_break() + || (!regions.may_progress() && limited.approx_empty()) + { + break; + } + regions.next(); + } + + // If there is still something remaining, apply it to the + // last region (it will overflow, but there's nothing else + // we can do). + if !remaining.approx_empty() { + if let Some(last) = buf.last_mut() { + *last += remaining; + } + } + + // Distribute the heights to the first region and the + // backlog. There is no last region, since the height is + // fixed. + (buf[0], &mut buf[1..]) +} diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index ffb45fda..aee5d508 100644 --- a/crates/typst/src/layout/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -6,22 +6,23 @@ use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; use comemo::{Track, Tracked, TrackedMut}; use once_cell::unsync::Lazy; - -use crate::diag::{bail, SourceResult}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Packed, Resolve, Smart, StyleChain}; -use crate::introspection::{ +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; +use typst_library::introspection::{ Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem, }; -use crate::layout::{ - layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, - FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, - PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem, +use typst_library::layout::{ + Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, + Fr, Fragment, Frame, PagebreakElem, PlaceElem, PlacementScope, Ratio, Region, + Regions, Rel, Size, Sizing, Spacing, VElem, }; -use crate::model::ParElem; -use crate::realize::Pair; -use crate::text::TextElem; -use crate::World; +use typst_library::model::ParElem; +use typst_library::routines::{Pair, Routines}; +use typst_library::text::TextElem; +use typst_library::World; + +use super::{layout_multi_block, layout_single_block}; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. @@ -110,7 +111,7 @@ impl<'a> Collector<'a, '_, '_> { let spacing = ParElem::spacing_in(styles); let costs = TextElem::costs_in(styles); - let lines = crate::layout::layout_inline( + let lines = crate::layout_inline( self.engine, &elem.children, self.locator.next(&elem.span()), @@ -332,6 +333,7 @@ impl SingleChild<'_> { // Vertical expansion is only kept if this block is the only child. region.expand.y &= self.alone; layout_single_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -350,6 +352,7 @@ impl SingleChild<'_> { #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_single_impl( + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -363,6 +366,7 @@ fn layout_single_impl( let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { + routines, world, introspector, traced, @@ -370,7 +374,7 @@ fn layout_single_impl( route: Route::extend(route), }; - elem.layout_single(&mut engine, locator, styles, region) + layout_single_block(elem, &mut engine, locator, styles, region) .map(|frame| frame.post_processed(styles)) } @@ -425,6 +429,7 @@ impl<'a> MultiChild<'a> { // Vertical expansion is only kept if this block is the only child. regions.expand.y &= self.alone; layout_multi_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -443,6 +448,7 @@ impl<'a> MultiChild<'a> { #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_multi_impl( + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -456,6 +462,7 @@ fn layout_multi_impl( let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { + routines, world, introspector, traced, @@ -463,13 +470,12 @@ fn layout_multi_impl( route: Route::extend(route), }; - elem.layout_multiple(&mut engine, locator, styles, regions) - .map(|mut fragment| { - for frame in &mut fragment { - frame.post_process(styles); - } - fragment - }) + layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { + for frame in &mut fragment { + frame.post_process(styles); + } + fragment + }) } /// The spilled remains of a `MultiChild` that broke across two regions. @@ -571,7 +577,7 @@ impl PlacedChild<'_> { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); - let mut frame = layout_frame( + let mut frame = crate::layout_frame( engine, &self.elem.body, self.locator.relayout(), @@ -614,7 +620,7 @@ impl<T> CachedCell<T> { T: Clone, F: FnOnce(I) -> T, { - let input_hash = crate::utils::hash128(&input); + let input_hash = typst_utils::hash128(&input); let mut slot = self.0.borrow_mut(); if let Some((hash, output)) = &*slot { diff --git a/crates/typst/src/layout/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index a262d5c1..932ccc9a 100644 --- a/crates/typst/src/layout/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -1,22 +1,23 @@ use std::num::NonZeroUsize; -use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart}; -use crate::introspection::{ +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, NativeElement, Packed, Resolve, Smart}; +use typst_library::introspection::{ Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator, SplitLocator, Tag, }; -use crate::layout::{ - layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame, - FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size, +use typst_library::layout::{ + Abs, Axes, Dir, FixedAlignment, Fragment, Frame, FrameItem, OuterHAlignment, + PlacementScope, Point, Region, Regions, Rel, Size, }; -use crate::model::{ +use typst_library::model::{ FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker, }; -use crate::syntax::Span; -use crate::utils::NonZeroExt; +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; /// Composes the contents of a single page/region. A region can have multiple /// columns/subregions. @@ -517,7 +518,7 @@ fn layout_footnote_separator( config: &Config, base: Size, ) -> SourceResult<Frame> { - layout_frame( + crate::layout_frame( engine, &config.footnote.separator, Locator::root(), @@ -534,7 +535,7 @@ fn layout_footnote( pod: Regions, ) -> SourceResult<Fragment> { let loc = elem.location().unwrap(); - layout_fragment( + crate::layout_fragment( engine, &FootnoteEntry::new(elem.clone()).pack(), Locator::synthesize(loc), @@ -785,7 +786,7 @@ fn layout_line_number_reset( let counter = Counter::of(ParLineMarker::elem()); let update = CounterUpdate::Set(CounterState::init(false)); let content = counter.update(Span::detached(), update); - layout_frame( + crate::layout_frame( engine, &content, locator.next(&()), @@ -821,7 +822,7 @@ fn layout_line_number( ]); // Layout the number. - let mut frame = layout_frame( + let mut frame = crate::layout_frame( engine, &content, locator.next(&()), diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst-layout/src/flow/distribute.rs index 3fa16626..1852f7ca 100644 --- a/crates/typst/src/layout/flow/distribute.rs +++ b/crates/typst-layout/src/flow/distribute.rs @@ -1,12 +1,13 @@ +use typst_library::introspection::Tag; +use typst_library::layout::{ + Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size, +}; +use typst_utils::Numeric; + use super::{ Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild, Stop, Work, }; -use crate::introspection::Tag; -use crate::layout::{ - Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size, -}; -use crate::utils::Numeric; /// Distributes as many children as fit from `composer.work` into the first /// region and returns the resulting frame. diff --git a/crates/typst/src/layout/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 66ec8e97..7cbec59a 100644 --- a/crates/typst/src/layout/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -1,9 +1,12 @@ //! Layout of content into a [`Frame`] or [`Fragment`]. +mod block; mod collect; mod compose; mod distribute; +pub(crate) use self::block::unbreakable_pod; + use std::collections::HashSet; use std::num::NonZeroUsize; use std::rc::Rc; @@ -11,26 +14,40 @@ use std::rc::Rc; use bumpalo::Bump; use comemo::{Track, Tracked, TrackedMut}; use ecow::EcoVec; +use typst_library::diag::{bail, At, SourceDiagnostic, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; +use typst_library::introspection::{ + Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, +}; +use typst_library::layout::{ + Abs, ColumnsElem, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region, + Regions, Rel, Size, +}; +use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; +use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::text::TextElem; +use typst_library::World; +use typst_utils::{NonZeroExt, Numeric}; +use self::block::{layout_multi_block, layout_single_block}; use self::collect::{ collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild, }; use self::compose::{compose, Composer}; use self::distribute::distribute; -use crate::diag::{bail, At, SourceDiagnostic, SourceResult}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Content, Packed, Resolve, StyleChain}; -use crate::introspection::{ - Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, -}; -use crate::layout::{ - Abs, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region, Regions, Rel, Size, -}; -use crate::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; -use crate::realize::{realize, Arenas, Pair, RealizationKind}; -use crate::text::TextElem; -use crate::utils::{NonZeroExt, Numeric}; -use crate::World; + +/// Lays out content into a single region, producing a single frame. +pub fn layout_frame( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + layout_fragment(engine, content, locator, styles, region.into()) + .map(Fragment::into_frame) +} /// Lays out content into multiple regions. /// @@ -43,6 +60,7 @@ pub fn layout_fragment( regions: Regions, ) -> SourceResult<Fragment> { layout_fragment_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -57,50 +75,39 @@ pub fn layout_fragment( ) } -/// Lays out content into regions with columns. +/// Layout the columns. /// /// This is different from just laying out into column-sized regions as the /// columns can interact due to parent-scoped placed elements. -pub fn layout_fragment_with_columns( +#[typst_macros::time(span = elem.span())] +pub fn layout_columns( + elem: &Packed<ColumnsElem>, engine: &mut Engine, - content: &Content, locator: Locator, styles: StyleChain, regions: Regions, - count: NonZeroUsize, - gutter: Rel<Abs>, ) -> SourceResult<Fragment> { layout_fragment_impl( + engine.routines, engine.world, engine.introspector, engine.traced, TrackedMut::reborrow_mut(&mut engine.sink), engine.route.track(), - content, + &elem.body, locator.track(), styles, regions, - count, - gutter, + elem.count(styles), + elem.gutter(styles), ) } -/// Lays out content into a single region, producing a single frame. -pub fn layout_frame( - engine: &mut Engine, - content: &Content, - locator: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - layout_fragment(engine, content, locator, styles, region.into()) - .map(Fragment::into_frame) -} - /// The cached, internal implementation of [`layout_fragment`]. #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_fragment_impl( + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -123,6 +130,7 @@ fn layout_fragment_impl( let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); let mut engine = Engine { + routines, world, introspector, traced, @@ -133,7 +141,7 @@ fn layout_fragment_impl( engine.route.check_layout_depth().at(content.span())?; let arenas = Arenas::default(); - let children = realize( + let children = (engine.routines.realize)( RealizationKind::Container, &mut engine, &mut locator, diff --git a/crates/typst/src/layout/grid/cells.rs b/crates/typst-layout/src/grid/cells.rs index 64234aaf..175e2183 100644 --- a/crates/typst/src/layout/grid/cells.rs +++ b/crates/typst-layout/src/grid/cells.rs @@ -1,161 +1,89 @@ use std::num::NonZeroUsize; use std::sync::Arc; -use comemo::Track; use ecow::eco_format; - -use super::lines::Line; -use super::repeated::{Footer, Header, Repeatable}; -use crate::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect, - Resolve, Smart, StyleChain, Value, -}; -use crate::introspection::Locator; -use crate::layout::{ - layout_fragment, Abs, Alignment, Axes, Fragment, Length, LinePosition, Regions, Rel, - Sides, Sizing, +use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides, + Sizing, }; -use crate::syntax::Span; -use crate::utils::NonZeroExt; -use crate::visualize::{Paint, Stroke}; - -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled<T> { - /// A bare value, the same for all cells. - Value(T), - /// A closure mapping from cell coordinates to a value. - Func(Func), - /// An array of alignment values corresponding to each column. - Array(Vec<T>), -} +use typst_library::visualize::{Paint, Stroke}; +use typst_syntax::Span; +use typst_utils::NonZeroExt; -impl<T: Default + Clone + FromValue> Celled<T> { - /// Resolve the value based on the cell position. - pub fn resolve( - &self, - engine: &mut Engine, - styles: StyleChain, +use super::{Footer, Header, Line, Repeatable}; + +/// Used for cell-like elements which are aware of their final properties in +/// the table, and may have property overrides. +pub trait ResolvableCell { + /// Resolves the cell's fields, given its coordinates and default grid-wide + /// fill, align, inset and stroke properties, plus the expected value of + /// the `breakable` field. + /// Returns a final Cell. + #[allow(clippy::too_many_arguments)] + fn resolve_cell<'a>( + self, x: usize, y: usize, - ) -> SourceResult<T> { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func) => func - .call(engine, Context::new(None, Some(styles)).track(), [x, y])? - .cast() - .at(func.span())?, - Self::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } -} - -impl<T: Default> Default for Celled<T> { - fn default() -> Self { - Self::Value(T::default()) - } -} - -impl<T: Reflect> Reflect for Celled<T> { - fn input() -> CastInfo { - T::input() + Array::input() + Func::input() - } - - fn output() -> CastInfo { - T::output() + Array::output() + Func::output() - } - - fn castable(value: &Value) -> bool { - Array::castable(value) || Func::castable(value) || T::castable(value) - } -} + fill: &Option<Paint>, + align: Smart<Alignment>, + inset: Sides<Option<Rel<Length>>>, + stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a>; -impl<T: IntoValue> IntoValue for Celled<T> { - fn into_value(self) -> Value { - match self { - Self::Value(value) => value.into_value(), - Self::Func(func) => func.into_value(), - Self::Array(arr) => arr.into_value(), - } - } -} + /// Returns this cell's column override. + fn x(&self, styles: StyleChain) -> Smart<usize>; -impl<T: FromValue> FromValue for Celled<T> { - fn from_value(value: Value) -> HintedStrResult<Self> { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => Ok(Self::Array( - array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?, - )), - v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), - v => Err(Self::error(&v)), - } - } -} + /// Returns this cell's row override. + fn y(&self, styles: StyleChain) -> Smart<usize>; -impl<T: Fold> Fold for Celled<T> { - fn fold(self, outer: Self) -> Self { - match (self, outer) { - (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)), - (self_, _) => self_, - } - } -} + /// The amount of columns spanned by this cell. + fn colspan(&self, styles: StyleChain) -> NonZeroUsize; -impl<T: Resolve> Resolve for Celled<T> { - type Output = ResolvedCelled<T>; + /// The amount of rows spanned by this cell. + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize; - fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))), - Self::Func(func) => ResolvedCelled(Celled::Func(func)), - Self::Array(values) => ResolvedCelled(Celled::Array( - values.into_iter().map(|value| value.resolve(styles)).collect(), - )), - } - } + /// The cell's span, for errors. + fn span(&self) -> Span; } -/// The result of resolving a Celled's value according to styles. -/// Holds resolved values which depend on each grid cell's position. -/// When it is a closure, however, it is only resolved when the closure is -/// called. -#[derive(Default, Clone)] -pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>); - -impl<T> ResolvedCelled<T> -where - T: FromValue + Resolve, - <T as Resolve>::Output: Default + Clone, -{ - /// Resolve the value based on the cell position. - pub fn resolve( - &self, - engine: &mut Engine, - styles: StyleChain, - x: usize, - y: usize, - ) -> SourceResult<T::Output> { - Ok(match &self.0 { - Celled::Value(value) => value.clone(), - Celled::Func(func) => func - .call(engine, Context::new(None, Some(styles)).track(), [x, y])? - .cast::<T>() - .at(func.span())? - .resolve(styles), - Celled::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } +/// A grid item, possibly affected by automatic cell positioning. Can be either +/// a line or a cell. +pub enum ResolvableGridItem<T: ResolvableCell> { + /// A horizontal line in the grid. + HLine { + /// The row above which the horizontal line is drawn. + y: Smart<usize>, + start: usize, + end: Option<NonZeroUsize>, + stroke: Option<Arc<Stroke<Abs>>>, + /// The span of the corresponding line element. + span: Span, + /// The line's position. "before" here means on top of row `y`, while + /// "after" means below it. + position: LinePosition, + }, + /// A vertical line in the grid. + VLine { + /// The column before which the vertical line is drawn. + x: Smart<usize>, + start: usize, + end: Option<NonZeroUsize>, + stroke: Option<Arc<Stroke<Abs>>>, + /// The span of the corresponding line element. + span: Span, + /// The line's position. "before" here means to the left of column `x`, + /// while "after" means to its right (both considering LTR). + position: LinePosition, + }, + /// A cell in the grid. + Cell(T), } /// Represents a cell in CellGrid, to be laid out by GridLayouter. @@ -221,12 +149,24 @@ impl<'a> Cell<'a> { if disambiguator > 0 { locator = locator.split().next_inner(disambiguator as u128); } - layout_fragment(engine, &self.body, locator, styles, regions) + crate::layout_fragment(engine, &self.body, locator, styles, regions) } } +/// Indicates whether the line should be drawn before or after the track with +/// its index. This is mostly only relevant when gutter is used, since, then, +/// the position after a track is not the same as before the next +/// non-gutter track. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum LinePosition { + /// The line should be drawn before its track (e.g. hline on top of a row). + Before, + /// The line should be drawn after its track (e.g. hline below a row). + After, +} + /// A grid entry. -pub(super) enum Entry<'a> { +pub enum Entry<'a> { /// An entry which holds a cell. Cell(Cell<'a>), /// An entry which is merged with another cell. @@ -246,39 +186,6 @@ impl<'a> Entry<'a> { } } -/// A grid item, possibly affected by automatic cell positioning. Can be either -/// a line or a cell. -pub enum ResolvableGridItem<T: ResolvableCell> { - /// A horizontal line in the grid. - HLine { - /// The row above which the horizontal line is drawn. - y: Smart<usize>, - start: usize, - end: Option<NonZeroUsize>, - stroke: Option<Arc<Stroke<Abs>>>, - /// The span of the corresponding line element. - span: Span, - /// The line's position. "before" here means on top of row `y`, while - /// "after" means below it. - position: LinePosition, - }, - /// A vertical line in the grid. - VLine { - /// The column before which the vertical line is drawn. - x: Smart<usize>, - start: usize, - end: Option<NonZeroUsize>, - stroke: Option<Arc<Stroke<Abs>>>, - /// The span of the corresponding line element. - span: Span, - /// The line's position. "before" here means to the left of column `x`, - /// while "after" means to its right (both considering LTR). - position: LinePosition, - }, - /// A cell in the grid. - Cell(T), -} - /// Any grid child, which can be either a header or an item. pub enum ResolvableGridChild<T: ResolvableCell, I> { Header { repeat: bool, span: Span, items: I }, @@ -286,65 +193,28 @@ pub enum ResolvableGridChild<T: ResolvableCell, I> { Item(ResolvableGridItem<T>), } -/// Used for cell-like elements which are aware of their final properties in -/// the table, and may have property overrides. -pub trait ResolvableCell { - /// Resolves the cell's fields, given its coordinates and default grid-wide - /// fill, align, inset and stroke properties, plus the expected value of - /// the `breakable` field. - /// Returns a final Cell. - #[allow(clippy::too_many_arguments)] - fn resolve_cell<'a>( - self, - x: usize, - y: usize, - fill: &Option<Paint>, - align: Smart<Alignment>, - inset: Sides<Option<Rel<Length>>>, - stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a>; - - /// Returns this cell's column override. - fn x(&self, styles: StyleChain) -> Smart<usize>; - - /// Returns this cell's row override. - fn y(&self, styles: StyleChain) -> Smart<usize>; - - /// The amount of columns spanned by this cell. - fn colspan(&self, styles: StyleChain) -> NonZeroUsize; - - /// The amount of rows spanned by this cell. - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize; - - /// The cell's span, for errors. - fn span(&self) -> Span; -} - /// A grid of cells, including the columns, rows, and cell data. pub struct CellGrid<'a> { /// The grid cells. - pub(super) entries: Vec<Entry<'a>>, + pub entries: Vec<Entry<'a>>, /// The column tracks including gutter tracks. - pub(super) cols: Vec<Sizing>, + pub cols: Vec<Sizing>, /// The row tracks including gutter tracks. - pub(super) rows: Vec<Sizing>, + pub rows: Vec<Sizing>, /// The vertical lines before each column, or on the end border. /// Gutter columns are not included. /// Contains up to 'cols_without_gutter.len() + 1' vectors of lines. - pub(super) vlines: Vec<Vec<Line>>, + pub vlines: Vec<Vec<Line>>, /// The horizontal lines on top of each row, or on the bottom border. /// Gutter rows are not included. /// Contains up to 'rows_without_gutter.len() + 1' vectors of lines. - pub(super) hlines: Vec<Vec<Line>>, + pub hlines: Vec<Vec<Line>>, /// The repeatable header of this grid. - pub(super) header: Option<Repeatable<Header>>, + pub header: Option<Repeatable<Header>>, /// The repeatable footer of this grid. - pub(super) footer: Option<Repeatable<Footer>>, + pub footer: Option<Repeatable<Footer>>, /// Whether this grid has gutters. - pub(super) has_gutter: bool, + pub has_gutter: bool, } impl<'a> CellGrid<'a> { @@ -1125,7 +995,7 @@ impl<'a> CellGrid<'a> { } /// Generates the cell grid, given the tracks and resolved entries. - pub(super) fn new_internal( + pub fn new_internal( tracks: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>, vlines: Vec<Vec<Line>>, @@ -1194,7 +1064,7 @@ impl<'a> CellGrid<'a> { /// /// Returns `None` if it's a gutter cell. #[track_caller] - pub(super) fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> { + pub fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> { assert!(x < self.cols.len()); assert!(y < self.rows.len()); @@ -1216,7 +1086,7 @@ impl<'a> CellGrid<'a> { /// /// Returns `None` if it's a gutter cell or merged position. #[track_caller] - pub(super) fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> { + pub fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> { self.entry(x, y).and_then(Entry::as_cell) } @@ -1228,7 +1098,7 @@ impl<'a> CellGrid<'a> { /// - If it is a merged cell, returns the parent cell's position. /// - If it is a gutter cell, returns None. #[track_caller] - pub(super) fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> { + pub fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> { self.entry(x, y).map(|entry| match entry { Entry::Cell(_) => Axes::new(x, y), Entry::Merged { parent } => { @@ -1260,7 +1130,7 @@ impl<'a> CellGrid<'a> { /// position, if it is gutter), if it exists; otherwise returns None (it's /// gutter and no cell spans it). #[track_caller] - pub(super) fn effective_parent_cell_position( + pub fn effective_parent_cell_position( &self, x: usize, y: usize, @@ -1283,14 +1153,14 @@ impl<'a> CellGrid<'a> { /// Checks if the track with the given index is gutter. /// Does not check if the index is a valid track. #[inline] - pub(super) fn is_gutter_track(&self, index: usize) -> bool { + pub fn is_gutter_track(&self, index: usize) -> bool { self.has_gutter && index % 2 == 1 } /// Returns the effective colspan of a cell, considering the gutters it /// might span if the grid has gutters. #[inline] - pub(super) fn effective_colspan_of_cell(&self, cell: &Cell) -> usize { + pub fn effective_colspan_of_cell(&self, cell: &Cell) -> usize { if self.has_gutter { 2 * cell.colspan.get() - 1 } else { @@ -1301,7 +1171,7 @@ impl<'a> CellGrid<'a> { /// Returns the effective rowspan of a cell, considering the gutters it /// might span if the grid has gutters. #[inline] - pub(super) fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize { + pub fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize { if self.has_gutter { 2 * cell.rowspan.get() - 1 } else { diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst-layout/src/grid/layouter.rs index 53eda0f0..7c94617d 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -1,22 +1,21 @@ use std::fmt::Debug; -use super::lines::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LinePosition, - LineSegment, +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Resolve, StyleChain}; +use typst_library::layout::{ + Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, + Size, Sizing, }; -use super::repeated::Repeatable; -use super::rowspans::{Rowspan, UnbreakableRowGroup}; -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{Resolve, StyleChain}; -use crate::layout::{ - Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, - Region, Regions, Rel, Size, Sizing, +use typst_library::text::TextElem; +use typst_library::visualize::Geometry; +use typst_syntax::Span; +use typst_utils::{MaybeReverseIter, Numeric}; + +use super::{ + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, + LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup, }; -use crate::syntax::Span; -use crate::text::TextElem; -use crate::utils::{MaybeReverseIter, Numeric}; -use crate::visualize::Geometry; /// Performs grid layout. pub struct GridLayouter<'a> { diff --git a/crates/typst/src/layout/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 660811c7..3e89612a 100644 --- a/crates/typst/src/layout/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -1,12 +1,11 @@ use std::num::NonZeroUsize; use std::sync::Arc; -use super::cells::CellGrid; -use super::layout::RowPiece; -use super::repeated::Repeatable; -use crate::foundations::{AlternativeFold, Fold}; -use crate::layout::Abs; -use crate::visualize::Stroke; +use typst_library::foundations::{AlternativeFold, Fold}; +use typst_library::layout::Abs; +use typst_library::visualize::Stroke; + +use super::{CellGrid, LinePosition, Repeatable, RowPiece}; /// Represents an explicit grid line (horizontal or vertical) specified by the /// user. @@ -38,22 +37,10 @@ pub struct Line { pub position: LinePosition, } -/// Indicates whether the line should be drawn before or after the track with -/// its index. This is mostly only relevant when gutter is used, since, then, -/// the position after a track is not the same as before the next -/// non-gutter track. -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum LinePosition { - /// The line should be drawn before its track (e.g. hline on top of a row). - Before, - /// The line should be drawn after its track (e.g. hline below a row). - After, -} - /// Indicates which priority a particular grid line segment should have, based /// on the highest priority configuration that defined the segment's stroke. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(super) enum StrokePriority { +pub enum StrokePriority { /// The stroke of the segment was derived solely from the grid's global /// stroke setting, so it should have the lowest priority. GridStroke = 0, @@ -71,19 +58,19 @@ pub(super) enum StrokePriority { /// Data for a particular line segment in the grid as generated by /// `generate_line_segments`. #[derive(Debug, PartialEq, Eq)] -pub(super) struct LineSegment { +pub struct LineSegment { /// The stroke with which to draw this segment. - pub(super) stroke: Arc<Stroke<Abs>>, + pub stroke: Arc<Stroke<Abs>>, /// The offset of this segment since the beginning of its axis. /// For a vertical line segment, this is the offset since the top of the /// table in the current page; for a horizontal line segment, this is the /// offset since the start border of the table. - pub(super) offset: Abs, + pub offset: Abs, /// The length of this segment. - pub(super) length: Abs, + pub length: Abs, /// The segment's drawing priority, indicating on top of which other /// segments this one should be drawn. - pub(super) priority: StrokePriority, + pub priority: StrokePriority, } /// Generates the segments of lines that should be drawn alongside a certain @@ -119,7 +106,7 @@ pub(super) struct LineSegment { /// number, and they must be iterable over pairs of (number, size). For /// vertical lines, for instance, `tracks` would describe the rows in the /// current region, as pairs (row index, row height). -pub(super) fn generate_line_segments<'grid, F, I, L>( +pub fn generate_line_segments<'grid, F, I, L>( grid: &'grid CellGrid, tracks: I, index: usize, @@ -269,7 +256,7 @@ where current_segment = Some(LineSegment { stroke, offset, length: size, priority }); } - } else if let Some(old_segment) = current_segment.take() { + } else if let Some(old_segment) = Option::take(&mut current_segment) { // We shouldn't draw here (stroke of None), so we yield the // current segment, as it was interrupted. offset += size; @@ -289,7 +276,9 @@ where // closure, the current segment will necessarily be 'None', // so the iterator will necessarily end (that is, we will return None) // after this. - current_segment.take() + // + // Note: Fully-qualified notation because rust-analyzer is confused. + Option::take(&mut current_segment) }) } @@ -312,7 +301,7 @@ where /// /// The priority associated with the returned stroke follows the rules /// described in the docs for `generate_line_segment`. -pub(super) fn vline_stroke_at_row( +pub fn vline_stroke_at_row( grid: &CellGrid, x: usize, y: usize, @@ -432,7 +421,7 @@ pub(super) fn vline_stroke_at_row( /// /// This function assumes columns are sorted by increasing `x`, and rows are /// sorted by increasing `y`. -pub(super) fn hline_stroke_at_column( +pub fn hline_stroke_at_column( grid: &CellGrid, rows: &[RowPiece], local_top_y: Option<usize>, @@ -599,12 +588,14 @@ pub(super) fn hline_stroke_at_column( #[cfg(test)] mod test { + use typst_library::foundations::Content; + use typst_library::introspection::Locator; + use typst_library::layout::{Axes, Sides, Sizing}; + use typst_utils::NonZeroExt; + use super::super::cells::Entry; + use super::super::Cell; use super::*; - use crate::foundations::Content; - use crate::introspection::Locator; - use crate::layout::{Axes, Cell, Sides, Sizing}; - use crate::utils::NonZeroExt; fn sample_cell() -> Cell<'static> { Cell { diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs new file mode 100644 index 00000000..769bef8c --- /dev/null +++ b/crates/typst-layout/src/grid/mod.rs @@ -0,0 +1,416 @@ +mod cells; +mod layouter; +mod lines; +mod repeated; +mod rowspans; + +pub use self::cells::{Cell, CellGrid}; +pub use self::layouter::GridLayouter; + +use std::num::NonZeroUsize; +use std::sync::Arc; + +use ecow::eco_format; +use typst_library::diag::{SourceResult, Trace, Tracepoint}; +use typst_library::engine::Engine; +use typst_library::foundations::{Fold, Packed, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length, + OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, +}; +use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; +use typst_library::text::TextElem; +use typst_library::visualize::{Paint, Stroke}; +use typst_syntax::Span; + +use self::cells::{ + LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem, +}; +use self::layouter::RowPiece; +use self::lines::{ + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line, + LineSegment, +}; +use self::repeated::{Footer, Header, Repeatable}; +use self::rowspans::{Rowspan, UnbreakableRowGroup}; + +/// Layout the grid. +#[typst_macros::time(span = elem.span())] +pub fn layout_grid( + elem: &Packed<GridElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the grid when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); + let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); + let children = elem.children().iter().map(|child| match child { + GridChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(resolve_item), + }, + GridChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children().iter().map(resolve_item), + }, + GridChild::Item(item) => { + ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) + } + }); + let grid = CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span())?; + + let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); + + // Measure the columns and layout the grid row-by-row. + layouter.layout(engine) +} + +/// Layout the table. +#[typst_macros::time(span = elem.span())] +pub fn layout_table( + elem: &Packed<TableElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the table when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); + let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); + let children = elem.children().iter().map(|child| match child { + TableChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(resolve_item), + }, + TableChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children().iter().map(resolve_item), + }, + TableChild::Item(item) => { + ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) + } + }); + let grid = CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span())?; + + let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); + layouter.layout(engine) +} + +fn grid_item_to_resolvable( + item: &GridItem, + styles: StyleChain, +) -> ResolvableGridItem<Packed<GridCell>> { + match item { + GridItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + GridItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +fn table_item_to_resolvable( + item: &TableItem, + styles: StyleChain, +) -> ResolvableGridItem<Packed<TableCell>> { + match item { + TableItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + TableItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +impl ResolvableCell for Packed<TableCell> { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option<Paint>, + align: Smart<Alignment>, + inset: Sides<Option<Rel<Length>>>, + stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the table is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on table cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart<usize> { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart<usize> { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} + +impl ResolvableCell for Packed<GridCell> { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option<Paint>, + align: Smart<Alignment>, + inset: Sides<Option<Rel<Length>>>, + stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the grid is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on grid cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart<usize> { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart<usize> { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} diff --git a/crates/typst/src/layout/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index f187d0bc..972179da 100644 --- a/crates/typst/src/layout/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,25 +1,27 @@ +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::layout::{Abs, Axes, Frame, Regions}; + +use super::layouter::GridLayouter; use super::rowspans::UnbreakableRowGroup; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::layout::{Abs, Axes, Frame, GridLayouter, Regions}; /// A repeatable grid header. Starts at the first row. -pub(super) struct Header { +pub struct Header { /// The index after the last row included in this header. - pub(super) end: usize, + pub end: usize, } /// A repeatable grid footer. Stops at the last row. -pub(super) struct Footer { +pub struct Footer { /// The first row included in this footer. - pub(super) start: usize, + pub start: usize, } /// A possibly repeatable grid object. /// It still exists even when not repeatable, but must not have additional /// considerations by grid layout, other than for consistency (such as making /// a certain group of rows unbreakable). -pub(super) enum Repeatable<T> { +pub enum Repeatable<T> { Repeated(T), NotRepeated(T), } @@ -27,7 +29,7 @@ pub(super) enum Repeatable<T> { impl<T> Repeatable<T> { /// Gets the value inside this repeatable, regardless of whether /// it repeats. - pub(super) fn unwrap(&self) -> &T { + pub fn unwrap(&self) -> &T { match self { Self::Repeated(repeated) => repeated, Self::NotRepeated(not_repeated) => not_repeated, @@ -35,7 +37,7 @@ impl<T> Repeatable<T> { } /// Returns `Some` if the value is repeated, `None` otherwise. - pub(super) fn as_repeated(&self) -> Option<&T> { + pub fn as_repeated(&self) -> Option<&T> { match self { Self::Repeated(repeated) => Some(repeated), Self::NotRepeated(_) => None, @@ -46,7 +48,7 @@ impl<T> Repeatable<T> { impl<'a> GridLayouter<'a> { /// Layouts the header's rows. /// Skips regions as necessary. - pub(super) fn layout_header( + pub fn layout_header( &mut self, header: &Header, engine: &mut Engine, @@ -90,7 +92,7 @@ impl<'a> GridLayouter<'a> { } /// Simulate the header's group of rows. - pub(super) fn simulate_header( + pub fn simulate_header( &self, header: &Header, regions: &Regions<'_>, @@ -112,7 +114,7 @@ impl<'a> GridLayouter<'a> { } /// Updates `self.footer_height` by simulating the footer, and skips to fitting region. - pub(super) fn prepare_footer( + pub fn prepare_footer( &mut self, footer: &Footer, engine: &mut Engine, @@ -146,7 +148,7 @@ impl<'a> GridLayouter<'a> { /// Lays out all rows in the footer. /// They are unbreakable. - pub(super) fn layout_footer( + pub fn layout_footer( &mut self, footer: &Footer, engine: &mut Engine, @@ -167,7 +169,7 @@ impl<'a> GridLayouter<'a> { } // Simulate the footer's group of rows. - pub(super) fn simulate_footer( + pub fn simulate_footer( &self, footer: &Footer, regions: &Regions<'_>, diff --git a/crates/typst/src/layout/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 6381ba66..03b4103f 100644 --- a/crates/typst/src/layout/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -1,88 +1,88 @@ -use super::layout::{in_last_with_offset, points, Row, RowPiece}; +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::Resolve; +use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; +use typst_utils::MaybeReverseIter; + +use super::layouter::{in_last_with_offset, points, Row, RowPiece}; use super::repeated::Repeatable; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::Resolve; -use crate::layout::{ - Abs, Axes, Cell, Frame, GridLayouter, Point, Region, Regions, Size, Sizing, -}; -use crate::utils::MaybeReverseIter; +use super::{Cell, GridLayouter}; /// All information needed to layout a single rowspan. -pub(super) struct Rowspan { +pub struct Rowspan { /// First column of this rowspan. - pub(super) x: usize, + pub x: usize, /// First row of this rowspan. - pub(super) y: usize, + pub y: usize, /// The disambiguator for laying out the cells. - pub(super) disambiguator: usize, + pub disambiguator: usize, /// Amount of rows spanned by the cell at (x, y). - pub(super) rowspan: usize, + pub rowspan: usize, /// Whether all rows of the rowspan are part of an unbreakable row group. /// This is true e.g. in headers and footers, regardless of what the user /// specified for the parent cell's `breakable` field. - pub(super) is_effectively_unbreakable: bool, + pub is_effectively_unbreakable: bool, /// The horizontal offset of this rowspan in all regions. - pub(super) dx: Abs, + pub dx: Abs, /// The vertical offset of this rowspan in the first region. - pub(super) dy: Abs, + pub dy: Abs, /// The index of the first region this rowspan appears in. - pub(super) first_region: usize, + pub first_region: usize, /// The full height in the first region this rowspan appears in, for /// relative sizing. - pub(super) region_full: Abs, + pub region_full: Abs, /// The vertical space available for this rowspan in each region. - pub(super) heights: Vec<Abs>, + pub heights: Vec<Abs>, /// The index of the largest resolved spanned row so far. /// Once a spanned row is resolved and its height added to `heights`, this /// number is increased. Older rows, even if repeated through e.g. a /// header, will no longer contribute height to this rowspan. /// /// This is `None` if no spanned rows were resolved in `finish_region` yet. - pub(super) max_resolved_row: Option<usize>, + pub max_resolved_row: Option<usize>, } /// The output of the simulation of an unbreakable row group. #[derive(Default)] -pub(super) struct UnbreakableRowGroup { +pub struct UnbreakableRowGroup { /// The rows in this group of unbreakable rows. /// Includes their indices and their predicted heights. - pub(super) rows: Vec<(usize, Abs)>, + pub rows: Vec<(usize, Abs)>, /// The total height of this row group. - pub(super) height: Abs, + pub height: Abs, } /// Data used to measure a cell in an auto row. -pub(super) struct CellMeasurementData<'layouter> { +pub struct CellMeasurementData<'layouter> { /// The available width for the cell across all regions. - pub(super) width: Abs, + pub width: Abs, /// The available height for the cell in its first region. /// Infinite when the auto row is unbreakable. - pub(super) height: Abs, + pub height: Abs, /// The backlog of heights available for the cell in later regions. /// /// When this is `None`, the `custom_backlog` field should be used instead. /// That's because, otherwise, this field would have to contain a reference /// to the `custom_backlog` field, which isn't possible in Rust without /// resorting to unsafe hacks. - pub(super) backlog: Option<&'layouter [Abs]>, + pub backlog: Option<&'layouter [Abs]>, /// If the backlog needs to be built from scratch instead of reusing the /// one at the current region, which is the case of a multi-region rowspan /// (needs to join its backlog of already laid out heights with the current /// backlog), then this vector will store the new backlog. - pub(super) custom_backlog: Vec<Abs>, + pub custom_backlog: Vec<Abs>, /// The full height of the first region of the cell. /// Infinite when the auto row is unbreakable. - pub(super) full: Abs, + pub full: Abs, /// The height of the last repeated region to use in the measurement pod, /// if any. - pub(super) last: Option<Abs>, + pub last: Option<Abs>, /// The total height of previous rows spanned by the cell in the current /// region (so far). - pub(super) height_in_this_region: Abs, + pub height_in_this_region: Abs, /// The amount of previous regions spanned by the cell. /// They are skipped for measurement purposes. - pub(super) frames_in_previous_regions: usize, + pub frames_in_previous_regions: usize, } impl<'a> GridLayouter<'a> { @@ -95,7 +95,7 @@ impl<'a> GridLayouter<'a> { /// We need to do this only once we already know the heights of all /// spanned rows, which is only possible after laying out the last row /// spanned by the rowspan (or some row immediately after the last one). - pub(super) fn layout_rowspan( + pub fn layout_rowspan( &mut self, rowspan_data: Rowspan, current_region_data: Option<(&mut Frame, &[RowPiece])>, @@ -184,7 +184,7 @@ impl<'a> GridLayouter<'a> { /// Checks if a row contains the beginning of one or more rowspan cells. /// If so, adds them to the rowspans vector. - pub(super) fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { + pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { // We will compute the horizontal offset of each rowspan in advance. // For that reason, we must reverse the column order when using RTL. let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl)); @@ -219,7 +219,7 @@ impl<'a> GridLayouter<'a> { /// unbreakable row group, and, if so, advances regions until there is /// enough space for them. This can be needed, for example, if there's an /// unbreakable rowspan crossing those rows. - pub(super) fn check_for_unbreakable_rows( + pub fn check_for_unbreakable_rows( &mut self, current_row: usize, engine: &mut Engine, @@ -292,7 +292,7 @@ impl<'a> GridLayouter<'a> { /// /// This is used to figure out how much height the next unbreakable row /// group (if any) needs. - pub(super) fn simulate_unbreakable_row_group( + pub fn simulate_unbreakable_row_group( &self, first_row: usize, amount_unbreakable_rows: Option<usize>, @@ -359,7 +359,7 @@ impl<'a> GridLayouter<'a> { /// Checks if one or more of the cells at the given row are unbreakable. /// If so, returns the largest rowspan among the unbreakable cells; /// the spanned rows must, as a result, be laid out in the same region. - pub(super) fn check_for_unbreakable_cells(&self, y: usize) -> usize { + pub fn check_for_unbreakable_cells(&self, y: usize) -> usize { (0..self.grid.cols.len()) .filter_map(|x| self.grid.cell(x, y)) .filter(|cell| !cell.breakable) @@ -369,7 +369,7 @@ impl<'a> GridLayouter<'a> { } /// Used by `measure_auto_row` to gather data needed to measure the cell. - pub(super) fn prepare_auto_row_cell_measurement( + pub fn prepare_auto_row_cell_measurement( &self, parent: Axes<usize>, cell: &Cell, @@ -577,7 +577,7 @@ impl<'a> GridLayouter<'a> { /// expand the auto row based on the rowspan's demanded size, or `false` /// otherwise. #[allow(clippy::too_many_arguments)] - pub(super) fn prepare_rowspan_sizes( + pub fn prepare_rowspan_sizes( &self, auto_row_y: usize, sizes: &mut Vec<Abs>, @@ -667,7 +667,7 @@ impl<'a> GridLayouter<'a> { /// in each region and the pending rowspans' data (parent Y, rowspan amount /// and vector of requested sizes). #[allow(clippy::too_many_arguments)] - pub(super) fn simulate_and_measure_rowspans_in_auto_row( + pub fn simulate_and_measure_rowspans_in_auto_row( &self, y: usize, resolved: &mut Vec<Abs>, diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs new file mode 100644 index 00000000..84a60282 --- /dev/null +++ b/crates/typst-layout/src/image.rs @@ -0,0 +1,142 @@ +use std::ffi::OsStr; + +use typst_library::diag::{bail, warning, At, SourceResult, StrResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, +}; +use typst_library::loading::Readable; +use typst_library::text::families; +use typst_library::visualize::{ + Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat, +}; + +/// Layout the image. +#[typst_macros::time(span = elem.span())] +pub fn layout_image( + elem: &Packed<ImageElem>, + engine: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let span = elem.span(); + + // Take the format that was explicitly defined, or parse the extension, + // or try to detect the format. + let data = elem.data(); + let format = match elem.format(styles) { + Smart::Custom(v) => v, + Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?, + }; + + // Warn the user if the image contains a foreign object. Not perfect + // because the svg could also be encoded, but that's an edge case. + if format == ImageFormat::Vector(VectorFormat::Svg) { + let has_foreign_object = + data.as_str().is_some_and(|s| s.contains("<foreignObject")); + + if has_foreign_object { + engine.sink.warn(warning!( + span, + "image contains foreign object"; + hint: "SVG images with foreign objects might render incorrectly in typst"; + hint: "see https://github.com/typst/typst/issues/1421 for more information" + )); + } + } + + // Construct the image itself. + let image = Image::with_fonts( + data.clone().into(), + format, + elem.alt(styles), + engine.world, + &families(styles).collect::<Vec<_>>(), + ) + .at(span)?; + + // Determine the image's pixel aspect ratio. + let pxw = image.width(); + let pxh = image.height(); + let px_ratio = pxw / pxh; + + // Determine the region's aspect ratio. + let region_ratio = region.size.x / region.size.y; + + // Find out whether the image is wider or taller than the region. + let wide = px_ratio > region_ratio; + + // The space into which the image will be placed according to its fit. + let target = if region.expand.x && region.expand.y { + // If both width and height are forced, take them. + region.size + } else if region.expand.x { + // If just width is forced, take it. + Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio)) + } else if region.expand.y { + // If just height is forced, take it. + Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y) + } else { + // If neither is forced, take the natural image size at the image's + // DPI bounded by the available space. + let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); + let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); + Size::new( + natural.x.min(region.size.x).min(region.size.y * px_ratio), + natural.y.min(region.size.y).min(region.size.x / px_ratio), + ) + }; + + // Compute the actual size of the fitted image. + let fit = elem.fit(styles); + let fitted = match fit { + ImageFit::Cover | ImageFit::Contain => { + if wide == (fit == ImageFit::Contain) { + Size::new(target.x, target.x / px_ratio) + } else { + Size::new(target.y * px_ratio, target.y) + } + } + ImageFit::Stretch => target, + }; + + // First, place the image in a frame of exactly its size and then resize + // the frame to the target size, center aligning the image in the + // process. + let mut frame = Frame::soft(fitted); + frame.push(Point::zero(), FrameItem::Image(image, fitted, span)); + frame.resize(target, Axes::splat(FixedAlignment::Center)); + + // Create a clipping group if only part of the image should be visible. + if fit == ImageFit::Cover && !target.fits(fitted) { + frame.clip(Path::rect(frame.size())); + } + + Ok(frame) +} + +/// Determine the image format based on path and data. +fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> { + let ext = std::path::Path::new(path) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); + + Ok(match ext.as_str() { + "png" => ImageFormat::Raster(RasterFormat::Png), + "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), + "gif" => ImageFormat::Raster(RasterFormat::Gif), + "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), + _ => match &data { + Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), + Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { + Some(f) => ImageFormat::Raster(f), + None => bail!("unknown image format"), + }, + }, + }) +} diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs new file mode 100644 index 00000000..30572e4e --- /dev/null +++ b/crates/typst-layout/src/inline/box.rs @@ -0,0 +1,87 @@ +use once_cell::unsync::Lazy; +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{BoxElem, Frame, FrameKind, Size}; +use typst_library::visualize::Stroke; +use typst_utils::Numeric; + +use crate::flow::unbreakable_pod; +use crate::shapes::{clip_rect, fill_and_stroke}; + +/// Lay out a box as part of a paragraph. +#[typst_macros::time(name = "box", span = elem.span())] +pub fn layout_box( + elem: &Packed<BoxElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, +) -> SourceResult<Frame> { + // Fetch sizing properties. + let width = elem.width(styles); + let height = elem.height(styles); + let inset = elem.inset(styles).unwrap_or_default(); + + // Build the pod region. + let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region); + + // Layout the body. + let mut frame = match elem.body(styles) { + // If we have no body, just create an empty frame. If necessary, + // its size will be adjusted below. + None => Frame::hard(Size::zero()), + + // If we have a child, layout it into the body. Boxes are boundaries + // for gradient relativeness, so we set the `FrameKind` to `Hard`. + Some(body) => crate::layout_frame(engine, body, locator, styles, pod)? + .with_kind(FrameKind::Hard), + }; + + // Enforce a correct frame size on the expanded axes. Do this before + // applying the inset, since the pod shrunk. + frame.set_size(pod.expand.select(pod.size, frame.size())); + + // Apply the inset. + if !inset.is_zero() { + crate::pad::grow(&mut frame, &inset); + } + + // Prepare fill and stroke. + let fill = elem.fill(styles); + let stroke = elem + .stroke(styles) + .unwrap_or_default() + .map(|s| s.map(Stroke::unwrap_or_default)); + + // Only fetch these if necessary (for clipping or filling/stroking). + let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default()); + let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default()); + + // Clip the contents, if requested. + if elem.clip(styles) { + let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); + frame.clip(clip_rect(size, &radius, &stroke)); + } + + // Add fill and/or stroke. + if fill.is_some() || stroke.iter().any(Option::is_some) { + fill_and_stroke(&mut frame, fill, &stroke, &outset, &radius, elem.span()); + } + + // Assign label to the frame. + if let Some(label) = elem.label() { + frame.label(label); + } + + // Apply baseline shift. Do this after setting the size and applying the + // inset, so that a relative shift is resolved relative to the final + // height. + let shift = elem.baseline(styles).relative_to(frame.height()); + if !shift.is_zero() { + frame.set_baseline(frame.baseline() - shift); + } + + Ok(frame) +} diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index af14b152..fbcddee5 100644 --- a/crates/typst/src/layout/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -1,17 +1,18 @@ -use super::*; -use crate::diag::bail; -use crate::foundations::{Packed, Resolve}; -use crate::introspection::{SplitLocator, Tag, TagElem}; -use crate::layout::{ +use typst_library::diag::bail; +use typst_library::foundations::{Packed, Resolve}; +use typst_library::introspection::{SplitLocator, Tag, TagElem}; +use typst_library::layout::{ Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; -use crate::syntax::Span; -use crate::text::{ +use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, }; -use crate::utils::Numeric; +use typst_syntax::Span; +use typst_utils::Numeric; + +use super::*; // The characters by which spacing, inline content and pins are replaced in the // paragraph's full text. @@ -222,7 +223,7 @@ pub fn collect<'a>( if let Sizing::Fr(v) = elem.width(styles) { collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } else { - let frame = elem.layout(engine, loc, styles, region)?; + let frame = layout_box(elem, engine, loc, styles, region)?; collector.push_item(Item::Frame(frame, styles)); } } else if let Some(elem) = child.to_packed::<TagElem>() { diff --git a/crates/typst-layout/src/inline/deco.rs b/crates/typst-layout/src/inline/deco.rs new file mode 100644 index 00000000..c01b369b --- /dev/null +++ b/crates/typst-layout/src/inline/deco.rs @@ -0,0 +1,213 @@ +use kurbo::{BezPath, Line, ParamCurve}; +use ttf_parser::{GlyphId, OutlineBuilder}; +use typst_library::layout::{Abs, Em, Frame, FrameItem, Point, Size}; +use typst_library::text::{ + BottomEdge, DecoLine, Decoration, TextEdgeBounds, TextItem, TopEdge, +}; +use typst_library::visualize::{FixedStroke, Geometry}; +use typst_syntax::Span; + +use crate::shapes::styled_rect; + +/// Add line decorations to a single run of shaped text. +pub fn decorate( + frame: &mut Frame, + deco: &Decoration, + text: &TextItem, + width: Abs, + shift: Abs, + pos: Point, +) { + let font_metrics = text.font.metrics(); + + if let DecoLine::Highlight { fill, stroke, top_edge, bottom_edge, radius } = + &deco.line + { + let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge); + let size = Size::new(width + 2.0 * deco.extent, top + bottom); + let rects = styled_rect(size, radius, fill.clone(), stroke); + let origin = Point::new(pos.x - deco.extent, pos.y - top - shift); + frame.prepend_multiple( + rects + .into_iter() + .map(|shape| (origin, FrameItem::Shape(shape, Span::detached()))), + ); + return; + } + + let (stroke, metrics, offset, evade, background) = match &deco.line { + DecoLine::Strikethrough { stroke, offset, background } => { + (stroke, font_metrics.strikethrough, offset, false, *background) + } + DecoLine::Overline { stroke, offset, evade, background } => { + (stroke, font_metrics.overline, offset, *evade, *background) + } + DecoLine::Underline { stroke, offset, evade, background } => { + (stroke, font_metrics.underline, offset, *evade, *background) + } + _ => return, + }; + + let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift; + let stroke = stroke.clone().unwrap_or(FixedStroke::from_pair( + text.fill.as_decoration(), + metrics.thickness.at(text.size), + )); + + let gap_padding = 0.08 * text.size; + let min_width = 0.162 * text.size; + + let start = pos.x - deco.extent; + let end = pos.x + width + deco.extent; + + let mut push_segment = |from: Abs, to: Abs, prepend: bool| { + let origin = Point::new(from, pos.y + offset); + let target = Point::new(to - from, Abs::zero()); + + if target.x >= min_width || !evade { + let shape = Geometry::Line(target).stroked(stroke.clone()); + + if prepend { + frame.prepend(origin, FrameItem::Shape(shape, Span::detached())); + } else { + frame.push(origin, FrameItem::Shape(shape, Span::detached())); + } + } + }; + + if !evade { + push_segment(start, end, background); + return; + } + + let line = Line::new( + kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), + kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), + ); + + let mut x = pos.x; + let mut intersections = vec![]; + + for glyph in text.glyphs.iter() { + let dx = glyph.x_offset.at(text.size) + x; + let mut builder = + BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); + + let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); + let path = builder.finish(); + + x += glyph.x_advance.at(text.size); + + // Only do the costly segments intersection test if the line + // intersects the bounding box. + let intersect = bbox.is_some_and(|bbox| { + let y_min = -text.font.to_em(bbox.y_max).at(text.size); + let y_max = -text.font.to_em(bbox.y_min).at(text.size); + offset >= y_min && offset <= y_max + }); + + if intersect { + // Find all intersections of segments with the line. + intersections.extend( + path.segments() + .flat_map(|seg| seg.intersect_line(line)) + .map(|is| Abs::raw(line.eval(is.line_t).x)), + ); + } + } + + // Add start and end points, taking padding into account. + intersections.push(start - gap_padding); + intersections.push(end + gap_padding); + // When emitting the decorative line segments, we move from left to + // right. The intersections are not necessarily in this order, yet. + intersections.sort(); + + for edge in intersections.windows(2) { + let l = edge[0]; + let r = edge[1]; + + // If we are too close, don't draw the segment + if r - l < gap_padding { + continue; + } else { + push_segment(l + gap_padding, r - gap_padding, background); + } + } +} + +// Return the top/bottom edge of the text given the metric of the font. +fn determine_edges( + text: &TextItem, + top_edge: TopEdge, + bottom_edge: BottomEdge, +) -> (Abs, Abs) { + let mut top = Abs::zero(); + let mut bottom = Abs::zero(); + + for g in text.glyphs.iter() { + let (t, b) = text.font.edges( + top_edge, + bottom_edge, + text.size, + TextEdgeBounds::Glyph(g.id), + ); + top.set_max(t); + bottom.set_max(b); + } + + (top, bottom) +} + +/// Builds a kurbo [`BezPath`] for a glyph. +struct BezPathBuilder { + path: BezPath, + units_per_em: f64, + font_size: Abs, + x_offset: f64, +} + +impl BezPathBuilder { + fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self { + Self { + path: BezPath::new(), + units_per_em, + font_size, + x_offset, + } + } + + fn finish(self) -> BezPath { + self.path + } + + fn p(&self, x: f32, y: f32) -> kurbo::Point { + kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) + } + + fn s(&self, v: f32) -> f64 { + Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() + } +} + +impl OutlineBuilder for BezPathBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.path.move_to(self.p(x, y)); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.path.line_to(self.p(x, y)); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.path.quad_to(self.p(x1, y1), self.p(x, y)); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); + } + + fn close(&mut self) { + self.path.close_path(); + } +} diff --git a/crates/typst/src/layout/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 082e3613..599ace9d 100644 --- a/crates/typst/src/layout/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -1,6 +1,7 @@ +use typst_library::introspection::SplitLocator; +use typst_utils::Numeric; + use super::*; -use crate::introspection::SplitLocator; -use crate::utils::Numeric; /// Turns the selected lines into frames. #[typst_macros::time] diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst-layout/src/inline/line.rs index a512c32d..596e109e 100644 --- a/crates/typst/src/layout/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -1,14 +1,15 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; +use typst_library::engine::Engine; +use typst_library::foundations::NativeElement; +use typst_library::introspection::{SplitLocator, Tag}; +use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; +use typst_library::model::{ParLine, ParLineMarker}; +use typst_library::text::{Lang, TextElem}; +use typst_utils::Numeric; + use super::*; -use crate::engine::Engine; -use crate::foundations::NativeElement; -use crate::introspection::{SplitLocator, Tag}; -use crate::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; -use crate::model::{ParLine, ParLineMarker}; -use crate::text::{Lang, TextElem}; -use crate::utils::Numeric; const SHY: char = '\u{ad}'; const HYPHEN: char = '-'; @@ -510,7 +511,7 @@ pub fn commit( if let Some((elem, loc, styles)) = elem { let region = Size::new(amount, full); let mut frame = - elem.layout(engine, loc.relayout(), *styles, region)?; + layout_box(elem, engine, loc.relayout(), *styles, region)?; frame.translate(Point::with_y(TextElem::baseline_in(*styles))); push(&mut offset, frame.post_processed(*styles)); } else { @@ -590,7 +591,7 @@ fn add_par_line_marker( // where line numbers can be displayed), so we just need it to be in a tag // and to be valid (to have a location). let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); - let key = crate::utils::hash128(&marker); + let key = typst_utils::hash128(&marker); let loc = locator.next_location(engine.introspector, key); marker.set_location(loc); diff --git a/crates/typst/src/layout/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index aa62d487..7fc8b368 100644 --- a/crates/typst/src/layout/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -8,14 +8,14 @@ use icu_provider_adapters::fork::ForkByKeyProvider; use icu_provider_blob::BlobDataProvider; use icu_segmenter::LineSegmenter; use once_cell::sync::Lazy; +use typst_library::engine::Engine; +use typst_library::layout::{Abs, Em}; +use typst_library::model::Linebreaks; +use typst_library::text::{is_default_ignorable, Lang, TextElem}; +use typst_syntax::link_prefix; use unicode_segmentation::UnicodeSegmentation; use super::*; -use crate::engine::Engine; -use crate::layout::{Abs, Em}; -use crate::model::Linebreaks; -use crate::syntax::link_prefix; -use crate::text::{is_default_ignorable, Lang, TextElem}; /// The cost of a line or paragraph layout. type Cost = f64; diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 275cb332..658e3084 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -1,13 +1,27 @@ +#[path = "box.rs"] +mod box_; mod collect; +mod deco; mod finalize; mod line; mod linebreak; mod prepare; mod shaping; +pub use self::box_::layout_box; + use comemo::{Track, Tracked, TrackedMut}; +use typst_library::diag::SourceResult; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{StyleChain, StyleVec}; +use typst_library::introspection::{Introspector, Locator, LocatorLink}; +use typst_library::layout::{Fragment, Size}; +use typst_library::model::ParElem; +use typst_library::routines::Routines; +use typst_library::World; use self::collect::{collect, Item, Segment, SpanMapper}; +use self::deco::decorate; use self::finalize::finalize; use self::line::{commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; @@ -16,19 +30,12 @@ use self::shaping::{ cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText, BEGIN_PUNCT_PAT, END_PUNCT_PAT, }; -use crate::diag::SourceResult; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{StyleChain, StyleVec}; -use crate::introspection::{Introspector, Locator, LocatorLink}; -use crate::layout::{Fragment, Size}; -use crate::model::ParElem; -use crate::World; /// Range of a substring of text. type Range = std::ops::Range<usize>; /// Layouts content inline. -pub(crate) fn layout_inline( +pub fn layout_inline( engine: &mut Engine, children: &StyleVec, locator: Locator, @@ -39,6 +46,7 @@ pub(crate) fn layout_inline( ) -> SourceResult<Fragment> { layout_inline_impl( children, + engine.routines, engine.world, engine.introspector, engine.traced, @@ -57,6 +65,7 @@ pub(crate) fn layout_inline( #[allow(clippy::too_many_arguments)] fn layout_inline_impl( children: &StyleVec, + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -71,6 +80,7 @@ fn layout_inline_impl( let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { + routines, world, introspector, traced, diff --git a/crates/typst/src/layout/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 3ac155b5..2dd79aec 100644 --- a/crates/typst/src/layout/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,10 +1,10 @@ +use typst_library::foundations::{Resolve, Smart}; +use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; +use typst_library::model::Linebreaks; +use typst_library::text::{Costs, Lang, TextElem}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; -use crate::foundations::{Resolve, Smart}; -use crate::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; -use crate::model::Linebreaks; -use crate::text::{Costs, Lang, TextElem}; /// A paragraph representation in which children are already layouted and text /// is already preshaped. diff --git a/crates/typst/src/layout/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 5b95dadb..bd803b52 100644 --- a/crates/typst/src/layout/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -7,19 +7,19 @@ use az::SaturatingAs; use ecow::EcoString; use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer}; use ttf_parser::Tag; +use typst_library::engine::Engine; +use typst_library::foundations::{Smart, StyleChain}; +use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; +use typst_library::text::{ + families, features, is_default_ignorable, variant, Font, FontVariant, Glyph, Lang, + Region, TextEdgeBounds, TextElem, TextItem, +}; +use typst_library::World; +use typst_utils::SliceExt; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; -use super::{Item, Range, SpanMapper}; -use crate::engine::Engine; -use crate::foundations::{Smart, StyleChain}; -use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; -use crate::text::{ - decorate, families, features, is_default_ignorable, variant, Font, FontVariant, - Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem, -}; -use crate::utils::SliceExt; -use crate::World; +use super::{decorate, Item, Range, SpanMapper}; /// The result of shaping text. /// diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs new file mode 100644 index 00000000..7069fc4d --- /dev/null +++ b/crates/typst-layout/src/lib.rs @@ -0,0 +1,30 @@ +//! Typst's layout engine. + +mod flow; +mod grid; +mod image; +mod inline; +mod lists; +mod math; +mod pad; +mod pages; +mod repeat; +mod shapes; +mod stack; +mod transforms; + +pub use self::flow::{layout_columns, layout_fragment, layout_frame}; +pub use self::grid::{layout_grid, layout_table}; +pub use self::image::layout_image; +pub use self::inline::{layout_box, layout_inline}; +pub use self::lists::{layout_enum, layout_list}; +pub use self::math::{layout_equation_block, layout_equation_inline}; +pub use self::pad::layout_pad; +pub use self::pages::layout_document; +pub use self::repeat::layout_repeat; +pub use self::shapes::{ + layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect, + layout_square, +}; +pub use self::stack::layout_stack; +pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew}; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs new file mode 100644 index 00000000..08c2a2f4 --- /dev/null +++ b/crates/typst-layout/src/lists.rs @@ -0,0 +1,146 @@ +use comemo::Track; +use smallvec::smallvec; +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; +use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; +use typst_library::text::TextElem; + +use crate::grid::{Cell, CellGrid, GridLayouter}; + +/// Layout the list. +#[typst_macros::time(span = elem.span())] +pub fn layout_list( + elem: &Packed<ListElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + let indent = elem.indent(styles); + let body_indent = elem.body_indent(styles); + let gutter = elem.spacing(styles).unwrap_or_else(|| { + if elem.tight(styles) { + ParElem::leading_in(styles).into() + } else { + ParElem::spacing_in(styles).into() + } + }); + + let Depth(depth) = ListElem::depth_in(styles); + let marker = elem + .marker(styles) + .resolve(engine, styles, depth)? + // avoid '#set align' interference with the list + .aligned(HAlignment::Start + VAlignment::Top); + + let mut cells = vec![]; + let mut locator = locator.split(); + + for item in elem.children() { + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new( + item.body.clone().styled(ListElem::set_depth(Depth(1))), + locator.next(&item.body.span()), + )); + } + + let grid = CellGrid::new( + Axes::with_x(&[ + Sizing::Rel(indent.into()), + Sizing::Auto, + Sizing::Rel(body_indent.into()), + Sizing::Auto, + ]), + Axes::with_y(&[gutter.into()]), + cells, + ); + let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); + + layouter.layout(engine) +} + +/// Layout the enumeration. +#[typst_macros::time(span = elem.span())] +pub fn layout_enum( + elem: &Packed<EnumElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + let numbering = elem.numbering(styles); + let indent = elem.indent(styles); + let body_indent = elem.body_indent(styles); + let gutter = elem.spacing(styles).unwrap_or_else(|| { + if elem.tight(styles) { + ParElem::leading_in(styles).into() + } else { + ParElem::spacing_in(styles).into() + } + }); + + let mut cells = vec![]; + let mut locator = locator.split(); + let mut number = elem.start(styles); + let mut parents = EnumElem::parents_in(styles); + + let full = elem.full(styles); + + // Horizontally align based on the given respective parameter. + // Vertically align to the top to avoid inheriting `horizon` or `bottom` + // alignment from the context and having the number be displaced in + // relation to the item it refers to. + let number_align = elem.number_align(styles); + + for item in elem.children() { + number = item.number(styles).unwrap_or(number); + + let context = Context::new(None, Some(styles)); + let resolved = if full { + parents.push(number); + let content = numbering.apply(engine, context.track(), &parents)?.display(); + parents.pop(); + content + } else { + match numbering { + Numbering::Pattern(pattern) => { + TextElem::packed(pattern.apply_kth(parents.len(), number)) + } + other => other.apply(engine, context.track(), &[number])?.display(), + } + }; + + // Disable overhang as a workaround to end-aligned dots glitching + // and decreasing spacing between numbers and items. + let resolved = + resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new(resolved, locator.next(&()))); + cells.push(Cell::new(Content::empty(), locator.next(&()))); + cells.push(Cell::new( + item.body.clone().styled(EnumElem::set_parents(smallvec![number])), + locator.next(&item.body.span()), + )); + number = number.saturating_add(1); + } + + let grid = CellGrid::new( + Axes::with_x(&[ + Sizing::Rel(indent.into()), + Sizing::Auto, + Sizing::Rel(body_indent.into()), + Sizing::Auto, + ]), + Axes::with_y(&[gutter.into()]), + cells, + ); + let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); + + layouter.layout(engine) +} diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs new file mode 100644 index 00000000..9fa7a5a0 --- /dev/null +++ b/crates/typst-layout/src/math/accent.rs @@ -0,0 +1,75 @@ +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::layout::{Em, Frame, Point, Rel, Size}; +use typst_library::math::{Accent, AccentElem}; + +use super::{ + scaled_font_size, style_cramped, FrameFragment, GlyphFragment, MathContext, + MathFragment, +}; + +/// How much the accent can be shorter than the base. +const ACCENT_SHORT_FALL: Em = Em::new(0.5); + +/// Lays out an [`AccentElem`]. +#[typst_macros::time(name = "math.accent", span = elem.span())] +pub fn layout_accent( + elem: &Packed<AccentElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let cramped = style_cramped(); + let base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; + + // Preserve class to preserve automatic spacing. + let base_class = base.class(); + let base_attach = base.accent_attach(); + + let width = elem + .size(styles) + .unwrap_or(Rel::one()) + .at(scaled_font_size(ctx, styles)) + .relative_to(base.width()); + + // Forcing the accent to be at least as large as the base makes it too + // wide in many case. + let Accent(c) = elem.accent(); + let glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); + let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); + let variant = glyph.stretch_horizontal(ctx, width, short_fall); + let accent = variant.frame; + let accent_attach = variant.accent_attach; + + // Descent is negative because the accent's ink bottom is above the + // baseline. Therefore, the default gap is the accent's negated descent + // minus the accent base height. Only if the base is very small, we need + // a larger gap so that the accent doesn't move too low. + let accent_base_height = scaled!(ctx, styles, accent_base_height); + let gap = -accent.descent() - base.height().min(accent_base_height); + let size = Size::new(base.width(), accent.height() + gap + base.height()); + let accent_pos = Point::with_x(base_attach - accent_attach); + let base_pos = Point::with_y(accent.height() + gap); + let baseline = base_pos.y + base.ascent(); + let base_italics_correction = base.italics_correction(); + let base_text_like = base.is_text_like(); + + let base_ascent = match &base { + MathFragment::Frame(frame) => frame.base_ascent, + _ => base.ascent(), + }; + + let mut frame = Frame::soft(size); + frame.set_baseline(baseline); + frame.push_frame(accent_pos, accent); + frame.push_frame(base_pos, base.into_frame()); + ctx.push( + FrameFragment::new(ctx, styles, frame) + .with_class(base_class) + .with_base_ascent(base_ascent) + .with_italics_correction(base_italics_correction) + .with_accent_attach(base_attach) + .with_text_like(base_text_like), + ); + + Ok(()) +} diff --git a/crates/typst/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 9eb0c824..0f9090f7 100644 --- a/crates/typst/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -1,14 +1,16 @@ -use unicode_math_class::MathClass; - -use crate::diag::SourceResult; -use crate::foundations::{elem, Content, Packed, Smart, StyleChain}; -use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; -use crate::math::{ - stretch_fragment, style_for_subscript, style_for_superscript, EquationElem, - FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem, +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; +use typst_library::math::{ + AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, +}; +use typst_library::text::TextElem; +use typst_utils::OptionExt; + +use super::{ + stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits, + MathContext, MathFragment, }; -use crate::text::TextElem; -use crate::utils::OptionExt; macro_rules! measure { ($e: ident, $attr: ident) => { @@ -16,300 +18,140 @@ macro_rules! measure { }; } -/// A base with optional attachments. -/// -/// ```example -/// $ attach( -/// Pi, t: alpha, b: beta, -/// tl: 1, tr: 2+3, bl: 4+5, br: 6, -/// ) $ -/// ``` -#[elem(LayoutMath)] -pub struct AttachElem { - /// The base to which things are attached. - #[required] - pub base: Content, - - /// The top attachment, smartly positioned at top-right or above the base. - /// - /// You can wrap the base in `{limits()}` or `{scripts()}` to override the - /// smart positioning. - pub t: Option<Content>, - - /// The bottom attachment, smartly positioned at the bottom-right or below - /// the base. - /// - /// You can wrap the base in `{limits()}` or `{scripts()}` to override the - /// smart positioning. - pub b: Option<Content>, - - /// The top-left attachment (before the base). - pub tl: Option<Content>, - - /// The bottom-left attachment (before base). - pub bl: Option<Content>, - - /// The top-right attachment (after the base). - pub tr: Option<Content>, - - /// The bottom-right attachment (after the base). - pub br: Option<Content>, -} +/// Lays out an [`AttachElem`]. +#[typst_macros::time(name = "math.attach", span = elem.span())] +pub fn layout_attach( + elem: &Packed<AttachElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let merged = elem.merge_base(); + let elem = merged.as_ref().unwrap_or(elem); + let stretch = stretch_size(styles, elem); + + let mut base = ctx.layout_into_fragment(elem.base(), styles)?; + let sup_style = style_for_superscript(styles); + let sup_style_chain = styles.chain(&sup_style); + let tl = elem.tl(sup_style_chain); + let tr = elem.tr(sup_style_chain); + let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>()); + let t = elem.t(sup_style_chain); + + let sub_style = style_for_subscript(styles); + let sub_style_chain = styles.chain(&sub_style); + let bl = elem.bl(sub_style_chain); + let br = elem.br(sub_style_chain); + let b = elem.b(sub_style_chain); + + let limits = base.limits().active(styles); + let (t, tr) = match (t, tr) { + (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)), + (Some(t), None) if !limits => (None, Some(t)), + (t, tr) => (t, tr), + }; + let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; -impl LayoutMath for Packed<AttachElem> { - #[typst_macros::time(name = "math.attach", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let new_elem = merge_base(self); - let elem = new_elem.as_ref().unwrap_or(self); - let stretch = stretch_size(styles, elem); - - let mut base = ctx.layout_into_fragment(elem.base(), styles)?; - let sup_style = style_for_superscript(styles); - let sup_style_chain = styles.chain(&sup_style); - let tl = elem.tl(sup_style_chain); - let tr = elem.tr(sup_style_chain); - let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>()); - let t = elem.t(sup_style_chain); - - let sub_style = style_for_subscript(styles); - let sub_style_chain = styles.chain(&sub_style); - let bl = elem.bl(sub_style_chain); - let br = elem.br(sub_style_chain); - let b = elem.b(sub_style_chain); - - let limits = base.limits().active(styles); - let (t, tr) = match (t, tr) { - (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)), - (Some(t), None) if !limits => (None, Some(t)), - (t, tr) => (t, tr), + macro_rules! layout { + ($content:ident, $style_chain:ident) => { + $content + .map(|elem| ctx.layout_into_fragment(&elem, $style_chain)) + .transpose() }; - let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; - - macro_rules! layout { - ($content:ident, $style_chain:ident) => { - $content - .map(|elem| ctx.layout_into_fragment(&elem, $style_chain)) - .transpose() - }; - } - - // Layout the top and bottom attachments early so we can measure their - // widths, in order to calculate what the stretch size is relative to. - let t = layout!(t, sup_style_chain)?; - let b = layout!(b, sub_style_chain)?; - if let Some(stretch) = stretch { - let relative_to_width = measure!(t, width).max(measure!(b, width)); - stretch_fragment( - ctx, - styles, - &mut base, - Some(Axis::X), - Some(relative_to_width), - stretch, - Abs::zero(), - ); - } - - let fragments = [ - layout!(tl, sup_style_chain)?, - t, - layout!(tr, sup_style_chain)?, - layout!(bl, sub_style_chain)?, - b, - layout!(br, sub_style_chain)?, - ]; - - layout_attachments(ctx, styles, base, fragments) } -} -/// Grouped primes. -/// -/// ```example -/// $ a'''_b = a^'''_b $ -/// ``` -/// -/// # Syntax -/// This function has dedicated syntax: use apostrophes instead of primes. They -/// will automatically attach to the previous element, moving superscripts to -/// the next level. -#[elem(LayoutMath)] -pub struct PrimesElem { - /// The number of grouped primes. - #[required] - pub count: usize, -} - -impl LayoutMath for Packed<PrimesElem> { - #[typst_macros::time(name = "math.primes", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - match *self.count() { - count @ 1..=4 => { - let c = match count { - 1 => '′', - 2 => '″', - 3 => '‴', - 4 => '⁗', - _ => unreachable!(), - }; - let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; - ctx.push(f); - } - count => { - // Custom amount of primes - let prime = ctx - .layout_into_fragment(&TextElem::packed('′'), styles)? - .into_frame(); - let width = prime.width() * (count + 1) as f64 / 2.0; - let mut frame = Frame::soft(Size::new(width, prime.height())); - frame.set_baseline(prime.ascent()); - - for i in 0..count { - frame.push_frame( - Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()), - prime.clone(), - ) - } - ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); - } - } - Ok(()) - } -} - -/// Forces a base to display attachments as scripts. -/// -/// ```example -/// $ scripts(sum)_1^2 != sum_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct ScriptsElem { - /// The base to attach the scripts to. - #[required] - pub body: Content, -} - -impl LayoutMath for Packed<ScriptsElem> { - #[typst_macros::time(name = "math.scripts", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(self.body(), styles)?; - fragment.set_limits(Limits::Never); - ctx.push(fragment); - Ok(()) + // Layout the top and bottom attachments early so we can measure their + // widths, in order to calculate what the stretch size is relative to. + let t = layout!(t, sup_style_chain)?; + let b = layout!(b, sub_style_chain)?; + if let Some(stretch) = stretch { + let relative_to_width = measure!(t, width).max(measure!(b, width)); + stretch_fragment( + ctx, + styles, + &mut base, + Some(Axis::X), + Some(relative_to_width), + stretch, + Abs::zero(), + ); } -} - -/// Forces a base to display attachments as limits. -/// -/// ```example -/// $ limits(A)_1^2 != A_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct LimitsElem { - /// The base to attach the limits to. - #[required] - pub body: Content, - - /// Whether to also force limits in inline equations. - /// - /// When applying limits globally (e.g., through a show rule), it is - /// typically a good idea to disable this. - #[default(true)] - pub inline: bool, -} -impl LayoutMath for Packed<LimitsElem> { - #[typst_macros::time(name = "math.limits", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let limits = if self.inline(styles) { Limits::Always } else { Limits::Display }; - let mut fragment = ctx.layout_into_fragment(self.body(), styles)?; - fragment.set_limits(limits); - ctx.push(fragment); - Ok(()) - } -} + let fragments = [ + layout!(tl, sup_style_chain)?, + t, + layout!(tr, sup_style_chain)?, + layout!(bl, sub_style_chain)?, + b, + layout!(br, sub_style_chain)?, + ]; -/// Describes in which situation a frame should use limits for attachments. -#[derive(Debug, Copy, Clone)] -pub enum Limits { - /// Always scripts. - Never, - /// Display limits only in `display` math. - Display, - /// Always limits. - Always, + layout_attachments(ctx, styles, base, fragments) } -impl Limits { - /// The default limit configuration if the given character is the base. - pub fn for_char(c: char) -> Self { - match unicode_math_class::class(c) { - Some(MathClass::Large) => { - if is_integral_char(c) { - Limits::Never - } else { - Limits::Display - } - } - Some(MathClass::Relation) => Limits::Always, - _ => Limits::Never, - } - } - - /// The default limit configuration for a math class. - pub fn for_class(class: MathClass) -> Self { - match class { - MathClass::Large => Self::Display, - MathClass::Relation => Self::Always, - _ => Self::Never, +/// Lays out a [`PrimeElem`]. +#[typst_macros::time(name = "math.primes", span = elem.span())] +pub fn layout_primes( + elem: &Packed<PrimesElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + match *elem.count() { + count @ 1..=4 => { + let c = match count { + 1 => '′', + 2 => '″', + 3 => '‴', + 4 => '⁗', + _ => unreachable!(), + }; + let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; + ctx.push(f); } - } - - /// Whether limits should be displayed in this context. - pub fn active(&self, styles: StyleChain) -> bool { - match self { - Self::Always => true, - Self::Display => EquationElem::size_in(styles) == MathSize::Display, - Self::Never => false, + count => { + // Custom amount of primes + let prime = + ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame(); + let width = prime.width() * (count + 1) as f64 / 2.0; + let mut frame = Frame::soft(Size::new(width, prime.height())); + frame.set_baseline(prime.ascent()); + + for i in 0..count { + frame.push_frame( + Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()), + prime.clone(), + ) + } + ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); } } + Ok(()) } -/// If an AttachElem's base is also an AttachElem, merge attachments into the -/// base AttachElem where possible. -fn merge_base(elem: &Packed<AttachElem>) -> Option<Packed<AttachElem>> { - // Extract from an EquationElem. - let mut base = elem.base(); - if let Some(equation) = base.to_packed::<EquationElem>() { - base = equation.body(); - } - - // Move attachments from elem into base where possible. - if let Some(base) = base.to_packed::<AttachElem>() { - let mut elem = elem.clone(); - let mut base = base.clone(); - - macro_rules! merge { - ($content:ident) => { - if base.$content.is_none() && elem.$content.is_some() { - base.$content = elem.$content.clone(); - elem.$content = None; - } - }; - } - - merge!(t); - merge!(b); - merge!(tl); - merge!(tr); - merge!(bl); - merge!(br); - - elem.base = base.pack(); - return Some(elem); - } +/// Lays out a [`ScriptsElem`]. +#[typst_macros::time(name = "math.scripts", span = elem.span())] +pub fn layout_scripts( + elem: &Packed<ScriptsElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + fragment.set_limits(Limits::Never); + ctx.push(fragment); + Ok(()) +} - None +/// Lays out a [`LimitsElem`]. +#[typst_macros::time(name = "math.limits", span = elem.span())] +pub fn layout_limits( + elem: &Packed<LimitsElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; + let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + fragment.set_limits(limits); + ctx.push(fragment); + Ok(()) } /// Get the size to stretch the base to, if the attach argument is true. @@ -326,7 +168,7 @@ fn stretch_size( base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles)) } -/// Layout the attachments. +/// Lay out the attachments. fn layout_attachments( ctx: &mut MathContext, styles: StyleChain, @@ -671,8 +513,3 @@ fn math_kern( // result in glyphs colliding. summed_kern(corr_height_top).max(summed_kern(corr_height_bot)) } - -/// Determines if the character is one of a variety of integral signs. -fn is_integral_char(c: char) -> bool { - ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c) -} diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs new file mode 100644 index 00000000..994e0e2f --- /dev/null +++ b/crates/typst-layout/src/math/cancel.rs @@ -0,0 +1,144 @@ +use comemo::Track; +use typst_library::diag::{At, SourceResult}; +use typst_library::foundations::{Context, Packed, Smart, StyleChain}; +use typst_library::layout::{Abs, Angle, Frame, FrameItem, Point, Rel, Size, Transform}; +use typst_library::math::{CancelAngle, CancelElem}; +use typst_library::text::TextElem; +use typst_library::visualize::{FixedStroke, Geometry}; +use typst_syntax::Span; + +use super::{scaled_font_size, FrameFragment, MathContext}; + +/// Lays out a [`CancelElem`]. +#[typst_macros::time(name = "math.cancel", span = elem.span())] +pub fn layout_cancel( + elem: &Packed<CancelElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let body = ctx.layout_into_fragment(elem.body(), styles)?; + + // Preserve properties of body. + let body_class = body.class(); + let body_italics = body.italics_correction(); + let body_attach = body.accent_attach(); + let body_text_like = body.is_text_like(); + + let mut body = body.into_frame(); + let body_size = body.size(); + let span = elem.span(); + let length = elem.length(styles).at(scaled_font_size(ctx, styles)); + + let stroke = elem.stroke(styles).unwrap_or(FixedStroke { + paint: TextElem::fill_in(styles).as_decoration(), + ..Default::default() + }); + + let invert = elem.inverted(styles); + let cross = elem.cross(styles); + let angle = elem.angle(styles); + + let invert_first_line = !cross && invert; + let first_line = draw_cancel_line( + ctx, + length, + stroke.clone(), + invert_first_line, + &angle, + body_size, + styles, + span, + )?; + + // The origin of our line is the very middle of the element. + let center = body_size.to_point() / 2.0; + body.push_frame(center, first_line); + + if cross { + // Draw the second line. + let second_line = + draw_cancel_line(ctx, length, stroke, true, &angle, body_size, styles, span)?; + + body.push_frame(center, second_line); + } + + ctx.push( + FrameFragment::new(ctx, styles, body) + .with_class(body_class) + .with_italics_correction(body_italics) + .with_accent_attach(body_attach) + .with_text_like(body_text_like), + ); + + Ok(()) +} + +/// Draws a cancel line. +#[allow(clippy::too_many_arguments)] +fn draw_cancel_line( + ctx: &mut MathContext, + length_scale: Rel<Abs>, + stroke: FixedStroke, + invert: bool, + angle: &Smart<CancelAngle>, + body_size: Size, + styles: StyleChain, + span: Span, +) -> SourceResult<Frame> { + let default = default_angle(body_size); + let mut angle = match angle { + // Non specified angle defaults to the diagonal + Smart::Auto => default, + Smart::Custom(angle) => match angle { + // This specifies the absolute angle w.r.t y-axis clockwise. + CancelAngle::Angle(v) => *v, + // This specifies a function that takes the default angle as input. + CancelAngle::Func(func) => func + .call(ctx.engine, Context::new(None, Some(styles)).track(), [default])? + .cast() + .at(span)?, + }, + }; + + // invert means flipping along the y-axis + if invert { + angle *= -1.0; + } + + // same as above, the default length is the diagonal of the body box. + let default_length = body_size.to_point().hypot(); + let length = length_scale.relative_to(default_length); + + // Draw a vertical line of length and rotate it by angle + let start = Point::new(Abs::zero(), length / 2.0); + let delta = Point::new(Abs::zero(), -length); + + let mut frame = Frame::soft(body_size); + frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span)); + + // Having the middle of the line at the origin is convenient here. + frame.transform(Transform::rotate(angle)); + Ok(frame) +} + +/// The default line angle for a body of the given size. +fn default_angle(body: Size) -> Angle { + // The default cancel line is the diagonal. + // We infer the default angle from + // the diagonal w.r.t to the body box. + // + // The returned angle is in the range of [0, Pi/2] + // + // Note that the angle is computed w.r.t to the y-axis + // + // B + // /| + // diagonal / | height + // / | + // / | + // O ---- + // width + let (width, height) = (body.x, body.y); + let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2]) + Angle::rad(default_angle) +} diff --git a/crates/typst/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 744a15c5..50686333 100644 --- a/crates/typst/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -1,90 +1,47 @@ -use crate::diag::{bail, SourceResult}; -use crate::foundations::{elem, Content, Packed, StyleChain, Value}; -use crate::layout::{Em, Frame, FrameItem, Point, Size}; -use crate::math::{ +use typst_library::diag::SourceResult; +use typst_library::foundations::{Content, Packed, StyleChain}; +use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; +use typst_library::math::{BinomElem, FracElem}; +use typst_library::text::TextElem; +use typst_library::visualize::{FixedStroke, Geometry}; +use typst_syntax::Span; + +use super::{ scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment, - GlyphFragment, LayoutMath, MathContext, Scaled, DELIM_SHORT_FALL, + GlyphFragment, MathContext, DELIM_SHORT_FALL, }; -use crate::syntax::{Span, Spanned}; -use crate::text::TextElem; -use crate::visualize::{FixedStroke, Geometry}; const FRAC_AROUND: Em = Em::new(0.1); -/// A mathematical fraction. -/// -/// # Example -/// ```example -/// $ 1/2 < (x+1)/2 $ -/// $ ((x+1)) / 2 = frac(a, b) $ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Use a slash to turn neighbouring -/// expressions into a fraction. Multiple atoms can be grouped into a single -/// expression using round grouping parenthesis. Such parentheses are removed -/// from the output, but you can nest multiple to force them. -#[elem(title = "Fraction", LayoutMath)] -pub struct FracElem { - /// The fraction's numerator. - #[required] - pub num: Content, - - /// The fraction's denominator. - #[required] - pub denom: Content, -} - -impl LayoutMath for Packed<FracElem> { - #[typst_macros::time(name = "math.frac", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout( - ctx, - styles, - self.num(), - std::slice::from_ref(self.denom()), - false, - self.span(), - ) - } -} - -/// A binomial expression. -/// -/// # Example -/// ```example -/// $ binom(n, k) $ -/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ -/// ``` -#[elem(title = "Binomial", LayoutMath)] -pub struct BinomElem { - /// The binomial's upper index. - #[required] - pub upper: Content, - - /// The binomial's lower index. - #[required] - #[variadic] - #[parse( - let values = args.all::<Spanned<Value>>()?; - if values.is_empty() { - // Prevents one element binomials - bail!(args.span, "missing argument: lower"); - } - values.into_iter().map(|spanned| spanned.v.display()).collect() - )] - pub lower: Vec<Content>, +/// Lays out a [`FracElem`]. +#[typst_macros::time(name = "math.frac", span = elem.span())] +pub fn layout_frac( + elem: &Packed<FracElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_frac_like( + ctx, + styles, + elem.num(), + std::slice::from_ref(elem.denom()), + false, + elem.span(), + ) } -impl LayoutMath for Packed<BinomElem> { - #[typst_macros::time(name = "math.binom", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout(ctx, styles, self.upper(), self.lower(), true, self.span()) - } +/// Lays out a [`BinomElem`]. +#[typst_macros::time(name = "math.binom", span = elem.span())] +pub fn layout_binom( + elem: &Packed<BinomElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) } /// Layout a fraction or binomial. -fn layout( +fn layout_frac_like( ctx: &mut MathContext, styles: StyleChain, num: &Content, diff --git a/crates/typst/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index a3fcc9c6..19a4494e 100644 --- a/crates/typst/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,22 +1,25 @@ use std::fmt::{self, Debug, Formatter}; +use rustybuzz::Feature; use smallvec::SmallVec; -use ttf_parser::gsub::AlternateSet; +use ttf_parser::gsub::{ + AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable, +}; +use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::{GlyphId, Rect}; +use typst_library::foundations::StyleChain; +use typst_library::introspection::Tag; +use typst_library::layout::{ + Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, +}; +use typst_library::math::{EquationElem, MathSize}; +use typst_library::model::{Destination, LinkElem}; +use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; +use typst_library::visualize::Paint; +use typst_syntax::Span; use unicode_math_class::MathClass; -use crate::foundations::StyleChain; -use crate::introspection::Tag; -use crate::layout::{ - Abs, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, -}; -use crate::math::{ - scaled_font_size, EquationElem, Limits, MathContext, MathSize, Scaled, -}; -use crate::model::{Destination, LinkElem}; -use crate::syntax::Span; -use crate::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; -use crate::visualize::Paint; +use super::{scaled_font_size, stretch_glyph, MathContext, Scaled}; #[derive(Debug, Clone)] pub enum MathFragment { @@ -350,7 +353,6 @@ impl GlyphFragment { pub fn into_variant(self) -> VariantFragment { VariantFragment { c: self.c, - id: Some(self.id), font_size: self.font_size, italics_correction: self.italics_correction, accent_attach: self.accent_attach, @@ -406,6 +408,26 @@ impl GlyphFragment { self.set_id(ctx, alt_id); } } + + /// Try to stretch a glyph to a desired height. + pub fn stretch_vertical( + self, + ctx: &MathContext, + height: Abs, + short_fall: Abs, + ) -> VariantFragment { + stretch_glyph(ctx, self, height, short_fall, Axis::Y) + } + + /// Try to stretch a glyph to a desired width. + pub fn stretch_horizontal( + self, + ctx: &MathContext, + width: Abs, + short_fall: Abs, + ) -> VariantFragment { + stretch_glyph(ctx, self, width, short_fall, Axis::X) + } } impl Debug for GlyphFragment { @@ -417,7 +439,6 @@ impl Debug for GlyphFragment { #[derive(Clone)] pub struct VariantFragment { pub c: char, - pub id: Option<GlyphId>, pub italics_correction: Abs, pub accent_attach: Abs, pub frame: Frame, @@ -582,3 +603,102 @@ fn kern_at_height( Some(kern.kern(i)?.scaled(ctx, font_size)) } + +/// Describes in which situation a frame should use limits for attachments. +#[derive(Debug, Copy, Clone)] +pub enum Limits { + /// Always scripts. + Never, + /// Display limits only in `display` math. + Display, + /// Always limits. + Always, +} + +impl Limits { + /// The default limit configuration if the given character is the base. + pub fn for_char(c: char) -> Self { + match unicode_math_class::class(c) { + Some(MathClass::Large) => { + if is_integral_char(c) { + Limits::Never + } else { + Limits::Display + } + } + Some(MathClass::Relation) => Limits::Always, + _ => Limits::Never, + } + } + + /// The default limit configuration for a math class. + pub fn for_class(class: MathClass) -> Self { + match class { + MathClass::Large => Self::Display, + MathClass::Relation => Self::Always, + _ => Self::Never, + } + } + + /// Whether limits should be displayed in this context. + pub fn active(&self, styles: StyleChain) -> bool { + match self { + Self::Always => true, + Self::Display => EquationElem::size_in(styles) == MathSize::Display, + Self::Never => false, + } + } +} + +/// Determines if the character is one of a variety of integral signs. +fn is_integral_char(c: char) -> bool { + ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c) +} + +/// An OpenType substitution table that is applicable to glyph-wise substitutions. +pub enum GlyphwiseSubsts<'a> { + Single(SingleSubstitution<'a>), + Alternate(AlternateSubstitution<'a>, u32), +} + +impl<'a> GlyphwiseSubsts<'a> { + pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> { + let table = gsub + .features + .find(ttf_parser::Tag(feature.tag.0)) + .and_then(|feature| feature.lookup_indices.get(0)) + .and_then(|index| gsub.lookups.get(index))?; + let table = table.subtables.get::<SubstitutionSubtable>(0)?; + match table { + SubstitutionSubtable::Single(single_glyphs) => { + Some(Self::Single(single_glyphs)) + } + SubstitutionSubtable::Alternate(alt_glyphs) => { + Some(Self::Alternate(alt_glyphs, feature.value)) + } + _ => None, + } + } + + pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> { + match self { + Self::Single(single) => match single { + SingleSubstitution::Format1 { coverage, delta } => coverage + .get(glyph_id) + .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))), + SingleSubstitution::Format2 { coverage, substitutes } => { + coverage.get(glyph_id).and_then(|idx| substitutes.get(idx)) + } + }, + Self::Alternate(alternate, value) => alternate + .coverage + .get(glyph_id) + .and_then(|idx| alternate.alternate_sets.get(idx)) + .and_then(|set| set.alternates.get(*value as u16)), + } + } + + pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { + self.try_apply(glyph_id).unwrap_or(glyph_id) + } +} diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs new file mode 100644 index 00000000..aba9012f --- /dev/null +++ b/crates/typst-layout/src/math/lr.rs @@ -0,0 +1,135 @@ +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::layout::{Abs, Axis, Length, Rel}; +use typst_library::math::{EquationElem, LrElem, MidElem}; +use unicode_math_class::MathClass; + +use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; + +/// Lays out an [`LrElem`]. +#[typst_macros::time(name = "math.lr", span = elem.span())] +pub fn layout_lr( + elem: &Packed<LrElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let mut body = elem.body(); + + // Extract from an EquationElem. + if let Some(equation) = body.to_packed::<EquationElem>() { + body = equation.body(); + } + + // Extract implicit LrElem. + if let Some(lr) = body.to_packed::<LrElem>() { + if lr.size(styles).is_auto() { + body = lr.body(); + } + } + + let mut fragments = ctx.layout_into_fragments(body, styles)?; + let axis = scaled!(ctx, styles, axis_height); + let max_extent = fragments + .iter() + .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) + .max() + .unwrap_or_default(); + + let relative_to = 2.0 * max_extent; + let height = elem.size(styles); + + // Scale up fragments at both ends. + match fragments.as_mut_slice() { + [one] => scale(ctx, styles, one, relative_to, height, None), + [first, .., last] => { + scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening)); + scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing)); + } + _ => {} + } + + // Handle MathFragment::Variant fragments that should be scaled up. + for fragment in &mut fragments { + if let MathFragment::Variant(ref mut variant) = fragment { + if variant.mid_stretched == Some(false) { + variant.mid_stretched = Some(true); + scale(ctx, styles, fragment, relative_to, height, Some(MathClass::Large)); + } + } + } + + // Remove weak SpacingFragment immediately after the opening or immediately + // before the closing. + let original_len = fragments.len(); + let mut index = 0; + fragments.retain(|fragment| { + index += 1; + (index != 2 && index + 1 != original_len) + || !matches!(fragment, MathFragment::Spacing(_, true)) + }); + + ctx.extend(fragments); + + Ok(()) +} + +/// Lays out a [`MidElem`]. +#[typst_macros::time(name = "math.mid", span = elem.span())] +pub fn layout_mid( + elem: &Packed<MidElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?; + + for fragment in &mut fragments { + match fragment { + MathFragment::Glyph(glyph) => { + let mut new = glyph.clone().into_variant(); + new.mid_stretched = Some(false); + new.class = MathClass::Fence; + *fragment = MathFragment::Variant(new); + } + MathFragment::Variant(variant) => { + variant.mid_stretched = Some(false); + variant.class = MathClass::Fence; + } + _ => {} + } + } + + ctx.extend(fragments); + Ok(()) +} + +/// Scale a math fragment to a height. +fn scale( + ctx: &mut MathContext, + styles: StyleChain, + fragment: &mut MathFragment, + relative_to: Abs, + height: Smart<Rel<Length>>, + apply: Option<MathClass>, +) { + if matches!( + fragment.class(), + MathClass::Opening | MathClass::Closing | MathClass::Fence + ) { + // This unwrap doesn't really matter. If it is None, then the fragment + // won't be stretchable anyways. + let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default()); + stretch_fragment( + ctx, + styles, + fragment, + Some(Axis::Y), + Some(relative_to), + height, + short_fall, + ); + + if let Some(class) = apply { + fragment.set_class(class); + } + } +} diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs new file mode 100644 index 00000000..6c8b0455 --- /dev/null +++ b/crates/typst-layout/src/math/mat.rs @@ -0,0 +1,333 @@ +use typst_library::diag::{bail, SourceResult}; +use typst_library::foundations::{Content, Packed, StyleChain}; +use typst_library::layout::{ + Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, +}; +use typst_library::math::{Augment, AugmentOffsets, CasesElem, MatElem, VecElem}; +use typst_library::text::TextElem; +use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; +use typst_syntax::Span; + +use super::{ + alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator, + AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, + Scaled, DELIM_SHORT_FALL, +}; + +const VERTICAL_PADDING: Ratio = Ratio::new(0.1); +const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); + +/// Lays out a [`VecElem`]. +#[typst_macros::time(name = "math.vec", span = elem.span())] +pub fn layout_vec( + elem: &Packed<VecElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let delim = elem.delim(styles); + let frame = layout_vec_body( + ctx, + styles, + elem.children(), + elem.align(styles), + elem.gap(styles).at(scaled_font_size(ctx, styles)), + LeftRightAlternator::Right, + )?; + + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span()) +} + +/// Lays out a [`MatElem`]. +#[typst_macros::time(name = "math.mat", span = elem.span())] +pub fn layout_mat( + elem: &Packed<MatElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let augment = elem.augment(styles); + let rows = elem.rows(); + + if let Some(aug) = &augment { + for &offset in &aug.hline.0 { + if offset == 0 || offset.unsigned_abs() >= rows.len() { + bail!( + elem.span(), + "cannot draw a horizontal line after row {} of a matrix with {} rows", + if offset < 0 { rows.len() as isize + offset } else { offset }, + rows.len() + ); + } + } + + let ncols = elem.rows().first().map_or(0, |row| row.len()); + + for &offset in &aug.vline.0 { + if offset == 0 || offset.unsigned_abs() >= ncols { + bail!( + elem.span(), + "cannot draw a vertical line after column {} of a matrix with {} columns", + if offset < 0 { ncols as isize + offset } else { offset }, + ncols + ); + } + } + } + + let font_size = scaled_font_size(ctx, styles); + let column_gap = elem.column_gap(styles).at(font_size); + let row_gap = elem.row_gap(styles).at(font_size); + let delim = elem.delim(styles); + let frame = layout_mat_body( + ctx, + styles, + rows, + elem.align(styles), + augment, + Axes::new(column_gap, row_gap), + elem.span(), + )?; + + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span()) +} + +/// Lays out a [`CasesElem`]. +#[typst_macros::time(name = "math.cases", span = elem.span())] +pub fn layout_cases( + elem: &Packed<CasesElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let delim = elem.delim(styles); + let frame = layout_vec_body( + ctx, + styles, + elem.children(), + FixedAlignment::Start, + elem.gap(styles).at(scaled_font_size(ctx, styles)), + LeftRightAlternator::None, + )?; + + let (open, close) = + if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; + + layout_delimiters(ctx, styles, frame, open, close, elem.span()) +} + +/// Layout the inner contents of a vector. +fn layout_vec_body( + ctx: &mut MathContext, + styles: StyleChain, + column: &[Content], + align: FixedAlignment, + row_gap: Rel<Abs>, + alternator: LeftRightAlternator, +) -> SourceResult<Frame> { + let gap = row_gap.relative_to(ctx.region.size.y); + + let denom_style = style_for_denominator(styles); + let mut flat = vec![]; + for child in column { + flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?); + } + // We pad ascent and descent with the ascent and descent of the paren + // to ensure that normal vectors are aligned with others unless they are + // way too big. + let paren = + GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); + Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent)))) +} + +/// Layout the inner contents of a matrix. +fn layout_mat_body( + ctx: &mut MathContext, + styles: StyleChain, + rows: &[Vec<Content>], + align: FixedAlignment, + augment: Option<Augment<Abs>>, + gap: Axes<Rel<Abs>>, + span: Span, +) -> SourceResult<Frame> { + let ncols = rows.first().map_or(0, |row| row.len()); + let nrows = rows.len(); + if ncols == 0 || nrows == 0 { + return Ok(Frame::soft(Size::zero())); + } + + let gap = gap.zip_map(ctx.region.size, Rel::relative_to); + let half_gap = gap * 0.5; + + // We provide a default stroke thickness that scales + // with font size to ensure that augmentation lines + // look correct by default at all matrix sizes. + // The line cap is also set to square because it looks more "correct". + let font_size = scaled_font_size(ctx, styles); + let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.at(font_size); + let default_stroke = FixedStroke { + thickness: default_stroke_thickness, + paint: TextElem::fill_in(styles).as_decoration(), + cap: LineCap::Square, + ..Default::default() + }; + + let (hline, vline, stroke) = match augment { + Some(augment) => { + // We need to get stroke here for ownership. + let stroke = augment.stroke.unwrap_or_default().unwrap_or(default_stroke); + (augment.hline, augment.vline, stroke) + } + _ => (AugmentOffsets::default(), AugmentOffsets::default(), default_stroke), + }; + + // Before the full matrix body can be laid out, the + // individual cells must first be independently laid out + // so we can ensure alignment across rows and columns. + + // This variable stores the maximum ascent and descent for each row. + let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; + + // We want to transpose our data layout to columns + // before final layout. For efficiency, the columns + // variable is set up here and newly generated + // individual cells are then added to it. + let mut cols = vec![vec![]; ncols]; + + let denom_style = style_for_denominator(styles); + // We pad ascent and descent with the ascent and descent of the paren + // to ensure that normal matrices are aligned with others unless they are + // way too big. + let paren = + GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); + + for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { + for (cell, col) in row.iter().zip(&mut cols) { + let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?; + + ascent.set_max(cell.ascent().max(paren.ascent)); + descent.set_max(cell.descent().max(paren.descent)); + + col.push(cell); + } + } + + // For each row, combine maximum ascent and descent into a row height. + // Sum the row heights, then add the total height of the gaps between rows. + let total_height = + heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64; + + // Width starts at zero because it can't be calculated until later + let mut frame = Frame::soft(Size::new(Abs::zero(), total_height)); + + let mut x = Abs::zero(); + + for (index, col) in cols.into_iter().enumerate() { + let AlignmentResult { points, width: rcol } = alignments(&col); + + let mut y = Abs::zero(); + + for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { + let cell = cell.into_line_frame(&points, LeftRightAlternator::Right); + let pos = Point::new( + if points.is_empty() { + x + align.position(rcol - cell.width()) + } else { + x + }, + y + ascent - cell.ascent(), + ); + + frame.push_frame(pos, cell); + + y += ascent + descent + gap.y; + } + + // Advance to the end of the column + x += rcol; + + // If a vertical line should be inserted after this column + if vline.0.contains(&(index as isize + 1)) + || vline.0.contains(&(1 - ((ncols - index) as isize))) + { + frame.push( + Point::with_x(x + half_gap.x), + line_item(total_height, true, stroke.clone(), span), + ); + } + + // Advance to the start of the next column + x += gap.x; + } + + // Once all the columns are laid out, the total width can be calculated + let total_width = x - gap.x; + + // This allows the horizontal lines to be laid out + for line in hline.0 { + let real_line = + if line < 0 { nrows - line.unsigned_abs() } else { line as usize }; + let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>() + + gap.y * (real_line - 1) as f64) + + half_gap.y; + + frame.push( + Point::with_y(offset), + line_item(total_width, false, stroke.clone(), span), + ); + } + + frame.size_mut().x = total_width; + + Ok(frame) +} + +fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem { + let line_geom = if vertical { + Geometry::Line(Point::with_y(length)) + } else { + Geometry::Line(Point::with_x(length)) + }; + + FrameItem::Shape( + Shape { + geometry: line_geom, + fill: None, + fill_rule: FillRule::default(), + stroke: Some(stroke), + }, + span, + ) +} + +/// Layout the outer wrapper around the body of a vector or matrix. +fn layout_delimiters( + ctx: &mut MathContext, + styles: StyleChain, + mut frame: Frame, + left: Option<char>, + right: Option<char>, + span: Span, +) -> SourceResult<()> { + let font_size = scaled_font_size(ctx, styles); + let short_fall = DELIM_SHORT_FALL.at(font_size); + let axis = ctx.constants.axis_height().scaled(ctx, font_size); + let height = frame.height(); + let target = height + VERTICAL_PADDING.of(height); + frame.set_baseline(height / 2.0 + axis); + + if let Some(left) = left { + let mut left = GlyphFragment::new(ctx, styles, left, span) + .stretch_vertical(ctx, target, short_fall); + left.align_on_axis(ctx, delimiter_alignment(left.c)); + ctx.push(left); + } + + ctx.push(FrameFragment::new(ctx, styles, frame)); + + if let Some(right) = right { + let mut right = GlyphFragment::new(ctx, styles, right, span) + .stretch_vertical(ctx, target, short_fall); + right.align_on_axis(ctx, delimiter_alignment(right.c)); + ctx.push(right); + } + + Ok(()) +} diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs new file mode 100644 index 00000000..b3dde977 --- /dev/null +++ b/crates/typst-layout/src/math/mod.rs @@ -0,0 +1,703 @@ +#[macro_use] +mod shared; +mod accent; +mod attach; +mod cancel; +mod frac; +mod fragment; +mod lr; +mod mat; +mod root; +mod run; +mod stretch; +mod text; +mod underover; + +use ttf_parser::gsub::SubstitutionSubtable; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain}; +use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem}; +use typst_library::layout::{ + Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem, + InlineItem, OuterHAlignment, PlaceElem, Point, Region, Regions, Size, Spacing, + SpecificAlignment, VAlignment, +}; +use typst_library::math::*; +use typst_library::model::ParElem; +use typst_library::routines::{Arenas, RealizationKind}; +use typst_library::text::{ + families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, + TextElem, TextSize, +}; +use typst_library::World; +use typst_syntax::Span; +use typst_utils::Numeric; +use unicode_math_class::MathClass; + +use self::fragment::{ + FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment, +}; +use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder}; +use self::shared::*; +use self::stretch::{stretch_fragment, stretch_glyph}; + +/// Layout an inline equation (in a paragraph). +#[typst_macros::time(span = elem.span())] +pub fn layout_equation_inline( + elem: &Packed<EquationElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, +) -> SourceResult<Vec<InlineItem>> { + assert!(!elem.block(styles)); + + let font = find_math_font(engine, styles, elem.span())?; + + let mut locator = locator.split(); + let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font); + let run = ctx.layout_into_run(&elem.body, styles)?; + + let mut items = if run.row_count() == 1 { + run.into_par_items() + } else { + vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())] + }; + + // An empty equation should have a height, so we still create a frame + // (which is then resized in the loop). + if items.is_empty() { + items.push(InlineItem::Frame(Frame::soft(Size::zero()))); + } + + for item in &mut items { + let InlineItem::Frame(frame) = item else { continue }; + + let font_size = scaled_font_size(&ctx, styles); + let slack = ParElem::leading_in(styles) * 0.7; + + let (t, b) = font.edges( + TextElem::top_edge_in(styles), + TextElem::bottom_edge_in(styles), + font_size, + TextEdgeBounds::Frame(frame), + ); + + let ascent = t.max(frame.ascent() - slack); + let descent = b.max(frame.descent() - slack); + frame.translate(Point::with_y(ascent - frame.baseline())); + frame.size_mut().y = ascent + descent; + } + + Ok(items) +} + +/// Layout a block-level equation (in a flow). +#[typst_macros::time(span = elem.span())] +pub fn layout_equation_block( + elem: &Packed<EquationElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + assert!(elem.block(styles)); + + let span = elem.span(); + let font = find_math_font(engine, styles, span)?; + + let mut locator = locator.split(); + let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font); + let full_equation_builder = ctx + .layout_into_run(&elem.body, styles)? + .multiline_frame_builder(&ctx, styles); + let width = full_equation_builder.size.x; + + let equation_builders = if BlockElem::breakable_in(styles) { + let mut rows = full_equation_builder.frames.into_iter().peekable(); + let mut equation_builders = vec![]; + let mut last_first_pos = Point::zero(); + let mut regions = regions; + + loop { + // Keep track of the position of the first row in this region, + // so that the offset can be reverted later. + let Some(&(_, first_pos)) = rows.peek() else { break }; + last_first_pos = first_pos; + + let mut frames = vec![]; + let mut height = Abs::zero(); + while let Some((sub, pos)) = rows.peek() { + let mut pos = *pos; + pos.y -= first_pos.y; + + // Finish this region if the line doesn't fit. Only do it if + // we placed at least one line _or_ we still have non-last + // regions. Crucially, we don't want to infinitely create + // new regions which are too small. + if !regions.size.y.fits(sub.height() + pos.y) + && (regions.may_progress() + || (regions.may_break() && !frames.is_empty())) + { + break; + } + + let (sub, _) = rows.next().unwrap(); + height = height.max(pos.y + sub.height()); + frames.push((sub, pos)); + } + + equation_builders + .push(MathRunFrameBuilder { frames, size: Size::new(width, height) }); + regions.next(); + } + + // Append remaining rows to the equation builder of the last region. + if let Some(equation_builder) = equation_builders.last_mut() { + equation_builder.frames.extend(rows.map(|(frame, mut pos)| { + pos.y -= last_first_pos.y; + (frame, pos) + })); + + let height = equation_builder + .frames + .iter() + .map(|(frame, pos)| frame.height() + pos.y) + .max() + .unwrap_or(equation_builder.size.y); + + equation_builder.size.y = height; + } + + // Ensure that there is at least one frame, even for empty equations. + if equation_builders.is_empty() { + equation_builders + .push(MathRunFrameBuilder { frames: vec![], size: Size::zero() }); + } + + equation_builders + } else { + vec![full_equation_builder] + }; + + let Some(numbering) = (**elem).numbering(styles) else { + let frames = equation_builders + .into_iter() + .map(MathRunFrameBuilder::build) + .collect(); + return Ok(Fragment::frames(frames)); + }; + + let pod = Region::new(regions.base(), Axes::splat(false)); + let counter = Counter::of(EquationElem::elem()) + .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? + .spanned(span); + let number = + (engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?; + + static NUMBER_GUTTER: Em = Em::new(0.5); + let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); + + let number_align = match elem.number_align(styles) { + SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon), + SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v), + SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v), + }; + + // Add equation numbers to each equation region. + let region_count = equation_builders.len(); + let frames = equation_builders + .into_iter() + .map(|builder| { + if builder.frames.is_empty() && region_count > 1 { + // Don't number empty regions, but do number empty equations. + return builder.build(); + } + add_equation_number( + builder, + number.clone(), + number_align.resolve(styles), + AlignElem::alignment_in(styles).resolve(styles).x, + regions.size.x, + full_number_width, + ) + }) + .collect(); + + Ok(Fragment::frames(frames)) +} + +fn find_math_font( + engine: &mut Engine<'_>, + styles: StyleChain, + span: Span, +) -> SourceResult<Font> { + let variant = variant(styles); + let world = engine.world; + let Some(font) = families(styles).find_map(|family| { + let id = world.book().select(family, variant)?; + let font = world.font(id)?; + let _ = font.ttf().tables().math?.constants?; + Some(font) + }) else { + bail!(span, "current font does not support math"); + }; + Ok(font) +} + +fn add_equation_number( + equation_builder: MathRunFrameBuilder, + number: Frame, + number_align: Axes<FixedAlignment>, + equation_align: FixedAlignment, + region_size_x: Abs, + full_number_width: Abs, +) -> Frame { + let first = + equation_builder.frames.first().map_or( + (equation_builder.size, Point::zero(), Abs::zero()), + |(frame, pos)| (frame.size(), *pos, frame.baseline()), + ); + let last = + equation_builder.frames.last().map_or( + (equation_builder.size, Point::zero(), Abs::zero()), + |(frame, pos)| (frame.size(), *pos, frame.baseline()), + ); + let line_count = equation_builder.frames.len(); + let mut equation = equation_builder.build(); + + let width = if region_size_x.is_finite() { + region_size_x + } else { + equation.width() + 2.0 * full_number_width + }; + + let is_multiline = line_count >= 2; + let resizing_offset = resize_equation( + &mut equation, + &number, + number_align, + equation_align, + width, + is_multiline, + [first, last], + ); + equation.translate(Point::with_x(match (equation_align, number_align.x) { + (FixedAlignment::Start, FixedAlignment::Start) => full_number_width, + (FixedAlignment::End, FixedAlignment::End) => -full_number_width, + _ => Abs::zero(), + })); + + let x = match number_align.x { + FixedAlignment::Start => Abs::zero(), + FixedAlignment::End => equation.width() - number.width(), + _ => unreachable!(), + }; + let y = { + let align_baselines = |(_, pos, baseline): (_, Point, Abs), number: &Frame| { + resizing_offset.y + pos.y + baseline - number.baseline() + }; + match number_align.y { + FixedAlignment::Start => align_baselines(first, &number), + FixedAlignment::Center if !is_multiline => align_baselines(first, &number), + // In this case, the center lines (not baselines) of the number frame + // and the equation frame shall be aligned. + FixedAlignment::Center => (equation.height() - number.height()) / 2.0, + FixedAlignment::End => align_baselines(last, &number), + } + }; + + equation.push_frame(Point::new(x, y), number); + equation +} + +/// Resize the equation's frame accordingly so that it encompasses the number. +fn resize_equation( + equation: &mut Frame, + number: &Frame, + number_align: Axes<FixedAlignment>, + equation_align: FixedAlignment, + width: Abs, + is_multiline: bool, + [first, last]: [(Axes<Abs>, Point, Abs); 2], +) -> Point { + if matches!(number_align.y, FixedAlignment::Center if is_multiline) { + // In this case, the center lines (not baselines) of the number frame + // and the equation frame shall be aligned. + return equation.resize( + Size::new(width, equation.height().max(number.height())), + Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Center), + ); + } + + let excess_above = Abs::zero().max({ + if !is_multiline || matches!(number_align.y, FixedAlignment::Start) { + let (.., baseline) = first; + number.baseline() - baseline + } else { + Abs::zero() + } + }); + let excess_below = Abs::zero().max({ + if !is_multiline || matches!(number_align.y, FixedAlignment::End) { + let (size, .., baseline) = last; + (number.height() - number.baseline()) - (size.y - baseline) + } else { + Abs::zero() + } + }); + + // The vertical expansion is asymmetric on the top and bottom edges, so we + // first align at the top then translate the content downward later. + let resizing_offset = equation.resize( + Size::new(width, equation.height() + excess_above + excess_below), + Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Start), + ); + equation.translate(Point::with_y(excess_above)); + resizing_offset + Point::with_y(excess_above) +} + +/// The context for math layout. +struct MathContext<'a, 'v, 'e> { + // External. + engine: &'v mut Engine<'e>, + locator: &'v mut SplitLocator<'a>, + region: Region, + // Font-related. + font: &'a Font, + ttf: &'a ttf_parser::Face<'a>, + table: ttf_parser::math::Table<'a>, + constants: ttf_parser::math::Constants<'a>, + ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>, + glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>, + space_width: Em, + // Mutable. + fragments: Vec<MathFragment>, +} + +impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { + /// Create a new math context. + fn new( + engine: &'v mut Engine<'e>, + locator: &'v mut SplitLocator<'a>, + styles: StyleChain<'a>, + base: Size, + font: &'a Font, + ) -> Self { + let math_table = font.ttf().tables().math.unwrap(); + let gsub_table = font.ttf().tables().gsub; + let constants = math_table.constants.unwrap(); + + let ssty_table = gsub_table + .and_then(|gsub| { + gsub.features + .find(ttf_parser::Tag::from_bytes(b"ssty")) + .and_then(|feature| feature.lookup_indices.get(0)) + .and_then(|index| gsub.lookups.get(index)) + }) + .and_then(|ssty| ssty.subtables.get::<SubstitutionSubtable>(0)) + .and_then(|ssty| match ssty { + SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), + _ => None, + }); + + let features = features(styles); + let glyphwise_tables = gsub_table.map(|gsub| { + features + .into_iter() + .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) + .collect() + }); + + let ttf = font.ttf(); + let space_width = ttf + .glyph_index(' ') + .and_then(|id| ttf.glyph_hor_advance(id)) + .map(|advance| font.to_em(advance)) + .unwrap_or(THICK); + + Self { + engine, + locator, + region: Region::new(base, Axes::splat(false)), + font, + ttf: font.ttf(), + table: math_table, + constants, + ssty_table, + glyphwise_tables, + space_width, + fragments: vec![], + } + } + + /// Push a fragment. + fn push(&mut self, fragment: impl Into<MathFragment>) { + self.fragments.push(fragment.into()); + } + + /// Push multiple fragments. + fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) { + self.fragments.extend(fragments); + } + + /// Layout the given element and return the result as a [`MathRun`]. + fn layout_into_run( + &mut self, + elem: &Content, + styles: StyleChain, + ) -> SourceResult<MathRun> { + Ok(MathRun::new(self.layout_into_fragments(elem, styles)?)) + } + + /// Layout the given element and return the resulting [`MathFragment`]s. + fn layout_into_fragments( + &mut self, + elem: &Content, + styles: StyleChain, + ) -> SourceResult<Vec<MathFragment>> { + // The element's layout_math() changes the fragments held in this + // MathContext object, but for convenience this function shouldn't change + // them, so we restore the MathContext's fragments after obtaining the + // layout result. + let prev = std::mem::take(&mut self.fragments); + self.layout_into_self(elem, styles)?; + Ok(std::mem::replace(&mut self.fragments, prev)) + } + + /// Layout the given element and return the result as a + /// unified [`MathFragment`]. + fn layout_into_fragment( + &mut self, + elem: &Content, + styles: StyleChain, + ) -> SourceResult<MathFragment> { + Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles)) + } + + /// Layout the given element and return the result as a [`Frame`]. + fn layout_into_frame( + &mut self, + elem: &Content, + styles: StyleChain, + ) -> SourceResult<Frame> { + Ok(self.layout_into_fragment(elem, styles)?.into_frame()) + } + + /// Layout arbitrary content. + fn layout_into_self( + &mut self, + content: &Content, + styles: StyleChain, + ) -> SourceResult<()> { + let arenas = Arenas::default(); + let pairs = (self.engine.routines.realize)( + RealizationKind::Math, + self.engine, + self.locator, + &arenas, + content, + styles, + )?; + + let outer = styles; + for (elem, styles) in pairs { + // Hack because the font is fixed in math. + if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { + let frame = layout_external(elem, self, styles)?; + self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); + continue; + } + + layout_realized(elem, self, styles)?; + } + + Ok(()) + } +} + +/// Lays out a leaf element resulting from realization. +fn layout_realized( + elem: &Content, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + if let Some(elem) = elem.to_packed::<TagElem>() { + ctx.push(MathFragment::Tag(elem.tag.clone())); + } else if elem.is::<SpaceElem>() { + let font_size = scaled_font_size(ctx, styles); + ctx.push(MathFragment::Space(ctx.space_width.at(font_size))); + } else if elem.is::<LinebreakElem>() { + ctx.push(MathFragment::Linebreak); + } else if let Some(elem) = elem.to_packed::<HElem>() { + layout_h(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<TextElem>() { + self::text::layout_text(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<BoxElem>() { + layout_box(elem, ctx, styles)?; + } else if elem.is::<AlignPointElem>() { + ctx.push(MathFragment::Align); + } else if let Some(elem) = elem.to_packed::<ClassElem>() { + layout_class(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<AccentElem>() { + self::accent::layout_accent(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<AttachElem>() { + self::attach::layout_attach(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<PrimesElem>() { + self::attach::layout_primes(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<ScriptsElem>() { + self::attach::layout_scripts(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<LimitsElem>() { + self::attach::layout_limits(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<CancelElem>() { + self::cancel::layout_cancel(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<FracElem>() { + self::frac::layout_frac(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<BinomElem>() { + self::frac::layout_binom(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::<LrElem>() { + self::lr::layout_lr(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<MidElem>() { + self::lr::layout_mid(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<VecElem>() { + self::mat::layout_vec(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<MatElem>() { + self::mat::layout_mat(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<CasesElem>() { + self::mat::layout_cases(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<OpElem>() { + layout_op(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<RootElem>() { + self::root::layout_root(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<StretchElem>() { + self::stretch::layout_stretch(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<UnderlineElem>() { + self::underover::layout_underline(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<OverlineElem>() { + self::underover::layout_overline(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<UnderbraceElem>() { + self::underover::layout_underbrace(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<OverbraceElem>() { + self::underover::layout_overbrace(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<UnderbracketElem>() { + self::underover::layout_underbracket(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<OverbracketElem>() { + self::underover::layout_overbracket(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<UnderparenElem>() { + self::underover::layout_underparen(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<OverparenElem>() { + self::underover::layout_overparen(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<UndershellElem>() { + self::underover::layout_undershell(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::<OvershellElem>() { + self::underover::layout_overshell(elem, ctx, styles)? + } else { + let mut frame = layout_external(elem, ctx, styles)?; + if !frame.has_baseline() { + let axis = scaled!(ctx, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + } + ctx.push( + FrameFragment::new(ctx, styles, frame) + .with_spaced(true) + .with_ignorant(elem.is::<PlaceElem>()), + ); + } + + Ok(()) +} + +/// Lays out an [`BoxElem`]. +fn layout_box( + elem: &Packed<BoxElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap(); + let frame = (ctx.engine.routines.layout_box)( + elem, + ctx.engine, + ctx.locator.next(&elem.span()), + styles.chain(&local), + ctx.region.size, + )?; + ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true)); + Ok(()) +} + +/// Lays out an [`HElem`]. +fn layout_h( + elem: &Packed<HElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + if let Spacing::Rel(rel) = elem.amount() { + if rel.rel.is_zero() { + ctx.push(MathFragment::Spacing( + rel.abs.at(scaled_font_size(ctx, styles)), + elem.weak(styles), + )); + } + } + Ok(()) +} + +/// Lays out a [`ClassElem`]. +#[typst_macros::time(name = "math.op", span = elem.span())] +fn layout_class( + elem: &Packed<ClassElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let class = *elem.class(); + let style = EquationElem::set_class(Some(class)).wrap(); + let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?; + fragment.set_class(class); + fragment.set_limits(Limits::for_class(class)); + ctx.push(fragment); + Ok(()) +} + +/// Lays out an [`OpElem`]. +#[typst_macros::time(name = "math.op", span = elem.span())] +fn layout_op( + elem: &Packed<OpElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let fragment = ctx.layout_into_fragment(elem.text(), styles)?; + let italics = fragment.italics_correction(); + let accent_attach = fragment.accent_attach(); + let text_like = fragment.is_text_like(); + + ctx.push( + FrameFragment::new(ctx, styles, fragment.into_frame()) + .with_class(MathClass::Large) + .with_italics_correction(italics) + .with_accent_attach(accent_attach) + .with_text_like(text_like) + .with_limits(if elem.limits(styles) { + Limits::Display + } else { + Limits::Never + }), + ); + Ok(()) +} + +/// Layout into a frame with normal layout. +fn layout_external( + content: &Content, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<Frame> { + let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap(); + (ctx.engine.routines.layout_frame)( + ctx.engine, + content, + ctx.locator.next(&content.span()), + styles.chain(&local), + ctx.region, + ) +} diff --git a/crates/typst/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 66707c59..0bb2f539 100644 --- a/crates/typst/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -1,63 +1,26 @@ -use crate::diag::SourceResult; -use crate::foundations::{elem, func, Content, NativeElement, Packed, StyleChain}; -use crate::layout::{Abs, Frame, FrameItem, Point, Size}; -use crate::math::{ - style_cramped, EquationElem, FrameFragment, GlyphFragment, LayoutMath, MathContext, - MathSize, Scaled, -}; -use crate::syntax::Span; -use crate::text::TextElem; -use crate::visualize::{FixedStroke, Geometry}; - -/// A square root. -/// -/// ```example -/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $ -/// ``` -#[func(title = "Square Root")] -pub fn sqrt( - /// The call span of this function. - span: Span, - /// The expression to take the square root of. - radicand: Content, -) -> Content { - RootElem::new(radicand).pack().spanned(span) -} - -/// A general root. -/// -/// ```example -/// $ root(3, x) $ -/// ``` -#[elem(LayoutMath)] -pub struct RootElem { - /// Which root of the radicand to take. - #[positional] - pub index: Option<Content>, - - /// The expression to take the root of. - #[required] - pub radicand: Content, -} +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::layout::{Abs, Frame, FrameItem, Point, Size}; +use typst_library::math::{EquationElem, MathSize, RootElem}; +use typst_library::text::TextElem; +use typst_library::visualize::{FixedStroke, Geometry}; -impl LayoutMath for Packed<RootElem> { - #[typst_macros::time(name = "math.root", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout(ctx, styles, self.index(styles).as_ref(), self.radicand(), self.span()) - } -} +use super::{style_cramped, FrameFragment, GlyphFragment, MathContext}; -/// Layout a root. +/// Lays out a [`RootElem`]. /// /// TeXbook page 443, page 360 /// See also: <https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot> -fn layout( +#[typst_macros::time(name = "math.root", span = elem.span())] +pub fn layout_root( + elem: &Packed<RootElem>, ctx: &mut MathContext, styles: StyleChain, - index: Option<&Content>, - radicand: &Content, - span: Span, ) -> SourceResult<()> { + let index = elem.index(styles); + let radicand = elem.radicand(); + let span = elem.span(); + let gap = scaled!( ctx, styles, text: radical_vertical_gap, @@ -94,6 +57,7 @@ fn layout( // Layout the index. let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); let index = index + .as_ref() .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .transpose()?; diff --git a/crates/typst/src/math/row.rs b/crates/typst-layout/src/math/run.rs index a8422b1e..8f12c509 100644 --- a/crates/typst/src/math/row.rs +++ b/crates/typst-layout/src/math/run.rs @@ -1,16 +1,14 @@ use std::iter::once; +use typst_library::foundations::{Resolve, StyleChain}; +use typst_library::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size}; +use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN}; +use typst_library::model::ParElem; use unicode_math_class::MathClass; -use crate::foundations::{Resolve, StyleChain}; -use crate::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size}; -use crate::math::{ - alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext, - MathFragment, MathSize, -}; -use crate::model::ParElem; +use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment}; -pub const TIGHT_LEADING: Em = Em::new(0.25); +const TIGHT_LEADING: Em = Em::new(0.25); /// A linear collection of [`MathFragment`]s. #[derive(Debug, Default, Clone)] @@ -423,3 +421,49 @@ impl MathRunFrameBuilder { fn affects_row_height(fragment: &MathFragment) -> bool { !matches!(fragment, MathFragment::Align | MathFragment::Linebreak) } + +/// Create the spacing between two fragments in a given style. +fn spacing( + l: &MathFragment, + space: Option<MathFragment>, + r: &MathFragment, +) -> Option<MathFragment> { + use MathClass::*; + + let resolve = |v: Em, size_ref: &MathFragment| -> Option<MathFragment> { + let width = size_ref.font_size().map_or(Abs::zero(), |size| v.at(size)); + Some(MathFragment::Spacing(width, false)) + }; + let script = |f: &MathFragment| f.math_size().is_some_and(|s| s <= MathSize::Script); + + match (l.class(), r.class()) { + // No spacing before punctuation; thin spacing after punctuation, unless + // in script size. + (_, Punctuation) => None, + (Punctuation, _) if !script(l) => resolve(THIN, l), + + // No spacing after opening delimiters and before closing delimiters. + (Opening, _) | (_, Closing) => None, + + // Thick spacing around relations, unless followed by a another relation + // or in script size. + (Relation, Relation) => None, + (Relation, _) if !script(l) => resolve(THICK, l), + (_, Relation) if !script(r) => resolve(THICK, r), + + // Medium spacing around binary operators, unless in script size. + (Binary, _) if !script(l) => resolve(MEDIUM, l), + (_, Binary) if !script(r) => resolve(MEDIUM, r), + + // Thin spacing around large operators, unless to the left of + // an opening delimiter. TeXBook, p170 + (Large, Opening | Fence) => None, + (Large, _) => resolve(THIN, l), + (_, Large) => resolve(THIN, r), + + // Spacing around spaced frames. + _ if (l.is_spaced() || r.is_spaced()) => space, + + _ => None, + } +} diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs new file mode 100644 index 00000000..13477c10 --- /dev/null +++ b/crates/typst-layout/src/math/shared.rs @@ -0,0 +1,207 @@ +use ttf_parser::math::MathValue; +use typst_library::foundations::{Style, StyleChain}; +use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; +use typst_library::math::{EquationElem, MathSize}; +use typst_library::text::TextElem; +use typst_utils::LazyHash; + +use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; + +macro_rules! scaled { + ($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { + match typst_library::math::EquationElem::size_in($styles) { + typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), + _ => scaled!($ctx, $styles, $text), + } + }; + ($ctx:expr, $styles:expr, $name:ident) => { + $crate::math::Scaled::scaled( + $ctx.constants.$name(), + $ctx, + $crate::math::scaled_font_size($ctx, $styles), + ) + }; +} + +macro_rules! percent { + ($ctx:expr, $name:ident) => { + $ctx.constants.$name() as f64 / 100.0 + }; +} + +/// How much less high scaled delimiters can be than what they wrap. +pub const DELIM_SHORT_FALL: Em = Em::new(0.1); + +/// Converts some unit to an absolute length with the current font & font size. +pub trait Scaled { + fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; +} + +impl Scaled for i16 { + fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { + ctx.font.to_em(self).at(font_size) + } +} + +impl Scaled for u16 { + fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { + ctx.font.to_em(self).at(font_size) + } +} + +impl Scaled for MathValue<'_> { + fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { + self.value.scaled(ctx, font_size) + } +} + +/// Get the font size scaled with the `MathSize`. +pub fn scaled_font_size(ctx: &MathContext, styles: StyleChain) -> Abs { + let factor = match EquationElem::size_in(styles) { + MathSize::Display | MathSize::Text => 1.0, + MathSize::Script => percent!(ctx, script_percent_scale_down), + MathSize::ScriptScript => percent!(ctx, script_script_percent_scale_down), + }; + factor * TextElem::size_in(styles) +} + +/// Styles something as cramped. +pub fn style_cramped() -> LazyHash<Style> { + EquationElem::set_cramped(true).wrap() +} + +/// The style for subscripts in the current style. +pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] { + [style_for_superscript(styles), EquationElem::set_cramped(true).wrap()] +} + +/// The style for superscripts in the current style. +pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> { + EquationElem::set_size(match EquationElem::size_in(styles) { + MathSize::Display | MathSize::Text => MathSize::Script, + MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, + }) + .wrap() +} + +/// The style for numerators in the current style. +pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> { + EquationElem::set_size(match EquationElem::size_in(styles) { + MathSize::Display => MathSize::Text, + MathSize::Text => MathSize::Script, + MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, + }) + .wrap() +} + +/// The style for denominators in the current style. +pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] { + [style_for_numerator(styles), EquationElem::set_cramped(true).wrap()] +} + +/// How a delimieter should be aligned when scaling. +pub fn delimiter_alignment(delimiter: char) -> VAlignment { + match delimiter { + '⌜' | '⌝' => VAlignment::Top, + '⌞' | '⌟' => VAlignment::Bottom, + _ => VAlignment::Horizon, + } +} + +/// Stack rows on top of each other. +/// +/// Add a `gap` between each row and uses the baseline of the `baseline`-th +/// row for the whole frame. `alternator` controls the left/right alternating +/// alignment behavior of `AlignPointElem` in the rows. +pub fn stack( + rows: Vec<MathRun>, + align: FixedAlignment, + gap: Abs, + baseline: usize, + alternator: LeftRightAlternator, + minimum_ascent_descent: Option<(Abs, Abs)>, +) -> Frame { + let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect(); + let AlignmentResult { points, width } = alignments(&rows); + let rows: Vec<_> = rows + .into_iter() + .map(|row| row.into_line_frame(&points, alternator)) + .collect(); + + let padded_height = |height: Abs| { + height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d)) + }; + + let mut frame = Frame::soft(Size::new( + width, + rows.iter().map(|row| padded_height(row.height())).sum::<Abs>() + + rows.len().saturating_sub(1) as f64 * gap, + )); + + let mut y = Abs::zero(); + for (i, row) in rows.into_iter().enumerate() { + let x = if points.is_empty() { + align.position(width - row.width()) + } else { + Abs::zero() + }; + let ascent_padded_part = minimum_ascent_descent + .map_or(Abs::zero(), |(a, _)| (a - row.ascent())) + .max(Abs::zero()); + let pos = Point::new(x, y + ascent_padded_part); + if i == baseline { + frame.set_baseline(y + row.baseline() + ascent_padded_part); + } + y += padded_height(row.height()) + gap; + frame.push_frame(pos, row); + } + + frame +} + +/// Determine the positions of the alignment points, according to the input rows combined. +pub fn alignments(rows: &[MathRun]) -> AlignmentResult { + let mut widths = Vec::<Abs>::new(); + + let mut pending_width = Abs::zero(); + for row in rows { + let mut width = Abs::zero(); + let mut alignment_index = 0; + + for fragment in row.iter() { + if matches!(fragment, MathFragment::Align) { + if alignment_index < widths.len() { + widths[alignment_index].set_max(width); + } else { + widths.push(width.max(pending_width)); + } + width = Abs::zero(); + alignment_index += 1; + } else { + width += fragment.width(); + } + } + if widths.is_empty() { + pending_width.set_max(width); + } else if alignment_index < widths.len() { + widths[alignment_index].set_max(width); + } else { + widths.push(width.max(pending_width)); + } + } + + let mut points = widths; + for i in 1..points.len() { + let prev = points[i - 1]; + points[i] += prev; + } + AlignmentResult { + width: points.last().copied().unwrap_or(pending_width), + points, + } +} + +pub struct AlignmentResult { + pub points: Vec<Abs>, + pub width: Abs, +} diff --git a/crates/typst/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 18a42b18..6dc82014 100644 --- a/crates/typst/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -1,64 +1,42 @@ use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::LazyArray16; - -use crate::diag::SourceResult; -use crate::foundations::{elem, Content, Packed, Smart, StyleChain}; -use crate::layout::{Abs, Axis, Frame, Length, Point, Rel, Size, VAlignment}; -use crate::math::{ - scaled_font_size, GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, - VariantFragment, +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::layout::{Abs, Axis, Frame, Length, Point, Rel, Size}; +use typst_library::math::StretchElem; +use typst_utils::Get; + +use super::{ + delimiter_alignment, scaled_font_size, GlyphFragment, MathContext, MathFragment, + Scaled, VariantFragment, }; -use crate::utils::Get; /// Maximum number of times extenders can be repeated. const MAX_REPEATS: usize = 1024; -/// Stretches a glyph. -/// -/// This function can also be used to automatically stretch the base of an -/// attachment, so that it fits the top and bottom attachments. -/// -/// Note that only some glyphs can be stretched, and which ones can depend on -/// the math font being used. However, most math fonts are the same in this -/// regard. -/// -/// ```example -/// $ H stretch(=)^"define" U + p V $ -/// $ f : X stretch(->>, size: #150%)_"surjective" Y $ -/// $ x stretch(harpoons.ltrb, size: #3em) y -/// stretch(\[, size: #150%) z $ -/// ``` -#[elem(LayoutMath)] -pub struct StretchElem { - /// The glyph to stretch. - #[required] - pub body: Content, - - /// The size to stretch to, relative to the maximum size of the glyph and - /// its attachments. - pub size: Smart<Rel<Length>>, -} - -impl LayoutMath for Packed<StretchElem> { - #[typst_macros::time(name = "math.stretch", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(self.body(), styles)?; - stretch_fragment( - ctx, - styles, - &mut fragment, - None, - None, - self.size(styles), - Abs::zero(), - ); - ctx.push(fragment); - Ok(()) - } +/// Lays out a [`StretchElem`]. +#[typst_macros::time(name = "math.stretch", span = elem.span())] +pub fn layout_stretch( + elem: &Packed<StretchElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + stretch_fragment( + ctx, + styles, + &mut fragment, + None, + None, + elem.size(styles), + Abs::zero(), + ); + ctx.push(fragment); + Ok(()) } /// Attempts to stretch the given fragment by/to the amount given in stretch. -pub(super) fn stretch_fragment( +pub fn stretch_fragment( ctx: &mut MathContext, styles: StyleChain, fragment: &mut MathFragment, @@ -77,9 +55,7 @@ pub(super) fn stretch_fragment( // Return if we attempt to stretch along an axis which isn't stretchable, // so that the original fragment isn't modified. - let Some(stretch_axis) = stretch_axis(ctx, &glyph) else { - return; - }; + let Some(stretch_axis) = stretch_axis(ctx, &glyph) else { return }; let axis = axis.unwrap_or(stretch_axis); if axis != stretch_axis { return; @@ -105,67 +81,10 @@ pub(super) fn stretch_fragment( *fragment = MathFragment::Variant(variant); } -pub(super) fn delimiter_alignment(delimiter: char) -> VAlignment { - match delimiter { - '\u{231c}' | '\u{231d}' => VAlignment::Top, - '\u{231e}' | '\u{231f}' => VAlignment::Bottom, - _ => VAlignment::Horizon, - } -} - -/// Return whether the glyph is stretchable and if it is, along which axis it -/// can be stretched. -fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option<Axis> { - let base_id = base.id; - let vertical = ctx - .table - .variants - .and_then(|variants| variants.vertical_constructions.get(base_id)) - .map(|_| Axis::Y); - let horizontal = ctx - .table - .variants - .and_then(|variants| variants.horizontal_constructions.get(base_id)) - .map(|_| Axis::X); - - match (vertical, horizontal) { - (vertical, None) => vertical, - (None, horizontal) => horizontal, - _ => { - // As far as we know, there aren't any glyphs that have both - // vertical and horizontal constructions. So for the time being, we - // will assume that a glyph cannot have both. - panic!("glyph {:?} has both vertical and horizontal constructions", base.c); - } - } -} - -impl GlyphFragment { - /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical( - self, - ctx: &MathContext, - height: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, height, short_fall, Axis::Y) - } - - /// Try to stretch a glyph to a desired width. - pub fn stretch_horizontal( - self, - ctx: &MathContext, - width: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, width, short_fall, Axis::X) - } -} - /// Try to stretch a glyph to a desired width or height. /// /// The resulting frame may not have the exact desired width. -fn stretch_glyph( +pub fn stretch_glyph( ctx: &MathContext, mut base: GlyphFragment, target: Abs, @@ -218,6 +137,33 @@ fn stretch_glyph( assemble(ctx, base, assembly, min_overlap, target, axis) } +/// Return whether the glyph is stretchable and if it is, along which axis it +/// can be stretched. +fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option<Axis> { + let base_id = base.id; + let vertical = ctx + .table + .variants + .and_then(|variants| variants.vertical_constructions.get(base_id)) + .map(|_| Axis::Y); + let horizontal = ctx + .table + .variants + .and_then(|variants| variants.horizontal_constructions.get(base_id)) + .map(|_| Axis::X); + + match (vertical, horizontal) { + (vertical, None) => vertical, + (None, horizontal) => horizontal, + _ => { + // As far as we know, there aren't any glyphs that have both + // vertical and horizontal constructions. So for the time being, we + // will assume that a glyph cannot have both. + panic!("glyph {:?} has both vertical and horizontal constructions", base.c); + } + } +} + /// Assemble a glyph from parts. fn assemble( ctx: &MathContext, @@ -323,7 +269,6 @@ fn assemble( VariantFragment { c: base.c, - id: None, frame, font_size: base.font_size, italics_correction: Abs::zero(), diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs new file mode 100644 index 00000000..df80b45a --- /dev/null +++ b/crates/typst-layout/src/math/text.rs @@ -0,0 +1,344 @@ +use std::f64::consts::SQRT_2; + +use ecow::{eco_vec, EcoString}; +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, StyleChain, StyleVec}; +use typst_library::layout::{Abs, Size}; +use typst_library::math::{EquationElem, MathSize, MathVariant}; +use typst_library::text::{ + BottomEdge, BottomEdgeMetric, TextElem, TextSize, TopEdge, TopEdgeMetric, +}; +use typst_syntax::{is_newline, Span}; +use unicode_math_class::MathClass; +use unicode_segmentation::UnicodeSegmentation; + +use super::{ + scaled_font_size, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun, +}; + +/// Lays out a [`TextElem`]. +pub fn layout_text( + elem: &Packed<TextElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let text = elem.text(); + let span = elem.span(); + let mut chars = text.chars(); + let math_size = EquationElem::size_in(styles); + + let fragment: MathFragment = if let Some(mut glyph) = chars + .next() + .filter(|_| chars.next().is_none()) + .map(|c| styled_char(styles, c, true)) + .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span)) + { + // A single letter that is available in the math font. + match math_size { + MathSize::Script => { + glyph.make_scriptsize(ctx); + } + MathSize::ScriptScript => { + glyph.make_scriptscriptsize(ctx); + } + _ => (), + } + + if glyph.class == MathClass::Large { + let mut variant = if math_size == MathSize::Display { + let height = scaled!(ctx, styles, display_operator_min_height) + .max(SQRT_2 * glyph.height()); + glyph.stretch_vertical(ctx, height, Abs::zero()) + } else { + glyph.into_variant() + }; + // TeXbook p 155. Large operators are always vertically centered on the axis. + variant.center_on_axis(ctx); + variant.into() + } else { + glyph.into() + } + } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { + // Numbers aren't that difficult. + let mut fragments = vec![]; + for c in text.chars() { + let c = styled_char(styles, c, false); + fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); + } + let frame = MathRun::new(fragments).into_frame(ctx, styles); + FrameFragment::new(ctx, styles, frame).with_text_like(true).into() + } else { + let local = [ + TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), + TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)), + TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())), + ] + .map(|p| p.wrap()); + + // Anything else is handled by Typst's standard text layout. + let styles = styles.chain(&local); + let text: EcoString = + text.chars().map(|c| styled_char(styles, c, false)).collect(); + if text.contains(is_newline) { + let mut fragments = vec![]; + for (i, piece) in text.split(is_newline).enumerate() { + if i != 0 { + fragments.push(MathFragment::Linebreak); + } + if !piece.is_empty() { + fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); + } + } + let mut frame = MathRun::new(fragments).into_frame(ctx, styles); + let axis = scaled!(ctx, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + FrameFragment::new(ctx, styles, frame).into() + } else { + layout_complex_text(&text, ctx, span, styles)?.into() + } + }; + + ctx.push(fragment); + Ok(()) +} + +/// Layout the given text string into a [`FrameFragment`]. +fn layout_complex_text( + text: &str, + ctx: &mut MathContext, + span: Span, + styles: StyleChain, +) -> SourceResult<FrameFragment> { + // There isn't a natural width for a paragraph in a math environment; + // because it will be placed somewhere probably not at the left margin + // it will overflow. So emulate an `hbox` instead and allow the paragraph + // to extend as far as needed. + let spaced = text.graphemes(true).nth(1).is_some(); + let elem = TextElem::packed(text).spanned(span); + let frame = (ctx.engine.routines.layout_inline)( + ctx.engine, + &StyleVec::wrap(eco_vec![elem]), + ctx.locator.next(&span), + styles, + false, + Size::splat(Abs::inf()), + false, + )? + .into_frame(); + + Ok(FrameFragment::new(ctx, styles, frame) + .with_class(MathClass::Alphabetic) + .with_text_like(true) + .with_spaced(spaced)) +} + +/// Select the correct styled math letter. +/// +/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings> +/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols> +fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char { + use MathVariant::*; + + let variant = EquationElem::variant_in(styles); + let bold = EquationElem::bold_in(styles); + let italic = EquationElem::italic_in(styles).unwrap_or( + auto_italic + && matches!( + c, + 'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' | + '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' + ) + && matches!(variant, Sans | Serif), + ); + + if let Some(c) = basic_exception(c) { + return c; + } + + if let Some(c) = latin_exception(c, variant, bold, italic) { + return c; + } + + if let Some(c) = greek_exception(c, variant, bold, italic) { + return c; + } + + let base = match c { + 'A'..='Z' => 'A', + 'a'..='z' => 'a', + 'Α'..='Ω' => 'Α', + 'α'..='ω' => 'α', + '0'..='9' => '0', + // Hebrew Alef -> Dalet. + '\u{05D0}'..='\u{05D3}' => '\u{05D0}', + _ => return c, + }; + + let tuple = (variant, bold, italic); + let start = match c { + // Latin upper. + 'A'..='Z' => match tuple { + (Serif, false, false) => 0x0041, + (Serif, true, false) => 0x1D400, + (Serif, false, true) => 0x1D434, + (Serif, true, true) => 0x1D468, + (Sans, false, false) => 0x1D5A0, + (Sans, true, false) => 0x1D5D4, + (Sans, false, true) => 0x1D608, + (Sans, true, true) => 0x1D63C, + (Cal, false, _) => 0x1D49C, + (Cal, true, _) => 0x1D4D0, + (Frak, false, _) => 0x1D504, + (Frak, true, _) => 0x1D56C, + (Mono, _, _) => 0x1D670, + (Bb, _, _) => 0x1D538, + }, + + // Latin lower. + 'a'..='z' => match tuple { + (Serif, false, false) => 0x0061, + (Serif, true, false) => 0x1D41A, + (Serif, false, true) => 0x1D44E, + (Serif, true, true) => 0x1D482, + (Sans, false, false) => 0x1D5BA, + (Sans, true, false) => 0x1D5EE, + (Sans, false, true) => 0x1D622, + (Sans, true, true) => 0x1D656, + (Cal, false, _) => 0x1D4B6, + (Cal, true, _) => 0x1D4EA, + (Frak, false, _) => 0x1D51E, + (Frak, true, _) => 0x1D586, + (Mono, _, _) => 0x1D68A, + (Bb, _, _) => 0x1D552, + }, + + // Greek upper. + 'Α'..='Ω' => match tuple { + (Serif, false, false) => 0x0391, + (Serif, true, false) => 0x1D6A8, + (Serif, false, true) => 0x1D6E2, + (Serif, true, true) => 0x1D71C, + (Sans, _, false) => 0x1D756, + (Sans, _, true) => 0x1D790, + (Cal | Frak | Mono | Bb, _, _) => return c, + }, + + // Greek lower. + 'α'..='ω' => match tuple { + (Serif, false, false) => 0x03B1, + (Serif, true, false) => 0x1D6C2, + (Serif, false, true) => 0x1D6FC, + (Serif, true, true) => 0x1D736, + (Sans, _, false) => 0x1D770, + (Sans, _, true) => 0x1D7AA, + (Cal | Frak | Mono | Bb, _, _) => return c, + }, + + // Hebrew Alef -> Dalet. + '\u{05D0}'..='\u{05D3}' => 0x2135, + + // Numbers. + '0'..='9' => match tuple { + (Serif, false, _) => 0x0030, + (Serif, true, _) => 0x1D7CE, + (Bb, _, _) => 0x1D7D8, + (Sans, false, _) => 0x1D7E2, + (Sans, true, _) => 0x1D7EC, + (Mono, _, _) => 0x1D7F6, + (Cal | Frak, _, _) => return c, + }, + + _ => unreachable!(), + }; + + std::char::from_u32(start + (c as u32 - base as u32)).unwrap() +} + +fn basic_exception(c: char) -> Option<char> { + Some(match c { + '〈' => '⟨', + '〉' => '⟩', + '《' => '⟪', + '》' => '⟫', + _ => return None, + }) +} + +fn latin_exception( + c: char, + variant: MathVariant, + bold: bool, + italic: bool, +) -> Option<char> { + use MathVariant::*; + Some(match (c, variant, bold, italic) { + ('B', Cal, false, _) => 'ℬ', + ('E', Cal, false, _) => 'ℰ', + ('F', Cal, false, _) => 'ℱ', + ('H', Cal, false, _) => 'ℋ', + ('I', Cal, false, _) => 'ℐ', + ('L', Cal, false, _) => 'ℒ', + ('M', Cal, false, _) => 'ℳ', + ('R', Cal, false, _) => 'ℛ', + ('C', Frak, false, _) => 'ℭ', + ('H', Frak, false, _) => 'ℌ', + ('I', Frak, false, _) => 'ℑ', + ('R', Frak, false, _) => 'ℜ', + ('Z', Frak, false, _) => 'ℨ', + ('C', Bb, ..) => 'ℂ', + ('H', Bb, ..) => 'ℍ', + ('N', Bb, ..) => 'ℕ', + ('P', Bb, ..) => 'ℙ', + ('Q', Bb, ..) => 'ℚ', + ('R', Bb, ..) => 'ℝ', + ('Z', Bb, ..) => 'ℤ', + ('D', Bb, _, true) => 'ⅅ', + ('d', Bb, _, true) => 'ⅆ', + ('e', Bb, _, true) => 'ⅇ', + ('i', Bb, _, true) => 'ⅈ', + ('j', Bb, _, true) => 'ⅉ', + ('h', Serif, false, true) => 'ℎ', + ('e', Cal, false, _) => 'ℯ', + ('g', Cal, false, _) => 'ℊ', + ('o', Cal, false, _) => 'ℴ', + ('ı', Serif, .., true) => '𝚤', + ('ȷ', Serif, .., true) => '𝚥', + _ => return None, + }) +} + +fn greek_exception( + c: char, + variant: MathVariant, + bold: bool, + italic: bool, +) -> Option<char> { + use MathVariant::*; + let list = match c { + 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'], + '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'], + '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'], + 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'], + 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'], + 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'], + 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'], + 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'], + 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'], + 'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'], + 'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'], + 'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'], + 'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'], + '∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'], + _ => return None, + }; + + Some(match (variant, bold, italic) { + (Serif, true, false) => list[0], + (Serif, false, true) => list[1], + (Serif, true, true) => list[2], + (Sans, _, false) => list[3], + (Sans, _, true) => list[4], + (Bb, ..) => list[5], + _ => return None, + }) +} diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs new file mode 100644 index 00000000..b1d4825b --- /dev/null +++ b/crates/typst-layout/src/math/underover.rs @@ -0,0 +1,327 @@ +use typst_library::diag::SourceResult; +use typst_library::foundations::{Content, Packed, StyleChain}; +use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size}; +use typst_library::math::{ + OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem, + UnderbraceElem, UnderbracketElem, UnderlineElem, UnderparenElem, UndershellElem, +}; +use typst_library::text::TextElem; +use typst_library::visualize::{FixedStroke, Geometry}; +use typst_syntax::Span; + +use super::{ + scaled_font_size, stack, style_cramped, style_for_subscript, style_for_superscript, + FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun, +}; + +const BRACE_GAP: Em = Em::new(0.25); +const BRACKET_GAP: Em = Em::new(0.25); +const PAREN_GAP: Em = Em::new(0.25); +const SHELL_GAP: Em = Em::new(0.25); + +/// A marker to distinguish under- and overlines. +enum Position { + Under, + Over, +} + +/// Lays out an [`UnderlineElem`]. +#[typst_macros::time(name = "math.underline", span = elem.span())] +pub fn layout_underline( + elem: &Packed<UnderlineElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under) +} + +/// Lays out an [`OverlineElem`]. +#[typst_macros::time(name = "math.overline", span = elem.span())] +pub fn layout_overline( + elem: &Packed<OverlineElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over) +} + +/// Lays out an [`UnderbraceElem`]. +#[typst_macros::time(name = "math.underbrace", span = elem.span())] +pub fn layout_underbrace( + elem: &Packed<UnderbraceElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⏟', + BRACE_GAP, + Position::Under, + elem.span(), + ) +} + +/// Lays out an [`OverbraceElem`]. +#[typst_macros::time(name = "math.overbrace", span = elem.span())] +pub fn layout_overbrace( + elem: &Packed<OverbraceElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⏞', + BRACE_GAP, + Position::Over, + elem.span(), + ) +} + +/// Lays out an [`UnderbracketElem`]. +#[typst_macros::time(name = "math.underbracket", span = elem.span())] +pub fn layout_underbracket( + elem: &Packed<UnderbracketElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⎵', + BRACKET_GAP, + Position::Under, + elem.span(), + ) +} + +/// Lays out an [`OverbracketElem`]. +#[typst_macros::time(name = "math.overbracket", span = elem.span())] +pub fn layout_overbracket( + elem: &Packed<OverbracketElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⎴', + BRACKET_GAP, + Position::Over, + elem.span(), + ) +} + +/// Lays out an [`UnderparenElem`]. +#[typst_macros::time(name = "math.underparen", span = elem.span())] +pub fn layout_underparen( + elem: &Packed<UnderparenElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⏝', + PAREN_GAP, + Position::Under, + elem.span(), + ) +} + +/// Lays out an [`OverparenElem`]. +#[typst_macros::time(name = "math.overparen", span = elem.span())] +pub fn layout_overparen( + elem: &Packed<OverparenElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⏜', + PAREN_GAP, + Position::Over, + elem.span(), + ) +} + +/// Lays out an [`UndershellElem`]. +#[typst_macros::time(name = "math.undershell", span = elem.span())] +pub fn layout_undershell( + elem: &Packed<UndershellElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⏡', + SHELL_GAP, + Position::Under, + elem.span(), + ) +} + +/// Lays out an [`OvershellElem`]. +#[typst_macros::time(name = "math.overshell", span = elem.span())] +pub fn layout_overshell( + elem: &Packed<OvershellElem>, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_underoverspreader( + ctx, + styles, + elem.body(), + &elem.annotation(styles), + '⏠', + SHELL_GAP, + Position::Over, + elem.span(), + ) +} + +/// layout under- or overlined content. +fn layout_underoverline( + ctx: &mut MathContext, + styles: StyleChain, + body: &Content, + span: Span, + position: Position, +) -> SourceResult<()> { + let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust); + match position { + Position::Under => { + let sep = scaled!(ctx, styles, underbar_extra_descender); + bar_height = scaled!(ctx, styles, underbar_rule_thickness); + let gap = scaled!(ctx, styles, underbar_vertical_gap); + extra_height = sep + bar_height + gap; + + content = ctx.layout_into_fragment(body, styles)?; + + line_pos = Point::with_y(content.height() + gap + bar_height / 2.0); + content_pos = Point::zero(); + baseline = content.ascent(); + line_adjust = -content.italics_correction(); + } + Position::Over => { + let sep = scaled!(ctx, styles, overbar_extra_ascender); + bar_height = scaled!(ctx, styles, overbar_rule_thickness); + let gap = scaled!(ctx, styles, overbar_vertical_gap); + extra_height = sep + bar_height + gap; + + let cramped = style_cramped(); + content = ctx.layout_into_fragment(body, styles.chain(&cramped))?; + + line_pos = Point::with_y(sep + bar_height / 2.0); + content_pos = Point::with_y(extra_height); + baseline = content.ascent() + extra_height; + line_adjust = Abs::zero(); + } + } + + let width = content.width(); + let height = content.height() + extra_height; + let size = Size::new(width, height); + let line_width = width + line_adjust; + + let content_class = content.class(); + let content_is_text_like = content.is_text_like(); + let content_italics_correction = content.italics_correction(); + let mut frame = Frame::soft(size); + frame.set_baseline(baseline); + frame.push_frame(content_pos, content.into_frame()); + frame.push( + line_pos, + FrameItem::Shape( + Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke { + paint: TextElem::fill_in(styles).as_decoration(), + thickness: bar_height, + ..FixedStroke::default() + }), + span, + ), + ); + + ctx.push( + FrameFragment::new(ctx, styles, frame) + .with_class(content_class) + .with_text_like(content_is_text_like) + .with_italics_correction(content_italics_correction), + ); + + Ok(()) +} + +/// Layout an over- or underbrace-like object. +#[allow(clippy::too_many_arguments)] +fn layout_underoverspreader( + ctx: &mut MathContext, + styles: StyleChain, + body: &Content, + annotation: &Option<Content>, + c: char, + gap: Em, + position: Position, + span: Span, +) -> SourceResult<()> { + let font_size = scaled_font_size(ctx, styles); + let gap = gap.at(font_size); + let body = ctx.layout_into_run(body, styles)?; + let body_class = body.class(); + let body = body.into_fragment(ctx, styles); + let glyph = GlyphFragment::new(ctx, styles, c, span); + let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); + + let mut rows = vec![]; + let baseline = match position { + Position::Under => { + rows.push(MathRun::new(vec![body])); + rows.push(stretched.into()); + if let Some(annotation) = annotation { + let under_style = style_for_subscript(styles); + let annotation_styles = styles.chain(&under_style); + rows.push(ctx.layout_into_run(annotation, annotation_styles)?); + } + 0 + } + Position::Over => { + if let Some(annotation) = annotation { + let over_style = style_for_superscript(styles); + let annotation_styles = styles.chain(&over_style); + rows.push(ctx.layout_into_run(annotation, annotation_styles)?); + } + rows.push(stretched.into()); + rows.push(MathRun::new(vec![body])); + rows.len() - 1 + } + }; + + let frame = stack( + rows, + FixedAlignment::Center, + gap, + baseline, + LeftRightAlternator::Right, + None, + ); + ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class)); + + Ok(()) +} diff --git a/crates/typst/src/layout/pad.rs b/crates/typst-layout/src/pad.rs index ff5d4e69..00badcdb 100644 --- a/crates/typst/src/layout/pad.rs +++ b/crates/typst-layout/src/pad.rs @@ -1,78 +1,14 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::{Packed, Resolve, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Fragment, Frame, PadElem, Point, Regions, Rel, Sides, Size, }; -use crate::introspection::Locator; -use crate::layout::{ - layout_fragment, Abs, BlockElem, Fragment, Frame, Length, Point, Regions, Rel, Sides, - Size, -}; - -/// Adds spacing around content. -/// -/// The spacing can be specified for each side individually, or for all sides at -/// once by specifying a positional argument. -/// -/// # Example -/// ```example -/// #set align(center) -/// -/// #pad(x: 16pt, image("typing.jpg")) -/// _Typing speeds can be -/// measured in words per minute._ -/// ``` -#[elem(title = "Padding", Show)] -pub struct PadElem { - /// The padding at the left side. - #[parse( - let all = args.named("rest")?.or(args.find()?); - let x = args.named("x")?.or(all); - let y = args.named("y")?.or(all); - args.named("left")?.or(x) - )] - pub left: Rel<Length>, - - /// The padding at the top side. - #[parse(args.named("top")?.or(y))] - pub top: Rel<Length>, - - /// The padding at the right side. - #[parse(args.named("right")?.or(x))] - pub right: Rel<Length>, - - /// The padding at the bottom side. - #[parse(args.named("bottom")?.or(y))] - pub bottom: Rel<Length>, - - /// A shorthand to set `left` and `right` to the same value. - #[external] - pub x: Rel<Length>, - - /// A shorthand to set `top` and `bottom` to the same value. - #[external] - pub y: Rel<Length>, - - /// A shorthand to set all four sides to the same value. - #[external] - pub rest: Rel<Length>, - - /// The content to pad at the sides. - #[required] - pub body: Content, -} - -impl Show for Packed<PadElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::multi_layouter(self.clone(), layout_pad) - .pack() - .spanned(self.span())) - } -} /// Layout the padded content. #[typst_macros::time(span = elem.span())] -fn layout_pad( +pub fn layout_pad( elem: &Packed<PadElem>, engine: &mut Engine, locator: Locator, @@ -90,7 +26,7 @@ fn layout_pad( let pod = regions.map(&mut backlog, |size| shrink(size, &padding)); // Layout child into padded regions. - let mut fragment = layout_fragment(engine, &elem.body, locator, styles, pod)?; + let mut fragment = crate::layout_fragment(engine, &elem.body, locator, styles, pod)?; for frame in &mut fragment { grow(frame, &padding); @@ -100,13 +36,13 @@ fn layout_pad( } /// Shrink a region size by an inset relative to the size itself. -pub(crate) fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size { +pub fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size { size - inset.sum_by_axis().relative_to(size) } /// Shrink the components of possibly multiple `Regions` by an inset relative to /// the regions themselves. -pub(crate) fn shrink_multiple( +pub fn shrink_multiple( size: &mut Size, full: &mut Abs, backlog: &mut [Abs], @@ -141,7 +77,7 @@ pub(crate) fn shrink_multiple( /// <=> w - p.rel * w - p.abs = s /// <=> (1 - p.rel) * w = s + p.abs /// <=> w = (s + p.abs) / (1 - p.rel) -pub(crate) fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) { +pub fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) { // Apply the padding inversely such that the grown size padded // yields the frame's size. let padded = frame diff --git a/crates/typst/src/layout/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs index 2e7201e2..1903d6ac 100644 --- a/crates/typst/src/layout/pages/collect.rs +++ b/crates/typst-layout/src/pages/collect.rs @@ -1,9 +1,9 @@ use std::collections::HashSet; -use crate::foundations::StyleChain; -use crate::introspection::{Locator, SplitLocator, Tag, TagElem}; -use crate::layout::{PagebreakElem, Parity}; -use crate::realize::Pair; +use typst_library::foundations::StyleChain; +use typst_library::introspection::{Locator, SplitLocator, Tag, TagElem}; +use typst_library::layout::{PagebreakElem, Parity}; +use typst_library::routines::Pair; /// An item in page layout. pub enum Item<'a> { diff --git a/crates/typst/src/layout/pages/finalize.rs b/crates/typst-layout/src/pages/finalize.rs index d1842613..8df6cb16 100644 --- a/crates/typst/src/layout/pages/finalize.rs +++ b/crates/typst-layout/src/pages/finalize.rs @@ -1,8 +1,9 @@ +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::introspection::{ManualPageCounter, Tag}; +use typst_library::layout::{Frame, FrameItem, Page, Point}; + use super::LayoutedPage; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::introspection::{ManualPageCounter, Tag}; -use crate::layout::{Frame, FrameItem, Page, Point}; /// Piece together the inner page frame and the marginals. We can only do this /// at the very end because inside/outside margins require knowledge of the diff --git a/crates/typst/src/layout/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 6833d535..b969749a 100644 --- a/crates/typst/src/layout/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -5,25 +5,25 @@ mod finalize; mod run; use comemo::{Tracked, TrackedMut}; +use typst_library::diag::SourceResult; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Content, StyleChain}; +use typst_library::introspection::{ + Introspector, Locator, ManualPageCounter, SplitLocator, TagElem, +}; +use typst_library::layout::{FrameItem, Page, Point}; +use typst_library::model::{Document, DocumentInfo}; +use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::World; use self::collect::{collect, Item}; use self::finalize::finalize; use self::run::{layout_blank_page, layout_page_run, LayoutedPage}; -use crate::diag::SourceResult; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Content, StyleChain}; -use crate::introspection::{ - Introspector, Locator, ManualPageCounter, SplitLocator, TagElem, -}; -use crate::layout::{FrameItem, Page, Point}; -use crate::model::{Document, DocumentInfo}; -use crate::realize::{realize, Arenas, Pair, RealizationKind}; -use crate::World; /// Layout content into a document. /// /// This first performs root-level realization and then lays out the resulting -/// elements. In contrast to [`layout_fragment`](crate::layout::layout_fragment), +/// elements. In contrast to [`layout_fragment`](crate::layout_fragment), /// this does not take regions since the regions are defined by the page /// configuration in the content and style chain. #[typst_macros::time(name = "document")] @@ -33,6 +33,7 @@ pub fn layout_document( styles: StyleChain, ) -> SourceResult<Document> { layout_document_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -45,7 +46,9 @@ pub fn layout_document( /// The internal implementation of `layout_document`. #[comemo::memoize] +#[allow(clippy::too_many_arguments)] fn layout_document_impl( + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -56,6 +59,7 @@ fn layout_document_impl( ) -> SourceResult<Document> { let mut locator = Locator::root().split(); let mut engine = Engine { + routines, world, introspector, traced, @@ -70,7 +74,7 @@ fn layout_document_impl( let arenas = Arenas::default(); let mut info = DocumentInfo::default(); - let mut children = realize( + let mut children = (engine.routines.realize)( RealizationKind::Root(&mut info), &mut engine, &mut locator, diff --git a/crates/typst/src/layout/pages/run.rs b/crates/typst-layout/src/pages/run.rs index b5c2834f..15735faa 100644 --- a/crates/typst/src/layout/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -1,22 +1,25 @@ use comemo::{Track, Tracked, TrackedMut}; - -use crate::diag::SourceResult; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Content, NativeElement, Resolve, Smart, StyleChain, Styles}; -use crate::introspection::{ +use typst_library::diag::SourceResult; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{ + Content, NativeElement, Resolve, Smart, StyleChain, Styles, +}; +use typst_library::introspection::{ Counter, CounterDisplayElem, CounterKey, Introspector, Locator, LocatorLink, TagElem, }; -use crate::layout::{ - layout_flow, layout_frame, Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem, - Dir, Frame, HAlignment, Length, OuterVAlignment, PageElem, Paper, Region, Regions, - Rel, Sides, Size, VAlignment, +use typst_library::layout::{ + Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem, Dir, Frame, HAlignment, + Length, OuterVAlignment, PageElem, Paper, Region, Regions, Rel, Sides, Size, + VAlignment, }; -use crate::model::Numbering; -use crate::realize::Pair; -use crate::text::TextElem; -use crate::utils::Numeric; -use crate::visualize::Paint; -use crate::World; +use typst_library::model::Numbering; +use typst_library::routines::{Pair, Routines}; +use typst_library::text::TextElem; +use typst_library::visualize::Paint; +use typst_library::World; +use typst_utils::Numeric; + +use crate::flow::layout_flow; /// A mostly finished layout for one page. Needs only knowledge of its exact /// page number to be finalized into a `Page`. (Because the margins can depend @@ -54,6 +57,7 @@ pub fn layout_page_run( initial: StyleChain, ) -> SourceResult<Vec<LayoutedPage>> { layout_page_run_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -69,6 +73,7 @@ pub fn layout_page_run( #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_page_run_impl( + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -81,6 +86,7 @@ fn layout_page_run_impl( let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); let mut engine = Engine { + routines, world, introspector, traced, @@ -177,7 +183,7 @@ fn layout_page_run_impl( let mut layout_marginal = |content: &Option<Content>, area, align| { let Some(content) = content else { return Ok(None) }; let aligned = content.clone().styled(AlignElem::set_alignment(align)); - layout_frame( + crate::layout_frame( &mut engine, &aligned, locator.next(&content.span()), diff --git a/crates/typst-layout/src/repeat.rs b/crates/typst-layout/src/repeat.rs new file mode 100644 index 00000000..b761438c --- /dev/null +++ b/crates/typst-layout/src/repeat.rs @@ -0,0 +1,60 @@ +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Packed, Resolve, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, AlignElem, Axes, Frame, Point, Region, RepeatElem, Size, +}; +use typst_utils::Numeric; + +/// Layout the repeated content. +#[typst_macros::time(span = elem.span())] +pub fn layout_repeat( + elem: &Packed<RepeatElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let pod = Region::new(region.size, Axes::new(false, false)); + let piece = crate::layout_frame(engine, &elem.body, locator, styles, pod)?; + let size = Size::new(region.size.x, piece.height()); + + if !size.is_finite() { + bail!(elem.span(), "repeat with no size restrictions"); + } + + let mut frame = Frame::soft(size); + if piece.has_baseline() { + frame.set_baseline(piece.baseline()); + } + + let mut gap = elem.gap(styles).resolve(styles); + let fill = region.size.x; + let width = piece.width(); + + // count * width + (count - 1) * gap = fill, but count is an integer so + // we need to round down and get the remainder. + let count = ((fill + gap) / (width + gap)).floor(); + let remaining = (fill + gap) % (width + gap); + + let justify = elem.justify(styles); + if justify { + gap += remaining / (count - 1.0); + } + + let align = AlignElem::alignment_in(styles).resolve(styles); + let mut offset = Abs::zero(); + if count == 1.0 || !justify { + offset += align.x.position(remaining); + } + + if width > Abs::zero() { + for _ in 0..(count as usize).min(1000) { + frame.push_frame(Point::with_x(offset), piece.clone()); + offset += piece.width() + gap; + } + } + + Ok(frame) +} diff --git a/crates/typst/src/visualize/shape.rs b/crates/typst-layout/src/shapes.rs index 56ecd029..31f8c42b 100644 --- a/crates/typst/src/visualize/shape.rs +++ b/crates/typst-layout/src/shapes.rs @@ -1,469 +1,319 @@ use std::f64::consts::SQRT_2; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, +use kurbo::ParamCurveExtrema; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Axes, Corner, Corners, Frame, FrameItem, Length, Point, Ratio, Region, Rel, + Sides, Size, }; -use crate::introspection::Locator; -use crate::layout::{ - layout_frame, Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, - Ratio, Region, Rel, Sides, Size, Sizing, +use typst_library::visualize::{ + CircleElem, EllipseElem, FillRule, FixedStroke, Geometry, LineElem, Paint, Path, + PathElem, PathVertex, PolygonElem, RectElem, Shape, SquareElem, Stroke, }; -use crate::syntax::Span; -use crate::utils::Get; -use crate::visualize::{FixedStroke, Paint, Path, Stroke}; - -/// A rectangle with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #rect(width: 35%, height: 30pt) -/// -/// // With content. -/// #rect[ -/// Automatically sized \ -/// to fit the content. -/// ] -/// ``` -#[elem(title = "Rectangle", Show)] -pub struct RectElem { - /// The rectangle's width, relative to its parent container. - pub width: Smart<Rel<Length>>, - - /// The rectangle's height, relative to its parent container. - pub height: Sizing, - - /// How to fill the rectangle. - /// - /// When setting a fill, the default stroke disappears. To create a - /// rectangle with both fill and stroke, you have to configure both. - /// - /// ```example - /// #rect(fill: blue) - /// ``` - pub fill: Option<Paint>, - - /// How to stroke the rectangle. This can be: - /// - /// - `{none}` to disable stroking - /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is - /// given. - /// - Any kind of [stroke] - /// - A dictionary describing the stroke for each side individually. The - /// dictionary can contain the following keys in order of precedence: - /// - `top`: The top stroke. - /// - `right`: The right stroke. - /// - `bottom`: The bottom stroke. - /// - `left`: The left stroke. - /// - `x`: The horizontal stroke. - /// - `y`: The vertical stroke. - /// - `rest`: The stroke on all sides except those for which the - /// dictionary explicitly sets a size. - /// - /// ```example - /// #stack( - /// dir: ltr, - /// spacing: 1fr, - /// rect(stroke: red), - /// rect(stroke: 2pt), - /// rect(stroke: 2pt + red), - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<Sides<Option<Option<Stroke>>>>, - - /// How much to round the rectangle's corners, relative to the minimum of - /// the width and height divided by two. This can be: - /// - /// - A relative length for a uniform corner radius. - /// - A dictionary: With a dictionary, the stroke for each side can be set - /// individually. The dictionary can contain the following keys in order - /// of precedence: - /// - `top-left`: The top-left corner radius. - /// - `top-right`: The top-right corner radius. - /// - `bottom-right`: The bottom-right corner radius. - /// - `bottom-left`: The bottom-left corner radius. - /// - `left`: The top-left and bottom-left corner radii. - /// - `top`: The top-left and top-right corner radii. - /// - `right`: The top-right and bottom-right corner radii. - /// - `bottom`: The bottom-left and bottom-right corner radii. - /// - `rest`: The radii for all corners except those for which the - /// dictionary explicitly sets a size. - /// - /// ```example - /// #set rect(stroke: 4pt) - /// #rect( - /// radius: ( - /// left: 5pt, - /// top-right: 20pt, - /// bottom-right: 10pt, - /// ), - /// stroke: ( - /// left: red, - /// top: yellow, - /// right: green, - /// bottom: blue, - /// ), - /// ) - /// ``` - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the rectangle's content. - /// See the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Some(Abs::pt(5.0).into())))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the rectangle's size without affecting the layout. - /// See the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the rectangle. - /// - /// When this is omitted, the rectangle takes on a default size of at most - /// `{45pt}` by `{30pt}`. - #[positional] - #[borrowed] - pub body: Option<Content>, +use typst_syntax::Span; +use typst_utils::{Get, Numeric}; + +/// Layout the line. +#[typst_macros::time(span = elem.span())] +pub fn layout_line( + elem: &Packed<LineElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to); + let start = resolve(elem.start(styles)); + let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| { + let length = elem.length(styles); + let angle = elem.angle(styles); + let x = angle.cos() * length; + let y = angle.sin() * length; + resolve(Axes::new(x, y)) + }); + + let stroke = elem.stroke(styles).unwrap_or_default(); + let size = start.max(start + delta).max(Size::zero()); + + if !size.is_finite() { + bail!(elem.span(), "cannot create line with infinite length"); + } + + let mut frame = Frame::soft(size); + let shape = Geometry::Line(delta.to_point()).stroked(stroke); + frame.push(start.to_point(), FrameItem::Shape(shape, elem.span())); + Ok(frame) } -impl Show for Packed<RectElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter( - self.clone(), - |elem, engine, locator, styles, region| { - layout_shape( - engine, - locator, - styles, - region, - ShapeKind::Rect, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles), - elem.inset(styles), - elem.outset(styles), - elem.radius(styles), - elem.span(), - ) - }, - ) - .with_width(self.width(styles)) - .with_height(self.height(styles)) - .pack() - .spanned(self.span())) +/// Layout the path. +#[typst_macros::time(span = elem.span())] +pub fn layout_path( + elem: &Packed<PathElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let resolve = |axes: Axes<Rel<Length>>| { + axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() + }; + + let vertices = elem.vertices(); + let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); + + let mut size = Size::zero(); + if points.is_empty() { + return Ok(Frame::soft(size)); } -} -/// A square with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #square(size: 40pt) -/// -/// // With content. -/// #square[ -/// Automatically \ -/// sized to fit. -/// ] -/// ``` -#[elem(Show)] -pub struct SquareElem { - /// The square's side length. This is mutually exclusive with `width` and - /// `height`. - #[external] - pub size: Smart<Length>, - - /// The square's width. This is mutually exclusive with `size` and `height`. - /// - /// In contrast to `size`, this can be relative to the parent container's - /// width. - #[parse( - let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from)); - match size { - None => args.named("width")?, - size => size, - } - )] - pub width: Smart<Rel<Length>>, - - /// The square's height. This is mutually exclusive with `size` and `width`. - /// - /// In contrast to `size`, this can be relative to the parent container's - /// height. - #[parse(match size { - None => args.named("height")?, - size => size.map(Into::into), - })] - pub height: Sizing, - - /// How to fill the square. See the [rectangle's documentation]($rect.fill) - /// for more details. - pub fill: Option<Paint>, - - /// How to stroke the square. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Smart<Sides<Option<Option<Stroke>>>>, - - /// How much to round the square's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the square's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Some(Abs::pt(5.0).into())))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the square's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the square. The square expands to fit this - /// content, keeping the 1-1 aspect ratio. - /// - /// When this is omitted, the square takes on a default size of at most - /// `{30pt}`. - #[positional] - #[borrowed] - pub body: Option<Content>, + // Only create a path if there are more than zero points. + // Construct a closed path given all points. + let mut path = Path::new(); + path.move_to(points[0]); + + let mut add_cubic = |from_point: Point, + to_point: Point, + from: PathVertex, + to: PathVertex| { + let from_control_point = resolve(from.control_point_from()) + from_point; + let to_control_point = resolve(to.control_point_to()) + to_point; + path.cubic_to(from_control_point, to_control_point, to_point); + + let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw()); + let p1 = kurbo::Point::new( + from_control_point.x.to_raw(), + from_control_point.y.to_raw(), + ); + let p2 = + kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw()); + let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw()); + let extrema = kurbo::CubicBez::new(p0, p1, p2, p3).bounding_box(); + size.x.set_max(Abs::raw(extrema.x1)); + size.y.set_max(Abs::raw(extrema.y1)); + }; + + for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) { + let from = vertex_window[0]; + let to = vertex_window[1]; + let from_point = point_window[0]; + let to_point = point_window[1]; + + add_cubic(from_point, to_point, from, to); + } + + if elem.closed(styles) { + let from = *vertices.last().unwrap(); // We checked that we have at least one element. + let to = vertices[0]; + let from_point = *points.last().unwrap(); + let to_point = points[0]; + + add_cubic(from_point, to_point, from, to); + path.close_path(); + } + + // Prepare fill and stroke. + let fill = elem.fill(styles); + let fill_rule = elem.fill_rule(styles); + let stroke = match elem.stroke(styles) { + Smart::Auto if fill.is_none() => Some(FixedStroke::default()), + Smart::Auto => None, + Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), + }; + + let mut frame = Frame::soft(size); + let shape = Shape { + geometry: Geometry::Path(path), + stroke, + fill, + fill_rule, + }; + frame.push(Point::zero(), FrameItem::Shape(shape, elem.span())); + Ok(frame) } -impl Show for Packed<SquareElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter( - self.clone(), - |elem, engine, locator, styles, regions| { - layout_shape( - engine, - locator, - styles, - regions, - ShapeKind::Square, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles), - elem.inset(styles), - elem.outset(styles), - elem.radius(styles), - elem.span(), - ) - }, - ) - .with_width(self.width(styles)) - .with_height(self.height(styles)) - .pack() - .spanned(self.span())) +/// Layout the polygon. +#[typst_macros::time(span = elem.span())] +pub fn layout_polygon( + elem: &Packed<PolygonElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let points: Vec<Point> = elem + .vertices() + .iter() + .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) + .collect(); + + let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size(); + if !size.is_finite() { + bail!(elem.span(), "cannot create polygon with infinite size"); + } + + let mut frame = Frame::hard(size); + + // Only create a path if there are more than zero points. + if points.is_empty() { + return Ok(frame); } + + // Prepare fill and stroke. + let fill = elem.fill(styles); + let fill_rule = elem.fill_rule(styles); + let stroke = match elem.stroke(styles) { + Smart::Auto if fill.is_none() => Some(FixedStroke::default()), + Smart::Auto => None, + Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), + }; + + // Construct a closed path given all points. + let mut path = Path::new(); + path.move_to(points[0]); + for &point in &points[1..] { + path.line_to(point); + } + path.close_path(); + + let shape = Shape { + geometry: Geometry::Path(path), + stroke, + fill, + fill_rule, + }; + frame.push(Point::zero(), FrameItem::Shape(shape, elem.span())); + Ok(frame) } -/// An ellipse with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #ellipse(width: 35%, height: 30pt) -/// -/// // With content. -/// #ellipse[ -/// #set align(center) -/// Automatically sized \ -/// to fit the content. -/// ] -/// ``` -#[elem(Show)] -pub struct EllipseElem { - /// The ellipse's width, relative to its parent container. - pub width: Smart<Rel<Length>>, - - /// The ellipse's height, relative to its parent container. - pub height: Sizing, - - /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill) - /// for more details. - pub fill: Option<Paint>, - - /// How to stroke the ellipse. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Smart<Option<Stroke>>, - - /// How much to pad the ellipse's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Some(Abs::pt(5.0).into())))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the ellipse's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the ellipse. - /// - /// When this is omitted, the ellipse takes on a default size of at most - /// `{45pt}` by `{30pt}`. - #[positional] - #[borrowed] - pub body: Option<Content>, +/// Lay out the rectangle. +#[typst_macros::time(span = elem.span())] +pub fn layout_rect( + elem: &Packed<RectElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + layout_shape( + engine, + locator, + styles, + region, + ShapeKind::Rect, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles), + elem.inset(styles), + elem.outset(styles), + elem.radius(styles), + elem.span(), + ) } -impl Show for Packed<EllipseElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter( - self.clone(), - |elem, engine, locator, styles, regions| { - layout_shape( - engine, - locator, - styles, - regions, - ShapeKind::Ellipse, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles).map(|s| Sides::splat(Some(s))), - elem.inset(styles), - elem.outset(styles), - Corners::splat(None), - elem.span(), - ) - }, - ) - .with_width(self.width(styles)) - .with_height(self.height(styles)) - .pack() - .spanned(self.span())) - } +/// Lay out the square. +#[typst_macros::time(span = elem.span())] +pub fn layout_square( + elem: &Packed<SquareElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + layout_shape( + engine, + locator, + styles, + region, + ShapeKind::Square, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles), + elem.inset(styles), + elem.outset(styles), + elem.radius(styles), + elem.span(), + ) } -/// A circle with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #circle(radius: 25pt) -/// -/// // With content. -/// #circle[ -/// #set align(center + horizon) -/// Automatically \ -/// sized to fit. -/// ] -/// ``` -#[elem(Show)] -pub struct CircleElem { - /// The circle's radius. This is mutually exclusive with `width` and - /// `height`. - #[external] - pub radius: Length, - - /// The circle's width. This is mutually exclusive with `radius` and - /// `height`. - /// - /// In contrast to `radius`, this can be relative to the parent container's - /// width. - #[parse( - let size = args - .named::<Smart<Length>>("radius")? - .map(|s| s.map(|r| 2.0 * Rel::from(r))); - match size { - None => args.named("width")?, - size => size, - } - )] - pub width: Smart<Rel<Length>>, - - /// The circle's height. This is mutually exclusive with `radius` and - /// `width`. - /// - /// In contrast to `radius`, this can be relative to the parent container's - /// height. - #[parse(match size { - None => args.named("height")?, - size => size.map(Into::into), - })] - pub height: Sizing, - - /// How to fill the circle. See the [rectangle's documentation]($rect.fill) - /// for more details. - pub fill: Option<Paint>, - - /// How to stroke the circle. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - #[default(Smart::Auto)] - pub stroke: Smart<Option<Stroke>>, - - /// How much to pad the circle's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Some(Abs::pt(5.0).into())))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the circle's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the circle. The circle expands to fit this - /// content, keeping the 1-1 aspect ratio. - #[positional] - #[borrowed] - pub body: Option<Content>, +/// Lay out the ellipse. +#[typst_macros::time(span = elem.span())] +pub fn layout_ellipse( + elem: &Packed<EllipseElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + layout_shape( + engine, + locator, + styles, + region, + ShapeKind::Ellipse, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles).map(|s| Sides::splat(Some(s))), + elem.inset(styles), + elem.outset(styles), + Corners::splat(None), + elem.span(), + ) +} + +/// Lay out the circle. +#[typst_macros::time(span = elem.span())] +pub fn layout_circle( + elem: &Packed<CircleElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + layout_shape( + engine, + locator, + styles, + region, + ShapeKind::Circle, + elem.body(styles), + elem.fill(styles), + elem.stroke(styles).map(|s| Sides::splat(Some(s))), + elem.inset(styles), + elem.outset(styles), + Corners::splat(None), + elem.span(), + ) +} + +/// A category of shape. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum ShapeKind { + /// A rectangle with equal side lengths. + Square, + /// A quadrilateral with four right angles. + Rect, + /// An ellipse with coinciding foci. + Circle, + /// A curve around two focal points. + Ellipse, } -impl Show for Packed<CircleElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter( - self.clone(), - |elem, engine, locator, styles, regions| { - layout_shape( - engine, - locator, - styles, - regions, - ShapeKind::Circle, - elem.body(styles), - elem.fill(styles), - elem.stroke(styles).map(|s| Sides::splat(Some(s))), - elem.inset(styles), - elem.outset(styles), - Corners::splat(None), - elem.span(), - ) - }, - ) - .with_width(self.width(styles)) - .with_height(self.height(styles)) - .pack() - .spanned(self.span())) +impl ShapeKind { + /// Whether this shape kind is curvy. + fn is_round(self) -> bool { + matches!(self, Self::Circle | Self::Ellipse) + } + + /// Whether this shape kind has equal side length. + fn is_quadratic(self) -> bool { + matches!(self, Self::Square | Self::Circle) } } /// Layout a shape. -#[typst_macros::time(span = span)] #[allow(clippy::too_many_arguments)] fn layout_shape( engine: &mut Engine, @@ -491,23 +341,23 @@ fn layout_shape( // Take the inset, if any, into account. let mut pod = region; if has_inset { - pod.size = crate::layout::shrink(region.size, &inset); + pod.size = crate::pad::shrink(region.size, &inset); } // Layout the child. - frame = layout_frame(engine, child, locator.relayout(), styles, pod)?; + frame = crate::layout_frame(engine, child, locator.relayout(), styles, pod)?; // If the child is a square or circle, relayout with full expansion into // square region to make sure the result is really quadratic. if kind.is_quadratic() { let length = frame.size().max_by_side().min(pod.size.min_by_side()); let quad_pod = Region::new(Size::splat(length), Axes::splat(true)); - frame = layout_frame(engine, child, locator, styles, quad_pod)?; + frame = crate::layout_frame(engine, child, locator, styles, quad_pod)?; } // Apply the inset. if has_inset { - crate::layout::grow(&mut frame, &inset); + crate::pad::grow(&mut frame, &inset); } } else { // The default size that a shape takes on if it has no child and @@ -535,10 +385,16 @@ fn layout_shape( let outset = outset.unwrap_or_default().relative_to(frame.size()); let size = frame.size() + outset.sum_by_axis(); let pos = Point::new(-outset.left, -outset.top); - let shape = ellipse(size, fill, stroke.left); + let shape = Shape { + geometry: Geometry::Path(Path::ellipse(size)), + fill, + stroke: stroke.left, + fill_rule: FillRule::default(), + }; frame.prepend(pos, FrameItem::Shape(shape, span)); } else { - frame.fill_and_stroke( + fill_and_stroke( + &mut frame, fill, &stroke, &outset.unwrap_or_default(), @@ -551,128 +407,8 @@ fn layout_shape( Ok(frame) } -/// A category of shape. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ShapeKind { - /// A rectangle with equal side lengths. - Square, - /// A quadrilateral with four right angles. - Rect, - /// An ellipse with coinciding foci. - Circle, - /// A curve around two focal points. - Ellipse, -} - -impl ShapeKind { - /// Whether this shape kind is curvy. - fn is_round(self) -> bool { - matches!(self, Self::Circle | Self::Ellipse) - } - - /// Whether this shape kind has equal side length. - fn is_quadratic(self) -> bool { - matches!(self, Self::Square | Self::Circle) - } -} - -/// A geometric shape with optional fill and stroke. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Shape { - /// The shape's geometry. - pub geometry: Geometry, - /// The shape's background fill. - pub fill: Option<Paint>, - /// The shape's fill rule. - pub fill_rule: FillRule, - /// The shape's border stroke. - pub stroke: Option<FixedStroke>, -} - -/// A path filling rule. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum FillRule { - /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings. - #[default] - NonZero, - /// Specifies that "inside" is computed by an odd number of edge crossings. - EvenOdd, -} - -/// A shape's geometry. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Geometry { - /// A line to a point (relative to its position). - Line(Point), - /// A rectangle with its origin in the topleft corner. - Rect(Size), - /// A bezier path. - Path(Path), -} - -impl Geometry { - /// Fill the geometry without a stroke. - pub fn filled(self, fill: Paint) -> Shape { - Shape { - geometry: self, - fill: Some(fill), - fill_rule: FillRule::default(), - stroke: None, - } - } - - /// Stroke the geometry without a fill. - pub fn stroked(self, stroke: FixedStroke) -> Shape { - Shape { - geometry: self, - fill: None, - fill_rule: FillRule::default(), - stroke: Some(stroke), - } - } - - /// The bounding box of the geometry. - pub fn bbox_size(&self) -> Size { - match self { - Self::Line(line) => Size::new(line.x, line.y), - Self::Rect(s) => *s, - Self::Path(p) => p.bbox_size(), - } - } -} - -/// Produce a shape that approximates an axis-aligned ellipse. -pub(crate) fn ellipse( - size: Size, - fill: Option<Paint>, - stroke: Option<FixedStroke>, -) -> Shape { - // https://stackoverflow.com/a/2007782 - let z = Abs::zero(); - let rx = size.x / 2.0; - let ry = size.y / 2.0; - let m = 0.551784; - let mx = m * rx; - let my = m * ry; - let point = |x, y| Point::new(x + rx, y + ry); - - let mut path = Path::new(); - path.move_to(point(-rx, z)); - path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry)); - path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z)); - path.cubic_to(point(rx, my), point(mx, ry), point(z, ry)); - path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z)); - - Shape { - geometry: Geometry::Path(path), - stroke, - fill, - fill_rule: FillRule::default(), - } -} - /// Creates a new rectangle as a path. -pub(crate) fn clip_rect( +pub fn clip_rect( size: Size, radius: &Corners<Rel<Abs>>, stroke: &Sides<Option<FixedStroke>>, @@ -708,11 +444,30 @@ pub(crate) fn clip_rect( path } +/// Add a fill and stroke with optional radius and outset to the frame. +pub fn fill_and_stroke( + frame: &mut Frame, + fill: Option<Paint>, + stroke: &Sides<Option<FixedStroke>>, + outset: &Sides<Rel<Abs>>, + radius: &Corners<Rel<Abs>>, + span: Span, +) { + let outset = outset.relative_to(frame.size()); + let size = frame.size() + outset.sum_by_axis(); + let pos = Point::new(-outset.left, -outset.top); + frame.prepend_multiple( + styled_rect(size, radius, fill, stroke) + .into_iter() + .map(|x| (pos, FrameItem::Shape(x, span))), + ); +} + /// Create a styled rectangle with shapes. /// - use rect primitive for simple rectangles /// - stroke sides if possible /// - use fill for sides for best looks -pub(crate) fn styled_rect( +pub fn styled_rect( size: Size, radius: &Corners<Rel<Abs>>, fill: Option<Paint>, diff --git a/crates/typst/src/layout/stack.rs b/crates/typst-layout/src/stack.rs index 6f1aa534..a3ebc9f3 100644 --- a/crates/typst/src/layout/stack.rs +++ b/crates/typst-layout/src/stack.rs @@ -1,99 +1,17 @@ -use std::fmt::{self, Debug, Formatter}; -use typst_syntax::Span; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, StyledElem, +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain, StyledElem}; +use typst_library::introspection::{Locator, SplitLocator}; +use typst_library::layout::{ + Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, HElem, Point, + Regions, Size, Spacing, StackChild, StackElem, VElem, }; -use crate::introspection::{Locator, SplitLocator}; -use crate::layout::{ - layout_fragment, Abs, AlignElem, Axes, Axis, BlockElem, Dir, FixedAlignment, Fr, - Fragment, Frame, HElem, Point, Regions, Size, Spacing, VElem, -}; -use crate::utils::{Get, Numeric}; - -/// Arranges content and spacing horizontally or vertically. -/// -/// The stack places a list of items along an axis, with optional spacing -/// between each item. -/// -/// # Example -/// ```example -/// #stack( -/// dir: ttb, -/// rect(width: 40pt), -/// rect(width: 120pt), -/// rect(width: 90pt), -/// ) -/// ``` -#[elem(Show)] -pub struct StackElem { - /// The direction along which the items are stacked. Possible values are: - /// - /// - `{ltr}`: Left to right. - /// - `{rtl}`: Right to left. - /// - `{ttb}`: Top to bottom. - /// - `{btt}`: Bottom to top. - /// - /// You can use the `start` and `end` methods to obtain the initial and - /// final points (respectively) of a direction, as `alignment`. You can also - /// use the `axis` method to determine whether a direction is - /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a - /// direction's inverse direction. - /// - /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`, - /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`. - #[default(Dir::TTB)] - pub dir: Dir, - - /// Spacing to insert between items where no explicit spacing was provided. - pub spacing: Option<Spacing>, - - /// The children to stack along the axis. - #[variadic] - pub children: Vec<StackChild>, -} - -impl Show for Packed<StackElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::multi_layouter(self.clone(), layout_stack) - .pack() - .spanned(self.span())) - } -} - -/// A child of a stack element. -#[derive(Clone, PartialEq, Hash)] -pub enum StackChild { - /// Spacing between other children. - Spacing(Spacing), - /// Arbitrary block-level content. - Block(Content), -} - -impl Debug for StackChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Spacing(kind) => kind.fmt(f), - Self::Block(block) => block.fmt(f), - } - } -} - -cast! { - StackChild, - self => match self { - Self::Spacing(spacing) => spacing.into_value(), - Self::Block(content) => content.into_value(), - }, - v: Spacing => Self::Spacing(v), - v: Content => Self::Block(v), -} +use typst_syntax::Span; +use typst_utils::{Get, Numeric}; /// Layout the stack. #[typst_macros::time(span = elem.span())] -fn layout_stack( +pub fn layout_stack( elem: &Packed<StackElem>, engine: &mut Engine, locator: Locator, @@ -257,7 +175,7 @@ impl<'a> StackLayouter<'a> { } .resolve(styles); - let fragment = layout_fragment( + let fragment = crate::layout_fragment( engine, block, self.locator.next(&block.span()), diff --git a/crates/typst-layout/src/transforms.rs b/crates/typst-layout/src/transforms.rs new file mode 100644 index 00000000..5ac9f777 --- /dev/null +++ b/crates/typst-layout/src/transforms.rs @@ -0,0 +1,246 @@ +use once_cell::unsync::Lazy; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Axes, FixedAlignment, Frame, MoveElem, Point, Ratio, Region, Rel, RotateElem, + ScaleAmount, ScaleElem, Size, SkewElem, Transform, +}; +use typst_utils::Numeric; + +/// Layout the moved content. +#[typst_macros::time(span = elem.span())] +pub fn layout_move( + elem: &Packed<MoveElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let mut frame = crate::layout_frame(engine, &elem.body, locator, styles, region)?; + let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); + let delta = delta.zip_map(region.size, Rel::relative_to); + frame.translate(delta.to_point()); + Ok(frame) +} + +/// Layout the rotated content. +#[typst_macros::time(span = elem.span())] +pub fn layout_rotate( + elem: &Packed<RotateElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let angle = elem.angle(styles); + let align = elem.origin(styles).resolve(styles); + + // Compute the new region's approximate size. + let is_finite = region.size.is_finite(); + let size = if is_finite { + compute_bounding_box(region.size, Transform::rotate(-angle)).1 + } else { + Size::splat(Abs::inf()) + }; + + measure_and_layout( + engine, + locator, + region, + size, + styles, + elem.body(), + Transform::rotate(angle), + align, + elem.reflow(styles), + ) +} + +/// Layout the scaled content. +#[typst_macros::time(span = elem.span())] +pub fn layout_scale( + elem: &Packed<ScaleElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + // Compute the new region's approximate size. + let scale = resolve_scale(elem, engine, locator.relayout(), region.size, styles)?; + let size = region + .size + .zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r }) + .map(Abs::abs); + + measure_and_layout( + engine, + locator, + region, + size, + styles, + elem.body(), + Transform::scale(scale.x, scale.y), + elem.origin(styles).resolve(styles), + elem.reflow(styles), + ) +} + +/// Resolves scale parameters, preserving aspect ratio if one of the scales +/// is set to `auto`. +fn resolve_scale( + elem: &Packed<ScaleElem>, + engine: &mut Engine, + locator: Locator, + container: Size, + styles: StyleChain, +) -> SourceResult<Axes<Ratio>> { + fn resolve_axis( + axis: Smart<ScaleAmount>, + body: impl Fn() -> SourceResult<Abs>, + styles: StyleChain, + ) -> SourceResult<Smart<Ratio>> { + Ok(match axis { + Smart::Auto => Smart::Auto, + Smart::Custom(amt) => Smart::Custom(match amt { + ScaleAmount::Ratio(ratio) => ratio, + ScaleAmount::Length(length) => { + let length = length.resolve(styles); + Ratio::new(length / body()?) + } + }), + }) + } + + let size = Lazy::new(|| { + let pod = Region::new(container, Axes::splat(false)); + let frame = crate::layout_frame(engine, &elem.body, locator, styles, pod)?; + SourceResult::Ok(frame.size()) + }); + + let x = resolve_axis( + elem.x(styles), + || size.as_ref().map(|size| size.x).map_err(Clone::clone), + styles, + )?; + + let y = resolve_axis( + elem.y(styles), + || size.as_ref().map(|size| size.y).map_err(Clone::clone), + styles, + )?; + + match (x, y) { + (Smart::Auto, Smart::Auto) => { + bail!(elem.span(), "x and y cannot both be auto") + } + (Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)), + (Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => { + Ok(Axes::splat(v)) + } + } +} + +/// Layout the skewed content. +#[typst_macros::time(span = elem.span())] +pub fn layout_skew( + elem: &Packed<SkewElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult<Frame> { + let ax = elem.ax(styles); + let ay = elem.ay(styles); + let align = elem.origin(styles).resolve(styles); + + // Compute the new region's approximate size. + let size = if region.size.is_finite() { + compute_bounding_box(region.size, Transform::skew(ax, ay)).1 + } else { + Size::splat(Abs::inf()) + }; + + measure_and_layout( + engine, + locator, + region, + size, + styles, + elem.body(), + Transform::skew(ax, ay), + align, + elem.reflow(styles), + ) +} + +/// Applies a transformation to a frame, reflowing the layout if necessary. +#[allow(clippy::too_many_arguments)] +fn measure_and_layout( + engine: &mut Engine, + locator: Locator, + region: Region, + size: Size, + styles: StyleChain, + body: &Content, + transform: Transform, + align: Axes<FixedAlignment>, + reflow: bool, +) -> SourceResult<Frame> { + if reflow { + // Measure the size of the body. + let pod = Region::new(size, Axes::splat(false)); + let frame = crate::layout_frame(engine, body, locator.relayout(), styles, pod)?; + + // Actually perform the layout. + let pod = Region::new(frame.size(), Axes::splat(true)); + let mut frame = crate::layout_frame(engine, body, locator, styles, pod)?; + let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); + + // Compute the transform. + let ts = Transform::translate(x, y) + .pre_concat(transform) + .pre_concat(Transform::translate(-x, -y)); + + // Compute the bounding box and offset and wrap in a new frame. + let (offset, size) = compute_bounding_box(frame.size(), ts); + frame.transform(ts); + frame.translate(offset); + frame.set_size(size); + Ok(frame) + } else { + // Layout the body. + let mut frame = crate::layout_frame(engine, body, locator, styles, region)?; + let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); + + // Compute the transform. + let ts = Transform::translate(x, y) + .pre_concat(transform) + .pre_concat(Transform::translate(-x, -y)); + + // Apply the transform. + frame.transform(ts); + Ok(frame) + } +} + +/// Computes the bounding box and offset of a transformed area. +fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) { + let top_left = Point::zero().transform_inf(ts); + let top_right = Point::with_x(size.x).transform_inf(ts); + let bottom_left = Point::with_y(size.y).transform_inf(ts); + let bottom_right = size.to_point().transform_inf(ts); + + // We first compute the new bounding box of the rotated area. + let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x); + let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y); + let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x); + let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y); + + // Then we compute the new size of the area. + let width = max_x - min_x; + let height = max_y - min_y; + + (Point::new(-min_x, -min_y), Size::new(width.abs(), height.abs())) +} 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/src/diag.rs b/crates/typst-library/src/diag.rs index ad3f3e4a..bd4c90a1 100644 --- a/crates/typst/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -8,9 +8,9 @@ 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::syntax::package::{PackageSpec, PackageVersion}; -use crate::syntax::{Span, Spanned, SyntaxError}; use crate::{World, WorldExt}; /// Early-return with a [`StrResult`] or [`SourceResult`]. diff --git a/crates/typst/src/engine.rs b/crates/typst-library/src/engine.rs index c3a862f8..e532172e 100644 --- a/crates/typst/src/engine.rs +++ b/crates/typst-library/src/engine.rs @@ -6,15 +6,19 @@ 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::syntax::{FileId, Span}; +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. @@ -51,7 +55,9 @@ impl Engine<'_> { U: Send, F: Fn(&mut Engine, T) -> U + Send + Sync, { - let Engine { world, introspector, traced, ref route, .. } = *self; + 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. @@ -68,6 +74,7 @@ impl Engine<'_> { traced, sink: sink.track_mut(), route: route.clone(), + routines, }; (f(&mut engine, value), sink) }) @@ -173,7 +180,7 @@ impl Sink { /// Add a warning. pub fn warn(&mut self, warning: SourceDiagnostic) { // Check if warning is a duplicate. - let hash = crate::utils::hash128(&(&warning.span, &warning.message)); + let hash = typst_utils::hash128(&(&warning.span, &warning.message)); if self.warnings_set.insert(hash) { self.warnings.push(warning); } diff --git a/crates/typst/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index db443648..ee282a87 100644 --- a/crates/typst/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -1,12 +1,12 @@ 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, }; -use crate::syntax::{Span, Spanned}; /// Captured arguments to a function. /// diff --git a/crates/typst/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index 77d8829d..9c8aecac 100644 --- a/crates/typst/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -7,15 +7,14 @@ 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::eval::ops; use crate::foundations::{ - cast, func, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, Func, - IntoValue, Reflect, Repr, Str, Value, Version, + cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, + Func, IntoValue, Reflect, Repr, Str, Value, Version, }; -use crate::syntax::{Span, Spanned}; /// Create a new [`Array`] from values. #[macro_export] diff --git a/crates/typst/src/foundations/auto.rs b/crates/typst-library/src/foundations/auto.rs index cb9d999e..8237b450 100644 --- a/crates/typst/src/foundations/auto.rs +++ b/crates/typst-library/src/foundations/auto.rs @@ -1,6 +1,7 @@ -use ecow::EcoString; 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, diff --git a/crates/typst/src/foundations/bool.rs b/crates/typst-library/src/foundations/bool.rs index e88c8c6f..e88c8c6f 100644 --- a/crates/typst/src/foundations/bool.rs +++ b/crates/typst-library/src/foundations/bool.rs diff --git a/crates/typst/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index 5b77aa2a..05fe4763 100644 --- a/crates/typst/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -5,10 +5,10 @@ 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}; -use crate::utils::LazyHash; /// A sequence of bytes. /// diff --git a/crates/typst/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index a4dbab88..f12ca74c 100644 --- a/crates/typst/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -4,13 +4,12 @@ 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::eval::ops; -use crate::foundations::{cast, func, Decimal, IntoValue, Module, Scope, Value}; +use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value}; use crate::layout::{Angle, Fr, Length, Ratio}; -use crate::syntax::{Span, Spanned}; -use crate::utils::{round_int_with_precision, round_with_precision}; /// A module with calculation definitions. pub fn module() -> Module { diff --git a/crates/typst/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs index 2dc69236..84f38f36 100644 --- a/crates/typst/src/foundations/cast.rs +++ b/crates/typst-library/src/foundations/cast.rs @@ -1,3 +1,7 @@ +#[rustfmt::skip] +#[doc(inline)] +pub use typst_macros::{cast, Cast}; + use std::borrow::Cow; use std::fmt::Write; use std::hash::Hash; @@ -5,15 +9,11 @@ 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}; -use crate::syntax::{Span, Spanned}; - -#[rustfmt::skip] -#[doc(inline)] -pub use typst_macros::{cast, Cast}; /// Determine details of a type. /// diff --git a/crates/typst/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index 0103ff9b..a274b8bf 100644 --- a/crates/typst/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -10,6 +10,8 @@ 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; @@ -21,9 +23,7 @@ use crate::foundations::{ use crate::introspection::Location; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; -use crate::syntax::Span; use crate::text::UnderlineElem; -use crate::utils::{fat, singleton, LazyHash, SmallBitSet}; /// A piece of document content. /// diff --git a/crates/typst/src/foundations/context.rs b/crates/typst-library/src/foundations/context.rs index bf4bdcd2..bf4bdcd2 100644 --- a/crates/typst/src/foundations/context.rs +++ b/crates/typst-library/src/foundations/context.rs diff --git a/crates/typst/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs index d15cd417..d15cd417 100644 --- a/crates/typst/src/foundations/datetime.rs +++ b/crates/typst-library/src/foundations/datetime.rs diff --git a/crates/typst/src/foundations/decimal.rs b/crates/typst-library/src/foundations/decimal.rs index 809b298a..cf11e1dd 100644 --- a/crates/typst/src/foundations/decimal.rs +++ b/crates/typst-library/src/foundations/decimal.rs @@ -5,10 +5,11 @@ use std::str::FromStr; use ecow::{eco_format, EcoString}; use rust_decimal::MathematicalOps; +use typst_syntax::{ast, Span, Spanned}; use crate::diag::{warning, At, SourceResult}; -use crate::foundations::{cast, func, repr, scope, ty, Engine, Repr, Str}; -use crate::syntax::{ast, Span, Spanned}; +use crate::engine::Engine; +use crate::foundations::{cast, func, repr, scope, ty, Repr, Str}; use crate::World; /// A fixed-point decimal number type. @@ -447,8 +448,9 @@ cast! { mod tests { use std::str::FromStr; + use typst_utils::hash128; + use super::Decimal; - use crate::utils::hash128; #[test] fn test_decimals_with_equal_scales_hash_identically() { diff --git a/crates/typst/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index 6e61838e..e4ab54e7 100644 --- a/crates/typst/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -6,13 +6,13 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typst_syntax::is_ident; +use typst_utils::ArcExt; use crate::diag::{Hint, HintedStrResult, StrResult}; use crate::foundations::{ array, cast, func, repr, scope, ty, Array, Module, Repr, Str, Value, }; -use crate::syntax::is_ident; -use crate::utils::ArcExt; /// Create a new [`Dict`] from key-value pairs. #[macro_export] diff --git a/crates/typst/src/foundations/duration.rs b/crates/typst-library/src/foundations/duration.rs index 94d44fb2..94d44fb2 100644 --- a/crates/typst/src/foundations/duration.rs +++ b/crates/typst-library/src/foundations/duration.rs diff --git a/crates/typst/src/foundations/element.rs b/crates/typst-library/src/foundations/element.rs index e578d209..8da71965 100644 --- a/crates/typst/src/foundations/element.rs +++ b/crates/typst-library/src/foundations/element.rs @@ -7,6 +7,9 @@ use std::ptr::NonNull; use ecow::EcoString; use once_cell::sync::Lazy; use smallvec::SmallVec; +#[doc(inline)] +pub use typst_macros::elem; +use typst_utils::Static; use crate::diag::SourceResult; use crate::engine::Engine; @@ -15,10 +18,6 @@ use crate::foundations::{ StyleChain, Styles, Value, }; use crate::text::{Lang, Region}; -use crate::utils::Static; - -#[doc(inline)] -pub use typst_macros::elem; /// A document element. #[derive(Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst/src/foundations/fields.rs b/crates/typst-library/src/foundations/fields.rs index 422f30b8..422f30b8 100644 --- a/crates/typst/src/foundations/fields.rs +++ b/crates/typst-library/src/foundations/fields.rs diff --git a/crates/typst/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index 752b01d6..bb3232ee 100644 --- a/crates/typst/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -2,9 +2,9 @@ use std::num::ParseFloatError; use ecow::{eco_format, EcoString}; -use crate::diag::StrResult; +use crate::diag::{bail, StrResult}; use crate::foundations::{ - bail, cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str, + cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str, }; use crate::layout::Ratio; diff --git a/crates/typst/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 7ba7496f..1b40714b 100644 --- a/crates/typst/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -1,9 +1,14 @@ +#[doc(inline)] +pub use typst_macros::func; + use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use comemo::{Tracked, TrackedMut}; use ecow::{eco_format, EcoString}; use once_cell::sync::Lazy; +use typst_syntax::{ast, Span, SyntaxNode}; +use typst_utils::{singleton, LazyHash, Static}; use crate::diag::{bail, SourceResult, StrResult}; use crate::engine::Engine; @@ -11,11 +16,6 @@ use crate::foundations::{ cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, Selector, Type, Value, }; -use crate::syntax::{ast, Span, SyntaxNode}; -use crate::utils::{singleton, LazyHash, Static}; - -#[doc(inline)] -pub use typst_macros::func; /// A mapping from argument values to a return value. /// @@ -296,9 +296,10 @@ impl Func { args.finish()?; Ok(Value::Content(value)) } - Repr::Closure(closure) => crate::eval::call_closure( + Repr::Closure(closure) => (engine.routines.eval_closure)( self, closure, + engine.routines, engine.world, engine.introspector, engine.traced, diff --git a/crates/typst/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index 2832be91..e936353c 100644 --- a/crates/typst/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -2,9 +2,9 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError use ecow::{eco_format, EcoString}; -use crate::diag::StrResult; +use crate::diag::{bail, StrResult}; use crate::foundations::{ - bail, cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value, + cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value, }; /// A whole number. @@ -402,13 +402,16 @@ macro_rules! unsigned_int { ($($ty:ty)*) => { $(cast! { $ty, - self => if let Ok(int) = i64::try_from(self) { - Value::Int(int) - } else { - // Some u64 are too large to be cast as i64 - // In that case, we accept that there may be a - // precision loss, and use a floating point number - Value::Float(self as _) + self => { + #[allow(irrefutable_let_patterns)] + if let Ok(int) = i64::try_from(self) { + Value::Int(int) + } else { + // Some u64 are too large to be cast as i64 + // In that case, we accept that there may be a + // precision loss, and use a floating point number + Value::Float(self as _) + } }, v: i64 => v.try_into().map_err(|_| { if v < 0 { diff --git a/crates/typst/src/foundations/label.rs b/crates/typst-library/src/foundations/label.rs index 6b8f6798..726958df 100644 --- a/crates/typst/src/foundations/label.rs +++ b/crates/typst-library/src/foundations/label.rs @@ -1,7 +1,7 @@ use ecow::{eco_format, EcoString}; +use typst_utils::PicoStr; use crate::foundations::{func, scope, ty, Repr}; -use crate::utils::PicoStr; /// A label for an element. /// diff --git a/crates/typst/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index dc939932..a6d6c253 100644 --- a/crates/typst/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -1,11 +1,10 @@ //! Foundational types and functions. pub mod calc; +pub mod ops; pub mod repr; pub mod sys; -pub use typst_macros::{scope, ty}; - mod args; mod array; mod auto; @@ -24,7 +23,6 @@ mod float; mod func; mod int; mod label; -mod methods; mod module; mod none; mod plugin; @@ -32,6 +30,7 @@ mod scope; mod selector; mod str; mod styles; +mod symbol; mod ty; mod value; mod version; @@ -53,7 +52,6 @@ pub use self::float::*; pub use self::func::*; pub use self::int::*; pub use self::label::*; -pub(crate) use self::methods::*; pub use self::module::*; pub use self::none::*; pub use self::plugin::*; @@ -62,9 +60,11 @@ pub use self::scope::*; pub use self::selector::*; pub use self::str::*; pub use self::styles::*; +pub use self::symbol::*; pub use self::ty::*; pub use self::value::*; pub use self::version::*; +pub use typst_macros::{scope, ty}; #[rustfmt::skip] #[doc(hidden)] @@ -75,11 +75,11 @@ pub use { }; use ecow::EcoString; +use typst_syntax::Spanned; use crate::diag::{bail, SourceResult, StrResult}; use crate::engine::Engine; -use crate::eval::EvalMode; -use crate::syntax::Spanned; +use crate::routines::EvalMode; /// Foundational types and functions. /// @@ -108,6 +108,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict) { global.define_type::<Selector>(); global.define_type::<Datetime>(); global.define_type::<Decimal>(); + global.define_type::<Symbol>(); global.define_type::<Duration>(); global.define_type::<Version>(); global.define_type::<Plugin>(); @@ -297,5 +298,5 @@ pub fn eval( for (key, value) in dict { scope.define_spanned(key, value, span); } - crate::eval::eval_string(engine.world, &text, span, mode, scope) + (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) } diff --git a/crates/typst/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 91b50855..a476d6af 100644 --- a/crates/typst/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -2,10 +2,10 @@ use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use ecow::{eco_format, EcoString}; +use typst_syntax::FileId; use crate::diag::StrResult; use crate::foundations::{repr, ty, Content, Scope, Value}; -use crate::syntax::FileId; /// An evaluated module, either built-in or resulting from a file. /// diff --git a/crates/typst/src/foundations/none.rs b/crates/typst-library/src/foundations/none.rs index d376c0c5..d376c0c5 100644 --- a/crates/typst/src/foundations/none.rs +++ b/crates/typst-library/src/foundations/none.rs diff --git a/crates/typst/src/eval/ops.rs b/crates/typst-library/src/foundations/ops.rs index 52032ae9..ba36137f 100644 --- a/crates/typst/src/eval/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -3,102 +3,14 @@ use std::cmp::Ordering; use ecow::eco_format; +use typst_utils::Numeric; -use crate::diag::{bail, At, HintedStrResult, SourceResult, StrResult}; -use crate::eval::{access_dict, Access, Eval, Vm}; +use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value}; use crate::layout::{Alignment, Length, Rel}; -use crate::syntax::ast::{self, AstNode}; use crate::text::TextElem; -use crate::utils::Numeric; use crate::visualize::Stroke; -impl Eval for ast::Unary<'_> { - type Output = Value; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let value = self.expr().eval(vm)?; - let result = match self.op() { - ast::UnOp::Pos => pos(value), - ast::UnOp::Neg => neg(value), - ast::UnOp::Not => not(value), - }; - result.at(self.span()) - } -} - -impl Eval for ast::Binary<'_> { - type Output = Value; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - match self.op() { - ast::BinOp::Add => apply_binary(self, vm, add), - ast::BinOp::Sub => apply_binary(self, vm, sub), - ast::BinOp::Mul => apply_binary(self, vm, mul), - ast::BinOp::Div => apply_binary(self, vm, div), - ast::BinOp::And => apply_binary(self, vm, and), - ast::BinOp::Or => apply_binary(self, vm, or), - ast::BinOp::Eq => apply_binary(self, vm, eq), - ast::BinOp::Neq => apply_binary(self, vm, neq), - ast::BinOp::Lt => apply_binary(self, vm, lt), - ast::BinOp::Leq => apply_binary(self, vm, leq), - ast::BinOp::Gt => apply_binary(self, vm, gt), - ast::BinOp::Geq => apply_binary(self, vm, geq), - ast::BinOp::In => apply_binary(self, vm, in_), - ast::BinOp::NotIn => apply_binary(self, vm, not_in), - ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)), - ast::BinOp::AddAssign => apply_assignment(self, vm, add), - ast::BinOp::SubAssign => apply_assignment(self, vm, sub), - ast::BinOp::MulAssign => apply_assignment(self, vm, mul), - ast::BinOp::DivAssign => apply_assignment(self, vm, div), - } - } -} - -/// Apply a basic binary operation. -fn apply_binary( - binary: ast::Binary, - vm: &mut Vm, - op: fn(Value, Value) -> HintedStrResult<Value>, -) -> SourceResult<Value> { - let lhs = binary.lhs().eval(vm)?; - - // Short-circuit boolean operations. - if (binary.op() == ast::BinOp::And && lhs == false.into_value()) - || (binary.op() == ast::BinOp::Or && lhs == true.into_value()) - { - return Ok(lhs); - } - - let rhs = binary.rhs().eval(vm)?; - op(lhs, rhs).at(binary.span()) -} - -/// Apply an assignment operation. -fn apply_assignment( - binary: ast::Binary, - vm: &mut Vm, - op: fn(Value, Value) -> HintedStrResult<Value>, -) -> SourceResult<Value> { - let rhs = binary.rhs().eval(vm)?; - let lhs = binary.lhs(); - - // An assignment to a dictionary field is different from a normal access - // since it can create the field instead of just modifying it. - if binary.op() == ast::BinOp::Assign { - if let ast::Expr::FieldAccess(access) = lhs { - let dict = access_dict(vm, access)?; - dict.insert(access.field().get().clone().into(), rhs); - return Ok(Value::None); - } - } - - let location = binary.lhs().access(vm)?; - let lhs = std::mem::take(&mut *location); - *location = op(lhs, rhs).at(binary.span())?; - Ok(Value::None) -} - /// Bail with a type mismatch error. macro_rules! mismatch { ($fmt:expr, $($value:expr),* $(,)?) => { diff --git a/crates/typst/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index 4dc03014..31107dc3 100644 --- a/crates/typst/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -3,12 +3,12 @@ use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; use wasmi::{AsContext, AsContextMut}; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{func, repr, scope, ty, Bytes}; -use crate::syntax::Spanned; use crate::World; /// A WebAssembly plugin. diff --git a/crates/typst/src/foundations/repr.rs b/crates/typst-library/src/foundations/repr.rs index 14510968..e219b0f1 100644 --- a/crates/typst/src/foundations/repr.rs +++ b/crates/typst-library/src/foundations/repr.rs @@ -1,9 +1,9 @@ //! Debug representation of values. use ecow::{eco_format, EcoString}; +use typst_utils::round_with_precision; use crate::foundations::{func, Str, Value}; -use crate::utils::round_with_precision; /// The Unicode minus sign. pub const MINUS_SIGN: &str = "\u{2212}"; diff --git a/crates/typst/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index c5c65485..b51f8caa 100644 --- a/crates/typst/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -1,22 +1,22 @@ +#[doc(inline)] +pub use typst_macros::category; + use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use ecow::{eco_format, EcoString}; use indexmap::IndexMap; +use typst_syntax::ast::{self, AstNode}; +use typst_syntax::Span; +use typst_utils::Static; use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, NativeType, Type, Value, }; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::Span; -use crate::utils::Static; use crate::Library; -#[doc(inline)] -pub use typst_macros::category; - /// A stack of scopes. #[derive(Debug, Default, Clone)] pub struct Scopes<'a> { diff --git a/crates/typst/src/foundations/selector.rs b/crates/typst-library/src/foundations/selector.rs index 575baa13..bf5449d9 100644 --- a/crates/typst/src/foundations/selector.rs +++ b/crates/typst-library/src/foundations/selector.rs @@ -8,10 +8,9 @@ use smallvec::SmallVec; use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{ cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue, - Func, Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value, + Func, Label, Reflect, Regex, Repr, Str, StyleChain, Symbol, Type, Value, }; use crate::introspection::{Introspector, Locatable, Location, Unqueriable}; -use crate::symbols::Symbol; /// A helper macro to create a field selector used in [`Selector::Elem`] #[macro_export] diff --git a/crates/typst/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index d90b6f20..fc221c49 100644 --- a/crates/typst/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -6,6 +6,8 @@ use std::ops::{Add, AddAssign, Deref, Range}; use comemo::Tracked; use ecow::EcoString; use serde::{Deserialize, Serialize}; +use typst_syntax::{Span, Spanned}; +use typst_utils::PicoStr; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, At, SourceResult, StrResult}; @@ -15,8 +17,6 @@ use crate::foundations::{ IntoValue, Label, Repr, Type, Value, Version, }; use crate::layout::Alignment; -use crate::syntax::{Span, Spanned}; -use crate::utils::PicoStr; /// Create a new [`Str`] from a format string. #[macro_export] @@ -27,12 +27,12 @@ macro_rules! __format_str { }}; } -#[doc(inline)] -pub use crate::__format_str as format_str; - #[doc(hidden)] pub use ecow::eco_format; +#[doc(inline)] +pub use crate::__format_str as format_str; + /// A sequence of Unicode codepoints. /// /// You can iterate over the grapheme clusters of the string using a [for diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index a3c8e0c4..af4909e5 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -6,6 +6,8 @@ use std::{mem, ptr}; use comemo::{Track, Tracked}; use ecow::{eco_vec, EcoString, EcoVec}; use smallvec::SmallVec; +use typst_syntax::Span; +use typst_utils::LazyHash; use crate::diag::{warning, SourceResult, Trace, Tracepoint}; use crate::engine::Engine; @@ -14,9 +16,7 @@ use crate::foundations::{ Selector, Show, }; use crate::introspection::Locatable; -use crate::syntax::Span; use crate::text::{FontFamily, FontList, TextElem}; -use crate::utils::LazyHash; /// Provides access to active styles. /// diff --git a/crates/typst/src/symbols/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 0be4adc4..86676fa2 100644 --- a/crates/typst/src/symbols/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -1,3 +1,6 @@ +#[doc(inline)] +pub use typst_macros::symbols; + use std::cmp::Reverse; use std::collections::BTreeSet; use std::fmt::{self, Debug, Display, Formatter, Write}; @@ -5,13 +8,10 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; +use typst_syntax::{Span, Spanned}; use crate::diag::{bail, SourceResult, StrResult}; use crate::foundations::{cast, func, scope, ty, Array, Func}; -use crate::syntax::{Span, Spanned}; - -#[doc(inline)] -pub use typst_macros::symbols; /// A Unicode symbol. /// diff --git a/crates/typst/src/foundations/sys.rs b/crates/typst-library/src/foundations/sys.rs index 7c128104..7c128104 100644 --- a/crates/typst/src/foundations/sys.rs +++ b/crates/typst-library/src/foundations/sys.rs diff --git a/crates/typst/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 3bcde61f..70845dd2 100644 --- a/crates/typst/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -1,18 +1,17 @@ +#[doc(inline)] +pub use typst_macros::{scope, ty}; + use std::cmp::Ordering; use std::fmt::{self, Debug, Display, Formatter}; use ecow::{eco_format, EcoString}; use once_cell::sync::Lazy; +use typst_utils::Static; use crate::diag::StrResult; use crate::foundations::{ cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, }; -use crate::utils::Static; - -#[rustfmt::skip] -#[doc(inline)] -pub use typst_macros::{scope, ty}; /// Describes a kind of value. /// diff --git a/crates/typst/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index cabe5647..fbfa5a0e 100644 --- a/crates/typst/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -8,20 +8,18 @@ use ecow::{eco_format, EcoString}; use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer}; use serde::de::{Error, MapAccess, SeqAccess, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use typst_syntax::{ast, Span}; +use typst_utils::ArcExt; use crate::diag::{HintedStrResult, HintedString, StrResult}; -use crate::eval::ops; use crate::foundations::{ - fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal, - Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement, - NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, Styles, Type, - Version, + fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, + Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, + NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, + Styles, Symbol, Type, Version, }; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; -use crate::symbols::Symbol; -use crate::syntax::{ast, Span}; use crate::text::{RawContent, RawElem, TextElem}; -use crate::utils::ArcExt; use crate::visualize::{Color, Gradient, Pattern}; /// A computational value. diff --git a/crates/typst/src/foundations/version.rs b/crates/typst-library/src/foundations/version.rs index 62c02917..62c02917 100644 --- a/crates/typst/src/foundations/version.rs +++ b/crates/typst-library/src/foundations/version.rs diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index 38da6363..2e7180c6 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -4,6 +4,8 @@ use std::str::FromStr; use comemo::{Track, Tracked, TrackedMut}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use smallvec::{smallvec, SmallVec}; +use typst_syntax::Span; +use typst_utils::NonZeroExt; use crate::diag::{bail, warning, At, HintedStrResult, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; @@ -16,8 +18,7 @@ use crate::introspection::{Introspector, Locatable, Location, Tag}; use crate::layout::{Frame, FrameItem, PageElem}; use crate::math::EquationElem; use crate::model::{FigureElem, FootnoteElem, HeadingElem, Numbering, NumberingPattern}; -use crate::syntax::Span; -use crate::utils::NonZeroExt; +use crate::routines::Routines; use crate::World; /// Counts through pages, elements, and more. @@ -278,6 +279,7 @@ impl Counter { engine: &mut Engine, ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { self.sequence_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -290,6 +292,7 @@ impl Counter { #[comemo::memoize] fn sequence_impl( &self, + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -297,6 +300,7 @@ impl Counter { route: Tracked<Route>, ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { let mut engine = Engine { + routines, world, introspector, traced, diff --git a/crates/typst/src/introspection/here.rs b/crates/typst-library/src/introspection/here.rs index 9d613381..9d613381 100644 --- a/crates/typst/src/introspection/here.rs +++ b/crates/typst-library/src/introspection/here.rs diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index 113f9afe..f4eaea30 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -6,13 +6,13 @@ use std::sync::RwLock; use ecow::EcoVec; use smallvec::SmallVec; +use typst_utils::NonZeroExt; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; use crate::introspection::{Location, Tag}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; -use crate::utils::NonZeroExt; /// Can be queried for elements and their positions. #[derive(Default, Clone)] @@ -97,7 +97,7 @@ impl Introspector { impl Introspector { /// Query for all matching elements. pub fn query(&self, selector: &Selector) -> EcoVec<Content> { - let hash = crate::utils::hash128(selector); + let hash = typst_utils::hash128(selector); if let Some(output) = self.queries.get(hash) { return output; } diff --git a/crates/typst/src/introspection/locate.rs b/crates/typst-library/src/introspection/locate.rs index 8991ae9b..9a0e28e2 100644 --- a/crates/typst/src/introspection/locate.rs +++ b/crates/typst-library/src/introspection/locate.rs @@ -1,4 +1,5 @@ use comemo::{Track, Tracked}; +use typst_syntax::Span; use crate::diag::{warning, HintedStrResult, SourceResult}; use crate::engine::Engine; @@ -7,7 +8,6 @@ use crate::foundations::{ Show, StyleChain, Value, }; use crate::introspection::{Locatable, Location}; -use crate::syntax::Span; /// Determines the location of an element in the document. /// diff --git a/crates/typst/src/introspection/location.rs b/crates/typst-library/src/introspection/location.rs index 5c520d25..8f4caaec 100644 --- a/crates/typst/src/introspection/location.rs +++ b/crates/typst-library/src/introspection/location.rs @@ -42,7 +42,7 @@ impl Location { /// example, in bibliography management to create individual linkable /// locations for reference entries from the bibliography's location. pub fn variant(self, n: usize) -> Self { - Self(crate::utils::hash128(&(self.0, n))) + Self(typst_utils::hash128(&(self.0, n))) } } diff --git a/crates/typst/src/introspection/locator.rs b/crates/typst-library/src/introspection/locator.rs index 3e9e5c89..4045aa9c 100644 --- a/crates/typst/src/introspection/locator.rs +++ b/crates/typst-library/src/introspection/locator.rs @@ -210,7 +210,7 @@ impl<'a> Locator<'a> { None => Resolved::Hash(self.local), Some(outer) => match outer.resolve() { Resolved::Hash(outer) => { - Resolved::Hash(crate::utils::hash128(&(self.local, outer))) + Resolved::Hash(typst_utils::hash128(&(self.local, outer))) } Resolved::Measure(anchor) => Resolved::Measure(anchor), }, @@ -257,7 +257,7 @@ impl<'a> SplitLocator<'a> { /// A common choice for a key is the span of the content that will be /// layouted with this locator. pub fn next<K: Hash>(&mut self, key: &K) -> Locator<'a> { - self.next_inner(crate::utils::hash128(key)) + self.next_inner(typst_utils::hash128(key)) } /// Produces a sublocator for a subtree. @@ -272,7 +272,7 @@ impl<'a> SplitLocator<'a> { // Combine the key, disambiguator and local hash into a sub-local hash. // The outer information is not yet merged into this, it is added // on-demand in `Locator::resolve`. - let local = crate::utils::hash128(&(key, disambiguator, self.local)); + let local = typst_utils::hash128(&(key, disambiguator, self.local)); Locator { outer: self.outer, local } } diff --git a/crates/typst/src/introspection/metadata.rs b/crates/typst-library/src/introspection/metadata.rs index 06000174..06000174 100644 --- a/crates/typst/src/introspection/metadata.rs +++ b/crates/typst-library/src/introspection/metadata.rs diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst-library/src/introspection/mod.rs index b1ff2e08..b1ff2e08 100644 --- a/crates/typst/src/introspection/mod.rs +++ b/crates/typst-library/src/introspection/mod.rs diff --git a/crates/typst/src/introspection/query.rs b/crates/typst-library/src/introspection/query.rs index 07f761a8..7b106bf0 100644 --- a/crates/typst/src/introspection/query.rs +++ b/crates/typst-library/src/introspection/query.rs @@ -1,10 +1,10 @@ use comemo::Tracked; +use typst_syntax::Span; use crate::diag::{warning, HintedStrResult}; use crate::engine::Engine; use crate::foundations::{func, Array, Context, LocatableSelector, Value}; use crate::introspection::Location; -use crate::syntax::Span; /// Finds elements in the document. /// diff --git a/crates/typst/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index 6a3d68e2..13c96b50 100644 --- a/crates/typst/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -1,5 +1,6 @@ use comemo::{Track, Tracked, TrackedMut}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_syntax::Span; use crate::diag::{bail, warning, At, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; @@ -9,7 +10,7 @@ use crate::foundations::{ Value, }; use crate::introspection::{Introspector, Locatable, Location}; -use crate::syntax::Span; +use crate::routines::Routines; use crate::World; /// Manages stateful parts of your document. @@ -211,6 +212,7 @@ impl State { /// of state updates from quadratic to linear. fn sequence(&self, engine: &mut Engine) -> SourceResult<EcoVec<Value>> { self.sequence_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -223,6 +225,7 @@ impl State { #[comemo::memoize] fn sequence_impl( &self, + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, traced: Tracked<Traced>, @@ -230,6 +233,7 @@ impl State { route: Tracked<Route>, ) -> SourceResult<EcoVec<Value>> { let mut engine = Engine { + routines, world, introspector, traced, diff --git a/crates/typst/src/introspection/tag.rs b/crates/typst-library/src/introspection/tag.rs index b2bae28e..b2bae28e 100644 --- a/crates/typst/src/introspection/tag.rs +++ b/crates/typst-library/src/introspection/tag.rs diff --git a/crates/typst/src/layout/abs.rs b/crates/typst-library/src/layout/abs.rs index 981a64ff..e19b037a 100644 --- a/crates/typst/src/layout/abs.rs +++ b/crates/typst-library/src/layout/abs.rs @@ -3,9 +3,9 @@ use std::iter::Sum; use std::ops::{Add, Div, Mul, Neg, Rem}; use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; use crate::foundations::{cast, repr, Fold, Repr, Value}; -use crate::utils::{Numeric, Scalar}; /// An absolute length. #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] diff --git a/crates/typst/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index e8ba4d7c..e8ba4d7c 100644 --- a/crates/typst/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs diff --git a/crates/typst/src/layout/angle.rs b/crates/typst-library/src/layout/angle.rs index 63e30186..d1410786 100644 --- a/crates/typst/src/layout/angle.rs +++ b/crates/typst-library/src/layout/angle.rs @@ -4,9 +4,9 @@ use std::iter::Sum; use std::ops::{Add, Div, Mul, Neg}; use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; use crate::foundations::{func, repr, scope, ty, Repr}; -use crate::utils::{Numeric, Scalar}; /// An angle describing a rotation. /// diff --git a/crates/typst/src/layout/axes.rs b/crates/typst-library/src/layout/axes.rs index a96fa850..7a73ba79 100644 --- a/crates/typst/src/layout/axes.rs +++ b/crates/typst-library/src/layout/axes.rs @@ -2,10 +2,11 @@ use std::any::Any; use std::fmt::{self, Debug, Formatter}; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not}; +use typst_utils::Get; + use crate::diag::bail; use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain}; use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size}; -use crate::utils::Get; /// A container with a horizontal and vertical component. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs index 7678e03f..f2f36441 100644 --- a/crates/typst/src/layout/columns.rs +++ b/crates/typst-library/src/layout/columns.rs @@ -3,10 +3,7 @@ use std::num::NonZeroUsize; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::introspection::Locator; -use crate::layout::{ - layout_fragment_with_columns, BlockElem, Fragment, Length, Ratio, Regions, Rel, -}; +use crate::layout::{BlockElem, Length, Ratio, Rel}; /// Separates a region into multiple equally sized columns. /// @@ -62,33 +59,13 @@ pub struct ColumnsElem { } impl Show for Packed<ColumnsElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::multi_layouter(self.clone(), layout_columns) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_columns) .pack() .spanned(self.span())) } } -/// Layout the columns. -#[typst_macros::time(span = elem.span())] -fn layout_columns( - elem: &Packed<ColumnsElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, -) -> SourceResult<Fragment> { - layout_fragment_with_columns( - engine, - &elem.body, - locator, - styles, - regions, - elem.count(styles), - elem.gutter(styles), - ) -} - /// Forces a column break. /// /// The function will behave like a [page break]($pagebreak) when used in a diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs new file mode 100644 index 00000000..266d1d88 --- /dev/null +++ b/crates/typst-library/src/layout/container.rs @@ -0,0 +1,563 @@ +use crate::diag::{bail, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Smart, + StyleChain, Value, +}; +use crate::introspection::Locator; +use crate::layout::{ + Abs, Corners, Em, Fr, Fragment, Frame, Length, Region, Regions, Rel, Sides, Size, + Spacing, +}; +use crate::visualize::{Paint, Stroke}; + +/// An inline-level container that sizes content. +/// +/// All elements except inline math, text, and boxes are block-level and cannot +/// occur inside of a paragraph. The box function can be used to integrate such +/// elements into a paragraph. Boxes take the size of their contents by default +/// but can also be sized explicitly. +/// +/// # Example +/// ```example +/// Refer to the docs +/// #box( +/// height: 9pt, +/// image("docs.svg") +/// ) +/// for more information. +/// ``` +#[elem] +pub struct BoxElem { + /// The width of the box. + /// + /// Boxes can have [fractional]($fraction) widths, as the example below + /// demonstrates. + /// + /// _Note:_ Currently, only boxes and only their widths might be fractionally + /// sized within paragraphs. Support for fractionally sized images, shapes, + /// and more might be added in the future. + /// + /// ```example + /// Line in #box(width: 1fr, line(length: 100%)) between. + /// ``` + pub width: Sizing, + + /// The height of the box. + pub height: Smart<Rel<Length>>, + + /// An amount to shift the box's baseline by. + /// + /// ```example + /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). + /// ``` + #[resolve] + pub baseline: Rel<Length>, + + /// The box's background color. See the + /// [rectangle's documentation]($rect.fill) for more details. + pub fill: Option<Paint>, + + /// The box's border color. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<Stroke>>>, + + /// How much to round the box's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the box's content. + /// + /// _Note:_ When the box contains text, its exact size depends on the + /// current [text edges]($text.top-edge). + /// + /// ```example + /// #rect(inset: 0pt)[Tight] + /// ``` + #[resolve] + #[fold] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the box's size without affecting the layout. + /// + /// This is useful to prevent padding from affecting line layout. For a + /// generalized version of the example below, see the documentation for the + /// [raw text's block parameter]($raw.block). + /// + /// ```example + /// An inline + /// #box( + /// fill: luma(235), + /// inset: (x: 3pt, y: 0pt), + /// outset: (y: 3pt), + /// radius: 2pt, + /// )[rectangle]. + /// ``` + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// Whether to clip the content inside the box. + /// + /// Clipping is useful when the box's content is larger than the box itself, + /// as any content that exceeds the box's bounds will be hidden. + /// + /// ```example + /// #box( + /// width: 50pt, + /// height: 50pt, + /// clip: true, + /// image("tiger.jpg", width: 100pt, height: 100pt) + /// ) + /// ``` + #[default(false)] + pub clip: bool, + + /// The contents of the box. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +/// An inline-level container that can produce arbitrary items that can break +/// across lines. +#[elem(Construct)] +pub struct InlineElem { + /// A callback that is invoked with the regions to produce arbitrary + /// inline items. + #[required] + #[internal] + body: callbacks::InlineCallback, +} + +impl Construct for InlineElem { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl InlineElem { + /// Create an inline-level item with a custom layouter. + #[allow(clippy::type_complexity)] + pub fn layouter<T: NativeElement>( + captured: Packed<T>, + callback: fn( + content: &Packed<T>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>>, + ) -> Self { + Self::new(callbacks::InlineCallback::new(captured, callback)) + } +} + +impl Packed<InlineElem> { + /// Layout the element. + pub fn layout( + &self, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>> { + self.body().call(engine, locator, styles, region) + } +} + +/// Layouted items suitable for placing in a paragraph. +#[derive(Debug, Clone)] +pub enum InlineItem { + /// Absolute spacing between other items, and whether it is weak. + Space(Abs, bool), + /// Layouted inline-level content. + Frame(Frame), +} + +/// A block-level container. +/// +/// Such a container can be used to separate content, size it, and give it a +/// background or border. +/// +/// # Examples +/// With a block, you can give a background to content while still allowing it +/// to break across multiple pages. +/// ```example +/// #set page(height: 100pt) +/// #block( +/// fill: luma(230), +/// inset: 8pt, +/// radius: 4pt, +/// lorem(30), +/// ) +/// ``` +/// +/// Blocks are also useful to force elements that would otherwise be inline to +/// become block-level, especially when writing show rules. +/// ```example +/// #show heading: it => it.body +/// = Blockless +/// More text. +/// +/// #show heading: it => block(it.body) +/// = Blocky +/// More text. +/// ``` +#[elem] +pub struct BlockElem { + /// The block's width. + /// + /// ```example + /// #set align(center) + /// #block( + /// width: 60%, + /// inset: 8pt, + /// fill: silver, + /// lorem(10), + /// ) + /// ``` + pub width: Smart<Rel<Length>>, + + /// The block's height. When the height is larger than the remaining space + /// on a page and [`breakable`]($block.breakable) is `{true}`, the + /// block will continue on the next page with the remaining height. + /// + /// ```example + /// #set page(height: 80pt) + /// #set align(center) + /// #block( + /// width: 80%, + /// height: 150%, + /// fill: aqua, + /// ) + /// ``` + pub height: Sizing, + + /// Whether the block can be broken and continue on the next page. + /// + /// ```example + /// #set page(height: 80pt) + /// The following block will + /// jump to its own page. + /// #block( + /// breakable: false, + /// lorem(15), + /// ) + /// ``` + #[default(true)] + pub breakable: bool, + + /// The block's background color. See the + /// [rectangle's documentation]($rect.fill) for more details. + pub fill: Option<Paint>, + + /// The block's border color. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Sides<Option<Option<Stroke>>>, + + /// How much to round the block's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the block's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the block's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The spacing around the block. When `{auto}`, inherits the paragraph + /// [`spacing`]($par.spacing). + /// + /// For two adjacent blocks, the larger of the first block's `above` and the + /// second block's `below` spacing wins. Moreover, block spacing takes + /// precedence over paragraph [`spacing`]($par.spacing). + /// + /// Note that this is only a shorthand to set `above` and `below` to the + /// same value. Since the values for `above` and `below` might differ, a + /// [context] block only provides access to `{block.above}` and + /// `{block.below}`, not to `{block.spacing}` directly. + /// + /// This property can be used in combination with a show rule to adjust the + /// spacing around arbitrary block-level elements. + /// + /// ```example + /// #set align(center) + /// #show math.equation: set block(above: 8pt, below: 16pt) + /// + /// This sum of $x$ and $y$: + /// $ x + y = z $ + /// A second paragraph. + /// ``` + #[external] + #[default(Em::new(1.2).into())] + pub spacing: Spacing, + + /// The spacing between this block and its predecessor. + #[parse( + let spacing = args.named("spacing")?; + args.named("above")?.or(spacing) + )] + pub above: Smart<Spacing>, + + /// The spacing between this block and its successor. + #[parse(args.named("below")?.or(spacing))] + pub below: Smart<Spacing>, + + /// Whether to clip the content inside the block. + /// + /// Clipping is useful when the block's content is larger than the block itself, + /// as any content that exceeds the block's bounds will be hidden. + /// + /// ```example + /// #block( + /// width: 50pt, + /// height: 50pt, + /// clip: true, + /// image("tiger.jpg", width: 100pt, height: 100pt) + /// ) + /// ``` + #[default(false)] + pub clip: bool, + + /// Whether this block must stick to the following one, with no break in + /// between. + /// + /// This is, by default, set on heading blocks to prevent orphaned headings + /// at the bottom of the page. + /// + /// ```example + /// >>> #set page(height: 140pt) + /// // Disable stickiness of headings. + /// #show heading: set block(sticky: false) + /// #lorem(20) + /// + /// = Chapter + /// #lorem(10) + /// ``` + #[default(false)] + pub sticky: bool, + + /// The contents of the block. + #[positional] + #[borrowed] + pub body: Option<BlockBody>, +} + +impl BlockElem { + /// Create a block with a custom single-region layouter. + /// + /// Such a block must have `breakable: false` (which is set by this + /// constructor). + pub fn single_layouter<T: NativeElement>( + captured: Packed<T>, + f: fn( + content: &Packed<T>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame>, + ) -> Self { + Self::new() + .with_breakable(false) + .with_body(Some(BlockBody::SingleLayouter( + callbacks::BlockSingleCallback::new(captured, f), + ))) + } + + /// Create a block with a custom multi-region layouter. + pub fn multi_layouter<T: NativeElement>( + captured: Packed<T>, + f: fn( + content: &Packed<T>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment>, + ) -> Self { + Self::new().with_body(Some(BlockBody::MultiLayouter( + callbacks::BlockMultiCallback::new(captured, f), + ))) + } +} + +/// The contents of a block. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum BlockBody { + /// The block contains normal content. + Content(Content), + /// The block contains a layout callback that needs access to just one + /// base region. + SingleLayouter(callbacks::BlockSingleCallback), + /// The block contains a layout callback that needs access to the exact + /// regions. + MultiLayouter(callbacks::BlockMultiCallback), +} + +impl Default for BlockBody { + fn default() -> Self { + Self::Content(Content::default()) + } +} + +cast! { + BlockBody, + self => match self { + Self::Content(content) => content.into_value(), + _ => Value::Auto, + }, + v: Content => Self::Content(v), +} + +/// Defines how to size something along an axis. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Sizing { + /// A track that fits its item's contents. + Auto, + /// A size specified in absolute terms and relative to the parent's size. + Rel(Rel), + /// A size specified as a fraction of the remaining free space in the + /// parent. + Fr(Fr), +} + +impl Sizing { + /// Whether this is an automatic sizing. + pub fn is_auto(self) -> bool { + matches!(self, Self::Auto) + } + + /// Whether this is fractional sizing. + pub fn is_fractional(self) -> bool { + matches!(self, Self::Fr(_)) + } +} + +impl Default for Sizing { + fn default() -> Self { + Self::Auto + } +} + +impl From<Smart<Rel>> for Sizing { + fn from(smart: Smart<Rel>) -> Self { + match smart { + Smart::Auto => Self::Auto, + Smart::Custom(rel) => Self::Rel(rel), + } + } +} + +impl<T: Into<Spacing>> From<T> for Sizing { + fn from(spacing: T) -> Self { + match spacing.into() { + Spacing::Rel(rel) => Self::Rel(rel), + Spacing::Fr(fr) => Self::Fr(fr), + } + } +} + +cast! { + Sizing, + self => match self { + Self::Auto => Value::Auto, + Self::Rel(rel) => rel.into_value(), + Self::Fr(fr) => fr.into_value(), + }, + _: AutoValue => Self::Auto, + v: Rel<Length> => Self::Rel(v), + v: Fr => Self::Fr(v), +} + +/// Manual closure implementations for layout callbacks. +/// +/// Normal closures are not `Hash`, so we can't use them. +mod callbacks { + use super::*; + + macro_rules! callback { + ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => { + #[derive(Debug, Clone, PartialEq, Hash)] + pub struct $name { + captured: Content, + f: fn(&Content, $($param_ty),*) -> $ret, + } + + impl $name { + pub fn new<T: NativeElement>( + captured: Packed<T>, + f: fn(&Packed<T>, $($param_ty),*) -> $ret, + ) -> Self { + Self { + // Type-erased the content. + captured: captured.pack(), + // Safety: The only difference between the two function + // pointer types is the type of the first parameter, + // which changes from `&Packed<T>` to `&Content`. This + // is safe because: + // - `Packed<T>` is a transparent wrapper around + // `Content`, so for any `T` it has the same memory + // representation as `Content`. + // - While `Packed<T>` imposes the additional constraint + // that the content is of type `T`, this constraint is + // upheld: It is initially the case because we store a + // `Packed<T>` above. It keeps being the case over the + // lifetime of the closure because `capture` is a + // private field and `Content`'s `Clone` impl is + // guaranteed to retain the type (if it didn't, + // literally everything would break). + #[allow(clippy::missing_transmute_annotations)] + f: unsafe { std::mem::transmute(f) }, + } + } + + pub fn call(&self, $($param: $param_ty),*) -> $ret { + (self.f)(&self.captured, $($param),*) + } + } + }; + } + + callback! { + InlineCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>> + } + + callback! { + BlockSingleCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + } + + callback! { + BlockMultiCallback = ( + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + } +} diff --git a/crates/typst/src/layout/corners.rs b/crates/typst-library/src/layout/corners.rs index 075f51bc..fe69126a 100644 --- a/crates/typst/src/layout/corners.rs +++ b/crates/typst-library/src/layout/corners.rs @@ -1,12 +1,13 @@ use std::fmt::{self, Debug, Formatter}; +use typst_utils::Get; + use crate::diag::HintedStrResult; use crate::foundations::{ AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value, }; use crate::layout::Side; -use crate::utils::Get; /// A container with components for the four corners of a rectangle. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst/src/layout/dir.rs b/crates/typst-library/src/layout/dir.rs index 9a2e7710..9a2e7710 100644 --- a/crates/typst/src/layout/dir.rs +++ b/crates/typst-library/src/layout/dir.rs diff --git a/crates/typst/src/layout/em.rs b/crates/typst-library/src/layout/em.rs index 7aa5dfcf..e2d8b120 100644 --- a/crates/typst/src/layout/em.rs +++ b/crates/typst-library/src/layout/em.rs @@ -3,11 +3,11 @@ use std::iter::Sum; use std::ops::{Add, Div, Mul, Neg}; use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; use crate::foundations::{cast, repr, Repr, Resolve, StyleChain, Value}; use crate::layout::Abs; use crate::text::TextElem; -use crate::utils::{Numeric, Scalar}; /// A length that is relative to the font size. /// diff --git a/crates/typst/src/layout/fr.rs b/crates/typst-library/src/layout/fr.rs index a61daf1e..ff8a6e6b 100644 --- a/crates/typst/src/layout/fr.rs +++ b/crates/typst-library/src/layout/fr.rs @@ -3,10 +3,10 @@ use std::iter::Sum; use std::ops::{Add, Div, Mul, Neg}; use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; use crate::foundations::{repr, ty, Repr}; use crate::layout::Abs; -use crate::utils::{Numeric, Scalar}; /// Defines how the remaining space in a layout is distributed. /// diff --git a/crates/typst/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs index c2666115..c2666115 100644 --- a/crates/typst/src/layout/fragment.rs +++ b/crates/typst-library/src/layout/fragment.rs diff --git a/crates/typst/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs index cf4153d6..204584fc 100644 --- a/crates/typst/src/layout/frame.rs +++ b/crates/typst-library/src/layout/frame.rs @@ -5,20 +5,17 @@ use std::num::NonZeroUsize; use std::sync::Arc; use smallvec::SmallVec; +use typst_syntax::Span; +use typst_utils::{LazyHash, Numeric}; use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; use crate::introspection::{Location, Tag}; use crate::layout::{ - Abs, Axes, Corners, FixedAlignment, HideElem, Length, Point, Rel, Sides, Size, - Transform, + Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform, }; use crate::model::{Destination, LinkElem}; -use crate::syntax::Span; use crate::text::TextItem; -use crate::utils::{LazyHash, Numeric}; -use crate::visualize::{ - ellipse, styled_rect, Color, FixedStroke, Geometry, Image, Paint, Path, Shape, -}; +use crate::visualize::{Color, FixedStroke, Geometry, Image, Paint, Path, Shape}; /// A finished layout with items at fixed positions. #[derive(Default, Clone, Hash)] @@ -363,32 +360,13 @@ impl Frame { } /// Add a background fill. - pub fn fill(&mut self, fill: Paint) { + pub fn fill(&mut self, fill: impl Into<Paint>) { self.prepend( Point::zero(), FrameItem::Shape(Geometry::Rect(self.size()).filled(fill), Span::detached()), ); } - /// Add a fill and stroke with optional radius and outset to the frame. - pub fn fill_and_stroke( - &mut self, - fill: Option<Paint>, - stroke: &Sides<Option<FixedStroke>>, - outset: &Sides<Rel<Abs>>, - radius: &Corners<Rel<Abs>>, - span: Span, - ) { - let outset = outset.relative_to(self.size()); - let size = self.size() + outset.sum_by_axis(); - let pos = Point::new(-outset.left, -outset.top); - self.prepend_multiple( - styled_rect(size, radius, fill, stroke) - .into_iter() - .map(|x| (pos, FrameItem::Shape(x, span))), - ); - } - /// Arbitrarily transform the contents of the frame. pub fn transform(&mut self, transform: Transform) { if !self.is_empty() { @@ -448,7 +426,7 @@ impl Frame { 0, Point::zero(), FrameItem::Shape( - Geometry::Rect(self.size).filled(Color::TEAL.with_alpha(0.5).into()), + Geometry::Rect(self.size).filled(Color::TEAL.with_alpha(0.5)), Span::detached(), ), ); @@ -469,7 +447,8 @@ impl Frame { self.push( pos - Point::splat(radius), FrameItem::Shape( - ellipse(Size::splat(2.0 * radius), Some(Color::GREEN.into()), None), + Geometry::Path(Path::ellipse(Size::splat(2.0 * radius))) + .filled(Color::GREEN), Span::detached(), ), ); diff --git a/crates/typst/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid.rs index ec0504f3..2e1e9abc 100644 --- a/crates/typst/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid.rs @@ -1,36 +1,20 @@ -mod cells; -mod layout; -mod lines; -mod repeated; -mod rowspans; - -pub use self::cells::{ - Cell, CellGrid, Celled, ResolvableCell, ResolvableGridChild, ResolvableGridItem, -}; -pub use self::layout::GridLayouter; -pub use self::lines::LinePosition; - use std::num::NonZeroUsize; use std::sync::Arc; -use ecow::eco_format; +use comemo::Track; use smallvec::{smallvec, SmallVec}; +use typst_utils::NonZeroExt; -use crate::diag::{bail, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint}; +use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, Fold, NativeElement, Packed, Show, Smart, - StyleChain, Value, + cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, + IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value, }; -use crate::introspection::Locator; use crate::layout::{ - Abs, Alignment, Axes, BlockElem, Dir, Fragment, Length, OuterHAlignment, - OuterVAlignment, Regions, Rel, Sides, Sizing, + Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, }; use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine}; -use crate::syntax::Span; -use crate::text::TextElem; -use crate::utils::NonZeroExt; use crate::visualize::{Paint, Stroke}; /// Arranges content in a grid. @@ -241,67 +225,67 @@ pub struct GridElem { /// #set page(height: 13em, width: 26em) /// /// #let cv(..jobs) = grid( - /// columns: 2, - /// inset: 5pt, - /// stroke: (x, y) => if x == 0 and y > 0 { - /// (right: ( - /// paint: luma(180), - /// thickness: 1.5pt, - /// dash: "dotted" - /// )) - /// }, - /// grid.header(grid.cell(colspan: 2)[ - /// *Professional Experience* - /// #box(width: 1fr, line(length: 100%, stroke: luma(180))) - /// ]), - /// ..{ - /// let last = none - /// for job in jobs.pos() { - /// ( - /// if job.year != last [*#job.year*], - /// [ - /// *#job.company* - #job.role _(#job.timeframe)_ \ - /// #job.details - /// ] - /// ) - /// last = job.year - /// } + /// columns: 2, + /// inset: 5pt, + /// stroke: (x, y) => if x == 0 and y > 0 { + /// (right: ( + /// paint: luma(180), + /// thickness: 1.5pt, + /// dash: "dotted" + /// )) + /// }, + /// grid.header(grid.cell(colspan: 2)[ + /// *Professional Experience* + /// #box(width: 1fr, line(length: 100%, stroke: luma(180))) + /// ]), + /// ..{ + /// let last = none + /// for job in jobs.pos() { + /// ( + /// if job.year != last [*#job.year*], + /// [ + /// *#job.company* - #job.role _(#job.timeframe)_ \ + /// #job.details + /// ] + /// ) + /// last = job.year /// } - /// ) - /// - /// #cv( - /// ( - /// year: 2012, - /// company: [Pear Seed & Co.], - /// role: [Lead Engineer], - /// timeframe: [Jul - Dec], - /// details: [ - /// - Raised engineers from 3x to 10x - /// - Did a great job - /// ], - /// ), - /// ( - /// year: 2012, - /// company: [Mega Corp.], - /// role: [VP of Sales], - /// timeframe: [Mar - Jun], - /// details: [- Closed tons of customers], - /// ), - /// ( - /// year: 2013, - /// company: [Tiny Co.], - /// role: [CEO], - /// timeframe: [Jan - Dec], - /// details: [- Delivered 4x more shareholder value], - /// ), - /// ( - /// year: 2014, - /// company: [Glorbocorp Ltd], - /// role: [CTO], - /// timeframe: [Jan - Mar], - /// details: [- Drove containerization forward], - /// ), - /// ) + /// } + /// ) + /// + /// #cv( + /// ( + /// year: 2012, + /// company: [Pear Seed & Co.], + /// role: [Lead Engineer], + /// timeframe: [Jul - Dec], + /// details: [ + /// - Raised engineers from 3x to 10x + /// - Did a great job + /// ], + /// ), + /// ( + /// year: 2012, + /// company: [Mega Corp.], + /// role: [VP of Sales], + /// timeframe: [Mar - Jun], + /// details: [- Closed tons of customers], + /// ), + /// ( + /// year: 2013, + /// company: [Tiny Co.], + /// role: [CEO], + /// timeframe: [Jan - Dec], + /// details: [- Delivered 4x more shareholder value], + /// ), + /// ( + /// year: 2014, + /// company: [Glorbocorp Ltd], + /// role: [CTO], + /// timeframe: [Jan - Mar], + /// details: [- Drove containerization forward], + /// ), + /// ) /// ``` #[resolve] #[fold] @@ -342,70 +326,13 @@ impl GridElem { } impl Show for Packed<GridElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::multi_layouter(self.clone(), layout_grid) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid) .pack() .spanned(self.span())) } } -/// Layout the grid. -#[typst_macros::time(span = elem.span())] -fn layout_grid( - elem: &Packed<GridElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, -) -> SourceResult<Fragment> { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the grid when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); - let resolve_item = |item: &GridItem| item.to_resolvable(styles); - let children = elem.children().iter().map(|child| match child { - GridChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - GridChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - GridChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)), - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - - // Measure the columns and layout the grid row-by-row. - layouter.layout(engine) -} - /// Track sizing definitions. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); @@ -470,44 +397,6 @@ pub enum GridItem { Cell(Packed<GridCell>), } -impl GridItem { - fn to_resolvable(&self, styles: StyleChain) -> ResolvableGridItem<Packed<GridCell>> { - match self { - Self::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - Self::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => { - LinePosition::Before - } - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - Self::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } - } -} - cast! { GridItem, self => match self { @@ -856,113 +745,18 @@ cast! { v: Content => v.into(), } -impl Default for Packed<GridCell> { - fn default() -> Self { - Packed::new(GridCell::new(Content::default())) - } -} - -impl ResolvableCell for Packed<GridCell> { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option<Paint>, - align: Smart<Alignment>, - inset: Sides<Option<Rel<Length>>>, - stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the grid is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on grid cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart<usize> { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart<usize> { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } -} - impl Show for Packed<GridCell> { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) } } +impl Default for Packed<GridCell> { + fn default() -> Self { + Packed::new(GridCell::new(Content::default())) + } +} + impl From<Content> for GridCell { fn from(value: Content) -> Self { #[allow(clippy::unwrap_or_default)] @@ -991,3 +785,140 @@ pub(crate) fn show_grid_cell( Ok(body) } + +/// A value that can be configured per cell. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Celled<T> { + /// A bare value, the same for all cells. + Value(T), + /// A closure mapping from cell coordinates to a value. + Func(Func), + /// An array of alignment values corresponding to each column. + Array(Vec<T>), +} + +impl<T: Default + Clone + FromValue> Celled<T> { + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + x: usize, + y: usize, + ) -> SourceResult<T> { + Ok(match self { + Self::Value(value) => value.clone(), + Self::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), [x, y])? + .cast() + .at(func.span())?, + Self::Array(array) => x + .checked_rem(array.len()) + .and_then(|i| array.get(i)) + .cloned() + .unwrap_or_default(), + }) + } +} + +impl<T: Default> Default for Celled<T> { + fn default() -> Self { + Self::Value(T::default()) + } +} + +impl<T: Reflect> Reflect for Celled<T> { + fn input() -> CastInfo { + T::input() + Array::input() + Func::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + Func::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || Func::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Celled<T> { + fn into_value(self) -> Value { + match self { + Self::Value(value) => value.into_value(), + Self::Func(func) => func.into_value(), + Self::Array(arr) => arr.into_value(), + } + } +} + +impl<T: FromValue> FromValue for Celled<T> { + fn from_value(value: Value) -> HintedStrResult<Self> { + match value { + Value::Func(v) => Ok(Self::Func(v)), + Value::Array(array) => Ok(Self::Array( + array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?, + )), + v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), + v => Err(Self::error(&v)), + } + } +} + +impl<T: Fold> Fold for Celled<T> { + fn fold(self, outer: Self) -> Self { + match (self, outer) { + (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)), + (self_, _) => self_, + } + } +} + +impl<T: Resolve> Resolve for Celled<T> { + type Output = ResolvedCelled<T>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))), + Self::Func(func) => ResolvedCelled(Celled::Func(func)), + Self::Array(values) => ResolvedCelled(Celled::Array( + values.into_iter().map(|value| value.resolve(styles)).collect(), + )), + } + } +} + +/// The result of resolving a Celled's value according to styles. +/// Holds resolved values which depend on each grid cell's position. +/// When it is a closure, however, it is only resolved when the closure is +/// called. +#[derive(Default, Clone)] +pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>); + +impl<T> ResolvedCelled<T> +where + T: FromValue + Resolve, + <T as Resolve>::Output: Default + Clone, +{ + /// Resolve the value based on the cell position. + pub fn resolve( + &self, + engine: &mut Engine, + styles: StyleChain, + x: usize, + y: usize, + ) -> SourceResult<T::Output> { + Ok(match &self.0 { + Celled::Value(value) => value.clone(), + Celled::Func(func) => func + .call(engine, Context::new(None, Some(styles)).track(), [x, y])? + .cast::<T>() + .at(func.span())? + .resolve(styles), + Celled::Array(array) => x + .checked_rem(array.len()) + .and_then(|i| array.get(i)) + .cloned() + .unwrap_or_default(), + }) + } +} diff --git a/crates/typst/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index 1b8b9bd5..1b8b9bd5 100644 --- a/crates/typst/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs diff --git a/crates/typst/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index f69acca9..c3d112e1 100644 --- a/crates/typst/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -1,4 +1,5 @@ use comemo::Track; +use typst_syntax::Span; use crate::diag::SourceResult; use crate::engine::Engine; @@ -6,8 +7,7 @@ use crate::foundations::{ dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain, }; use crate::introspection::Locatable; -use crate::layout::{layout_fragment, BlockElem, Size}; -use crate::syntax::Span; +use crate::layout::{BlockElem, Size}; /// Provides access to the current outer container's (or page's, if none) /// dimensions (width and height). @@ -96,7 +96,9 @@ impl Show for Packed<LayoutElem> { [dict! { "width" => x, "height" => y }], )? .display(); - layout_fragment(engine, &result, locator, styles, regions) + (engine.routines.layout_fragment)( + engine, &result, locator, styles, regions, + ) }, ) .pack() diff --git a/crates/typst/src/layout/length.rs b/crates/typst-library/src/layout/length.rs index 5e352f13..800140c4 100644 --- a/crates/typst/src/layout/length.rs +++ b/crates/typst-library/src/layout/length.rs @@ -4,12 +4,12 @@ use std::ops::{Add, Div, Mul, Neg}; use comemo::Tracked; use ecow::{eco_format, EcoString}; +use typst_syntax::Span; +use typst_utils::Numeric; use crate::diag::{bail, HintedStrResult, SourceResult}; use crate::foundations::{func, scope, ty, Context, Fold, Repr, Resolve, StyleChain}; use crate::layout::{Abs, Em}; -use crate::syntax::Span; -use crate::utils::Numeric; /// A size or distance, possibly expressed with contextual units. /// diff --git a/crates/typst/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs index ad68b948..2fa51b2d 100644 --- a/crates/typst/src/layout/measure.rs +++ b/crates/typst-library/src/layout/measure.rs @@ -1,4 +1,5 @@ use comemo::Tracked; +use typst_syntax::Span; use crate::diag::{warning, At, SourceResult}; use crate::engine::Engine; @@ -6,8 +7,7 @@ use crate::foundations::{ dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles, }; use crate::introspection::{Locator, LocatorLink}; -use crate::layout::{layout_frame, Abs, Axes, Length, Region, Size}; -use crate::syntax::Span; +use crate::layout::{Abs, Axes, Length, Region, Size}; /// Measures the layouted size of content. /// @@ -109,7 +109,7 @@ pub fn measure( let link = LocatorLink::measure(here); let locator = Locator::link(&link); - let frame = layout_frame(engine, &content, locator, styles, pod)?; + let frame = (engine.routines.layout_frame)(engine, &content, locator, styles, pod)?; let Size { x, y } = frame.size(); Ok(dict! { "width" => x, "height" => y }) } diff --git a/crates/typst/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index f43a7138..b54d6906 100644 --- a/crates/typst/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -9,13 +9,11 @@ mod container; mod corners; mod dir; mod em; -mod flow; mod fr; mod fragment; mod frame; mod grid; mod hide; -mod inline; #[path = "layout.rs"] mod layout_; mod length; @@ -23,7 +21,6 @@ mod length; mod measure_; mod pad; mod page; -mod pages; mod place; mod point; mod ratio; @@ -45,7 +42,6 @@ pub use self::container::*; pub use self::corners::*; pub use self::dir::*; pub use self::em::*; -pub use self::flow::*; pub use self::fr::*; pub use self::fragment::*; pub use self::frame::*; @@ -56,7 +52,6 @@ pub use self::length::*; pub use self::measure_::*; pub use self::pad::*; pub use self::page::*; -pub use self::pages::*; pub use self::place::*; pub use self::point::*; pub use self::ratio::*; @@ -69,8 +64,6 @@ pub use self::spacing::*; pub use self::stack::*; pub use self::transform::*; -pub(crate) use self::inline::layout_inline; - use crate::foundations::{category, Category, Scope}; /// Arranging elements on the page in different ways. diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs new file mode 100644 index 00000000..1dc6d131 --- /dev/null +++ b/crates/typst-library/src/layout/pad.rs @@ -0,0 +1,65 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{BlockElem, Length, Rel}; + +/// Adds spacing around content. +/// +/// The spacing can be specified for each side individually, or for all sides at +/// once by specifying a positional argument. +/// +/// # Example +/// ```example +/// #set align(center) +/// +/// #pad(x: 16pt, image("typing.jpg")) +/// _Typing speeds can be +/// measured in words per minute._ +/// ``` +#[elem(title = "Padding", Show)] +pub struct PadElem { + /// The padding at the left side. + #[parse( + let all = args.named("rest")?.or(args.find()?); + let x = args.named("x")?.or(all); + let y = args.named("y")?.or(all); + args.named("left")?.or(x) + )] + pub left: Rel<Length>, + + /// The padding at the top side. + #[parse(args.named("top")?.or(y))] + pub top: Rel<Length>, + + /// The padding at the right side. + #[parse(args.named("right")?.or(x))] + pub right: Rel<Length>, + + /// The padding at the bottom side. + #[parse(args.named("bottom")?.or(y))] + pub bottom: Rel<Length>, + + /// A shorthand to set `left` and `right` to the same value. + #[external] + pub x: Rel<Length>, + + /// A shorthand to set `top` and `bottom` to the same value. + #[external] + pub y: Rel<Length>, + + /// A shorthand to set all four sides to the same value. + #[external] + pub rest: Rel<Length>, + + /// The content to pad at the sides. + #[required] + pub body: Content, +} + +impl Show for Packed<PadElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_pad) + .pack() + .spanned(self.span())) + } +} diff --git a/crates/typst/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 95b3cd1b..de278cb6 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -4,6 +4,7 @@ use std::ops::RangeInclusive; use std::str::FromStr; use comemo::Track; +use typst_utils::{singleton, NonZeroExt, Scalar}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; @@ -16,7 +17,6 @@ use crate::layout::{ Sides, SpecificAlignment, }; use crate::model::Numbering; -use crate::utils::{singleton, NonZeroExt, Scalar}; use crate::visualize::{Color, Paint}; /// Layouts its child onto one or multiple pages. diff --git a/crates/typst/src/layout/place.rs b/crates/typst-library/src/layout/place.rs index bedeb507..bedeb507 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst-library/src/layout/place.rs diff --git a/crates/typst/src/layout/point.rs b/crates/typst-library/src/layout/point.rs index 172b6021..c3d1594e 100644 --- a/crates/typst/src/layout/point.rs +++ b/crates/typst-library/src/layout/point.rs @@ -1,8 +1,9 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, Div, Mul, Neg}; +use typst_utils::{Get, Numeric}; + use crate::layout::{Abs, Axis, Size, Transform}; -use crate::utils::{Get, Numeric}; /// A point in 2D. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst/src/layout/ratio.rs b/crates/typst-library/src/layout/ratio.rs index 020d689a..1c0dcd29 100644 --- a/crates/typst/src/layout/ratio.rs +++ b/crates/typst-library/src/layout/ratio.rs @@ -2,9 +2,9 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, Div, Mul, Neg}; use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; use crate::foundations::{repr, ty, Repr}; -use crate::utils::{Numeric, Scalar}; /// A ratio of a whole. /// diff --git a/crates/typst/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs index 385664bb..385664bb 100644 --- a/crates/typst/src/layout/regions.rs +++ b/crates/typst-library/src/layout/regions.rs diff --git a/crates/typst/src/layout/rel.rs b/crates/typst-library/src/layout/rel.rs index cce0ac86..30441a42 100644 --- a/crates/typst/src/layout/rel.rs +++ b/crates/typst-library/src/layout/rel.rs @@ -3,10 +3,10 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use ecow::{eco_format, EcoString}; +use typst_utils::Numeric; use crate::foundations::{cast, ty, Fold, Repr, Resolve, StyleChain}; use crate::layout::{Abs, Em, Length, Ratio}; -use crate::utils::Numeric; /// A length in relation to some known length. /// diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs new file mode 100644 index 00000000..e423410a --- /dev/null +++ b/crates/typst-library/src/layout/repeat.rs @@ -0,0 +1,49 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{BlockElem, Length}; + +/// Repeats content to the available space. +/// +/// This can be useful when implementing a custom index, reference, or outline. +/// +/// Space may be inserted between the instances of the body parameter, so be +/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly. +/// +/// Errors if there no bounds on the available space, as it would create +/// infinite content. +/// +/// # Example +/// ```example +/// Sign on the dotted line: +/// #box(width: 1fr, repeat[.]) +/// +/// #set text(10pt) +/// #v(8pt, weak: true) +/// #align(right)[ +/// Berlin, the 22nd of December, 2022 +/// ] +/// ``` +#[elem(Show)] +pub struct RepeatElem { + /// The content to repeat. + #[required] + pub body: Content, + + /// The gap between each instance of the body. + #[default] + pub gap: Length, + + /// Whether to increase the gap between instances to completely fill the + /// available space. + #[default(true)] + pub justify: bool, +} + +impl Show for Packed<RepeatElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_repeat) + .pack() + .spanned(self.span())) + } +} diff --git a/crates/typst/src/layout/sides.rs b/crates/typst-library/src/layout/sides.rs index 61d6a1fd..e04b63d9 100644 --- a/crates/typst/src/layout/sides.rs +++ b/crates/typst-library/src/layout/sides.rs @@ -1,13 +1,14 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::Add; +use typst_utils::Get; + use crate::diag::{bail, HintedStrResult}; use crate::foundations::{ cast, AlternativeFold, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value, }; use crate::layout::{Abs, Alignment, Axes, Axis, Corner, Rel, Size}; -use crate::utils::Get; /// A container with left, top, right and bottom components. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst/src/layout/size.rs b/crates/typst-library/src/layout/size.rs index 8b5dc8fc..294eec66 100644 --- a/crates/typst/src/layout/size.rs +++ b/crates/typst-library/src/layout/size.rs @@ -1,7 +1,8 @@ use std::ops::{Add, Div, Mul, Neg}; +use typst_utils::Numeric; + use crate::layout::{Abs, Axes, Point, Ratio}; -use crate::utils::Numeric; /// A size in 2D. pub type Size = Axes<Abs>; diff --git a/crates/typst/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs index c7c0823c..b3ca1e81 100644 --- a/crates/typst/src/layout/spacing.rs +++ b/crates/typst-library/src/layout/spacing.rs @@ -1,6 +1,7 @@ +use typst_utils::Numeric; + use crate::foundations::{cast, elem, Content}; use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel}; -use crate::utils::Numeric; /// Inserts horizontal spacing into a paragraph. /// diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs new file mode 100644 index 00000000..5fc78480 --- /dev/null +++ b/crates/typst-library/src/layout/stack.rs @@ -0,0 +1,84 @@ +use std::fmt::{self, Debug, Formatter}; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{cast, elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::layout::{BlockElem, Dir, Spacing}; + +/// Arranges content and spacing horizontally or vertically. +/// +/// The stack places a list of items along an axis, with optional spacing +/// between each item. +/// +/// # Example +/// ```example +/// #stack( +/// dir: ttb, +/// rect(width: 40pt), +/// rect(width: 120pt), +/// rect(width: 90pt), +/// ) +/// ``` +#[elem(Show)] +pub struct StackElem { + /// The direction along which the items are stacked. Possible values are: + /// + /// - `{ltr}`: Left to right. + /// - `{rtl}`: Right to left. + /// - `{ttb}`: Top to bottom. + /// - `{btt}`: Bottom to top. + /// + /// You can use the `start` and `end` methods to obtain the initial and + /// final points (respectively) of a direction, as `alignment`. You can also + /// use the `axis` method to determine whether a direction is + /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a + /// direction's inverse direction. + /// + /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`, + /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`. + #[default(Dir::TTB)] + pub dir: Dir, + + /// Spacing to insert between items where no explicit spacing was provided. + pub spacing: Option<Spacing>, + + /// The children to stack along the axis. + #[variadic] + pub children: Vec<StackChild>, +} + +impl Show for Packed<StackElem> { + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_stack) + .pack() + .spanned(self.span())) + } +} + +/// A child of a stack element. +#[derive(Clone, PartialEq, Hash)] +pub enum StackChild { + /// Spacing between other children. + Spacing(Spacing), + /// Arbitrary block-level content. + Block(Content), +} + +impl Debug for StackChild { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Spacing(kind) => kind.fmt(f), + Self::Block(block) => block.fmt(f), + } + } +} + +cast! { + StackChild, + self => match self { + Self::Spacing(spacing) => spacing.into_value(), + Self::Block(content) => content.into_value(), + }, + v: Spacing => Self::Spacing(v), + v: Content => Self::Block(v), +} diff --git a/crates/typst/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs index 871bf5b7..183df609 100644 --- a/crates/typst/src/layout/transform.rs +++ b/crates/typst-library/src/layout/transform.rs @@ -1,18 +1,11 @@ -use std::ops::Div; - -use once_cell::unsync::Lazy; - -use crate::diag::{bail, SourceResult}; +use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain, + cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain, }; -use crate::introspection::Locator; use crate::layout::{ - layout_frame, Abs, Alignment, Angle, Axes, BlockElem, FixedAlignment, Frame, - HAlignment, Length, Point, Ratio, Region, Rel, Size, VAlignment, + Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment, }; -use crate::utils::Numeric; /// Moves content without affecting layout. /// @@ -46,29 +39,13 @@ pub struct MoveElem { } impl Show for Packed<MoveElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_move) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move) .pack() .spanned(self.span())) } } -/// Layout the moved content. -#[typst_macros::time(span = elem.span())] -fn layout_move( - elem: &Packed<MoveElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let mut frame = layout_frame(engine, &elem.body, locator, styles, region)?; - let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); - let delta = delta.zip_map(region.size, Rel::relative_to); - frame.translate(delta.to_point()); - Ok(frame) -} - /// Rotates content without affecting layout. /// /// Rotates an element by a given angle. The layout will act as if the element @@ -131,45 +108,13 @@ pub struct RotateElem { } impl Show for Packed<RotateElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_rotate) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate) .pack() .spanned(self.span())) } } -/// Layout the rotated content. -#[typst_macros::time(span = elem.span())] -fn layout_rotate( - elem: &Packed<RotateElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let angle = elem.angle(styles); - let align = elem.origin(styles).resolve(styles); - - // Compute the new region's approximate size. - let size = if region.size.is_finite() { - compute_bounding_box(region.size, Transform::rotate(-angle)).1 - } else { - Size::splat(Abs::inf()) - }; - - measure_and_layout( - engine, - locator, - region, - size, - styles, - elem.body(), - Transform::rotate(angle), - align, - elem.reflow(styles), - ) -} - /// Scales content without affecting layout. /// /// Lets you mirror content by specifying a negative scale on a single axis. @@ -235,104 +180,20 @@ pub struct ScaleElem { } impl Show for Packed<ScaleElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_scale) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale) .pack() .spanned(self.span())) } } -/// Layout the scaled content. -#[typst_macros::time(span = elem.span())] -fn layout_scale( - elem: &Packed<ScaleElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - // Compute the new region's approximate size. - let scale = elem.resolve_scale(engine, locator.relayout(), region.size, styles)?; - let size = region - .size - .zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r }) - .map(Abs::abs); - - measure_and_layout( - engine, - locator, - region, - size, - styles, - elem.body(), - Transform::scale(scale.x, scale.y), - elem.origin(styles).resolve(styles), - elem.reflow(styles), - ) -} - +/// To what size something shall be scaled. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum ScaleAmount { +pub enum ScaleAmount { Ratio(Ratio), Length(Length), } -impl Packed<ScaleElem> { - /// Resolves scale parameters, preserving aspect ratio if one of the scales is set to `auto`. - fn resolve_scale( - &self, - engine: &mut Engine, - locator: Locator, - container: Size, - styles: StyleChain, - ) -> SourceResult<Axes<Ratio>> { - fn resolve_axis( - axis: Smart<ScaleAmount>, - body: impl Fn() -> SourceResult<Abs>, - styles: StyleChain, - ) -> SourceResult<Smart<Ratio>> { - Ok(match axis { - Smart::Auto => Smart::Auto, - Smart::Custom(amt) => Smart::Custom(match amt { - ScaleAmount::Ratio(ratio) => ratio, - ScaleAmount::Length(length) => { - let length = length.resolve(styles); - Ratio::new(length.div(body()?)) - } - }), - }) - } - - let size = Lazy::new(|| { - let pod = Region::new(container, Axes::splat(false)); - let frame = layout_frame(engine, &self.body, locator, styles, pod)?; - SourceResult::Ok(frame.size()) - }); - - let x = resolve_axis( - self.x(styles), - || size.as_ref().map(|size| size.x).map_err(Clone::clone), - styles, - )?; - - let y = resolve_axis( - self.y(styles), - || size.as_ref().map(|size| size.y).map_err(Clone::clone), - styles, - )?; - - match (x, y) { - (Smart::Auto, Smart::Auto) => { - bail!(self.span(), "x and y cannot both be auto") - } - (Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)), - (Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => { - Ok(Axes::splat(v)) - } - } - } -} - cast! { ScaleAmount, self => match self { @@ -405,46 +266,13 @@ pub struct SkewElem { } impl Show for Packed<SkewElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_skew) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew) .pack() .spanned(self.span())) } } -/// Layout the skewed content. -#[typst_macros::time(span = elem.span())] -fn layout_skew( - elem: &Packed<SkewElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let ax = elem.ax(styles); - let ay = elem.ay(styles); - let align = elem.origin(styles).resolve(styles); - - // Compute the new region's approximate size. - let size = if region.size.is_finite() { - compute_bounding_box(region.size, Transform::skew(ax, ay)).1 - } else { - Size::splat(Abs::inf()) - }; - - measure_and_layout( - engine, - locator, - region, - size, - styles, - elem.body(), - Transform::skew(ax, ay), - align, - elem.reflow(styles), - ) -} - /// A scale-skew-translate transformation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Transform { @@ -578,73 +406,3 @@ impl Default for Transform { Self::identity() } } - -/// Applies a transformation to a frame, reflowing the layout if necessary. -#[allow(clippy::too_many_arguments)] -fn measure_and_layout( - engine: &mut Engine, - locator: Locator, - region: Region, - size: Size, - styles: StyleChain, - body: &Content, - transform: Transform, - align: Axes<FixedAlignment>, - reflow: bool, -) -> SourceResult<Frame> { - if reflow { - // Measure the size of the body. - let pod = Region::new(size, Axes::splat(false)); - let frame = layout_frame(engine, body, locator.relayout(), styles, pod)?; - - // Actually perform the layout. - let pod = Region::new(frame.size(), Axes::splat(true)); - let mut frame = layout_frame(engine, body, locator, styles, pod)?; - let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); - - // Compute the transform. - let ts = Transform::translate(x, y) - .pre_concat(transform) - .pre_concat(Transform::translate(-x, -y)); - - // Compute the bounding box and offset and wrap in a new frame. - let (offset, size) = compute_bounding_box(frame.size(), ts); - frame.transform(ts); - frame.translate(offset); - frame.set_size(size); - Ok(frame) - } else { - // Layout the body. - let mut frame = layout_frame(engine, body, locator, styles, region)?; - let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position); - - // Compute the transform. - let ts = Transform::translate(x, y) - .pre_concat(transform) - .pre_concat(Transform::translate(-x, -y)); - - // Apply the transform. - frame.transform(ts); - Ok(frame) - } -} - -/// Computes the bounding box and offset of a transformed area. -fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) { - let top_left = Point::zero().transform_inf(ts); - let top_right = Point::with_x(size.x).transform_inf(ts); - let bottom_left = Point::with_y(size.y).transform_inf(ts); - let bottom_right = size.to_point().transform_inf(ts); - - // We first compute the new bounding box of the rotated area. - let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x); - let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y); - let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x); - let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y); - - // Then we compute the new size of the area. - let width = max_x - min_x; - let height = max_y - min_y; - - (Point::new(-min_x, -min_y), Size::new(width.abs(), height.abs())) -} diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs new file mode 100644 index 00000000..34c2e526 --- /dev/null +++ b/crates/typst-library/src/lib.rs @@ -0,0 +1,264 @@ +//! Typst's standard library. +//! +//! This crate also contains all of the compiler's central type definitions as +//! these are interwoven with the standard library types. +//! +//! In contrast to the _types,_ most of the compilation _behaviour_ is split out +//! into separate crates (`typst-eval`, `typst-realize`, `typst-layout`, etc.) +//! +//! Note that, unless you are working on the compiler itself, you will rarely +//! need to interact with this crate, as it is fully reexported by the `typst` +//! crate. + +extern crate self as typst_library; + +pub mod diag; +pub mod engine; +pub mod foundations; +pub mod introspection; +pub mod layout; +pub mod loading; +pub mod math; +pub mod model; +pub mod routines; +pub mod symbols; +pub mod text; +pub mod visualize; + +use std::ops::{Deref, Range}; + +use ecow::EcoString; +use typst_syntax::package::PackageSpec; +use typst_syntax::{FileId, Source, Span}; +use typst_utils::LazyHash; + +use crate::diag::FileResult; +use crate::foundations::{Array, Bytes, Datetime, Dict, Module, Scope, Styles, Value}; +use crate::layout::{Alignment, Dir}; +use crate::text::{Font, FontBook}; +use crate::visualize::Color; + +/// The environment in which typesetting occurs. +/// +/// All loading functions (`main`, `source`, `file`, `font`) should perform +/// internal caching so that they are relatively cheap on repeated invocations +/// with the same argument. [`Source`], [`Bytes`], and [`Font`] are +/// all reference-counted and thus cheap to clone. +/// +/// The compiler doesn't do the caching itself because the world has much more +/// information on when something can change. For example, fonts typically don't +/// change and can thus even be cached across multiple compilations (for +/// long-running applications like `typst watch`). Source files on the other +/// hand can change and should thus be cleared after each compilation. Advanced +/// clients like language servers can also retain the source files and +/// [edit](Source::edit) them in-place to benefit from better incremental +/// performance. +#[comemo::track] +pub trait World: Send + Sync { + /// The standard library. + /// + /// Can be created through `Library::build()`. + fn library(&self) -> &LazyHash<Library>; + + /// Metadata about all known fonts. + fn book(&self) -> &LazyHash<FontBook>; + + /// Get the file id of the main source file. + fn main(&self) -> FileId; + + /// Try to access the specified source file. + fn source(&self, id: FileId) -> FileResult<Source>; + + /// Try to access the specified file. + fn file(&self, id: FileId) -> FileResult<Bytes>; + + /// Try to access the font with the given index in the font book. + fn font(&self, index: usize) -> Option<Font>; + + /// Get the current date. + /// + /// If no offset is specified, the local date should be chosen. Otherwise, + /// the UTC date should be chosen with the corresponding offset in hours. + /// + /// If this function returns `None`, Typst's `datetime` function will + /// return an error. + fn today(&self, offset: Option<i64>) -> Option<Datetime>; + + /// A list of all available packages and optionally descriptions for them. + /// + /// This function is optional to implement. It enhances the user experience + /// by enabling autocompletion for packages. Details about packages from the + /// `@preview` namespace are available from + /// `https://packages.typst.org/preview/index.json`. + fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { + &[] + } +} + +macro_rules! world_impl { + ($W:ident for $ptr:ty) => { + impl<$W: World> World for $ptr { + fn library(&self) -> &LazyHash<Library> { + self.deref().library() + } + + fn book(&self) -> &LazyHash<FontBook> { + self.deref().book() + } + + fn main(&self) -> FileId { + self.deref().main() + } + + fn source(&self, id: FileId) -> FileResult<Source> { + self.deref().source(id) + } + + fn file(&self, id: FileId) -> FileResult<Bytes> { + self.deref().file(id) + } + + fn font(&self, index: usize) -> Option<Font> { + self.deref().font(index) + } + + fn today(&self, offset: Option<i64>) -> Option<Datetime> { + self.deref().today(offset) + } + + fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { + self.deref().packages() + } + } + }; +} + +world_impl!(W for std::boxed::Box<W>); +world_impl!(W for std::sync::Arc<W>); +world_impl!(W for &W); + +/// Helper methods on [`World`] implementations. +pub trait WorldExt { + /// Get the byte range for a span. + /// + /// Returns `None` if the `Span` does not point into any source file. + fn range(&self, span: Span) -> Option<Range<usize>>; +} + +impl<T: World> WorldExt for T { + fn range(&self, span: Span) -> Option<Range<usize>> { + self.source(span.id()?).ok()?.range(span) + } +} + +/// Definition of Typst's standard library. +#[derive(Debug, Clone, Hash)] +pub struct Library { + /// The module that contains the definitions that are available everywhere. + pub global: Module, + /// The module that contains the definitions available in math mode. + pub math: Module, + /// The default style properties (for page size, font selection, and + /// everything else configurable via set and show rules). + pub styles: Styles, + /// The standard library as a value. + /// Used to provide the `std` variable. + pub std: Value, +} + +impl Library { + /// Create a new builder for a library. + pub fn builder() -> LibraryBuilder { + LibraryBuilder::default() + } +} + +impl Default for Library { + /// Constructs the standard library with the default configuration. + fn default() -> Self { + Self::builder().build() + } +} + +/// Configurable builder for the standard library. +/// +/// This struct is created by [`Library::builder`]. +#[derive(Debug, Clone, Default)] +pub struct LibraryBuilder { + inputs: Option<Dict>, +} + +impl LibraryBuilder { + /// Configure the inputs visible through `sys.inputs`. + pub fn with_inputs(mut self, inputs: Dict) -> Self { + self.inputs = Some(inputs); + self + } + + /// Consumes the builder and returns a `Library`. + pub fn build(self) -> Library { + let math = math::module(); + let inputs = self.inputs.unwrap_or_default(); + let global = global(math.clone(), inputs); + let std = Value::Module(global.clone()); + Library { global, math, styles: Styles::new(), std } + } +} + +/// Construct the module with global definitions. +fn global(math: Module, inputs: Dict) -> Module { + let mut global = Scope::deduplicating(); + self::foundations::define(&mut global, inputs); + self::model::define(&mut global); + self::text::define(&mut global); + global.reset_category(); + global.define_module(math); + self::layout::define(&mut global); + self::visualize::define(&mut global); + self::introspection::define(&mut global); + self::loading::define(&mut global); + self::symbols::define(&mut global); + prelude(&mut global); + Module::new("global", global) +} + +/// Defines scoped values that are globally available, too. +fn prelude(global: &mut Scope) { + global.reset_category(); + global.define("black", Color::BLACK); + global.define("gray", Color::GRAY); + global.define("silver", Color::SILVER); + global.define("white", Color::WHITE); + global.define("navy", Color::NAVY); + global.define("blue", Color::BLUE); + global.define("aqua", Color::AQUA); + global.define("teal", Color::TEAL); + global.define("eastern", Color::EASTERN); + global.define("purple", Color::PURPLE); + global.define("fuchsia", Color::FUCHSIA); + global.define("maroon", Color::MAROON); + global.define("red", Color::RED); + global.define("orange", Color::ORANGE); + global.define("yellow", Color::YELLOW); + global.define("olive", Color::OLIVE); + global.define("green", Color::GREEN); + global.define("lime", Color::LIME); + global.define("luma", Color::luma_data()); + global.define("oklab", Color::oklab_data()); + global.define("oklch", Color::oklch_data()); + global.define("rgb", Color::rgb_data()); + global.define("cmyk", Color::cmyk_data()); + global.define("range", Array::range_data()); + global.define("ltr", Dir::LTR); + global.define("rtl", Dir::RTL); + global.define("ttb", Dir::TTB); + global.define("btt", Dir::BTT); + global.define("start", Alignment::START); + global.define("left", Alignment::LEFT); + global.define("center", Alignment::CENTER); + global.define("right", Alignment::RIGHT); + global.define("end", Alignment::END); + global.define("top", Alignment::TOP); + global.define("horizon", Alignment::HORIZON); + global.define("bottom", Alignment::BOTTOM); +} diff --git a/crates/typst/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index e2d0f7c7..977059c3 100644 --- a/crates/typst/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -1,9 +1,9 @@ use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Bytes, Value}; -use crate::syntax::Spanned; use crate::World; /// Reads structured data from a CBOR file. diff --git a/crates/typst/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 62a9bf69..6822505d 100644 --- a/crates/typst/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -1,10 +1,10 @@ use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; use crate::loading::Readable; -use crate::syntax::Spanned; use crate::World; /// Reads structured data from a CSV file. diff --git a/crates/typst/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index d466d043..597cf4cc 100644 --- a/crates/typst/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -1,10 +1,10 @@ use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::Readable; -use crate::syntax::Spanned; use crate::World; /// Reads structured data from a JSON file. diff --git a/crates/typst/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index a02fb9a9..ae74df86 100644 --- a/crates/typst/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -54,14 +54,14 @@ pub enum Readable { } impl Readable { - fn as_slice(&self) -> &[u8] { + pub fn as_slice(&self) -> &[u8] { match self { Readable::Bytes(v) => v, Readable::Str(v) => v.as_bytes(), } } - pub(crate) fn as_str(&self) -> Option<&str> { + pub fn as_str(&self) -> Option<&str> { match self { Readable::Str(v) => Some(v.as_str()), Readable::Bytes(v) => std::str::from_utf8(v).ok(), diff --git a/crates/typst/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index 3bec9bd7..23e6e27e 100644 --- a/crates/typst/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -1,10 +1,10 @@ use ecow::EcoString; +use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, Cast}; use crate::loading::Readable; -use crate::syntax::Spanned; use crate::World; /// Reads plain text or data from a file. diff --git a/crates/typst/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 17ab6b53..5167703e 100644 --- a/crates/typst/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -1,10 +1,10 @@ use ecow::{eco_format, EcoString}; +use typst_syntax::{is_newline, Spanned}; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::Readable; -use crate::syntax::{is_newline, Spanned}; use crate::World; /// Reads structured data from a TOML file. diff --git a/crates/typst/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index fefb63c2..3b1a9674 100644 --- a/crates/typst/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -1,11 +1,11 @@ use ecow::EcoString; use roxmltree::ParsingOptions; +use typst_syntax::Spanned; use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; use crate::loading::Readable; -use crate::syntax::Spanned; use crate::World; /// Reads structured data from an XML file. diff --git a/crates/typst/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 22311cf6..0e8ca3fb 100644 --- a/crates/typst/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -1,10 +1,10 @@ use ecow::{eco_format, EcoString}; +use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::Readable; -use crate::syntax::Spanned; use crate::World; /// Reads structured data from a YAML file. diff --git a/crates/typst/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index 08ed7238..a1c70b4a 100644 --- a/crates/typst/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -1,64 +1,9 @@ -use crate::diag::{bail, SourceResult}; -use crate::foundations::{ - cast, elem, func, Content, NativeElement, Packed, Smart, StyleChain, Value, -}; -use crate::layout::{Em, Frame, Length, Point, Rel, Size}; -use crate::math::{ - scaled_font_size, style_cramped, FrameFragment, GlyphFragment, LayoutMath, - MathContext, MathFragment, Scaled, -}; +use crate::diag::bail; +use crate::foundations::{cast, elem, func, Content, NativeElement, Smart, Value}; +use crate::layout::{Length, Rel}; +use crate::math::Mathy; use crate::text::TextElem; -/// How much the accent can be shorter than the base. -const ACCENT_SHORT_FALL: Em = Em::new(0.5); - -/// This macro generates accent-related functions. -/// -/// ```ignore -/// accents! { -/// '\u{0300}' | '`' => grave, -/// // ^^^^^^^^^ ^^^ ^^^^^ -/// // | | | -/// // | | +-- The name of the function. -/// // | +--------- The alternative characters that represent the accent. -/// // +---------------------- The primary character that represents the accent. -/// } -/// ``` -/// -/// When combined with the `Accent::combine` function, accent characters can be normalized -/// to the primary character. -macro_rules! accents { - ($($primary:literal $(| $alt:literal)* => $name:ident),* $(,)?) => { - impl Accent { - /// Normalize an accent to a combining one. - pub fn combine(c: char) -> Option<char> { - Some(match c { - $($primary $(| $alt)* => $primary,)* - _ => return None, - }) - } - } - - $( - /// The accent function for callable symbol definitions. - #[func] - pub fn $name( - /// The base to which the accent is applied. - base: Content, - /// The size of the accent, relative to the width of the base. - #[named] - size: Option<Smart<Rel<Length>>>, - ) -> Content { - let mut accent = AccentElem::new(base, Accent::new($primary)); - if let Some(size) = size { - accent = accent.with_size(size); - } - accent.pack() - } - )+ - }; -} - /// Attaches an accent to a base. /// /// # Example @@ -67,7 +12,7 @@ macro_rules! accents { /// $arrow(a) = accent(a, arrow)$ \ /// $tilde(a) = accent(a, \u{0303})$ /// ``` -#[elem(LayoutMath)] +#[elem(Mathy)] pub struct AccentElem { /// The base to which the accent is applied. /// May consist of multiple letters. @@ -110,69 +55,9 @@ pub struct AccentElem { pub size: Smart<Rel<Length>>, } -impl LayoutMath for Packed<AccentElem> { - #[typst_macros::time(name = "math.accent", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let cramped = style_cramped(); - let base = ctx.layout_into_fragment(self.base(), styles.chain(&cramped))?; - - // Preserve class to preserve automatic spacing. - let base_class = base.class(); - let base_attach = base.accent_attach(); - - let width = self - .size(styles) - .unwrap_or(Rel::one()) - .at(scaled_font_size(ctx, styles)) - .relative_to(base.width()); - - // Forcing the accent to be at least as large as the base makes it too - // wide in many case. - let Accent(c) = self.accent(); - let glyph = GlyphFragment::new(ctx, styles, *c, self.span()); - let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); - let variant = glyph.stretch_horizontal(ctx, width, short_fall); - let accent = variant.frame; - let accent_attach = variant.accent_attach; - - // Descent is negative because the accent's ink bottom is above the - // baseline. Therefore, the default gap is the accent's negated descent - // minus the accent base height. Only if the base is very small, we need - // a larger gap so that the accent doesn't move too low. - let accent_base_height = scaled!(ctx, styles, accent_base_height); - let gap = -accent.descent() - base.height().min(accent_base_height); - let size = Size::new(base.width(), accent.height() + gap + base.height()); - let accent_pos = Point::with_x(base_attach - accent_attach); - let base_pos = Point::with_y(accent.height() + gap); - let baseline = base_pos.y + base.ascent(); - let base_italics_correction = base.italics_correction(); - let base_text_like = base.is_text_like(); - - let base_ascent = match &base { - MathFragment::Frame(frame) => frame.base_ascent, - _ => base.ascent(), - }; - - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(accent_pos, accent); - frame.push_frame(base_pos, base.into_frame()); - ctx.push( - FrameFragment::new(ctx, styles, frame) - .with_class(base_class) - .with_base_ascent(base_ascent) - .with_italics_correction(base_italics_correction) - .with_accent_attach(base_attach) - .with_text_like(base_text_like), - ); - - Ok(()) - } -} - /// An accent character. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Accent(char); +pub struct Accent(pub char); impl Accent { /// Normalize a character into an accent. @@ -181,6 +66,53 @@ impl Accent { } } +/// This macro generates accent-related functions. +/// +/// ```ignore +/// accents! { +/// '\u{0300}' | '`' => grave, +/// // ^^^^^^^^^ ^^^ ^^^^^ +/// // | | | +/// // | | +-- The name of the function. +/// // | +--------- The alternative characters that represent the accent. +/// // +---------------------- The primary character that represents the accent. +/// } +/// ``` +/// +/// When combined with the `Accent::combine` function, accent characters can be normalized +/// to the primary character. +macro_rules! accents { + ($($primary:literal $(| $alt:literal)* => $name:ident),* $(,)?) => { + impl Accent { + /// Normalize an accent to a combining one. + pub fn combine(c: char) -> Option<char> { + Some(match c { + $($primary $(| $alt)* => $primary,)* + _ => return None, + }) + } + } + + $( + /// The accent function for callable symbol definitions. + #[func] + pub fn $name( + /// The base to which the accent is applied. + base: Content, + /// The size of the accent, relative to the width of the base. + #[named] + size: Option<Smart<Rel<Length>>>, + ) -> Content { + let mut accent = AccentElem::new(base, Accent::new($primary)); + if let Some(size) = size { + accent = accent.with_size(size); + } + accent.pack() + } + )+ + }; +} + // Keep it synced with the documenting table above. accents! { '\u{0300}' | '`' => grave, diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs new file mode 100644 index 00000000..74eabd10 --- /dev/null +++ b/crates/typst-library/src/math/attach.rs @@ -0,0 +1,156 @@ +use crate::foundations::{elem, Content, Packed, Smart}; +use crate::layout::{Length, Rel}; +use crate::math::{EquationElem, Mathy}; + +/// A base with optional attachments. +/// +/// ```example +/// $ attach( +/// Pi, t: alpha, b: beta, +/// tl: 1, tr: 2+3, bl: 4+5, br: 6, +/// ) $ +/// ``` +#[elem(Mathy)] +pub struct AttachElem { + /// The base to which things are attached. + #[required] + pub base: Content, + + /// The top attachment, smartly positioned at top-right or above the base. + /// + /// You can wrap the base in `{limits()}` or `{scripts()}` to override the + /// smart positioning. + pub t: Option<Content>, + + /// The bottom attachment, smartly positioned at the bottom-right or below + /// the base. + /// + /// You can wrap the base in `{limits()}` or `{scripts()}` to override the + /// smart positioning. + pub b: Option<Content>, + + /// The top-left attachment (before the base). + pub tl: Option<Content>, + + /// The bottom-left attachment (before base). + pub bl: Option<Content>, + + /// The top-right attachment (after the base). + pub tr: Option<Content>, + + /// The bottom-right attachment (after the base). + pub br: Option<Content>, +} + +impl Packed<AttachElem> { + /// If an AttachElem's base is also an AttachElem, merge attachments into the + /// base AttachElem where possible. + pub fn merge_base(&self) -> Option<Self> { + // Extract from an EquationElem. + let mut base = self.base(); + if let Some(equation) = base.to_packed::<EquationElem>() { + base = equation.body(); + } + + // Move attachments from elem into base where possible. + if let Some(base) = base.to_packed::<AttachElem>() { + let mut elem = self.clone(); + let mut base = base.clone(); + + macro_rules! merge { + ($content:ident) => { + if base.$content.is_none() && elem.$content.is_some() { + base.$content = elem.$content.clone(); + elem.$content = None; + } + }; + } + + merge!(t); + merge!(b); + merge!(tl); + merge!(tr); + merge!(bl); + merge!(br); + + elem.base = base.pack(); + return Some(elem); + } + + None + } +} + +/// Grouped primes. +/// +/// ```example +/// $ a'''_b = a^'''_b $ +/// ``` +/// +/// # Syntax +/// This function has dedicated syntax: use apostrophes instead of primes. They +/// will automatically attach to the previous element, moving superscripts to +/// the next level. +#[elem(Mathy)] +pub struct PrimesElem { + /// The number of grouped primes. + #[required] + pub count: usize, +} + +/// Forces a base to display attachments as scripts. +/// +/// ```example +/// $ scripts(sum)_1^2 != sum_1^2 $ +/// ``` +#[elem(Mathy)] +pub struct ScriptsElem { + /// The base to attach the scripts to. + #[required] + pub body: Content, +} + +/// Forces a base to display attachments as limits. +/// +/// ```example +/// $ limits(A)_1^2 != A_1^2 $ +/// ``` +#[elem(Mathy)] +pub struct LimitsElem { + /// The base to attach the limits to. + #[required] + pub body: Content, + + /// Whether to also force limits in inline equations. + /// + /// When applying limits globally (e.g., through a show rule), it is + /// typically a good idea to disable this. + #[default(true)] + pub inline: bool, +} + +/// Stretches a glyph. +/// +/// This function can also be used to automatically stretch the base of an +/// attachment, so that it fits the top and bottom attachments. +/// +/// Note that only some glyphs can be stretched, and which ones can depend on +/// the math font being used. However, most math fonts are the same in this +/// regard. +/// +/// ```example +/// $ H stretch(=)^"define" U + p V $ +/// $ f : X stretch(->>, size: #150%)_"surjective" Y $ +/// $ x stretch(harpoons.ltrb, size: #3em) y +/// stretch(\[, size: #150%) z $ +/// ``` +#[elem(Mathy)] +pub struct StretchElem { + /// The glyph to stretch. + #[required] + pub body: Content, + + /// The size to stretch to, relative to the maximum size of the glyph and + /// its attachments. + pub size: Smart<Rel<Length>>, +} diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs new file mode 100644 index 00000000..a72505c0 --- /dev/null +++ b/crates/typst-library/src/math/cancel.rs @@ -0,0 +1,116 @@ +use crate::foundations::{cast, elem, Content, Func, Smart}; +use crate::layout::{Abs, Angle, Length, Ratio, Rel}; +use crate::math::Mathy; +use crate::visualize::Stroke; + +/// Displays a diagonal line over a part of an equation. +/// +/// This is commonly used to show the elimination of a term. +/// +/// # Example +/// ```example +/// >>> #set page(width: 140pt) +/// Here, we can simplify: +/// $ (a dot b dot cancel(x)) / +/// cancel(x) $ +/// ``` +#[elem(Mathy)] +pub struct CancelElem { + /// The content over which the line should be placed. + #[required] + pub body: Content, + + /// The length of the line, relative to the length of the diagonal spanning + /// the whole element being "cancelled". A value of `{100%}` would then have + /// the line span precisely the element's diagonal. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ a + cancel(x, length: #200%) + /// - cancel(x, length: #200%) $ + /// ``` + #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] + pub length: Rel<Length>, + + /// Whether the cancel line should be inverted (flipped along the y-axis). + /// For the default angle setting, inverted means the cancel line + /// points to the top left instead of top right. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ (a cancel((b + c), inverted: #true)) / + /// cancel(b + c, inverted: #true) $ + /// ``` + #[default(false)] + pub inverted: bool, + + /// Whether two opposing cancel lines should be drawn, forming a cross over + /// the element. Overrides `inverted`. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi, cross: #true) $ + /// ``` + #[default(false)] + pub cross: bool, + + /// How much to rotate the cancel line. + /// + /// - If given an angle, the line is rotated by that angle clockwise with + /// respect to the y-axis. + /// - If `{auto}`, the line assumes the default angle; that is, along the + /// rising diagonal of the content box. + /// - If given a function `angle => angle`, the line is rotated, with + /// respect to the y-axis, by the angle returned by that function. The + /// function receives the default angle as its input. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel(Pi) + /// cancel(Pi, angle: #0deg) + /// cancel(Pi, angle: #45deg) + /// cancel(Pi, angle: #90deg) + /// cancel(1/(1+x), angle: #(a => a + 45deg)) + /// cancel(1/(1+x), angle: #(a => a + 90deg)) $ + /// ``` + pub angle: Smart<CancelAngle>, + + /// How to [stroke]($stroke) the cancel line. + /// + /// ```example + /// >>> #set page(width: 140pt) + /// $ cancel( + /// sum x, + /// stroke: #( + /// paint: red, + /// thickness: 1.5pt, + /// dash: "dashed", + /// ), + /// ) $ + /// ``` + #[resolve] + #[fold] + #[default(Stroke { + // Default stroke has 0.5pt for better visuals. + thickness: Smart::Custom(Abs::pt(0.5).into()), + ..Default::default() + })] + pub stroke: Stroke, +} + +/// Defines the cancel line. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CancelAngle { + Angle(Angle), + Func(Func), +} + +cast! { + CancelAngle, + self => match self { + Self::Angle(v) => v.into_value(), + Self::Func(v) => v.into_value() + }, + v: Angle => CancelAngle::Angle(v), + v: Func => CancelAngle::Func(v), +} diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs new file mode 100644 index 00000000..4edafe5e --- /dev/null +++ b/crates/typst-library/src/math/equation.rs @@ -0,0 +1,256 @@ +use std::num::NonZeroUsize; + +use typst_utils::NonZeroExt; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, + Synthesize, +}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::layout::{ + AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment, + VAlignment, +}; +use crate::math::{MathSize, MathVariant}; +use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; +use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; + +/// A mathematical equation. +/// +/// Can be displayed inline with text or as a separate block. +/// +/// # Example +/// ```example +/// #set text(font: "New Computer Modern") +/// +/// Let $a$, $b$, and $c$ be the side +/// lengths of right-angled triangle. +/// Then, we know that: +/// $ a^2 + b^2 = c^2 $ +/// +/// Prove by induction: +/// $ sum_(k=1)^n k = (n(n+1)) / 2 $ +/// ``` +/// +/// By default, block-level equations will not break across pages. This can be +/// changed through `{show math.equation: set block(breakable: true)}`. +/// +/// # Syntax +/// This function also has dedicated syntax: Write mathematical markup within +/// dollar signs to create an equation. Starting and ending the equation with at +/// least one space lifts it into a separate block that is centered +/// horizontally. For more details about math syntax, see the +/// [main math page]($category/math). +#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] +pub struct EquationElem { + /// Whether the equation is displayed as a separate block. + #[default(false)] + pub block: bool, + + /// How to [number]($numbering) block-level equations. + /// + /// ```example + /// #set math.equation(numbering: "(1)") + /// + /// We define: + /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> + /// + /// With @ratio, we get: + /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ + /// ``` + #[borrowed] + pub numbering: Option<Numbering>, + + /// The alignment of the equation numbering. + /// + /// By default, the alignment is `{end + horizon}`. For the horizontal + /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}` + /// of the text direction; for the vertical component, you can use + /// `{top}`, `{horizon}`, or `{bottom}`. + /// + /// ```example + /// #set math.equation(numbering: "(1)", number-align: bottom) + /// + /// We can calculate: + /// $ E &= sqrt(m_0^2 + p^2) \ + /// &approx 125 "GeV" $ + /// ``` + #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))] + pub number_align: SpecificAlignment<OuterHAlignment, VAlignment>, + + /// A supplement for the equation. + /// + /// For references to equations, this is added before the referenced number. + /// + /// If a function is specified, it is passed the referenced equation and + /// should return content. + /// + /// ```example + /// #set math.equation(numbering: "(1)", supplement: [Eq.]) + /// + /// We define: + /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> + /// + /// With @ratio, we get: + /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ + /// ``` + pub supplement: Smart<Option<Supplement>>, + + /// The contents of the equation. + #[required] + pub body: Content, + + /// The size of the glyphs. + #[internal] + #[default(MathSize::Text)] + #[ghost] + pub size: MathSize, + + /// The style variant to select. + #[internal] + #[ghost] + pub variant: MathVariant, + + /// Affects the height of exponents. + #[internal] + #[default(false)] + #[ghost] + pub cramped: bool, + + /// Whether to use bold glyphs. + #[internal] + #[default(false)] + #[ghost] + pub bold: bool, + + /// Whether to use italic glyphs. + #[internal] + #[ghost] + pub italic: Smart<bool>, + + /// A forced class to use for all fragment. + #[internal] + #[ghost] + pub class: Option<MathClass>, +} + +impl Synthesize for Packed<EquationElem> { + fn synthesize( + &mut self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<()> { + let supplement = match self.as_ref().supplement(styles) { + Smart::Auto => TextElem::packed(Self::local_name_in(styles)), + Smart::Custom(None) => Content::empty(), + Smart::Custom(Some(supplement)) => { + supplement.resolve(engine, styles, [self.clone().pack()])? + } + }; + + self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); + Ok(()) + } +} + +impl Show for Packed<EquationElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + if self.block(styles) { + Ok(BlockElem::multi_layouter( + self.clone(), + engine.routines.layout_equation_block, + ) + .pack() + .spanned(self.span())) + } else { + Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline) + .pack() + .spanned(self.span())) + } + } +} + +impl ShowSet for Packed<EquationElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let mut out = Styles::new(); + if self.block(styles) { + out.set(AlignElem::set_alignment(Alignment::CENTER)); + out.set(BlockElem::set_breakable(false)); + out.set(ParLine::set_numbering(None)); + out.set(EquationElem::set_size(MathSize::Display)); + } else { + out.set(EquationElem::set_size(MathSize::Text)); + } + out.set(TextElem::set_weight(FontWeight::from_number(450))); + out.set(TextElem::set_font(FontList(vec![FontFamily::new( + "New Computer Modern Math", + )]))); + out + } +} + +impl Count for Packed<EquationElem> { + fn update(&self) -> Option<CounterUpdate> { + (self.block(StyleChain::default()) && self.numbering().is_some()) + .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) + } +} + +impl LocalName for Packed<EquationElem> { + const KEY: &'static str = "equation"; +} + +impl Refable for Packed<EquationElem> { + fn supplement(&self) -> Content { + // After synthesis, this should always be custom content. + match (**self).supplement(StyleChain::default()) { + Smart::Custom(Some(Supplement::Content(content))) => content, + _ => Content::empty(), + } + } + + fn counter(&self) -> Counter { + Counter::of(EquationElem::elem()) + } + + fn numbering(&self) -> Option<&Numbering> { + (**self).numbering(StyleChain::default()).as_ref() + } +} + +impl Outlinable for Packed<EquationElem> { + fn outline( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult<Option<Content>> { + if !self.block(StyleChain::default()) { + return Ok(None); + } + let Some(numbering) = self.numbering() else { + return Ok(None); + }; + + // After synthesis, this should always be custom content. + let mut supplement = match (**self).supplement(StyleChain::default()) { + Smart::Custom(Some(Supplement::Content(content))) => content, + _ => Content::empty(), + }; + + if !supplement.is_empty() { + supplement += TextElem::packed("\u{a0}"); + } + + let numbers = self.counter().display_at_loc( + engine, + self.location().unwrap(), + styles, + numbering, + )?; + + Ok(Some(supplement + numbers)) + } +} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs new file mode 100644 index 00000000..f5c4514d --- /dev/null +++ b/crates/typst-library/src/math/frac.rs @@ -0,0 +1,56 @@ +use typst_syntax::Spanned; + +use crate::diag::bail; +use crate::foundations::{elem, Content, Value}; +use crate::math::Mathy; + +/// A mathematical fraction. +/// +/// # Example +/// ```example +/// $ 1/2 < (x+1)/2 $ +/// $ ((x+1)) / 2 = frac(a, b) $ +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: Use a slash to turn neighbouring +/// expressions into a fraction. Multiple atoms can be grouped into a single +/// expression using round grouping parenthesis. Such parentheses are removed +/// from the output, but you can nest multiple to force them. +#[elem(title = "Fraction", Mathy)] +pub struct FracElem { + /// The fraction's numerator. + #[required] + pub num: Content, + + /// The fraction's denominator. + #[required] + pub denom: Content, +} + +/// A binomial expression. +/// +/// # Example +/// ```example +/// $ binom(n, k) $ +/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ +/// ``` +#[elem(title = "Binomial", Mathy)] +pub struct BinomElem { + /// The binomial's upper index. + #[required] + pub upper: Content, + + /// The binomial's lower index. + #[required] + #[variadic] + #[parse( + let values = args.all::<Spanned<Value>>()?; + if values.is_empty() { + // Prevents one element binomials + bail!(args.span, "missing argument: lower"); + } + values.into_iter().map(|spanned| spanned.v.display()).collect() + )] + pub lower: Vec<Content>, +} diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs new file mode 100644 index 00000000..07ab0dd4 --- /dev/null +++ b/crates/typst-library/src/math/lr.rs @@ -0,0 +1,135 @@ +use crate::foundations::{elem, func, Content, NativeElement, Smart}; +use crate::layout::{Length, Rel}; +use crate::math::Mathy; +use crate::text::TextElem; + +/// Scales delimiters. +/// +/// While matched delimiters scale by default, this can be used to scale +/// unmatched delimiters and to control the delimiter scaling more precisely. +#[elem(title = "Left/Right", Mathy)] +pub struct LrElem { + /// The size of the brackets, relative to the height of the wrapped content. + pub size: Smart<Rel<Length>>, + + /// The delimited content, including the delimiters. + #[required] + #[parse( + let mut arguments = args.all::<Content>()?.into_iter(); + let mut body = arguments.next().unwrap_or_default(); + arguments.for_each(|arg| body += TextElem::packed(',') + arg); + body + )] + pub body: Content, +} + +/// Scales delimiters vertically to the nearest surrounding `{lr()}` group. +/// +/// ```example +/// $ { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } $ +/// ``` +#[elem(Mathy)] +pub struct MidElem { + /// The content to be scaled. + #[required] + pub body: Content, +} + +/// Floors an expression. +/// +/// ```example +/// $ floor(x/2) $ +/// ``` +#[func] +pub fn floor( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to floor. + body: Content, +) -> Content { + delimited(body, '⌊', '⌋', size) +} + +/// Ceils an expression. +/// +/// ```example +/// $ ceil(x/2) $ +/// ``` +#[func] +pub fn ceil( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to ceil. + body: Content, +) -> Content { + delimited(body, '⌈', '⌉', size) +} + +/// Rounds an expression. +/// +/// ```example +/// $ round(x/2) $ +/// ``` +#[func] +pub fn round( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to round. + body: Content, +) -> Content { + delimited(body, '⌊', '⌉', size) +} + +/// Takes the absolute value of an expression. +/// +/// ```example +/// $ abs(x/2) $ +/// ``` +#[func] +pub fn abs( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to take the absolute value of. + body: Content, +) -> Content { + delimited(body, '|', '|', size) +} + +/// Takes the norm of an expression. +/// +/// ```example +/// $ norm(x/2) $ +/// ``` +#[func] +pub fn norm( + /// The size of the brackets, relative to the height of the wrapped content. + #[named] + size: Option<Smart<Rel<Length>>>, + /// The expression to take the norm of. + body: Content, +) -> Content { + delimited(body, '‖', '‖', size) +} + +fn delimited( + body: Content, + left: char, + right: char, + size: Option<Smart<Rel<Length>>>, +) -> Content { + let span = body.span(); + let mut elem = LrElem::new(Content::sequence([ + TextElem::packed(left), + body, + TextElem::packed(right), + ])); + // Push size only if size is provided + if let Some(size) = size { + elem.push_size(size); + } + elem.pack().spanned(span) +} diff --git a/crates/typst/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs index 66b83e20..1c788995 100644 --- a/crates/typst/src/math/matrix.rs +++ b/crates/typst-library/src/math/matrix.rs @@ -1,30 +1,19 @@ use smallvec::{smallvec, SmallVec}; +use typst_syntax::Spanned; +use typst_utils::Numeric; use unicode_math_class::MathClass; -use crate::diag::{bail, At, HintedStrResult, SourceResult, StrResult}; +use crate::diag::{bail, At, HintedStrResult, StrResult}; use crate::foundations::{ - array, cast, dict, elem, Array, Content, Dict, Fold, NoneValue, Packed, Resolve, - Smart, StyleChain, Value, + array, cast, dict, elem, Array, Content, Dict, Fold, NoneValue, Resolve, Smart, + StyleChain, Symbol, Value, }; -use crate::layout::{ - Abs, Axes, Em, FixedAlignment, Frame, FrameItem, HAlignment, Length, Point, Ratio, - Rel, Size, -}; -use crate::math::{ - alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator, - AlignmentResult, FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, - MathContext, Scaled, DELIM_SHORT_FALL, -}; -use crate::symbols::Symbol; -use crate::syntax::{Span, Spanned}; -use crate::text::TextElem; -use crate::utils::Numeric; -use crate::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape, Stroke}; +use crate::layout::{Abs, Em, HAlignment, Length, Rel}; +use crate::math::Mathy; +use crate::visualize::Stroke; const DEFAULT_ROW_GAP: Em = Em::new(0.2); const DEFAULT_COL_GAP: Em = Em::new(0.5); -const VERTICAL_PADDING: Ratio = Ratio::new(0.1); -const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); /// A column vector. /// @@ -36,7 +25,7 @@ const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); /// $ vec(a, b, c) dot vec(1, 2, 3) /// = a + 2b + 3c $ /// ``` -#[elem(title = "Vector", LayoutMath)] +#[elem(title = "Vector", Mathy)] pub struct VecElem { /// The delimiter to use. /// @@ -75,23 +64,6 @@ pub struct VecElem { pub children: Vec<Content>, } -impl LayoutMath for Packed<VecElem> { - #[typst_macros::time(name = "math.vec", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let delim = self.delim(styles); - let frame = layout_vec_body( - ctx, - styles, - self.children(), - self.align(styles), - self.gap(styles).at(scaled_font_size(ctx, styles)), - LeftRightAlternator::Right, - )?; - - layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), self.span()) - } -} - /// A matrix. /// /// The elements of a row should be separated by commas, while the rows @@ -113,7 +85,7 @@ impl LayoutMath for Packed<VecElem> { /// 10, 10, ..., 10; /// ) $ /// ``` -#[elem(title = "Matrix", LayoutMath)] +#[elem(title = "Matrix", Mathy)] pub struct MatElem { /// The delimiter to use. /// @@ -241,56 +213,6 @@ pub struct MatElem { pub rows: Vec<Vec<Content>>, } -impl LayoutMath for Packed<MatElem> { - #[typst_macros::time(name = "math.mat", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let augment = self.augment(styles); - let rows = self.rows(); - - if let Some(aug) = &augment { - for &offset in &aug.hline.0 { - if offset == 0 || offset.unsigned_abs() >= rows.len() { - bail!( - self.span(), - "cannot draw a horizontal line after row {} of a matrix with {} rows", - if offset < 0 { rows.len() as isize + offset } else { offset }, - rows.len() - ); - } - } - - let ncols = self.rows().first().map_or(0, |row| row.len()); - - for &offset in &aug.vline.0 { - if offset == 0 || offset.unsigned_abs() >= ncols { - bail!( - self.span(), - "cannot draw a vertical line after column {} of a matrix with {} columns", - if offset < 0 { ncols as isize + offset } else { offset }, - ncols - ); - } - } - } - - let font_size = scaled_font_size(ctx, styles); - let column_gap = self.column_gap(styles).at(font_size); - let row_gap = self.row_gap(styles).at(font_size); - let delim = self.delim(styles); - let frame = layout_mat_body( - ctx, - styles, - rows, - self.align(styles), - augment, - Axes::new(column_gap, row_gap), - self.span(), - )?; - - layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), self.span()) - } -} - /// A case distinction. /// /// Content across different branches can be aligned with the `&` symbol. @@ -304,7 +226,7 @@ impl LayoutMath for Packed<MatElem> { /// 4 "else", /// ) $ /// ``` -#[elem(LayoutMath)] +#[elem(Mathy)] pub struct CasesElem { /// The delimiter to use. /// @@ -342,29 +264,6 @@ pub struct CasesElem { pub children: Vec<Content>, } -impl LayoutMath for Packed<CasesElem> { - #[typst_macros::time(name = "math.cases", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let delim = self.delim(styles); - let frame = layout_vec_body( - ctx, - styles, - self.children(), - FixedAlignment::Start, - self.gap(styles).at(scaled_font_size(ctx, styles)), - LeftRightAlternator::None, - )?; - - let (open, close) = if self.reverse(styles) { - (None, delim.close()) - } else { - (delim.open(), None) - }; - - layout_delimiters(ctx, styles, frame, open, close, self.span()) - } -} - /// A delimiter is a single character that is used to delimit a matrix, vector /// or cases. The character has to be a Unicode codepoint tagged as a math /// "opening", "closing" or "fence". @@ -372,7 +271,7 @@ impl LayoutMath for Packed<CasesElem> { /// Typically, the delimiter is stretched to fit the height of whatever it /// delimits. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -struct Delimiter(Option<char>); +pub struct Delimiter(Option<char>); cast! { Delimiter, @@ -383,11 +282,11 @@ cast! { } impl Delimiter { - fn none() -> Self { + pub fn none() -> Self { Self(None) } - fn char(c: char) -> StrResult<Self> { + pub fn char(c: char) -> StrResult<Self> { if !matches!( unicode_math_class::class(c), Some(MathClass::Opening | MathClass::Closing | MathClass::Fence), @@ -397,11 +296,11 @@ impl Delimiter { Ok(Self(Some(c))) } - fn get(self) -> Option<char> { + pub fn get(self) -> Option<char> { self.0 } - fn find_matching(self) -> Self { + pub fn find_matching(self) -> Self { match self.0 { None => Self::none(), Some('[') => Self(Some(']')), @@ -451,235 +350,16 @@ impl DelimiterPair { }; /// The delimiter's opening character. - fn open(self) -> Option<char> { + pub fn open(self) -> Option<char> { self.open.get() } /// The delimiter's closing character. - fn close(self) -> Option<char> { + pub fn close(self) -> Option<char> { self.close.get() } } -/// Layout the inner contents of a vector. -fn layout_vec_body( - ctx: &mut MathContext, - styles: StyleChain, - column: &[Content], - align: FixedAlignment, - row_gap: Rel<Abs>, - alternator: LeftRightAlternator, -) -> SourceResult<Frame> { - let gap = row_gap.relative_to(ctx.region.size.y); - - let denom_style = style_for_denominator(styles); - let mut flat = vec![]; - for child in column { - flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?); - } - // We pad ascent and descent with the ascent and descent of the paren - // to ensure that normal vectors are aligned with others unless they are - // way too big. - let paren = - GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); - Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent)))) -} - -/// Layout the inner contents of a matrix. -fn layout_mat_body( - ctx: &mut MathContext, - styles: StyleChain, - rows: &[Vec<Content>], - align: FixedAlignment, - augment: Option<Augment<Abs>>, - gap: Axes<Rel<Abs>>, - span: Span, -) -> SourceResult<Frame> { - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); - if ncols == 0 || nrows == 0 { - return Ok(Frame::soft(Size::zero())); - } - - let gap = gap.zip_map(ctx.region.size, Rel::relative_to); - let half_gap = gap * 0.5; - - // We provide a default stroke thickness that scales - // with font size to ensure that augmentation lines - // look correct by default at all matrix sizes. - // The line cap is also set to square because it looks more "correct". - let font_size = scaled_font_size(ctx, styles); - let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.at(font_size); - let default_stroke = FixedStroke { - thickness: default_stroke_thickness, - paint: TextElem::fill_in(styles).as_decoration(), - cap: LineCap::Square, - ..Default::default() - }; - - let (hline, vline, stroke) = match augment { - Some(augment) => { - // We need to get stroke here for ownership. - let stroke = augment.stroke.unwrap_or_default().unwrap_or(default_stroke); - (augment.hline, augment.vline, stroke) - } - _ => (AugmentOffsets::default(), AugmentOffsets::default(), default_stroke), - }; - - // Before the full matrix body can be laid out, the - // individual cells must first be independently laid out - // so we can ensure alignment across rows and columns. - - // This variable stores the maximum ascent and descent for each row. - let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; - - // We want to transpose our data layout to columns - // before final layout. For efficiency, the columns - // variable is set up here and newly generated - // individual cells are then added to it. - let mut cols = vec![vec![]; ncols]; - - let denom_style = style_for_denominator(styles); - // We pad ascent and descent with the ascent and descent of the paren - // to ensure that normal matrices are aligned with others unless they are - // way too big. - let paren = - GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); - - for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { - for (cell, col) in row.iter().zip(&mut cols) { - let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?; - - ascent.set_max(cell.ascent().max(paren.ascent)); - descent.set_max(cell.descent().max(paren.descent)); - - col.push(cell); - } - } - - // For each row, combine maximum ascent and descent into a row height. - // Sum the row heights, then add the total height of the gaps between rows. - let total_height = - heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64; - - // Width starts at zero because it can't be calculated until later - let mut frame = Frame::soft(Size::new(Abs::zero(), total_height)); - - let mut x = Abs::zero(); - - for (index, col) in cols.into_iter().enumerate() { - let AlignmentResult { points, width: rcol } = alignments(&col); - - let mut y = Abs::zero(); - - for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { - let cell = cell.into_line_frame(&points, LeftRightAlternator::Right); - let pos = Point::new( - if points.is_empty() { - x + align.position(rcol - cell.width()) - } else { - x - }, - y + ascent - cell.ascent(), - ); - - frame.push_frame(pos, cell); - - y += ascent + descent + gap.y; - } - - // Advance to the end of the column - x += rcol; - - // If a vertical line should be inserted after this column - if vline.0.contains(&(index as isize + 1)) - || vline.0.contains(&(1 - ((ncols - index) as isize))) - { - frame.push( - Point::with_x(x + half_gap.x), - line_item(total_height, true, stroke.clone(), span), - ); - } - - // Advance to the start of the next column - x += gap.x; - } - - // Once all the columns are laid out, the total width can be calculated - let total_width = x - gap.x; - - // This allows the horizontal lines to be laid out - for line in hline.0 { - let real_line = - if line < 0 { nrows - line.unsigned_abs() } else { line as usize }; - let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>() - + gap.y * (real_line - 1) as f64) - + half_gap.y; - - frame.push( - Point::with_y(offset), - line_item(total_width, false, stroke.clone(), span), - ); - } - - frame.size_mut().x = total_width; - - Ok(frame) -} - -fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem { - let line_geom = if vertical { - Geometry::Line(Point::with_y(length)) - } else { - Geometry::Line(Point::with_x(length)) - }; - - FrameItem::Shape( - Shape { - geometry: line_geom, - fill: None, - fill_rule: FillRule::default(), - stroke: Some(stroke), - }, - span, - ) -} - -/// Layout the outer wrapper around the body of a vector or matrix. -fn layout_delimiters( - ctx: &mut MathContext, - styles: StyleChain, - mut frame: Frame, - left: Option<char>, - right: Option<char>, - span: Span, -) -> SourceResult<()> { - let font_size = scaled_font_size(ctx, styles); - let short_fall = DELIM_SHORT_FALL.at(font_size); - let axis = ctx.constants.axis_height().scaled(ctx, font_size); - let height = frame.height(); - let target = height + VERTICAL_PADDING.of(height); - frame.set_baseline(height / 2.0 + axis); - - if let Some(left) = left { - let mut left = GlyphFragment::new(ctx, styles, left, span) - .stretch_vertical(ctx, target, short_fall); - left.align_on_axis(ctx, delimiter_alignment(left.c)); - ctx.push(left); - } - - ctx.push(FrameFragment::new(ctx, styles, frame)); - - if let Some(right) = right { - let mut right = GlyphFragment::new(ctx, styles, right, span) - .stretch_vertical(ctx, target, short_fall); - right.align_on_axis(ctx, delimiter_alignment(right.c)); - ctx.push(right); - } - - Ok(()) -} - /// Parameters specifying how augmentation lines /// should be drawn on a matrix. #[derive(Debug, Default, Clone, PartialEq, Hash)] @@ -758,7 +438,7 @@ cast! { /// The offsets at which augmentation lines should be drawn on a matrix. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct AugmentOffsets(SmallVec<[isize; 1]>); +pub struct AugmentOffsets(pub SmallVec<[isize; 1]>); cast! { AugmentOffsets, diff --git a/crates/typst/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 2c5c32cb..53b1f072 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -1,50 +1,36 @@ //! Mathematical formulas. -#[macro_use] -mod ctx; - pub mod accent; - -mod align; mod attach; mod cancel; -#[path = "class.rs"] -mod class_; mod equation; mod frac; -mod fragment; mod lr; mod matrix; mod op; mod root; -mod row; -mod spacing; -mod stretch; mod style; mod underover; pub use self::accent::{Accent, AccentElem}; -pub use self::align::*; pub use self::attach::*; pub use self::cancel::*; -pub use self::class_::*; pub use self::equation::*; pub use self::frac::*; pub use self::lr::*; pub use self::matrix::*; pub use self::op::*; pub use self::root::*; -pub use self::stretch::*; pub use self::style::*; pub use self::underover::*; -use self::ctx::*; -use self::fragment::*; -use self::row::*; -use self::spacing::*; +use typst_utils::singleton; +use unicode_math_class::MathClass; -use crate::diag::SourceResult; -use crate::foundations::{category, Category, Module, Scope, StyleChain}; +use crate::foundations::{ + category, elem, Category, Content, Module, NativeElement, Scope, +}; +use crate::layout::{Em, HElem}; use crate::text::TextElem; /// Typst has special [syntax]($syntax/#math) and library functions to typeset @@ -152,6 +138,13 @@ use crate::text::TextElem; #[category] pub static MATH: Category; +// Spacings. +pub const THIN: Em = Em::new(1.0 / 6.0); +pub const MEDIUM: Em = Em::new(2.0 / 9.0); +pub const THICK: Em = Em::new(5.0 / 18.0); +pub const QUAD: Em = Em::new(1.0); +pub const WIDE: Em = Em::new(2.0); + /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); @@ -203,9 +196,17 @@ pub fn module() -> Module { math.define_func::<script>(); math.define_func::<sscript>(); - // Text operators, spacings, and symbols. + // Text operators. op::define(&mut math); - spacing::define(&mut math); + + // Spacings. + math.define("thin", HElem::new(THIN.into()).pack()); + math.define("med", HElem::new(MEDIUM.into()).pack()); + math.define("thick", HElem::new(THICK.into()).pack()); + math.define("quad", HElem::new(QUAD.into()).pack()); + math.define("wide", HElem::new(WIDE.into()).pack()); + + // Symbols. for (name, symbol) in crate::symbols::SYM { math.define(*name, symbol.clone()); } @@ -213,8 +214,44 @@ pub fn module() -> Module { Module::new("math", math) } -/// Layout for math elements. -pub trait LayoutMath { - /// Layout the element, producing fragment in the context. - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()>; +/// Trait for recognizing math elements and auto-wrapping them in equations. +pub trait Mathy {} + +/// A math alignment point: `&`, `&&`. +#[elem(title = "Alignment Point", Mathy)] +pub struct AlignPointElem {} + +impl AlignPointElem { + /// Get the globally shared alignment point element. + pub fn shared() -> &'static Content { + singleton!(Content, AlignPointElem::new().pack()) + } +} + +/// Forced use of a certain math class. +/// +/// This is useful to treat certain symbols as if they were of a different +/// class, e.g. to make a symbol behave like a relation. The class of a symbol +/// defines the way it is laid out, including spacing around it, and how its +/// scripts are attached by default. Note that the latter can always be +/// overridden using [`{limits}`](math.limits) and [`{scripts}`](math.scripts). +/// +/// # Example +/// ```example +/// #let loves = math.class( +/// "relation", +/// sym.suit.heart, +/// ) +/// +/// $x loves y and y loves 5$ +/// ``` +#[elem(Mathy)] +pub struct ClassElem { + /// The class to apply to the content. + #[required] + pub class: MathClass, + + /// The content to which the class is applied. + #[required] + pub body: Content, } diff --git a/crates/typst/src/math/op.rs b/crates/typst-library/src/math/op.rs index 8e93b4e4..ef24705a 100644 --- a/crates/typst/src/math/op.rs +++ b/crates/typst-library/src/math/op.rs @@ -1,10 +1,8 @@ use ecow::EcoString; -use unicode_math_class::MathClass; -use crate::diag::SourceResult; -use crate::foundations::{elem, Content, NativeElement, Packed, Scope, StyleChain}; +use crate::foundations::{elem, Content, NativeElement, Scope}; use crate::layout::HElem; -use crate::math::{upright, FrameFragment, LayoutMath, Limits, MathContext, THIN}; +use crate::math::{upright, Mathy, THIN}; use crate::text::TextElem; /// A text operator in an equation. @@ -22,7 +20,7 @@ use crate::text::TextElem; /// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, /// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, /// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. -#[elem(title = "Text Operator", LayoutMath)] +#[elem(title = "Text Operator", Mathy)] pub struct OpElem { /// The operator's text. #[required] @@ -33,30 +31,6 @@ pub struct OpElem { pub limits: bool, } -impl LayoutMath for Packed<OpElem> { - #[typst_macros::time(name = "math.op", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let fragment = ctx.layout_into_fragment(self.text(), styles)?; - let italics = fragment.italics_correction(); - let accent_attach = fragment.accent_attach(); - let text_like = fragment.is_text_like(); - - ctx.push( - FrameFragment::new(ctx, styles, fragment.into_frame()) - .with_class(MathClass::Large) - .with_italics_correction(italics) - .with_accent_attach(accent_attach) - .with_text_like(text_like) - .with_limits(if self.limits(styles) { - Limits::Display - } else { - Limits::Never - }), - ); - Ok(()) - } -} - macro_rules! ops { ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => { pub(super) fn define(math: &mut Scope) { diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs new file mode 100644 index 00000000..e25c6d42 --- /dev/null +++ b/crates/typst-library/src/math/root.rs @@ -0,0 +1,35 @@ +use typst_syntax::Span; + +use crate::foundations::{elem, func, Content, NativeElement}; +use crate::math::Mathy; + +/// A square root. +/// +/// ```example +/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $ +/// ``` +#[func(title = "Square Root")] +pub fn sqrt( + /// The call span of this function. + span: Span, + /// The expression to take the square root of. + radicand: Content, +) -> Content { + RootElem::new(radicand).pack().spanned(span) +} + +/// A general root. +/// +/// ```example +/// $ root(3, x) $ +/// ``` +#[elem(Mathy)] +pub struct RootElem { + /// Which root of the radicand to take. + #[positional] + pub index: Option<Content>, + + /// The expression to take the root of. + #[required] + pub radicand: Content, +} diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs new file mode 100644 index 00000000..f3d28f2a --- /dev/null +++ b/crates/typst-library/src/math/style.rs @@ -0,0 +1,254 @@ +use crate::foundations::{func, Cast, Content, Smart}; +use crate::math::EquationElem; + +/// Bold font style in math. +/// +/// ```example +/// $ bold(A) := B^+ $ +/// ``` +#[func(keywords = ["mathbf"])] +pub fn bold( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_bold(true)) +} + +/// Upright (non-italic) font style in math. +/// +/// ```example +/// $ upright(A) != A $ +/// ``` +#[func(keywords = ["mathup"])] +pub fn upright( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_italic(Smart::Custom(false))) +} + +/// Italic font style in math. +/// +/// For roman letters and greek lowercase letters, this is already the default. +#[func(keywords = ["mathit"])] +pub fn italic( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_italic(Smart::Custom(true))) +} + +/// Serif (roman) font style in math. +/// +/// This is already the default. +#[func(keywords = ["mathrm"])] +pub fn serif( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Serif)) +} + +/// Sans-serif font style in math. +/// +/// ```example +/// $ sans(A B C) $ +/// ``` +#[func(title = "Sans Serif", keywords = ["mathsf"])] +pub fn sans( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Sans)) +} + +/// Calligraphic font style in math. +/// +/// ```example +/// Let $cal(P)$ be the set of ... +/// ``` +/// +/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these +/// styles share the same Unicode codepoints. Switching between the styles is +/// thus only possible if supported by the font via +/// [font features]($text.features). +/// +/// For the default math font, the roundhand style is available through the +/// `ss01` feature. Therefore, you could define your own version of `\mathscr` +/// like this: +/// +/// ```example +/// #let scr(it) = text( +/// features: ("ss01",), +/// box($cal(it)$), +/// ) +/// +/// We establish $cal(P) != scr(P)$. +/// ``` +/// +/// (The box is not conceptually necessary, but unfortunately currently needed +/// due to limitations in Typst's text style handling in math.) +#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] +pub fn cal( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Cal)) +} + +/// Fraktur font style in math. +/// +/// ```example +/// $ frak(P) $ +/// ``` +#[func(title = "Fraktur", keywords = ["mathfrak"])] +pub fn frak( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Frak)) +} + +/// Monospace font style in math. +/// +/// ```example +/// $ mono(x + y = z) $ +/// ``` +#[func(title = "Monospace", keywords = ["mathtt"])] +pub fn mono( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Mono)) +} + +/// Blackboard bold (double-struck) font style in math. +/// +/// For uppercase latin letters, blackboard bold is additionally available +/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`. +/// +/// ```example +/// $ bb(b) $ +/// $ bb(N) = NN $ +/// $ f: NN -> RR $ +/// ``` +#[func(title = "Blackboard Bold", keywords = ["mathbb"])] +pub fn bb( + /// The content to style. + body: Content, +) -> Content { + body.styled(EquationElem::set_variant(MathVariant::Bb)) +} + +/// Forced display style in math. +/// +/// This is the normal size for block equations. +/// +/// ```example +/// $sum_i x_i/2 = display(sum_i x_i/2)$ +/// ``` +#[func(title = "Display Size", keywords = ["displaystyle"])] +pub fn display( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(false)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Display)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced inline (text) style in math. +/// +/// This is the normal size for inline equations. +/// +/// ```example +/// $ sum_i x_i/2 +/// = inline(sum_i x_i/2) $ +/// ``` +#[func(title = "Inline Size", keywords = ["textstyle"])] +pub fn inline( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(false)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Text)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced script style in math. +/// +/// This is the smaller size used in powers or sub- or superscripts. +/// +/// ```example +/// $sum_i x_i/2 = script(sum_i x_i/2)$ +/// ``` +#[func(title = "Script Size", keywords = ["scriptstyle"])] +pub fn script( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(true)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::Script)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// Forced second script style in math. +/// +/// This is the smallest size, used in second-level sub- and superscripts +/// (script of the script). +/// +/// ```example +/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ +/// ``` +#[func(title = "Script-Script Size", keywords = ["scriptscriptstyle"])] +pub fn sscript( + /// The content to size. + body: Content, + /// Whether to impose a height restriction for exponents, like regular sub- + /// and superscripts do. + #[named] + #[default(true)] + cramped: bool, +) -> Content { + body.styled(EquationElem::set_size(MathSize::ScriptScript)) + .styled(EquationElem::set_cramped(cramped)) +} + +/// The size of elements in an equation. +/// +/// See the TeXbook p. 141. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)] +pub enum MathSize { + /// Second-level sub- and superscripts. + ScriptScript, + /// Sub- and superscripts. + Script, + /// Math in text. + Text, + /// Math on its own line. + Display, +} + +/// A mathematical style variant, as defined by Unicode. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)] +pub enum MathVariant { + #[default] + Serif, + Sans, + Cal, + Frak, + Mono, + Bb, +} diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs new file mode 100644 index 00000000..302c51af --- /dev/null +++ b/crates/typst-library/src/math/underover.rs @@ -0,0 +1,156 @@ +use crate::foundations::{elem, Content}; +use crate::math::Mathy; + +/// A horizontal line under content. +/// +/// ```example +/// $ underline(1 + 2 + ... + 5) $ +/// ``` +#[elem(Mathy)] +pub struct UnderlineElem { + /// The content above the line. + #[required] + pub body: Content, +} + +/// A horizontal line over content. +/// +/// ```example +/// $ overline(1 + 2 + ... + 5) $ +/// ``` +#[elem(Mathy)] +pub struct OverlineElem { + /// The content below the line. + #[required] + pub body: Content, +} + +/// A horizontal brace under content, with an optional annotation below. +/// +/// ```example +/// $ underbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderbraceElem { + /// The content above the brace. + #[required] + pub body: Content, + + /// The optional content below the brace. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal brace over content, with an optional annotation above. +/// +/// ```example +/// $ overbrace(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverbraceElem { + /// The content below the brace. + #[required] + pub body: Content, + + /// The optional content above the brace. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal bracket under content, with an optional annotation below. +/// +/// ```example +/// $ underbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderbracketElem { + /// The content above the bracket. + #[required] + pub body: Content, + + /// The optional content below the bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal bracket over content, with an optional annotation above. +/// +/// ```example +/// $ overbracket(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverbracketElem { + /// The content below the bracket. + #[required] + pub body: Content, + + /// The optional content above the bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal parenthesis under content, with an optional annotation below. +/// +/// ```example +/// $ underparen(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UnderparenElem { + /// The content above the parenthesis. + #[required] + pub body: Content, + + /// The optional content below the parenthesis. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal parenthesis over content, with an optional annotation above. +/// +/// ```example +/// $ overparen(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OverparenElem { + /// The content below the parenthesis. + #[required] + pub body: Content, + + /// The optional content above the parenthesis. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal tortoise shell bracket under content, with an optional +/// annotation below. +/// +/// ```example +/// $ undershell(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct UndershellElem { + /// The content above the tortoise shell bracket. + #[required] + pub body: Content, + + /// The optional content below the tortoise shell bracket. + #[positional] + pub annotation: Option<Content>, +} + +/// A horizontal tortoise shell bracket over content, with an optional +/// annotation above. +/// +/// ```example +/// $ overshell(1 + 2 + ... + 5, "numbers") $ +/// ``` +#[elem(Mathy)] +pub struct OvershellElem { + /// The content below the tortoise shell bracket. + #[required] + pub body: Content, + + /// The optional content above the tortoise shell bracket. + #[positional] + pub annotation: Option<Content>, +} diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index ebc2c003..d11055b9 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -18,10 +18,11 @@ use indexmap::IndexMap; use once_cell::sync::Lazy; use smallvec::{smallvec, SmallVec}; use typed_arena::Arena; +use typst_syntax::{Span, Spanned}; +use typst_utils::{LazyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; -use crate::eval::{eval_string, EvalMode}; use crate::foundations::{ cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, @@ -36,12 +37,10 @@ use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, Url, }; - -use crate::syntax::{Span, Spanned}; +use crate::routines::{EvalMode, Routines}; use crate::text::{ FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta, }; -use crate::utils::{LazyHash, NonZeroExt, PicoStr}; use crate::World; /// A bibliography / reference listing. @@ -225,7 +224,7 @@ impl Show for Packed<BibliographyElem> { } let span = self.span(); - let works = Works::generate(engine.world, engine.introspector).at(span)?; + let works = Works::generate(engine).at(span)?; let references = works .references .as_ref() @@ -360,7 +359,7 @@ impl Bibliography { Ok(Bibliography { map: Arc::new(map), - hash: crate::utils::hash128(data), + hash: typst_utils::hash128(data), }) } @@ -553,12 +552,18 @@ pub(super) struct Works { impl Works { /// Generate all citations and the whole bibliography. + pub fn generate(engine: &Engine) -> StrResult<Arc<Works>> { + Self::generate_impl(engine.routines, engine.world, engine.introspector) + } + + /// The internal implementation of [`Works::generate`]. #[comemo::memoize] - pub fn generate( + fn generate_impl( + routines: &Routines, world: Tracked<dyn World + '_>, introspector: Tracked<Introspector>, ) -> StrResult<Arc<Works>> { - let mut generator = Generator::new(world, introspector)?; + let mut generator = Generator::new(routines, world, introspector)?; let rendered = generator.drive(); let works = generator.display(&rendered)?; Ok(Arc::new(works)) @@ -567,6 +572,8 @@ impl Works { /// Context for generating the bibliography. struct Generator<'a> { + /// The routines that is used to evaluate mathematical material in citations. + routines: &'a Routines, /// The world that is used to evaluate mathematical material in citations. world: Tracked<'a, dyn World + 'a>, /// The document's bibliography. @@ -607,6 +614,7 @@ struct CiteInfo { impl<'a> Generator<'a> { /// Create a new generator. fn new( + routines: &'a Routines, world: Tracked<'a, dyn World + 'a>, introspector: Tracked<Introspector>, ) -> StrResult<Self> { @@ -614,6 +622,7 @@ impl<'a> Generator<'a> { let groups = introspector.query(&CiteGroup::elem().select()); let infos = Vec::with_capacity(groups.len()); Ok(Self { + routines, world, bibliography, groups, @@ -770,6 +779,7 @@ impl<'a> Generator<'a> { let link = |i: usize| links.get(info.subinfos.get(i)?.key.as_str()).copied(); let renderer = ElemRenderer { + routines: self.routines, world: self.world, span: info.span, supplement: &supplement, @@ -822,6 +832,7 @@ impl<'a> Generator<'a> { let mut output = vec![]; for (k, item) in rendered.items.iter().enumerate() { let renderer = ElemRenderer { + routines: self.routines, world: self.world, span: self.bibliography.span(), supplement: &|_| None, @@ -865,6 +876,8 @@ impl<'a> Generator<'a> { /// Renders hayagriva elements into content. struct ElemRenderer<'a> { + /// The routines that is used to evaluate mathematical material in citations. + routines: &'a Routines, /// The world that is used to evaluate mathematical material. world: Tracked<'a, dyn World + 'a>, /// The span that is attached to all of the resulting content. @@ -984,9 +997,16 @@ impl ElemRenderer<'_> { /// Display math. fn display_math(&self, math: &str) -> Content { - eval_string(self.world, math, self.span, EvalMode::Math, Scope::new()) - .map(Value::display) - .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span)) + (self.routines.eval_string)( + self.routines, + self.world, + math, + self.span, + EvalMode::Math, + Scope::new(), + ) + .map(Value::display) + .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span)) } /// Display a link. diff --git a/crates/typst/src/model/cite.rs b/crates/typst-library/src/model/cite.rs index e156e0ec..ac0cfa79 100644 --- a/crates/typst/src/model/cite.rs +++ b/crates/typst-library/src/model/cite.rs @@ -154,7 +154,7 @@ impl Show for Packed<CiteGroup> { fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { let location = self.location().unwrap(); let span = self.span(); - Works::generate(engine.world, engine.introspector) + Works::generate(engine) .at(span)? .citations .get(&location) diff --git a/crates/typst/src/model/document.rs b/crates/typst-library/src/model/document.rs index b693d785..b693d785 100644 --- a/crates/typst/src/model/document.rs +++ b/crates/typst-library/src/model/document.rs diff --git a/crates/typst/src/model/emph.rs b/crates/typst-library/src/model/emph.rs index e36e5ef7..e36e5ef7 100644 --- a/crates/typst/src/model/emph.rs +++ b/crates/typst-library/src/model/emph.rs diff --git a/crates/typst/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index fcb62f89..bac792d3 100644 --- a/crates/typst/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -1,21 +1,15 @@ use std::str::FromStr; -use comemo::Track; -use smallvec::{smallvec, SmallVec}; +use smallvec::SmallVec; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, Context, NativeElement, Packed, Show, Smart, - StyleChain, Styles, -}; -use crate::introspection::Locator; -use crate::layout::{ - Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, - Length, Regions, Sizing, VAlignment, VElem, + cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, + Styles, }; +use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; -use crate::text::TextElem; /// A numbered list. /// @@ -209,7 +203,7 @@ pub struct EnumElem { #[internal] #[fold] #[ghost] - parents: SmallVec<[usize; 4]>, + pub parents: SmallVec<[usize; 4]>, } #[scope] @@ -219,10 +213,11 @@ impl EnumElem { } impl Show for Packed<EnumElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - let mut realized = BlockElem::multi_layouter(self.clone(), layout_enum) - .pack() - .spanned(self.span()); + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = + BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum) + .pack() + .spanned(self.span()); if self.tight(styles) { let leading = ParElem::leading_in(styles); @@ -235,87 +230,6 @@ impl Show for Packed<EnumElem> { } } -/// Layout the enumeration. -#[typst_macros::time(span = elem.span())] -fn layout_enum( - elem: &Packed<EnumElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, -) -> SourceResult<Fragment> { - let numbering = elem.numbering(styles); - let indent = elem.indent(styles); - let body_indent = elem.body_indent(styles); - let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { - ParElem::leading_in(styles).into() - } else { - ParElem::spacing_in(styles).into() - } - }); - - let mut cells = vec![]; - let mut locator = locator.split(); - let mut number = elem.start(styles); - let mut parents = EnumElem::parents_in(styles); - - let full = elem.full(styles); - - // Horizontally align based on the given respective parameter. - // Vertically align to the top to avoid inheriting `horizon` or `bottom` - // alignment from the context and having the number be displaced in - // relation to the item it refers to. - let number_align = elem.number_align(styles); - - for item in elem.children() { - number = item.number(styles).unwrap_or(number); - - let context = Context::new(None, Some(styles)); - let resolved = if full { - parents.push(number); - let content = numbering.apply(engine, context.track(), &parents)?.display(); - parents.pop(); - content - } else { - match numbering { - Numbering::Pattern(pattern) => { - TextElem::packed(pattern.apply_kth(parents.len(), number)) - } - other => other.apply(engine, context.track(), &[number])?.display(), - } - }; - - // Disable overhang as a workaround to end-aligned dots glitching - // and decreasing spacing between numbers and items. - let resolved = - resolved.aligned(number_align).styled(TextElem::set_overhang(false)); - - cells.push(Cell::new(Content::empty(), locator.next(&()))); - cells.push(Cell::new(resolved, locator.next(&()))); - cells.push(Cell::new(Content::empty(), locator.next(&()))); - cells.push(Cell::new( - item.body.clone().styled(EnumElem::set_parents(smallvec![number])), - locator.next(&item.body.span()), - )); - number = number.saturating_add(1); - } - - let grid = CellGrid::new( - Axes::with_x(&[ - Sizing::Rel(indent.into()), - Sizing::Auto, - Sizing::Rel(body_indent.into()), - Sizing::Auto, - ]), - Axes::with_y(&[gutter.into()]), - cells, - ); - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - - layouter.layout(engine) -} - /// An enumeration item. #[elem(name = "item", title = "Numbered List Item")] pub struct EnumItem { diff --git a/crates/typst/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 1d3c9c95..abdf2a4e 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; use std::str::FromStr; use ecow::EcoString; +use typst_utils::NonZeroExt; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; @@ -19,7 +20,6 @@ use crate::layout::{ }; use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; use crate::text::{Lang, Region, TextElem}; -use crate::utils::NonZeroExt; use crate::visualize::ImageElem; /// A figure with an optional caption. diff --git a/crates/typst/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index 5468b4f5..d9971dd1 100644 --- a/crates/typst/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -1,6 +1,8 @@ use std::num::NonZeroUsize; use std::str::FromStr; +use typst_utils::NonZeroExt; + use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ @@ -11,7 +13,6 @@ use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location}; use crate::layout::{Abs, Em, HElem, Length, Ratio}; use crate::model::{Destination, Numbering, NumberingPattern, ParElem}; use crate::text::{SuperElem, TextElem, TextSize}; -use crate::utils::NonZeroExt; use crate::visualize::{LineElem, Stroke}; /// A footnote. diff --git a/crates/typst/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index 57717f7d..269e95eb 100644 --- a/crates/typst/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,5 +1,7 @@ use std::num::NonZeroUsize; +use typst_utils::NonZeroExt; + use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ @@ -9,12 +11,9 @@ use crate::foundations::{ use crate::introspection::{ Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, }; -use crate::layout::{ - layout_frame, Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, -}; +use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region}; use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; -use crate::utils::NonZeroExt; /// A section heading. /// @@ -240,9 +239,14 @@ impl Show for Packed<HeadingElem> { // We don't have a locator for the numbering here, so we just // use the measurement infrastructure for now. let link = LocatorLink::measure(location); - let size = - layout_frame(engine, &numbering, Locator::link(&link), styles, pod)? - .size(); + let size = (engine.routines.layout_frame)( + engine, + &numbering, + Locator::link(&link), + styles, + pod, + )? + .size(); indent = size.x + SPACING_TO_NUMBERING.resolve(styles); } diff --git a/crates/typst/src/model/link.rs b/crates/typst-library/src/model/link.rs index 31c65a1d..31c65a1d 100644 --- a/crates/typst/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs diff --git a/crates/typst/src/model/list.rs b/crates/typst-library/src/model/list.rs index 3a2bd651..18bddd10 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -6,11 +6,7 @@ use crate::foundations::{ cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, Smart, StyleChain, Styles, Value, }; -use crate::introspection::Locator; -use crate::layout::{ - Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length, - Regions, Sizing, VAlignment, VElem, -}; +use crate::layout::{BlockElem, Em, Length, VElem}; use crate::model::ParElem; use crate::text::TextElem; @@ -133,7 +129,7 @@ pub struct ListElem { #[internal] #[fold] #[ghost] - depth: Depth, + pub depth: Depth, } #[scope] @@ -143,10 +139,11 @@ impl ListElem { } impl Show for Packed<ListElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - let mut realized = BlockElem::multi_layouter(self.clone(), layout_list) - .pack() - .spanned(self.span()); + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = + BlockElem::multi_layouter(self.clone(), engine.routines.layout_list) + .pack() + .spanned(self.span()); if self.tight(styles) { let leading = ParElem::leading_in(styles); @@ -159,60 +156,6 @@ impl Show for Packed<ListElem> { } } -/// Layout the list. -#[typst_macros::time(span = elem.span())] -fn layout_list( - elem: &Packed<ListElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, -) -> SourceResult<Fragment> { - let indent = elem.indent(styles); - let body_indent = elem.body_indent(styles); - let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { - ParElem::leading_in(styles).into() - } else { - ParElem::spacing_in(styles).into() - } - }); - - let Depth(depth) = ListElem::depth_in(styles); - let marker = elem - .marker(styles) - .resolve(engine, styles, depth)? - // avoid '#set align' interference with the list - .aligned(HAlignment::Start + VAlignment::Top); - - let mut cells = vec![]; - let mut locator = locator.split(); - - for item in elem.children() { - cells.push(Cell::new(Content::empty(), locator.next(&()))); - cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); - cells.push(Cell::new(Content::empty(), locator.next(&()))); - cells.push(Cell::new( - item.body.clone().styled(ListElem::set_depth(Depth(1))), - locator.next(&item.body.span()), - )); - } - - let grid = CellGrid::new( - Axes::with_x(&[ - Sizing::Rel(indent.into()), - Sizing::Auto, - Sizing::Rel(body_indent.into()), - Sizing::Auto, - ]), - Axes::with_y(&[gutter.into()]), - cells, - ); - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - - layouter.layout(engine) -} - /// A bullet list item. #[elem(name = "item", title = "Bullet List Item")] pub struct ListItem { @@ -235,7 +178,7 @@ pub enum ListMarker { impl ListMarker { /// Resolve the marker for the given depth. - fn resolve( + pub fn resolve( &self, engine: &mut Engine, styles: StyleChain, diff --git a/crates/typst/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 7dad51c3..7dad51c3 100644 --- a/crates/typst/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs diff --git a/crates/typst/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index f0aa06e5..f0aa06e5 100644 --- a/crates/typst/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs diff --git a/crates/typst/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index b89a1524..85257c2c 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -2,6 +2,8 @@ use std::num::NonZeroUsize; use std::str::FromStr; use comemo::Track; +use typst_syntax::Span; +use typst_utils::NonZeroExt; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; @@ -16,9 +18,7 @@ use crate::layout::{ use crate::model::{ Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, }; -use crate::syntax::Span; use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; -use crate::utils::NonZeroExt; /// A table of contents, figures, or other elements. /// diff --git a/crates/typst/src/model/par.rs b/crates/typst-library/src/model/par.rs index 15bad61f..a43499d8 100644 --- a/crates/typst/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -1,5 +1,7 @@ use std::fmt::{self, Debug, Formatter}; +use typst_utils::singleton; + use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ @@ -9,7 +11,6 @@ use crate::foundations::{ use crate::introspection::{Count, CounterUpdate, Locatable}; use crate::layout::{Em, HAlignment, Length, OuterHAlignment}; use crate::model::Numbering; -use crate::utils::singleton; /// Arranges text, spacing and inline-level elements into a paragraph. /// diff --git a/crates/typst/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 110825f1..110825f1 100644 --- a/crates/typst/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs diff --git a/crates/typst/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index bc1919d6..bc1919d6 100644 --- a/crates/typst/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs diff --git a/crates/typst/src/model/strong.rs b/crates/typst-library/src/model/strong.rs index 0e23179e..0e23179e 100644 --- a/crates/typst/src/model/strong.rs +++ b/crates/typst-library/src/model/strong.rs diff --git a/crates/typst/src/model/table.rs b/crates/typst-library/src/model/table.rs index 74b12196..7dfaf45d 100644 --- a/crates/typst/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -1,24 +1,20 @@ use std::num::NonZeroUsize; use std::sync::Arc; -use ecow::eco_format; +use typst_utils::NonZeroExt; -use crate::diag::{bail, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint}; +use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Content, Fold, NativeElement, Packed, Show, Smart, StyleChain, + cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, }; -use crate::introspection::Locator; use crate::layout::{ - show_grid_cell, Abs, Alignment, Axes, BlockElem, Cell, CellGrid, Celled, Dir, - Fragment, GridCell, GridFooter, GridHLine, GridHeader, GridLayouter, GridVLine, - Length, LinePosition, OuterHAlignment, OuterVAlignment, Regions, Rel, ResolvableCell, - ResolvableGridChild, ResolvableGridItem, Sides, TrackSizings, + show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, + GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, + TrackSizings, }; use crate::model::Figurable; -use crate::syntax::Span; -use crate::text::{LocalName, TextElem}; -use crate::utils::NonZeroExt; +use crate::text::LocalName; use crate::visualize::{Paint, Stroke}; /// A table of items. @@ -263,68 +259,13 @@ impl TableElem { } impl Show for Packed<TableElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::multi_layouter(self.clone(), layout_table) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table) .pack() .spanned(self.span())) } } -/// Layout the table. -#[typst_macros::time(span = elem.span())] -fn layout_table( - elem: &Packed<TableElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, -) -> SourceResult<Fragment> { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the table when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); - let resolve_item = |item: &TableItem| item.to_resolvable(styles); - let children = elem.children().iter().map(|child| match child { - TableChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - TableChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - TableChild::Item(item) => ResolvableGridChild::Item(item.to_resolvable(styles)), - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - layouter.layout(engine) -} - impl LocalName for Packed<TableElem> { const KEY: &'static str = "table"; } @@ -384,44 +325,6 @@ pub enum TableItem { Cell(Packed<TableCell>), } -impl TableItem { - fn to_resolvable(&self, styles: StyleChain) -> ResolvableGridItem<Packed<TableCell>> { - match self { - Self::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - Self::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => { - LinePosition::Before - } - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - Self::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } - } -} - cast! { TableItem, self => match self { @@ -773,7 +676,7 @@ pub struct TableCell { /// The amount of rows spanned by this cell. #[default(NonZeroUsize::ONE)] - rowspan: NonZeroUsize, + pub rowspan: NonZeroUsize, /// The cell's [fill]($table.fill) override. pub fill: Smart<Option<Paint>>, @@ -801,113 +704,18 @@ cast! { v: Content => v.into(), } -impl Default for Packed<TableCell> { - fn default() -> Self { - Packed::new(TableCell::new(Content::default())) - } -} - -impl ResolvableCell for Packed<TableCell> { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option<Paint>, - align: Smart<Alignment>, - inset: Sides<Option<Rel<Length>>>, - stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the table is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on table cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart<usize> { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart<usize> { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } -} - impl Show for Packed<TableCell> { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) } } +impl Default for Packed<TableCell> { + fn default() -> Self { + Packed::new(TableCell::new(Content::default())) + } +} + impl From<Content> for TableCell { fn from(value: Content) -> Self { #[allow(clippy::unwrap_or_default)] diff --git a/crates/typst/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 557895f7..036a03e2 100644 --- a/crates/typst/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -1,3 +1,5 @@ +use typst_utils::Numeric; + use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ @@ -7,7 +9,6 @@ use crate::foundations::{ use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::model::{ListItemLike, ListLike, ParElem}; use crate::text::TextElem; -use crate::utils::Numeric; /// A list of terms and their descriptions. /// diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs new file mode 100644 index 00000000..6b78b7fb --- /dev/null +++ b/crates/typst-library/src/routines.rs @@ -0,0 +1,368 @@ +#![allow(unused)] + +use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; + +use comemo::{Tracked, TrackedMut}; +use typst_syntax::Span; +use typst_utils::LazyHash; + +use crate::diag::SourceResult; +use crate::engine::{Engine, Route, Sink, Traced}; +use crate::foundations::{ + Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, StyleVec, + Styles, Value, +}; +use crate::introspection::{Introspector, Locator, SplitLocator}; +use crate::layout::{ + Abs, BoxElem, ColumnsElem, Fragment, Frame, GridElem, InlineItem, MoveElem, PadElem, + Region, Regions, Rel, RepeatElem, RotateElem, ScaleElem, Size, SkewElem, StackElem, +}; +use crate::math::EquationElem; +use crate::model::{Document, DocumentInfo, EnumElem, ListElem, TableElem}; +use crate::visualize::{ + CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, + SquareElem, +}; +use crate::World; + +/// Defines the `Routines` struct. +macro_rules! routines { + ($( + $(#[$attr:meta])* + fn $name:ident $(<$($time:lifetime),*>)? ($($args:tt)*) -> $ret:ty + )*) => { + /// Defines implementation of various Typst compiler routines as a table + /// of function pointers. + /// + /// This is essentially dynamic linking and done to allow for crate + /// splitting. + pub struct Routines { + $( + $(#[$attr])* + pub $name: $(for<$($time),*>)? fn ($($args)*) -> $ret + ),* + } + + impl Hash for Routines { + fn hash<H: Hasher>(&self, _: &mut H) {} + } + }; +} + +routines! { + /// Evaluates a string as code and return the resulting value. + fn eval_string( + routines: &Routines, + world: Tracked<dyn World + '_>, + string: &str, + span: Span, + mode: EvalMode, + scope: Scope, + ) -> SourceResult<Value> + + /// Call the closure in the context with the arguments. + fn eval_closure( + func: &Func, + closure: &LazyHash<Closure>, + routines: &Routines, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + traced: Tracked<Traced>, + sink: TrackedMut<Sink>, + route: Tracked<Route>, + context: Tracked<Context>, + args: Args, + ) -> SourceResult<Value> + + /// Realizes content into a flat list of well-known, styled items. + fn realize<'a>( + kind: RealizationKind, + engine: &mut Engine, + locator: &mut SplitLocator, + arenas: &'a Arenas, + content: &'a Content, + styles: StyleChain<'a>, + ) -> SourceResult<Vec<Pair<'a>>> + + /// Layout content into a document. + fn layout_document( + engine: &mut Engine, + content: &Content, + styles: StyleChain, + ) -> SourceResult<Document> + + /// Lays out content into multiple regions. + fn layout_fragment( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out content into a single region, producing a single frame. + fn layout_frame( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out inline content. + fn layout_inline( + engine: &mut Engine, + children: &StyleVec, + locator: Locator, + styles: StyleChain, + consecutive: bool, + region: Size, + expand: bool, + ) -> SourceResult<Fragment> + + /// Lays out a [`BoxElem`]. + fn layout_box( + elem: &Packed<BoxElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Frame> + + /// Lays out a [`ListElem`]. + fn layout_list( + elem: &Packed<ListElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out an [`EnumElem`]. + fn layout_enum( + elem: &Packed<EnumElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`GridElem`]. + fn layout_grid( + elem: &Packed<GridElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`TableElem`]. + fn layout_table( + elem: &Packed<TableElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`StackElem`]. + fn layout_stack( + elem: &Packed<StackElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`ColumnsElem`]. + fn layout_columns( + elem: &Packed<ColumnsElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`MoveElem`]. + fn layout_move( + elem: &Packed<MoveElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`RotateElem`]. + fn layout_rotate( + elem: &Packed<RotateElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`ScaleElem`]. + fn layout_scale( + elem: &Packed<ScaleElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`SkewElem`]. + fn layout_skew( + elem: &Packed<SkewElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`RepeatElem`]. + fn layout_repeat( + elem: &Packed<RepeatElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`PadElem`]. + fn layout_pad( + elem: &Packed<PadElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> + + /// Lays out a [`LineElem`]. + fn layout_line( + elem: &Packed<LineElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`PathElem`]. + fn layout_path( + elem: &Packed<PathElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`PolygonElem`]. + fn layout_polygon( + elem: &Packed<PolygonElem>, + _: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`RectElem`]. + fn layout_rect( + elem: &Packed<RectElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`SquareElem`]. + fn layout_square( + elem: &Packed<SquareElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`EllipseElem`]. + fn layout_ellipse( + elem: &Packed<EllipseElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out a [`CircleElem`]. + fn layout_circle( + elem: &Packed<CircleElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out an [`ImageElem`]. + fn layout_image( + elem: &Packed<ImageElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, + ) -> SourceResult<Frame> + + /// Lays out an [`EquationElem`] in a paragraph. + fn layout_equation_inline( + elem: &Packed<EquationElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, + ) -> SourceResult<Vec<InlineItem>> + + /// Lays out an [`EquationElem`] in a flow. + fn layout_equation_block( + elem: &Packed<EquationElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> +} + +/// In which mode to evaluate a string. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum EvalMode { + /// Evaluate as code, as after a hash. + Code, + /// Evaluate as markup, like in a Typst file. + Markup, + /// Evaluate as math, as in an equation. + Math, +} + +/// Defines what kind of realization we are performing. +pub enum RealizationKind<'a> { + /// This the root realization for the document. Requires a mutable reference + /// to document metadata that will be filled from `set document` rules. + Root(&'a mut DocumentInfo), + /// A nested realization in a container (e.g. a `block`). + Container, + /// A realization within math. + Math, +} + +/// Temporary storage arenas for lifetime extension during realization. +/// +/// Must be kept live while the content returned from realization is processed. +#[derive(Default)] +pub struct Arenas { + /// A typed arena for owned content. + pub content: typed_arena::Arena<Content>, + /// A typed arena for owned styles. + pub styles: typed_arena::Arena<Styles>, + /// An untyped arena for everything that is `Copy`. + pub bump: bumpalo::Bump, +} + +/// A pair of content and a style chain that applies to it. +pub type Pair<'a> = (&'a Content, StyleChain<'a>); diff --git a/crates/typst/src/symbols/emoji.rs b/crates/typst-library/src/symbols/emoji.rs index e6b2bbf9..1bd57ec0 100644 --- a/crates/typst/src/symbols/emoji.rs +++ b/crates/typst-library/src/symbols/emoji.rs @@ -1,5 +1,4 @@ -use crate::foundations::{Module, Scope}; -use crate::symbols::{symbols, Symbol}; +use crate::foundations::{Module, Scope, Symbol}; /// A module with all emoji. pub fn emoji() -> Module { @@ -11,7 +10,7 @@ pub fn emoji() -> Module { } /// A list of named emoji. -const EMOJI: &[(&str, Symbol)] = symbols! { +const EMOJI: &[(&str, Symbol)] = typst_macros::symbols! { abacus: '🧮', abc: '🔤', abcd: '🔡', diff --git a/crates/typst/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs index 711ab149..7e0fb55b 100644 --- a/crates/typst/src/symbols/mod.rs +++ b/crates/typst-library/src/symbols/mod.rs @@ -2,11 +2,9 @@ mod emoji; mod sym; -mod symbol; pub use self::emoji::*; pub use self::sym::*; -pub use self::symbol::*; use crate::foundations::{category, Category, Scope}; @@ -21,7 +19,6 @@ pub static SYMBOLS: Category; /// Hook up all `symbol` definitions. pub(super) fn define(global: &mut Scope) { global.category(SYMBOLS); - global.define_type::<Symbol>(); global.define_module(sym()); global.define_module(emoji()); } diff --git a/crates/typst/src/symbols/sym.rs b/crates/typst-library/src/symbols/sym.rs index 7e957543..606e44ea 100644 --- a/crates/typst/src/symbols/sym.rs +++ b/crates/typst-library/src/symbols/sym.rs @@ -1,5 +1,4 @@ -use crate::foundations::{Module, Scope}; -use crate::symbols::{symbols, Symbol}; +use crate::foundations::{Module, Scope, Symbol}; /// A module with all general symbols. pub fn sym() -> Module { @@ -11,7 +10,7 @@ pub fn sym() -> Module { } /// The list of general symbols. -pub(crate) const SYM: &[(&str, Symbol)] = symbols! { +pub(crate) const SYM: &[(&str, Symbol)] = typst_macros::symbols! { // Control. wj: '\u{2060}', zwj: '\u{200D}', diff --git a/crates/typst/src/text/case.rs b/crates/typst-library/src/text/case.rs index 69dbf5e1..69dbf5e1 100644 --- a/crates/typst/src/text/case.rs +++ b/crates/typst-library/src/text/case.rs diff --git a/crates/typst/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index ce98348d..5da7ecec 100644 --- a/crates/typst/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -1,19 +1,11 @@ -use kurbo::{BezPath, Line, ParamCurve}; use smallvec::smallvec; -use ttf_parser::{GlyphId, OutlineBuilder}; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain}; -use crate::layout::{ - Abs, Corners, Em, Frame, FrameItem, Length, Point, Rel, Sides, Size, -}; -use crate::syntax::Span; -use crate::text::{ - BottomEdge, BottomEdgeMetric, TextEdgeBounds, TextElem, TextItem, TopEdge, - TopEdgeMetric, -}; -use crate::visualize::{styled_rect, Color, FixedStroke, Geometry, Paint, Stroke}; +use crate::layout::{Abs, Corners, Length, Rel, Sides}; +use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; +use crate::visualize::{Color, FixedStroke, Paint, Stroke}; /// Underlines text. /// @@ -375,13 +367,13 @@ impl Show for Packed<HighlightElem> { /// a background. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Decoration { - line: DecoLine, - extent: Abs, + pub line: DecoLine, + pub extent: Abs, } /// A kind of decorative line. #[derive(Debug, Clone, Eq, PartialEq, Hash)] -enum DecoLine { +pub enum DecoLine { Underline { stroke: Stroke<Abs>, offset: Smart<Abs>, @@ -407,206 +399,3 @@ enum DecoLine { radius: Corners<Rel<Abs>>, }, } - -/// Add line decorations to a single run of shaped text. -pub(crate) fn decorate( - frame: &mut Frame, - deco: &Decoration, - text: &TextItem, - width: Abs, - shift: Abs, - pos: Point, -) { - let font_metrics = text.font.metrics(); - - if let DecoLine::Highlight { fill, stroke, top_edge, bottom_edge, radius } = - &deco.line - { - let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge); - let size = Size::new(width + 2.0 * deco.extent, top + bottom); - let rects = styled_rect(size, radius, fill.clone(), stroke); - let origin = Point::new(pos.x - deco.extent, pos.y - top - shift); - frame.prepend_multiple( - rects - .into_iter() - .map(|shape| (origin, FrameItem::Shape(shape, Span::detached()))), - ); - return; - } - - let (stroke, metrics, offset, evade, background) = match &deco.line { - DecoLine::Strikethrough { stroke, offset, background } => { - (stroke, font_metrics.strikethrough, offset, false, *background) - } - DecoLine::Overline { stroke, offset, evade, background } => { - (stroke, font_metrics.overline, offset, *evade, *background) - } - DecoLine::Underline { stroke, offset, evade, background } => { - (stroke, font_metrics.underline, offset, *evade, *background) - } - _ => return, - }; - - let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift; - let stroke = stroke.clone().unwrap_or(FixedStroke::from_pair( - text.fill.as_decoration(), - metrics.thickness.at(text.size), - )); - - let gap_padding = 0.08 * text.size; - let min_width = 0.162 * text.size; - - let start = pos.x - deco.extent; - let end = pos.x + width + deco.extent; - - let mut push_segment = |from: Abs, to: Abs, prepend: bool| { - let origin = Point::new(from, pos.y + offset); - let target = Point::new(to - from, Abs::zero()); - - if target.x >= min_width || !evade { - let shape = Geometry::Line(target).stroked(stroke.clone()); - - if prepend { - frame.prepend(origin, FrameItem::Shape(shape, Span::detached())); - } else { - frame.push(origin, FrameItem::Shape(shape, Span::detached())); - } - } - }; - - if !evade { - push_segment(start, end, background); - return; - } - - let line = Line::new( - kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), - kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), - ); - - let mut x = pos.x; - let mut intersections = vec![]; - - for glyph in text.glyphs.iter() { - let dx = glyph.x_offset.at(text.size) + x; - let mut builder = - BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); - - let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); - let path = builder.finish(); - - x += glyph.x_advance.at(text.size); - - // Only do the costly segments intersection test if the line - // intersects the bounding box. - let intersect = bbox.is_some_and(|bbox| { - let y_min = -text.font.to_em(bbox.y_max).at(text.size); - let y_max = -text.font.to_em(bbox.y_min).at(text.size); - offset >= y_min && offset <= y_max - }); - - if intersect { - // Find all intersections of segments with the line. - intersections.extend( - path.segments() - .flat_map(|seg| seg.intersect_line(line)) - .map(|is| Abs::raw(line.eval(is.line_t).x)), - ); - } - } - - // Add start and end points, taking padding into account. - intersections.push(start - gap_padding); - intersections.push(end + gap_padding); - // When emitting the decorative line segments, we move from left to - // right. The intersections are not necessarily in this order, yet. - intersections.sort(); - - for edge in intersections.windows(2) { - let l = edge[0]; - let r = edge[1]; - - // If we are too close, don't draw the segment - if r - l < gap_padding { - continue; - } else { - push_segment(l + gap_padding, r - gap_padding, background); - } - } -} - -// Return the top/bottom edge of the text given the metric of the font. -fn determine_edges( - text: &TextItem, - top_edge: TopEdge, - bottom_edge: BottomEdge, -) -> (Abs, Abs) { - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - for g in text.glyphs.iter() { - let (t, b) = text.font.edges( - top_edge, - bottom_edge, - text.size, - TextEdgeBounds::Glyph(g.id), - ); - top.set_max(t); - bottom.set_max(b); - } - - (top, bottom) -} - -/// Builds a kurbo [`BezPath`] for a glyph. -struct BezPathBuilder { - path: BezPath, - units_per_em: f64, - font_size: Abs, - x_offset: f64, -} - -impl BezPathBuilder { - fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self { - Self { - path: BezPath::new(), - units_per_em, - font_size, - x_offset, - } - } - - fn finish(self) -> BezPath { - self.path - } - - fn p(&self, x: f32, y: f32) -> kurbo::Point { - kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) - } - - fn s(&self, v: f32) -> f64 { - Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() - } -} - -impl OutlineBuilder for BezPathBuilder { - fn move_to(&mut self, x: f32, y: f32) { - self.path.move_to(self.p(x, y)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.path.line_to(self.p(x, y)); - } - - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.path.quad_to(self.p(x1, y1), self.p(x, y)); - } - - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); - } - - fn close(&mut self) { - self.path.close_path(); - } -} diff --git a/crates/typst/src/text/font/book.rs b/crates/typst-library/src/text/font/book.rs index 23e27f64..23e27f64 100644 --- a/crates/typst/src/text/font/book.rs +++ b/crates/typst-library/src/text/font/book.rs diff --git a/crates/typst/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index e26d323c..08f6fe0a 100644 --- a/crates/typst/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -3,13 +3,13 @@ use std::io::Read; use ttf_parser::{GlyphId, RgbaColor}; +use typst_syntax::Span; use usvg::tiny_skia_path; use xmlwriter::XmlWriter; use crate::layout::{Abs, Frame, FrameItem, Point, Size}; -use crate::syntax::Span; use crate::text::{Font, Glyph}; -use crate::visualize::{FixedStroke, Geometry, Image}; +use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat}; /// Whether this glyph should be rendered via simple outlining instead of via /// `glyph_frame`. @@ -101,12 +101,8 @@ fn draw_raster_glyph( upem: Abs, raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { - let image = Image::new( - raster_image.data.into(), - typst::visualize::ImageFormat::Raster(typst::visualize::RasterFormat::Png), - None, - ) - .ok()?; + let image = + Image::new(raster_image.data.into(), RasterFormat::Png.into(), None).ok()?; // Apple Color emoji doesn't provide offset information (or at least // not in a way ttf-parser understands), so we artificially shift their @@ -179,12 +175,7 @@ fn draw_colr_glyph( let data = svg.end_document().into_bytes(); - let image = Image::new( - data.into(), - typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg), - None, - ) - .ok()?; + let image = Image::new(data.into(), VectorFormat::Svg.into(), None).ok()?; let y_shift = Abs::pt(upem.to_pt() - y_max); let position = Point::new(Abs::pt(x_min), y_shift); @@ -259,12 +250,9 @@ fn draw_svg_glyph( ty = -top, ); - let image = Image::new( - wrapper_svg.into_bytes().into(), - typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg), - None, - ) - .ok()?; + let image = + Image::new(wrapper_svg.into_bytes().into(), VectorFormat::Svg.into(), None) + .ok()?; let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); let size = Size::new(Abs::pt(width), Abs::pt(height)); diff --git a/crates/typst/src/text/font/exceptions.rs b/crates/typst-library/src/text/font/exceptions.rs index 40998bb9..6393df4b 100644 --- a/crates/typst/src/text/font/exceptions.rs +++ b/crates/typst-library/src/text/font/exceptions.rs @@ -2,6 +2,10 @@ use serde::Deserialize; use super::{FontStretch, FontStyle, FontWeight}; +pub fn find_exception(postscript_name: &str) -> Option<&'static Exception> { + EXCEPTION_MAP.get(postscript_name) +} + #[derive(Debug, Default, Deserialize)] pub struct Exception { pub family: Option<&'static str>, @@ -11,7 +15,7 @@ pub struct Exception { } impl Exception { - pub const fn new() -> Self { + const fn new() -> Self { Self { family: None, style: None, @@ -38,10 +42,6 @@ impl Exception { } } -pub fn find_exception(postscript_name: &str) -> Option<&'static Exception> { - EXCEPTION_MAP.get(postscript_name) -} - /// A map which keys are PostScript name and values are override entries. static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { // The old version of Arial-Black, published by Microsoft in 1996 in their diff --git a/crates/typst/src/text/font/mod.rs b/crates/typst-library/src/text/font/mod.rs index 09837312..09837312 100644 --- a/crates/typst/src/text/font/mod.rs +++ b/crates/typst-library/src/text/font/mod.rs diff --git a/crates/typst/src/text/font/variant.rs b/crates/typst-library/src/text/font/variant.rs index c7a00fb3..c7a00fb3 100644 --- a/crates/typst/src/text/font/variant.rs +++ b/crates/typst-library/src/text/font/variant.rs diff --git a/crates/typst/src/text/item.rs b/crates/typst-library/src/text/item.rs index b2f9e272..ed559aec 100644 --- a/crates/typst/src/text/item.rs +++ b/crates/typst-library/src/text/item.rs @@ -2,9 +2,9 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::Range; use ecow::EcoString; +use typst_syntax::Span; use crate::layout::{Abs, Em}; -use crate::syntax::Span; use crate::text::{is_default_ignorable, Font, Lang, Region}; use crate::visualize::{FixedStroke, Paint}; diff --git a/crates/typst/src/text/lang.rs b/crates/typst-library/src/text/lang.rs index 6c9c886a..64ab1a7c 100644 --- a/crates/typst/src/text/lang.rs +++ b/crates/typst-library/src/text/lang.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::str::FromStr; -use crate::diag::Hint; use ecow::{eco_format, EcoString}; +use crate::diag::Hint; use crate::foundations::{cast, StyleChain}; use crate::layout::Dir; use crate::text::TextElem; @@ -304,8 +304,9 @@ fn lang_str(lang: Lang, region: Option<Region>) -> EcoString { #[cfg(test)] mod tests { + use typst_utils::option_eq; + use super::*; - use crate::utils::option_eq; #[test] fn test_region_option_eq() { diff --git a/crates/typst/src/text/linebreak.rs b/crates/typst-library/src/text/linebreak.rs index f32b9223..0519e1c4 100644 --- a/crates/typst/src/text/linebreak.rs +++ b/crates/typst-library/src/text/linebreak.rs @@ -1,5 +1,6 @@ +use typst_utils::singleton; + use crate::foundations::{elem, Content, NativeElement}; -use crate::utils::singleton; /// Inserts a line break. /// diff --git a/crates/typst/src/text/lorem.rs b/crates/typst-library/src/text/lorem.rs index 5d01a550..5d01a550 100644 --- a/crates/typst/src/text/lorem.rs +++ b/crates/typst-library/src/text/lorem.rs diff --git a/crates/typst/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 6f3f741a..acf100b5 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -38,6 +38,7 @@ use once_cell::sync::Lazy; use rustybuzz::Feature; use smallvec::SmallVec; use ttf_parser::Tag; +use typst_syntax::Spanned; use crate::diag::{bail, warning, HintedStrResult, SourceResult}; use crate::engine::Engine; @@ -48,7 +49,6 @@ use crate::foundations::{ }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::model::ParElem; -use crate::syntax::Spanned; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; @@ -816,7 +816,7 @@ cast! { } /// Resolve a prioritized iterator over the font families. -pub(crate) fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { +pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { const FALLBACKS: &[&str] = &[ "libertinus serif", "twitter color emoji", @@ -833,7 +833,7 @@ pub(crate) fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone } /// Resolve the font variant. -pub(crate) fn variant(styles: StyleChain) -> FontVariant { +pub fn variant(styles: StyleChain) -> FontVariant { let mut variant = FontVariant::new( TextElem::style_in(styles), TextElem::weight_in(styles), @@ -1121,7 +1121,7 @@ impl Fold for FontFeatures { } /// Collect the OpenType features to apply. -pub(crate) fn features(styles: StyleChain) -> Vec<Feature> { +pub fn features(styles: StyleChain) -> Vec<Feature> { let mut tags = vec![]; let mut feat = |tag: &[u8; 4], value: u32| { tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); @@ -1273,7 +1273,7 @@ cast! { } /// Whether a codepoint is Unicode `Default_Ignorable`. -pub(crate) fn is_default_ignorable(c: char) -> bool { +pub fn is_default_ignorable(c: char) -> bool { /// The set of Unicode default ignorables. static DEFAULT_IGNORABLE_DATA: Lazy<CodePointSetData> = Lazy::new(|| { icu_properties::sets::load_default_ignorable_code_point( diff --git a/crates/typst/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index e669be38..5ce77348 100644 --- a/crates/typst/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -7,6 +7,7 @@ use once_cell::sync::Lazy; use once_cell::unsync::Lazy as UnsyncLazy; use syntect::highlighting::{self as synt, Theme}; use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; +use typst_syntax::{split_newlines, LinkedNode, Span, Spanned}; use unicode_segmentation::UnicodeSegmentation; use super::Lang; @@ -18,12 +19,11 @@ use crate::foundations::{ }; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::model::{Figurable, ParElem}; -use crate::syntax::{split_newlines, LinkedNode, Span, Spanned}; use crate::text::{ FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, }; use crate::visualize::Color; -use crate::{syntax, World}; +use crate::World; // Shorthand for highlighter closures. type StyleFn<'a> = @@ -358,9 +358,9 @@ impl Packed<RawElem> { let text = lines.iter().map(|(s, _)| s.clone()).collect::<Vec<_>>().join("\n"); let root = match lang.as_deref() { - Some("typc") => syntax::parse_code(&text), - Some("typm") => syntax::parse_math(&text), - _ => syntax::parse(&text), + Some("typc") => typst_syntax::parse_code(&text), + Some("typm") => typst_syntax::parse_math(&text), + _ => typst_syntax::parse(&text), }; ThemedHighlighter::new( @@ -654,7 +654,7 @@ impl<'a> ThemedHighlighter<'a> { for child in self.node.children() { let mut scopes = self.scopes.clone(); - if let Some(tag) = crate::syntax::highlight(&child) { + if let Some(tag) = typst_syntax::highlight(&child) { scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap()) } diff --git a/crates/typst/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 003ecf47..003ecf47 100644 --- a/crates/typst/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs diff --git a/crates/typst/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index bf003bd1..bf003bd1 100644 --- a/crates/typst/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs diff --git a/crates/typst/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 467165b6..5917550d 100644 --- a/crates/typst/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -1,4 +1,5 @@ use ecow::EcoString; +use typst_syntax::is_newline; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, HintedStrResult, StrResult}; @@ -6,7 +7,6 @@ use crate::foundations::{ array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str, }; use crate::layout::Dir; -use crate::syntax::is_newline; use crate::text::{Lang, Region}; /// A language-aware quote that reacts to its context. diff --git a/crates/typst/src/text/space.rs b/crates/typst-library/src/text/space.rs index 2ebc3b33..38a55482 100644 --- a/crates/typst/src/text/space.rs +++ b/crates/typst-library/src/text/space.rs @@ -1,9 +1,9 @@ use ecow::EcoString; +use typst_utils::singleton; use crate::foundations::{ elem, Content, NativeElement, Packed, PlainText, Repr, Unlabellable, }; -use crate::utils::singleton; /// A text space. #[elem(Unlabellable, PlainText, Repr)] diff --git a/crates/typst/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index 6e05f0e3..1a279fbb 100644 --- a/crates/typst/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -9,6 +9,7 @@ use palette::{ Alpha, Darken, Desaturate, FromColor, Lighten, OklabHue, RgbHue, Saturate, ShiftHue, }; use qcms::Profile; +use typst_syntax::{Span, Spanned}; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::foundations::{ @@ -16,7 +17,6 @@ use crate::foundations::{ Value, }; use crate::layout::{Angle, Ratio}; -use crate::syntax::{Span, Spanned}; // Type aliases for `palette` internal types in f32. pub type Oklab = palette::oklab::Oklaba<f32>; @@ -211,7 +211,7 @@ pub enum Color { #[scope] impl Color { /// The module of preset color maps. - pub const MAP: fn() -> Module = || crate::utils::singleton!(Module, map()).clone(); + pub const MAP: fn() -> Module = || typst_utils::singleton!(Module, map()).clone(); pub const BLACK: Self = Self::Luma(Luma::new(0.0, 1.0)); pub const GRAY: Self = Self::Luma(Luma::new(0.6666666, 1.0)); diff --git a/crates/typst/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 45281820..2be7e370 100644 --- a/crates/typst/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -5,13 +5,13 @@ use std::sync::Arc; use ecow::EcoString; use kurbo::Vec2; +use typst_syntax::{Span, Spanned}; use crate::diag::{bail, SourceResult}; use crate::foundations::{ array, cast, func, scope, ty, Args, Array, Cast, Func, IntoValue, Repr, Smart, }; use crate::layout::{Angle, Axes, Dir, Quadrant, Ratio}; -use crate::syntax::{Span, Spanned}; use crate::visualize::{Color, ColorSpace, WeightedColor}; /// A color gradient. diff --git a/crates/typst/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 0bc2f3b1..359db252 100644 --- a/crates/typst/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -6,30 +6,24 @@ mod svg; pub use self::raster::{RasterFormat, RasterImage}; pub use self::svg::SvgImage; -use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use comemo::Tracked; use ecow::EcoString; +use typst_syntax::{Span, Spanned}; +use typst_utils::LazyHash; -use crate::diag::{bail, warning, At, SourceResult, StrResult}; +use crate::diag::{At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, }; -use crate::introspection::Locator; -use crate::layout::{ - Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel, - Size, Sizing, -}; +use crate::layout::{BlockElem, Length, Rel, Sizing}; use crate::loading::Readable; use crate::model::Figurable; -use crate::syntax::{Span, Spanned}; -use crate::text::{families, LocalName}; -use crate::utils::LazyHash; -use crate::visualize::Path; +use crate::text::LocalName; use crate::World; /// A raster or vector graphic. @@ -159,8 +153,8 @@ impl ImageElem { } impl Show for Packed<ImageElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_image) + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_image) .with_width(self.width(styles)) .with_height(self.height(styles)) .pack() @@ -174,134 +168,6 @@ impl LocalName for Packed<ImageElem> { impl Figurable for Packed<ImageElem> {} -/// Layout the image. -#[typst_macros::time(span = elem.span())] -fn layout_image( - elem: &Packed<ImageElem>, - engine: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let span = elem.span(); - - // Take the format that was explicitly defined, or parse the extension, - // or try to detect the format. - let data = elem.data(); - let format = match elem.format(styles) { - Smart::Custom(v) => v, - Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?, - }; - - // Warn the user if the image contains a foreign object. Not perfect - // because the svg could also be encoded, but that's an edge case. - if format == ImageFormat::Vector(VectorFormat::Svg) { - let has_foreign_object = - data.as_str().is_some_and(|s| s.contains("<foreignObject")); - - if has_foreign_object { - engine.sink.warn(warning!( - span, - "image contains foreign object"; - hint: "SVG images with foreign objects might render incorrectly in typst"; - hint: "see https://github.com/typst/typst/issues/1421 for more information" - )); - } - } - - // Construct the image itself. - let image = Image::with_fonts( - data.clone().into(), - format, - elem.alt(styles), - engine.world, - &families(styles).collect::<Vec<_>>(), - ) - .at(span)?; - - // Determine the image's pixel aspect ratio. - let pxw = image.width(); - let pxh = image.height(); - let px_ratio = pxw / pxh; - - // Determine the region's aspect ratio. - let region_ratio = region.size.x / region.size.y; - - // Find out whether the image is wider or taller than the region. - let wide = px_ratio > region_ratio; - - // The space into which the image will be placed according to its fit. - let target = if region.expand.x && region.expand.y { - // If both width and height are forced, take them. - region.size - } else if region.expand.x { - // If just width is forced, take it. - Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio)) - } else if region.expand.y { - // If just height is forced, take it. - Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y) - } else { - // If neither is forced, take the natural image size at the image's - // DPI bounded by the available space. - let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); - let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); - Size::new( - natural.x.min(region.size.x).min(region.size.y * px_ratio), - natural.y.min(region.size.y).min(region.size.x / px_ratio), - ) - }; - - // Compute the actual size of the fitted image. - let fit = elem.fit(styles); - let fitted = match fit { - ImageFit::Cover | ImageFit::Contain => { - if wide == (fit == ImageFit::Contain) { - Size::new(target.x, target.x / px_ratio) - } else { - Size::new(target.y * px_ratio, target.y) - } - } - ImageFit::Stretch => target, - }; - - // First, place the image in a frame of exactly its size and then resize - // the frame to the target size, center aligning the image in the - // process. - let mut frame = Frame::soft(fitted); - frame.push(Point::zero(), FrameItem::Image(image, fitted, span)); - frame.resize(target, Axes::splat(FixedAlignment::Center)); - - // Create a clipping group if only part of the image should be visible. - if fit == ImageFit::Cover && !target.fits(fitted) { - frame.clip(Path::rect(frame.size())); - } - - Ok(frame) -} - -/// Determine the image format based on path and data. -fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> { - let ext = std::path::Path::new(path) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - Ok(match ext.as_str() { - "png" => ImageFormat::Raster(RasterFormat::Png), - "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), - "gif" => ImageFormat::Raster(RasterFormat::Gif), - "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), - _ => match &data { - Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), - Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { - Some(f) => ImageFormat::Raster(f), - None => bail!("unknown image format"), - }, - }, - }) -} - /// How an image should adjust itself to a given area, #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum ImageFit { diff --git a/crates/typst/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 3a57e491..829826c7 100644 --- a/crates/typst/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -7,8 +7,7 @@ use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; -use image::Limits; -use image::{guess_format, DynamicImage, ImageDecoder, ImageResult}; +use image::{guess_format, DynamicImage, ImageDecoder, ImageResult, Limits}; use crate::diag::{bail, StrResult}; use crate::foundations::{Bytes, Cast}; diff --git a/crates/typst/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index f7a498a8..f7a498a8 100644 --- a/crates/typst/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs diff --git a/crates/typst/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs index e00bf220..d9ddab84 100644 --- a/crates/typst/src/visualize/line.rs +++ b/crates/typst-library/src/visualize/line.rs @@ -1,12 +1,8 @@ -use crate::diag::{bail, SourceResult}; +use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::introspection::Locator; -use crate::layout::{ - Abs, Angle, Axes, BlockElem, Frame, FrameItem, Length, Region, Rel, Size, -}; -use crate::utils::Numeric; -use crate::visualize::{Geometry, Stroke}; +use crate::layout::{Abs, Angle, Axes, BlockElem, Length, Rel}; +use crate::visualize::Stroke; /// A line from one point to another. /// @@ -60,41 +56,9 @@ pub struct LineElem { } impl Show for Packed<LineElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_line) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_line) .pack() .spanned(self.span())) } } - -/// Layout the line. -#[typst_macros::time(span = elem.span())] -fn layout_line( - elem: &Packed<LineElem>, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to); - let start = resolve(elem.start(styles)); - let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| { - let length = elem.length(styles); - let angle = elem.angle(styles); - let x = angle.cos() * length; - let y = angle.sin() * length; - resolve(Axes::new(x, y)) - }); - - let stroke = elem.stroke(styles).unwrap_or_default(); - let size = start.max(start + delta).max(Size::zero()); - - if !size.is_finite() { - bail!(elem.span(), "cannot create line with infinite length"); - } - - let mut frame = Frame::soft(size); - let shape = Geometry::Line(delta.to_point()).stroked(stroke); - frame.push(start.to_point(), FrameItem::Shape(shape, elem.span())); - Ok(frame) -} diff --git a/crates/typst/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index 5c8bf646..5c8bf646 100644 --- a/crates/typst/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs diff --git a/crates/typst/src/visualize/paint.rs b/crates/typst-library/src/visualize/paint.rs index cd1006aa..cd1006aa 100644 --- a/crates/typst/src/visualize/paint.rs +++ b/crates/typst-library/src/visualize/paint.rs diff --git a/crates/typst/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index 035b76a5..76fd0df0 100644 --- a/crates/typst/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -1,18 +1,14 @@ -use kurbo::{CubicBez, ParamCurveExtrema}; +use kurbo::ParamCurveExtrema; +use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Resolve, Show, - Smart, StyleChain, + array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart, + StyleChain, }; -use crate::introspection::Locator; -use crate::layout::{ - Abs, Axes, BlockElem, Frame, FrameItem, Length, Point, Region, Rel, Size, -}; -use crate::visualize::{FillRule, FixedStroke, Geometry, Paint, Shape, Stroke}; - -use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; +use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; +use crate::visualize::{FillRule, Paint, Stroke}; /// A path through a list of points, connected by Bezier curves. /// @@ -91,99 +87,13 @@ pub struct PathElem { } impl Show for Packed<PathElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_path) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_path) .pack() .spanned(self.span())) } } -/// Layout the path. -#[typst_macros::time(span = elem.span())] -fn layout_path( - elem: &Packed<PathElem>, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let resolve = |axes: Axes<Rel<Length>>| { - axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() - }; - - let vertices = elem.vertices(); - let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); - - let mut size = Size::zero(); - if points.is_empty() { - return Ok(Frame::soft(size)); - } - - // Only create a path if there are more than zero points. - // Construct a closed path given all points. - let mut path = Path::new(); - path.move_to(points[0]); - - let mut add_cubic = |from_point: Point, - to_point: Point, - from: PathVertex, - to: PathVertex| { - let from_control_point = resolve(from.control_point_from()) + from_point; - let to_control_point = resolve(to.control_point_to()) + to_point; - path.cubic_to(from_control_point, to_control_point, to_point); - - let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw()); - let p1 = kurbo::Point::new( - from_control_point.x.to_raw(), - from_control_point.y.to_raw(), - ); - let p2 = - kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw()); - let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw()); - let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box(); - size.x.set_max(Abs::raw(extrema.x1)); - size.y.set_max(Abs::raw(extrema.y1)); - }; - - for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) { - let from = vertex_window[0]; - let to = vertex_window[1]; - let from_point = point_window[0]; - let to_point = point_window[1]; - - add_cubic(from_point, to_point, from, to); - } - - if elem.closed(styles) { - let from = *vertices.last().unwrap(); // We checked that we have at least one element. - let to = vertices[0]; - let from_point = *points.last().unwrap(); - let to_point = points[0]; - - add_cubic(from_point, to_point, from, to); - path.close_path(); - } - - // Prepare fill and stroke. - let fill = elem.fill(styles); - let fill_rule = elem.fill_rule(styles); - let stroke = match elem.stroke(styles) { - Smart::Auto if fill.is_none() => Some(FixedStroke::default()), - Smart::Auto => None, - Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), - }; - - let mut frame = Frame::soft(size); - let shape = Shape { - geometry: Geometry::Path(path), - stroke, - fill, - fill_rule, - }; - frame.push(Point::zero(), FrameItem::Shape(shape, elem.span())); - Ok(frame) -} - /// A component used for path creation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum PathVertex { @@ -278,6 +188,26 @@ impl Path { path } + /// Create a path that describes an axis-aligned ellipse. + pub fn ellipse(size: Size) -> Self { + // https://stackoverflow.com/a/2007782 + let z = Abs::zero(); + let rx = size.x / 2.0; + let ry = size.y / 2.0; + let m = 0.551784; + let mx = m * rx; + let my = m * ry; + let point = |x, y| Point::new(x + rx, y + ry); + + let mut path = Path::new(); + path.move_to(point(-rx, z)); + path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry)); + path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z)); + path.cubic_to(point(rx, my), point(mx, ry), point(z, ry)); + path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z)); + path + } + /// Push a [`MoveTo`](PathItem::MoveTo) item. pub fn move_to(&mut self, p: Point) { self.0.push(PathItem::MoveTo(p)); diff --git a/crates/typst/src/visualize/pattern.rs b/crates/typst-library/src/visualize/pattern.rs index ca4bb46f..2017ea65 100644 --- a/crates/typst/src/visualize/pattern.rs +++ b/crates/typst-library/src/visualize/pattern.rs @@ -2,14 +2,14 @@ use std::hash::Hash; use std::sync::Arc; use ecow::{eco_format, EcoString}; +use typst_syntax::{Span, Spanned}; +use typst_utils::{LazyHash, Numeric}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, repr, scope, ty, Content, Smart, StyleChain}; use crate::introspection::Locator; -use crate::layout::{layout_frame, Abs, Axes, Frame, Length, Region, Size}; -use crate::syntax::{Span, Spanned}; -use crate::utils::{LazyHash, Numeric}; +use crate::layout::{Abs, Axes, Frame, Length, Region, Size}; use crate::visualize::RelativeTo; use crate::World; @@ -193,7 +193,8 @@ impl Pattern { let locator = Locator::root(); let styles = StyleChain::new(&library.styles); let pod = Region::new(region, Axes::splat(false)); - let mut frame = layout_frame(engine, &body, locator, styles, pod)?; + let mut frame = + (engine.routines.layout_frame)(engine, &body, locator, styles, pod)?; // Set the size of the frame if the size is enforced. if let Smart::Custom(size) = size { diff --git a/crates/typst/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index 19428204..33e4fd32 100644 --- a/crates/typst/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -1,15 +1,14 @@ use std::f64::consts::PI; -use crate::diag::{bail, SourceResult}; +use typst_syntax::Span; + +use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - elem, func, scope, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain, + elem, func, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, }; -use crate::introspection::Locator; -use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel}; -use crate::syntax::Span; -use crate::utils::Numeric; -use crate::visualize::{FillRule, FixedStroke, Geometry, Paint, Path, Shape, Stroke}; +use crate::layout::{Axes, BlockElem, Em, Length, Rel}; +use crate::visualize::{FillRule, Paint, Stroke}; /// A closed polygon. /// @@ -128,63 +127,9 @@ impl PolygonElem { } impl Show for Packed<PolygonElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_polygon) + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_polygon) .pack() .spanned(self.span())) } } - -/// Layout the polygon. -#[typst_macros::time(span = elem.span())] -fn layout_polygon( - elem: &Packed<PolygonElem>, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let points: Vec<Point> = elem - .vertices() - .iter() - .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) - .collect(); - - let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size(); - if !size.is_finite() { - bail!(elem.span(), "cannot create polygon with infinite size"); - } - - let mut frame = Frame::hard(size); - - // Only create a path if there are more than zero points. - if points.is_empty() { - return Ok(frame); - } - - // Prepare fill and stroke. - let fill = elem.fill(styles); - let fill_rule = elem.fill_rule(styles); - let stroke = match elem.stroke(styles) { - Smart::Auto if fill.is_none() => Some(FixedStroke::default()), - Smart::Auto => None, - Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), - }; - - // Construct a closed path given all points. - let mut path = Path::new(); - path.move_to(points[0]); - for &point in &points[1..] { - path.line_to(point); - } - path.close_path(); - - let shape = Shape { - geometry: Geometry::Path(path), - stroke, - fill, - fill_rule, - }; - frame.push(Point::zero(), FrameItem::Shape(shape, elem.span())); - Ok(frame) -} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs new file mode 100644 index 00000000..01e316a8 --- /dev/null +++ b/crates/typst-library/src/visualize/shape.rs @@ -0,0 +1,448 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, +}; +use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing}; +use crate::visualize::{FixedStroke, Paint, Path, Stroke}; + +/// A rectangle with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #rect(width: 35%, height: 30pt) +/// +/// // With content. +/// #rect[ +/// Automatically sized \ +/// to fit the content. +/// ] +/// ``` +#[elem(title = "Rectangle", Show)] +pub struct RectElem { + /// The rectangle's width, relative to its parent container. + pub width: Smart<Rel<Length>>, + + /// The rectangle's height, relative to its parent container. + pub height: Sizing, + + /// How to fill the rectangle. + /// + /// When setting a fill, the default stroke disappears. To create a + /// rectangle with both fill and stroke, you have to configure both. + /// + /// ```example + /// #rect(fill: blue) + /// ``` + pub fill: Option<Paint>, + + /// How to stroke the rectangle. This can be: + /// + /// - `{none}` to disable stroking + /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is + /// given. + /// - Any kind of [stroke] + /// - A dictionary describing the stroke for each side individually. The + /// dictionary can contain the following keys in order of precedence: + /// - `top`: The top stroke. + /// - `right`: The right stroke. + /// - `bottom`: The bottom stroke. + /// - `left`: The left stroke. + /// - `x`: The horizontal stroke. + /// - `y`: The vertical stroke. + /// - `rest`: The stroke on all sides except those for which the + /// dictionary explicitly sets a size. + /// + /// ```example + /// #stack( + /// dir: ltr, + /// spacing: 1fr, + /// rect(stroke: red), + /// rect(stroke: 2pt), + /// rect(stroke: 2pt + red), + /// ) + /// ``` + #[resolve] + #[fold] + pub stroke: Smart<Sides<Option<Option<Stroke>>>>, + + /// How much to round the rectangle's corners, relative to the minimum of + /// the width and height divided by two. This can be: + /// + /// - A relative length for a uniform corner radius. + /// - A dictionary: With a dictionary, the stroke for each side can be set + /// individually. The dictionary can contain the following keys in order + /// of precedence: + /// - `top-left`: The top-left corner radius. + /// - `top-right`: The top-right corner radius. + /// - `bottom-right`: The bottom-right corner radius. + /// - `bottom-left`: The bottom-left corner radius. + /// - `left`: The top-left and bottom-left corner radii. + /// - `top`: The top-left and top-right corner radii. + /// - `right`: The top-right and bottom-right corner radii. + /// - `bottom`: The bottom-left and bottom-right corner radii. + /// - `rest`: The radii for all corners except those for which the + /// dictionary explicitly sets a size. + /// + /// ```example + /// #set rect(stroke: 4pt) + /// #rect( + /// radius: ( + /// left: 5pt, + /// top-right: 20pt, + /// bottom-right: 10pt, + /// ), + /// stroke: ( + /// left: red, + /// top: yellow, + /// right: green, + /// bottom: blue, + /// ), + /// ) + /// ``` + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the rectangle's content. + /// See the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the rectangle's size without affecting the layout. + /// See the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the rectangle. + /// + /// When this is omitted, the rectangle takes on a default size of at most + /// `{45pt}` by `{30pt}`. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<RectElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rect) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// A square with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #square(size: 40pt) +/// +/// // With content. +/// #square[ +/// Automatically \ +/// sized to fit. +/// ] +/// ``` +#[elem(Show)] +pub struct SquareElem { + /// The square's side length. This is mutually exclusive with `width` and + /// `height`. + #[external] + pub size: Smart<Length>, + + /// The square's width. This is mutually exclusive with `size` and `height`. + /// + /// In contrast to `size`, this can be relative to the parent container's + /// width. + #[parse( + let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from)); + match size { + None => args.named("width")?, + size => size, + } + )] + pub width: Smart<Rel<Length>>, + + /// The square's height. This is mutually exclusive with `size` and `width`. + /// + /// In contrast to `size`, this can be relative to the parent container's + /// height. + #[parse(match size { + None => args.named("height")?, + size => size.map(Into::into), + })] + pub height: Sizing, + + /// How to fill the square. See the [rectangle's documentation]($rect.fill) + /// for more details. + pub fill: Option<Paint>, + + /// How to stroke the square. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Smart<Sides<Option<Option<Stroke>>>>, + + /// How much to round the square's corners. See the + /// [rectangle's documentation]($rect.radius) for more details. + #[resolve] + #[fold] + pub radius: Corners<Option<Rel<Length>>>, + + /// How much to pad the square's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the square's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the square. The square expands to fit this + /// content, keeping the 1-1 aspect ratio. + /// + /// When this is omitted, the square takes on a default size of at most + /// `{30pt}`. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<SquareElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_square) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// An ellipse with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #ellipse(width: 35%, height: 30pt) +/// +/// // With content. +/// #ellipse[ +/// #set align(center) +/// Automatically sized \ +/// to fit the content. +/// ] +/// ``` +#[elem(Show)] +pub struct EllipseElem { + /// The ellipse's width, relative to its parent container. + pub width: Smart<Rel<Length>>, + + /// The ellipse's height, relative to its parent container. + pub height: Sizing, + + /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill) + /// for more details. + pub fill: Option<Paint>, + + /// How to stroke the ellipse. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + pub stroke: Smart<Option<Stroke>>, + + /// How much to pad the ellipse's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the ellipse's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the ellipse. + /// + /// When this is omitted, the ellipse takes on a default size of at most + /// `{45pt}` by `{30pt}`. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<EllipseElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_ellipse) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// A circle with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #circle(radius: 25pt) +/// +/// // With content. +/// #circle[ +/// #set align(center + horizon) +/// Automatically \ +/// sized to fit. +/// ] +/// ``` +#[elem(Show)] +pub struct CircleElem { + /// The circle's radius. This is mutually exclusive with `width` and + /// `height`. + #[external] + pub radius: Length, + + /// The circle's width. This is mutually exclusive with `radius` and + /// `height`. + /// + /// In contrast to `radius`, this can be relative to the parent container's + /// width. + #[parse( + let size = args + .named::<Smart<Length>>("radius")? + .map(|s| s.map(|r| 2.0 * Rel::from(r))); + match size { + None => args.named("width")?, + size => size, + } + )] + pub width: Smart<Rel<Length>>, + + /// The circle's height. This is mutually exclusive with `radius` and + /// `width`. + /// + /// In contrast to `radius`, this can be relative to the parent container's + /// height. + #[parse(match size { + None => args.named("height")?, + size => size.map(Into::into), + })] + pub height: Sizing, + + /// How to fill the circle. See the [rectangle's documentation]($rect.fill) + /// for more details. + pub fill: Option<Paint>, + + /// How to stroke the circle. See the + /// [rectangle's documentation]($rect.stroke) for more details. + #[resolve] + #[fold] + #[default(Smart::Auto)] + pub stroke: Smart<Option<Stroke>>, + + /// How much to pad the circle's content. See the + /// [box's documentation]($box.inset) for more details. + #[resolve] + #[fold] + #[default(Sides::splat(Some(Abs::pt(5.0).into())))] + pub inset: Sides<Option<Rel<Length>>>, + + /// How much to expand the circle's size without affecting the layout. See + /// the [box's documentation]($box.outset) for more details. + #[resolve] + #[fold] + pub outset: Sides<Option<Rel<Length>>>, + + /// The content to place into the circle. The circle expands to fit this + /// content, keeping the 1-1 aspect ratio. + #[positional] + #[borrowed] + pub body: Option<Content>, +} + +impl Show for Packed<CircleElem> { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_circle) + .with_width(self.width(styles)) + .with_height(self.height(styles)) + .pack() + .spanned(self.span())) + } +} + +/// A geometric shape with optional fill and stroke. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Shape { + /// The shape's geometry. + pub geometry: Geometry, + /// The shape's background fill. + pub fill: Option<Paint>, + /// The shape's fill rule. + pub fill_rule: FillRule, + /// The shape's border stroke. + pub stroke: Option<FixedStroke>, +} + +/// A path filling rule. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum FillRule { + /// Specifies that "inside" is computed by a non-zero sum of signed edge crossings. + #[default] + NonZero, + /// Specifies that "inside" is computed by an odd number of edge crossings. + EvenOdd, +} + +/// A shape's geometry. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Geometry { + /// A line to a point (relative to its position). + Line(Point), + /// A rectangle with its origin in the topleft corner. + Rect(Size), + /// A bezier path. + Path(Path), +} + +impl Geometry { + /// Fill the geometry without a stroke. + pub fn filled(self, fill: impl Into<Paint>) -> Shape { + Shape { + geometry: self, + fill: Some(fill.into()), + fill_rule: FillRule::default(), + stroke: None, + } + } + + /// Stroke the geometry without a fill. + pub fn stroked(self, stroke: FixedStroke) -> Shape { + Shape { + geometry: self, + fill: None, + fill_rule: FillRule::default(), + stroke: Some(stroke), + } + } + + /// The bounding box of the geometry. + pub fn bbox_size(&self) -> Size { + match self { + Self::Line(line) => Size::new(line.x, line.y), + Self::Rect(s) => *s, + Self::Path(p) => p.bbox_size(), + } + } +} diff --git a/crates/typst/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs index 234ad1da..4ca10920 100644 --- a/crates/typst/src/visualize/stroke.rs +++ b/crates/typst-library/src/visualize/stroke.rs @@ -1,4 +1,5 @@ use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; use crate::diag::{HintedStrResult, SourceResult}; use crate::foundations::{ @@ -6,7 +7,6 @@ use crate::foundations::{ Resolve, Smart, StyleChain, Value, }; use crate::layout::{Abs, Length}; -use crate::utils::{Numeric, Scalar}; use crate::visualize::{Color, Gradient, Paint, Pattern}; /// Defines how to draw a line. diff --git a/crates/typst/translations/ar.txt b/crates/typst-library/translations/ar.txt index 5c17d379..5c17d379 100644 --- a/crates/typst/translations/ar.txt +++ b/crates/typst-library/translations/ar.txt diff --git a/crates/typst/translations/ca.txt b/crates/typst-library/translations/ca.txt index b16d2f9f..b16d2f9f 100644 --- a/crates/typst/translations/ca.txt +++ b/crates/typst-library/translations/ca.txt diff --git a/crates/typst/translations/cs.txt b/crates/typst-library/translations/cs.txt index 13670586..13670586 100644 --- a/crates/typst/translations/cs.txt +++ b/crates/typst-library/translations/cs.txt diff --git a/crates/typst/translations/da.txt b/crates/typst-library/translations/da.txt index 9664c3b0..9664c3b0 100644 --- a/crates/typst/translations/da.txt +++ b/crates/typst-library/translations/da.txt diff --git a/crates/typst/translations/de.txt b/crates/typst-library/translations/de.txt index 8ba72adb..8ba72adb 100644 --- a/crates/typst/translations/de.txt +++ b/crates/typst-library/translations/de.txt diff --git a/crates/typst/translations/en.txt b/crates/typst-library/translations/en.txt index 02e7accd..02e7accd 100644 --- a/crates/typst/translations/en.txt +++ b/crates/typst-library/translations/en.txt diff --git a/crates/typst/translations/es.txt b/crates/typst-library/translations/es.txt index b0e56642..b0e56642 100644 --- a/crates/typst/translations/es.txt +++ b/crates/typst-library/translations/es.txt diff --git a/crates/typst/translations/et.txt b/crates/typst-library/translations/et.txt index 01f23b4b..01f23b4b 100644 --- a/crates/typst/translations/et.txt +++ b/crates/typst-library/translations/et.txt diff --git a/crates/typst/translations/fi.txt b/crates/typst-library/translations/fi.txt index da516d24..da516d24 100644 --- a/crates/typst/translations/fi.txt +++ b/crates/typst-library/translations/fi.txt diff --git a/crates/typst/translations/fr.txt b/crates/typst-library/translations/fr.txt index 6c5b60f6..6c5b60f6 100644 --- a/crates/typst/translations/fr.txt +++ b/crates/typst-library/translations/fr.txt diff --git a/crates/typst/translations/gl.txt b/crates/typst-library/translations/gl.txt index 7c26953d..7c26953d 100644 --- a/crates/typst/translations/gl.txt +++ b/crates/typst-library/translations/gl.txt diff --git a/crates/typst/translations/gr.txt b/crates/typst-library/translations/gr.txt index 34aa3c44..34aa3c44 100644 --- a/crates/typst/translations/gr.txt +++ b/crates/typst-library/translations/gr.txt diff --git a/crates/typst/translations/he.txt b/crates/typst-library/translations/he.txt index 7c3705a4..7c3705a4 100644 --- a/crates/typst/translations/he.txt +++ b/crates/typst-library/translations/he.txt diff --git a/crates/typst/translations/hu.txt b/crates/typst-library/translations/hu.txt index f1b616b7..f1b616b7 100644 --- a/crates/typst/translations/hu.txt +++ b/crates/typst-library/translations/hu.txt diff --git a/crates/typst/translations/is.txt b/crates/typst-library/translations/is.txt index dcfabacd..dcfabacd 100644 --- a/crates/typst/translations/is.txt +++ b/crates/typst-library/translations/is.txt diff --git a/crates/typst/translations/it.txt b/crates/typst-library/translations/it.txt index 4c1cd90c..4c1cd90c 100644 --- a/crates/typst/translations/it.txt +++ b/crates/typst-library/translations/it.txt diff --git a/crates/typst/translations/ja.txt b/crates/typst-library/translations/ja.txt index 6c1f6f9c..6c1f6f9c 100644 --- a/crates/typst/translations/ja.txt +++ b/crates/typst-library/translations/ja.txt diff --git a/crates/typst/translations/la.txt b/crates/typst-library/translations/la.txt index 3ec63fff..3ec63fff 100644 --- a/crates/typst/translations/la.txt +++ b/crates/typst-library/translations/la.txt diff --git a/crates/typst/translations/nb.txt b/crates/typst-library/translations/nb.txt index f51bcd75..f51bcd75 100644 --- a/crates/typst/translations/nb.txt +++ b/crates/typst-library/translations/nb.txt diff --git a/crates/typst/translations/nl.txt b/crates/typst-library/translations/nl.txt index ecd464b3..ecd464b3 100644 --- a/crates/typst/translations/nl.txt +++ b/crates/typst-library/translations/nl.txt diff --git a/crates/typst/translations/nn.txt b/crates/typst-library/translations/nn.txt index b067bc56..b067bc56 100644 --- a/crates/typst/translations/nn.txt +++ b/crates/typst-library/translations/nn.txt diff --git a/crates/typst/translations/pl.txt b/crates/typst-library/translations/pl.txt index c4934257..c4934257 100644 --- a/crates/typst/translations/pl.txt +++ b/crates/typst-library/translations/pl.txt diff --git a/crates/typst/translations/pt-PT.txt b/crates/typst-library/translations/pt-PT.txt index 6fbb586a..6fbb586a 100644 --- a/crates/typst/translations/pt-PT.txt +++ b/crates/typst-library/translations/pt-PT.txt diff --git a/crates/typst/translations/pt.txt b/crates/typst-library/translations/pt.txt index a8372e6d..a8372e6d 100644 --- a/crates/typst/translations/pt.txt +++ b/crates/typst-library/translations/pt.txt diff --git a/crates/typst/translations/ro.txt b/crates/typst-library/translations/ro.txt index 44d6bc8b..44d6bc8b 100644 --- a/crates/typst/translations/ro.txt +++ b/crates/typst-library/translations/ro.txt diff --git a/crates/typst/translations/ru.txt b/crates/typst-library/translations/ru.txt index fccf7bc9..fccf7bc9 100644 --- a/crates/typst/translations/ru.txt +++ b/crates/typst-library/translations/ru.txt diff --git a/crates/typst/translations/sl.txt b/crates/typst-library/translations/sl.txt index 1375c4dd..1375c4dd 100644 --- a/crates/typst/translations/sl.txt +++ b/crates/typst-library/translations/sl.txt diff --git a/crates/typst/translations/sq.txt b/crates/typst-library/translations/sq.txt index 3b429545..3b429545 100644 --- a/crates/typst/translations/sq.txt +++ b/crates/typst-library/translations/sq.txt diff --git a/crates/typst/translations/sr.txt b/crates/typst-library/translations/sr.txt index 3a2f92b9..3a2f92b9 100644 --- a/crates/typst/translations/sr.txt +++ b/crates/typst-library/translations/sr.txt diff --git a/crates/typst/translations/sv.txt b/crates/typst-library/translations/sv.txt index 61a39403..61a39403 100644 --- a/crates/typst/translations/sv.txt +++ b/crates/typst-library/translations/sv.txt diff --git a/crates/typst/translations/tl.txt b/crates/typst-library/translations/tl.txt index a7b6b571..a7b6b571 100644 --- a/crates/typst/translations/tl.txt +++ b/crates/typst-library/translations/tl.txt diff --git a/crates/typst/translations/tr.txt b/crates/typst-library/translations/tr.txt index a7068a4f..a7068a4f 100644 --- a/crates/typst/translations/tr.txt +++ b/crates/typst-library/translations/tr.txt diff --git a/crates/typst/translations/ua.txt b/crates/typst-library/translations/ua.txt index 30b409a6..30b409a6 100644 --- a/crates/typst/translations/ua.txt +++ b/crates/typst-library/translations/ua.txt diff --git a/crates/typst/translations/vi.txt b/crates/typst-library/translations/vi.txt index a65ac09a..a65ac09a 100644 --- a/crates/typst/translations/vi.txt +++ b/crates/typst-library/translations/vi.txt diff --git a/crates/typst/translations/zh-TW.txt b/crates/typst-library/translations/zh-TW.txt index af6d5950..af6d5950 100644 --- a/crates/typst/translations/zh-TW.txt +++ b/crates/typst-library/translations/zh-TW.txt diff --git a/crates/typst/translations/zh.txt b/crates/typst-library/translations/zh.txt index d4b86544..d4b86544 100644 --- a/crates/typst/translations/zh.txt +++ b/crates/typst-library/translations/zh.txt diff --git a/crates/typst-macros/src/cast.rs b/crates/typst-macros/src/cast.rs index c732ce6b..9254cdb9 100644 --- a/crates/typst-macros/src/cast.rs +++ b/crates/typst-macros/src/cast.rs @@ -108,7 +108,7 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> { let from_value = (!input.from_value.is_empty() || input.dynamic).then(|| { quote! { impl #foundations::FromValue for #ty { - fn from_value(value: #foundations::Value) -> ::typst::diag::HintedStrResult<Self> { + fn from_value(value: #foundations::Value) -> ::typst_library::diag::HintedStrResult<Self> { #from_value_body } } diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index a3666e1d..3b968f76 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -314,6 +314,7 @@ fn create(element: &Elem) -> Result<TokenStream> { let fields_impl = create_fields_impl(element); let repr_impl = element.cannot("Repr").then(|| create_repr_impl(element)); let locatable_impl = element.can("Locatable").then(|| create_locatable_impl(element)); + let mathy_impl = element.can("Mathy").then(|| create_mathy_impl(element)); let into_value_impl = create_into_value_impl(element); // We use a const block to create an anonymous scope, as to not leak any @@ -333,6 +334,7 @@ fn create(element: &Elem) -> Result<TokenStream> { #partial_eq_impl #repr_impl #locatable_impl + #mathy_impl #into_value_impl }; }) @@ -632,7 +634,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { let Elem { name, ident, title, scope, keywords, docs, .. } = element; let local_name = if element.can("LocalName") { - quote! { Some(<#foundations::Packed<#ident> as ::typst::text::LocalName>::local_name) } + quote! { Some(<#foundations::Packed<#ident> as ::typst_library::text::LocalName>::local_name) } } else { quote! { None } }; @@ -761,9 +763,9 @@ fn create_construct_impl(element: &Elem) -> TokenStream { quote! { impl #foundations::Construct for #ident { fn construct( - engine: &mut ::typst::engine::Engine, + engine: &mut ::typst_library::engine::Engine, args: &mut #foundations::Args, - ) -> ::typst::diag::SourceResult<#foundations::Content> { + ) -> ::typst_library::diag::SourceResult<#foundations::Content> { #(#setup)* Ok(#foundations::Content::new(Self { #(#fields),* })) } @@ -788,9 +790,9 @@ fn create_set_impl(element: &Elem) -> TokenStream { quote! { impl #foundations::Set for #ident { fn set( - engine: &mut ::typst::engine::Engine, + engine: &mut ::typst_library::engine::Engine, args: &mut #foundations::Args, - ) -> ::typst::diag::SourceResult<#foundations::Styles> { + ) -> ::typst_library::diag::SourceResult<#foundations::Styles> { let mut styles = #foundations::Styles::new(); #(#handlers)* Ok(styles) @@ -837,7 +839,7 @@ fn create_capable_impl(element: &Elem) -> TokenStream { // Safety: The vtable function doesn't require initialized // data, so it's fine to use a dangling pointer. return Some(unsafe { - ::typst::utils::fat::vtable(dangling as *const dyn #capability) + ::typst_utils::fat::vtable(dangling as *const dyn #capability) }); } } @@ -1047,7 +1049,13 @@ fn create_repr_impl(element: &Elem) -> TokenStream { /// Creates the element's `Locatable` implementation. fn create_locatable_impl(element: &Elem) -> TokenStream { let ident = &element.ident; - quote! { impl ::typst::introspection::Locatable for #foundations::Packed<#ident> {} } + quote! { impl ::typst_library::introspection::Locatable for #foundations::Packed<#ident> {} } +} + +/// Creates the element's `Mathy` implementation. +fn create_mathy_impl(element: &Elem) -> TokenStream { + let ident = &element.ident; + quote! { impl ::typst_library::math::Mathy for #foundations::Packed<#ident> {} } } /// Creates the element's `IntoValue` implementation. diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs index 8d3fd439..d402efd9 100644 --- a/crates/typst-macros/src/func.rs +++ b/crates/typst-macros/src/func.rs @@ -262,6 +262,7 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { let ident_data = quote::format_ident!("{ident}_data"); quote! { #[doc(hidden)] + #[allow(non_snake_case)] #vis fn #ident_data() -> &'static #foundations::NativeFuncData { static DATA: #foundations::NativeFuncData = #data; &DATA diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs index 6b35f87f..9917f436 100644 --- a/crates/typst-macros/src/symbols.rs +++ b/crates/typst-macros/src/symbols.rs @@ -5,6 +5,8 @@ use syn::parse::{Parse, ParseStream, Parser}; use syn::punctuated::Punctuated; use syn::{Ident, LitChar, Path, Result, Token}; +use crate::util::foundations; + /// Expand the `symbols!` macro. pub fn symbols(stream: TokenStream) -> Result<TokenStream> { let list: Punctuated<Symbol, Token![,]> = @@ -14,7 +16,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> { let kind = match &symbol.kind { Kind::Single(c, h) => { let symbol = construct_sym_char(c, h); - quote! { ::typst::symbols::Symbol::single(#symbol), } + quote! { #foundations::Symbol::single(#symbol), } } Kind::Multiple(variants) => { let variants = variants.iter().map(|variant| { @@ -24,7 +26,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> { quote! { (#name, #symbol) } }); quote! { - ::typst::symbols::Symbol::list(&[#(#variants),*]) + #foundations::Symbol::list(&[#(#variants),*]) } } }; @@ -35,11 +37,11 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> { fn construct_sym_char(ch: &LitChar, handler: &Handler) -> TokenStream { match &handler.0 { - None => quote! { ::typst::symbols::SymChar::pure(#ch), }, + None => quote! { #foundations::SymChar::pure(#ch), }, Some(path) => quote! { - ::typst::symbols::SymChar::with_func( + #foundations::SymChar::with_func( #ch, - <#path as ::typst::foundations::NativeFunc>::func, + <#path as ::typst_library::foundations::NativeFunc>::func, ), }, } diff --git a/crates/typst-macros/src/time.rs b/crates/typst-macros/src/time.rs index b0758dc2..94fa4e5b 100644 --- a/crates/typst-macros/src/time.rs +++ b/crates/typst-macros/src/time.rs @@ -28,17 +28,15 @@ impl Parse for Meta { fn create(meta: Meta, mut item: syn::ItemFn) -> TokenStream { let name = meta.name.unwrap_or_else(|| item.sig.ident.to_string()); - let span = meta - .span - .as_ref() - .map(|span| quote! { Some(#span) }) - .unwrap_or_else(|| quote! { None }); + let construct = match meta.span.as_ref() { + Some(span) => quote! { with_span(#name, Some(#span.into_raw())) }, + None => quote! { new(#name) }, + }; item.block.stmts.insert( 0, parse_quote! { - #[cfg(not(target_arch = "wasm32"))] - let __scope = ::typst_timing::TimingScope::new(#name, #span); + let __scope = ::typst_timing::TimingScope::#construct; }, ); diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs index 4d8255e0..e8c0910b 100644 --- a/crates/typst-macros/src/util.rs +++ b/crates/typst-macros/src/util.rs @@ -200,13 +200,13 @@ impl<T: Parse> Parse for Array<T> { } } -/// Shorthand for `::typst::foundations`. +/// Shorthand for `::typst_library::foundations`. #[allow(non_camel_case_types)] pub struct foundations; impl quote::ToTokens for foundations { fn to_tokens(&self, tokens: &mut TokenStream) { - quote! { ::typst::foundations }.to_tokens(tokens); + quote! { ::typst_library::foundations }.to_tokens(tokens); } } diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index 64229945..4cef14bd 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -13,10 +13,13 @@ keywords = { workspace = true } readme = { workspace = true } [dependencies] -typst = { workspace = true } typst-assets = { workspace = true } +typst-library = { workspace = true } typst-macros = { workspace = true } +typst-syntax = { workspace = true } typst-timing = { workspace = true } +typst-utils = { workspace = true } +arrayvec = { workspace = true } base64 = { workspace = true } bytemuck = { workspace = true } comemo = { workspace = true } @@ -26,12 +29,10 @@ indexmap = { workspace = true } miniz_oxide = { workspace = true } once_cell = { workspace = true } pdf-writer = { workspace = true } -arrayvec = { workspace = true } serde = { workspace = true } subsetter = { workspace = true } svg2pdf = { workspace = true } ttf-parser = { workspace = true } -unscanny = { workspace = true } xmp-writer = { workspace = true } [lints] diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs index 7aba0eb6..1412afe6 100644 --- a/crates/typst-pdf/src/catalog.rs +++ b/crates/typst-pdf/src/catalog.rs @@ -4,11 +4,11 @@ use ecow::eco_format; use pdf_writer::types::Direction; use pdf_writer::writers::PageLabel; use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr}; -use typst::diag::{bail, SourceResult}; -use typst::foundations::{Datetime, Smart}; -use typst::layout::Dir; -use typst::syntax::Span; -use typst::text::Lang; +use typst_library::diag::{bail, SourceResult}; +use typst_library::foundations::{Datetime, Smart}; +use typst_library::layout::Dir; +use typst_library::text::Lang; +use typst_syntax::Span; use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter}; use crate::page::PdfPageLabel; diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs index a19d776a..26f2044c 100644 --- a/crates/typst-pdf/src/color.rs +++ b/crates/typst-pdf/src/color.rs @@ -1,9 +1,9 @@ use arrayvec::ArrayVec; use once_cell::sync::Lazy; use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref}; -use typst::diag::{bail, SourceResult}; -use typst::syntax::Span; -use typst::visualize::{Color, ColorSpace, Paint}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::visualize::{Color, ColorSpace, Paint}; +use typst_syntax::Span; use crate::{content, deflate, PdfChunk, PdfOptions, Renumber, WithResources}; diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs index f6fea396..1183e966 100644 --- a/crates/typst-pdf/src/color_font.rs +++ b/crates/typst-pdf/src/color_font.rs @@ -12,16 +12,15 @@ use indexmap::IndexMap; use pdf_writer::types::UnicodeCmap; use pdf_writer::writers::WMode; use pdf_writer::{Filter, Finish, Name, Rect, Ref}; -use typst::diag::{bail, error, SourceDiagnostic, SourceResult}; -use typst::foundations::Repr; -use typst::layout::Em; -use typst::text::color::glyph_frame; -use typst::text::{Font, Glyph, TextItemView}; +use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; +use typst_library::foundations::Repr; +use typst_library::layout::Em; +use typst_library::text::color::glyph_frame; +use typst_library::text::{Font, Glyph, TextItemView}; -use crate::content; use crate::font::{base_font_name, write_font_descriptor, CMAP_NAME, SYSTEM_INFO}; use crate::resources::{Resources, ResourcesRefs}; -use crate::{EmExt, PdfChunk, PdfOptions, WithGlobalRefs}; +use crate::{content, EmExt, PdfChunk, PdfOptions, WithGlobalRefs}; /// Write color fonts in the PDF document. /// diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index babb3e57..ce72365d 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -10,20 +10,20 @@ use pdf_writer::types::{ }; use pdf_writer::writers::PositionedItems; use pdf_writer::{Content, Finish, Name, Rect, Str}; -use typst::diag::{bail, error, SourceDiagnostic, SourceResult}; -use typst::foundations::Repr; -use typst::layout::{ +use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; +use typst_library::foundations::Repr; +use typst_library::layout::{ Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform, }; -use typst::model::Destination; -use typst::syntax::Span; -use typst::text::color::should_outline; -use typst::text::{Font, Glyph, TextItem, TextItemView}; -use typst::utils::{Deferred, Numeric, SliceExt}; -use typst::visualize::{ +use typst_library::model::Destination; +use typst_library::text::color::should_outline; +use typst_library::text::{Font, Glyph, TextItem, TextItemView}; +use typst_library::visualize::{ FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, }; +use typst_syntax::Span; +use typst_utils::{Deferred, Numeric, SliceExt}; use crate::color::PaintEncode; use crate::color_font::ColorFontMap; diff --git a/crates/typst-pdf/src/extg.rs b/crates/typst-pdf/src/extg.rs index 12bfa26a..06617d8d 100644 --- a/crates/typst-pdf/src/extg.rs +++ b/crates/typst-pdf/src/extg.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use pdf_writer::Ref; -use typst::diag::SourceResult; +use typst_library::diag::SourceResult; use crate::{PdfChunk, WithGlobalRefs}; diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs index c870d32a..93d75e50 100644 --- a/crates/typst-pdf/src/font.rs +++ b/crates/typst-pdf/src/font.rs @@ -8,10 +8,10 @@ use pdf_writer::writers::{FontDescriptor, WMode}; use pdf_writer::{Chunk, Filter, Finish, Name, Rect, Ref, Str}; use subsetter::GlyphRemapper; use ttf_parser::{name_id, GlyphId, Tag}; -use typst::diag::{At, SourceResult}; -use typst::syntax::Span; -use typst::text::Font; -use typst::utils::SliceExt; +use typst_library::diag::{At, SourceResult}; +use typst_library::text::Font; +use typst_syntax::Span; +use typst_utils::SliceExt; use crate::{deflate, EmExt, NameExt, PdfChunk, WithGlobalRefs}; @@ -249,7 +249,7 @@ pub(crate) fn base_font_name<T: Hash>(font: &Font, glyphs: &T) -> EcoString { /// Produce a unique 6 letter tag for a glyph set. pub(crate) fn subset_tag<T: Hash>(glyphs: &T) -> EcoString { const BASE: u128 = 26; - let mut hash = typst::utils::hash128(&glyphs); + let mut hash = typst_utils::hash128(&glyphs); let mut letter = [b'A'; SUBSET_TAG_LEN]; for l in letter.iter_mut() { *l = b'A' + (hash % BASE) as u8; diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs index be0a3ea0..6cd4c1ae 100644 --- a/crates/typst-pdf/src/gradient.rs +++ b/crates/typst-pdf/src/gradient.rs @@ -6,18 +6,17 @@ use ecow::eco_format; use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType}; use pdf_writer::writers::StreamShadingType; use pdf_writer::{Filter, Finish, Name, Ref}; -use typst::diag::SourceResult; -use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; -use typst::utils::Numeric; -use typst::visualize::{ +use typst_library::diag::SourceResult; +use typst_library::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; +use typst_library::visualize::{ Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor, }; +use typst_utils::Numeric; use crate::color::{ self, check_cmyk_allowed, ColorSpaceExt, PaintEncode, QuantizedColor, }; -use crate::{content, WithGlobalRefs}; -use crate::{deflate, transform_to_array, AbsExt, PdfChunk}; +use crate::{content, deflate, transform_to_array, AbsExt, PdfChunk, WithGlobalRefs}; /// A unique-transform-aspect-ratio combination that will be encoded into the /// PDF. diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index bff09e09..9651d31b 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -4,11 +4,11 @@ use std::io::Cursor; use ecow::eco_format; use image::{DynamicImage, GenericImageView, Rgba}; use pdf_writer::{Chunk, Filter, Finish, Ref}; -use typst::diag::{At, SourceResult, StrResult}; -use typst::utils::Deferred; -use typst::visualize::{ +use typst_library::diag::{At, SourceResult, StrResult}; +use typst_library::visualize::{ ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage, }; +use typst_utils::Deferred; use crate::{color, deflate, PdfChunk, WithGlobalRefs}; diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 7df77f10..efc99b74 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -22,14 +22,14 @@ use std::ops::{Deref, DerefMut}; use base64::Engine; use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; use serde::{Deserialize, Serialize}; -use typst::diag::{bail, SourceResult, StrResult}; -use typst::foundations::{Datetime, Smart}; -use typst::layout::{Abs, Em, PageRanges, Transform}; -use typst::model::Document; -use typst::syntax::Span; -use typst::text::Font; -use typst::utils::Deferred; -use typst::visualize::Image; +use typst_library::diag::{bail, SourceResult, StrResult}; +use typst_library::foundations::{Datetime, Smart}; +use typst_library::layout::{Abs, Em, PageRanges, Transform}; +use typst_library::model::Document; +use typst_library::text::Font; +use typst_library::visualize::Image; +use typst_syntax::Span; +use typst_utils::Deferred; use crate::catalog::write_catalog; use crate::color::{alloc_color_functions_refs, ColorFunctionRefs}; @@ -518,7 +518,7 @@ fn deflate_deferred(content: Vec<u8>) -> Deferred<Vec<u8>> { /// Create a base64-encoded hash of the value. fn hash_base64<T: Hash>(value: &T) -> String { base64::engine::general_purpose::STANDARD - .encode(typst::utils::hash128(value).to_be_bytes()) + .encode(typst_utils::hash128(value).to_be_bytes()) } /// Additional methods for [`Abs`]. diff --git a/crates/typst-pdf/src/named_destination.rs b/crates/typst-pdf/src/named_destination.rs index 2d893526..90552335 100644 --- a/crates/typst-pdf/src/named_destination.rs +++ b/crates/typst-pdf/src/named_destination.rs @@ -2,11 +2,11 @@ use std::collections::{HashMap, HashSet}; use pdf_writer::writers::Destination; use pdf_writer::{Ref, Str}; -use typst::diag::SourceResult; -use typst::foundations::{Label, NativeElement}; -use typst::introspection::Location; -use typst::layout::Abs; -use typst::model::HeadingElem; +use typst_library::diag::SourceResult; +use typst_library::foundations::{Label, NativeElement}; +use typst_library::introspection::Location; +use typst_library::layout::Abs; +use typst_library::model::HeadingElem; use crate::{AbsExt, PdfChunk, Renumber, StrExt, WithGlobalRefs}; diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index 5c099b89..b9e71319 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -1,9 +1,9 @@ use std::num::NonZeroUsize; use pdf_writer::{Finish, Pdf, Ref, TextStr}; -use typst::foundations::{NativeElement, Packed, StyleChain}; -use typst::layout::Abs; -use typst::model::HeadingElem; +use typst_library::foundations::{NativeElement, Packed, StyleChain}; +use typst_library::layout::Abs; +use typst_library::model::HeadingElem; use crate::{AbsExt, TextStrExt, WithEverything}; diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 631eec11..27daf6c9 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -4,15 +4,15 @@ use std::num::NonZeroUsize; use ecow::EcoString; use pdf_writer::types::{ActionType, AnnotationFlags, AnnotationType, NumberingStyle}; use pdf_writer::{Filter, Finish, Name, Rect, Ref, Str}; -use typst::diag::SourceResult; -use typst::foundations::Label; -use typst::introspection::Location; -use typst::layout::{Abs, Page}; -use typst::model::{Destination, Numbering}; +use typst_library::diag::SourceResult; +use typst_library::foundations::Label; +use typst_library::introspection::Location; +use typst_library::layout::{Abs, Page}; +use typst_library::model::{Destination, Numbering}; -use crate::content; use crate::{ - AbsExt, PdfChunk, PdfOptions, Resources, WithDocument, WithRefs, WithResources, + content, AbsExt, PdfChunk, PdfOptions, Resources, WithDocument, WithRefs, + WithResources, }; /// Construct page objects. @@ -252,7 +252,8 @@ impl PdfPageLabel { // If there is a suffix, we cannot use the common style optimisation, // since PDF does not provide a suffix field. let style = if pat.suffix.is_empty() { - use {typst::model::NumberingKind as Kind, PdfPageLabelStyle as Style}; + use typst_library::model::NumberingKind as Kind; + use PdfPageLabelStyle as Style; match kind { Kind::Arabic => Some(Style::Arabic), Kind::LowerRoman => Some(Style::LowerRoman), diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs index fd9d9dbb..ddc22fbd 100644 --- a/crates/typst-pdf/src/pattern.rs +++ b/crates/typst-pdf/src/pattern.rs @@ -3,15 +3,14 @@ use std::collections::HashMap; use ecow::eco_format; use pdf_writer::types::{ColorSpaceOperand, PaintType, TilingType}; use pdf_writer::{Filter, Name, Rect, Ref}; -use typst::diag::SourceResult; -use typst::layout::{Abs, Ratio, Transform}; -use typst::utils::Numeric; -use typst::visualize::{Pattern, RelativeTo}; +use typst_library::diag::SourceResult; +use typst_library::layout::{Abs, Ratio, Transform}; +use typst_library::visualize::{Pattern, RelativeTo}; +use typst_utils::Numeric; use crate::color::PaintEncode; -use crate::content; use crate::resources::{Remapper, ResourcesRefs}; -use crate::{transform_to_array, PdfChunk, Resources, WithGlobalRefs}; +use crate::{content, transform_to_array, PdfChunk, Resources, WithGlobalRefs}; /// Writes the actual patterns (tiling patterns) to the PDF. /// This is performed once after writing all pages. diff --git a/crates/typst-pdf/src/resources.rs b/crates/typst-pdf/src/resources.rs index fabf0b3f..d3fde5dd 100644 --- a/crates/typst-pdf/src/resources.rs +++ b/crates/typst-pdf/src/resources.rs @@ -12,11 +12,11 @@ use std::hash::Hash; use ecow::{eco_format, EcoString}; use pdf_writer::{Dict, Finish, Name, Ref}; use subsetter::GlyphRemapper; -use typst::diag::{SourceResult, StrResult}; -use typst::syntax::Span; -use typst::text::{Font, Lang}; -use typst::utils::Deferred; -use typst::visualize::Image; +use typst_library::diag::{SourceResult, StrResult}; +use typst_library::text::{Font, Lang}; +use typst_library::visualize::Image; +use typst_syntax::Span; +use typst_utils::Deferred; use crate::color::ColorSpaces; use crate::color_font::ColorFontMap; diff --git a/crates/typst-realize/Cargo.toml b/crates/typst-realize/Cargo.toml new file mode 100644 index 00000000..78bda259 --- /dev/null +++ b/crates/typst-realize/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "typst-realize" +description = "Typst's realization subsystem." +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-library = { workspace = true } +typst-macros = { workspace = true } +typst-syntax = { workspace = true } +typst-timing = { workspace = true } +typst-utils = { workspace = true } +arrayvec = { workspace = true } +bumpalo = { workspace = true } +comemo = { workspace = true } +ecow = { workspace = true } +once_cell = { workspace = true } +regex = { workspace = true } + +[lints] +workspace = true diff --git a/crates/typst/src/realize.rs b/crates/typst-realize/src/lib.rs index b6a969ba..58668962 100644 --- a/crates/typst/src/realize.rs +++ b/crates/typst-realize/src/lib.rs @@ -1,7 +1,8 @@ -//! Realization of content. +//! Typst's realization subsystem. //! //! *Realization* is the process of recursively applying styling and, in -//! particular, show rules to produce well-known elements that can be laid out. +//! particular, show rules to produce well-known elements that can be processed +//! further. use std::borrow::Cow; @@ -10,29 +11,26 @@ use bumpalo::collections::{String as BumpString, Vec as BumpVec}; use comemo::Track; use ecow::EcoString; use once_cell::unsync::Lazy; - -use crate::diag::{bail, At, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ +use typst_library::diag::{bail, At, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{ Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles, Synthesize, Transformation, }; -use crate::introspection::{Locatable, SplitLocator, Tag, TagElem}; -use crate::layout::{ +use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; +use typst_library::layout::{ AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, }; -use crate::math::{EquationElem, LayoutMath}; -use crate::model::{ - CiteElem, CiteGroup, DocumentElem, DocumentInfo, EnumElem, ListElem, ListItemLike, - ListLike, ParElem, ParbreakElem, TermsElem, +use typst_library::math::{EquationElem, Mathy}; +use typst_library::model::{ + CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike, + ParElem, ParbreakElem, TermsElem, }; -use crate::syntax::Span; -use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use crate::utils::{SliceExt, SmallBitSet}; - -/// A pair of content and a style chain that applies to it. -pub type Pair<'a> = (&'a Content, StyleChain<'a>); +use typst_library::routines::{Arenas, Pair, RealizationKind}; +use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; +use typst_syntax::Span; +use typst_utils::{SliceExt, SmallBitSet}; /// Realize content into a flat list of well-known, styled items. #[typst_macros::time(name = "realize")] @@ -65,30 +63,6 @@ pub fn realize<'a>( Ok(s.sink) } -/// Defines what kind of realization we are performing. -pub enum RealizationKind<'a> { - /// This the root realization for the document. Requires a mutable reference - /// to document metadata that will be filled from `set document` rules. - Root(&'a mut DocumentInfo), - /// A nested realization in a container (e.g. a `block`). - Container, - /// A realization within math. - Math, -} - -/// Temporary storage arenas for lifetime extension during realization. -/// -/// Must be kept live while the content returned from realization is processed. -#[derive(Default)] -pub struct Arenas { - /// A typed arena for owned content. - pub content: typed_arena::Arena<Content>, - /// A typed arena for owned styles. - pub styles: typed_arena::Arena<Styles>, - /// An untyped arena for everything that is `Copy`. - pub bump: bumpalo::Bump, -} - /// Mutable state for realization. /// /// Sadly, we need that many lifetimes because &mut references are invariant and @@ -323,8 +297,8 @@ fn visit_math_rules<'a>( } } } else { - // Transparently wrap math-y content into equations. - if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() { + // Transparently wrap mathy content into equations. + if content.can::<dyn Mathy>() && !content.is::<EquationElem>() { let eq = EquationElem::new(content.clone()).pack().spanned(content.span()); visit(s, s.store(eq), styles)?; return Ok(true); @@ -523,7 +497,7 @@ fn prepare( // // The element could already have a location even if it is not prepared // when it stems from a query. - let key = crate::utils::hash128(&target); + let key = typst_utils::hash128(&target); if target.location().is_none() && (target.can::<dyn Locatable>() || target.label().is_some()) { diff --git a/crates/typst-render/Cargo.toml b/crates/typst-render/Cargo.toml index 56a18e80..7d01d7e3 100644 --- a/crates/typst-render/Cargo.toml +++ b/crates/typst-render/Cargo.toml @@ -13,7 +13,7 @@ keywords = { workspace = true } readme = { workspace = true } [dependencies] -typst = { workspace = true } +typst-library = { workspace = true } typst-macros = { workspace = true } typst-timing = { workspace = true } bytemuck = { workspace = true } @@ -21,10 +21,8 @@ comemo = { workspace = true } image = { workspace = true } pixglyph = { workspace = true } resvg = { workspace = true } -roxmltree = { workspace = true } tiny-skia = { workspace = true } ttf-parser = { workspace = true } -usvg = { workspace = true } [lints] workspace = true diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index dcbf7982..27b03911 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; use tiny_skia as sk; -use typst::layout::Size; -use typst::visualize::{Image, ImageKind}; +use typst_library::layout::Size; +use typst_library::visualize::{Image, ImageKind}; use crate::{AbsExt, State}; diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index d5eeacce..8de3852d 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -6,11 +6,11 @@ mod shape; mod text; use tiny_skia as sk; -use typst::layout::{ +use typst_library::layout::{ Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Size, Transform, }; -use typst::model::Document; -use typst::visualize::{Color, Geometry, Paint}; +use typst_library::model::Document; +use typst_library::visualize::{Color, Geometry, Paint}; /// Export a page into a raster image. /// diff --git a/crates/typst-render/src/paint.rs b/crates/typst-render/src/paint.rs index 3a507ca4..689f28a8 100644 --- a/crates/typst-render/src/paint.rs +++ b/crates/typst-render/src/paint.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use tiny_skia as sk; -use typst::layout::{Axes, Point, Ratio, Size}; -use typst::visualize::{Color, Gradient, Paint, Pattern, RelativeTo}; +use typst_library::layout::{Axes, Point, Ratio, Size}; +use typst_library::visualize::{Color, Gradient, Paint, Pattern, RelativeTo}; use crate::{AbsExt, State}; diff --git a/crates/typst-render/src/shape.rs b/crates/typst-render/src/shape.rs index f31262ef..f9ff7f3a 100644 --- a/crates/typst-render/src/shape.rs +++ b/crates/typst-render/src/shape.rs @@ -1,6 +1,6 @@ use tiny_skia as sk; -use typst::layout::{Abs, Axes, Point, Ratio, Size}; -use typst::visualize::{ +use typst_library::layout::{Abs, Axes, Point, Ratio, Size}; +use typst_library::visualize::{ DashPattern, FillRule, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem, Shape, }; diff --git a/crates/typst-render/src/text.rs b/crates/typst-render/src/text.rs index 1685e67d..c017b1ce 100644 --- a/crates/typst-render/src/text.rs +++ b/crates/typst-render/src/text.rs @@ -3,10 +3,10 @@ use std::sync::Arc; use pixglyph::Bitmap; use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; -use typst::layout::{Abs, Axes, Point, Size}; -use typst::text::color::{glyph_frame, should_outline}; -use typst::text::{Font, TextItem}; -use typst::visualize::{FixedStroke, Paint}; +use typst_library::layout::{Abs, Axes, Point, Size}; +use typst_library::text::color::{glyph_frame, should_outline}; +use typst_library::text::{Font, TextItem}; +use typst_library::visualize::{FixedStroke, Paint}; use crate::paint::{self, GradientSampler, PaintSampler, PatternSampler}; use crate::{shape, AbsExt, State}; diff --git a/crates/typst-svg/Cargo.toml b/crates/typst-svg/Cargo.toml index df49a2b1..41d35565 100644 --- a/crates/typst-svg/Cargo.toml +++ b/crates/typst-svg/Cargo.toml @@ -13,9 +13,10 @@ keywords = { workspace = true } readme = { workspace = true } [dependencies] -typst = { workspace = true } +typst-library = { workspace = true } typst-macros = { workspace = true } typst-timing = { workspace = true } +typst-utils = { workspace = true } base64 = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 87bfbd27..ede4e76e 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,7 +1,7 @@ use base64::Engine; use ecow::{eco_format, EcoString}; -use typst::layout::{Abs, Axes}; -use typst::visualize::{Image, ImageFormat, RasterFormat, VectorFormat}; +use typst_library::layout::{Abs, Axes}; +use typst_library::visualize::{Image, ImageFormat, RasterFormat, VectorFormat}; use crate::SVGRenderer; diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index deb5fb00..0ae7b2da 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -10,12 +10,12 @@ use std::fmt::{self, Display, Formatter, Write}; use ecow::EcoString; use ttf_parser::OutlineBuilder; -use typst::layout::{ +use typst_library::layout::{ Abs, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Ratio, Size, Transform, }; -use typst::model::Document; -use typst::utils::hash128; -use typst::visualize::{Geometry, Gradient, Pattern}; +use typst_library::model::Document; +use typst_library::visualize::{Geometry, Gradient, Pattern}; +use typst_utils::hash128; use xmlwriter::XmlWriter; use crate::paint::{GradientRef, PatternRef, SVGSubGradient}; diff --git a/crates/typst-svg/src/paint.rs b/crates/typst-svg/src/paint.rs index 364cdd23..58348c94 100644 --- a/crates/typst-svg/src/paint.rs +++ b/crates/typst-svg/src/paint.rs @@ -2,10 +2,10 @@ use std::f32::consts::TAU; use ecow::{eco_format, EcoString}; use ttf_parser::OutlineBuilder; -use typst::foundations::Repr; -use typst::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform}; -use typst::utils::hash128; -use typst::visualize::{Color, FillRule, Gradient, Paint, Pattern, RatioOrAngle}; +use typst_library::foundations::Repr; +use typst_library::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform}; +use typst_library::visualize::{Color, FillRule, Gradient, Paint, Pattern, RatioOrAngle}; +use typst_utils::hash128; use xmlwriter::XmlWriter; use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder}; diff --git a/crates/typst-svg/src/shape.rs b/crates/typst-svg/src/shape.rs index 12be2e22..e188ed8b 100644 --- a/crates/typst-svg/src/shape.rs +++ b/crates/typst-svg/src/shape.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use ttf_parser::OutlineBuilder; -use typst::layout::{Abs, Ratio, Size, Transform}; -use typst::visualize::{ +use typst_library::layout::{Abs, Ratio, Size, Transform}; +use typst_library::visualize::{ FixedStroke, Geometry, LineCap, LineJoin, Paint, Path, PathItem, RelativeTo, Shape, }; diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 4a99b4b8..76cd53cb 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -3,10 +3,10 @@ use std::io::Read; use base64::Engine; use ecow::EcoString; use ttf_parser::GlyphId; -use typst::layout::{Abs, Point, Ratio, Size, Transform}; -use typst::text::{Font, TextItem}; -use typst::utils::hash128; -use typst::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; +use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; +use typst_library::text::{Font, TextItem}; +use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; +use typst_utils::hash128; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml index e9c39992..3dc983eb 100644 --- a/crates/typst-syntax/Cargo.toml +++ b/crates/typst-syntax/Cargo.toml @@ -13,6 +13,7 @@ keywords = { workspace = true } readme = { workspace = true } [dependencies] +typst-timing = { workspace = true } typst-utils = { workspace = true } ecow = { workspace = true } once_cell = { workspace = true } diff --git a/crates/typst-syntax/src/file.rs b/crates/typst-syntax/src/file.rs index 89aaa55e..bc7dd314 100644 --- a/crates/typst-syntax/src/file.rs +++ b/crates/typst-syntax/src/file.rs @@ -97,12 +97,16 @@ impl FileId { } /// Construct from a raw number. - pub(crate) const fn from_raw(v: u16) -> Self { + /// + /// Should only be used with numbers retrieved via + /// [`into_raw`](Self::into_raw). Misuse may results in panics, but no + /// unsafety. + pub const fn from_raw(v: u16) -> Self { Self(v) } /// Extract the raw underlying number. - pub(crate) const fn into_raw(self) -> u16 { + pub const fn into_raw(self) -> u16 { self.0 } diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs index ddd29326..de8ed65c 100644 --- a/crates/typst-syntax/src/highlight.rs +++ b/crates/typst-syntax/src/highlight.rs @@ -411,9 +411,10 @@ fn highlight_html_impl(html: &mut String, node: &LinkedNode) { #[cfg(test)] mod tests { - use super::*; use std::ops::Range; + use super::*; + #[test] fn test_highlighting() { use Tag::*; diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index dba6d69d..8c783ffe 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -12,6 +12,7 @@ use crate::{ /// Parses a source file. pub fn parse(text: &str) -> SyntaxNode { + let _scope = typst_timing::TimingScope::new("parse"); let mut p = Parser::new(text, 0, LexMode::Markup); markup(&mut p, true, 0, |_| false); p.finish().into_iter().next().unwrap() @@ -19,6 +20,7 @@ pub fn parse(text: &str) -> SyntaxNode { /// Parses top-level code. pub fn parse_code(text: &str) -> SyntaxNode { + let _scope = typst_timing::TimingScope::new("parse code"); let mut p = Parser::new(text, 0, LexMode::Code); let m = p.marker(); p.skip(); @@ -29,6 +31,7 @@ pub fn parse_code(text: &str) -> SyntaxNode { /// Parses top-level math. pub fn parse_math(text: &str) -> SyntaxNode { + let _scope = typst_timing::TimingScope::new("parse math"); let mut p = Parser::new(text, 0, LexMode::Math); math(&mut p, |_| false); p.finish().into_iter().next().unwrap() diff --git a/crates/typst-syntax/src/source.rs b/crates/typst-syntax/src/source.rs index a2ccb5bb..3454a265 100644 --- a/crates/typst-syntax/src/source.rs +++ b/crates/typst-syntax/src/source.rs @@ -32,6 +32,7 @@ struct Repr { impl Source { /// Create a new source file. pub fn new(id: FileId, text: String) -> Self { + let _scope = typst_timing::TimingScope::new("create source"); let mut root = parse(&text); root.numberize(id, Span::FULL).unwrap(); Self(Arc::new(Repr { @@ -75,6 +76,7 @@ impl Source { /// /// Returns the range in the new source that was ultimately reparsed. pub fn replace(&mut self, new: &str) -> Range<usize> { + let _scope = typst_timing::TimingScope::new("replace source"); let old = self.text(); let mut prefix = diff --git a/crates/typst-syntax/src/span.rs b/crates/typst-syntax/src/span.rs index 85745f60..0847ceea 100644 --- a/crates/typst-syntax/src/span.rs +++ b/crates/typst-syntax/src/span.rs @@ -83,6 +83,20 @@ impl Span { self.0.get() & ((1 << Self::BITS) - 1) } + /// Construct from a raw number. + /// + /// Should only be used with numbers retrieved via + /// [`into_raw`](Self::into_raw). Misuse may results in panics, but no + /// unsafety. + pub const fn from_raw(v: NonZeroU64) -> Self { + Self(v) + } + + /// Extract the raw underlying number. + pub const fn into_raw(self) -> NonZeroU64 { + self.0 + } + /// Return `other` if `self` is detached and `self` otherwise. pub fn or(self, other: Self) -> Self { if self.is_detached() { diff --git a/crates/typst-timing/Cargo.toml b/crates/typst-timing/Cargo.toml index 525d65c5..2d42269f 100644 --- a/crates/typst-timing/Cargo.toml +++ b/crates/typst-timing/Cargo.toml @@ -13,7 +13,6 @@ keywords = { workspace = true } readme = { workspace = true } [dependencies] -typst-syntax = { workspace = true } parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/typst-timing/src/lib.rs b/crates/typst-timing/src/lib.rs index 4e711d5f..0df65443 100644 --- a/crates/typst-timing/src/lib.rs +++ b/crates/typst-timing/src/lib.rs @@ -2,14 +2,15 @@ use std::hash::Hash; use std::io::Write; -use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::num::NonZeroU64; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; use std::thread::ThreadId; use std::time::{Duration, SystemTime}; use parking_lot::Mutex; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -use typst_syntax::Span; /// Whether the timer is enabled. Defaults to `false`. static ENABLED: AtomicBool = AtomicBool::new(false); @@ -43,8 +44,8 @@ struct Event { id: u64, /// The name of this event. name: &'static str, - /// The span of code that this event was recorded in. - span: Option<Span>, + /// The raw value of the span of code that this event was recorded in. + span: Option<NonZeroU64>, /// The thread ID of this event. thread_id: ThreadId, } @@ -79,18 +80,34 @@ pub fn clear() { /// A scope that records an event when it is dropped. pub struct TimingScope { name: &'static str, - span: Option<Span>, + span: Option<NonZeroU64>, id: u64, thread_id: ThreadId, } impl TimingScope { /// Create a new scope if timing is enabled. - pub fn new(name: &'static str, span: Option<Span>) -> Option<Self> { - if !is_enabled() { - return None; + #[inline] + pub fn new(name: &'static str) -> Option<Self> { + Self::with_span(name, None) + } + + /// Create a new scope with a span if timing is enabled. + /// + /// The span is a raw number because `typst-timing` can't depend on + /// `typst-syntax` (or else `typst-syntax` couldn't depend on + /// `typst-timing`). + #[inline] + pub fn with_span(name: &'static str, span: Option<NonZeroU64>) -> Option<Self> { + #[cfg(not(target_arch = "wasm32"))] + if is_enabled() { + return Some(Self::new_impl(name, span)); } + None + } + /// Create a new scope without checking if timing is enabled. + fn new_impl(name: &'static str, span: Option<NonZeroU64>) -> Self { let timestamp = SystemTime::now(); let thread_id = std::thread::current().id(); @@ -106,7 +123,7 @@ impl TimingScope { thread_id, }); - Some(TimingScope { name, span, id, thread_id }) + Self { name, span, id, thread_id } } } @@ -151,11 +168,11 @@ impl Drop for TimingScope { #[macro_export] macro_rules! timed { ($name:expr, span = $span:expr, $body:expr $(,)?) => {{ - let __scope = $crate::TimingScope::new($name, Some($span)); + let __scope = $crate::TimingScope::with_span($name, Some($span)); $body }}; ($name:expr, $body:expr $(,)?) => {{ - let __scope = $crate::TimingScope::new($name, None); + let __scope = $crate::TimingScope::new($name); $body }}; } @@ -167,7 +184,7 @@ macro_rules! timed { /// the second element is the line number. pub fn export_json<W: Write>( writer: W, - mut source: impl FnMut(Span) -> (String, u32), + mut source: impl FnMut(NonZeroU64) -> (String, u32), ) -> Result<(), String> { #[derive(Serialize)] struct Entry { diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 8488b72d..e199e1bb 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -20,6 +20,9 @@ pub use self::pico::PicoStr; pub use self::round::{round_int_with_precision, round_with_precision}; pub use self::scalar::Scalar; +#[doc(hidden)] +pub use once_cell; + use std::fmt::{Debug, Formatter}; use std::hash::Hash; use std::iter::{Chain, Flatten, Rev}; @@ -29,9 +32,6 @@ use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher13}; -#[doc(hidden)] -pub use once_cell; - /// Turn a closure into a struct implementing [`Debug`]. pub fn debug<F>(f: F) -> impl Debug where diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index a3e21123..6158d791 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -13,72 +13,16 @@ license = { workspace = true } readme = { workspace = true } [dependencies] -typst-assets = { workspace = true } +typst-eval = { workspace = true } +typst-layout = { workspace = true } +typst-library = { workspace = true } typst-macros = { workspace = true } +typst-realize = { workspace = true } typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } -arrayvec = { 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 } -hypher = { workspace = true } -icu_properties = { workspace = true } -icu_provider = { workspace = true } -icu_provider_adapters = { workspace = true } -icu_provider_blob = { workspace = true } -icu_segmenter = { workspace = true } -if_chain = { workspace = true } -image = { workspace = true } -indexmap = { workspace = true } -kamadak-exif = { workspace = true } -kurbo = { workspace = true } -lipsum = { workspace = true } -log = { workspace = true } -once_cell = { workspace = true } -palette = { workspace = true } -qcms = { workspace = true } -phf = { workspace = true } -png = { workspace = true } -portable-atomic = { 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-bidi = { workspace = true } -unicode-math-class = { workspace = true } -unicode-script = { workspace = true } -unicode-segmentation = { workspace = true } -unscanny = { workspace = true } -usvg = { workspace = true } -xmlwriter = { workspace = true } -wasmi = { workspace = true } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -stacker = { workspace = true } - -[dev-dependencies] -typst-dev-assets = { workspace = true } [lints] workspace = true diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs deleted file mode 100644 index d97edd5a..00000000 --- a/crates/typst/src/layout/container.rs +++ /dev/null @@ -1,1046 +0,0 @@ -use once_cell::unsync::Lazy; -use smallvec::SmallVec; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Resolve, - Smart, StyleChain, Value, -}; -use crate::introspection::Locator; -use crate::layout::{ - layout_fragment, layout_frame, Abs, Axes, Corners, Em, Fr, Fragment, Frame, - FrameKind, Length, Region, Regions, Rel, Sides, Size, Spacing, -}; -use crate::utils::Numeric; -use crate::visualize::{clip_rect, Paint, Stroke}; - -/// An inline-level container that sizes content. -/// -/// All elements except inline math, text, and boxes are block-level and cannot -/// occur inside of a paragraph. The box function can be used to integrate such -/// elements into a paragraph. Boxes take the size of their contents by default -/// but can also be sized explicitly. -/// -/// # Example -/// ```example -/// Refer to the docs -/// #box( -/// height: 9pt, -/// image("docs.svg") -/// ) -/// for more information. -/// ``` -#[elem] -pub struct BoxElem { - /// The width of the box. - /// - /// Boxes can have [fractional]($fraction) widths, as the example below - /// demonstrates. - /// - /// _Note:_ Currently, only boxes and only their widths might be fractionally - /// sized within paragraphs. Support for fractionally sized images, shapes, - /// and more might be added in the future. - /// - /// ```example - /// Line in #box(width: 1fr, line(length: 100%)) between. - /// ``` - pub width: Sizing, - - /// The height of the box. - pub height: Smart<Rel<Length>>, - - /// An amount to shift the box's baseline by. - /// - /// ```example - /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). - /// ``` - #[resolve] - pub baseline: Rel<Length>, - - /// The box's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option<Paint>, - - /// The box's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides<Option<Option<Stroke>>>, - - /// How much to round the box's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the box's content. - /// - /// _Note:_ When the box contains text, its exact size depends on the - /// current [text edges]($text.top-edge). - /// - /// ```example - /// #rect(inset: 0pt)[Tight] - /// ``` - #[resolve] - #[fold] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the box's size without affecting the layout. - /// - /// This is useful to prevent padding from affecting line layout. For a - /// generalized version of the example below, see the documentation for the - /// [raw text's block parameter]($raw.block). - /// - /// ```example - /// An inline - /// #box( - /// fill: luma(235), - /// inset: (x: 3pt, y: 0pt), - /// outset: (y: 3pt), - /// radius: 2pt, - /// )[rectangle]. - /// ``` - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// Whether to clip the content inside the box. - /// - /// Clipping is useful when the box's content is larger than the box itself, - /// as any content that exceeds the box's bounds will be hidden. - /// - /// ```example - /// #box( - /// width: 50pt, - /// height: 50pt, - /// clip: true, - /// image("tiger.jpg", width: 100pt, height: 100pt) - /// ) - /// ``` - #[default(false)] - pub clip: bool, - - /// The contents of the box. - #[positional] - #[borrowed] - pub body: Option<Content>, -} - -impl Packed<BoxElem> { - /// Layout this box as part of a paragraph. - #[typst_macros::time(name = "box", span = self.span())] - pub fn layout( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult<Frame> { - // Fetch sizing properties. - let width = self.width(styles); - let height = self.height(styles); - let inset = self.inset(styles).unwrap_or_default(); - - // Build the pod region. - let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region); - - // Layout the body. - let mut frame = match self.body(styles) { - // If we have no body, just create an empty frame. If necessary, - // its size will be adjusted below. - None => Frame::hard(Size::zero()), - - // If we have a child, layout it into the body. Boxes are boundaries - // for gradient relativeness, so we set the `FrameKind` to `Hard`. - Some(body) => layout_frame(engine, body, locator, styles, pod)? - .with_kind(FrameKind::Hard), - }; - - // Enforce a correct frame size on the expanded axes. Do this before - // applying the inset, since the pod shrunk. - frame.set_size(pod.expand.select(pod.size, frame.size())); - - // Apply the inset. - if !inset.is_zero() { - crate::layout::grow(&mut frame, &inset); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self - .stroke(styles) - .unwrap_or_default() - .map(|s| s.map(Stroke::unwrap_or_default)); - - // Only fetch these if necessary (for clipping or filling/stroking). - let outset = Lazy::new(|| self.outset(styles).unwrap_or_default()); - let radius = Lazy::new(|| self.radius(styles).unwrap_or_default()); - - // Clip the contents, if requested. - if self.clip(styles) { - let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); - frame.clip(clip_rect(size, &radius, &stroke)); - } - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span()); - } - - // Assign label to the frame. - if let Some(label) = self.label() { - frame.label(label); - } - - // Apply baseline shift. Do this after setting the size and applying the - // inset, so that a relative shift is resolved relative to the final - // height. - let shift = self.baseline(styles).relative_to(frame.height()); - if !shift.is_zero() { - frame.set_baseline(frame.baseline() - shift); - } - - Ok(frame) - } -} - -/// An inline-level container that can produce arbitrary items that can break -/// across lines. -#[elem(Construct)] -pub struct InlineElem { - /// A callback that is invoked with the regions to produce arbitrary - /// inline items. - #[required] - #[internal] - body: callbacks::InlineCallback, -} - -impl Construct for InlineElem { - fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> { - bail!(args.span, "cannot be constructed manually"); - } -} - -impl InlineElem { - /// Create an inline-level item with a custom layouter. - #[allow(clippy::type_complexity)] - pub fn layouter<T: NativeElement>( - captured: Packed<T>, - callback: fn( - content: &Packed<T>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult<Vec<InlineItem>>, - ) -> Self { - Self::new(callbacks::InlineCallback::new(captured, callback)) - } -} - -impl Packed<InlineElem> { - /// Layout the element. - pub fn layout( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult<Vec<InlineItem>> { - self.body().call(engine, locator, styles, region) - } -} - -/// Layouted items suitable for placing in a paragraph. -#[derive(Debug, Clone)] -pub enum InlineItem { - /// Absolute spacing between other items, and whether it is weak. - Space(Abs, bool), - /// Layouted inline-level content. - Frame(Frame), -} - -/// A block-level container. -/// -/// Such a container can be used to separate content, size it, and give it a -/// background or border. -/// -/// # Examples -/// With a block, you can give a background to content while still allowing it -/// to break across multiple pages. -/// ```example -/// #set page(height: 100pt) -/// #block( -/// fill: luma(230), -/// inset: 8pt, -/// radius: 4pt, -/// lorem(30), -/// ) -/// ``` -/// -/// Blocks are also useful to force elements that would otherwise be inline to -/// become block-level, especially when writing show rules. -/// ```example -/// #show heading: it => it.body -/// = Blockless -/// More text. -/// -/// #show heading: it => block(it.body) -/// = Blocky -/// More text. -/// ``` -#[elem] -pub struct BlockElem { - /// The block's width. - /// - /// ```example - /// #set align(center) - /// #block( - /// width: 60%, - /// inset: 8pt, - /// fill: silver, - /// lorem(10), - /// ) - /// ``` - pub width: Smart<Rel<Length>>, - - /// The block's height. When the height is larger than the remaining space - /// on a page and [`breakable`]($block.breakable) is `{true}`, the - /// block will continue on the next page with the remaining height. - /// - /// ```example - /// #set page(height: 80pt) - /// #set align(center) - /// #block( - /// width: 80%, - /// height: 150%, - /// fill: aqua, - /// ) - /// ``` - pub height: Sizing, - - /// Whether the block can be broken and continue on the next page. - /// - /// ```example - /// #set page(height: 80pt) - /// The following block will - /// jump to its own page. - /// #block( - /// breakable: false, - /// lorem(15), - /// ) - /// ``` - #[default(true)] - pub breakable: bool, - - /// The block's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option<Paint>, - - /// The block's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides<Option<Option<Stroke>>>, - - /// How much to round the block's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the block's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the block's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The spacing around the block. When `{auto}`, inherits the paragraph - /// [`spacing`]($par.spacing). - /// - /// For two adjacent blocks, the larger of the first block's `above` and the - /// second block's `below` spacing wins. Moreover, block spacing takes - /// precedence over paragraph [`spacing`]($par.spacing). - /// - /// Note that this is only a shorthand to set `above` and `below` to the - /// same value. Since the values for `above` and `below` might differ, a - /// [context] block only provides access to `{block.above}` and - /// `{block.below}`, not to `{block.spacing}` directly. - /// - /// This property can be used in combination with a show rule to adjust the - /// spacing around arbitrary block-level elements. - /// - /// ```example - /// #set align(center) - /// #show math.equation: set block(above: 8pt, below: 16pt) - /// - /// This sum of $x$ and $y$: - /// $ x + y = z $ - /// A second paragraph. - /// ``` - #[external] - #[default(Em::new(1.2).into())] - pub spacing: Spacing, - - /// The spacing between this block and its predecessor. - #[parse( - let spacing = args.named("spacing")?; - args.named("above")?.or(spacing) - )] - pub above: Smart<Spacing>, - - /// The spacing between this block and its successor. - #[parse(args.named("below")?.or(spacing))] - pub below: Smart<Spacing>, - - /// Whether to clip the content inside the block. - /// - /// Clipping is useful when the block's content is larger than the block itself, - /// as any content that exceeds the block's bounds will be hidden. - /// - /// ```example - /// #block( - /// width: 50pt, - /// height: 50pt, - /// clip: true, - /// image("tiger.jpg", width: 100pt, height: 100pt) - /// ) - /// ``` - #[default(false)] - pub clip: bool, - - /// Whether this block must stick to the following one, with no break in - /// between. - /// - /// This is, by default, set on heading blocks to prevent orphaned headings - /// at the bottom of the page. - /// - /// ```example - /// >>> #set page(height: 140pt) - /// // Disable stickiness of headings. - /// #show heading: set block(sticky: false) - /// #lorem(20) - /// - /// = Chapter - /// #lorem(10) - /// ``` - #[default(false)] - pub sticky: bool, - - /// The contents of the block. - #[positional] - #[borrowed] - pub body: Option<BlockBody>, -} - -impl BlockElem { - /// Create a block with a custom single-region layouter. - /// - /// Such a block must have `breakable: false` (which is set by this - /// constructor). - pub fn single_layouter<T: NativeElement>( - captured: Packed<T>, - f: fn( - content: &Packed<T>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult<Frame>, - ) -> Self { - Self::new() - .with_breakable(false) - .with_body(Some(BlockBody::SingleLayouter( - callbacks::BlockSingleCallback::new(captured, f), - ))) - } - - /// Create a block with a custom multi-region layouter. - pub fn multi_layouter<T: NativeElement>( - captured: Packed<T>, - f: fn( - content: &Packed<T>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment>, - ) -> Self { - Self::new().with_body(Some(BlockBody::MultiLayouter( - callbacks::BlockMultiCallback::new(captured, f), - ))) - } -} - -impl Packed<BlockElem> { - /// Lay this out as an unbreakable block. - #[typst_macros::time(name = "block", span = self.span())] - pub fn layout_single( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult<Frame> { - // Fetch sizing properties. - let width = self.width(styles); - let height = self.height(styles); - let inset = self.inset(styles).unwrap_or_default(); - - // Build the pod regions. - let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size); - - // Layout the body. - let body = self.body(styles); - let mut frame = match body { - // If we have no body, just create one frame. Its size will be - // adjusted below. - None => Frame::hard(Size::zero()), - - // If we have content as our body, just layout it. - Some(BlockBody::Content(body)) => { - layout_frame(engine, body, locator.relayout(), styles, pod)? - } - - // If we have a child that wants to layout with just access to the - // base region, give it that. - Some(BlockBody::SingleLayouter(callback)) => { - callback.call(engine, locator, styles, pod)? - } - - // If we have a child that wants to layout with full region access, - // we layout it. - Some(BlockBody::MultiLayouter(callback)) => { - let expand = (pod.expand | region.expand) & pod.size.map(Abs::is_finite); - let pod = Region { expand, ..pod }; - callback.call(engine, locator, styles, pod.into())?.into_frame() - } - }; - - // Explicit blocks are boundaries for gradient relativeness. - if matches!(body, None | Some(BlockBody::Content(_))) { - frame.set_kind(FrameKind::Hard); - } - - // Enforce a correct frame size on the expanded axes. Do this before - // applying the inset, since the pod shrunk. - frame.set_size(pod.expand.select(pod.size, frame.size())); - - // Apply the inset. - if !inset.is_zero() { - crate::layout::grow(&mut frame, &inset); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self - .stroke(styles) - .unwrap_or_default() - .map(|s| s.map(Stroke::unwrap_or_default)); - - // Only fetch these if necessary (for clipping or filling/stroking). - let outset = Lazy::new(|| self.outset(styles).unwrap_or_default()); - let radius = Lazy::new(|| self.radius(styles).unwrap_or_default()); - - // Clip the contents, if requested. - if self.clip(styles) { - let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); - frame.clip(clip_rect(size, &radius, &stroke)); - } - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span()); - } - - // Assign label to each frame in the fragment. - if let Some(label) = self.label() { - frame.label(label); - } - - Ok(frame) - } -} - -impl Packed<BlockElem> { - /// Lay this out as a breakable block. - #[typst_macros::time(name = "block", span = self.span())] - pub fn layout_multiple( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - // Fetch sizing properties. - let width = self.width(styles); - let height = self.height(styles); - let inset = self.inset(styles).unwrap_or_default(); - - // Allocate a small vector for backlogs. - let mut buf = SmallVec::<[Abs; 2]>::new(); - - // Build the pod regions. - let pod = - breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf); - - // Layout the body. - let body = self.body(styles); - let mut fragment = match body { - // If we have no body, just create one frame plus one per backlog - // region. We create them zero-sized; if necessary, their size will - // be adjusted below. - None => { - let mut frames = vec![]; - frames.push(Frame::hard(Size::zero())); - if pod.expand.y { - let mut iter = pod; - while !iter.backlog.is_empty() { - frames.push(Frame::hard(Size::zero())); - iter.next(); - } - } - Fragment::frames(frames) - } - - // If we have content as our body, just layout it. - Some(BlockBody::Content(body)) => { - let mut fragment = - layout_fragment(engine, body, locator.relayout(), styles, pod)?; - - // If the body is automatically sized and produced more than one - // fragment, ensure that the width was consistent across all - // regions. If it wasn't, we need to relayout with expansion. - if !pod.expand.x - && fragment - .as_slice() - .windows(2) - .any(|w| !w[0].width().approx_eq(w[1].width())) - { - let max_width = fragment - .iter() - .map(|frame| frame.width()) - .max() - .unwrap_or_default(); - let pod = Regions { - size: Size::new(max_width, pod.size.y), - expand: Axes::new(true, pod.expand.y), - ..pod - }; - fragment = layout_fragment(engine, body, locator, styles, pod)?; - } - - fragment - } - - // If we have a child that wants to layout with just access to the - // base region, give it that. - Some(BlockBody::SingleLayouter(callback)) => { - let pod = Region::new(pod.base(), pod.expand); - callback.call(engine, locator, styles, pod).map(Fragment::frame)? - } - - // If we have a child that wants to layout with full region access, - // we layout it. - // - // For auto-sized multi-layouters, we propagate the outer expansion - // so that they can decide for themselves. We also ensure again to - // only expand if the size is finite. - Some(BlockBody::MultiLayouter(callback)) => { - let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite); - let pod = Regions { expand, ..pod }; - callback.call(engine, locator, styles, pod)? - } - }; - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self - .stroke(styles) - .unwrap_or_default() - .map(|s| s.map(Stroke::unwrap_or_default)); - - // Only fetch these if necessary (for clipping or filling/stroking). - let outset = Lazy::new(|| self.outset(styles).unwrap_or_default()); - let radius = Lazy::new(|| self.radius(styles).unwrap_or_default()); - - // Fetch/compute these outside of the loop. - let clip = self.clip(styles); - let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some); - let has_inset = !inset.is_zero(); - let is_explicit = matches!(body, None | Some(BlockBody::Content(_))); - - // Skip filling/stroking the first frame if it is empty and a non-empty - // one follows. - let mut skip_first = false; - if let [first, rest @ ..] = fragment.as_slice() { - skip_first = has_fill_or_stroke - && first.is_empty() - && rest.iter().any(|frame| !frame.is_empty()); - } - - // Post-process to apply insets, clipping, fills, and strokes. - for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() { - // Explicit blocks are boundaries for gradient relativeness. - if is_explicit { - frame.set_kind(FrameKind::Hard); - } - - // Enforce a correct frame size on the expanded axes. Do this before - // applying the inset, since the pod shrunk. - frame.set_size(pod.expand.select(region, frame.size())); - - // Apply the inset. - if has_inset { - crate::layout::grow(frame, &inset); - } - - // Clip the contents, if requested. - if clip { - let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); - frame.clip(clip_rect(size, &radius, &stroke)); - } - - // Add fill and/or stroke. - if has_fill_or_stroke && (i > 0 || !skip_first) { - frame.fill_and_stroke( - fill.clone(), - &stroke, - &outset, - &radius, - self.span(), - ); - } - } - - // Assign label to each frame in the fragment. - if let Some(label) = self.label() { - for frame in fragment.iter_mut() { - frame.label(label); - } - } - - Ok(fragment) - } -} - -/// The contents of a block. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum BlockBody { - /// The block contains normal content. - Content(Content), - /// The block contains a layout callback that needs access to just one - /// base region. - SingleLayouter(callbacks::BlockSingleCallback), - /// The block contains a layout callback that needs access to the exact - /// regions. - MultiLayouter(callbacks::BlockMultiCallback), -} - -impl Default for BlockBody { - fn default() -> Self { - Self::Content(Content::default()) - } -} - -cast! { - BlockBody, - self => match self { - Self::Content(content) => content.into_value(), - _ => Value::Auto, - }, - v: Content => Self::Content(v), -} - -/// Defines how to size something along an axis. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Sizing { - /// A track that fits its item's contents. - Auto, - /// A size specified in absolute terms and relative to the parent's size. - Rel(Rel), - /// A size specified as a fraction of the remaining free space in the - /// parent. - Fr(Fr), -} - -impl Sizing { - /// Whether this is an automatic sizing. - pub fn is_auto(self) -> bool { - matches!(self, Self::Auto) - } - - /// Whether this is fractional sizing. - pub fn is_fractional(self) -> bool { - matches!(self, Self::Fr(_)) - } -} - -impl Default for Sizing { - fn default() -> Self { - Self::Auto - } -} - -impl From<Smart<Rel>> for Sizing { - fn from(smart: Smart<Rel>) -> Self { - match smart { - Smart::Auto => Self::Auto, - Smart::Custom(rel) => Self::Rel(rel), - } - } -} - -impl<T: Into<Spacing>> From<T> for Sizing { - fn from(spacing: T) -> Self { - match spacing.into() { - Spacing::Rel(rel) => Self::Rel(rel), - Spacing::Fr(fr) => Self::Fr(fr), - } - } -} - -cast! { - Sizing, - self => match self { - Self::Auto => Value::Auto, - Self::Rel(rel) => rel.into_value(), - Self::Fr(fr) => fr.into_value(), - }, - _: AutoValue => Self::Auto, - v: Rel<Length> => Self::Rel(v), - v: Fr => Self::Fr(v), -} - -/// Builds the pod region for an unbreakable sized container. -fn unbreakable_pod( - width: &Sizing, - height: &Sizing, - inset: &Sides<Rel<Abs>>, - styles: StyleChain, - base: Size, -) -> Region { - // Resolve the size. - let mut size = Size::new( - match width { - // - For auto, the whole region is available. - // - Fr is handled outside and already factored into the `region`, - // so we can treat it equivalently to 100%. - Sizing::Auto | Sizing::Fr(_) => base.x, - // Resolve the relative sizing. - Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), - }, - match height { - Sizing::Auto | Sizing::Fr(_) => base.y, - Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y), - }, - ); - - // Take the inset, if any, into account. - if !inset.is_zero() { - size = crate::layout::shrink(size, inset); - } - - // If the child is manually, the size is forced and we should enable - // expansion. - let expand = Axes::new( - *width != Sizing::Auto && size.x.is_finite(), - *height != Sizing::Auto && size.y.is_finite(), - ); - - Region::new(size, expand) -} - -/// Builds the pod regions for a breakable sized container. -fn breakable_pod<'a>( - width: &Sizing, - height: &Sizing, - inset: &Sides<Rel<Abs>>, - styles: StyleChain, - regions: Regions, - buf: &'a mut SmallVec<[Abs; 2]>, -) -> Regions<'a> { - let base = regions.base(); - - // The vertical region sizes we're about to build. - let first; - let full; - let backlog: &mut [Abs]; - let last; - - // If the block has a fixed height, things are very different, so we - // handle that case completely separately. - match height { - Sizing::Auto | Sizing::Fr(_) => { - // If the block is automatically sized, we can just inherit the - // regions. - first = regions.size.y; - full = regions.full; - buf.extend_from_slice(regions.backlog); - backlog = buf; - last = regions.last; - } - - Sizing::Rel(rel) => { - // Resolve the sizing to a concrete size. - let resolved = rel.resolve(styles).relative_to(base.y); - - // Since we're manually sized, the resolved size is the base height. - full = resolved; - - // Distribute the fixed height across a start region and a backlog. - (first, backlog) = distribute(resolved, regions, buf); - - // If the height is manually sized, we don't want a final repeatable - // region. - last = None; - } - }; - - // Resolve the horizontal sizing to a concrete width and combine - // `width` and `first` into `size`. - let mut size = Size::new( - match width { - Sizing::Auto | Sizing::Fr(_) => regions.size.x, - Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), - }, - first, - ); - - // Take the inset, if any, into account, applying it to the - // individual region components. - let (mut full, mut last) = (full, last); - if !inset.is_zero() { - crate::layout::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset); - } - - // If the child is manually, the size is forced and we should enable - // expansion. - let expand = Axes::new( - *width != Sizing::Auto && size.x.is_finite(), - *height != Sizing::Auto && size.y.is_finite(), - ); - - Regions { size, full, backlog, last, expand } -} - -/// Distribute a fixed height spread over existing regions into a new first -/// height and a new backlog. -fn distribute<'a>( - height: Abs, - mut regions: Regions, - buf: &'a mut SmallVec<[Abs; 2]>, -) -> (Abs, &'a mut [Abs]) { - // Build new region heights from old regions. - let mut remaining = height; - loop { - let limited = regions.size.y.clamp(Abs::zero(), remaining); - buf.push(limited); - remaining -= limited; - if remaining.approx_empty() - || !regions.may_break() - || (!regions.may_progress() && limited.approx_empty()) - { - break; - } - regions.next(); - } - - // If there is still something remaining, apply it to the - // last region (it will overflow, but there's nothing else - // we can do). - if !remaining.approx_empty() { - if let Some(last) = buf.last_mut() { - *last += remaining; - } - } - - // Distribute the heights to the first region and the - // backlog. There is no last region, since the height is - // fixed. - (buf[0], &mut buf[1..]) -} - -/// Manual closure implementations for layout callbacks. -/// -/// Normal closures are not `Hash`, so we can't use them. -mod callbacks { - use super::*; - - macro_rules! callback { - ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => { - #[derive(Debug, Clone, PartialEq, Hash)] - pub struct $name { - captured: Content, - f: fn(&Content, $($param_ty),*) -> $ret, - } - - impl $name { - pub fn new<T: NativeElement>( - captured: Packed<T>, - f: fn(&Packed<T>, $($param_ty),*) -> $ret, - ) -> Self { - Self { - // Type-erased the content. - captured: captured.pack(), - // Safety: The only difference between the two function - // pointer types is the type of the first parameter, - // which changes from `&Packed<T>` to `&Content`. This - // is safe because: - // - `Packed<T>` is a transparent wrapper around - // `Content`, so for any `T` it has the same memory - // representation as `Content`. - // - While `Packed<T>` imposes the additional constraint - // that the content is of type `T`, this constraint is - // upheld: It is initially the case because we store a - // `Packed<T>` above. It keeps being the case over the - // lifetime of the closure because `capture` is a - // private field and `Content`'s `Clone` impl is - // guaranteed to retain the type (if it didn't, - // literally everything would break). - #[allow(clippy::missing_transmute_annotations)] - f: unsafe { std::mem::transmute(f) }, - } - } - - pub fn call(&self, $($param: $param_ty),*) -> $ret { - (self.f)(&self.captured, $($param),*) - } - } - }; - } - - callback! { - InlineCallback = ( - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult<Vec<InlineItem>> - } - - callback! { - BlockSingleCallback = ( - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult<Frame> - } - - callback! { - BlockMultiCallback = ( - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> - } -} diff --git a/crates/typst/src/layout/repeat.rs b/crates/typst/src/layout/repeat.rs deleted file mode 100644 index 66a6f923..00000000 --- a/crates/typst/src/layout/repeat.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, -}; -use crate::introspection::Locator; -use crate::layout::{ - layout_frame, Abs, AlignElem, Axes, BlockElem, Frame, Length, Point, Region, Size, -}; -use crate::utils::Numeric; - -/// Repeats content to the available space. -/// -/// This can be useful when implementing a custom index, reference, or outline. -/// -/// Space may be inserted between the instances of the body parameter, so be -/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly. -/// -/// Errors if there no bounds on the available space, as it would create -/// infinite content. -/// -/// # Example -/// ```example -/// Sign on the dotted line: -/// #box(width: 1fr, repeat[.]) -/// -/// #set text(10pt) -/// #v(8pt, weak: true) -/// #align(right)[ -/// Berlin, the 22nd of December, 2022 -/// ] -/// ``` -#[elem(Show)] -pub struct RepeatElem { - /// The content to repeat. - #[required] - pub body: Content, - - /// The gap between each instance of the body. - #[default] - pub gap: Length, - - /// Whether to increase the gap between instances to completely fill the - /// available space. - #[default(true)] - pub justify: bool, -} - -impl Show for Packed<RepeatElem> { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { - Ok(BlockElem::single_layouter(self.clone(), layout_repeat) - .pack() - .spanned(self.span())) - } -} - -/// Layout the repeated content. -#[typst_macros::time(span = elem.span())] -fn layout_repeat( - elem: &Packed<RepeatElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult<Frame> { - let pod = Region::new(region.size, Axes::new(false, false)); - let piece = layout_frame(engine, &elem.body, locator, styles, pod)?; - let size = Size::new(region.size.x, piece.height()); - - if !size.is_finite() { - bail!(elem.span(), "repeat with no size restrictions"); - } - - let mut frame = Frame::soft(size); - if piece.has_baseline() { - frame.set_baseline(piece.baseline()); - } - - let mut gap = elem.gap(styles).resolve(styles); - let fill = region.size.x; - let width = piece.width(); - - // count * width + (count - 1) * gap = fill, but count is an integer so - // we need to round down and get the remainder. - let count = ((fill + gap) / (width + gap)).floor(); - let remaining = (fill + gap) % (width + gap); - - let justify = elem.justify(styles); - if justify { - gap += remaining / (count - 1.0); - } - - let align = AlignElem::alignment_in(styles).resolve(styles); - let mut offset = Abs::zero(); - if count == 1.0 || !justify { - offset += align.x.position(remaining); - } - - if width > Abs::zero() { - for _ in 0..(count as usize).min(1000) { - frame.push_frame(Point::with_x(offset), piece.clone()); - offset += piece.width() + gap; - } - } - - Ok(frame) -} diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 7f0b8e69..db1673d2 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -13,72 +13,45 @@ //! order-independent and thus much better suited for further processing than //! the raw markup. //! - **Layouting:** -//! Next, the content is [layouted] into a [document] containing one [frame] +//! Next, the content is [laid out] into a [document] containing one [frame] //! per page with items at fixed positions. //! - **Exporting:** //! These frames can finally be exported into an output format (currently PDF, //! PNG, or SVG). //! -//! [tokens]: syntax::SyntaxKind -//! [parsed]: syntax::parse -//! [syntax tree]: syntax::SyntaxNode -//! [AST]: syntax::ast -//! [evaluate]: eval::eval -//! [module]: foundations::Module -//! [content]: foundations::Content -//! [layouted]: crate::layout::layout_document -//! [document]: model::Document -//! [frame]: layout::Frame - -#![recursion_limit = "1000"] -#![allow(clippy::comparison_chain)] -#![allow(clippy::wildcard_in_or_patterns)] -#![allow(clippy::manual_range_contains)] - -extern crate self as typst; - -pub mod diag; -pub mod engine; -pub mod eval; -pub mod foundations; -pub mod introspection; -pub mod layout; -pub mod loading; -pub mod math; -pub mod model; -pub mod realize; -pub mod symbols; -pub mod text; -pub mod visualize; - +//! [tokens]: typst_syntax::SyntaxKind +//! [parsed]: typst_syntax::parse +//! [syntax tree]: typst_syntax::SyntaxNode +//! [AST]: typst_syntax::ast +//! [evaluate]: typst_eval::eval +//! [module]: crate::foundations::Module +//! [content]: crate::foundations::Content +//! [laid out]: typst_layout::layout_document +//! [document]: crate::model::Document +//! [frame]: crate::layout::Frame + +pub extern crate comemo; +pub extern crate ecow; + +pub use typst_library::*; #[doc(inline)] pub use typst_syntax as syntax; #[doc(inline)] pub use typst_utils as utils; use std::collections::HashSet; -use std::ops::{Deref, Range}; use comemo::{Track, Tracked, Validate}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_library::diag::{warning, FileError, SourceDiagnostic, SourceResult, Warned}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{StyleChain, Styles, Value}; +use typst_library::introspection::Introspector; +use typst_library::model::Document; +use typst_library::routines::Routines; +use typst_syntax::{FileId, Span}; use typst_timing::{timed, TimingScope}; -use crate::diag::{ - warning, FileError, FileResult, SourceDiagnostic, SourceResult, Warned, -}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{ - Array, Bytes, Datetime, Dict, Module, Scope, StyleChain, Styles, Value, -}; -use crate::introspection::Introspector; -use crate::layout::{Alignment, Dir}; -use crate::model::Document; -use crate::syntax::package::PackageSpec; -use crate::syntax::{FileId, Source, Span}; -use crate::text::{Font, FontBook}; -use crate::utils::LazyHash; -use crate::visualize::Color; - /// Compile sources into a fully layouted document. /// /// - Returns `Ok(document)` if there were no fatal errors. @@ -118,7 +91,8 @@ fn compile_impl( .map_err(|err| hint_invalid_main_file(world, err, main))?; // First evaluate the main source file into a module. - let content = crate::eval::eval( + let content = typst_eval::eval( + &ROUTINES, world, traced, sink.track_mut(), @@ -137,7 +111,7 @@ fn compile_impl( // The name of the iterations for timing scopes. const ITER_NAMES: &[&str] = &["layout (1)", "layout (2)", "layout (3)", "layout (4)", "layout (5)"]; - let _scope = TimingScope::new(ITER_NAMES[iter], None); + let _scope = TimingScope::new(ITER_NAMES[iter]); subsink = Sink::new(); @@ -148,10 +122,11 @@ fn compile_impl( traced, sink: subsink.track_mut(), route: Route::default(), + routines: &ROUTINES, }; // Layout! - document = crate::layout::layout_document(&mut engine, &content, styles)?; + document = (engine.routines.layout_document)(&mut engine, &content, styles)?; iter += 1; if timed!("check stabilized", document.introspector.validate(&constraint)) { @@ -182,237 +157,12 @@ fn compile_impl( fn deduplicate(mut diags: EcoVec<SourceDiagnostic>) -> EcoVec<SourceDiagnostic> { let mut unique = HashSet::new(); diags.retain(|diag| { - let hash = crate::utils::hash128(&(&diag.span, &diag.message)); + let hash = typst_utils::hash128(&(&diag.span, &diag.message)); unique.insert(hash) }); diags } -/// The environment in which typesetting occurs. -/// -/// All loading functions (`main`, `source`, `file`, `font`) should perform -/// internal caching so that they are relatively cheap on repeated invocations -/// with the same argument. [`Source`], [`Bytes`], and [`Font`] are -/// all reference-counted and thus cheap to clone. -/// -/// The compiler doesn't do the caching itself because the world has much more -/// information on when something can change. For example, fonts typically don't -/// change and can thus even be cached across multiple compilations (for -/// long-running applications like `typst watch`). Source files on the other -/// hand can change and should thus be cleared after each compilation. Advanced -/// clients like language servers can also retain the source files and -/// [edit](Source::edit) them in-place to benefit from better incremental -/// performance. -#[comemo::track] -pub trait World: Send + Sync { - /// The standard library. - /// - /// Can be created through `Library::build()`. - fn library(&self) -> &LazyHash<Library>; - - /// Metadata about all known fonts. - fn book(&self) -> &LazyHash<FontBook>; - - /// Get the file id of the main source file. - fn main(&self) -> FileId; - - /// Try to access the specified source file. - fn source(&self, id: FileId) -> FileResult<Source>; - - /// Try to access the specified file. - fn file(&self, id: FileId) -> FileResult<Bytes>; - - /// Try to access the font with the given index in the font book. - fn font(&self, index: usize) -> Option<Font>; - - /// Get the current date. - /// - /// If no offset is specified, the local date should be chosen. Otherwise, - /// the UTC date should be chosen with the corresponding offset in hours. - /// - /// If this function returns `None`, Typst's `datetime` function will - /// return an error. - fn today(&self, offset: Option<i64>) -> Option<Datetime>; - - /// A list of all available packages and optionally descriptions for them. - /// - /// This function is optional to implement. It enhances the user experience - /// by enabling autocompletion for packages. Details about packages from the - /// `@preview` namespace are available from - /// `https://packages.typst.org/preview/index.json`. - fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { - &[] - } -} - -macro_rules! delegate_for_ptr { - ($W:ident for $ptr:ty) => { - impl<$W: World> World for $ptr { - fn library(&self) -> &LazyHash<Library> { - self.deref().library() - } - - fn book(&self) -> &LazyHash<FontBook> { - self.deref().book() - } - - fn main(&self) -> FileId { - self.deref().main() - } - - fn source(&self, id: FileId) -> FileResult<Source> { - self.deref().source(id) - } - - fn file(&self, id: FileId) -> FileResult<Bytes> { - self.deref().file(id) - } - - fn font(&self, index: usize) -> Option<Font> { - self.deref().font(index) - } - - fn today(&self, offset: Option<i64>) -> Option<Datetime> { - self.deref().today(offset) - } - - fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { - self.deref().packages() - } - } - }; -} - -delegate_for_ptr!(W for std::boxed::Box<W>); -delegate_for_ptr!(W for std::sync::Arc<W>); -delegate_for_ptr!(W for &W); - -/// Helper methods on [`World`] implementations. -pub trait WorldExt { - /// Get the byte range for a span. - /// - /// Returns `None` if the `Span` does not point into any source file. - fn range(&self, span: Span) -> Option<Range<usize>>; -} - -impl<T: World> WorldExt for T { - fn range(&self, span: Span) -> Option<Range<usize>> { - self.source(span.id()?).ok()?.range(span) - } -} - -/// Definition of Typst's standard library. -#[derive(Debug, Clone, Hash)] -pub struct Library { - /// The module that contains the definitions that are available everywhere. - pub global: Module, - /// The module that contains the definitions available in math mode. - pub math: Module, - /// The default style properties (for page size, font selection, and - /// everything else configurable via set and show rules). - pub styles: Styles, - /// The standard library as a value. - /// Used to provide the `std` variable. - pub std: Value, -} - -impl Library { - /// Create a new builder for a library. - pub fn builder() -> LibraryBuilder { - LibraryBuilder::default() - } -} - -impl Default for Library { - /// Constructs the standard library with the default configuration. - fn default() -> Self { - Self::builder().build() - } -} - -/// Configurable builder for the standard library. -/// -/// This struct is created by [`Library::builder`]. -#[derive(Debug, Clone, Default)] -pub struct LibraryBuilder { - inputs: Option<Dict>, -} - -impl LibraryBuilder { - /// Configure the inputs visible through `sys.inputs`. - pub fn with_inputs(mut self, inputs: Dict) -> Self { - self.inputs = Some(inputs); - self - } - - /// Consumes the builder and returns a `Library`. - pub fn build(self) -> Library { - let math = math::module(); - let inputs = self.inputs.unwrap_or_default(); - let global = global(math.clone(), inputs); - let std = Value::Module(global.clone()); - Library { global, math, styles: Styles::new(), std } - } -} - -/// Construct the module with global definitions. -fn global(math: Module, inputs: Dict) -> Module { - let mut global = Scope::deduplicating(); - self::foundations::define(&mut global, inputs); - self::model::define(&mut global); - self::text::define(&mut global); - global.reset_category(); - global.define_module(math); - self::layout::define(&mut global); - self::visualize::define(&mut global); - self::introspection::define(&mut global); - self::loading::define(&mut global); - self::symbols::define(&mut global); - prelude(&mut global); - Module::new("global", global) -} - -/// Defines scoped values that are globally available, too. -fn prelude(global: &mut Scope) { - global.reset_category(); - global.define("black", Color::BLACK); - global.define("gray", Color::GRAY); - global.define("silver", Color::SILVER); - global.define("white", Color::WHITE); - global.define("navy", Color::NAVY); - global.define("blue", Color::BLUE); - global.define("aqua", Color::AQUA); - global.define("teal", Color::TEAL); - global.define("eastern", Color::EASTERN); - global.define("purple", Color::PURPLE); - global.define("fuchsia", Color::FUCHSIA); - global.define("maroon", Color::MAROON); - global.define("red", Color::RED); - global.define("orange", Color::ORANGE); - global.define("yellow", Color::YELLOW); - global.define("olive", Color::OLIVE); - global.define("green", Color::GREEN); - global.define("lime", Color::LIME); - global.define("luma", Color::luma_data()); - global.define("oklab", Color::oklab_data()); - global.define("oklch", Color::oklch_data()); - global.define("rgb", Color::rgb_data()); - global.define("cmyk", Color::cmyk_data()); - global.define("range", Array::range_data()); - global.define("ltr", Dir::LTR); - global.define("rtl", Dir::RTL); - global.define("ttb", Dir::TTB); - global.define("btt", Dir::BTT); - global.define("start", Alignment::START); - global.define("left", Alignment::LEFT); - global.define("center", Alignment::CENTER); - global.define("right", Alignment::RIGHT); - global.define("end", Alignment::END); - global.define("top", Alignment::TOP); - global.define("horizon", Alignment::HORIZON); - global.define("bottom", Alignment::BOTTOM); -} - /// Adds useful hints when the main source file couldn't be read /// and returns the final diagnostic. fn hint_invalid_main_file( @@ -457,3 +207,40 @@ fn hint_invalid_main_file( eco_vec![diagnostic] } + +/// Defines implementation of various Typst compiler routines as a table of +/// function pointers. +/// +/// This is essentially dynamic linking and done to allow for crate splitting. +pub static ROUTINES: Routines = Routines { + eval_string: typst_eval::eval_string, + eval_closure: typst_eval::eval_closure, + realize: typst_realize::realize, + layout_document: typst_layout::layout_document, + layout_fragment: typst_layout::layout_fragment, + layout_frame: typst_layout::layout_frame, + layout_inline: typst_layout::layout_inline, + layout_box: typst_layout::layout_box, + layout_list: typst_layout::layout_list, + layout_enum: typst_layout::layout_enum, + layout_grid: typst_layout::layout_grid, + layout_table: typst_layout::layout_table, + layout_stack: typst_layout::layout_stack, + layout_columns: typst_layout::layout_columns, + layout_move: typst_layout::layout_move, + layout_rotate: typst_layout::layout_rotate, + layout_scale: typst_layout::layout_scale, + layout_skew: typst_layout::layout_skew, + layout_repeat: typst_layout::layout_repeat, + layout_pad: typst_layout::layout_pad, + layout_line: typst_layout::layout_line, + layout_path: typst_layout::layout_path, + layout_polygon: typst_layout::layout_polygon, + layout_rect: typst_layout::layout_rect, + layout_square: typst_layout::layout_square, + layout_ellipse: typst_layout::layout_ellipse, + layout_circle: typst_layout::layout_circle, + layout_image: typst_layout::layout_image, + layout_equation_block: typst_layout::layout_equation_block, + layout_equation_inline: typst_layout::layout_equation_inline, +}; diff --git a/crates/typst/src/math/align.rs b/crates/typst/src/math/align.rs deleted file mode 100644 index 41580bf9..00000000 --- a/crates/typst/src/math/align.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::diag::SourceResult; -use crate::foundations::{elem, Content, NativeElement, Packed, StyleChain}; -use crate::layout::Abs; -use crate::math::{LayoutMath, MathContext, MathFragment, MathRun}; -use crate::utils::singleton; - -/// A math alignment point: `&`, `&&`. -#[elem(title = "Alignment Point", LayoutMath)] -pub struct AlignPointElem {} - -impl AlignPointElem { - /// Get the globally shared alignment point element. - pub fn shared() -> &'static Content { - singleton!(Content, AlignPointElem::new().pack()) - } -} - -impl LayoutMath for Packed<AlignPointElem> { - fn layout_math(&self, ctx: &mut MathContext, _: StyleChain) -> SourceResult<()> { - ctx.push(MathFragment::Align); - Ok(()) - } -} - -pub(super) struct AlignmentResult { - pub points: Vec<Abs>, - pub width: Abs, -} - -/// Determine the positions of the alignment points, according to the input rows combined. -pub(super) fn alignments(rows: &[MathRun]) -> AlignmentResult { - let mut widths = Vec::<Abs>::new(); - - let mut pending_width = Abs::zero(); - for row in rows { - let mut width = Abs::zero(); - let mut alignment_index = 0; - - for fragment in row.iter() { - if matches!(fragment, MathFragment::Align) { - if alignment_index < widths.len() { - widths[alignment_index].set_max(width); - } else { - widths.push(width.max(pending_width)); - } - width = Abs::zero(); - alignment_index += 1; - } else { - width += fragment.width(); - } - } - if widths.is_empty() { - pending_width.set_max(width); - } else if alignment_index < widths.len() { - widths[alignment_index].set_max(width); - } else { - widths.push(width.max(pending_width)); - } - } - - let mut points = widths; - for i in 1..points.len() { - let prev = points[i - 1]; - points[i] += prev; - } - AlignmentResult { - width: points.last().copied().unwrap_or(pending_width), - points, - } -} diff --git a/crates/typst/src/math/cancel.rs b/crates/typst/src/math/cancel.rs deleted file mode 100644 index 0bc28f07..00000000 --- a/crates/typst/src/math/cancel.rs +++ /dev/null @@ -1,254 +0,0 @@ -use comemo::Track; - -use crate::diag::{At, SourceResult}; -use crate::foundations::{cast, elem, Content, Context, Func, Packed, Smart, StyleChain}; -use crate::layout::{ - Abs, Angle, Frame, FrameItem, Length, Point, Ratio, Rel, Size, Transform, -}; -use crate::math::{scaled_font_size, FrameFragment, LayoutMath, MathContext}; -use crate::syntax::Span; -use crate::text::TextElem; -use crate::visualize::{FixedStroke, Geometry, Stroke}; - -/// Displays a diagonal line over a part of an equation. -/// -/// This is commonly used to show the elimination of a term. -/// -/// # Example -/// ```example -/// >>> #set page(width: 140pt) -/// Here, we can simplify: -/// $ (a dot b dot cancel(x)) / -/// cancel(x) $ -/// ``` -#[elem(LayoutMath)] -pub struct CancelElem { - /// The content over which the line should be placed. - #[required] - pub body: Content, - - /// The length of the line, relative to the length of the diagonal spanning - /// the whole element being "cancelled". A value of `{100%}` would then have - /// the line span precisely the element's diagonal. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ a + cancel(x, length: #200%) - /// - cancel(x, length: #200%) $ - /// ``` - #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] - pub length: Rel<Length>, - - /// Whether the cancel line should be inverted (flipped along the y-axis). - /// For the default angle setting, inverted means the cancel line - /// points to the top left instead of top right. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ (a cancel((b + c), inverted: #true)) / - /// cancel(b + c, inverted: #true) $ - /// ``` - #[default(false)] - pub inverted: bool, - - /// Whether two opposing cancel lines should be drawn, forming a cross over - /// the element. Overrides `inverted`. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi, cross: #true) $ - /// ``` - #[default(false)] - pub cross: bool, - - /// How much to rotate the cancel line. - /// - /// - If given an angle, the line is rotated by that angle clockwise with - /// respect to the y-axis. - /// - If `{auto}`, the line assumes the default angle; that is, along the - /// rising diagonal of the content box. - /// - If given a function `angle => angle`, the line is rotated, with - /// respect to the y-axis, by the angle returned by that function. The - /// function receives the default angle as its input. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi) - /// cancel(Pi, angle: #0deg) - /// cancel(Pi, angle: #45deg) - /// cancel(Pi, angle: #90deg) - /// cancel(1/(1+x), angle: #(a => a + 45deg)) - /// cancel(1/(1+x), angle: #(a => a + 90deg)) $ - /// ``` - pub angle: Smart<CancelAngle>, - - /// How to [stroke]($stroke) the cancel line. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel( - /// sum x, - /// stroke: #( - /// paint: red, - /// thickness: 1.5pt, - /// dash: "dashed", - /// ), - /// ) $ - /// ``` - #[resolve] - #[fold] - #[default(Stroke { - // Default stroke has 0.5pt for better visuals. - thickness: Smart::Custom(Abs::pt(0.5).into()), - ..Default::default() - })] - pub stroke: Stroke, -} - -impl LayoutMath for Packed<CancelElem> { - #[typst_macros::time(name = "math.cancel", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let body = ctx.layout_into_fragment(self.body(), styles)?; - // Preserve properties of body. - let body_class = body.class(); - let body_italics = body.italics_correction(); - let body_attach = body.accent_attach(); - let body_text_like = body.is_text_like(); - - let mut body = body.into_frame(); - let body_size = body.size(); - let span = self.span(); - let length = self.length(styles).at(scaled_font_size(ctx, styles)); - - let stroke = self.stroke(styles).unwrap_or(FixedStroke { - paint: TextElem::fill_in(styles).as_decoration(), - ..Default::default() - }); - - let invert = self.inverted(styles); - let cross = self.cross(styles); - let angle = self.angle(styles); - - let invert_first_line = !cross && invert; - let first_line = draw_cancel_line( - ctx, - length, - stroke.clone(), - invert_first_line, - &angle, - body_size, - styles, - span, - )?; - - // The origin of our line is the very middle of the element. - let center = body_size.to_point() / 2.0; - body.push_frame(center, first_line); - - if cross { - // Draw the second line. - let second_line = draw_cancel_line( - ctx, length, stroke, true, &angle, body_size, styles, span, - )?; - - body.push_frame(center, second_line); - } - - ctx.push( - FrameFragment::new(ctx, styles, body) - .with_class(body_class) - .with_italics_correction(body_italics) - .with_accent_attach(body_attach) - .with_text_like(body_text_like), - ); - - Ok(()) - } -} - -/// Defines the cancel line. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum CancelAngle { - Angle(Angle), - Func(Func), -} - -cast! { - CancelAngle, - self => match self { - Self::Angle(v) => v.into_value(), - Self::Func(v) => v.into_value() - }, - v: Angle => CancelAngle::Angle(v), - v: Func => CancelAngle::Func(v), -} - -/// Draws a cancel line. -#[allow(clippy::too_many_arguments)] -fn draw_cancel_line( - ctx: &mut MathContext, - length_scale: Rel<Abs>, - stroke: FixedStroke, - invert: bool, - angle: &Smart<CancelAngle>, - body_size: Size, - styles: StyleChain, - span: Span, -) -> SourceResult<Frame> { - let default = default_angle(body_size); - let mut angle = match angle { - // Non specified angle defaults to the diagonal - Smart::Auto => default, - Smart::Custom(angle) => match angle { - // This specifies the absolute angle w.r.t y-axis clockwise. - CancelAngle::Angle(v) => *v, - // This specifies a function that takes the default angle as input. - CancelAngle::Func(func) => func - .call(ctx.engine, Context::new(None, Some(styles)).track(), [default])? - .cast() - .at(span)?, - }, - }; - - // invert means flipping along the y-axis - if invert { - angle *= -1.0; - } - - // same as above, the default length is the diagonal of the body box. - let default_length = body_size.to_point().hypot(); - let length = length_scale.relative_to(default_length); - - // Draw a vertical line of length and rotate it by angle - let start = Point::new(Abs::zero(), length / 2.0); - let delta = Point::new(Abs::zero(), -length); - - let mut frame = Frame::soft(body_size); - frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span)); - - // Having the middle of the line at the origin is convenient here. - frame.transform(Transform::rotate(angle)); - Ok(frame) -} - -/// The default line angle for a body of the given size. -fn default_angle(body: Size) -> Angle { - // The default cancel line is the diagonal. - // We infer the default angle from - // the diagonal w.r.t to the body box. - // - // The returned angle is in the range of [0, Pi/2] - // - // Note that the angle is computed w.r.t to the y-axis - // - // B - // /| - // diagonal / | height - // / | - // / | - // O ---- - // width - let (width, height) = (body.x, body.y); - let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2]) - Angle::rad(default_angle) -} diff --git a/crates/typst/src/math/class.rs b/crates/typst/src/math/class.rs deleted file mode 100644 index 1110ebc9..00000000 --- a/crates/typst/src/math/class.rs +++ /dev/null @@ -1,46 +0,0 @@ -use unicode_math_class::MathClass; - -use crate::diag::SourceResult; -use crate::foundations::{elem, Content, Packed, StyleChain}; -use crate::math::{EquationElem, LayoutMath, Limits, MathContext}; - -/// Forced use of a certain math class. -/// -/// This is useful to treat certain symbols as if they were of a different -/// class, e.g. to make a symbol behave like a relation. The class of a symbol -/// defines the way it is laid out, including spacing around it, and how its -/// scripts are attached by default. Note that the latter can always be -/// overridden using [`{limits}`](math.limits) and [`{scripts}`](math.scripts). -/// -/// # Example -/// ```example -/// #let loves = math.class( -/// "relation", -/// sym.suit.heart, -/// ) -/// -/// $x loves y and y loves 5$ -/// ``` -#[elem(LayoutMath)] -pub struct ClassElem { - /// The class to apply to the content. - #[required] - pub class: MathClass, - - /// The content to which the class is applied. - #[required] - pub body: Content, -} - -impl LayoutMath for Packed<ClassElem> { - #[typst_macros::time(name = "math.class", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let class = *self.class(); - let style = EquationElem::set_class(Some(class)).wrap(); - let mut fragment = ctx.layout_into_fragment(self.body(), styles.chain(&style))?; - fragment.set_class(class); - fragment.set_limits(Limits::for_class(class)); - ctx.push(fragment); - Ok(()) - } -} diff --git a/crates/typst/src/math/ctx.rs b/crates/typst/src/math/ctx.rs deleted file mode 100644 index 35eb665c..00000000 --- a/crates/typst/src/math/ctx.rs +++ /dev/null @@ -1,469 +0,0 @@ -use std::f64::consts::SQRT_2; - -use ecow::{eco_vec, EcoString}; -use rustybuzz::Feature; -use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; -use ttf_parser::math::MathValue; -use ttf_parser::opentype_layout::LayoutTable; -use ttf_parser::GlyphId; -use unicode_math_class::MathClass; -use unicode_segmentation::UnicodeSegmentation; - -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{Content, Packed, StyleChain, StyleVec}; -use crate::introspection::{SplitLocator, TagElem}; -use crate::layout::{ - layout_frame, Abs, Axes, BoxElem, Em, Frame, HElem, PlaceElem, Region, Size, Spacing, -}; -use crate::math::{ - scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment, - LayoutMath, MathFragment, MathRun, MathSize, THICK, -}; -use crate::realize::{realize, Arenas, RealizationKind}; -use crate::syntax::{is_newline, Span}; -use crate::text::{ - features, BottomEdge, BottomEdgeMetric, Font, LinebreakElem, SpaceElem, TextElem, - TextSize, TopEdge, TopEdgeMetric, -}; - -macro_rules! scaled { - ($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { - match $crate::math::EquationElem::size_in($styles) { - $crate::math::MathSize::Display => scaled!($ctx, $styles, $display), - _ => scaled!($ctx, $styles, $text), - } - }; - ($ctx:expr, $styles:expr, $name:ident) => { - $ctx.constants - .$name() - .scaled($ctx, $crate::math::scaled_font_size($ctx, $styles)) - }; -} - -macro_rules! percent { - ($ctx:expr, $name:ident) => { - $ctx.constants.$name() as f64 / 100.0 - }; -} - -/// The context for math layout. -pub struct MathContext<'a, 'v, 'e> { - // External. - pub engine: &'v mut Engine<'e>, - pub locator: &'v mut SplitLocator<'a>, - pub region: Region, - // Font-related. - pub font: &'a Font, - pub ttf: &'a ttf_parser::Face<'a>, - pub table: ttf_parser::math::Table<'a>, - pub constants: ttf_parser::math::Constants<'a>, - pub ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>, - pub glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>, - pub space_width: Em, - // Mutable. - pub fragments: Vec<MathFragment>, -} - -impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { - /// Create a new math context. - pub fn new( - engine: &'v mut Engine<'e>, - locator: &'v mut SplitLocator<'a>, - styles: StyleChain<'a>, - base: Size, - font: &'a Font, - ) -> Self { - let math_table = font.ttf().tables().math.unwrap(); - let gsub_table = font.ttf().tables().gsub; - let constants = math_table.constants.unwrap(); - - let ssty_table = gsub_table - .and_then(|gsub| { - gsub.features - .find(ttf_parser::Tag::from_bytes(b"ssty")) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index)) - }) - .and_then(|ssty| ssty.subtables.get::<SubstitutionSubtable>(0)) - .and_then(|ssty| match ssty { - SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), - _ => None, - }); - - let features = features(styles); - let glyphwise_tables = gsub_table.map(|gsub| { - features - .into_iter() - .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) - .collect() - }); - - let ttf = font.ttf(); - let space_width = ttf - .glyph_index(' ') - .and_then(|id| ttf.glyph_hor_advance(id)) - .map(|advance| font.to_em(advance)) - .unwrap_or(THICK); - - Self { - engine, - locator, - region: Region::new(base, Axes::splat(false)), - font, - ttf: font.ttf(), - table: math_table, - constants, - ssty_table, - glyphwise_tables, - space_width, - fragments: vec![], - } - } - - /// Push a fragment. - pub fn push(&mut self, fragment: impl Into<MathFragment>) { - self.fragments.push(fragment.into()); - } - - /// Push multiple fragments. - pub fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) { - self.fragments.extend(fragments); - } - - /// Layout the given element and return the result as a [`MathRun`]. - pub fn layout_into_run( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult<MathRun> { - Ok(MathRun::new(self.layout_into_fragments(elem, styles)?)) - } - - /// Layout the given element and return the resulting [`MathFragment`]s. - pub fn layout_into_fragments( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult<Vec<MathFragment>> { - // The element's layout_math() changes the fragments held in this - // MathContext object, but for convenience this function shouldn't change - // them, so we restore the MathContext's fragments after obtaining the - // layout result. - let prev = std::mem::take(&mut self.fragments); - self.layout(elem, styles)?; - Ok(std::mem::replace(&mut self.fragments, prev)) - } - - /// Layout the given element and return the result as a - /// unified [`MathFragment`]. - pub fn layout_into_fragment( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult<MathFragment> { - Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles)) - } - - /// Layout the given element and return the result as a [`Frame`]. - pub fn layout_into_frame( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult<Frame> { - Ok(self.layout_into_fragment(elem, styles)?.into_frame()) - } -} - -impl MathContext<'_, '_, '_> { - /// Layout arbitrary content. - fn layout(&mut self, content: &Content, styles: StyleChain) -> SourceResult<()> { - let arenas = Arenas::default(); - let pairs = realize( - RealizationKind::Math, - self.engine, - self.locator, - &arenas, - content, - styles, - )?; - - let outer = styles; - for (elem, styles) in pairs { - // Hack because the font is fixed in math. - if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { - let frame = self.layout_external(elem, styles)?; - self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); - continue; - } - - self.layout_realized(elem, styles)?; - } - - Ok(()) - } - - /// Layout an element resulting from realization. - fn layout_realized( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - if let Some(elem) = elem.to_packed::<TagElem>() { - self.push(MathFragment::Tag(elem.tag.clone())); - } else if elem.is::<SpaceElem>() { - let font_size = scaled_font_size(self, styles); - self.push(MathFragment::Space(self.space_width.at(font_size))); - } else if elem.is::<LinebreakElem>() { - self.push(MathFragment::Linebreak); - } else if let Some(elem) = elem.to_packed::<HElem>() { - if let Spacing::Rel(rel) = elem.amount() { - if rel.rel.is_zero() { - self.push(MathFragment::Spacing( - rel.abs.at(scaled_font_size(self, styles)), - elem.weak(styles), - )); - } - } - } else if let Some(elem) = elem.to_packed::<TextElem>() { - let fragment = self.layout_text(elem, styles)?; - self.push(fragment); - } else if let Some(boxed) = elem.to_packed::<BoxElem>() { - let frame = self.layout_box(boxed, styles)?; - self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); - } else if let Some(elem) = elem.with::<dyn LayoutMath>() { - elem.layout_math(self, styles)?; - } else { - let mut frame = self.layout_external(elem, styles)?; - if !frame.has_baseline() { - let axis = scaled!(self, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - } - self.push( - FrameFragment::new(self, styles, frame) - .with_spaced(true) - .with_ignorant(elem.is::<PlaceElem>()), - ); - } - - Ok(()) - } - - /// Layout a box into a frame. - fn layout_box( - &mut self, - boxed: &Packed<BoxElem>, - styles: StyleChain, - ) -> SourceResult<Frame> { - let local = - TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap(); - boxed.layout( - self.engine, - self.locator.next(&boxed.span()), - styles.chain(&local), - self.region.size, - ) - } - - /// Layout into a frame with normal layout. - fn layout_external( - &mut self, - content: &Content, - styles: StyleChain, - ) -> SourceResult<Frame> { - let local = - TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap(); - layout_frame( - self.engine, - content, - self.locator.next(&content.span()), - styles.chain(&local), - self.region, - ) - } - - /// Layout the given [`TextElem`] into a [`MathFragment`]. - fn layout_text( - &mut self, - elem: &Packed<TextElem>, - styles: StyleChain, - ) -> SourceResult<MathFragment> { - let text = elem.text(); - let span = elem.span(); - let mut chars = text.chars(); - let math_size = EquationElem::size_in(styles); - let fragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| styled_char(styles, c, true)) - .and_then(|c| GlyphFragment::try_new(self, styles, c, span)) - { - // A single letter that is available in the math font. - match math_size { - MathSize::Script => { - glyph.make_scriptsize(self); - } - MathSize::ScriptScript => { - glyph.make_scriptscriptsize(self); - } - _ => (), - } - - if glyph.class == MathClass::Large { - let mut variant = if math_size == MathSize::Display { - let height = scaled!(self, styles, display_operator_min_height) - .max(SQRT_2 * glyph.height()); - glyph.stretch_vertical(self, height, Abs::zero()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(self); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. - let mut fragments = vec![]; - for c in text.chars() { - let c = styled_char(styles, c, false); - fragments.push(GlyphFragment::new(self, styles, c, span).into()); - } - let frame = MathRun::new(fragments).into_frame(self, styles); - FrameFragment::new(self, styles, frame).with_text_like(true).into() - } else { - let local = [ - TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), - TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)), - TextElem::set_size(TextSize(scaled_font_size(self, styles).into())), - ] - .map(|p| p.wrap()); - - // Anything else is handled by Typst's standard text layout. - let styles = styles.chain(&local); - let text: EcoString = - text.chars().map(|c| styled_char(styles, c, false)).collect(); - if text.contains(is_newline) { - let mut fragments = vec![]; - for (i, piece) in text.split(is_newline).enumerate() { - if i != 0 { - fragments.push(MathFragment::Linebreak); - } - if !piece.is_empty() { - fragments - .push(self.layout_complex_text(piece, span, styles)?.into()); - } - } - let mut frame = MathRun::new(fragments).into_frame(self, styles); - let axis = scaled!(self, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(self, styles, frame).into() - } else { - self.layout_complex_text(&text, span, styles)?.into() - } - }; - Ok(fragment) - } - - /// Layout the given text string into a [`FrameFragment`]. - fn layout_complex_text( - &mut self, - text: &str, - span: Span, - styles: StyleChain, - ) -> SourceResult<FrameFragment> { - // There isn't a natural width for a paragraph in a math environment; - // because it will be placed somewhere probably not at the left margin - // it will overflow. So emulate an `hbox` instead and allow the paragraph - // to extend as far as needed. - let spaced = text.graphemes(true).nth(1).is_some(); - let elem = TextElem::packed(text).spanned(span); - let frame = crate::layout::layout_inline( - self.engine, - &StyleVec::wrap(eco_vec![elem]), - self.locator.next(&span), - styles, - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); - - Ok(FrameFragment::new(self, styles, frame) - .with_class(MathClass::Alphabetic) - .with_text_like(true) - .with_spaced(spaced)) - } -} - -/// Converts some unit to an absolute length with the current font & font size. -pub(super) trait Scaled { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; -} - -impl Scaled for i16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) - } -} - -impl Scaled for u16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) - } -} - -impl Scaled for MathValue<'_> { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - self.value.scaled(ctx, font_size) - } -} - -/// An OpenType substitution table that is applicable to glyph-wise substitutions. -pub enum GlyphwiseSubsts<'a> { - Single(SingleSubstitution<'a>), - Alternate(AlternateSubstitution<'a>, u32), -} - -impl<'a> GlyphwiseSubsts<'a> { - pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> { - let table = gsub - .features - .find(ttf_parser::Tag(feature.tag.0)) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index))?; - let table = table.subtables.get::<SubstitutionSubtable>(0)?; - match table { - SubstitutionSubtable::Single(single_glyphs) => { - Some(Self::Single(single_glyphs)) - } - SubstitutionSubtable::Alternate(alt_glyphs) => { - Some(Self::Alternate(alt_glyphs, feature.value)) - } - _ => None, - } - } - - pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> { - match self { - Self::Single(single) => match single { - SingleSubstitution::Format1 { coverage, delta } => coverage - .get(glyph_id) - .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))), - SingleSubstitution::Format2 { coverage, substitutes } => { - coverage.get(glyph_id).and_then(|idx| substitutes.get(idx)) - } - }, - Self::Alternate(alternate, value) => alternate - .coverage - .get(glyph_id) - .and_then(|idx| alternate.alternate_sets.get(idx)) - .and_then(|set| set.alternates.get(*value as u16)), - } - } - - pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { - self.try_apply(glyph_id).unwrap_or(glyph_id) - } -} diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs deleted file mode 100644 index 19e3ce57..00000000 --- a/crates/typst/src/math/equation.rs +++ /dev/null @@ -1,576 +0,0 @@ -use std::num::NonZeroUsize; - -use unicode_math_class::MathClass; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, - Styles, Synthesize, -}; -use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Locator}; -use crate::layout::{ - layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, Em, FixedAlignment, - Fragment, Frame, InlineElem, InlineItem, OuterHAlignment, Point, Region, Regions, - Size, SpecificAlignment, VAlignment, -}; -use crate::math::{ - scaled_font_size, MathContext, MathRunFrameBuilder, MathSize, MathVariant, -}; -use crate::model::{Numbering, Outlinable, ParElem, ParLine, Refable, Supplement}; -use crate::syntax::Span; -use crate::text::{ - families, variant, Font, FontFamily, FontList, FontWeight, LocalName, TextEdgeBounds, - TextElem, -}; -use crate::utils::{NonZeroExt, Numeric}; -use crate::World; - -/// A mathematical equation. -/// -/// Can be displayed inline with text or as a separate block. -/// -/// # Example -/// ```example -/// #set text(font: "New Computer Modern") -/// -/// Let $a$, $b$, and $c$ be the side -/// lengths of right-angled triangle. -/// Then, we know that: -/// $ a^2 + b^2 = c^2 $ -/// -/// Prove by induction: -/// $ sum_(k=1)^n k = (n(n+1)) / 2 $ -/// ``` -/// -/// By default, block-level equations will not break across pages. This can be -/// changed through `{show math.equation: set block(breakable: true)}`. -/// -/// # Syntax -/// This function also has dedicated syntax: Write mathematical markup within -/// dollar signs to create an equation. Starting and ending the equation with at -/// least one space lifts it into a separate block that is centered -/// horizontally. For more details about math syntax, see the -/// [main math page]($category/math). -#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] -pub struct EquationElem { - /// Whether the equation is displayed as a separate block. - #[default(false)] - pub block: bool, - - /// How to [number]($numbering) block-level equations. - /// - /// ```example - /// #set math.equation(numbering: "(1)") - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - #[borrowed] - pub numbering: Option<Numbering>, - - /// The alignment of the equation numbering. - /// - /// By default, the alignment is `{end + horizon}`. For the horizontal - /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}` - /// of the text direction; for the vertical component, you can use - /// `{top}`, `{horizon}`, or `{bottom}`. - /// - /// ```example - /// #set math.equation(numbering: "(1)", number-align: bottom) - /// - /// We can calculate: - /// $ E &= sqrt(m_0^2 + p^2) \ - /// &approx 125 "GeV" $ - /// ``` - #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))] - pub number_align: SpecificAlignment<OuterHAlignment, VAlignment>, - - /// A supplement for the equation. - /// - /// For references to equations, this is added before the referenced number. - /// - /// If a function is specified, it is passed the referenced equation and - /// should return content. - /// - /// ```example - /// #set math.equation(numbering: "(1)", supplement: [Eq.]) - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub supplement: Smart<Option<Supplement>>, - - /// The contents of the equation. - #[required] - pub body: Content, - - /// The size of the glyphs. - #[internal] - #[default(MathSize::Text)] - #[ghost] - pub size: MathSize, - - /// The style variant to select. - #[internal] - #[ghost] - pub variant: MathVariant, - - /// Affects the height of exponents. - #[internal] - #[default(false)] - #[ghost] - pub cramped: bool, - - /// Whether to use bold glyphs. - #[internal] - #[default(false)] - #[ghost] - pub bold: bool, - - /// Whether to use italic glyphs. - #[internal] - #[ghost] - pub italic: Smart<bool>, - - /// A forced class to use for all fragment. - #[internal] - #[ghost] - pub class: Option<MathClass>, -} - -impl Synthesize for Packed<EquationElem> { - fn synthesize( - &mut self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult<()> { - let supplement = match self.as_ref().supplement(styles) { - Smart::Auto => TextElem::packed(Self::local_name_in(styles)), - Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => { - supplement.resolve(engine, styles, [self.clone().pack()])? - } - }; - - self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); - Ok(()) - } -} - -impl Show for Packed<EquationElem> { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - if self.block(styles) { - Ok(BlockElem::multi_layouter(self.clone(), layout_equation_block) - .pack() - .spanned(self.span())) - } else { - Ok(InlineElem::layouter(self.clone(), layout_equation_inline) - .pack() - .spanned(self.span())) - } - } -} - -impl ShowSet for Packed<EquationElem> { - fn show_set(&self, styles: StyleChain) -> Styles { - let mut out = Styles::new(); - if self.block(styles) { - out.set(AlignElem::set_alignment(Alignment::CENTER)); - out.set(BlockElem::set_breakable(false)); - out.set(ParLine::set_numbering(None)); - out.set(EquationElem::set_size(MathSize::Display)); - } else { - out.set(EquationElem::set_size(MathSize::Text)); - } - out.set(TextElem::set_weight(FontWeight::from_number(450))); - out.set(TextElem::set_font(FontList(vec![FontFamily::new( - "New Computer Modern Math", - )]))); - out - } -} - -impl Count for Packed<EquationElem> { - fn update(&self) -> Option<CounterUpdate> { - (self.block(StyleChain::default()) && self.numbering().is_some()) - .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) - } -} - -impl LocalName for Packed<EquationElem> { - const KEY: &'static str = "equation"; -} - -impl Refable for Packed<EquationElem> { - fn supplement(&self) -> Content { - // After synthesis, this should always be custom content. - match (**self).supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - } - } - - fn counter(&self) -> Counter { - Counter::of(EquationElem::elem()) - } - - fn numbering(&self) -> Option<&Numbering> { - (**self).numbering(StyleChain::default()).as_ref() - } -} - -impl Outlinable for Packed<EquationElem> { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult<Option<Content>> { - if !self.block(StyleChain::default()) { - return Ok(None); - } - let Some(numbering) = self.numbering() else { - return Ok(None); - }; - - // After synthesis, this should always be custom content. - let mut supplement = match (**self).supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - }; - - if !supplement.is_empty() { - supplement += TextElem::packed("\u{a0}"); - } - - let numbers = self.counter().display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - Ok(Some(supplement + numbers)) - } -} - -/// Layout an inline equation (in a paragraph). -#[typst_macros::time(span = elem.span())] -fn layout_equation_inline( - elem: &Packed<EquationElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, -) -> SourceResult<Vec<InlineItem>> { - assert!(!elem.block(styles)); - - let font = find_math_font(engine, styles, elem.span())?; - - let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font); - let run = ctx.layout_into_run(&elem.body, styles)?; - - let mut items = if run.row_count() == 1 { - run.into_par_items() - } else { - vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())] - }; - - // An empty equation should have a height, so we still create a frame - // (which is then resized in the loop). - if items.is_empty() { - items.push(InlineItem::Frame(Frame::soft(Size::zero()))); - } - - for item in &mut items { - let InlineItem::Frame(frame) = item else { continue }; - - let font_size = scaled_font_size(&ctx, styles); - let slack = ParElem::leading_in(styles) * 0.7; - - let (t, b) = font.edges( - TextElem::top_edge_in(styles), - TextElem::bottom_edge_in(styles), - font_size, - TextEdgeBounds::Frame(frame), - ); - - let ascent = t.max(frame.ascent() - slack); - let descent = b.max(frame.descent() - slack); - frame.translate(Point::with_y(ascent - frame.baseline())); - frame.size_mut().y = ascent + descent; - } - - Ok(items) -} - -/// Layout a block-level equation (in a flow). -#[typst_macros::time(span = elem.span())] -fn layout_equation_block( - elem: &Packed<EquationElem>, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, -) -> SourceResult<Fragment> { - assert!(elem.block(styles)); - - let span = elem.span(); - let font = find_math_font(engine, styles, span)?; - - let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font); - let full_equation_builder = ctx - .layout_into_run(&elem.body, styles)? - .multiline_frame_builder(&ctx, styles); - let width = full_equation_builder.size.x; - - let equation_builders = if BlockElem::breakable_in(styles) { - let mut rows = full_equation_builder.frames.into_iter().peekable(); - let mut equation_builders = vec![]; - let mut last_first_pos = Point::zero(); - let mut regions = regions; - - loop { - // Keep track of the position of the first row in this region, - // so that the offset can be reverted later. - let Some(&(_, first_pos)) = rows.peek() else { break }; - last_first_pos = first_pos; - - let mut frames = vec![]; - let mut height = Abs::zero(); - while let Some((sub, pos)) = rows.peek() { - let mut pos = *pos; - pos.y -= first_pos.y; - - // Finish this region if the line doesn't fit. Only do it if - // we placed at least one line _or_ we still have non-last - // regions. Crucially, we don't want to infinitely create - // new regions which are too small. - if !regions.size.y.fits(sub.height() + pos.y) - && (regions.may_progress() - || (regions.may_break() && !frames.is_empty())) - { - break; - } - - let (sub, _) = rows.next().unwrap(); - height = height.max(pos.y + sub.height()); - frames.push((sub, pos)); - } - - equation_builders - .push(MathRunFrameBuilder { frames, size: Size::new(width, height) }); - regions.next(); - } - - // Append remaining rows to the equation builder of the last region. - if let Some(equation_builder) = equation_builders.last_mut() { - equation_builder.frames.extend(rows.map(|(frame, mut pos)| { - pos.y -= last_first_pos.y; - (frame, pos) - })); - - let height = equation_builder - .frames - .iter() - .map(|(frame, pos)| frame.height() + pos.y) - .max() - .unwrap_or(equation_builder.size.y); - - equation_builder.size.y = height; - } - - // Ensure that there is at least one frame, even for empty equations. - if equation_builders.is_empty() { - equation_builders - .push(MathRunFrameBuilder { frames: vec![], size: Size::zero() }); - } - - equation_builders - } else { - vec![full_equation_builder] - }; - - let Some(numbering) = (**elem).numbering(styles) else { - let frames = equation_builders - .into_iter() - .map(MathRunFrameBuilder::build) - .collect(); - return Ok(Fragment::frames(frames)); - }; - - let pod = Region::new(regions.base(), Axes::splat(false)); - let counter = Counter::of(EquationElem::elem()) - .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? - .spanned(span); - let number = layout_frame(engine, &counter, locator.next(&()), styles, pod)?; - - static NUMBER_GUTTER: Em = Em::new(0.5); - let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); - - let number_align = match elem.number_align(styles) { - SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon), - SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v), - SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v), - }; - - // Add equation numbers to each equation region. - let region_count = equation_builders.len(); - let frames = equation_builders - .into_iter() - .map(|builder| { - if builder.frames.is_empty() && region_count > 1 { - // Don't number empty regions, but do number empty equations. - return builder.build(); - } - add_equation_number( - builder, - number.clone(), - number_align.resolve(styles), - AlignElem::alignment_in(styles).resolve(styles).x, - regions.size.x, - full_number_width, - ) - }) - .collect(); - - Ok(Fragment::frames(frames)) -} - -fn find_math_font( - engine: &mut Engine<'_>, - styles: StyleChain, - span: Span, -) -> SourceResult<Font> { - let variant = variant(styles); - let world = engine.world; - let Some(font) = families(styles).find_map(|family| { - let id = world.book().select(family, variant)?; - let font = world.font(id)?; - let _ = font.ttf().tables().math?.constants?; - Some(font) - }) else { - bail!(span, "current font does not support math"); - }; - Ok(font) -} - -fn add_equation_number( - equation_builder: MathRunFrameBuilder, - number: Frame, - number_align: Axes<FixedAlignment>, - equation_align: FixedAlignment, - region_size_x: Abs, - full_number_width: Abs, -) -> Frame { - let first = - equation_builder.frames.first().map_or( - (equation_builder.size, Point::zero(), Abs::zero()), - |(frame, pos)| (frame.size(), *pos, frame.baseline()), - ); - let last = - equation_builder.frames.last().map_or( - (equation_builder.size, Point::zero(), Abs::zero()), - |(frame, pos)| (frame.size(), *pos, frame.baseline()), - ); - let line_count = equation_builder.frames.len(); - let mut equation = equation_builder.build(); - - let width = if region_size_x.is_finite() { - region_size_x - } else { - equation.width() + 2.0 * full_number_width - }; - - let is_multiline = line_count >= 2; - let resizing_offset = resize_equation( - &mut equation, - &number, - number_align, - equation_align, - width, - is_multiline, - [first, last], - ); - equation.translate(Point::with_x(match (equation_align, number_align.x) { - (FixedAlignment::Start, FixedAlignment::Start) => full_number_width, - (FixedAlignment::End, FixedAlignment::End) => -full_number_width, - _ => Abs::zero(), - })); - - let x = match number_align.x { - FixedAlignment::Start => Abs::zero(), - FixedAlignment::End => equation.width() - number.width(), - _ => unreachable!(), - }; - let y = { - let align_baselines = |(_, pos, baseline): (_, Point, Abs), number: &Frame| { - resizing_offset.y + pos.y + baseline - number.baseline() - }; - match number_align.y { - FixedAlignment::Start => align_baselines(first, &number), - FixedAlignment::Center if !is_multiline => align_baselines(first, &number), - // In this case, the center lines (not baselines) of the number frame - // and the equation frame shall be aligned. - FixedAlignment::Center => (equation.height() - number.height()) / 2.0, - FixedAlignment::End => align_baselines(last, &number), - } - }; - - equation.push_frame(Point::new(x, y), number); - equation -} - -/// Resize the equation's frame accordingly so that it encompasses the number. -fn resize_equation( - equation: &mut Frame, - number: &Frame, - number_align: Axes<FixedAlignment>, - equation_align: FixedAlignment, - width: Abs, - is_multiline: bool, - [first, last]: [(Axes<Abs>, Point, Abs); 2], -) -> Point { - if matches!(number_align.y, FixedAlignment::Center if is_multiline) { - // In this case, the center lines (not baselines) of the number frame - // and the equation frame shall be aligned. - return equation.resize( - Size::new(width, equation.height().max(number.height())), - Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Center), - ); - } - - let excess_above = Abs::zero().max({ - if !is_multiline || matches!(number_align.y, FixedAlignment::Start) { - let (.., baseline) = first; - number.baseline() - baseline - } else { - Abs::zero() - } - }); - let excess_below = Abs::zero().max({ - if !is_multiline || matches!(number_align.y, FixedAlignment::End) { - let (size, .., baseline) = last; - (number.height() - number.baseline()) - (size.y - baseline) - } else { - Abs::zero() - } - }); - - // The vertical expansion is asymmetric on the top and bottom edges, so we - // first align at the top then translate the content downward later. - let resizing_offset = equation.resize( - Size::new(width, equation.height() + excess_above + excess_below), - Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Start), - ); - equation.translate(Point::with_y(excess_above)); - resizing_offset + Point::with_y(excess_above) -} diff --git a/crates/typst/src/math/lr.rs b/crates/typst/src/math/lr.rs deleted file mode 100644 index ccaf2959..00000000 --- a/crates/typst/src/math/lr.rs +++ /dev/null @@ -1,272 +0,0 @@ -use unicode_math_class::MathClass; - -use crate::diag::SourceResult; -use crate::foundations::{elem, func, Content, NativeElement, Packed, Smart, StyleChain}; -use crate::layout::{Abs, Axis, Em, Length, Rel}; -use crate::math::{ - stretch_fragment, EquationElem, LayoutMath, MathContext, MathFragment, Scaled, -}; -use crate::text::TextElem; - -/// How much less high scaled delimiters can be than what they wrap. -pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); - -/// Scales delimiters. -/// -/// While matched delimiters scale by default, this can be used to scale -/// unmatched delimiters and to control the delimiter scaling more precisely. -#[elem(title = "Left/Right", LayoutMath)] -pub struct LrElem { - /// The size of the brackets, relative to the height of the wrapped content. - pub size: Smart<Rel<Length>>, - - /// The delimited content, including the delimiters. - #[required] - #[parse( - let mut arguments = args.all::<Content>()?.into_iter(); - let mut body = arguments.next().unwrap_or_default(); - arguments.for_each(|arg| body += TextElem::packed(',') + arg); - body - )] - pub body: Content, -} - -impl LayoutMath for Packed<LrElem> { - #[typst_macros::time(name = "math.lr", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let mut body = self.body(); - - // Extract from an EquationElem. - if let Some(equation) = body.to_packed::<EquationElem>() { - body = equation.body(); - } - - // Extract implicit LrElem. - if let Some(lr) = body.to_packed::<LrElem>() { - if lr.size(styles).is_auto() { - body = lr.body(); - } - } - - let mut fragments = ctx.layout_into_fragments(body, styles)?; - let axis = scaled!(ctx, styles, axis_height); - let max_extent = fragments - .iter() - .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) - .max() - .unwrap_or_default(); - - let relative_to = 2.0 * max_extent; - let height = self.size(styles); - - // Scale up fragments at both ends. - match fragments.as_mut_slice() { - [one] => scale(ctx, styles, one, relative_to, height, None), - [first, .., last] => { - scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening)); - scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing)); - } - _ => {} - } - - // Handle MathFragment::Variant fragments that should be scaled up. - for fragment in &mut fragments { - if let MathFragment::Variant(ref mut variant) = fragment { - if variant.mid_stretched == Some(false) { - variant.mid_stretched = Some(true); - scale( - ctx, - styles, - fragment, - relative_to, - height, - Some(MathClass::Large), - ); - } - } - } - - // Remove weak SpacingFragment immediately after the opening or immediately - // before the closing. - let original_len = fragments.len(); - let mut index = 0; - fragments.retain(|fragment| { - index += 1; - (index != 2 && index + 1 != original_len) - || !matches!(fragment, MathFragment::Spacing(_, true)) - }); - - ctx.extend(fragments); - - Ok(()) - } -} - -/// Scales delimiters vertically to the nearest surrounding `{lr()}` group. -/// -/// ```example -/// $ { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } $ -/// ``` -#[elem(LayoutMath)] -pub struct MidElem { - /// The content to be scaled. - #[required] - pub body: Content, -} - -impl LayoutMath for Packed<MidElem> { - #[typst_macros::time(name = "math.mid", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let mut fragments = ctx.layout_into_fragments(self.body(), styles)?; - - for fragment in &mut fragments { - match fragment { - MathFragment::Glyph(glyph) => { - let mut new = glyph.clone().into_variant(); - new.mid_stretched = Some(false); - new.class = MathClass::Fence; - *fragment = MathFragment::Variant(new); - } - MathFragment::Variant(variant) => { - variant.mid_stretched = Some(false); - variant.class = MathClass::Fence; - } - _ => {} - } - } - - ctx.extend(fragments); - Ok(()) - } -} - -/// Scale a math fragment to a height. -fn scale( - ctx: &mut MathContext, - styles: StyleChain, - fragment: &mut MathFragment, - relative_to: Abs, - height: Smart<Rel<Length>>, - apply: Option<MathClass>, -) { - if matches!( - fragment.class(), - MathClass::Opening | MathClass::Closing | MathClass::Fence - ) { - // This unwrap doesn't really matter. If it is None, then the fragment - // won't be stretchable anyways. - let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default()); - stretch_fragment( - ctx, - styles, - fragment, - Some(Axis::Y), - Some(relative_to), - height, - short_fall, - ); - - if let Some(class) = apply { - fragment.set_class(class); - } - } -} - -/// Floors an expression. -/// -/// ```example -/// $ floor(x/2) $ -/// ``` -#[func] -pub fn floor( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to floor. - body: Content, -) -> Content { - delimited(body, '⌊', '⌋', size) -} - -/// Ceils an expression. -/// -/// ```example -/// $ ceil(x/2) $ -/// ``` -#[func] -pub fn ceil( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to ceil. - body: Content, -) -> Content { - delimited(body, '⌈', '⌉', size) -} - -/// Rounds an expression. -/// -/// ```example -/// $ round(x/2) $ -/// ``` -#[func] -pub fn round( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to round. - body: Content, -) -> Content { - delimited(body, '⌊', '⌉', size) -} - -/// Takes the absolute value of an expression. -/// -/// ```example -/// $ abs(x/2) $ -/// ``` -#[func] -pub fn abs( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to take the absolute value of. - body: Content, -) -> Content { - delimited(body, '|', '|', size) -} - -/// Takes the norm of an expression. -/// -/// ```example -/// $ norm(x/2) $ -/// ``` -#[func] -pub fn norm( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to take the norm of. - body: Content, -) -> Content { - delimited(body, '‖', '‖', size) -} - -fn delimited( - body: Content, - left: char, - right: char, - size: Option<Smart<Rel<Length>>>, -) -> Content { - let span = body.span(); - let mut elem = LrElem::new(Content::sequence([ - TextElem::packed(left), - body, - TextElem::packed(right), - ])); - // Push size only if size is provided - if let Some(size) = size { - elem.push_size(size); - } - elem.pack().spanned(span) -} diff --git a/crates/typst/src/math/spacing.rs b/crates/typst/src/math/spacing.rs deleted file mode 100644 index 377a2248..00000000 --- a/crates/typst/src/math/spacing.rs +++ /dev/null @@ -1,66 +0,0 @@ -use unicode_math_class::MathClass; - -use crate::foundations::{NativeElement, Scope}; -use crate::layout::{Abs, Em, HElem}; -use crate::math::{MathFragment, MathSize}; - -pub(super) const THIN: Em = Em::new(1.0 / 6.0); -pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); -pub(super) const THICK: Em = Em::new(5.0 / 18.0); -pub(super) const QUAD: Em = Em::new(1.0); -pub(super) const WIDE: Em = Em::new(2.0); - -/// Hook up all spacings. -pub(super) fn define(math: &mut Scope) { - math.define("thin", HElem::new(THIN.into()).pack()); - math.define("med", HElem::new(MEDIUM.into()).pack()); - math.define("thick", HElem::new(THICK.into()).pack()); - math.define("quad", HElem::new(QUAD.into()).pack()); - math.define("wide", HElem::new(WIDE.into()).pack()); -} - -/// Create the spacing between two fragments in a given style. -pub(super) fn spacing( - l: &MathFragment, - space: Option<MathFragment>, - r: &MathFragment, -) -> Option<MathFragment> { - use MathClass::*; - - let resolve = |v: Em, size_ref: &MathFragment| -> Option<MathFragment> { - let width = size_ref.font_size().map_or(Abs::zero(), |size| v.at(size)); - Some(MathFragment::Spacing(width, false)) - }; - let script = |f: &MathFragment| f.math_size().is_some_and(|s| s <= MathSize::Script); - - match (l.class(), r.class()) { - // No spacing before punctuation; thin spacing after punctuation, unless - // in script size. - (_, Punctuation) => None, - (Punctuation, _) if !script(l) => resolve(THIN, l), - - // No spacing after opening delimiters and before closing delimiters. - (Opening, _) | (_, Closing) => None, - - // Thick spacing around relations, unless followed by a another relation - // or in script size. - (Relation, Relation) => None, - (Relation, _) if !script(l) => resolve(THICK, l), - (_, Relation) if !script(r) => resolve(THICK, r), - - // Medium spacing around binary operators, unless in script size. - (Binary, _) if !script(l) => resolve(MEDIUM, l), - (_, Binary) if !script(r) => resolve(MEDIUM, r), - - // Thin spacing around large operators, unless to the left of - // an opening delimiter. TeXBook, p170 - (Large, Opening | Fence) => None, - (Large, _) => resolve(THIN, l), - (_, Large) => resolve(THIN, r), - - // Spacing around spaced frames. - _ if (l.is_spaced() || r.is_spaced()) => space, - - _ => None, - } -} diff --git a/crates/typst/src/math/style.rs b/crates/typst/src/math/style.rs deleted file mode 100644 index 67a198a7..00000000 --- a/crates/typst/src/math/style.rs +++ /dev/null @@ -1,518 +0,0 @@ -use crate::foundations::{func, Cast, Content, Smart, Style, StyleChain}; -use crate::layout::Abs; -use crate::math::{EquationElem, MathContext}; -use crate::text::TextElem; -use crate::utils::LazyHash; - -/// Bold font style in math. -/// -/// ```example -/// $ bold(A) := B^+ $ -/// ``` -#[func(keywords = ["mathbf"])] -pub fn bold( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_bold(true)) -} - -/// Upright (non-italic) font style in math. -/// -/// ```example -/// $ upright(A) != A $ -/// ``` -#[func(keywords = ["mathup"])] -pub fn upright( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_italic(Smart::Custom(false))) -} - -/// Italic font style in math. -/// -/// For roman letters and greek lowercase letters, this is already the default. -#[func(keywords = ["mathit"])] -pub fn italic( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_italic(Smart::Custom(true))) -} - -/// Serif (roman) font style in math. -/// -/// This is already the default. -#[func(keywords = ["mathrm"])] -pub fn serif( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_variant(MathVariant::Serif)) -} - -/// Sans-serif font style in math. -/// -/// ```example -/// $ sans(A B C) $ -/// ``` -#[func(title = "Sans Serif", keywords = ["mathsf"])] -pub fn sans( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_variant(MathVariant::Sans)) -} - -/// Calligraphic font style in math. -/// -/// ```example -/// Let $cal(P)$ be the set of ... -/// ``` -/// -/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these -/// styles share the same Unicode codepoints. Switching between the styles is -/// thus only possible if supported by the font via -/// [font features]($text.features). -/// -/// For the default math font, the roundhand style is available through the -/// `ss01` feature. Therefore, you could define your own version of `\mathscr` -/// like this: -/// -/// ```example -/// #let scr(it) = text( -/// features: ("ss01",), -/// box($cal(it)$), -/// ) -/// -/// We establish $cal(P) != scr(P)$. -/// ``` -/// -/// (The box is not conceptually necessary, but unfortunately currently needed -/// due to limitations in Typst's text style handling in math.) -#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] -pub fn cal( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_variant(MathVariant::Cal)) -} - -/// Fraktur font style in math. -/// -/// ```example -/// $ frak(P) $ -/// ``` -#[func(title = "Fraktur", keywords = ["mathfrak"])] -pub fn frak( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_variant(MathVariant::Frak)) -} - -/// Monospace font style in math. -/// -/// ```example -/// $ mono(x + y = z) $ -/// ``` -#[func(title = "Monospace", keywords = ["mathtt"])] -pub fn mono( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_variant(MathVariant::Mono)) -} - -/// Blackboard bold (double-struck) font style in math. -/// -/// For uppercase latin letters, blackboard bold is additionally available -/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`. -/// -/// ```example -/// $ bb(b) $ -/// $ bb(N) = NN $ -/// $ f: NN -> RR $ -/// ``` -#[func(title = "Blackboard Bold", keywords = ["mathbb"])] -pub fn bb( - /// The content to style. - body: Content, -) -> Content { - body.styled(EquationElem::set_variant(MathVariant::Bb)) -} - -/// Forced display style in math. -/// -/// This is the normal size for block equations. -/// -/// ```example -/// $sum_i x_i/2 = display(sum_i x_i/2)$ -/// ``` -#[func(title = "Display Size", keywords = ["displaystyle"])] -pub fn display( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(false)] - cramped: bool, -) -> Content { - body.styled(EquationElem::set_size(MathSize::Display)) - .styled(EquationElem::set_cramped(cramped)) -} - -/// Forced inline (text) style in math. -/// -/// This is the normal size for inline equations. -/// -/// ```example -/// $ sum_i x_i/2 -/// = inline(sum_i x_i/2) $ -/// ``` -#[func(title = "Inline Size", keywords = ["textstyle"])] -pub fn inline( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(false)] - cramped: bool, -) -> Content { - body.styled(EquationElem::set_size(MathSize::Text)) - .styled(EquationElem::set_cramped(cramped)) -} - -/// Forced script style in math. -/// -/// This is the smaller size used in powers or sub- or superscripts. -/// -/// ```example -/// $sum_i x_i/2 = script(sum_i x_i/2)$ -/// ``` -#[func(title = "Script Size", keywords = ["scriptstyle"])] -pub fn script( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(true)] - cramped: bool, -) -> Content { - body.styled(EquationElem::set_size(MathSize::Script)) - .styled(EquationElem::set_cramped(cramped)) -} - -/// Forced second script style in math. -/// -/// This is the smallest size, used in second-level sub- and superscripts -/// (script of the script). -/// -/// ```example -/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ -/// ``` -#[func(title = "Script-Script Size", keywords = ["scriptscriptstyle"])] -pub fn sscript( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(true)] - cramped: bool, -) -> Content { - body.styled(EquationElem::set_size(MathSize::ScriptScript)) - .styled(EquationElem::set_cramped(cramped)) -} - -/// The size of elements in an equation. -/// -/// See the TeXbook p. 141. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)] -pub enum MathSize { - /// Second-level sub- and superscripts. - ScriptScript, - /// Sub- and superscripts. - Script, - /// Math in text. - Text, - /// Math on its own line. - Display, -} - -impl MathSize { - /// The scaling factor. - pub fn factor(self, ctx: &MathContext) -> f64 { - match self { - Self::Display | Self::Text => 1.0, - Self::Script => percent!(ctx, script_percent_scale_down), - Self::ScriptScript => percent!(ctx, script_script_percent_scale_down), - } - } -} - -/// A mathematical style variant, as defined by Unicode. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)] -pub enum MathVariant { - #[default] - Serif, - Sans, - Cal, - Frak, - Mono, - Bb, -} - -/// Get the font size scaled with the `MathSize`. -pub fn scaled_font_size(ctx: &MathContext, styles: StyleChain) -> Abs { - EquationElem::size_in(styles).factor(ctx) * TextElem::size_in(styles) -} - -/// Styles something as cramped. -pub fn style_cramped() -> LazyHash<Style> { - EquationElem::set_cramped(true).wrap() -} - -/// The style for subscripts in the current style. -pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] { - [style_for_superscript(styles), EquationElem::set_cramped(true).wrap()] -} - -/// The style for superscripts in the current style. -pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> { - EquationElem::set_size(match EquationElem::size_in(styles) { - MathSize::Display | MathSize::Text => MathSize::Script, - MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, - }) - .wrap() -} - -/// The style for numerators in the current style. -pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> { - EquationElem::set_size(match EquationElem::size_in(styles) { - MathSize::Display => MathSize::Text, - MathSize::Text => MathSize::Script, - MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, - }) - .wrap() -} - -/// The style for denominators in the current style. -pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] { - [style_for_numerator(styles), EquationElem::set_cramped(true).wrap()] -} - -/// Select the correct styled math letter. -/// -/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings> -/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols> -pub fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char { - use MathVariant::*; - - let variant = EquationElem::variant_in(styles); - let bold = EquationElem::bold_in(styles); - let italic = EquationElem::italic_in(styles).unwrap_or( - auto_italic - && matches!( - c, - 'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' | - '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' - ) - && matches!(variant, Sans | Serif), - ); - - if let Some(c) = basic_exception(c) { - return c; - } - - if let Some(c) = latin_exception(c, variant, bold, italic) { - return c; - } - - if let Some(c) = greek_exception(c, variant, bold, italic) { - return c; - } - - let base = match c { - 'A'..='Z' => 'A', - 'a'..='z' => 'a', - 'Α'..='Ω' => 'Α', - 'α'..='ω' => 'α', - '0'..='9' => '0', - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => '\u{05D0}', - _ => return c, - }; - - let tuple = (variant, bold, italic); - let start = match c { - // Latin upper. - 'A'..='Z' => match tuple { - (Serif, false, false) => 0x0041, - (Serif, true, false) => 0x1D400, - (Serif, false, true) => 0x1D434, - (Serif, true, true) => 0x1D468, - (Sans, false, false) => 0x1D5A0, - (Sans, true, false) => 0x1D5D4, - (Sans, false, true) => 0x1D608, - (Sans, true, true) => 0x1D63C, - (Cal, false, _) => 0x1D49C, - (Cal, true, _) => 0x1D4D0, - (Frak, false, _) => 0x1D504, - (Frak, true, _) => 0x1D56C, - (Mono, _, _) => 0x1D670, - (Bb, _, _) => 0x1D538, - }, - - // Latin lower. - 'a'..='z' => match tuple { - (Serif, false, false) => 0x0061, - (Serif, true, false) => 0x1D41A, - (Serif, false, true) => 0x1D44E, - (Serif, true, true) => 0x1D482, - (Sans, false, false) => 0x1D5BA, - (Sans, true, false) => 0x1D5EE, - (Sans, false, true) => 0x1D622, - (Sans, true, true) => 0x1D656, - (Cal, false, _) => 0x1D4B6, - (Cal, true, _) => 0x1D4EA, - (Frak, false, _) => 0x1D51E, - (Frak, true, _) => 0x1D586, - (Mono, _, _) => 0x1D68A, - (Bb, _, _) => 0x1D552, - }, - - // Greek upper. - 'Α'..='Ω' => match tuple { - (Serif, false, false) => 0x0391, - (Serif, true, false) => 0x1D6A8, - (Serif, false, true) => 0x1D6E2, - (Serif, true, true) => 0x1D71C, - (Sans, _, false) => 0x1D756, - (Sans, _, true) => 0x1D790, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Greek lower. - 'α'..='ω' => match tuple { - (Serif, false, false) => 0x03B1, - (Serif, true, false) => 0x1D6C2, - (Serif, false, true) => 0x1D6FC, - (Serif, true, true) => 0x1D736, - (Sans, _, false) => 0x1D770, - (Sans, _, true) => 0x1D7AA, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => 0x2135, - - // Numbers. - '0'..='9' => match tuple { - (Serif, false, _) => 0x0030, - (Serif, true, _) => 0x1D7CE, - (Bb, _, _) => 0x1D7D8, - (Sans, false, _) => 0x1D7E2, - (Sans, true, _) => 0x1D7EC, - (Mono, _, _) => 0x1D7F6, - (Cal | Frak, _, _) => return c, - }, - - _ => unreachable!(), - }; - - std::char::from_u32(start + (c as u32 - base as u32)).unwrap() -} - -fn basic_exception(c: char) -> Option<char> { - Some(match c { - '〈' => '⟨', - '〉' => '⟩', - '《' => '⟪', - '》' => '⟫', - _ => return None, - }) -} - -fn latin_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option<char> { - use MathVariant::*; - Some(match (c, variant, bold, italic) { - ('B', Cal, false, _) => 'ℬ', - ('E', Cal, false, _) => 'ℰ', - ('F', Cal, false, _) => 'ℱ', - ('H', Cal, false, _) => 'ℋ', - ('I', Cal, false, _) => 'ℐ', - ('L', Cal, false, _) => 'ℒ', - ('M', Cal, false, _) => 'ℳ', - ('R', Cal, false, _) => 'ℛ', - ('C', Frak, false, _) => 'ℭ', - ('H', Frak, false, _) => 'ℌ', - ('I', Frak, false, _) => 'ℑ', - ('R', Frak, false, _) => 'ℜ', - ('Z', Frak, false, _) => 'ℨ', - ('C', Bb, ..) => 'ℂ', - ('H', Bb, ..) => 'ℍ', - ('N', Bb, ..) => 'ℕ', - ('P', Bb, ..) => 'ℙ', - ('Q', Bb, ..) => 'ℚ', - ('R', Bb, ..) => 'ℝ', - ('Z', Bb, ..) => 'ℤ', - ('D', Bb, _, true) => 'ⅅ', - ('d', Bb, _, true) => 'ⅆ', - ('e', Bb, _, true) => 'ⅇ', - ('i', Bb, _, true) => 'ⅈ', - ('j', Bb, _, true) => 'ⅉ', - ('h', Serif, false, true) => 'ℎ', - ('e', Cal, false, _) => 'ℯ', - ('g', Cal, false, _) => 'ℊ', - ('o', Cal, false, _) => 'ℴ', - ('ı', Serif, .., true) => '𝚤', - ('ȷ', Serif, .., true) => '𝚥', - _ => return None, - }) -} - -fn greek_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option<char> { - use MathVariant::*; - let list = match c { - 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'], - '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'], - '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'], - 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'], - 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'], - 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'], - 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'], - 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'], - 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'], - 'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'], - 'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'], - 'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'], - 'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'], - '∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'], - _ => return None, - }; - - Some(match (variant, bold, italic) { - (Serif, true, false) => list[0], - (Serif, false, true) => list[1], - (Serif, true, true) => list[2], - (Sans, _, false) => list[3], - (Sans, _, true) => list[4], - (Bb, ..) => list[5], - _ => return None, - }) -} diff --git a/crates/typst/src/math/underover.rs b/crates/typst/src/math/underover.rs deleted file mode 100644 index 5ad7f457..00000000 --- a/crates/typst/src/math/underover.rs +++ /dev/null @@ -1,496 +0,0 @@ -use crate::diag::SourceResult; -use crate::foundations::{elem, Content, Packed, StyleChain}; -use crate::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size}; -use crate::math::{ - alignments, scaled_font_size, style_cramped, style_for_subscript, - style_for_superscript, AlignmentResult, FrameFragment, GlyphFragment, LayoutMath, - LeftRightAlternator, MathContext, MathRun, Scaled, -}; -use crate::syntax::Span; -use crate::text::TextElem; -use crate::visualize::{FixedStroke, Geometry}; - -const BRACE_GAP: Em = Em::new(0.25); -const BRACKET_GAP: Em = Em::new(0.25); -const PAREN_GAP: Em = Em::new(0.25); -const SHELL_GAP: Em = Em::new(0.25); - -/// A marker to distinguish under- vs. overlines. -enum Position { - Under, - Over, -} - -/// A horizontal line under content. -/// -/// ```example -/// $ underline(1 + 2 + ... + 5) $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderlineElem { - /// The content above the line. - #[required] - pub body: Content, -} - -impl LayoutMath for Packed<UnderlineElem> { - #[typst_macros::time(name = "math.underline", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverline(ctx, styles, self.body(), self.span(), Position::Under) - } -} - -/// A horizontal line over content. -/// -/// ```example -/// $ overline(1 + 2 + ... + 5) $ -/// ``` -#[elem(LayoutMath)] -pub struct OverlineElem { - /// The content below the line. - #[required] - pub body: Content, -} - -impl LayoutMath for Packed<OverlineElem> { - #[typst_macros::time(name = "math.overline", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverline(ctx, styles, self.body(), self.span(), Position::Over) - } -} - -/// layout under- or overlined content -fn layout_underoverline( - ctx: &mut MathContext, - styles: StyleChain, - body: &Content, - span: Span, - position: Position, -) -> SourceResult<()> { - let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust); - match position { - Position::Under => { - let sep = scaled!(ctx, styles, underbar_extra_descender); - bar_height = scaled!(ctx, styles, underbar_rule_thickness); - let gap = scaled!(ctx, styles, underbar_vertical_gap); - extra_height = sep + bar_height + gap; - - content = ctx.layout_into_fragment(body, styles)?; - - line_pos = Point::with_y(content.height() + gap + bar_height / 2.0); - content_pos = Point::zero(); - baseline = content.ascent(); - line_adjust = -content.italics_correction(); - } - Position::Over => { - let sep = scaled!(ctx, styles, overbar_extra_ascender); - bar_height = scaled!(ctx, styles, overbar_rule_thickness); - let gap = scaled!(ctx, styles, overbar_vertical_gap); - extra_height = sep + bar_height + gap; - - let cramped = style_cramped(); - content = ctx.layout_into_fragment(body, styles.chain(&cramped))?; - - line_pos = Point::with_y(sep + bar_height / 2.0); - content_pos = Point::with_y(extra_height); - baseline = content.ascent() + extra_height; - line_adjust = Abs::zero(); - } - } - - let width = content.width(); - let height = content.height() + extra_height; - let size = Size::new(width, height); - let line_width = width + line_adjust; - - let content_class = content.class(); - let content_is_text_like = content.is_text_like(); - let content_italics_correction = content.italics_correction(); - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(content_pos, content.into_frame()); - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke { - paint: TextElem::fill_in(styles).as_decoration(), - thickness: bar_height, - ..FixedStroke::default() - }), - span, - ), - ); - - ctx.push( - FrameFragment::new(ctx, styles, frame) - .with_class(content_class) - .with_text_like(content_is_text_like) - .with_italics_correction(content_italics_correction), - ); - - Ok(()) -} - -/// A horizontal brace under content, with an optional annotation below. -/// -/// ```example -/// $ underbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderbraceElem { - /// The content above the brace. - #[required] - pub body: Content, - - /// The optional content below the brace. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<UnderbraceElem> { - #[typst_macros::time(name = "math.underbrace", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⏟', - BRACE_GAP, - Position::Under, - self.span(), - ) - } -} - -/// A horizontal brace over content, with an optional annotation above. -/// -/// ```example -/// $ overbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OverbraceElem { - /// The content below the brace. - #[required] - pub body: Content, - - /// The optional content above the brace. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<OverbraceElem> { - #[typst_macros::time(name = "math.overbrace", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⏞', - BRACE_GAP, - Position::Over, - self.span(), - ) - } -} - -/// A horizontal bracket under content, with an optional annotation below. -/// -/// ```example -/// $ underbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderbracketElem { - /// The content above the bracket. - #[required] - pub body: Content, - - /// The optional content below the bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<UnderbracketElem> { - #[typst_macros::time(name = "math.underbracket", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⎵', - BRACKET_GAP, - Position::Under, - self.span(), - ) - } -} - -/// A horizontal bracket over content, with an optional annotation above. -/// -/// ```example -/// $ overbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OverbracketElem { - /// The content below the bracket. - #[required] - pub body: Content, - - /// The optional content above the bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<OverbracketElem> { - #[typst_macros::time(name = "math.overbracket", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⎴', - BRACKET_GAP, - Position::Over, - self.span(), - ) - } -} - -/// A horizontal parenthesis under content, with an optional annotation below. -/// -/// ```example -/// $ underparen(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderparenElem { - /// The content above the parenthesis. - #[required] - pub body: Content, - - /// The optional content below the parenthesis. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<UnderparenElem> { - #[typst_macros::time(name = "math.underparen", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⏝', - PAREN_GAP, - Position::Under, - self.span(), - ) - } -} - -/// A horizontal parenthesis over content, with an optional annotation above. -/// -/// ```example -/// $ overparen(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OverparenElem { - /// The content below the parenthesis. - #[required] - pub body: Content, - - /// The optional content above the parenthesis. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<OverparenElem> { - #[typst_macros::time(name = "math.overparen", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⏜', - PAREN_GAP, - Position::Over, - self.span(), - ) - } -} - -/// A horizontal tortoise shell bracket under content, with an optional annotation below. -/// -/// ```example -/// $ undershell(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UndershellElem { - /// The content above the tortoise shell bracket. - #[required] - pub body: Content, - - /// The optional content below the tortoise shell bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<UndershellElem> { - #[typst_macros::time(name = "math.undershell", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⏡', - SHELL_GAP, - Position::Under, - self.span(), - ) - } -} - -/// A horizontal tortoise shell bracket over content, with an optional annotation above. -/// -/// ```example -/// $ overshell(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OvershellElem { - /// The content below the tortoise shell bracket. - #[required] - pub body: Content, - - /// The optional content above the tortoise shell bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for Packed<OvershellElem> { - #[typst_macros::time(name = "math.overshell", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout_underoverspreader( - ctx, - styles, - self.body(), - &self.annotation(styles), - '⏠', - SHELL_GAP, - Position::Over, - self.span(), - ) - } -} - -/// Layout an over- or underbrace-like object. -#[allow(clippy::too_many_arguments)] -fn layout_underoverspreader( - ctx: &mut MathContext, - styles: StyleChain, - body: &Content, - annotation: &Option<Content>, - c: char, - gap: Em, - position: Position, - span: Span, -) -> SourceResult<()> { - let font_size = scaled_font_size(ctx, styles); - let gap = gap.at(font_size); - let body = ctx.layout_into_run(body, styles)?; - let body_class = body.class(); - let body = body.into_fragment(ctx, styles); - let glyph = GlyphFragment::new(ctx, styles, c, span); - let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); - - let mut rows = vec![]; - let baseline = match position { - Position::Under => { - rows.push(MathRun::new(vec![body])); - rows.push(stretched.into()); - if let Some(annotation) = annotation { - let under_style = style_for_subscript(styles); - let annotation_styles = styles.chain(&under_style); - rows.push(ctx.layout_into_run(annotation, annotation_styles)?); - } - 0 - } - Position::Over => { - if let Some(annotation) = annotation { - let over_style = style_for_superscript(styles); - let annotation_styles = styles.chain(&over_style); - rows.push(ctx.layout_into_run(annotation, annotation_styles)?); - } - rows.push(stretched.into()); - rows.push(MathRun::new(vec![body])); - rows.len() - 1 - } - }; - - let frame = stack( - rows, - FixedAlignment::Center, - gap, - baseline, - LeftRightAlternator::Right, - None, - ); - ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class)); - - Ok(()) -} - -/// Stack rows on top of each other. -/// -/// Add a `gap` between each row and uses the baseline of the `baseline`-th -/// row for the whole frame. `alternator` controls the left/right alternating -/// alignment behavior of `AlignPointElem` in the rows. -pub(super) fn stack( - rows: Vec<MathRun>, - align: FixedAlignment, - gap: Abs, - baseline: usize, - alternator: LeftRightAlternator, - minimum_ascent_descent: Option<(Abs, Abs)>, -) -> Frame { - let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect(); - let AlignmentResult { points, width } = alignments(&rows); - let rows: Vec<_> = rows - .into_iter() - .map(|row| row.into_line_frame(&points, alternator)) - .collect(); - - let padded_height = |height: Abs| { - height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d)) - }; - - let mut frame = Frame::soft(Size::new( - width, - rows.iter().map(|row| padded_height(row.height())).sum::<Abs>() - + rows.len().saturating_sub(1) as f64 * gap, - )); - - let mut y = Abs::zero(); - for (i, row) in rows.into_iter().enumerate() { - let x = if points.is_empty() { - align.position(width - row.width()) - } else { - Abs::zero() - }; - let ascent_padded_part = minimum_ascent_descent - .map_or(Abs::zero(), |(a, _)| (a - row.ascent())) - .max(Abs::zero()); - let pos = Point::new(x, y + ascent_padded_part); - if i == baseline { - frame.set_baseline(y + row.baseline() + ascent_padded_part); - } - y += padded_height(row.height()) + gap; - frame.push_frame(pos, row); - } - - frame -} diff --git a/docs/Cargo.toml b/docs/Cargo.toml index dbae0b77..a24ee254 100644 --- a/docs/Cargo.toml +++ b/docs/Cargo.toml @@ -19,20 +19,19 @@ cli = ["clap", "typst-render", "serde_json"] typst = { workspace = true } typst-assets = { workspace = true, features = ["fonts"] } typst-dev-assets = { workspace = true } -comemo = { workspace = true } +clap = { workspace = true, optional = true } ecow = { workspace = true } heck = { workspace = true } once_cell = { workspace = true } pulldown-cmark = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true, optional = true } serde_yaml = { workspace = true } syntect = { workspace = true, features = ["html"] } typed-arena = { workspace = true } +typst-render = { workspace = true, optional = true } unscanny = { workspace = true } yaml-front-matter = { workspace = true } -clap = { workspace = true, optional = true } -typst-render = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } [lints] workspace = true diff --git a/docs/src/model.rs b/docs/src/model.rs index a4416c3d..b222322a 100644 --- a/docs/src/model.rs +++ b/docs/src/model.rs @@ -49,6 +49,7 @@ impl OutlineItem { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] #[serde(tag = "kind", content = "content")] +#[allow(clippy::large_enum_variant)] pub enum BodyModel { Html(Html), Category(CategoryModel), diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 2923f4d0..b2fedd75 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -15,6 +15,7 @@ harness = false typst = { workspace = true } typst-assets = { workspace = true, features = ["fonts"] } typst-dev-assets = { workspace = true } +typst-library = { workspace = true } typst-pdf = { workspace = true } typst-render = { workspace = true } typst-svg = { workspace = true } @@ -27,7 +28,6 @@ parking_lot = { workspace = true } rayon = { workspace = true } regex = { workspace = true } tiny-skia = { workspace = true } -ttf-parser = { workspace = true } unscanny = { workspace = true } walkdir = { workspace = true } |
