diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/typst-cli/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/typst-cli/src/args.rs | 3 | ||||
| -rw-r--r-- | crates/typst-cli/src/compile.rs | 8 | ||||
| -rw-r--r-- | crates/typst-cli/src/fonts.rs | 4 | ||||
| -rw-r--r-- | crates/typst-cli/src/query.rs | 9 | ||||
| -rw-r--r-- | crates/typst-cli/src/tracing.rs | 5 | ||||
| -rw-r--r-- | crates/typst-cli/src/update.rs | 5 | ||||
| -rw-r--r-- | crates/typst-cli/src/watch.rs | 2 | ||||
| -rw-r--r-- | crates/typst-cli/src/world.rs | 11 | ||||
| -rw-r--r-- | crates/typst-docs/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/typst-docs/src/contribs.rs | 2 | ||||
| -rw-r--r-- | crates/typst-docs/src/html.rs | 13 | ||||
| -rw-r--r-- | crates/typst-docs/src/lib.rs | 252 | ||||
| -rw-r--r-- | crates/typst-docs/src/link.rs | 30 | ||||
| -rw-r--r-- | crates/typst-docs/src/model.rs | 6 | ||||
| -rw-r--r-- | crates/typst-ide/src/analyze.rs | 17 | ||||
| -rw-r--r-- | crates/typst-ide/src/complete.rs | 26 | ||||
| -rw-r--r-- | crates/typst-ide/src/jump.rs | 7 | ||||
| -rw-r--r-- | crates/typst-ide/src/lib.rs | 4 | ||||
| -rw-r--r-- | crates/typst-ide/src/tooltip.rs | 21 | ||||
| -rw-r--r-- | crates/typst-library/Cargo.toml | 53 | ||||
| -rw-r--r-- | crates/typst-library/src/compute/data.rs | 609 | ||||
| -rw-r--r-- | crates/typst-library/src/compute/mod.rs | 20 | ||||
| -rw-r--r-- | crates/typst-library/src/layout/align.rs | 46 | ||||
| -rw-r--r-- | crates/typst-library/src/lib.rs | 170 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/mod.rs | 76 | ||||
| -rw-r--r-- | crates/typst-library/src/prelude.rs | 42 | ||||
| -rw-r--r-- | crates/typst-library/src/shared/ext.rs | 92 | ||||
| -rw-r--r-- | crates/typst-library/src/shared/mod.rs | 7 | ||||
| -rw-r--r-- | crates/typst-library/src/symbols/mod.rs | 17 | ||||
| -rw-r--r-- | crates/typst-library/src/text/misc.rs | 315 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/shape.rs | 547 | ||||
| -rw-r--r-- | crates/typst-macros/src/cast.rs | 59 | ||||
| -rw-r--r-- | crates/typst-macros/src/category.rs | 57 | ||||
| -rw-r--r-- | crates/typst-macros/src/elem.rs | 234 | ||||
| -rw-r--r-- | crates/typst-macros/src/func.rs | 50 | ||||
| -rw-r--r-- | crates/typst-macros/src/lib.rs | 23 | ||||
| -rw-r--r-- | crates/typst-macros/src/scope.rs | 18 | ||||
| -rw-r--r-- | crates/typst-macros/src/symbols.rs | 11 | ||||
| -rw-r--r-- | crates/typst-macros/src/ty.rs | 30 | ||||
| -rw-r--r-- | crates/typst-macros/src/util.rs | 39 | ||||
| -rw-r--r-- | crates/typst-pdf/src/color.rs | 2 | ||||
| -rw-r--r-- | crates/typst-pdf/src/font.rs | 2 | ||||
| -rw-r--r-- | crates/typst-pdf/src/gradient.rs | 15 | ||||
| -rw-r--r-- | crates/typst-pdf/src/image.rs | 5 | ||||
| -rw-r--r-- | crates/typst-pdf/src/lib.rs | 12 | ||||
| -rw-r--r-- | crates/typst-pdf/src/outline.rs | 8 | ||||
| -rw-r--r-- | crates/typst-pdf/src/page.rs | 29 | ||||
| -rw-r--r-- | crates/typst-render/src/lib.rs | 32 | ||||
| -rw-r--r-- | crates/typst-svg/src/lib.rs | 32 | ||||
| -rw-r--r-- | crates/typst-syntax/src/reparser.rs | 2 | ||||
| -rw-r--r-- | crates/typst-syntax/src/span.rs | 4 | ||||
| -rw-r--r-- | crates/typst/Cargo.toml | 18 | ||||
| -rw-r--r-- | crates/typst/assets/cj_linebreak_data.postcard (renamed from crates/typst-library/assets/cj_linebreak_data.postcard) | bin | 18848 -> 18848 bytes | |||
| -rw-r--r-- | crates/typst/assets/icudata.postcard (renamed from crates/typst-library/assets/icudata.postcard) | bin | 352005 -> 352005 bytes | |||
| -rw-r--r-- | crates/typst/assets/syntect.bin (renamed from crates/typst-library/assets/syntect.bin) | bin | 687378 -> 687378 bytes | |||
| -rw-r--r-- | crates/typst/src/diag.rs | 29 | ||||
| -rw-r--r-- | crates/typst/src/eval/access.rs | 99 | ||||
| -rw-r--r-- | crates/typst/src/eval/binding.rs | 179 | ||||
| -rw-r--r-- | crates/typst/src/eval/call.rs | 587 | ||||
| -rw-r--r-- | crates/typst/src/eval/code.rs | 317 | ||||
| -rw-r--r-- | crates/typst/src/eval/flow.rs | 227 | ||||
| -rw-r--r-- | crates/typst/src/eval/import.rs | 227 | ||||
| -rw-r--r-- | crates/typst/src/eval/library.rs | 179 | ||||
| -rw-r--r-- | crates/typst/src/eval/markup.rs | 272 | ||||
| -rw-r--r-- | crates/typst/src/eval/math.rs | 113 | ||||
| -rw-r--r-- | crates/typst/src/eval/mod.rs | 1988 | ||||
| -rw-r--r-- | crates/typst/src/eval/ops.rs | 145 | ||||
| -rw-r--r-- | crates/typst/src/eval/rules.rs | 51 | ||||
| -rw-r--r-- | crates/typst/src/eval/tracer.rs | 4 | ||||
| -rw-r--r-- | crates/typst/src/eval/vm.rs | 127 | ||||
| -rw-r--r-- | crates/typst/src/foundations/args.rs (renamed from crates/typst/src/eval/args.rs) | 2 | ||||
| -rw-r--r-- | crates/typst/src/foundations/array.rs (renamed from crates/typst/src/eval/array.rs) | 22 | ||||
| -rw-r--r-- | crates/typst/src/foundations/auto.rs (renamed from crates/typst/src/eval/auto.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/foundations/bool.rs (renamed from crates/typst/src/eval/bool.rs) | 2 | ||||
| -rw-r--r-- | crates/typst/src/foundations/bytes.rs (renamed from crates/typst/src/eval/bytes.rs) | 2 | ||||
| -rw-r--r-- | crates/typst/src/foundations/calc.rs (renamed from crates/typst-library/src/compute/calc.rs) | 19 | ||||
| -rw-r--r-- | crates/typst/src/foundations/cast.rs (renamed from crates/typst/src/eval/cast.rs) | 7 | ||||
| -rw-r--r-- | crates/typst/src/foundations/content.rs (renamed from crates/typst/src/model/content.rs) | 152 | ||||
| -rw-r--r-- | crates/typst/src/foundations/datetime.rs (renamed from crates/typst/src/eval/datetime.rs) | 65 | ||||
| -rw-r--r-- | crates/typst/src/foundations/dict.rs (renamed from crates/typst/src/eval/dict.rs) | 8 | ||||
| -rw-r--r-- | crates/typst/src/foundations/duration.rs (renamed from crates/typst/src/eval/duration.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/foundations/element.rs (renamed from crates/typst/src/model/element.rs) | 87 | ||||
| -rw-r--r-- | crates/typst/src/foundations/fields.rs (renamed from crates/typst/src/eval/fields.rs) | 7 | ||||
| -rw-r--r-- | crates/typst/src/foundations/float.rs (renamed from crates/typst/src/eval/float.rs) | 4 | ||||
| -rw-r--r-- | crates/typst/src/foundations/func.rs (renamed from crates/typst/src/eval/func.rs) | 368 | ||||
| -rw-r--r-- | crates/typst/src/foundations/int.rs (renamed from crates/typst/src/eval/int.rs) | 2 | ||||
| -rw-r--r-- | crates/typst/src/foundations/label.rs (renamed from crates/typst/src/model/label.rs) | 4 | ||||
| -rw-r--r-- | crates/typst/src/foundations/methods.rs (renamed from crates/typst/src/eval/methods.rs) | 26 | ||||
| -rw-r--r-- | crates/typst/src/foundations/mod.rs (renamed from crates/typst-library/src/compute/foundations.rs) | 119 | ||||
| -rw-r--r-- | crates/typst/src/foundations/module.rs (renamed from crates/typst/src/eval/module.rs) | 4 | ||||
| -rw-r--r-- | crates/typst/src/foundations/none.rs (renamed from crates/typst/src/eval/none.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/foundations/plugin.rs (renamed from crates/typst/src/eval/plugin.rs) | 7 | ||||
| -rw-r--r-- | crates/typst/src/foundations/repr.rs (renamed from crates/typst/src/eval/repr.rs) | 29 | ||||
| -rw-r--r-- | crates/typst/src/foundations/scope.rs (renamed from crates/typst/src/eval/scope.rs) | 75 | ||||
| -rw-r--r-- | crates/typst/src/foundations/selector.rs (renamed from crates/typst/src/model/selector.rs) | 26 | ||||
| -rw-r--r-- | crates/typst/src/foundations/str.rs (renamed from crates/typst/src/eval/str.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/foundations/styles.rs (renamed from crates/typst/src/model/styles.rs) | 87 | ||||
| -rw-r--r-- | crates/typst/src/foundations/sys.rs (renamed from crates/typst-library/src/compute/sys.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/foundations/ty.rs (renamed from crates/typst/src/eval/ty.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/foundations/value.rs (renamed from crates/typst/src/eval/value.rs) | 36 | ||||
| -rw-r--r-- | crates/typst/src/foundations/version.rs (renamed from crates/typst/src/eval/version.rs) | 2 | ||||
| -rw-r--r-- | crates/typst/src/geom/ellipse.rs | 22 | ||||
| -rw-r--r-- | crates/typst/src/geom/mod.rs | 124 | ||||
| -rw-r--r-- | crates/typst/src/geom/path.rs | 102 | ||||
| -rw-r--r-- | crates/typst/src/geom/rect.rs | 599 | ||||
| -rw-r--r-- | crates/typst/src/geom/shape.rs | 44 | ||||
| -rw-r--r-- | crates/typst/src/geom/transform.rs | 126 | ||||
| -rw-r--r-- | crates/typst/src/image/mod.rs | 175 | ||||
| -rw-r--r-- | crates/typst/src/introspection/counter.rs (renamed from crates/typst-library/src/meta/counter.rs) | 21 | ||||
| -rw-r--r-- | crates/typst/src/introspection/introspector.rs (renamed from crates/typst/src/model/introspect.rs) | 128 | ||||
| -rw-r--r-- | crates/typst/src/introspection/locate.rs | 47 | ||||
| -rw-r--r-- | crates/typst/src/introspection/location.rs (renamed from crates/typst/src/model/location.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/introspection/locator.rs | 117 | ||||
| -rw-r--r-- | crates/typst/src/introspection/metadata.rs (renamed from crates/typst-library/src/meta/metadata.rs) | 5 | ||||
| -rw-r--r-- | crates/typst/src/introspection/mod.rs | 109 | ||||
| -rw-r--r-- | crates/typst/src/introspection/query.rs (renamed from crates/typst-library/src/meta/query.rs) | 4 | ||||
| -rw-r--r-- | crates/typst/src/introspection/state.rs (renamed from crates/typst-library/src/meta/state.rs) | 17 | ||||
| -rw-r--r-- | crates/typst/src/layout/abs.rs (renamed from crates/typst/src/geom/abs.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/layout/align.rs (renamed from crates/typst/src/geom/align.rs) | 60 | ||||
| -rw-r--r-- | crates/typst/src/layout/angle.rs (renamed from crates/typst/src/geom/angle.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/layout/axes.rs (renamed from crates/typst/src/geom/axes.rs) | 8 | ||||
| -rw-r--r-- | crates/typst/src/layout/columns.rs (renamed from crates/typst-library/src/layout/columns.rs) | 9 | ||||
| -rw-r--r-- | crates/typst/src/layout/container.rs (renamed from crates/typst-library/src/layout/container.rs) | 14 | ||||
| -rw-r--r-- | crates/typst/src/layout/corners.rs (renamed from crates/typst/src/geom/corners.rs) | 10 | ||||
| -rw-r--r-- | crates/typst/src/layout/dir.rs (renamed from crates/typst/src/geom/dir.rs) | 5 | ||||
| -rw-r--r-- | crates/typst/src/layout/em.rs (renamed from crates/typst/src/geom/em.rs) | 15 | ||||
| -rw-r--r-- | crates/typst/src/layout/flow.rs (renamed from crates/typst-library/src/layout/flow.rs) | 21 | ||||
| -rw-r--r-- | crates/typst/src/layout/fr.rs (renamed from crates/typst/src/geom/fr.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/layout/fragment.rs (renamed from crates/typst-library/src/layout/fragment.rs) | 4 | ||||
| -rw-r--r-- | crates/typst/src/layout/frame.rs (renamed from crates/typst/src/doc.rs) | 326 | ||||
| -rw-r--r-- | crates/typst/src/layout/grid.rs (renamed from crates/typst-library/src/layout/grid.rs) | 16 | ||||
| -rw-r--r-- | crates/typst/src/layout/hide.rs (renamed from crates/typst-library/src/layout/hide.rs) | 5 | ||||
| -rw-r--r-- | crates/typst/src/layout/inline/linebreak.rs (renamed from crates/typst-library/src/text/linebreak.rs) | 15 | ||||
| -rw-r--r-- | crates/typst/src/layout/inline/mod.rs (renamed from crates/typst-library/src/layout/par.rs) | 356 | ||||
| -rw-r--r-- | crates/typst/src/layout/inline/shaping.rs (renamed from crates/typst-library/src/text/shaping.rs) | 161 | ||||
| -rw-r--r-- | crates/typst/src/layout/layout.rs (renamed from crates/typst-library/src/meta/context.rs) | 91 | ||||
| -rw-r--r-- | crates/typst/src/layout/length.rs (renamed from crates/typst/src/geom/length.rs) | 10 | ||||
| -rw-r--r-- | crates/typst/src/layout/measure.rs (renamed from crates/typst-library/src/layout/measure.rs) | 5 | ||||
| -rw-r--r-- | crates/typst/src/layout/mod.rs | 256 | ||||
| -rw-r--r-- | crates/typst/src/layout/pad.rs (renamed from crates/typst-library/src/layout/pad.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/layout/page.rs (renamed from crates/typst-library/src/layout/page.rs) | 23 | ||||
| -rw-r--r-- | crates/typst/src/layout/place.rs (renamed from crates/typst-library/src/layout/place.rs) | 8 | ||||
| -rw-r--r-- | crates/typst/src/layout/point.rs (renamed from crates/typst/src/geom/point.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/layout/ratio.rs (renamed from crates/typst/src/geom/ratio.rs) | 10 | ||||
| -rw-r--r-- | crates/typst/src/layout/regions.rs (renamed from crates/typst-library/src/layout/regions.rs) | 2 | ||||
| -rw-r--r-- | crates/typst/src/layout/rel.rs (renamed from crates/typst/src/geom/rel.rs) | 10 | ||||
| -rw-r--r-- | crates/typst/src/layout/repeat.rs (renamed from crates/typst-library/src/layout/repeat.rs) | 8 | ||||
| -rw-r--r-- | crates/typst/src/layout/sides.rs (renamed from crates/typst/src/geom/sides.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/layout/size.rs (renamed from crates/typst/src/geom/size.rs) | 5 | ||||
| -rw-r--r-- | crates/typst/src/layout/spacing.rs (renamed from crates/typst-library/src/layout/spacing.rs) | 4 | ||||
| -rw-r--r-- | crates/typst/src/layout/stack.rs (renamed from crates/typst-library/src/layout/stack.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/layout/transform.rs (renamed from crates/typst-library/src/layout/transform.rs) | 134 | ||||
| -rw-r--r-- | crates/typst/src/layout/vt.rs | 43 | ||||
| -rw-r--r-- | crates/typst/src/lib.rs | 246 | ||||
| -rw-r--r-- | crates/typst/src/loading/cbor.rs | 57 | ||||
| -rw-r--r-- | crates/typst/src/loading/csv.rs | 118 | ||||
| -rw-r--r-- | crates/typst/src/loading/json.rs | 94 | ||||
| -rw-r--r-- | crates/typst/src/loading/mod.rs | 82 | ||||
| -rw-r--r-- | crates/typst/src/loading/read.rs | 57 | ||||
| -rw-r--r-- | crates/typst/src/loading/toml.rs | 90 | ||||
| -rw-r--r-- | crates/typst/src/loading/xml.rs | 116 | ||||
| -rw-r--r-- | crates/typst/src/loading/yaml.rs | 78 | ||||
| -rw-r--r-- | crates/typst/src/math/accent.rs (renamed from crates/typst-library/src/math/accent.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/math/align.rs (renamed from crates/typst-library/src/math/align.rs) | 5 | ||||
| -rw-r--r-- | crates/typst/src/math/attach.rs (renamed from crates/typst-library/src/math/attach.rs) | 10 | ||||
| -rw-r--r-- | crates/typst/src/math/cancel.rs (renamed from crates/typst-library/src/math/cancel.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/math/class.rs (renamed from crates/typst-library/src/math/class.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/math/ctx.rs (renamed from crates/typst-library/src/math/ctx.rs) | 76 | ||||
| -rw-r--r-- | crates/typst/src/math/equation.rs (renamed from crates/typst-library/src/math/mod.rs) | 225 | ||||
| -rw-r--r-- | crates/typst/src/math/frac.rs (renamed from crates/typst-library/src/math/frac.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/math/fragment.rs (renamed from crates/typst-library/src/math/fragment.rs) | 67 | ||||
| -rw-r--r-- | crates/typst/src/math/lr.rs (renamed from crates/typst-library/src/math/lr.rs) | 8 | ||||
| -rw-r--r-- | crates/typst/src/math/matrix.rs (renamed from crates/typst-library/src/math/matrix.rs) | 38 | ||||
| -rw-r--r-- | crates/typst/src/math/mod.rs | 311 | ||||
| -rw-r--r-- | crates/typst/src/math/op.rs (renamed from crates/typst-library/src/math/op.rs) | 9 | ||||
| -rw-r--r-- | crates/typst/src/math/root.rs (renamed from crates/typst-library/src/math/root.rs) | 10 | ||||
| -rw-r--r-- | crates/typst/src/math/row.rs (renamed from crates/typst-library/src/math/row.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/math/spacing.rs (renamed from crates/typst-library/src/math/spacing.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/math/stretch.rs (renamed from crates/typst-library/src/math/stretch.rs) | 3 | ||||
| -rw-r--r-- | crates/typst/src/math/style.rs (renamed from crates/typst-library/src/math/style.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/math/underover.rs (renamed from crates/typst-library/src/math/underover.rs) | 13 | ||||
| -rw-r--r-- | crates/typst/src/model/bibliography.rs (renamed from crates/typst-library/src/meta/bibliography.rs) | 58 | ||||
| -rw-r--r-- | crates/typst/src/model/cite.rs (renamed from crates/typst-library/src/meta/cite.rs) | 13 | ||||
| -rw-r--r-- | crates/typst/src/model/document.rs (renamed from crates/typst-library/src/meta/document.rs) | 38 | ||||
| -rw-r--r-- | crates/typst/src/model/emph.rs | 41 | ||||
| -rw-r--r-- | crates/typst/src/model/enum.rs (renamed from crates/typst-library/src/layout/enum.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/model/figure.rs (renamed from crates/typst-library/src/meta/figure.rs) | 21 | ||||
| -rw-r--r-- | crates/typst/src/model/footnote.rs (renamed from crates/typst-library/src/meta/footnote.rs) | 18 | ||||
| -rw-r--r-- | crates/typst/src/model/heading.rs (renamed from crates/typst-library/src/meta/heading.rs) | 18 | ||||
| -rw-r--r-- | crates/typst/src/model/link.rs (renamed from crates/typst-library/src/meta/link.rs) | 38 | ||||
| -rw-r--r-- | crates/typst/src/model/list.rs (renamed from crates/typst-library/src/layout/list.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/model/mod.rs | 228 | ||||
| -rw-r--r-- | crates/typst/src/model/numbering.rs (renamed from crates/typst-library/src/meta/numbering.rs) | 12 | ||||
| -rw-r--r-- | crates/typst/src/model/outline.rs (renamed from crates/typst-library/src/meta/outline.rs) | 27 | ||||
| -rw-r--r-- | crates/typst/src/model/par.rs | 180 | ||||
| -rw-r--r-- | crates/typst/src/model/quote.rs (renamed from crates/typst-library/src/text/quote.rs) | 15 | ||||
| -rw-r--r-- | crates/typst/src/model/realize.rs | 242 | ||||
| -rw-r--r-- | crates/typst/src/model/reference.rs (renamed from crates/typst-library/src/meta/reference.rs) | 15 | ||||
| -rw-r--r-- | crates/typst/src/model/strong.rs | 48 | ||||
| -rw-r--r-- | crates/typst/src/model/table.rs (renamed from crates/typst-library/src/layout/table.rs) | 17 | ||||
| -rw-r--r-- | crates/typst/src/model/terms.rs (renamed from crates/typst-library/src/layout/terms.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/realize/behave.rs (renamed from crates/typst-library/src/shared/behave.rs) | 9 | ||||
| -rw-r--r-- | crates/typst/src/realize/mod.rs (renamed from crates/typst-library/src/layout/mod.rs) | 402 | ||||
| -rw-r--r-- | crates/typst/src/symbols/emoji.rs (renamed from crates/typst-library/src/symbols/emoji.rs) | 3 | ||||
| -rw-r--r-- | crates/typst/src/symbols/mod.rs | 27 | ||||
| -rw-r--r-- | crates/typst/src/symbols/sym.rs (renamed from crates/typst-library/src/symbols/sym.rs) | 3 | ||||
| -rw-r--r-- | crates/typst/src/symbols/symbol.rs (renamed from crates/typst/src/eval/symbol.rs) | 10 | ||||
| -rw-r--r-- | crates/typst/src/text/case.rs | 79 | ||||
| -rw-r--r-- | crates/typst/src/text/deco.rs (renamed from crates/typst-library/src/text/deco.rs) | 20 | ||||
| -rw-r--r-- | crates/typst/src/text/font/book.rs (renamed from crates/typst/src/font/book.rs) | 2 | ||||
| -rw-r--r-- | crates/typst/src/text/font/mod.rs (renamed from crates/typst/src/font/mod.rs) | 4 | ||||
| -rw-r--r-- | crates/typst/src/text/font/variant.rs (renamed from crates/typst/src/font/variant.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/text/item.rs | 63 | ||||
| -rw-r--r-- | crates/typst/src/text/lang.rs | 182 | ||||
| -rw-r--r-- | crates/typst/src/text/linebreak.rs | 43 | ||||
| -rw-r--r-- | crates/typst/src/text/lorem.rs | 24 | ||||
| -rw-r--r-- | crates/typst/src/text/mod.rs (renamed from crates/typst-library/src/text/mod.rs) | 214 | ||||
| -rw-r--r-- | crates/typst/src/text/raw.rs (renamed from crates/typst-library/src/text/raw.rs) | 42 | ||||
| -rw-r--r-- | crates/typst/src/text/shift.rs (renamed from crates/typst-library/src/text/shift.rs) | 7 | ||||
| -rw-r--r-- | crates/typst/src/text/smallcaps.rs | 32 | ||||
| -rw-r--r-- | crates/typst/src/text/smartquote.rs (renamed from crates/typst-library/src/text/quotes.rs) | 48 | ||||
| -rw-r--r-- | crates/typst/src/text/space.rs | 26 | ||||
| -rw-r--r-- | crates/typst/src/util/deferred.rs | 11 | ||||
| -rw-r--r-- | crates/typst/src/util/fat.rs | 55 | ||||
| -rw-r--r-- | crates/typst/src/util/macros.rs (renamed from crates/typst/src/geom/macros.rs) | 0 | ||||
| -rw-r--r-- | crates/typst/src/util/mod.rs | 63 | ||||
| -rw-r--r-- | crates/typst/src/util/pico.rs | 3 | ||||
| -rw-r--r-- | crates/typst/src/util/scalar.rs (renamed from crates/typst/src/geom/scalar.rs) | 54 | ||||
| -rw-r--r-- | crates/typst/src/visualize/color.rs (renamed from crates/typst/src/geom/color.rs) | 29 | ||||
| -rw-r--r-- | crates/typst/src/visualize/gradient.rs (renamed from crates/typst/src/geom/gradient.rs) | 82 | ||||
| -rw-r--r-- | crates/typst/src/visualize/image/mod.rs (renamed from crates/typst-library/src/visualize/image.rs) | 191 | ||||
| -rw-r--r-- | crates/typst/src/visualize/image/raster.rs (renamed from crates/typst/src/image/raster.rs) | 3 | ||||
| -rw-r--r-- | crates/typst/src/visualize/image/svg.rs (renamed from crates/typst/src/image/svg.rs) | 6 | ||||
| -rw-r--r-- | crates/typst/src/visualize/line.rs (renamed from crates/typst-library/src/visualize/line.rs) | 11 | ||||
| -rw-r--r-- | crates/typst/src/visualize/mod.rs (renamed from crates/typst-library/src/visualize/mod.rs) | 20 | ||||
| -rw-r--r-- | crates/typst/src/visualize/paint.rs (renamed from crates/typst/src/geom/paint.rs) | 15 | ||||
| -rw-r--r-- | crates/typst/src/visualize/path.rs (renamed from crates/typst-library/src/visualize/path.rs) | 110 | ||||
| -rw-r--r-- | crates/typst/src/visualize/polygon.rs (renamed from crates/typst-library/src/visualize/polygon.rs) | 13 | ||||
| -rw-r--r-- | crates/typst/src/visualize/shape.rs | 1221 | ||||
| -rw-r--r-- | crates/typst/src/visualize/stroke.rs (renamed from crates/typst/src/geom/stroke.rs) | 15 |
241 files changed, 9459 insertions, 8741 deletions
diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 7ac4ee5e..3819469e 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -21,7 +21,6 @@ doc = false [dependencies] typst = { workspace = true } -typst-library = { workspace = true } typst-pdf = { workspace = true } typst-render = { workspace = true } typst-svg = { workspace = true } diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index cfd1ae08..075412cd 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -1,9 +1,8 @@ use std::fmt::{self, Display, Formatter}; use std::path::PathBuf; -use semver::Version; - use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum}; +use semver::Version; /// The character typically used to separate path components /// in environment variables. diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 48d6401c..73115924 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -4,12 +4,14 @@ use std::path::{Path, PathBuf}; use chrono::{Datelike, Timelike}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term::{self, termcolor}; +use ecow::eco_format; use termcolor::{ColorChoice, StandardStream}; use typst::diag::{bail, Severity, SourceDiagnostic, StrResult}; -use typst::doc::Document; -use typst::eval::{eco_format, Datetime, Tracer}; -use typst::geom::Color; +use typst::eval::Tracer; +use typst::foundations::Datetime; +use typst::model::Document; use typst::syntax::{FileId, Source, Span}; +use typst::visualize::Color; use typst::{World, WorldExt}; use crate::args::{CompileCommand, DiagnosticFormat, OutputFormat}; diff --git a/crates/typst-cli/src/fonts.rs b/crates/typst-cli/src/fonts.rs index 7c785716..f4711b82 100644 --- a/crates/typst-cli/src/fonts.rs +++ b/crates/typst-cli/src/fonts.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use fontdb::{Database, Source}; use typst::diag::StrResult; -use typst::font::{Font, FontBook, FontInfo, FontVariant}; +use typst::text::{Font, FontBook, FontInfo, FontVariant}; use crate::args::FontsCommand; @@ -106,7 +106,7 @@ impl FontSearcher { #[cfg(feature = "embed-fonts")] fn add_embedded(&mut self) { let mut process = |bytes: &'static [u8]| { - let buffer = typst::eval::Bytes::from_static(bytes); + let buffer = typst::foundations::Bytes::from_static(bytes); for (i, font) in Font::iter(buffer).enumerate() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs index cc9cfc23..a84cef79 100644 --- a/crates/typst-cli/src/query.rs +++ b/crates/typst-cli/src/query.rs @@ -1,10 +1,13 @@ use comemo::Track; +use ecow::{eco_format, EcoString}; use serde::Serialize; use typst::diag::{bail, StrResult}; use typst::eval::{eval_string, EvalMode, Tracer}; -use typst::model::Introspector; +use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; +use typst::introspection::Introspector; +use typst::model::Document; +use typst::syntax::Span; use typst::World; -use typst_library::prelude::*; use crate::args::{QueryCommand, SerializationFormat}; use crate::compile::print_diagnostics; @@ -95,7 +98,7 @@ fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> { .collect(); if command.one { - let Some(value) = mapped.get(0) else { + let Some(value) = mapped.first() else { bail!("no such field found for element"); }; serialize(value, command.format) diff --git a/crates/typst-cli/src/tracing.rs b/crates/typst-cli/src/tracing.rs index c01efd6d..331c6327 100644 --- a/crates/typst-cli/src/tracing.rs +++ b/crates/typst-cli/src/tracing.rs @@ -6,8 +6,9 @@ use inferno::flamegraph::Options; use tracing::metadata::LevelFilter; use tracing_error::ErrorLayer; use tracing_flame::{FlameLayer, FlushGuard}; -use tracing_subscriber::fmt; -use tracing_subscriber::prelude::*; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{fmt, Layer}; use crate::args::{CliArguments, Command}; diff --git a/crates/typst-cli/src/update.rs b/crates/typst-cli/src/update.rs index c94515b6..3cd65c9d 100644 --- a/crates/typst-cli/src/update.rs +++ b/crates/typst-cli/src/update.rs @@ -1,13 +1,12 @@ -use std::env; -use std::fs; use std::io::{Cursor, Read, Write}; use std::path::PathBuf; +use std::{env, fs}; +use ecow::eco_format; use semver::Version; use serde::Deserialize; use tempfile::NamedTempFile; use typst::diag::{bail, StrResult}; -use typst::eval::eco_format; use xz2::bufread::XzDecoder; use zip::ZipArchive; diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index 138f473a..aea3ca48 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -3,11 +3,11 @@ use std::io::{self, IsTerminal, Write}; use std::path::{Path, PathBuf}; use codespan_reporting::term::{self, termcolor}; +use ecow::eco_format; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use same_file::is_same_file; use termcolor::WriteColor; use typst::diag::StrResult; -use typst::eval::eco_format; use crate::args::CompileCommand; use crate::color_stream; diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 3b774f1d..f375c648 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -5,13 +5,14 @@ use std::path::{Path, PathBuf}; use chrono::{DateTime, Datelike, Local}; use comemo::Prehashed; +use ecow::eco_format; use typst::diag::{FileError, FileResult, StrResult}; -use typst::doc::Frame; -use typst::eval::{eco_format, Bytes, Datetime, Library}; -use typst::font::{Font, FontBook}; +use typst::foundations::{Bytes, Datetime}; +use typst::layout::Frame; use typst::syntax::{FileId, Source, VirtualPath}; +use typst::text::{Font, FontBook}; use typst::util::hash128; -use typst::World; +use typst::{Library, World}; use crate::args::SharedArgs; use crate::fonts::{FontSearcher, FontSlot}; @@ -75,7 +76,7 @@ impl SystemWorld { input, root, main: FileId::new(None, main_path), - library: Prehashed::new(typst_library::build()), + library: Prehashed::new(Library::build()), book: Prehashed::new(searcher.book), fonts: searcher.fonts, slots: RefCell::default(), diff --git a/crates/typst-docs/Cargo.toml b/crates/typst-docs/Cargo.toml index b2e82e43..152f5e79 100644 --- a/crates/typst-docs/Cargo.toml +++ b/crates/typst-docs/Cargo.toml @@ -12,7 +12,6 @@ bench = false [dependencies] typst = { workspace = true } -typst-library = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } heck = { workspace = true } diff --git a/crates/typst-docs/src/contribs.rs b/crates/typst-docs/src/contribs.rs index cbd87dc6..58a730e2 100644 --- a/crates/typst-docs/src/contribs.rs +++ b/crates/typst-docs/src/contribs.rs @@ -4,7 +4,7 @@ use std::fmt::Write; use serde::{Deserialize, Serialize}; -use super::{Html, Resolver}; +use crate::{Html, Resolver}; /// Build HTML detailing the contributors between two tags. pub fn contributors(resolver: &dyn Resolver, from: &str, to: &str) -> Option<Html> { diff --git a/crates/typst-docs/src/html.rs b/crates/typst-docs/src/html.rs index 1210545f..7481b050 100644 --- a/crates/typst-docs/src/html.rs +++ b/crates/typst-docs/src/html.rs @@ -8,11 +8,12 @@ use pulldown_cmark as md; use serde::{Deserialize, Serialize}; use typed_arena::Arena; use typst::diag::{FileResult, StrResult}; -use typst::eval::{Bytes, Datetime, Library, Tracer}; -use typst::font::{Font, FontBook}; -use typst::geom::{Abs, Point, Size}; +use typst::eval::Tracer; +use typst::foundations::{Bytes, Datetime}; +use typst::layout::{Abs, Point, Size}; use typst::syntax::{FileId, Source, VirtualPath}; -use typst::World; +use typst::text::{Font, FontBook}; +use typst::{Library, World}; use unscanny::Scanner; use yaml_front_matter::YamlFrontMatter; @@ -360,13 +361,13 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html { buf.push_str("</pre>"); return Html::new(buf); } else if !matches!(lang, "example" | "typ" | "preview") { - let set = &*typst_library::text::SYNTAXES; + let set = &*typst::text::RAW_SYNTAXES; let buf = syntect::html::highlighted_html_for_string( &display, set, set.find_syntax_by_token(lang) .unwrap_or_else(|| panic!("unsupported highlighting language: {lang}")), - &typst_library::text::THEME, + &typst::text::RAW_THEME, ) .expect("failed to highlight code"); return Html::new(buf); diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs index d58734c6..444dda32 100644 --- a/crates/typst-docs/src/lib.rs +++ b/crates/typst-docs/src/lib.rs @@ -5,9 +5,9 @@ mod html; mod link; mod model; -pub use contribs::{contributors, Author, Commit}; -pub use html::Html; -pub use model::*; +pub use self::contribs::*; +pub use self::html::*; +pub use self::model::*; use std::path::Path; @@ -20,30 +20,48 @@ use serde::de::DeserializeOwned; use serde::Deserialize; use serde_yaml as yaml; use typst::diag::{bail, StrResult}; -use typst::doc::Frame; -use typst::eval::{ - CastInfo, Func, Library, Module, ParamInfo, Repr, Scope, Smart, Type, Value, +use typst::foundations::{ + CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value, + FOUNDATIONS, }; -use typst::font::{Font, FontBook}; -use typst::geom::Abs; -use typst_library::layout::{Margin, PageElem}; +use typst::introspection::INTROSPECTION; +use typst::layout::{Abs, Frame, Margin, PageElem, LAYOUT}; +use typst::loading::DATA_LOADING; +use typst::math::MATH; +use typst::model::MODEL; +use typst::symbols::SYMBOLS; +use typst::text::{Font, FontBook, TEXT}; +use typst::visualize::VISUALIZE; +use typst::Library; static DOCS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs"); static FILE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/files"); static FONT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/fonts"); -static CATEGORIES: Lazy<yaml::Mapping> = Lazy::new(|| yaml("reference/categories.yml")); -static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| yaml("reference/groups.yml")); +static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| { + let mut groups: Vec<GroupData> = yaml("reference/groups.yml"); + for group in &mut groups { + if group.filter.is_empty() { + group.filter = group + .module() + .scope() + .iter() + .filter(|(_, v)| matches!(v, Value::Func(_))) + .map(|(k, _)| k.clone()) + .collect(); + } + } + groups +}); static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| { - let mut lib = typst_library::build(); + let mut lib = Library::build(); lib.styles .set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into()))); lib.styles.set(PageElem::set_height(Smart::Auto)); lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom( Abs::pt(15.0).into(), ))))); - typst::eval::set_lang_items(lib.items.clone()); Prehashed::new(lib) }); @@ -128,14 +146,15 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { .with_part("Language"), markdown_page(resolver, "/docs/reference/", "reference/styling.md"), markdown_page(resolver, "/docs/reference/", "reference/scripting.md"), - category_page(resolver, "foundations").with_part("Library"), - category_page(resolver, "text"), - category_page(resolver, "math"), - category_page(resolver, "layout"), - category_page(resolver, "visualize"), - category_page(resolver, "meta"), - category_page(resolver, "symbols"), - category_page(resolver, "data-loading"), + category_page(resolver, FOUNDATIONS).with_part("Library"), + category_page(resolver, MODEL), + category_page(resolver, TEXT), + category_page(resolver, MATH), + category_page(resolver, SYMBOLS), + category_page(resolver, LAYOUT), + category_page(resolver, VISUALIZE), + category_page(resolver, INTROSPECTION), + category_page(resolver, DATA_LOADING), ]; page } @@ -152,50 +171,73 @@ fn guide_pages(resolver: &dyn Resolver) -> PageModel { /// Build the packages section. fn packages_page(resolver: &dyn Resolver) -> PageModel { + let md = DOCS_DIR + .get_file("reference/packages.md") + .unwrap() + .contents_utf8() + .unwrap(); PageModel { route: "/docs/packages/".into(), title: "Packages".into(), description: "Packages for Typst.".into(), part: None, outline: vec![], - body: BodyModel::Packages(Html::markdown( - resolver, - category_details("packages"), - Some(1), - )), + body: BodyModel::Packages(Html::markdown(resolver, md, Some(1))), children: vec![], } } /// Create a page for a category. #[track_caller] -fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel { - let route = eco_format!("/docs/reference/{category}/"); +fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { + let route = eco_format!("/docs/reference/{}/", category.name()); let mut children = vec![]; let mut items = vec![]; + let mut shorthands = None; + let mut markup = vec![]; + let mut math = vec![]; - let (module, path): (&Module, &[&str]) = match category { - "math" => (&LIBRARY.math, &["math"]), - _ => (&LIBRARY.global, &[]), + let (module, path): (&Module, &[&str]) = if category == MATH { + (&LIBRARY.math, &["math"]) + } else { + (&LIBRARY.global, &[]) }; // Add groups. - for mut group in GROUPS.iter().filter(|g| g.category == category).cloned() { - let mut focus = module; - if matches!(group.name.as_str(), "calc" | "sys") { - focus = get_module(focus, &group.name).unwrap(); - group.functions = focus - .scope() - .iter() - .filter(|(_, v)| matches!(v, Value::Func(_))) - .map(|(k, _)| k.clone()) - .collect(); + for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() { + if matches!(group.name.as_str(), "sym" | "emoji") { + let subpage = symbols_page(resolver, &route, &group); + let BodyModel::Symbols(model) = &subpage.body else { continue }; + let list = &model.list; + markup.extend( + list.iter() + .filter(|symbol| symbol.markup_shorthand.is_some()) + .cloned(), + ); + math.extend( + list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(), + ); + + items.push(CategoryItem { + name: group.name.clone(), + route: subpage.route.clone(), + oneliner: oneliner(category.docs()).into(), + code: true, + }); + children.push(subpage); + continue; } - let (child, item) = group_page(resolver, &route, &group, focus.scope()); + + let (child, item) = group_page(resolver, &route, &group); children.push(child); items.push(item); } + // Add symbol pages. These are ordered manually. + if category == SYMBOLS { + shorthands = Some(ShorthandsModel { markup, math }); + } + // Add functions. let scope = module.scope(); for (name, value) in scope.iter() { @@ -203,9 +245,9 @@ fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel { continue; } - if category == "math" { + if category == MATH { // Skip grouped functions. - if GROUPS.iter().flat_map(|group| &group.functions).any(|f| f == name) { + if GROUPS.iter().flat_map(|group| &group.filter).any(|f| f == name) { continue; } @@ -242,54 +284,35 @@ fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel { } } - children.sort_by_cached_key(|child| child.title.clone()); - items.sort_by_cached_key(|item| item.name.clone()); - - // Add symbol pages. These are ordered manually. - let mut shorthands = None; - if category == "symbols" { - let mut markup = vec![]; - let mut math = vec![]; - for module in ["sym", "emoji"] { - let subpage = symbols_page(resolver, &route, module); - let BodyModel::Symbols(model) = &subpage.body else { continue }; - let list = &model.list; - markup.extend( - list.iter() - .filter(|symbol| symbol.markup_shorthand.is_some()) - .cloned(), - ); - math.extend( - list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(), - ); - - items.push(CategoryItem { - name: module.into(), - route: subpage.route.clone(), - oneliner: oneliner(category_details(module)).into(), - code: true, - }); - children.push(subpage); - } - shorthands = Some(ShorthandsModel { markup, math }); + if category != SYMBOLS { + children.sort_by_cached_key(|child| child.title.clone()); + items.sort_by_cached_key(|item| item.name.clone()); } - let name: EcoString = category.to_title_case().into(); - - let details = Html::markdown(resolver, category_details(category), Some(1)); + let name = category.title(); + let details = Html::markdown(resolver, category.docs(), Some(1)); let mut outline = vec![OutlineItem::from_name("Summary")]; outline.extend(details.outline()); outline.push(OutlineItem::from_name("Definitions")); + if shorthands.is_some() { + outline.push(OutlineItem::from_name("Shorthands")); + } PageModel { route, - title: name.clone(), + title: name.into(), description: eco_format!( "Documentation for functions related to {name} in Typst." ), part: None, outline, - body: BodyModel::Category(CategoryModel { name, details, items, shorthands }), + body: BodyModel::Category(CategoryModel { + name: category.name(), + title: category.title(), + details, + items, + shorthands, + }), children, } } @@ -498,18 +521,17 @@ fn group_page( resolver: &dyn Resolver, parent: &str, group: &GroupData, - scope: &Scope, ) -> (PageModel, CategoryItem) { let mut functions = vec![]; let mut outline = vec![OutlineItem::from_name("Summary")]; let path: Vec<_> = group.path.iter().map(|s| s.as_str()).collect(); - let details = Html::markdown(resolver, &group.description, Some(1)); + let details = Html::markdown(resolver, &group.details, Some(1)); outline.extend(details.outline()); let mut outline_items = vec![]; - for name in &group.functions { - let value = scope.get(name).unwrap(); + for name in &group.filter { + let value = group.module().scope().get(name).unwrap(); let Value::Func(func) = value else { panic!("not a function") }; let func = func_model(resolver, func, &path, true); let id_base = urlify(&eco_format!("functions-{}", func.name)); @@ -530,13 +552,13 @@ fn group_page( let model = PageModel { route: eco_format!("{parent}{}", group.name), - title: group.display.clone(), + title: group.title.clone(), description: eco_format!("Documentation for the {} functions.", group.name), part: None, outline, body: BodyModel::Group(GroupModel { name: group.name.clone(), - title: group.display.clone(), + title: group.title.clone(), details, functions, }), @@ -546,7 +568,7 @@ fn group_page( let item = CategoryItem { name: group.name.clone(), route: model.route.clone(), - oneliner: oneliner(&group.description).into(), + oneliner: oneliner(&group.details).into(), code: false, }; @@ -601,19 +623,12 @@ fn type_outline(model: &TypeModel) -> Vec<OutlineItem> { } /// Create a page for symbols. -fn symbols_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel { - let module = get_module(&LIBRARY.global, name).unwrap(); - let title = match name { - "sym" => "General", - "emoji" => "Emoji", - _ => unreachable!(), - }; - - let model = symbols_model(resolver, name, title, module.scope()); +fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> PageModel { + let model = symbols_model(resolver, group); PageModel { - route: eco_format!("{parent}{name}/"), - title: title.into(), - description: eco_format!("Documentation for the `{name}` module."), + route: eco_format!("{parent}{}/", group.name), + title: group.title.clone(), + description: eco_format!("Documentation for the `{}` module.", group.name), part: None, outline: vec![], body: BodyModel::Symbols(model), @@ -622,14 +637,9 @@ fn symbols_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel } /// Produce a symbol list's model. -fn symbols_model( - resolver: &dyn Resolver, - name: &str, - title: &'static str, - scope: &Scope, -) -> SymbolsModel { +fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { let mut list = vec![]; - for (name, value) in scope.iter() { + for (name, value) in group.module().scope().iter() { let Value::Symbol(symbol) = value else { continue }; let complete = |variant: &str| { if variant.is_empty() { @@ -649,7 +659,7 @@ fn symbols_model( markup_shorthand: shorthand(typst::syntax::ast::Shorthand::MARKUP_LIST), math_shorthand: shorthand(typst::syntax::ast::Shorthand::MATH_LIST), codepoint: c as u32, - accent: typst::eval::Symbol::combining_accent(c).is_some(), + accent: typst::symbols::Symbol::combining_accent(c).is_some(), unicode_name: unicode_names2::name(c) .map(|s| s.to_string().to_title_case().into()), alternates: symbol @@ -662,8 +672,9 @@ fn symbols_model( } SymbolsModel { - name: title, - details: Html::markdown(resolver, category_details(name), Some(1)), + name: group.name.clone(), + title: group.title.clone(), + details: Html::markdown(resolver, &group.details, Some(1)), list, } } @@ -684,15 +695,6 @@ fn yaml<T: DeserializeOwned>(path: &str) -> T { yaml::from_slice(file.contents()).unwrap() } -/// Load details for an identifying key. -#[track_caller] -fn category_details(key: &str) -> &str { - CATEGORIES - .get(&yaml::Value::String(key.into())) - .and_then(|value| value.as_str()) - .unwrap_or_else(|| panic!("missing details for {key}")) -} - /// Turn a title into an URL fragment. pub fn urlify(title: &str) -> EcoString { title @@ -752,13 +754,23 @@ const TYPE_ORDER: &[&str] = &[ #[derive(Debug, Clone, Deserialize)] struct GroupData { name: EcoString, + title: EcoString, category: EcoString, - display: EcoString, #[serde(default)] path: Vec<EcoString>, #[serde(default)] - functions: Vec<EcoString>, - description: EcoString, + filter: Vec<EcoString>, + details: EcoString, +} + +impl GroupData { + fn module(&self) -> &'static Module { + let mut focus = &LIBRARY.global; + for path in &self.path { + focus = get_module(focus, path).unwrap(); + } + focus + } } #[cfg(test)] diff --git a/crates/typst-docs/src/link.rs b/crates/typst-docs/src/link.rs index 38730c32..e2721b95 100644 --- a/crates/typst-docs/src/link.rs +++ b/crates/typst-docs/src/link.rs @@ -1,5 +1,5 @@ use typst::diag::{bail, StrResult}; -use typst::eval::Func; +use typst::foundations::Func; use crate::{get_module, GROUPS, LIBRARY}; @@ -55,6 +55,15 @@ fn resolve_known(head: &str) -> Option<&'static str> { fn resolve_definition(head: &str) -> StrResult<String> { let mut parts = head.trim_start_matches('$').split('.').peekable(); let mut focus = &LIBRARY.global; + + let Some(name) = parts.peek() else { + bail!("missing first link component"); + }; + + let Some(category) = focus.scope().get_category(name) else { + bail!("{name} has no category"); + }; + while let Some(m) = parts.peek().and_then(|&name| get_module(focus, name).ok()) { focus = m; parts.next(); @@ -62,18 +71,15 @@ fn resolve_definition(head: &str) -> StrResult<String> { let name = parts.next().ok_or("link is missing first part")?; let value = focus.field(name)?; - let Some(category) = focus.scope().get_category(name) else { - bail!("{name} has no category"); - }; // Handle grouped functions. - if let Some(group) = GROUPS - .iter() - .filter(|_| category == "math") - .find(|group| group.functions.iter().any(|func| func == name)) - { - let mut route = - format!("/docs/reference/math/{}/#functions-{}", group.name, name); + if let Some(group) = GROUPS.iter().find(|group| { + group.category == category.name() && group.filter.iter().any(|func| func == name) + }) { + let mut route = format!( + "/docs/reference/{}/{}/#functions-{}", + group.category, group.name, name + ); if let Some(param) = parts.next() { route.push('-'); route.push_str(param); @@ -81,7 +87,7 @@ fn resolve_definition(head: &str) -> StrResult<String> { return Ok(route); } - let mut route = format!("/docs/reference/{category}/{name}/"); + let mut route = format!("/docs/reference/{}/{name}/", category.name()); if let Some(next) = parts.next() { if value.field(next).is_ok() { route.push_str("#definitions-"); diff --git a/crates/typst-docs/src/model.rs b/crates/typst-docs/src/model.rs index 580ae0d3..93742825 100644 --- a/crates/typst-docs/src/model.rs +++ b/crates/typst-docs/src/model.rs @@ -62,7 +62,8 @@ pub enum BodyModel { /// Details about a category. #[derive(Debug, Serialize)] pub struct CategoryModel { - pub name: EcoString, + pub name: &'static str, + pub title: &'static str, pub details: Html, pub items: Vec<CategoryItem>, pub shorthands: Option<ShorthandsModel>, @@ -144,7 +145,8 @@ pub struct TypeModel { /// A collection of symbols. #[derive(Debug, Serialize)] pub struct SymbolsModel { - pub name: &'static str, + pub name: EcoString, + pub title: EcoString, pub details: Html, pub list: Vec<SymbolModel>, } diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index 4d12e1c5..2d48039b 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -1,8 +1,11 @@ use comemo::Track; use ecow::{eco_vec, EcoString, EcoVec}; -use typst::doc::Frame; -use typst::eval::{Route, Scopes, Tracer, Value, Vm}; -use typst::model::{DelayedErrors, Introspector, Label, Locator, Vt}; +use typst::diag::DelayedErrors; +use typst::eval::{Route, Tracer, Vm}; +use typst::foundations::{Label, Scopes, Value}; +use typst::introspection::{Introspector, Locator}; +use typst::layout::{Frame, Vt}; +use typst::model::BibliographyElem; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use typst::World; @@ -75,13 +78,9 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> { /// - All labels and descriptions for them, if available /// - A split offset: All labels before this offset belong to nodes, all after /// belong to a bibliography. -pub fn analyze_labels( - world: &dyn World, - frames: &[Frame], -) -> (Vec<(Label, Option<EcoString>)>, usize) { +pub fn analyze_labels(frames: &[Frame]) -> (Vec<(Label, Option<EcoString>)>, usize) { let mut output = vec![]; let introspector = Introspector::new(frames); - let items = &world.library().items; // Labels in the document. for elem in introspector.all() { @@ -102,7 +101,7 @@ pub fn analyze_labels( let split = output.len(); // Bibliography keys. - for (key, detail) in (items.bibliography_keys)(introspector.track()) { + for (key, detail) in BibliographyElem::keys(introspector.track()) { output.push((Label::new(&key), detail)); } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 71ae95db..5cff5a81 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -4,21 +4,21 @@ use std::collections::{BTreeSet, HashSet}; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use serde::{Deserialize, Serialize}; -use typst::doc::Frame; -use typst::eval::{ - format_str, repr, AutoValue, CastInfo, Func, Library, NoneValue, Repr, Scope, Type, - Value, +use typst::foundations::{ + fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, + NoneValue, Repr, Scope, Type, Value, }; -use typst::geom::Color; -use typst::model::Label; +use typst::layout::Frame; use typst::syntax::{ ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, }; +use typst::text::RawElem; +use typst::visualize::Color; use typst::World; use unscanny::Scanner; -use crate::analyze::analyze_labels; -use crate::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; +use crate::analyze::{analyze_expr, analyze_import, analyze_labels}; +use crate::{plain_docs_sentence, summarize_font_family}; /// Autocomplete a cursor position in a source file. /// @@ -367,7 +367,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { } } - for &(method, args) in typst::eval::mutable_methods_on(value.ty()) { + for &(method, args) in mutable_methods_on(value.ty()) { ctx.completions.push(Completion { kind: CompletionKind::Func, label: method.into(), @@ -380,7 +380,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) { }) } - for &field in typst::eval::fields_on(value.ty()) { + for &field in fields_on(value.ty()) { // Complete the field name along with its value. Notes: // 1. No parentheses since function fields cannot currently be called // with method syntax; @@ -967,7 +967,6 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) { struct CompletionContext<'a> { world: &'a (dyn World + 'a), frames: &'a [Frame], - library: &'a Library, global: &'a Scope, math: &'a Scope, text: &'a str, @@ -996,7 +995,6 @@ impl<'a> CompletionContext<'a> { Some(Self { world, frames, - library, global: library.global.scope(), math: library.math.scope(), text, @@ -1074,7 +1072,7 @@ impl<'a> CompletionContext<'a> { /// Add completions for raw block tags. fn raw_completions(&mut self) { - for (name, mut tags) in (self.library.items.raw_languages)() { + for (name, mut tags) in RawElem::languages() { let lower = name.to_lowercase(); if !tags.contains(&lower.as_str()) { tags.push(lower.as_str()); @@ -1096,7 +1094,7 @@ impl<'a> CompletionContext<'a> { /// Add completions for labels and references. fn label_completions(&mut self) { - let (labels, split) = analyze_labels(self.world, self.frames); + let (labels, split) = analyze_labels(self.frames); let head = &self.text[..self.from]; let at = head.ends_with('@'); diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index a33e743c..700f475f 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -1,10 +1,11 @@ use std::num::NonZeroUsize; use ecow::EcoString; -use typst::doc::{Destination, Frame, FrameItem, Meta, Position}; -use typst::geom::{Geometry, Point, Size}; -use typst::model::Introspector; +use typst::introspection::{Introspector, Meta}; +use typst::layout::{Frame, FrameItem, Point, Position, Size}; +use typst::model::Destination; use typst::syntax::{FileId, LinkedNode, Source, Span, SyntaxKind}; +use typst::visualize::Geometry; use typst::World; /// Where to [jump](jump_from_click) to. diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs index 3ab367dc..173a4264 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -13,9 +13,7 @@ pub use self::tooltip::{tooltip, Tooltip}; use std::fmt::Write; use ecow::{eco_format, EcoString}; -use typst::font::{FontInfo, FontStyle}; - -use self::analyze::*; +use typst::text::{FontInfo, FontStyle}; /// Extract the first sentence of plain text of a piece of documentation. /// diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index d3f040e7..4f079166 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -2,14 +2,15 @@ use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; -use typst::doc::Frame; -use typst::eval::{repr, CapturesVisitor, CastInfo, Repr, Tracer, Value}; -use typst::geom::{round_2, Length, Numeric}; +use typst::eval::{CapturesVisitor, Tracer}; +use typst::foundations::{repr, CastInfo, Repr, Value}; +use typst::layout::{Frame, Length}; use typst::syntax::{ast, LinkedNode, Source, SyntaxKind}; +use typst::util::{round_2, Numeric}; use typst::World; -use crate::analyze::analyze_labels; -use crate::{analyze_expr, plain_docs_sentence, summarize_font_family}; +use crate::analyze::{analyze_expr, analyze_labels}; +use crate::{plain_docs_sentence, summarize_font_family}; /// Describe the item under the cursor. pub fn tooltip( @@ -25,7 +26,7 @@ pub fn tooltip( named_param_tooltip(world, &leaf) .or_else(|| font_tooltip(world, &leaf)) - .or_else(|| label_tooltip(world, frames, &leaf)) + .or_else(|| label_tooltip(frames, &leaf)) .or_else(|| expr_tooltip(world, &leaf)) .or_else(|| closure_tooltip(&leaf)) } @@ -144,18 +145,14 @@ fn length_tooltip(length: Length) -> Option<Tooltip> { } /// Tooltip for a hovered reference or label. -fn label_tooltip( - world: &dyn World, - frames: &[Frame], - leaf: &LinkedNode, -) -> Option<Tooltip> { +fn label_tooltip(frames: &[Frame], leaf: &LinkedNode) -> Option<Tooltip> { let target = match leaf.kind() { SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'), SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'), _ => return None, }; - for (label, detail) in analyze_labels(world, frames).0 { + for (label, detail) in analyze_labels(frames).0 { if label.as_str() == target { return Some(Tooltip::Text(detail?)); } diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml deleted file mode 100644 index 426c56f3..00000000 --- a/crates/typst-library/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "typst-library" -description = "The standard library for Typst." -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 - -[lib] -test = false -doctest = false -bench = false - -[dependencies] -typst = { workspace = true } -az = { workspace = true } -chinese-number = { workspace = true } -ciborium = { workspace = true } -comemo = { workspace = true } -csv = { workspace = true } -ecow = { 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 } -indexmap = { workspace = true } -kurbo = { workspace = true } -lipsum = { workspace = true } -log = { workspace = true } -once_cell = { workspace = true } -roxmltree = { workspace = true } -rustybuzz = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -smallvec = { workspace = true } -syntect = { workspace = true } -time = { workspace = true } -toml = { workspace = true, features = ["display"] } -tracing = { workspace = true } -ttf-parser = { workspace = true } -typed-arena = { workspace = true } -unicode-bidi = { workspace = true } -unicode-math-class = { workspace = true } -unicode-script = { workspace = true } -unicode-segmentation = { workspace = true } diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs deleted file mode 100644 index e4767ebf..00000000 --- a/crates/typst-library/src/compute/data.rs +++ /dev/null @@ -1,609 +0,0 @@ -use typst::diag::{format_xml_like_error, FileError}; -use typst::eval::Bytes; -use typst::syntax::is_newline; - -use crate::prelude::*; - -/// Hook up all data loading definitions. -pub(super) fn define(global: &mut Scope) { - global.category("data-loading"); - global.define_func::<read>(); - global.define_func::<csv>(); - global.define_func::<json>(); - global.define_func::<toml>(); - global.define_func::<yaml>(); - global.define_func::<cbor>(); - global.define_func::<xml>(); -} - -/// Reads plain text or data from a file. -/// -/// By default, the file will be read as UTF-8 and returned as a [string]($str). -/// -/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead. -/// -/// # Example -/// ```example -/// An example for a HTML file: \ -/// #let text = read("data.html") -/// #raw(text, lang: "html") -/// -/// Raw bytes: -/// #read("tiger.jpg", encoding: none) -/// ``` -#[func] -pub fn read( - /// The virtual machine. - vm: &mut Vm, - /// Path to a file. - path: Spanned<EcoString>, - /// The encoding to read the file with. - /// - /// If set to `{none}`, this function returns raw bytes. - #[named] - #[default(Some(Encoding::Utf8))] - encoding: Option<Encoding>, -) -> SourceResult<Readable> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - Ok(match encoding { - None => Readable::Bytes(data), - Some(Encoding::Utf8) => Readable::Str( - std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)? - .into(), - ), - }) -} - -/// An encoding of a file. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Encoding { - /// The Unicode UTF-8 encoding. - Utf8, -} - -/// A value that can be read from a file. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Readable { - /// A decoded string. - Str(Str), - /// Raw bytes. - Bytes(Bytes), -} - -impl Readable { - fn as_slice(&self) -> &[u8] { - match self { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes(), - } - } -} - -cast! { - Readable, - self => match self { - Self::Str(v) => v.into_value(), - Self::Bytes(v) => v.into_value(), - }, - v: Str => Self::Str(v), - v: Bytes => Self::Bytes(v), -} - -impl From<Readable> for Bytes { - fn from(value: Readable) -> Self { - match value { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes().into(), - } - } -} - -/// Reads structured data from a CSV file. -/// -/// The CSV file will be read and parsed into a 2-dimensional array of strings: -/// Each row in the CSV file will be represented as an array of strings, and all -/// rows will be collected into a single array. Header rows will not be -/// stripped. -/// -/// # Example -/// ```example -/// #let results = csv("data.csv") -/// -/// #table( -/// columns: 2, -/// [*Condition*], [*Result*], -/// ..results.flatten(), -/// ) -/// ``` -#[func(scope, title = "CSV")] -pub fn csv( - /// The virtual machine. - vm: &mut Vm, - /// Path to a CSV file. - path: Spanned<EcoString>, - /// The delimiter that separates columns in the CSV file. - /// Must be a single ASCII character. - #[named] - #[default] - delimiter: Delimiter, -) -> SourceResult<Array> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter) -} - -#[scope] -impl csv { - /// Reads structured data from a CSV string/bytes. - #[func(title = "Decode CSV")] - pub fn decode( - /// CSV data. - data: Spanned<Readable>, - /// The delimiter that separates columns in the CSV file. - /// Must be a single ASCII character. - #[named] - #[default] - delimiter: Delimiter, - ) -> SourceResult<Array> { - let Spanned { v: data, span } = data; - let mut builder = ::csv::ReaderBuilder::new(); - builder.has_headers(false); - builder.delimiter(delimiter.0 as u8); - let mut reader = builder.from_reader(data.as_slice()); - let mut array = Array::new(); - - for (line, result) in reader.records().enumerate() { - // Original solution use line from error, but that is incorrect with - // `has_headers` set to `false`. See issue: - // https://github.com/BurntSushi/rust-csv/issues/184 - let line = line + 1; // Counting lines from 1 - let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; - let sub = row.into_iter().map(|field| field.into_value()).collect(); - array.push(Value::Array(sub)) - } - - Ok(array) - } -} - -/// The delimiter to use when parsing CSV files. -pub struct Delimiter(char); - -impl Default for Delimiter { - fn default() -> Self { - Self(',') - } -} - -cast! { - Delimiter, - self => self.0.into_value(), - v: EcoString => { - let mut chars = v.chars(); - let first = chars.next().ok_or("delimiter must not be empty")?; - if chars.next().is_some() { - bail!("delimiter must be a single character"); - } - - if !first.is_ascii() { - bail!("delimiter must be an ASCII character"); - } - - Self(first) - }, -} - -/// Format the user-facing CSV error message. -fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString { - match err.kind() { - ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), - ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { - eco_format!( - "failed to parse CSV (found {len} instead of \ - {expected_len} fields in line {line})" - ) - } - _ => eco_format!("failed to parse CSV ({err})"), - } -} - -/// Reads structured data from a JSON file. -/// -/// The file must contain a valid JSON object or array. JSON objects will be -/// converted into Typst dictionaries, and JSON arrays will be converted into -/// Typst arrays. Strings and booleans will be converted into the Typst -/// equivalents, `null` will be converted into `{none}`, and numbers will be -/// converted to floats or integers depending on whether they are whole numbers. -/// -/// The function returns a dictionary or an array, depending on the JSON file. -/// -/// The JSON files in the example contain objects with the keys `temperature`, -/// `unit`, and `weather`. -/// -/// # Example -/// ```example -/// #let forecast(day) = block[ -/// #box(square( -/// width: 2cm, -/// inset: 8pt, -/// fill: if day.weather == "sunny" { -/// yellow -/// } else { -/// aqua -/// }, -/// align( -/// bottom + right, -/// strong(day.weather), -/// ), -/// )) -/// #h(6pt) -/// #set text(22pt, baseline: -8pt) -/// #day.temperature °#day.unit -/// ] -/// -/// #forecast(json("monday.json")) -/// #forecast(json("tuesday.json")) -/// ``` -#[func(scope, title = "JSON")] -pub fn json( - /// The virtual machine. - vm: &mut Vm, - /// Path to a JSON file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - json::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl json { - /// Reads structured data from a JSON string/bytes. - #[func(title = "Decode JSON")] - pub fn decode( - /// JSON data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(span) - } - - /// Encodes structured data into a JSON string. - #[func(title = "Encode JSON")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - /// Whether to pretty print the JSON with newlines and indentation. - #[named] - #[default(true)] - pretty: bool, - ) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - if pretty { - serde_json::to_string_pretty(&value) - } else { - serde_json::to_string(&value) - } - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as JSON ({err})")) - .at(span) - } -} - -/// Reads structured data from a TOML file. -/// -/// The file must contain a valid TOML table. TOML tables will be converted into -/// Typst dictionaries, and TOML arrays will be converted into Typst arrays. -/// Strings, booleans and datetimes will be converted into the Typst equivalents -/// and numbers will be converted to floats or integers depending on whether -/// they are whole numbers. -/// -/// The TOML file in the example consists of a table with the keys `title`, -/// `version`, and `authors`. -/// -/// # Example -/// ```example -/// #let details = toml("details.toml") -/// -/// Title: #details.title \ -/// Version: #details.version \ -/// Authors: #(details.authors -/// .join(", ", last: " and ")) -/// ``` -#[func(scope, title = "TOML")] -pub fn toml( - /// The virtual machine. - vm: &mut Vm, - /// Path to a TOML file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - toml::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl toml { - /// Reads structured data from a TOML string/bytes. - #[func(title = "Decode TOML")] - pub fn decode( - /// TOML data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - let raw = std::str::from_utf8(data.as_slice()) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(span) - } - - /// Encodes structured data into a TOML string. - #[func(title = "Encode TOML")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - /// Whether to pretty-print the resulting TOML. - #[named] - #[default(true)] - pretty: bool, - ) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) } - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as TOML ({err})")) - .at(span) - } -} - -/// Format the user-facing TOML error message. -fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString { - if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) { - let line = head.lines().count(); - let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count(); - eco_format!( - "failed to parse TOML ({} at line {line} column {column})", - error.message(), - ) - } else { - eco_format!("failed to parse TOML ({})", error.message()) - } -} - -/// Reads structured data from a YAML file. -/// -/// The file must contain a valid YAML object or array. YAML mappings will be -/// converted into Typst dictionaries, and YAML sequences will be converted into -/// Typst arrays. Strings and booleans will be converted into the Typst -/// equivalents, null-values (`null`, `~` or empty ``) will be converted into -/// `{none}`, and numbers will be converted to floats or integers depending on -/// whether they are whole numbers. Custom YAML tags are ignored, though the -/// loaded value will still be present. -/// -/// The YAML files in the example contain objects with authors as keys, -/// each with a sequence of their own submapping with the keys -/// "title" and "published" -/// -/// # Example -/// ```example -/// #let bookshelf(contents) = { -/// for (author, works) in contents { -/// author -/// for work in works [ -/// - #work.title (#work.published) -/// ] -/// } -/// } -/// -/// #bookshelf( -/// yaml("scifi-authors.yaml") -/// ) -/// ``` -#[func(scope, title = "YAML")] -pub fn yaml( - /// The virtual machine. - vm: &mut Vm, - /// Path to a YAML file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - yaml::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl yaml { - /// Reads structured data from a YAML string/bytes. - #[func(title = "Decode YAML")] - pub fn decode( - /// YAML data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(span) - } - - /// Encode structured data into a YAML string. - #[func(title = "Encode YAML")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - ) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - serde_yaml::to_string(&value) - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as YAML ({err})")) - .at(span) - } -} - -/// Reads structured data from a CBOR file. -/// -/// The file must contain a valid cbor serialization. Mappings will be -/// converted into Typst dictionaries, and sequences will be converted into -/// Typst arrays. Strings and booleans will be converted into the Typst -/// equivalents, null-values (`null`, `~` or empty ``) will be converted into -/// `{none}`, and numbers will be converted to floats or integers depending on -/// whether they are whole numbers. -#[func(scope, title = "CBOR")] -pub fn cbor( - /// The virtual machine. - vm: &mut Vm, - /// Path to a CBOR file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - cbor::decode(Spanned::new(data, span)) -} - -#[scope] -impl cbor { - /// Reads structured data from CBOR bytes. - #[func(title = "Decode CBOR")] - pub fn decode( - /// cbor data. - data: Spanned<Bytes>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - ciborium::from_reader(data.as_slice()) - .map_err(|err| eco_format!("failed to parse CBOR ({err})")) - .at(span) - } - - /// Encode structured data into CBOR bytes. - #[func(title = "Encode CBOR")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - ) -> SourceResult<Bytes> { - let Spanned { v: value, span } = value; - let mut res = Vec::new(); - ciborium::into_writer(&value, &mut res) - .map(|_| res.into()) - .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) - .at(span) - } -} - -/// Reads structured data from an XML file. -/// -/// The XML file is parsed into an array of dictionaries and strings. XML nodes -/// can be elements or strings. Elements are represented as dictionaries with -/// the the following keys: -/// -/// - `tag`: The name of the element as a string. -/// - `attrs`: A dictionary of the element's attributes as strings. -/// - `children`: An array of the element's child nodes. -/// -/// The XML file in the example contains a root `news` tag with multiple -/// `article` tags. Each article has a `title`, `author`, and `content` tag. The -/// `content` tag contains one or more paragraphs, which are represented as `p` -/// tags. -/// -/// # Example -/// ```example -/// #let find-child(elem, tag) = { -/// elem.children -/// .find(e => "tag" in e and e.tag == tag) -/// } -/// -/// #let article(elem) = { -/// let title = find-child(elem, "title") -/// let author = find-child(elem, "author") -/// let pars = find-child(elem, "content") -/// -/// heading(title.children.first()) -/// text(10pt, weight: "medium")[ -/// Published by -/// #author.children.first() -/// ] -/// -/// for p in pars.children { -/// if (type(p) == "dictionary") { -/// parbreak() -/// p.children.first() -/// } -/// } -/// } -/// -/// #let data = xml("example.xml") -/// #for elem in data.first().children { -/// if (type(elem) == "dictionary") { -/// article(elem) -/// } -/// } -/// ``` -#[func(scope, title = "XML")] -pub fn xml( - /// The virtual machine. - vm: &mut Vm, - /// Path to an XML file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - xml::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl xml { - /// Reads structured data from an XML string/bytes. - #[func(title = "Decode XML")] - pub fn decode( - /// XML data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - let text = std::str::from_utf8(data.as_slice()) - .map_err(FileError::from) - .at(span)?; - let document = - roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?; - Ok(convert_xml(document.root())) - } -} - -/// Convert an XML node to a Typst value. -fn convert_xml(node: roxmltree::Node) -> Value { - if node.is_text() { - return node.text().unwrap_or_default().into_value(); - } - - let children: Array = node.children().map(convert_xml).collect(); - if node.is_root() { - return Value::Array(children); - } - - let tag: Str = node.tag_name().name().into(); - let attrs: Dict = node - .attributes() - .map(|attr| (attr.name().into(), attr.value().into_value())) - .collect(); - - Value::Dict(dict! { - "tag" => tag, - "attrs" => attrs, - "children" => children, - }) -} - -/// Format the user-facing XML error message. -fn format_xml_error(error: roxmltree::Error) -> EcoString { - format_xml_like_error("XML", error) -} diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs deleted file mode 100644 index f1af24c5..00000000 --- a/crates/typst-library/src/compute/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Computational functions. - -pub mod calc; -pub mod sys; - -mod data; -mod foundations; - -pub use self::data::*; -pub use self::foundations::*; - -use crate::prelude::*; - -/// Hook up all compute definitions. -pub(super) fn define(global: &mut Scope) { - self::foundations::define(global); - self::data::define(global); - self::calc::define(global); - self::sys::define(global); -} diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs deleted file mode 100644 index 9c18266d..00000000 --- a/crates/typst-library/src/layout/align.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::prelude::*; - -/// Aligns content horizontally and vertically. -/// -/// # Example -/// ```example -/// #set align(center) -/// -/// Centered text, a sight to see \ -/// In perfect balance, visually \ -/// Not left nor right, it stands alone \ -/// A work of art, a visual throne -/// ``` -#[elem(Show)] -pub struct AlignElem { - /// The [alignment]($alignment) along both axes. - /// - /// ```example - /// #set page(height: 6cm) - /// #set text(lang: "ar") - /// - /// مثال - /// #align( - /// end + horizon, - /// rect(inset: 12pt)[ركن] - /// ) - /// ``` - #[positional] - #[fold] - #[default] - pub alignment: Align, - - /// The content to align. - #[required] - pub body: Content, -} - -impl Show for AlignElem { - #[tracing::instrument(name = "AlignElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self - .body() - .clone() - .styled(Self::set_alignment(self.alignment(styles)))) - } -} diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs deleted file mode 100644 index 212debb0..00000000 --- a/crates/typst-library/src/lib.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! Typst's standard library. - -#![allow(clippy::wildcard_in_or_patterns)] -#![allow(clippy::manual_range_contains)] -#![allow(clippy::comparison_chain)] - -pub mod compute; -pub mod layout; -pub mod math; -pub mod meta; -pub mod prelude; -pub mod shared; -pub mod symbols; -pub mod text; -pub mod visualize; - -use typst::eval::{Array, LangItems, Library, Module, Scope, Smart}; -use typst::geom::{Align, Color, Dir}; -use typst::model::{NativeElement, Styles}; - -use self::layout::LayoutRoot; - -/// Construct the standard library. -pub fn build() -> Library { - let math = math::module(); - let global = global(math.clone()); - Library { global, math, styles: styles(), items: items() } -} - -/// Construct the module with global definitions. -#[tracing::instrument(skip_all)] -fn global(math: Module) -> Module { - let mut global = Scope::deduplicating(); - text::define(&mut global); - global.define_module(math); - layout::define(&mut global); - visualize::define(&mut global); - meta::define(&mut global); - symbols::define(&mut global); - compute::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", Align::START); - global.define("left", Align::LEFT); - global.define("center", Align::CENTER); - global.define("right", Align::RIGHT); - global.define("end", Align::END); - global.define("top", Align::TOP); - global.define("horizon", Align::HORIZON); - global.define("bottom", Align::BOTTOM); -} - -/// Construct the standard style map. -fn styles() -> Styles { - Styles::new() -} - -/// Construct the standard lang item mapping. -fn items() -> LangItems { - LangItems { - layout: |world, content, styles| content.layout_root(world, styles), - em: text::TextElem::size_in, - dir: text::TextElem::dir_in, - space: || text::SpaceElem::new().pack(), - linebreak: || text::LinebreakElem::new().pack(), - text: |text| text::TextElem::new(text).pack(), - text_elem: text::TextElem::elem(), - text_str: |content| Some(content.to::<text::TextElem>()?.text()), - smart_quote: |double| text::SmartquoteElem::new().with_double(double).pack(), - parbreak: || layout::ParbreakElem::new().pack(), - strong: |body| text::StrongElem::new(body).pack(), - emph: |body| text::EmphElem::new(body).pack(), - raw: |text, lang, block| { - let mut elem = text::RawElem::new(text).with_block(block); - if let Some(lang) = lang { - elem.push_lang(Some(lang)); - } - elem.pack() - }, - raw_languages: text::RawElem::languages, - link: |url| meta::LinkElem::from_url(url).pack(), - reference: |target, supplement| { - let mut elem = meta::RefElem::new(target); - if let Some(supplement) = supplement { - elem.push_supplement(Smart::Custom(Some(meta::Supplement::Content( - supplement, - )))); - } - elem.pack() - }, - bibliography_keys: meta::BibliographyElem::keys, - heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(), - heading_elem: meta::HeadingElem::elem(), - list_item: |body| layout::ListItem::new(body).pack(), - enum_item: |number, body| { - let mut elem = layout::EnumItem::new(body); - if let Some(number) = number { - elem.push_number(Some(number)); - } - elem.pack() - }, - term_item: |term, description| layout::TermItem::new(term, description).pack(), - equation: |body, block| math::EquationElem::new(body).with_block(block).pack(), - math_align_point: || math::AlignPointElem::new().pack(), - math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(), - math_attach: |base, t, b, tl, bl, tr, br| { - let mut elem = math::AttachElem::new(base); - if let Some(t) = t { - elem.push_t(Some(t)); - } - if let Some(b) = b { - elem.push_b(Some(b)); - } - if let Some(tl) = tl { - elem.push_tl(Some(tl)); - } - if let Some(bl) = bl { - elem.push_bl(Some(bl)); - } - if let Some(tr) = tr { - elem.push_tr(Some(tr)); - } - if let Some(br) = br { - elem.push_br(Some(br)); - } - elem.pack() - }, - math_primes: |count| math::PrimesElem::new(count).pack(), - math_accent: |base, accent| { - math::AccentElem::new(base, math::Accent::new(accent)).pack() - }, - math_frac: |num, denom| math::FracElem::new(num, denom).pack(), - math_root: |index, radicand| { - math::RootElem::new(radicand).with_index(index).pack() - }, - } -} diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs deleted file mode 100644 index ffe861ab..00000000 --- a/crates/typst-library/src/meta/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Interaction between document parts. - -mod bibliography; -mod cite; -mod context; -mod counter; -mod document; -mod figure; -mod footnote; -mod heading; -mod link; -mod metadata; -#[path = "numbering.rs"] -mod numbering_; -mod outline; -#[path = "query.rs"] -mod query_; -mod reference; -mod state; - -pub use self::bibliography::*; -pub use self::cite::*; -pub use self::context::*; -pub use self::counter::*; -pub use self::document::*; -pub use self::figure::*; -pub use self::footnote::*; -pub use self::heading::*; -pub use self::link::*; -pub use self::metadata::*; -pub use self::numbering_::*; -pub use self::outline::*; -pub use self::query_::*; -pub use self::reference::*; -pub use self::state::*; - -use crate::prelude::*; -use crate::text::TextElem; - -/// Hook up all meta definitions. -pub(super) fn define(global: &mut Scope) { - global.category("meta"); - global.define_type::<Label>(); - global.define_type::<Selector>(); - global.define_type::<Location>(); - global.define_type::<Counter>(); - global.define_type::<State>(); - global.define_elem::<DocumentElem>(); - global.define_elem::<RefElem>(); - global.define_elem::<LinkElem>(); - global.define_elem::<OutlineElem>(); - global.define_elem::<HeadingElem>(); - global.define_elem::<FigureElem>(); - global.define_elem::<FootnoteElem>(); - global.define_elem::<CiteElem>(); - global.define_elem::<BibliographyElem>(); - global.define_elem::<MetadataElem>(); - global.define_func::<locate>(); - global.define_func::<style>(); - global.define_func::<layout>(); - global.define_func::<numbering>(); - global.define_func::<query>(); -} - -/// An element that has a local name. -pub trait LocalNameIn: LocalName { - /// Gets the local name from the style chain. - fn local_name_in(styles: StyleChain) -> &'static str - where - Self: Sized, - { - Self::local_name(TextElem::lang_in(styles), TextElem::region_in(styles)) - } -} - -impl<T: LocalName> LocalNameIn for T {} diff --git a/crates/typst-library/src/prelude.rs b/crates/typst-library/src/prelude.rs deleted file mode 100644 index 00700540..00000000 --- a/crates/typst-library/src/prelude.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Helpful imports for creating library functionality. - -#[doc(no_inline)] -pub use std::fmt::{self, Debug, Formatter}; -#[doc(no_inline)] -pub use std::num::NonZeroUsize; - -#[doc(no_inline)] -pub use comemo::{Track, Tracked, TrackedMut}; -#[doc(no_inline)] -pub use ecow::{eco_format, EcoString}; -#[doc(no_inline)] -pub use smallvec::{smallvec, SmallVec}; -#[doc(no_inline)] -pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult}; -#[doc(no_inline)] -pub use typst::doc::*; -#[doc(no_inline)] -pub use typst::eval::{ - array, cast, dict, format_str, func, scope, ty, Args, Array, Bytes, Cast, Dict, - FromValue, Func, IntoValue, Repr, Scope, Smart, Str, Symbol, Type, Value, Vm, -}; -#[doc(no_inline)] -pub use typst::geom::*; -#[doc(no_inline)] -pub use typst::model::{ - elem, select_where, Behave, Behaviour, Construct, Content, Element, ElementFields, - Finalize, Fold, Introspector, Label, LocalName, Locatable, LocatableSelector, - Location, Locator, MetaElem, NativeElement, PlainText, Resolve, Selector, Set, Show, - StyleChain, StyleVec, Styles, Synthesize, Unlabellable, Vt, -}; -#[doc(no_inline)] -pub use typst::syntax::{FileId, Span, Spanned}; -#[doc(no_inline)] -pub use typst::util::NonZeroExt; -#[doc(no_inline)] -pub use typst::World; - -#[doc(no_inline)] -pub use crate::layout::{Fragment, Layout, Regions}; -#[doc(no_inline)] -pub use crate::shared::{ContentExt, StylesExt}; diff --git a/crates/typst-library/src/shared/ext.rs b/crates/typst-library/src/shared/ext.rs deleted file mode 100644 index 60614820..00000000 --- a/crates/typst-library/src/shared/ext.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Extension traits. - -use crate::layout::{AlignElem, MoveElem, PadElem}; -use crate::prelude::*; -use crate::text::{EmphElem, FontFamily, FontList, StrongElem, TextElem, UnderlineElem}; - -/// Additional methods on content. -pub trait ContentExt { - /// Make this content strong. - fn strong(self) -> Self; - - /// Make this content emphasized. - fn emph(self) -> Self; - - /// Underline this content. - fn underlined(self) -> Self; - - /// Link the content somewhere. - fn linked(self, dest: Destination) -> Self; - - /// Make the content linkable by `.linked(Destination::Location(loc))`. - /// - /// Should be used in combination with [`Location::variant`]. - fn backlinked(self, loc: Location) -> Self; - - /// Set alignments for this content. - fn aligned(self, align: Align) -> Self; - - /// Pad this content at the sides. - fn padded(self, padding: Sides<Rel<Length>>) -> Self; - - /// Transform this content's contents without affecting layout. - fn moved(self, delta: Axes<Rel<Length>>) -> Self; -} - -impl ContentExt for Content { - fn strong(self) -> Self { - StrongElem::new(self).pack() - } - - fn emph(self) -> Self { - EmphElem::new(self).pack() - } - - fn underlined(self) -> Self { - UnderlineElem::new(self).pack() - } - - fn linked(self, dest: Destination) -> Self { - self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)])) - } - - fn backlinked(self, loc: Location) -> Self { - let mut backlink = Content::empty(); - backlink.set_location(loc); - self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)])) - } - - fn aligned(self, align: Align) -> Self { - self.styled(AlignElem::set_alignment(align)) - } - - fn padded(self, padding: Sides<Rel<Length>>) -> Self { - PadElem::new(self) - .with_left(padding.left) - .with_top(padding.top) - .with_right(padding.right) - .with_bottom(padding.bottom) - .pack() - } - - fn moved(self, delta: Axes<Rel<Length>>) -> Self { - MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack() - } -} - -/// Additional methods for style lists. -pub trait StylesExt { - /// Set a font family composed of a preferred family and existing families - /// from a style chain. - fn set_family(&mut self, preferred: FontFamily, existing: StyleChain); -} - -impl StylesExt for Styles { - fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { - self.set(TextElem::set_font(FontList( - std::iter::once(preferred) - .chain(TextElem::font_in(existing).into_iter().cloned()) - .collect(), - ))); - } -} diff --git a/crates/typst-library/src/shared/mod.rs b/crates/typst-library/src/shared/mod.rs deleted file mode 100644 index f54241cf..00000000 --- a/crates/typst-library/src/shared/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Shared definitions for the standard library. - -mod behave; -mod ext; - -pub use behave::*; -pub use ext::*; diff --git a/crates/typst-library/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs deleted file mode 100644 index 0d288c3b..00000000 --- a/crates/typst-library/src/symbols/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Modifiable symbols. - -mod emoji; -mod sym; - -pub use emoji::*; -pub use sym::*; - -use crate::prelude::*; - -/// 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-library/src/text/misc.rs b/crates/typst-library/src/text/misc.rs deleted file mode 100644 index 1bf28a34..00000000 --- a/crates/typst-library/src/text/misc.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::prelude::*; -use crate::text::TextElem; - -/// A text space. -#[elem(Behave, Unlabellable, PlainText, Repr)] -pub struct SpaceElem {} - -impl Repr for SpaceElem { - fn repr(&self) -> EcoString { - EcoString::inline("[ ]") - } -} - -impl Behave for SpaceElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Weak(2) - } -} - -impl Unlabellable for SpaceElem {} - -impl PlainText for SpaceElem { - fn plain_text(&self, text: &mut EcoString) { - text.push(' '); - } -} - -/// Inserts a line break. -/// -/// Advances the paragraph to the next line. A single trailing line break at the -/// end of a paragraph is ignored, but more than one creates additional empty -/// lines. -/// -/// # Example -/// ```example -/// *Date:* 26.12.2022 \ -/// *Topic:* Infrastructure Test \ -/// *Severity:* High \ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: To insert a line break, simply write -/// a backslash followed by whitespace. This always creates an unjustified -/// break. -#[elem(title = "Line Break", Behave)] -pub struct LinebreakElem { - /// Whether to justify the line before the break. - /// - /// This is useful if you found a better line break opportunity in your - /// justified text than Typst did. - /// - /// ```example - /// #set par(justify: true) - /// #let jb = linebreak(justify: true) - /// - /// I have manually tuned the #jb - /// line breaks in this paragraph #jb - /// for an _interesting_ result. #jb - /// ``` - #[default(false)] - pub justify: bool, -} - -impl Behave for LinebreakElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Destructive - } -} - -/// Strongly emphasizes content by increasing the font weight. -/// -/// Increases the current font weight by a given `delta`. -/// -/// # Example -/// ```example -/// This is *strong.* \ -/// This is #strong[too.] \ -/// -/// #show strong: set text(red) -/// And this is *evermore.* -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: To strongly emphasize content, -/// simply enclose it in stars/asterisks (`*`). Note that this only works at -/// word boundaries. To strongly emphasize part of a word, you have to use the -/// function. -#[elem(title = "Strong Emphasis", Show)] -pub struct StrongElem { - /// The delta to apply on the font weight. - /// - /// ```example - /// #set strong(delta: 0) - /// No *effect!* - /// ``` - #[default(300)] - pub delta: i64, - - /// The content to strongly emphasize. - #[required] - pub body: Content, -} - -impl Show for StrongElem { - #[tracing::instrument(name = "StrongElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self - .body() - .clone() - .styled(TextElem::set_delta(Delta(self.delta(styles))))) - } -} - -/// A delta that is summed up when folded. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Delta(pub i64); - -cast! { - Delta, - self => self.0.into_value(), - v: i64 => Self(v), -} - -impl Fold for Delta { - type Output = i64; - - fn fold(self, outer: Self::Output) -> Self::Output { - outer + self.0 - } -} - -/// Emphasizes content by setting it in italics. -/// -/// - If the current [text style]($text.style) is `{"normal"}`, this turns it -/// into `{"italic"}`. -/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to -/// `{"normal"}`. -/// -/// # Example -/// ```example -/// This is _emphasized._ \ -/// This is #emph[too.] -/// -/// #show emph: it => { -/// text(blue, it.body) -/// } -/// -/// This is _emphasized_ differently. -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: To emphasize content, simply -/// enclose it in underscores (`_`). Note that this only works at word -/// boundaries. To emphasize part of a word, you have to use the function. -#[elem(title = "Emphasis", Show)] -pub struct EmphElem { - /// The content to emphasize. - #[required] - pub body: Content, -} - -impl Show for EmphElem { - #[tracing::instrument(name = "EmphElem::show", skip(self))] - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(TextElem::set_emph(Toggle))) - } -} - -/// A toggle that turns on and off alternatingly if folded. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Toggle; - -cast! { - Toggle, - self => Value::None, - _: Value => Self, -} - -impl Fold for Toggle { - type Output = bool; - - fn fold(self, outer: Self::Output) -> Self::Output { - !outer - } -} - -/// Converts text or content to lowercase. -/// -/// # Example -/// ```example -/// #lower("ABC") \ -/// #lower[*My Text*] \ -/// #lower[already low] -/// ``` -#[func(title = "Lowercase")] -pub fn lower( - /// The text to convert to lowercase. - text: Caseable, -) -> Caseable { - case(text, Case::Lower) -} - -/// Converts text or content to uppercase. -/// -/// # Example -/// ```example -/// #upper("abc") \ -/// #upper[*my text*] \ -/// #upper[ALREADY HIGH] -/// ``` -#[func(title = "Uppercase")] -pub fn upper( - /// The text to convert to uppercase. - text: Caseable, -) -> Caseable { - case(text, Case::Upper) -} - -/// Change the case of text. -fn case(text: Caseable, case: Case) -> Caseable { - match text { - Caseable::Str(v) => Caseable::Str(case.apply(&v).into()), - Caseable::Content(v) => { - Caseable::Content(v.styled(TextElem::set_case(Some(case)))) - } - } -} - -/// A value whose case can be changed. -pub enum Caseable { - Str(Str), - Content(Content), -} - -cast! { - Caseable, - self => match self { - Self::Str(v) => v.into_value(), - Self::Content(v) => v.into_value(), - }, - v: Str => Self::Str(v), - v: Content => Self::Content(v), -} - -/// A case transformation on text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Case { - /// Everything is lowercased. - Lower, - /// Everything is uppercased. - Upper, -} - -impl Case { - /// Apply the case to a string. - pub fn apply(self, text: &str) -> String { - match self { - Self::Lower => text.to_lowercase(), - Self::Upper => text.to_uppercase(), - } - } -} - -/// Displays text in small capitals. -/// -/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts -/// support this feature. Sometimes smallcaps are part of a dedicated font and -/// sometimes they are not available at all. In the future, this function will -/// support selecting a dedicated smallcaps font as well as synthesizing -/// smallcaps from normal letters, but this is not yet implemented. -/// -/// # Example -/// ```example -/// #set par(justify: true) -/// #set heading(numbering: "I.") -/// -/// #show heading: it => { -/// set block(below: 10pt) -/// set text(weight: "regular") -/// align(center, smallcaps(it)) -/// } -/// -/// = Introduction -/// #lorem(40) -/// ``` -#[func(title = "Small Capitals")] -pub fn smallcaps( - /// The text to display to small capitals. - body: Content, -) -> Content { - body.styled(TextElem::set_smallcaps(true)) -} - -/// Creates blind text. -/// -/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given -/// number of words. The sequence of words generated by the function is always -/// the same but randomly chosen. As usual for blind texts, it does not make any -/// sense. Use it as a placeholder to try layouts. -/// -/// # Example -/// ```example -/// = Blind Text -/// #lorem(30) -/// -/// = More Blind Text -/// #lorem(15) -/// ``` -#[func(keywords = ["Blind Text"])] -pub fn lorem( - /// The length of the blind text in words. - words: usize, -) -> Str { - lipsum::lipsum(words).replace("--", "–").into() -} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs deleted file mode 100644 index 779b07ff..00000000 --- a/crates/typst-library/src/visualize/shape.rs +++ /dev/null @@ -1,547 +0,0 @@ -use std::f64::consts::SQRT_2; - -use crate::prelude::*; - -/// 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", Layout)] -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: Smart<Rel<Length>>, - - /// 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]($stroke) - /// - A dictionary describing the stroke for each side inidvidually. 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(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] - pub body: Option<Content>, -} - -impl Layout for RectElem { - #[tracing::instrument(name = "RectElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Rect, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles), - self.inset(styles), - self.outset(styles), - self.radius(styles), - self.span(), - ) - } -} - -/// A square with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #square(size: 40pt) -/// -/// // With content. -/// #square[ -/// Automatically \ -/// sized to fit. -/// ] -/// ``` -#[elem(Layout)] -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, - })] - pub height: Smart<Rel<Length>>, - - /// 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(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] - pub body: Option<Content>, -} - -impl Layout for SquareElem { - #[tracing::instrument(name = "SquareElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Square, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles), - self.inset(styles), - self.outset(styles), - self.radius(styles), - 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(Layout)] -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: Smart<Rel<Length>>, - - /// 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(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] - pub body: Option<Content>, -} - -impl Layout for EllipseElem { - #[tracing::instrument(name = "EllipseElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Ellipse, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles).map(Sides::splat), - self.inset(styles), - self.outset(styles), - Corners::splat(Rel::zero()), - 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(Layout)] -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, - })] - pub height: Smart<Rel<Length>>, - - /// 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(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] - pub body: Option<Content>, -} - -impl Layout for CircleElem { - #[tracing::instrument(name = "CircleElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Circle, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles).map(Sides::splat), - self.inset(styles), - self.outset(styles), - Corners::splat(Rel::zero()), - self.span(), - ) - } -} - -/// Layout a shape. -#[tracing::instrument(name = "shape::layout", skip_all)] -#[allow(clippy::too_many_arguments)] -fn layout( - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - kind: ShapeKind, - body: &Option<Content>, - sizing: Axes<Smart<Rel<Length>>>, - fill: Option<Paint>, - stroke: Smart<Sides<Option<Stroke<Abs>>>>, - mut inset: Sides<Rel<Abs>>, - outset: Sides<Rel<Abs>>, - radius: Corners<Rel<Abs>>, - span: Span, -) -> SourceResult<Fragment> { - let resolved = sizing - .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r))); - - let mut frame; - if let Some(child) = body { - let region = resolved.unwrap_or(regions.base()); - if kind.is_round() { - inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0)); - } - - // Pad the child. - let child = child.clone().padded(inset.map(|side| side.map(Length::from))); - let expand = sizing.as_ref().map(Smart::is_custom); - let pod = Regions::one(region, expand); - frame = child.layout(vt, styles, pod)?.into_frame(); - - // Enforce correct size. - *frame.size_mut() = expand.select(region, frame.size()); - - // Relayout with full expansion into square region to make sure - // the result is really a square or circle. - if kind.is_quadratic() { - frame.set_size(Size::splat(frame.size().max_by_side())); - let length = frame.size().max_by_side().min(region.min_by_side()); - let pod = Regions::one(Size::splat(length), Axes::splat(true)); - frame = child.layout(vt, styles, pod)?.into_frame(); - } - - // Enforce correct size again. - *frame.size_mut() = expand.select(region, frame.size()); - if kind.is_quadratic() { - frame.set_size(Size::splat(frame.size().max_by_side())); - } - } else { - // The default size that a shape takes on if it has no child and - // enough space. - let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)); - let mut size = resolved.unwrap_or(default.min(regions.base())); - if kind.is_quadratic() { - size = Size::splat(size.min_by_side()); - } - frame = Frame::soft(size); - } - - // Prepare stroke. - let stroke = match stroke { - Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())), - Smart::Auto => Sides::splat(None), - Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)), - }; - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - if kind.is_round() { - let outset = outset.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); - frame.prepend(pos, FrameItem::Shape(shape, span)); - } else { - frame.fill_and_stroke(fill, stroke, outset, radius, span); - } - } - - // Apply metadata. - frame.meta(styles, false); - - Ok(Fragment::frame(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) - } -} diff --git a/crates/typst-macros/src/cast.rs b/crates/typst-macros/src/cast.rs index b83dbedd..f2115aa9 100644 --- a/crates/typst-macros/src/cast.rs +++ b/crates/typst-macros/src/cast.rs @@ -1,4 +1,11 @@ -use super::*; +use heck::ToKebabCase; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{DeriveInput, Ident, Result, Token}; + +use crate::util::{documentation, foundations}; /// Expand the `#[derive(Cast)]` macro. pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> { @@ -43,9 +50,9 @@ pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> { }); Ok(quote! { - ::typst::eval::cast! { + #foundations::cast! { #ty, - self => ::typst::eval::IntoValue::into_value(match self { + self => #foundations::IntoValue::into_value(match self { #(#variants_to_strs),* }), #(#strs_to_variants),* @@ -62,8 +69,6 @@ struct Variant { /// Expand the `cast!` macro. pub fn cast(stream: TokenStream) -> Result<TokenStream> { - let eval = quote! { ::typst::eval }; - let input: CastInput = syn::parse2(stream)?; let ty = &input.ty; let castable_body = create_castable_body(&input); @@ -74,16 +79,16 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> { let reflect = (!input.from_value.is_empty() || input.dynamic).then(|| { quote! { - impl #eval::Reflect for #ty { - fn input() -> #eval::CastInfo { + impl #foundations::Reflect for #ty { + fn input() -> #foundations::CastInfo { #input_body } - fn output() -> #eval::CastInfo { + fn output() -> #foundations::CastInfo { #output_body } - fn castable(value: &#eval::Value) -> bool { + fn castable(value: &#foundations::Value) -> bool { #castable_body } } @@ -92,8 +97,8 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> { let into_value = (input.into_value.is_some() || input.dynamic).then(|| { quote! { - impl #eval::IntoValue for #ty { - fn into_value(self) -> #eval::Value { + impl #foundations::IntoValue for #ty { + fn into_value(self) -> #foundations::Value { #into_value_body } } @@ -102,8 +107,8 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> { let from_value = (!input.from_value.is_empty() || input.dynamic).then(|| { quote! { - impl #eval::FromValue for #ty { - fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> { + impl #foundations::FromValue for #ty { + fn from_value(value: #foundations::Value) -> ::typst::diag::StrResult<Self> { #from_value_body } } @@ -196,7 +201,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream { } Pattern::Ty(_, ty) => { casts.push(quote! { - if <#ty as ::typst::eval::Reflect>::castable(value) { + if <#ty as #foundations::Reflect>::castable(value) { return true; } }); @@ -206,7 +211,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream { let dynamic_check = input.dynamic.then(|| { quote! { - if let ::typst::eval::Value::Dyn(dynamic) = &value { + if let #foundations::Value::Dyn(dynamic) = &value { if dynamic.is::<Self>() { return true; } @@ -216,7 +221,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream { let str_check = (!strings.is_empty()).then(|| { quote! { - if let ::typst::eval::Value::Str(string) = &value { + if let #foundations::Value::Str(string) = &value { match string.as_str() { #(#strings,)* _ => {} @@ -241,21 +246,21 @@ fn create_input_body(input: &CastInput) -> TokenStream { infos.push(match &cast.pattern { Pattern::Str(lit) => { quote! { - ::typst::eval::CastInfo::Value( - ::typst::eval::IntoValue::into_value(#lit), + #foundations::CastInfo::Value( + #foundations::IntoValue::into_value(#lit), #docs, ) } } Pattern::Ty(_, ty) => { - quote! { <#ty as ::typst::eval::Reflect>::input() } + quote! { <#ty as #foundations::Reflect>::input() } } }); } if input.dynamic { infos.push(quote! { - ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>()) + #foundations::CastInfo::Type(#foundations::Type::of::<Self>()) }); } @@ -266,7 +271,7 @@ fn create_input_body(input: &CastInput) -> TokenStream { fn create_output_body(input: &CastInput) -> TokenStream { if input.dynamic { - quote! { ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>()) } + quote! { #foundations::CastInfo::Type(#foundations::Type::of::<Self>()) } } else { quote! { Self::input() } } @@ -276,7 +281,7 @@ fn create_into_value_body(input: &CastInput) -> TokenStream { if let Some(expr) = &input.into_value { quote! { #expr } } else { - quote! { ::typst::eval::Value::dynamic(self) } + quote! { #foundations::Value::dynamic(self) } } } @@ -292,8 +297,8 @@ fn create_from_value_body(input: &CastInput) -> TokenStream { } Pattern::Ty(binding, ty) => { cast_checks.push(quote! { - if <#ty as ::typst::eval::Reflect>::castable(&value) { - let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?; + if <#ty as #foundations::Reflect>::castable(&value) { + let #binding = <#ty as #foundations::FromValue>::from_value(value)?; return Ok(#expr); } }); @@ -303,7 +308,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream { let dynamic_check = input.dynamic.then(|| { quote! { - if let ::typst::eval::Value::Dyn(dynamic) = &value { + if let #foundations::Value::Dyn(dynamic) = &value { if let Some(concrete) = dynamic.downcast::<Self>() { return Ok(concrete.clone()); } @@ -313,7 +318,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream { let str_check = (!string_arms.is_empty()).then(|| { quote! { - if let ::typst::eval::Value::Str(string) = &value { + if let #foundations::Value::Str(string) = &value { match string.as_str() { #(#string_arms,)* _ => {} @@ -326,6 +331,6 @@ fn create_from_value_body(input: &CastInput) -> TokenStream { #dynamic_check #str_check #(#cast_checks)* - Err(<Self as ::typst::eval::Reflect>::error(&value)) + Err(<Self as #foundations::Reflect>::error(&value)) } } diff --git a/crates/typst-macros/src/category.rs b/crates/typst-macros/src/category.rs new file mode 100644 index 00000000..399a0510 --- /dev/null +++ b/crates/typst-macros/src/category.rs @@ -0,0 +1,57 @@ +use heck::{ToKebabCase, ToTitleCase}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Ident, Result, Token, Type, Visibility}; + +use crate::util::{documentation, foundations}; + +/// Expand the `#[category]` macro. +pub fn category(_: TokenStream, item: syn::Item) -> Result<TokenStream> { + let syn::Item::Verbatim(stream) = item else { + bail!(item, "expected bare static"); + }; + + let BareStatic { attrs, vis, ident, ty, .. } = syn::parse2(stream)?; + + let name = ident.to_string().to_kebab_case(); + let title = name.to_title_case(); + let docs = documentation(&attrs); + + Ok(quote! { + #(#attrs)* + #vis static #ident: #ty = { + static DATA: #foundations::CategoryData = #foundations::CategoryData { + name: #name, + title: #title, + docs: #docs, + }; + #foundations::Category::from_data(&DATA) + }; + }) +} + +/// Parse a bare `pub static CATEGORY: Category;` item. +pub struct BareStatic { + pub attrs: Vec<Attribute>, + pub vis: Visibility, + pub static_token: Token![static], + pub ident: Ident, + pub colon_token: Token![:], + pub ty: Type, + pub semi_token: Token![;], +} + +impl Parse for BareStatic { + fn parse(input: ParseStream) -> Result<Self> { + Ok(Self { + attrs: input.call(Attribute::parse_outer)?, + vis: input.parse()?, + static_token: input.parse()?, + ident: input.parse()?, + colon_token: input.parse()?, + ty: input.parse()?, + semi_token: input.parse()?, + }) + } +} diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index 2de0adf2..9791427b 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -1,4 +1,15 @@ -use super::*; +use heck::{ToKebabCase, ToShoutySnakeCase, ToUpperCamelCase}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, Ident, Result, Token}; + +use crate::util::{ + determine_name_and_title, documentation, foundations, has_attr, kw, parse_attr, + parse_flag, parse_string, parse_string_array, quote_option, validate_attrs, + BlockWithReturn, +}; /// Expand the `#[elem]` macro. pub fn elem(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> { @@ -136,7 +147,6 @@ struct Field { synthesized: bool, borrowed: bool, ghost: bool, - forced_variant: Option<usize>, parse: Option<BlockWithReturn>, default: Option<syn::Expr>, } @@ -226,10 +236,6 @@ fn parse_field(field: &syn::Field) -> Result<Field> { docs: documentation(&attrs), internal: has_attr(&mut attrs, "internal"), external: has_attr(&mut attrs, "external"), - forced_variant: parse_attr::<syn::LitInt>(&mut attrs, "variant")? - .flatten() - .map(|lit| lit.base10_parse()) - .transpose()?, positional, required, variadic, @@ -262,12 +268,12 @@ fn parse_field(field: &syn::Field) -> Result<Field> { if field.resolve { let output = &field.output; - field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output }; + field.output = parse_quote! { <#output as #foundations::Resolve>::Output }; } if field.fold { let output = &field.output; - field.output = parse_quote! { <#output as ::typst::model::Fold>::Output }; + field.output = parse_quote! { <#output as #foundations::Fold>::Output }; } validate_attrs(&attrs)?; @@ -317,8 +323,8 @@ fn create(element: &Elem) -> Result<TokenStream> { let label_and_location = element.unless_capability("Unlabellable", || { quote! { - location: Option<::typst::model::Location>, - label: Option<::typst::model::Label>, + location: Option<::typst::introspection::Location>, + label: Option<#foundations::Label>, prepared: bool, } }); @@ -330,7 +336,7 @@ fn create(element: &Elem) -> Result<TokenStream> { #vis struct #ident { span: ::typst::syntax::Span, #label_and_location - guards: ::std::vec::Vec<::typst::model::Guard>, + guards: ::std::vec::Vec<#foundations::Guard>, #(#fields,)* } @@ -353,9 +359,9 @@ fn create(element: &Elem) -> Result<TokenStream> { #partial_eq_impl #repr_impl - impl ::typst::eval::IntoValue for #ident { - fn into_value(self) -> ::typst::eval::Value { - ::typst::eval::Value::Content(::typst::model::Content::new(self)) + impl #foundations::IntoValue for #ident { + fn into_value(self) -> #foundations::Value { + #foundations::Value::Content(#foundations::Content::new(self)) } } }) @@ -382,12 +388,9 @@ fn create_field(field: &Field) -> TokenStream { /// Creates the element's enum for field identifiers. fn create_fields_enum(element: &Elem) -> TokenStream { - let model = quote! { ::typst::model }; let Elem { ident, enum_ident, .. } = element; - let mut fields = element.real_fields().collect::<Vec<_>>(); - fields.sort_by_key(|field| field.forced_variant.unwrap_or(usize::MAX)); - + let fields = element.real_fields().collect::<Vec<_>>(); let field_names = fields.iter().map(|Field { name, .. }| name).collect::<Vec<_>>(); let field_consts = fields .iter() @@ -399,20 +402,14 @@ fn create_fields_enum(element: &Elem) -> TokenStream { .map(|Field { enum_ident, .. }| enum_ident) .collect::<Vec<_>>(); - let definitions = - fields.iter().map(|Field { forced_variant, enum_ident, .. }| { - if let Some(variant) = forced_variant { - let variant = proc_macro2::Literal::u8_unsuffixed(*variant as _); - quote! { #enum_ident = #variant } - } else { - quote! { #enum_ident } - } - }); + let definitions = fields.iter().map(|Field { enum_ident, .. }| { + quote! { #enum_ident } + }); quote! { // To hide the private type const _: () = { - impl #model::ElementFields for #ident { + impl #foundations::ElementFields for #ident { type Fields = #enum_ident; } @@ -578,16 +575,15 @@ fn create_push_field_method(field: &Field) -> TokenStream { /// Create a setter method for a field. fn create_set_field_method(element: &Elem, field: &Field) -> TokenStream { - let model = quote! { ::typst::model }; let elem = &element.ident; let Field { vis, ident, set_ident, enum_ident, ty, name, .. } = field; let doc = format!("Create a style property for the `{}` field.", name); quote! { #[doc = #doc] - #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { - ::typst::model::Style::Property(::typst::model::Property::new( - <Self as ::typst::model::NativeElement>::elem(), - <#elem as #model::ElementFields>::Fields::#enum_ident as u8, + #vis fn #set_ident(#ident: #ty) -> #foundations::Style { + #foundations::Style::Property(#foundations::Property::new( + <Self as #foundations::NativeElement>::elem(), + <#elem as #foundations::ElementFields>::Fields::#enum_ident as u8, #ident, )) } @@ -608,7 +604,7 @@ fn create_field_in_method(element: &Elem, field: &Field) -> TokenStream { quote! { #[doc = #doc] - #vis fn #ident_in(styles: ::typst::model::StyleChain) -> #output { + #vis fn #ident_in(styles: #foundations::StyleChain) -> #output { #access } } @@ -646,7 +642,7 @@ fn create_field_method(element: &Elem, field: &Field) -> TokenStream { quote! { #[doc = #docs] - #vis fn #ident<'a>(&'a self, styles: ::typst::model::StyleChain<'a>) -> &'a #output { + #vis fn #ident<'a>(&'a self, styles: #foundations::StyleChain<'a>) -> &'a #output { #access } } @@ -655,7 +651,7 @@ fn create_field_method(element: &Elem, field: &Field) -> TokenStream { quote! { #[doc = #docs] - #vis fn #ident(&self, styles: ::typst::model::StyleChain) -> #output { + #vis fn #ident(&self, styles: #foundations::StyleChain) -> #output { #access } } @@ -668,7 +664,6 @@ fn create_style_chain_access( field: &Field, inherent: TokenStream, ) -> TokenStream { - let model = quote! { ::typst::model }; let elem = &element.ident; let Field { ty, default, enum_ident, .. } = field; @@ -693,8 +688,8 @@ fn create_style_chain_access( quote! { #init styles.#getter::<#ty>( - <Self as ::typst::model::NativeElement>::elem(), - <#elem as #model::ElementFields>::Fields::#enum_ident as u8, + <Self as #foundations::NativeElement>::elem(), + <#elem as #foundations::ElementFields>::Fields::#enum_ident as u8, #inherent, #default, ) @@ -703,9 +698,6 @@ fn create_style_chain_access( /// Creates the element's `Pack` implementation. fn create_native_elem_impl(element: &Elem) -> TokenStream { - let eval = quote! { ::typst::eval }; - let model = quote! { ::typst::model }; - let Elem { name, ident, title, scope, keywords, docs, .. } = element; let vtable_func = create_vtable_func(element); @@ -716,9 +708,9 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { .map(create_param_info); let scope = if *scope { - quote! { <#ident as #eval::NativeScope>::scope() } + quote! { <#ident as #foundations::NativeScope>::scope() } } else { - quote! { #eval::Scope::new() } + quote! { #foundations::Scope::new() } }; // Fields that can be accessed using the `field` method. @@ -729,37 +721,39 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { if field.ghost { quote! { - <#elem as #model::ElementFields>::Fields::#name => None, + <#elem as #foundations::ElementFields>::Fields::#name => None, } } else if field.inherent() { quote! { - <#elem as #model::ElementFields>::Fields::#name => Some( - ::typst::eval::IntoValue::into_value(self.#field_ident.clone()) + <#elem as #foundations::ElementFields>::Fields::#name => Some( + #foundations::IntoValue::into_value(self.#field_ident.clone()) ), } } else { quote! { - <#elem as #model::ElementFields>::Fields::#name => { - self.#field_ident.clone().map(::typst::eval::IntoValue::into_value) + <#elem as #foundations::ElementFields>::Fields::#name => { + self.#field_ident.clone().map(#foundations::IntoValue::into_value) } } } }); // Fields that can be set using the `set_field` method. - let field_set_matches = element.visible_fields() - .filter(|field| field.settable() && !field.synthesized && !field.ghost).map(|field| { - let elem = &element.ident; - let name = &field.enum_ident; - let field_ident = &field.ident; + let field_set_matches = element + .visible_fields() + .filter(|field| field.settable() && !field.synthesized && !field.ghost) + .map(|field| { + let elem = &element.ident; + let name = &field.enum_ident; + let field_ident = &field.ident; - quote! { - <#elem as #model::ElementFields>::Fields::#name => { - self.#field_ident = Some(::typst::eval::FromValue::from_value(value)?); - return Ok(()); + quote! { + <#elem as #foundations::ElementFields>::Fields::#name => { + self.#field_ident = Some(#foundations::FromValue::from_value(value)?); + return Ok(()); + } } - } - }); + }); // Fields that are inherent. let field_inherent_matches = element @@ -771,8 +765,8 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { let field_ident = &field.ident; quote! { - <#elem as #model::ElementFields>::Fields::#name => { - self.#field_ident = ::typst::eval::FromValue::from_value(value)?; + <#elem as #foundations::ElementFields>::Fields::#name => { + self.#field_ident = #foundations::FromValue::from_value(value)?; return Ok(()); } } @@ -790,13 +784,13 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { // Internal fields create an error that they are unknown. let unknown_field = format!("unknown field `{field_name}` on `{name}`"); quote! { - <#elem as #model::ElementFields>::Fields::#ident => ::typst::diag::bail!(#unknown_field), + <#elem as #foundations::ElementFields>::Fields::#ident => ::typst::diag::bail!(#unknown_field), } } else { // Fields that cannot be set create an error that they are not settable. let not_settable = format!("cannot set `{field_name}` on `{name}`"); quote! { - <#elem as #model::ElementFields>::Fields::#ident => ::typst::diag::bail!(#not_settable), + <#elem as #foundations::ElementFields>::Fields::#ident => ::typst::diag::bail!(#not_settable), } } }); @@ -824,15 +818,15 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { let field_ident = &field.ident; let field_call = if name.len() > 15 { - quote! { EcoString::from(#name).into() } + quote! { ::ecow::EcoString::from(#name).into() } } else { - quote! { EcoString::inline(#name).into() } + quote! { ::ecow::EcoString::inline(#name).into() } }; quote! { fields.insert( #field_call, - ::typst::eval::IntoValue::into_value(self.#field_ident.clone()) + #foundations::IntoValue::into_value(self.#field_ident.clone()) ); } }); @@ -847,16 +841,16 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { let field_ident = &field.ident; let field_call = if name.len() > 15 { - quote! { EcoString::from(#name).into() } + quote! { ::ecow::EcoString::from(#name).into() } } else { - quote! { EcoString::inline(#name).into() } + quote! { ::ecow::EcoString::inline(#name).into() } }; quote! { if let Some(value) = &self.#field_ident { fields.insert( #field_call, - ::typst::eval::IntoValue::into_value(value.clone()) + #foundations::IntoValue::into_value(value.clone()) ); } } @@ -889,7 +883,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { let label_field = element .unless_capability("Unlabellable", || { quote! { - self.label().map(::typst::eval::Value::Label) + self.label().map(#foundations::Value::Label) } }) .unwrap_or_else(|| quote! { None }); @@ -905,51 +899,51 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { let local_name = element .if_capability( "LocalName", - || quote! { Some(<#ident as ::typst::model::LocalName>::local_name) }, + || quote! { Some(<#ident as ::typst::text::LocalName>::local_name) }, ) .unwrap_or_else(|| quote! { None }); let unknown_field = format!("unknown field {{}} on {}", name); let label_error = format!("cannot set label on {}", name); let data = quote! { - #model::NativeElementData { + #foundations::NativeElementData { name: #name, title: #title, docs: #docs, keywords: &[#(#keywords),*], - construct: <#ident as #model::Construct>::construct, - set: <#ident as #model::Set>::set, + construct: <#ident as #foundations::Construct>::construct, + set: <#ident as #foundations::Set>::set, vtable: #vtable_func, field_id: |name| < - <#ident as #model::ElementFields>::Fields as ::std::str::FromStr + <#ident as #foundations::ElementFields>::Fields as ::std::str::FromStr >::from_str(name).ok().map(|id| id as u8), field_name: |id| < - <#ident as #model::ElementFields>::Fields as ::std::convert::TryFrom<u8> - >::try_from(id).ok().map(<#ident as #model::ElementFields>::Fields::to_str), + <#ident as #foundations::ElementFields>::Fields as ::std::convert::TryFrom<u8> + >::try_from(id).ok().map(<#ident as #foundations::ElementFields>::Fields::to_str), local_name: #local_name, - scope: #eval::Lazy::new(|| #scope), - params: #eval::Lazy::new(|| ::std::vec![#(#params),*]) + scope: #foundations::Lazy::new(|| #scope), + params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]) } }; quote! { - impl #model::NativeElement for #ident { - fn data() -> &'static #model::NativeElementData { - static DATA: #model::NativeElementData = #data; + impl #foundations::NativeElement for #ident { + fn data() -> &'static #foundations::NativeElementData { + static DATA: #foundations::NativeElementData = #data; &DATA } - fn dyn_elem(&self) -> #model::Element { - #model::Element::of::<Self>() + fn dyn_elem(&self) -> #foundations::Element { + #foundations::Element::of::<Self>() } fn dyn_hash(&self, mut hasher: &mut dyn ::std::hash::Hasher) { <Self as ::std::hash::Hash>::hash(self, &mut hasher); } - fn dyn_eq(&self, other: &#model::Content) -> bool { + fn dyn_eq(&self, other: &#foundations::Content) -> bool { if let Some(other) = other.to::<Self>() { <Self as ::std::cmp::PartialEq>::eq(self, other) } else { @@ -957,7 +951,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { } } - fn dyn_clone(&self) -> ::std::sync::Arc<dyn #model::NativeElement> { + fn dyn_clone(&self) -> ::std::sync::Arc<dyn #foundations::NativeElement> { ::std::sync::Arc::new(Clone::clone(self)) } @@ -983,27 +977,27 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { } } - fn label(&self) -> Option<#model::Label> { + fn label(&self) -> Option<#foundations::Label> { #label } - fn set_label(&mut self, label: #model::Label) { + fn set_label(&mut self, label: #foundations::Label) { #set_label } - fn location(&self) -> Option<#model::Location> { + fn location(&self) -> Option<::typst::introspection::Location> { #location } - fn set_location(&mut self, location: #model::Location) { + fn set_location(&mut self, location: ::typst::introspection::Location) { #set_location } - fn push_guard(&mut self, guard: #model::Guard) { + fn push_guard(&mut self, guard: #foundations::Guard) { self.guards.push(guard); } - fn is_guarded(&self, guard: #model::Guard) -> bool { + fn is_guarded(&self, guard: #foundations::Guard) -> bool { self.guards.contains(&guard) } @@ -1023,30 +1017,30 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream { #prepared } - fn field(&self, id: u8) -> Option<::typst::eval::Value> { - let id = <#ident as #model::ElementFields>::Fields::try_from(id).ok()?; + fn field(&self, id: u8) -> Option<#foundations::Value> { + let id = <#ident as #foundations::ElementFields>::Fields::try_from(id).ok()?; match id { - <#ident as #model::ElementFields>::Fields::Label => #label_field, + <#ident as #foundations::ElementFields>::Fields::Label => #label_field, #(#field_matches)* _ => None, } } - fn fields(&self) -> Dict { - let mut fields = Dict::new(); + fn fields(&self) -> #foundations::Dict { + let mut fields = #foundations::Dict::new(); #(#field_dict)* #(#field_opt_dict)* fields } - fn set_field(&mut self, id: u8, value: Value) -> ::typst::diag::StrResult<()> { - let id = <#ident as #model::ElementFields>::Fields::try_from(id) + fn set_field(&mut self, id: u8, value: #foundations::Value) -> ::typst::diag::StrResult<()> { + let id = <#ident as #foundations::ElementFields>::Fields::try_from(id) .map_err(|_| ::ecow::eco_format!(#unknown_field, id))?; match id { #(#field_set_matches)* #(#field_inherent_matches)* #(#field_not_set_matches)* - <#ident as #model::ElementFields>::Fields::Label => { + <#ident as #foundations::ElementFields>::Fields::Label => { ::typst::diag::bail!(#label_error); } } @@ -1087,18 +1081,18 @@ fn create_construct_impl(element: &Elem) -> TokenStream { .map(|field| &field.ident); quote! { - impl ::typst::model::Construct for #ident { + impl #foundations::Construct for #ident { fn construct( vm: &mut ::typst::eval::Vm, - args: &mut ::typst::eval::Args, - ) -> ::typst::diag::SourceResult<::typst::model::Content> { + args: &mut #foundations::Args, + ) -> ::typst::diag::SourceResult<#foundations::Content> { #(#pre)* let mut element = Self::new(#(#defaults),*); #(#handlers)* - Ok(::typst::model::Content::new(element)) + Ok(#foundations::Content::new(element)) } } } @@ -1119,12 +1113,12 @@ fn create_set_impl(element: &Elem) -> TokenStream { }); quote! { - impl ::typst::model::Set for #ident { + impl #foundations::Set for #ident { fn set( - vm: &mut Vm, - args: &mut ::typst::eval::Args, - ) -> ::typst::diag::SourceResult<::typst::model::Styles> { - let mut styles = ::typst::model::Styles::new(); + vm: &mut ::typst::eval::Vm, + args: &mut #foundations::Args, + ) -> ::typst::diag::SourceResult<#foundations::Styles> { + let mut styles = #foundations::Styles::new(); #(#handlers)* Ok(styles) } @@ -1135,7 +1129,7 @@ fn create_set_impl(element: &Elem) -> TokenStream { /// Creates the element's `Locatable` implementation. fn create_locatable_impl(element: &Elem) -> TokenStream { let ident = &element.ident; - quote! { impl ::typst::model::Locatable for #ident {} } + quote! { impl ::typst::introspection::Locatable for #ident {} } } /// Creates the element's `PartialEq` implementation. @@ -1159,12 +1153,12 @@ fn create_repr_impl(element: &Elem) -> TokenStream { let ident = &element.ident; let repr_format = format!("{}{{}}", element.name); quote! { - impl ::typst::eval::Repr for #ident { + impl #foundations::Repr for #ident { fn repr(&self) -> ::ecow::EcoString { - let fields = self.fields().into_iter() - .map(|(name, value)| eco_format!("{}: {}", name, value.repr())) + let fields = #foundations::NativeElement::fields(self).into_iter() + .map(|(name, value)| ::ecow::eco_format!("{}: {}", name, value.repr())) .collect::<Vec<_>>(); - ::ecow::eco_format!(#repr_format, ::typst::eval::repr::pretty_array_like(&fields, false)) + ::ecow::eco_format!(#repr_format, #foundations::repr::pretty_array_like(&fields, false)) } } } @@ -1185,7 +1179,7 @@ fn create_vtable_func(element: &Elem) -> TokenStream { if id == ::std::any::TypeId::of::<dyn #capability>() { let vtable = unsafe { let dangling = ::std::ptr::NonNull::<#ident>::dangling().as_ptr() as *const dyn #capability; - ::typst::model::fat::vtable(dangling) + ::typst::util::fat::vtable(dangling) }; return Some(vtable); } @@ -1224,20 +1218,20 @@ fn create_param_info(field: &Field) -> TokenStream { quote! { || { let typed: #default_ty = #default; - ::typst::eval::IntoValue::into_value(typed) + #foundations::IntoValue::into_value(typed) } } })); let ty = if *variadic { - quote! { <#ty as ::typst::eval::Container>::Inner } + quote! { <#ty as #foundations::Container>::Inner } } else { quote! { #ty } }; quote! { - ::typst::eval::ParamInfo { + #foundations::ParamInfo { name: #name, docs: #docs, - input: <#ty as ::typst::eval::Reflect>::input(), + input: <#ty as #foundations::Reflect>::input(), default: #default, positional: #positional, named: #named, diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs index 9040abf9..8537ac4f 100644 --- a/crates/typst-macros/src/func.rs +++ b/crates/typst-macros/src/func.rs @@ -1,4 +1,14 @@ -use super::*; +use heck::ToKebabCase; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{parse_quote, Ident, Result}; + +use crate::util::{ + determine_name_and_title, documentation, foundations, has_attr, kw, parse_attr, + parse_flag, parse_key_value, parse_string, parse_string_array, quote_option, + validate_attrs, +}; /// Expand the `#[func]` macro. pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> { @@ -191,8 +201,6 @@ fn parse_param( /// Produce the function's definition. fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { - let eval = quote! { ::typst::eval }; - let Func { docs, vis, ident, .. } = func; let item = rewrite_fn_item(item); let ty = create_func_ty(func); @@ -200,9 +208,9 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { let creator = if ty.is_some() { quote! { - impl #eval::NativeFunc for #ident { - fn data() -> &'static #eval::NativeFuncData { - static DATA: #eval::NativeFuncData = #data; + impl #foundations::NativeFunc for #ident { + fn data() -> &'static #foundations::NativeFuncData { + static DATA: #foundations::NativeFuncData = #data; &DATA } } @@ -211,8 +219,8 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { let ident_data = quote::format_ident!("{ident}_data"); quote! { #[doc(hidden)] - #vis fn #ident_data() -> &'static #eval::NativeFuncData { - static DATA: #eval::NativeFuncData = #data; + #vis fn #ident_data() -> &'static #foundations::NativeFuncData { + static DATA: #foundations::NativeFuncData = #data; &DATA } } @@ -231,8 +239,6 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { /// Create native function data for the function. fn create_func_data(func: &Func) -> TokenStream { - let eval = quote! { ::typst::eval }; - let Func { ident, name, @@ -247,30 +253,30 @@ fn create_func_data(func: &Func) -> TokenStream { } = func; let scope = if *scope { - quote! { <#ident as #eval::NativeScope>::scope() } + quote! { <#ident as #foundations::NativeScope>::scope() } } else { - quote! { #eval::Scope::new() } + quote! { #foundations::Scope::new() } }; let closure = create_wrapper_closure(func); let params = func.special.self_.iter().chain(&func.params).map(create_param_info); let name = if *constructor { - quote! { <#parent as #eval::NativeType>::NAME } + quote! { <#parent as #foundations::NativeType>::NAME } } else { quote! { #name } }; quote! { - #eval::NativeFuncData { + #foundations::NativeFuncData { function: #closure, name: #name, title: #title, docs: #docs, keywords: &[#(#keywords),*], - scope: #eval::Lazy::new(|| #scope), - params: #eval::Lazy::new(|| ::std::vec![#(#params),*]), - returns: #eval::Lazy::new(|| <#returns as #eval::Reflect>::output()), + scope: #foundations::Lazy::new(|| #scope), + params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]), + returns: #foundations::Lazy::new(|| <#returns as #foundations::Reflect>::output()), } } } @@ -335,7 +341,7 @@ fn create_wrapper_closure(func: &Func) -> TokenStream { #handlers #finish let output = #call; - ::typst::eval::IntoResult::into_result(output, args.span) + #foundations::IntoResult::into_result(output, args.span) } } } @@ -346,7 +352,7 @@ fn create_param_info(param: &Param) -> TokenStream { let positional = !named; let required = !named && default.is_none(); let ty = if *variadic || (*named && default.is_none()) { - quote! { <#ty as ::typst::eval::Container>::Inner } + quote! { <#ty as #foundations::Container>::Inner } } else { quote! { #ty } }; @@ -354,15 +360,15 @@ fn create_param_info(param: &Param) -> TokenStream { quote! { || { let typed: #ty = #default; - ::typst::eval::IntoValue::into_value(typed) + #foundations::IntoValue::into_value(typed) } } })); quote! { - ::typst::eval::ParamInfo { + #foundations::ParamInfo { name: #name, docs: #docs, - input: <#ty as ::typst::eval::Reflect>::input(), + input: <#ty as #foundations::Reflect>::input(), default: #default, positional: #positional, named: #named, diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs index 87048ee5..28d02a6e 100644 --- a/crates/typst-macros/src/lib.rs +++ b/crates/typst-macros/src/lib.rs @@ -5,22 +5,15 @@ extern crate proc_macro; #[macro_use] mod util; mod cast; +mod category; mod elem; mod func; mod scope; mod symbols; mod ty; -use heck::*; use proc_macro::TokenStream as BoundaryStream; -use proc_macro2::TokenStream; -use quote::quote; -use syn::ext::IdentExt; -use syn::parse::{Parse, ParseStream, Parser}; -use syn::punctuated::Punctuated; -use syn::{parse_quote, DeriveInput, Ident, Result, Token}; - -use self::util::*; +use syn::DeriveInput; /// Makes a native Rust function usable as a Typst function. /// @@ -190,9 +183,6 @@ pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { /// - `#[synthesized]`: The field cannot be specified in a constructor or set /// rule. Instead, it is added to an element before its show rule runs /// through the `Synthesize` trait. -/// - `#[variant]`: Allows setting the ID of a field's variant. This is used -/// for fields that are accessed in `typst` and not `typst-library`. It gives -/// the field a stable ID that can be used to access it. /// - `#[ghost]`: Allows creating fields that are only present in the style chain, /// this means that they *cannot* be accessed by the user, they cannot be set /// on an individual instantiated element, and must be set via the style chain. @@ -256,6 +246,15 @@ pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { .into() } +/// Defines a category of definitions. +#[proc_macro_attribute] +pub fn category(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as syn::Item); + category::category(stream.into(), item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + /// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. /// /// - `Reflect` makes Typst's runtime aware of the type's characteristics. diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs index 4aa17c6c..07a0efe0 100644 --- a/crates/typst-macros/src/scope.rs +++ b/crates/typst-macros/src/scope.rs @@ -1,6 +1,9 @@ use heck::ToKebabCase; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse_quote, Result}; -use super::*; +use crate::util::{foundations, BareType}; /// Expand the `#[scope]` macro. pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> { @@ -8,7 +11,6 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> { bail!(item, "expected module or impl item"); }; - let eval = quote! { ::typst::eval }; let self_ty = &item.self_ty; let mut definitions = vec![]; @@ -43,13 +45,13 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> { Ok(quote! { #base - impl #eval::NativeScope for #self_ty { - fn constructor() -> ::std::option::Option<&'static #eval::NativeFuncData> { + impl #foundations::NativeScope for #self_ty { + fn constructor() -> ::std::option::Option<&'static #foundations::NativeFuncData> { #constructor } - fn scope() -> #eval::Scope { - let mut scope = #eval::Scope::deduplicating(); + fn scope() -> #foundations::Scope { + let mut scope = #foundations::Scope::deduplicating(); #(#definitions;)* scope } @@ -92,7 +94,7 @@ fn handle_fn(self_ty: &syn::Type, item: &mut syn::ImplItemFn) -> Result<FnKind> } syn::Meta::List(list) => { let tokens = &list.tokens; - let meta: super::func::Meta = syn::parse2(tokens.clone())?; + let meta: crate::func::Meta = syn::parse2(tokens.clone())?; list.tokens = quote! { #tokens, parent = #self_ty }; if meta.constructor { return Ok(FnKind::Constructor(quote! { Some(#self_ty::#ident_data()) })); @@ -135,7 +137,7 @@ fn rewrite_primitive_base(item: &syn::ItemImpl, ident: &syn::Ident) -> TokenStre let ident_data = quote::format_ident!("{}_data", sig.ident); sigs.push(quote! { #sig; }); sigs.push(quote! { - fn #ident_data() -> &'static ::typst::eval::NativeFuncData; + fn #ident_data() -> &'static #foundations::NativeFuncData; }); } diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs index 8ab47f08..2ddb922f 100644 --- a/crates/typst-macros/src/symbols.rs +++ b/crates/typst-macros/src/symbols.rs @@ -1,4 +1,9 @@ -use super::*; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::punctuated::Punctuated; +use syn::{Ident, Result, Token}; /// Expand the `symbols!` macro. pub fn symbols(stream: TokenStream) -> Result<TokenStream> { @@ -7,7 +12,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> { let pairs = list.iter().map(|symbol| { let name = symbol.name.to_string(); let kind = match &symbol.kind { - Kind::Single(c) => quote! { typst::eval::Symbol::single(#c), }, + Kind::Single(c) => quote! { ::typst::symbols::Symbol::single(#c), }, Kind::Multiple(variants) => { let variants = variants.iter().map(|variant| { let name = &variant.name; @@ -15,7 +20,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> { quote! { (#name, #c) } }); quote! { - typst::eval::Symbol::list(&[#(#variants),*]) + ::typst::symbols::Symbol::list(&[#(#variants),*]) } } }; diff --git a/crates/typst-macros/src/ty.rs b/crates/typst-macros/src/ty.rs index df60e7bb..23f818bd 100644 --- a/crates/typst-macros/src/ty.rs +++ b/crates/typst-macros/src/ty.rs @@ -1,6 +1,12 @@ -use syn::Attribute; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Ident, Result}; -use super::*; +use crate::util::{ + determine_name_and_title, documentation, foundations, kw, parse_flag, parse_string, + parse_string_array, BareType, +}; /// Expand the `#[ty]` macro. pub fn ty(stream: TokenStream, item: syn::Item) -> Result<TokenStream> { @@ -68,44 +74,42 @@ fn parse(meta: Meta, ident: Ident, attrs: &[Attribute]) -> Result<Type> { /// Produce the output of the macro. fn create(ty: &Type, item: Option<&syn::Item>) -> TokenStream { - let eval = quote! { ::typst::eval }; - let Type { ident, name, long, title, docs, keywords, scope, .. } = ty; let constructor = if *scope { - quote! { <#ident as #eval::NativeScope>::constructor() } + quote! { <#ident as #foundations::NativeScope>::constructor() } } else { quote! { None } }; let scope = if *scope { - quote! { <#ident as #eval::NativeScope>::scope() } + quote! { <#ident as #foundations::NativeScope>::scope() } } else { - quote! { #eval::Scope::new() } + quote! { #foundations::Scope::new() } }; let data = quote! { - #eval::NativeTypeData { + #foundations::NativeTypeData { name: #name, long_name: #long, title: #title, docs: #docs, keywords: &[#(#keywords),*], - constructor: #eval::Lazy::new(|| #constructor), - scope: #eval::Lazy::new(|| #scope), + constructor: #foundations::Lazy::new(|| #constructor), + scope: #foundations::Lazy::new(|| #scope), } }; quote! { #item - impl #eval::NativeType for #ident { + impl #foundations::NativeType for #ident { const NAME: &'static str = #name; - fn data() -> &'static #eval::NativeTypeData { - static DATA: #eval::NativeTypeData = #data; + fn data() -> &'static #foundations::NativeTypeData { + static DATA: #foundations::NativeTypeData = #data; &DATA } } diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs index 790db3e1..32c0aa4e 100644 --- a/crates/typst-macros/src/util.rs +++ b/crates/typst-macros/src/util.rs @@ -1,8 +1,9 @@ -use quote::ToTokens; +use heck::{ToKebabCase, ToTitleCase}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream}; use syn::token::Token; -use syn::Attribute; - -use super::*; +use syn::{Attribute, Ident, Result, Token}; /// Return an error at the given item. macro_rules! bail { @@ -199,6 +200,16 @@ impl<T: Parse> Parse for Array<T> { } } +/// Shorthand for `::typst::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); + } +} + /// For parsing attributes of the form: /// #[attr( /// statement; @@ -220,15 +231,6 @@ impl Parse for BlockWithReturn { } } -pub mod kw { - syn::custom_keyword!(name); - syn::custom_keyword!(title); - syn::custom_keyword!(scope); - syn::custom_keyword!(constructor); - syn::custom_keyword!(keywords); - syn::custom_keyword!(parent); -} - /// Parse a bare `type Name;` item. pub struct BareType { pub attrs: Vec<Attribute>, @@ -239,7 +241,7 @@ pub struct BareType { impl Parse for BareType { fn parse(input: ParseStream) -> Result<Self> { - Ok(BareType { + Ok(Self { attrs: input.call(Attribute::parse_outer)?, type_token: input.parse()?, ident: input.parse()?, @@ -247,3 +249,12 @@ impl Parse for BareType { }) } } + +pub mod kw { + syn::custom_keyword!(name); + syn::custom_keyword!(title); + syn::custom_keyword!(scope); + syn::custom_keyword!(constructor); + syn::custom_keyword!(keywords); + syn::custom_keyword!(parent); +} diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs index e23e7cbe..17c4686a 100644 --- a/crates/typst-pdf/src/color.rs +++ b/crates/typst-pdf/src/color.rs @@ -1,7 +1,7 @@ use once_cell::sync::Lazy; use pdf_writer::types::DeviceNSubtype; use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref}; -use typst::geom::{Color, ColorSpace, Paint}; +use typst::visualize::{Color, ColorSpace, Paint}; use crate::deflate; use crate::page::{PageContext, Transforms}; diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs index a358f9ea..ce3913f7 100644 --- a/crates/typst-pdf/src/font.rs +++ b/crates/typst-pdf/src/font.rs @@ -5,7 +5,7 @@ use ecow::{eco_format, EcoString}; use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; use pdf_writer::{Filter, Finish, Name, Rect, Str}; use ttf_parser::{name_id, GlyphId, Tag}; -use typst::font::Font; +use typst::text::Font; use typst::util::SliceExt; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs index ecb01ee6..5e7e5f3d 100644 --- a/crates/typst-pdf/src/gradient.rs +++ b/crates/typst-pdf/src/gradient.rs @@ -5,9 +5,10 @@ use ecow::{eco_format, EcoString}; use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType}; use pdf_writer::writers::StreamShadingType; use pdf_writer::{Filter, Finish, Name, Ref}; -use typst::geom::{ - Abs, Angle, Color, ColorSpace, ConicGradient, Gradient, Numeric, Point, Quadrant, - Ratio, Relative, Transform, WeightedColor, +use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; +use typst::util::Numeric; +use typst::visualize::{ + Color, ColorSpace, ConicGradient, Gradient, GradientRelative, WeightedColor, }; use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor}; @@ -301,8 +302,8 @@ fn register_gradient( transforms.size.y = Abs::pt(1.0); } let size = match gradient.unwrap_relative(on_text) { - Relative::Self_ => transforms.size, - Relative::Parent => transforms.container_size, + GradientRelative::Self_ => transforms.size, + GradientRelative::Parent => transforms.container_size, }; let (offset_x, offset_y) = match gradient { @@ -316,8 +317,8 @@ fn register_gradient( let rotation = gradient.angle().unwrap_or_else(Angle::zero); let transform = match gradient.unwrap_relative(on_text) { - Relative::Self_ => transforms.transform, - Relative::Parent => transforms.container_transform, + GradientRelative::Self_ => transforms.transform, + GradientRelative::Parent => transforms.container_transform, }; let scale_offset = match gradient { diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 63329b42..2e99eef5 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -3,9 +3,10 @@ use std::io::Cursor; use image::{DynamicImage, GenericImageView, Rgba}; use pdf_writer::{Chunk, Filter, Finish, Ref}; -use typst::geom::ColorSpace; -use typst::image::{Image, ImageKind, RasterFormat, RasterImage, SvgImage}; use typst::util::Deferred; +use typst::visualize::{ + ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage, +}; use crate::{deflate, PdfContext}; diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 628129b6..c753315c 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -16,13 +16,13 @@ use base64::Engine; use ecow::{eco_format, EcoString}; use pdf_writer::types::Direction; use pdf_writer::{Finish, Name, Pdf, Ref, TextStr}; -use typst::doc::{Document, Lang}; -use typst::eval::Datetime; -use typst::font::Font; -use typst::geom::{Abs, Dir, Em}; -use typst::image::Image; -use typst::model::Introspector; +use typst::foundations::Datetime; +use typst::introspection::Introspector; +use typst::layout::{Abs, Dir, Em}; +use typst::model::Document; +use typst::text::{Font, Lang}; use typst::util::Deferred; +use typst::visualize::Image; use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter}; use crate::color::ColorSpaces; diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index 4fd072b6..aafaa5b9 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, Ref, TextStr}; -use typst::eval::{item, Smart}; -use typst::geom::Abs; -use typst::model::Content; +use typst::foundations::{Content, NativeElement, Smart}; +use typst::layout::Abs; +use typst::model::HeadingElem; use crate::{AbsExt, PdfContext}; @@ -18,7 +18,7 @@ pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> { // Therefore, its next descendant must be added at its level, which is // enforced in the manner shown below. let mut last_skipped_level = None; - for heading in ctx.introspector.query(&item!(heading_elem).select()).iter() { + for heading in ctx.introspector.query(&HeadingElem::elem().select()).iter() { let leaf = HeadingNode::leaf((**heading).clone()); if leaf.bookmarked { diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 74b32302..545380da 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -8,16 +8,17 @@ use pdf_writer::types::{ }; use pdf_writer::writers::PageLabel; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr}; -use typst::doc::{ - Destination, Frame, FrameItem, GroupItem, Meta, PdfPageLabel, PdfPageLabelStyle, - TextItem, +use typst::introspection::Meta; +use typst::layout::{ + Abs, Em, Frame, FrameItem, GroupItem, PdfPageLabel, PdfPageLabelStyle, Point, Ratio, + Size, Transform, }; -use typst::font::Font; -use typst::geom::{ - self, Abs, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint, Point, - Ratio, Shape, Size, Transform, +use typst::model::Destination; +use typst::text::{Font, TextItem}; +use typst::util::Numeric; +use typst::visualize::{ + FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, }; -use typst::image::Image; use crate::color::PaintEncode; use crate::extg::ExtGState; @@ -581,7 +582,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) { adjustment = Em::zero(); } - let cid = super::font::glyph_cid(&text.font, glyph.id); + let cid = crate::font::glyph_cid(&text.font, glyph.id); encoded.push((cid >> 8) as u8); encoded.push((cid & 0xff) as u8); @@ -656,16 +657,16 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) { } /// Encode a bezier path into the content stream. -fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) { +fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &Path) { for elem in &path.0 { match elem { - geom::PathItem::MoveTo(p) => { + PathItem::MoveTo(p) => { ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) } - geom::PathItem::LineTo(p) => { + PathItem::LineTo(p) => { ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()) } - geom::PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to( + PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to( x + p1.x.to_f32(), y + p1.y.to_f32(), x + p2.x.to_f32(), @@ -673,7 +674,7 @@ fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) { x + p3.x.to_f32(), y + p3.y.to_f32(), ), - geom::PathItem::ClosePath => ctx.content.close_path(), + PathItem::ClosePath => ctx.content.close_path(), }; } } diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index 0b6edf00..251f647a 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -9,13 +9,15 @@ use pixglyph::Bitmap; use resvg::tiny_skia::IntRect; use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; -use typst::doc::{Frame, FrameItem, FrameKind, GroupItem, Meta, TextItem}; -use typst::font::Font; -use typst::geom::{ - self, Abs, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint, - PathItem, Point, Ratio, Relative, Shape, Size, Transform, +use typst::introspection::Meta; +use typst::layout::{ + Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, +}; +use typst::text::{Font, TextItem}; +use typst::visualize::{ + Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageKind, LineCap, + LineJoin, Paint, Path, PathItem, RasterFormat, Shape, }; -use typst::image::{Image, ImageKind, RasterFormat}; use usvg::{NodeExt, TreeParsing}; /// Export a frame into a raster image. @@ -634,7 +636,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option< } /// Convert a Typst path into a tiny-skia path. -fn convert_path(path: &geom::Path) -> Option<sk::Path> { +fn convert_path(path: &Path) -> Option<sk::Path> { let mut builder = sk::PathBuilder::new(); for elem in &path.0 { match elem { @@ -773,13 +775,13 @@ impl<'a> GradientSampler<'a> { ) -> Self { let relative = gradient.unwrap_relative(on_text); let container_size = match relative { - Relative::Self_ => item_size, - Relative::Parent => state.size, + GradientRelative::Self_ => item_size, + GradientRelative::Parent => state.size, }; let fill_transform = match relative { - Relative::Self_ => sk::Transform::identity(), - Relative::Parent => state.container_transform.invert().unwrap(), + GradientRelative::Self_ => sk::Transform::identity(), + GradientRelative::Parent => state.container_transform.invert().unwrap(), }; Self { @@ -857,13 +859,13 @@ fn to_sk_paint<'a>( Paint::Gradient(gradient) => { let relative = gradient.unwrap_relative(on_text); let container_size = match relative { - Relative::Self_ => item_size, - Relative::Parent => state.size, + GradientRelative::Self_ => item_size, + GradientRelative::Parent => state.size, }; let fill_transform = match relative { - Relative::Self_ => fill_transform.unwrap_or_default(), - Relative::Parent => state + GradientRelative::Self_ => fill_transform.unwrap_or_default(), + GradientRelative::Parent => state .container_transform .post_concat(state.transform.invert().unwrap()), }; diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index ed34f3dc..7d3a773d 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -6,16 +6,18 @@ use std::io::Read; use base64::Engine; use ecow::{eco_format, EcoString}; use ttf_parser::{GlyphId, OutlineBuilder}; -use typst::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem}; -use typst::eval::Repr; -use typst::font::Font; -use typst::geom::{ - self, Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, - Paint, PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size, - Transform, +use typst::foundations::Repr; +use typst::layout::{ + Abs, Angle, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Quadrant, Ratio, + Size, Transform, }; -use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; +use typst::text::{Font, TextItem}; use typst::util::hash128; +use typst::visualize::{ + Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageFormat, + LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RatioOrAngle, Shape, + VectorFormat, +}; use xmlwriter::XmlWriter; /// The number of segments in a conic gradient. @@ -432,8 +434,8 @@ impl SVGRenderer { }; match gradient.unwrap_relative(true) { - Relative::Self_ => Transform::scale(Ratio::one(), Ratio::one()), - Relative::Parent => Transform::scale( + GradientRelative::Self_ => Transform::scale(Ratio::one(), Ratio::one()), + GradientRelative::Parent => Transform::scale( Ratio::new(state.size.x.to_pt()), Ratio::new(state.size.y.to_pt()), ) @@ -488,11 +490,11 @@ impl SVGRenderer { if let Paint::Gradient(gradient) = paint { match gradient.unwrap_relative(false) { - Relative::Self_ => Transform::scale( + GradientRelative::Self_ => Transform::scale( Ratio::new(shape_size.x.to_pt()), Ratio::new(shape_size.y.to_pt()), ), - Relative::Parent => Transform::scale( + GradientRelative::Parent => Transform::scale( Ratio::new(state.size.x.to_pt()), Ratio::new(state.size.y.to_pt()), ) @@ -517,8 +519,8 @@ impl SVGRenderer { if let Paint::Gradient(gradient) = paint { match gradient.unwrap_relative(false) { - Relative::Self_ => shape_size, - Relative::Parent => state.size, + GradientRelative::Self_ => shape_size, + GradientRelative::Parent => state.size, } } else { shape_size @@ -1047,7 +1049,7 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString { builder.0 } -fn convert_path(path: &geom::Path) -> EcoString { +fn convert_path(path: &Path) -> EcoString { let mut builder = SvgPathBuilder::default(); for item in &path.0 { match item { diff --git a/crates/typst-syntax/src/reparser.rs b/crates/typst-syntax/src/reparser.rs index 1374bde0..1b7dc42f 100644 --- a/crates/typst-syntax/src/reparser.rs +++ b/crates/typst-syntax/src/reparser.rs @@ -11,7 +11,7 @@ use crate::{ /// ultimately reparsed. /// /// The high-level API for this function is -/// [`Source::edit`](super::Source::edit). +/// [`Source::edit`](crate::Source::edit). pub fn reparse( root: &mut SyntaxNode, text: &str, diff --git a/crates/typst-syntax/src/span.rs b/crates/typst-syntax/src/span.rs index 5b8fd693..14e5e216 100644 --- a/crates/typst-syntax/src/span.rs +++ b/crates/typst-syntax/src/span.rs @@ -7,7 +7,7 @@ use crate::FileId; /// A unique identifier for a syntax node. /// /// This is used throughout the compiler to track which source section an error -/// or element stems from. Can be [mapped back](super::Source::range) to a byte +/// or element stems from. Can be [mapped back](crate::Source::range) to a byte /// range for user facing display. /// /// During editing, the span values stay mostly stable, even for nodes behind an @@ -76,7 +76,7 @@ impl Span { Some(FileId::from_raw(bits)) } - /// The unique number of the span within its [`Source`](super::Source). + /// The unique number of the span within its [`Source`](crate::Source). pub const fn number(self) -> u64 { self.0.get() & ((1 << Self::BITS) - 1) } diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml index 53512d90..c9c19f8e 100644 --- a/crates/typst/Cargo.toml +++ b/crates/typst/Cargo.toml @@ -18,14 +18,26 @@ bench = false [dependencies] typst-macros = { workspace = true } typst-syntax = { workspace = true } +az = { workspace = true } bitflags = { workspace = true } +chinese-number = { workspace = true } +ciborium = { workspace = true } comemo = { workspace = true } +csv = { workspace = true } ecow = { 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 } image = { workspace = true } indexmap = { workspace = true } kurbo = { workspace = true } lasso = { workspace = true } +lipsum = { workspace = true } log = { workspace = true } once_cell = { workspace = true } palette = { workspace = true } @@ -34,13 +46,19 @@ regex = { workspace = true } roxmltree = { 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 } tracing = { workspace = true } ttf-parser = { workspace = true } +typed-arena = { workspace = true } +unicode-bidi = { workspace = true } unicode-math-class = { workspace = true } +unicode-script = { workspace = true } unicode-segmentation = { workspace = true } usvg = { workspace = true } wasmi = { workspace = true } diff --git a/crates/typst-library/assets/cj_linebreak_data.postcard b/crates/typst/assets/cj_linebreak_data.postcard Binary files differindex 910dd167..910dd167 100644 --- a/crates/typst-library/assets/cj_linebreak_data.postcard +++ b/crates/typst/assets/cj_linebreak_data.postcard diff --git a/crates/typst-library/assets/icudata.postcard b/crates/typst/assets/icudata.postcard Binary files differindex a1fdbd48..a1fdbd48 100644 --- a/crates/typst-library/assets/icudata.postcard +++ b/crates/typst/assets/icudata.postcard diff --git a/crates/typst-library/assets/syntect.bin b/crates/typst/assets/syntect.bin Binary files differindex 043602a4..043602a4 100644 --- a/crates/typst-library/assets/syntect.bin +++ b/crates/typst/assets/syntect.bin diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs index 830237e3..c0106b6a 100644 --- a/crates/typst/src/diag.rs +++ b/crates/typst/src/diag.rs @@ -42,10 +42,16 @@ macro_rules! __bail { } #[doc(inline)] -pub use crate::{__bail as bail, __error as error, __warning as warning}; +pub use crate::__bail as bail; +#[doc(inline)] +pub use crate::__error as error; +#[doc(inline)] +pub use crate::__warning as warning; #[doc(hidden)] -pub use ecow::{eco_format, EcoString}; +pub use ecow::eco_format; +#[doc(hidden)] +pub use ecow::EcoString; /// Construct an [`EcoString`] or [`SourceDiagnostic`] with severity `Error`. #[macro_export] @@ -159,6 +165,25 @@ impl From<SyntaxError> for SourceDiagnostic { } } +/// Holds delayed errors. +#[derive(Default, Clone)] +pub struct DelayedErrors(pub EcoVec<SourceDiagnostic>); + +impl DelayedErrors { + /// Create an empty list of delayed errors. + pub fn new() -> Self { + Self::default() + } +} + +#[comemo::track] +impl DelayedErrors { + /// Push a delayed error. + pub fn push(&mut self, error: SourceDiagnostic) { + self.0.push(error); + } +} + /// A part of a diagnostic's [trace](SourceDiagnostic::trace). #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Tracepoint { diff --git a/crates/typst/src/eval/access.rs b/crates/typst/src/eval/access.rs new file mode 100644 index 00000000..ff0b7512 --- /dev/null +++ b/crates/typst/src/eval/access.rs @@ -0,0 +1,99 @@ +use ecow::eco_format; + +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}; + +/// Access an expression mutably. +pub(crate) trait Access { + /// Access the value. + fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value>; +} + +impl Access for ast::Expr<'_> { + fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + match self { + Self::Ident(v) => v.access(vm), + Self::Parenthesized(v) => v.access(vm), + Self::FieldAccess(v) => v.access(vm), + Self::FuncCall(v) => v.access(vm), + _ => { + let _ = self.eval(vm)?; + bail!(self.span(), "cannot mutate a temporary value"); + } + } + } +} + +impl Access for ast::Ident<'_> { + fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + let span = self.span(); + let value = vm.scopes.get_mut(&self).at(span)?; + if vm.inspected == Some(span) { + vm.vt.tracer.value(value.clone()); + } + Ok(value) + } +} + +impl Access for ast::Parenthesized<'_> { + fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + self.expr().access(vm) + } +} + +impl Access for ast::FieldAccess<'_> { + fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + access_dict(vm, self)?.at_mut(self.field().get()).at(self.span()) + } +} + +impl Access for ast::FuncCall<'_> { + fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { + if let ast::Expr::FieldAccess(access) = self.callee() { + let method = access.field(); + if is_accessor_method(&method) { + let span = self.span(); + let world = vm.world(); + let args = self.args().eval(vm)?; + let value = access.target().access(vm)?; + let result = call_method_access(value, &method, args, span); + let point = || Tracepoint::Call(Some(method.get().clone())); + return result.trace(world, point, span); + } + } + + let _ = self.eval(vm)?; + bail!(self.span(), "cannot mutate a temporary value"); + } +} + +pub(crate) fn access_dict<'a>( + vm: &'a mut Vm, + access: ast::FieldAccess, +) -> SourceResult<&'a mut Dict> { + match access.target().access(vm)? { + Value::Dict(dict) => Ok(dict), + value => { + let ty = value.ty(); + let span = access.target().span(); + if matches!( + value, // those types have their own field getters + Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_) + ) { + bail!(span, "cannot mutate fields on {ty}"); + } else if crate::foundations::fields_on(ty).is_empty() { + bail!(span, "{ty} does not have accessible fields"); + } else { + // type supports static fields, which don't yet have + // setters + Err(eco_format!("fields on {ty} are not yet mutable")) + .hint(eco_format!( + "try creating a new {ty} with the updated field value instead" + )) + .at(span) + } + } + } +} diff --git a/crates/typst/src/eval/binding.rs b/crates/typst/src/eval/binding.rs new file mode 100644 index 00000000..eac35924 --- /dev/null +++ b/crates/typst/src/eval/binding.rs @@ -0,0 +1,179 @@ +use std::collections::HashSet; + +use crate::diag::{bail, At, SourceResult}; +use crate::eval::{Access, Eval, Vm}; +use crate::foundations::{Array, Dict, Value}; +use crate::syntax::ast::{self, AstNode}; + +impl Eval for ast::LetBinding<'_> { + type Output = Value; + + #[tracing::instrument(name = "LetBinding::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = match self.init() { + Some(expr) => expr.eval(vm)?, + None => Value::None, + }; + if vm.flow.is_some() { + return Ok(Value::None); + } + + match self.kind() { + ast::LetBindingKind::Normal(pattern) => destructure(vm, pattern, value)?, + ast::LetBindingKind::Closure(ident) => vm.define(ident, value), + } + + Ok(Value::None) + } +} + +impl Eval for ast::DestructAssignment<'_> { + type Output = Value; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.value().eval(vm)?; + destructure_impl(vm, self.pattern(), value, |vm, expr, value| { + let location = expr.access(vm)?; + *location = value; + Ok(()) + })?; + Ok(Value::None) + } +} + +/// Destructures a value into a pattern. +pub(crate) fn destructure( + vm: &mut Vm, + pattern: ast::Pattern, + value: Value, +) -> SourceResult<()> { + destructure_impl(vm, pattern, value, |vm, expr, value| match expr { + ast::Expr::Ident(ident) => { + vm.define(ident, value); + Ok(()) + } + _ => bail!(expr.span(), "nested patterns are currently not supported"), + }) +} + +/// Destruct the given value into the pattern and apply the function to each binding. +#[tracing::instrument(skip_all)] +fn destructure_impl<T>( + vm: &mut Vm, + pattern: ast::Pattern, + value: Value, + f: T, +) -> SourceResult<()> +where + T: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>, +{ + match pattern { + ast::Pattern::Normal(expr) => { + f(vm, expr, value)?; + } + ast::Pattern::Placeholder(_) => {} + ast::Pattern::Destructuring(destruct) => match value { + Value::Array(value) => destructure_array(vm, pattern, value, f, destruct)?, + Value::Dict(value) => destructure_dict(vm, value, f, destruct)?, + _ => bail!(pattern.span(), "cannot destructure {}", value.ty()), + }, + } + Ok(()) +} + +fn destructure_array<F>( + vm: &mut Vm, + pattern: ast::Pattern, + value: Array, + f: F, + destruct: ast::Destructuring, +) -> SourceResult<()> +where + F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>, +{ + let mut i = 0; + let len = value.as_slice().len(); + for p in destruct.bindings() { + match p { + ast::DestructuringKind::Normal(expr) => { + let Ok(v) = value.at(i as i64, None) else { + bail!(expr.span(), "not enough elements to destructure"); + }; + f(vm, expr, v)?; + i += 1; + } + ast::DestructuringKind::Sink(spread) => { + let sink_size = (1 + len).checked_sub(destruct.bindings().count()); + let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s)); + if let (Some(sink_size), Some(sink)) = (sink_size, sink) { + if let Some(expr) = spread.expr() { + f(vm, expr, Value::Array(sink.into()))?; + } + i += sink_size; + } else { + bail!(pattern.span(), "not enough elements to destructure") + } + } + ast::DestructuringKind::Named(named) => { + bail!(named.span(), "cannot destructure named elements from an array") + } + ast::DestructuringKind::Placeholder(underscore) => { + if i < len { + i += 1 + } else { + bail!(underscore.span(), "not enough elements to destructure") + } + } + } + } + if i < len { + bail!(pattern.span(), "too many elements to destructure"); + } + + Ok(()) +} + +fn destructure_dict<F>( + vm: &mut Vm, + dict: Dict, + f: F, + destruct: ast::Destructuring, +) -> SourceResult<()> +where + F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>, +{ + let mut sink = None; + let mut used = HashSet::new(); + for p in destruct.bindings() { + match p { + ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => { + let v = dict.get(&ident).at(ident.span())?; + f(vm, ast::Expr::Ident(ident), v.clone())?; + used.insert(ident.as_str()); + } + ast::DestructuringKind::Sink(spread) => sink = spread.expr(), + ast::DestructuringKind::Named(named) => { + let name = named.name(); + let v = dict.get(&name).at(name.span())?; + f(vm, named.expr(), v.clone())?; + used.insert(name.as_str()); + } + ast::DestructuringKind::Placeholder(_) => {} + ast::DestructuringKind::Normal(expr) => { + bail!(expr.span(), "expected key, found expression"); + } + } + } + + if let Some(expr) = sink { + let mut sink = Dict::new(); + for (key, value) in dict { + if !used.contains(key.as_str()) { + sink.insert(key, value); + } + } + f(vm, expr, Value::Dict(sink))?; + } + + Ok(()) +} diff --git a/crates/typst/src/eval/call.rs b/crates/typst/src/eval/call.rs new file mode 100644 index 00000000..f87ac771 --- /dev/null +++ b/crates/typst/src/eval/call.rs @@ -0,0 +1,587 @@ +use comemo::{Prehashed, Tracked, TrackedMut}; +use ecow::EcoVec; + +use crate::diag::{ + bail, error, At, DelayedErrors, HintedStrResult, SourceResult, Trace, Tracepoint, +}; +use crate::eval::{Access, Eval, FlowEvent, Route, Tracer, Vm}; +use crate::foundations::{ + call_method_mut, is_mutating_method, Arg, Args, Bytes, Closure, Content, Func, + IntoValue, NativeElement, Scope, Scopes, Value, +}; +use crate::introspection::{Introspector, Locator}; +use crate::layout::Vt; +use crate::math::{Accent, AccentElem, LrElem}; +use crate::symbols::Symbol; +use crate::syntax::ast::{self, AstNode}; +use crate::syntax::{Spanned, SyntaxNode}; +use crate::text::TextElem; +use crate::World; + +/// The maxmium function call depth. +const MAX_CALL_DEPTH: usize = 64; + +impl Eval for ast::FuncCall<'_> { + type Output = Value; + + #[tracing::instrument(name = "FuncCall::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.span(); + if vm.depth >= MAX_CALL_DEPTH { + bail!(span, "maximum function call depth exceeded"); + } + + let callee = self.callee(); + let in_math = in_math(callee); + let callee_span = callee.span(); + let args = self.args(); + + // Try to evaluate as a call to an associated function or field. + let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { + let target = access.target(); + let target_span = target.span(); + let field = access.field(); + let field_span = field.span(); + + let target = if is_mutating_method(&field) { + let mut args = args.eval(vm)?; + let target = target.access(vm)?; + + // Only arrays and dictionaries have mutable methods. + if matches!(target, Value::Array(_) | Value::Dict(_)) { + args.span = span; + let point = || Tracepoint::Call(Some(field.get().clone())); + return call_method_mut(target, &field, args, span).trace( + vm.world(), + point, + span, + ); + } + + target.clone() + } else { + access.target().eval(vm)? + }; + + let mut args = args.eval(vm)?; + + // Handle plugins. + if let Value::Plugin(plugin) = &target { + let bytes = args.all::<Bytes>()?; + args.finish()?; + return Ok(plugin.call(&field, bytes).at(span)?.into_value()); + } + + // Prioritize associated functions on the value's type (i.e., + // methods) over its fields. A function call on a field is only + // allowed for functions, types, modules (because they are scopes), + // and symbols (because they have modifiers). + // + // For dictionaries, it is not allowed because it would be ambiguous + // (prioritizing associated functions would make an addition of a + // new associated function a breaking change and prioritizing fields + // would break associated functions for certain dictionaries). + if let Some(callee) = target.ty().scope().get(&field) { + let this = Arg { + span: target_span, + name: None, + value: Spanned::new(target, target_span), + }; + args.span = span; + args.items.insert(0, this); + (callee.clone(), args) + } else if matches!( + target, + Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) + ) { + (target.field(&field).at(field_span)?, args) + } else { + let mut error = error!( + field_span, + "type {} has no method `{}`", + target.ty(), + field.as_str() + ); + + if let Value::Dict(dict) = target { + if matches!(dict.get(&field), Ok(Value::Func(_))) { + error.hint( + "to call the function stored in the dictionary, \ + surround the field access with parentheses", + ); + } + } + + bail!(error); + } + } else { + (callee.eval(vm)?, args.eval(vm)?) + }; + + // Handle math special cases for non-functions: + // Combining accent symbols apply themselves while everything else + // simply displays the arguments verbatim. + if in_math && !matches!(callee, Value::Func(_)) { + if let Value::Symbol(sym) = &callee { + let c = sym.get(); + if let Some(accent) = Symbol::combining_accent(c) { + let base = args.expect("base")?; + args.finish()?; + return Ok(Value::Content( + AccentElem::new(base, Accent::new(accent)).pack(), + )); + } + } + let mut body = Content::empty(); + for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { + if i > 0 { + body += TextElem::packed(','); + } + body += arg; + } + return Ok(Value::Content( + callee.display().spanned(callee_span) + + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) + .pack(), + )); + } + + let callee = callee.cast::<Func>().at(callee_span)?; + let point = || Tracepoint::Call(callee.name().map(Into::into)); + let f = || callee.call_vm(vm, args).trace(vm.world(), point, span); + + // Stacker is broken on WASM. + #[cfg(target_arch = "wasm32")] + return f(); + + #[cfg(not(target_arch = "wasm32"))] + stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, f) + } +} + +impl Eval for ast::Args<'_> { + type Output = Args; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let mut items = EcoVec::with_capacity(self.items().count()); + + for arg in self.items() { + let span = arg.span(); + match arg { + ast::Arg::Pos(expr) => { + items.push(Arg { + span, + name: None, + value: Spanned::new(expr.eval(vm)?, expr.span()), + }); + } + ast::Arg::Named(named) => { + items.push(Arg { + span, + name: Some(named.name().get().clone().into()), + value: Spanned::new(named.expr().eval(vm)?, named.expr().span()), + }); + } + ast::Arg::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Array(array) => { + items.extend(array.into_iter().map(|value| Arg { + span, + name: None, + value: Spanned::new(value, span), + })); + } + Value::Dict(dict) => { + items.extend(dict.into_iter().map(|(key, value)| Arg { + span, + name: Some(key), + value: Spanned::new(value, span), + })); + } + Value::Args(args) => items.extend(args.items), + v => bail!(expr.span(), "cannot spread {}", v.ty()), + }, + } + } + + Ok(Args { span: self.span(), items }) + } +} + +impl Eval for ast::Closure<'_> { + type Output = Value; + + #[tracing::instrument(name = "Closure::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + // Evaluate default values of named parameters. + let mut defaults = Vec::new(); + for param in self.params().children() { + if let ast::Param::Named(named) = param { + defaults.push(named.expr().eval(vm)?); + } + } + + // Collect captured variables. + let captured = { + let mut visitor = CapturesVisitor::new(Some(&vm.scopes)); + visitor.visit(self.to_untyped()); + visitor.finish() + }; + + // Define the closure. + let closure = Closure { + node: self.to_untyped().clone(), + file: vm.file, + defaults, + captured, + }; + + Ok(Value::Func(Func::from(closure).spanned(self.params().span()))) + } +} + +/// Call the function in the context with the arguments. +#[comemo::memoize] +#[tracing::instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub(crate) fn call_closure( + func: &Func, + closure: &Prehashed<Closure>, + world: Tracked<dyn World + '_>, + route: Tracked<Route>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + depth: usize, + mut args: Args, +) -> SourceResult<Value> { + let node = closure.node.cast::<ast::Closure>().unwrap(); + + // Don't leak the scopes from the call site. Instead, we use the scope + // of captured variables we collected earlier. + let mut scopes = Scopes::new(None); + scopes.top = closure.captured.clone(); + + // Prepare VT. + let mut locator = Locator::chained(locator); + let vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + + // Prepare VM. + let mut vm = Vm::new(vt, route, closure.file, scopes); + vm.depth = depth; + + // Provide the closure itself for recursive calls. + if let Some(name) = node.name() { + vm.define(name, Value::Func(func.clone())); + } + + // Parse the arguments according to the parameter list. + let num_pos_params = node + .params() + .children() + .filter(|p| matches!(p, ast::Param::Pos(_))) + .count(); + let num_pos_args = args.to_pos().len(); + let sink_size = num_pos_args.checked_sub(num_pos_params); + + let mut sink = None; + let mut sink_pos_values = None; + let mut defaults = closure.defaults.iter(); + for p in node.params().children() { + match p { + ast::Param::Pos(pattern) => match pattern { + ast::Pattern::Normal(ast::Expr::Ident(ident)) => { + vm.define(ident, args.expect::<Value>(&ident)?) + } + ast::Pattern::Normal(_) => unreachable!(), + pattern => { + crate::eval::destructure( + &mut vm, + pattern, + args.expect::<Value>("pattern parameter")?, + )?; + } + }, + ast::Param::Sink(ident) => { + sink = ident.name(); + if let Some(sink_size) = sink_size { + sink_pos_values = Some(args.consume(sink_size)?); + } + } + ast::Param::Named(named) => { + let name = named.name(); + let default = defaults.next().unwrap(); + let value = + args.named::<Value>(&name)?.unwrap_or_else(|| default.clone()); + vm.define(name, value); + } + } + } + + if let Some(sink) = sink { + let mut remaining_args = args.take(); + if let Some(sink_pos_values) = sink_pos_values { + remaining_args.items.extend(sink_pos_values); + } + vm.define(sink, remaining_args); + } + + // Ensure all arguments have been used. + args.finish()?; + + // Handle control flow. + let output = node.body().eval(&mut vm)?; + match vm.flow { + Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit), + Some(FlowEvent::Return(_, None)) => {} + Some(flow) => bail!(flow.forbidden()), + None => {} + } + + Ok(output) +} + +fn in_math(expr: ast::Expr) -> bool { + match expr { + ast::Expr::MathIdent(_) => true, + ast::Expr::FieldAccess(access) => in_math(access.target()), + _ => false, + } +} + +/// A visitor that determines which variables to capture for a closure. +pub struct CapturesVisitor<'a> { + external: Option<&'a Scopes<'a>>, + internal: Scopes<'a>, + captures: Scope, +} + +impl<'a> CapturesVisitor<'a> { + /// Create a new visitor for the given external scopes. + pub fn new(external: Option<&'a Scopes<'a>>) -> Self { + Self { + external, + internal: Scopes::new(None), + captures: Scope::new(), + } + } + + /// Return the scope of captured variables. + pub fn finish(self) -> Scope { + self.captures + } + + /// Visit any node and collect all captured variables. + #[tracing::instrument(skip_all)] + pub fn visit(&mut self, node: &SyntaxNode) { + match node.cast() { + // Every identifier is a potential variable that we need to capture. + // Identifiers that shouldn't count as captures because they + // actually bind a new name are handled below (individually through + // the expressions that contain them). + Some(ast::Expr::Ident(ident)) => self.capture(&ident, Scopes::get), + Some(ast::Expr::MathIdent(ident)) => { + self.capture(&ident, Scopes::get_in_math) + } + + // Code and content blocks create a scope. + Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { + self.internal.enter(); + for child in node.children() { + self.visit(child); + } + self.internal.exit(); + } + + // Don't capture the field of a field access. + Some(ast::Expr::FieldAccess(access)) => { + self.visit(access.target().to_untyped()); + } + + // A closure contains parameter bindings, which are bound before the + // body is evaluated. Care must be taken so that the default values + // of named parameters cannot access previous parameter bindings. + Some(ast::Expr::Closure(expr)) => { + for param in expr.params().children() { + if let ast::Param::Named(named) = param { + self.visit(named.expr().to_untyped()); + } + } + + self.internal.enter(); + if let Some(name) = expr.name() { + self.bind(name); + } + + for param in expr.params().children() { + match param { + ast::Param::Pos(pattern) => { + for ident in pattern.idents() { + self.bind(ident); + } + } + ast::Param::Named(named) => self.bind(named.name()), + ast::Param::Sink(spread) => { + self.bind(spread.name().unwrap_or_default()) + } + } + } + + self.visit(expr.body().to_untyped()); + self.internal.exit(); + } + + // A let expression contains a binding, but that binding is only + // active after the body is evaluated. + Some(ast::Expr::Let(expr)) => { + if let Some(init) = expr.init() { + self.visit(init.to_untyped()); + } + + for ident in expr.kind().idents() { + self.bind(ident); + } + } + + // A for loop contains one or two bindings in its pattern. These are + // active after the iterable is evaluated but before the body is + // evaluated. + Some(ast::Expr::For(expr)) => { + self.visit(expr.iter().to_untyped()); + self.internal.enter(); + + let pattern = expr.pattern(); + for ident in pattern.idents() { + self.bind(ident); + } + + self.visit(expr.body().to_untyped()); + self.internal.exit(); + } + + // An import contains items, but these are active only after the + // path is evaluated. + Some(ast::Expr::Import(expr)) => { + self.visit(expr.source().to_untyped()); + if let Some(ast::Imports::Items(items)) = expr.imports() { + for item in items.iter() { + self.bind(item.bound_name()); + } + } + } + + _ => { + // Never capture the name part of a named pair. + if let Some(named) = node.cast::<ast::Named>() { + self.visit(named.expr().to_untyped()); + return; + } + + // Everything else is traversed from left to right. + for child in node.children() { + self.visit(child); + } + } + } + } + + /// Bind a new internal variable. + fn bind(&mut self, ident: ast::Ident) { + self.internal.top.define(ident.get().clone(), Value::None); + } + + /// Capture a variable if it isn't internal. + #[inline] + fn capture( + &mut self, + ident: &str, + getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>, + ) { + if self.internal.get(ident).is_err() { + let Some(value) = self + .external + .map(|external| getter(external, ident).ok()) + .unwrap_or(Some(&Value::None)) + else { + return; + }; + + self.captures.define_captured(ident, value.clone()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::syntax::parse; + + #[track_caller] + fn test(text: &str, result: &[&str]) { + let mut scopes = Scopes::new(None); + scopes.top.define("f", 0); + scopes.top.define("x", 0); + scopes.top.define("y", 0); + scopes.top.define("z", 0); + + let mut visitor = CapturesVisitor::new(Some(&scopes)); + let root = parse(text); + visitor.visit(&root); + + let captures = visitor.finish(); + let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect(); + names.sort(); + + assert_eq!(names, result); + } + + #[test] + fn test_captures() { + // Let binding and function definition. + test("#let x = x", &["x"]); + test("#let x; #(x + y)", &["y"]); + test("#let f(x, y) = x + y", &[]); + test("#let f(x, y) = f", &[]); + test("#let f = (x, y) => f", &["f"]); + + // Closure with different kinds of params. + test("#((x, y) => x + z)", &["z"]); + test("#((x: y, z) => x + z)", &["y"]); + test("#((..x) => x + y)", &["y"]); + test("#((x, y: x + z) => x + y)", &["x", "z"]); + test("#{x => x; x}", &["x"]); + + // Show rule. + test("#show y: x => x", &["y"]); + test("#show y: x => x + z", &["y", "z"]); + test("#show x: x => x", &["x"]); + + // For loop. + test("#for x in y { x + z }", &["y", "z"]); + test("#for (x, y) in y { x + y }", &["y"]); + test("#for x in y {} #x", &["x", "y"]); + + // Import. + test("#import z: x, y", &["z"]); + test("#import x + y: x, y, z", &["x", "y"]); + + // Blocks. + test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); + test("#[#let x = 1]#x", &["x"]); + + // Field access. + test("#foo(body: 1)", &[]); + test("#(body: 1)", &[]); + test("#(body = 1)", &[]); + test("#(body += y)", &["y"]); + test("#{ (body, a) = (y, 1) }", &["y"]); + test("#(x.at(y) = 5)", &["x", "y"]) + } +} diff --git a/crates/typst/src/eval/code.rs b/crates/typst/src/eval/code.rs new file mode 100644 index 00000000..1df5876c --- /dev/null +++ b/crates/typst/src/eval/code.rs @@ -0,0 +1,317 @@ +use ecow::{eco_vec, EcoVec}; + +use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult}; +use crate::eval::{ops, Eval, Vm}; +use crate::foundations::{Array, Content, Dict, Str, Value}; +use crate::syntax::ast::{self, AstNode}; + +impl Eval for ast::Code<'_> { + type Output = Value; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + eval_code(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of expressions. +fn eval_code<'a>( + vm: &mut Vm, + exprs: &mut impl Iterator<Item = ast::Expr<'a>>, +) -> SourceResult<Value> { + let flow = vm.flow.take(); + let mut output = Value::None; + + while let Some(expr) = exprs.next() { + let span = expr.span(); + let value = match expr { + ast::Expr::Set(set) => { + let styles = set.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_code(vm, exprs)?.display(); + Value::Content(tail.styled_with_map(styles)) + } + ast::Expr::Show(show) => { + let recipe = show.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_code(vm, exprs)?.display(); + Value::Content(tail.styled_with_recipe(vm, recipe)?) + } + _ => expr.eval(vm)?, + }; + + output = ops::join(output, value).at(span)?; + + if vm.flow.is_some() { + break; + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) +} + +impl Eval for ast::Expr<'_> { + type Output = Value; + + #[tracing::instrument(name = "Expr::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.span(); + let forbidden = |name| { + error!(span, "{} is only allowed directly in code and content blocks", name) + }; + + let v = match self { + Self::Text(v) => v.eval(vm).map(Value::Content), + Self::Space(v) => v.eval(vm).map(Value::Content), + Self::Linebreak(v) => v.eval(vm).map(Value::Content), + Self::Parbreak(v) => v.eval(vm).map(Value::Content), + Self::Escape(v) => v.eval(vm), + Self::Shorthand(v) => v.eval(vm), + Self::SmartQuote(v) => v.eval(vm).map(Value::Content), + Self::Strong(v) => v.eval(vm).map(Value::Content), + Self::Emph(v) => v.eval(vm).map(Value::Content), + Self::Raw(v) => v.eval(vm).map(Value::Content), + Self::Link(v) => v.eval(vm).map(Value::Content), + Self::Label(v) => v.eval(vm), + Self::Ref(v) => v.eval(vm).map(Value::Content), + Self::Heading(v) => v.eval(vm).map(Value::Content), + Self::List(v) => v.eval(vm).map(Value::Content), + Self::Enum(v) => v.eval(vm).map(Value::Content), + Self::Term(v) => v.eval(vm).map(Value::Content), + Self::Equation(v) => v.eval(vm).map(Value::Content), + Self::Math(v) => v.eval(vm).map(Value::Content), + Self::MathIdent(v) => v.eval(vm), + Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), + Self::MathDelimited(v) => v.eval(vm).map(Value::Content), + Self::MathAttach(v) => v.eval(vm).map(Value::Content), + Self::MathPrimes(v) => v.eval(vm).map(Value::Content), + Self::MathFrac(v) => v.eval(vm).map(Value::Content), + Self::MathRoot(v) => v.eval(vm).map(Value::Content), + Self::Ident(v) => v.eval(vm), + Self::None(v) => v.eval(vm), + Self::Auto(v) => v.eval(vm), + Self::Bool(v) => v.eval(vm), + Self::Int(v) => v.eval(vm), + Self::Float(v) => v.eval(vm), + Self::Numeric(v) => v.eval(vm), + Self::Str(v) => v.eval(vm), + Self::Code(v) => v.eval(vm), + Self::Content(v) => v.eval(vm).map(Value::Content), + Self::Array(v) => v.eval(vm).map(Value::Array), + Self::Dict(v) => v.eval(vm).map(Value::Dict), + Self::Parenthesized(v) => v.eval(vm), + Self::FieldAccess(v) => v.eval(vm), + Self::FuncCall(v) => v.eval(vm), + Self::Closure(v) => v.eval(vm), + Self::Unary(v) => v.eval(vm), + Self::Binary(v) => v.eval(vm), + Self::Let(v) => v.eval(vm), + Self::DestructAssign(v) => v.eval(vm), + Self::Set(_) => bail!(forbidden("set")), + Self::Show(_) => bail!(forbidden("show")), + Self::Conditional(v) => v.eval(vm), + Self::While(v) => v.eval(vm), + Self::For(v) => v.eval(vm), + Self::Import(v) => v.eval(vm), + Self::Include(v) => v.eval(vm).map(Value::Content), + Self::Break(v) => v.eval(vm), + Self::Continue(v) => v.eval(vm), + Self::Return(v) => v.eval(vm), + }? + .spanned(span); + + if vm.inspected == Some(span) { + vm.vt.tracer.value(v.clone()); + } + + Ok(v) + } +} + +impl Eval for ast::Ident<'_> { + type Output = Value; + + #[tracing::instrument(name = "Ident::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.get(&self).cloned().at(self.span()) + } +} + +impl Eval for ast::None<'_> { + type Output = Value; + + #[tracing::instrument(name = "None::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::None) + } +} + +impl Eval for ast::Auto<'_> { + type Output = Value; + + #[tracing::instrument(name = "Auto::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Auto) + } +} + +impl Eval for ast::Bool<'_> { + type Output = Value; + + #[tracing::instrument(name = "Bool::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Bool(self.get())) + } +} + +impl Eval for ast::Int<'_> { + type Output = Value; + + #[tracing::instrument(name = "Int::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Int(self.get())) + } +} + +impl Eval for ast::Float<'_> { + type Output = Value; + + #[tracing::instrument(name = "Float::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Float(self.get())) + } +} + +impl Eval for ast::Numeric<'_> { + type Output = Value; + + #[tracing::instrument(name = "Numeric::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::numeric(self.get())) + } +} + +impl Eval for ast::Str<'_> { + type Output = Value; + + #[tracing::instrument(name = "Str::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Str(self.get().into())) + } +} + +impl Eval for ast::Array<'_> { + type Output = Array; + + #[tracing::instrument(skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let items = self.items(); + + let mut vec = EcoVec::with_capacity(items.size_hint().0); + for item in items { + match item { + ast::ArrayItem::Pos(expr) => vec.push(expr.eval(vm)?), + ast::ArrayItem::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Array(array) => vec.extend(array.into_iter()), + v => bail!(expr.span(), "cannot spread {} into array", v.ty()), + }, + } + } + + Ok(vec.into()) + } +} + +impl Eval for ast::Dict<'_> { + type Output = Dict; + + #[tracing::instrument(skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let mut map = indexmap::IndexMap::new(); + + let mut invalid_keys = eco_vec![]; + + for item in self.items() { + match item { + ast::DictItem::Named(named) => { + map.insert(named.name().get().clone().into(), named.expr().eval(vm)?); + } + ast::DictItem::Keyed(keyed) => { + let raw_key = keyed.key(); + let key = raw_key.eval(vm)?; + let key = key.cast::<Str>().unwrap_or_else(|error| { + let error = SourceDiagnostic::error(raw_key.span(), error); + invalid_keys.push(error); + Str::default() + }); + map.insert(key, keyed.expr().eval(vm)?); + } + ast::DictItem::Spread(expr) => match expr.eval(vm)? { + Value::None => {} + Value::Dict(dict) => map.extend(dict.into_iter()), + v => bail!(expr.span(), "cannot spread {} into dictionary", v.ty()), + }, + } + } + + if !invalid_keys.is_empty() { + return Err(invalid_keys); + } + + Ok(map.into()) + } +} + +impl Eval for ast::CodeBlock<'_> { + type Output = Value; + + #[tracing::instrument(name = "CodeBlock::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.enter(); + let output = self.body().eval(vm)?; + vm.scopes.exit(); + Ok(output) + } +} + +impl Eval for ast::ContentBlock<'_> { + type Output = Content; + + #[tracing::instrument(name = "ContentBlock::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.enter(); + let content = self.body().eval(vm)?; + vm.scopes.exit(); + Ok(content) + } +} + +impl Eval for ast::Parenthesized<'_> { + type Output = Value; + + #[tracing::instrument(name = "Parenthesized::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + self.expr().eval(vm) + } +} + +impl Eval for ast::FieldAccess<'_> { + type Output = Value; + + #[tracing::instrument(name = "FieldAccess::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.target().eval(vm)?; + let field = self.field(); + value.field(&field).at(field.span()) + } +} diff --git a/crates/typst/src/eval/flow.rs b/crates/typst/src/eval/flow.rs new file mode 100644 index 00000000..ff1f1c8c --- /dev/null +++ b/crates/typst/src/eval/flow.rs @@ -0,0 +1,227 @@ +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}; + +/// The maximum number of loop iterations. +const MAX_ITERATIONS: usize = 10_000; + +/// A control flow event that occurred during evaluation. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum FlowEvent { + /// Stop iteration in a loop. + Break(Span), + /// Skip the remainder of the current iteration in a loop. + Continue(Span), + /// Stop execution of a function early, optionally returning an explicit + /// value. + Return(Span, Option<Value>), +} + +impl FlowEvent { + /// Return an error stating that this control flow is forbidden. + pub fn forbidden(&self) -> SourceDiagnostic { + match *self { + Self::Break(span) => { + error!(span, "cannot break outside of loop") + } + Self::Continue(span) => { + error!(span, "cannot continue outside of loop") + } + Self::Return(span, _) => { + error!(span, "cannot return outside of function") + } + } + } +} + +impl Eval for ast::Conditional<'_> { + type Output = Value; + + #[tracing::instrument(name = "Conditional::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let condition = self.condition(); + if condition.eval(vm)?.cast::<bool>().at(condition.span())? { + self.if_body().eval(vm) + } else if let Some(else_body) = self.else_body() { + else_body.eval(vm) + } else { + Ok(Value::None) + } + } +} + +impl Eval for ast::WhileLoop<'_> { + type Output = Value; + + #[tracing::instrument(name = "WhileLoop::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let flow = vm.flow.take(); + let mut output = Value::None; + let mut i = 0; + + let condition = self.condition(); + let body = self.body(); + + while condition.eval(vm)?.cast::<bool>().at(condition.span())? { + if i == 0 + && is_invariant(condition.to_untyped()) + && !can_diverge(body.to_untyped()) + { + bail!(condition.span(), "condition is always true"); + } else if i >= MAX_ITERATIONS { + bail!(self.span(), "loop seems to be infinite"); + } + + let value = body.eval(vm)?; + output = ops::join(output, value).at(body.span())?; + + match vm.flow { + Some(FlowEvent::Break(_)) => { + vm.flow = None; + break; + } + Some(FlowEvent::Continue(_)) => vm.flow = None, + Some(FlowEvent::Return(..)) => break, + None => {} + } + + i += 1; + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) + } +} + +impl Eval for ast::ForLoop<'_> { + type Output = Value; + + #[tracing::instrument(name = "ForLoop::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let flow = vm.flow.take(); + let mut output = Value::None; + + macro_rules! iter { + (for $pat:ident in $iter:expr) => {{ + vm.scopes.enter(); + + #[allow(unused_parens)] + for value in $iter { + destructure(vm, $pat, value.into_value())?; + + let body = self.body(); + let value = body.eval(vm)?; + output = ops::join(output, value).at(body.span())?; + + match vm.flow { + Some(FlowEvent::Break(_)) => { + vm.flow = None; + break; + } + Some(FlowEvent::Continue(_)) => vm.flow = None, + Some(FlowEvent::Return(..)) => break, + None => {} + } + } + + vm.scopes.exit(); + }}; + } + + let iter = self.iter().eval(vm)?; + let pattern = self.pattern(); + + match (&pattern, iter.clone()) { + (ast::Pattern::Normal(_), Value::Str(string)) => { + // Iterate over graphemes of string. + iter!(for pattern in string.as_str().graphemes(true)); + } + (_, Value::Dict(dict)) => { + // Iterate over pairs of dict. + iter!(for pattern in dict.pairs()); + } + (_, Value::Array(array)) => { + // Iterate over values of array. + iter!(for pattern in array); + } + (ast::Pattern::Normal(_), _) => { + bail!(self.iter().span(), "cannot loop over {}", iter.ty()); + } + (_, _) => { + bail!(pattern.span(), "cannot destructure values of {}", iter.ty()) + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(output) + } +} + +impl Eval for ast::LoopBreak<'_> { + type Output = Value; + + #[tracing::instrument(name = "LoopBreak::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + if vm.flow.is_none() { + vm.flow = Some(FlowEvent::Break(self.span())); + } + Ok(Value::None) + } +} + +impl Eval for ast::LoopContinue<'_> { + type Output = Value; + + #[tracing::instrument(name = "LoopContinue::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + if vm.flow.is_none() { + vm.flow = Some(FlowEvent::Continue(self.span())); + } + Ok(Value::None) + } +} + +impl Eval for ast::FuncReturn<'_> { + type Output = Value; + + #[tracing::instrument(name = "FuncReturn::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let value = self.body().map(|body| body.eval(vm)).transpose()?; + if vm.flow.is_none() { + vm.flow = Some(FlowEvent::Return(self.span(), value)); + } + Ok(Value::None) + } +} + +/// Whether the expression always evaluates to the same value. +fn is_invariant(expr: &SyntaxNode) -> bool { + match expr.cast() { + Some(ast::Expr::Ident(_)) => false, + Some(ast::Expr::MathIdent(_)) => false, + Some(ast::Expr::FieldAccess(access)) => { + is_invariant(access.target().to_untyped()) + } + Some(ast::Expr::FuncCall(call)) => { + is_invariant(call.callee().to_untyped()) + && is_invariant(call.args().to_untyped()) + } + _ => expr.children().all(is_invariant), + } +} + +/// Whether the expression contains a break or return. +fn can_diverge(expr: &SyntaxNode) -> bool { + matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return) + || expr.children().any(can_diverge) +} diff --git a/crates/typst/src/eval/import.rs b/crates/typst/src/eval/import.rs new file mode 100644 index 00000000..79daf999 --- /dev/null +++ b/crates/typst/src/eval/import.rs @@ -0,0 +1,227 @@ +use comemo::TrackedMut; +use ecow::{eco_format, eco_vec, EcoString}; +use serde::{Deserialize, Serialize}; + +use crate::diag::{ + bail, error, warning, At, FileError, SourceResult, StrResult, Trace, Tracepoint, +}; +use crate::eval::{eval, Eval, Vm}; +use crate::foundations::{Content, Module, Value}; +use crate::syntax::ast::{self, AstNode}; +use crate::syntax::{FileId, PackageSpec, PackageVersion, Span, VirtualPath}; +use crate::World; + +impl Eval for ast::ModuleImport<'_> { + type Output = Value; + + #[tracing::instrument(name = "ModuleImport::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let source = self.source(); + let source_span = source.span(); + let mut source = source.eval(vm)?; + let new_name = self.new_name(); + let imports = self.imports(); + + match &source { + Value::Func(func) => { + if func.scope().is_none() { + bail!(source_span, "cannot import from user-defined functions"); + } + } + Value::Type(_) => {} + other => { + source = Value::Module(import(vm, other.clone(), source_span, true)?); + } + } + + if let Some(new_name) = &new_name { + if let ast::Expr::Ident(ident) = self.source() { + if ident.as_str() == new_name.as_str() { + // Warn on `import x as x` + vm.vt.tracer.warn(warning!( + new_name.span(), + "unnecessary import rename to same name", + )); + } + } + + // Define renamed module on the scope. + vm.scopes.top.define(new_name.as_str(), source.clone()); + } + + let scope = source.scope().unwrap(); + match imports { + None => { + // Only import here if there is no rename. + if new_name.is_none() { + let name: EcoString = source.name().unwrap().into(); + vm.scopes.top.define(name, source); + } + } + Some(ast::Imports::Wildcard) => { + for (var, value) in scope.iter() { + vm.scopes.top.define(var.clone(), value.clone()); + } + } + Some(ast::Imports::Items(items)) => { + let mut errors = eco_vec![]; + for item in items.iter() { + let original_ident = item.original_name(); + if let Some(value) = scope.get(&original_ident) { + // Warn on `import ...: x as x` + if let ast::ImportItem::Renamed(renamed_item) = &item { + if renamed_item.original_name().as_str() + == renamed_item.new_name().as_str() + { + vm.vt.tracer.warn(warning!( + renamed_item.new_name().span(), + "unnecessary import rename to same name", + )); + } + } + + vm.define(item.bound_name(), value.clone()); + } else { + errors.push(error!(original_ident.span(), "unresolved import")); + } + } + if !errors.is_empty() { + return Err(errors); + } + } + } + + Ok(Value::None) + } +} + +impl Eval for ast::ModuleInclude<'_> { + type Output = Content; + + #[tracing::instrument(name = "ModuleInclude::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let span = self.source().span(); + let source = self.source().eval(vm)?; + let module = import(vm, source, span, false)?; + Ok(module.content()) + } +} + +/// Process an import of a module relative to the current location. +pub fn import( + vm: &mut Vm, + source: Value, + span: Span, + allow_scopes: bool, +) -> SourceResult<Module> { + let path = match source { + Value::Str(path) => path, + Value::Module(module) => return Ok(module), + v if allow_scopes => { + bail!(span, "expected path, module, function, or type, found {}", v.ty()) + } + v => bail!(span, "expected path or module, found {}", v.ty()), + }; + + // Handle package and file imports. + let path = path.as_str(); + if path.starts_with('@') { + let spec = path.parse::<PackageSpec>().at(span)?; + import_package(vm, spec, span) + } else { + import_file(vm, path, span) + } +} + +/// Import an external package. +fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> { + // Evaluate the manifest. + let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); + let bytes = vm.world().file(manifest_id).at(span)?; + let manifest = PackageManifest::parse(&bytes).at(span)?; + manifest.validate(&spec).at(span)?; + + // Evaluate the entry point. + let entrypoint_id = manifest_id.join(&manifest.package.entrypoint); + let source = vm.world().source(entrypoint_id).at(span)?; + let point = || Tracepoint::Import; + Ok(eval(vm.world(), vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) + .trace(vm.world(), point, span)? + .with_name(manifest.package.name)) +} + +/// Import a file from a path. +fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> { + // Load the source file. + let world = vm.world(); + let id = vm.resolve_path(path).at(span)?; + let source = world.source(id).at(span)?; + + // Prevent cyclic importing. + if vm.route.contains(source.id()) { + bail!(span, "cyclic import"); + } + + // Evaluate the file. + let point = || Tracepoint::Import; + eval(world, vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) + .trace(world, point, span) +} + +/// A parsed package manifest. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +struct PackageManifest { + /// Details about the package itself. + package: PackageInfo, +} + +/// The `package` key in the manifest. +/// +/// More fields are specified, but they are not relevant to the compiler. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +struct PackageInfo { + /// The name of the package within its namespace. + name: EcoString, + /// The package's version. + version: PackageVersion, + /// The path of the entrypoint into the package. + entrypoint: EcoString, + /// The minimum required compiler version for the package. + compiler: Option<PackageVersion>, +} + +impl PackageManifest { + /// Parse the manifest from raw bytes. + fn parse(bytes: &[u8]) -> StrResult<Self> { + let string = std::str::from_utf8(bytes).map_err(FileError::from)?; + toml::from_str(string).map_err(|err| { + eco_format!("package manifest is malformed: {}", err.message()) + }) + } + + /// Ensure that this manifest is indeed for the specified package. + fn validate(&self, spec: &PackageSpec) -> StrResult<()> { + if self.package.name != spec.name { + bail!("package manifest contains mismatched name `{}`", self.package.name); + } + + if self.package.version != spec.version { + bail!( + "package manifest contains mismatched version {}", + self.package.version + ); + } + + if let Some(compiler) = self.package.compiler { + let current = PackageVersion::compiler(); + if current < compiler { + bail!( + "package requires typst {compiler} or newer \ + (current version is {current})" + ); + } + } + + Ok(()) + } +} diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs deleted file mode 100644 index 77d5ae64..00000000 --- a/crates/typst/src/eval/library.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::num::NonZeroUsize; - -use comemo::Tracked; -use ecow::EcoString; -use std::sync::OnceLock; - -use crate::diag::SourceResult; -use crate::doc::Document; -use crate::eval::Module; -use crate::geom::{Abs, Dir}; -use crate::model::{Content, Element, Introspector, Label, StyleChain, Styles, Vt}; -use crate::util::hash128; - -/// Definition of Typst's standard library. -#[derive(Debug, Clone, Hash)] -pub struct Library { - /// The scope containing definitions that are available everywhere. - pub global: Module, - /// The scope containing definitions available in math mode. - pub math: Module, - /// The default properties for page size, font selection and so on. - pub styles: Styles, - /// Defines which standard library items fulfill which syntactical roles. - pub items: LangItems, -} - -/// Definition of library items the language is aware of. -#[derive(Clone)] -pub struct LangItems { - /// The root layout function. - pub layout: - fn(vt: &mut Vt, content: &Content, styles: StyleChain) -> SourceResult<Document>, - /// Access the em size. - pub em: fn(StyleChain) -> Abs, - /// Access the text direction. - pub dir: fn(StyleChain) -> Dir, - /// Whitespace. - pub space: fn() -> Content, - /// A forced line break: `\`. - pub linebreak: fn() -> Content, - /// Plain text without markup. - pub text: fn(text: EcoString) -> Content, - /// The text element. - pub text_elem: Element, - /// Get the string if this is a text element. - pub text_str: fn(&Content) -> Option<&EcoString>, - /// A smart quote: `'` or `"`. - pub smart_quote: fn(double: bool) -> Content, - /// A paragraph break. - pub parbreak: fn() -> Content, - /// Strong content: `*Strong*`. - pub strong: fn(body: Content) -> Content, - /// Emphasized content: `_Emphasized_`. - pub emph: fn(body: Content) -> Content, - /// Raw text with optional syntax highlighting: `` `...` ``. - pub raw: fn(text: EcoString, tag: Option<EcoString>, block: bool) -> Content, - /// The language names and tags supported by raw text. - pub raw_languages: fn() -> Vec<(&'static str, Vec<&'static str>)>, - /// A hyperlink: `https://typst.org`. - pub link: fn(url: EcoString) -> Content, - /// A reference: `@target`, `@target[..]`. - pub reference: fn(target: Label, supplement: Option<Content>) -> Content, - /// The keys contained in the bibliography and short descriptions of them. - #[allow(clippy::type_complexity)] - pub bibliography_keys: - fn(introspector: Tracked<Introspector>) -> Vec<(EcoString, Option<EcoString>)>, - /// A section heading: `= Introduction`. - pub heading: fn(level: NonZeroUsize, body: Content) -> Content, - /// The heading element. - pub heading_elem: Element, - /// An item in a bullet list: `- ...`. - pub list_item: fn(body: Content) -> Content, - /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - pub enum_item: fn(number: Option<usize>, body: Content) -> Content, - /// An item in a term list: `/ Term: Details`. - pub term_item: fn(term: Content, description: Content) -> Content, - /// A mathematical equation: `$x$`, `$ x^2 $`. - pub equation: fn(body: Content, block: bool) -> Content, - /// An alignment point in math: `&`. - pub math_align_point: fn() -> Content, - /// Matched delimiters in math: `[x + y]`. - pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content, - /// A base with optional attachments in math: `a_1^2`. - #[allow(clippy::type_complexity)] - pub math_attach: fn( - base: Content, - // Positioned smartly. - t: Option<Content>, - b: Option<Content>, - // Fixed positions. - tl: Option<Content>, - bl: Option<Content>, - tr: Option<Content>, - br: Option<Content>, - ) -> Content, - /// Grouped primes: `a'''`. - pub math_primes: fn(count: usize) -> Content, - /// A base with an accent: `arrow(x)`. - pub math_accent: fn(base: Content, accent: char) -> Content, - /// A fraction in math: `x/2`. - pub math_frac: fn(num: Content, denom: Content) -> Content, - /// A root in math: `√x`, `∛x` or `∜x`. - pub math_root: fn(index: Option<Content>, radicand: Content) -> Content, -} - -impl Debug for LangItems { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("LangItems { .. }") - } -} - -impl Hash for LangItems { - fn hash<H: Hasher>(&self, state: &mut H) { - (self.layout as usize).hash(state); - (self.em as usize).hash(state); - (self.dir as usize).hash(state); - self.space.hash(state); - self.linebreak.hash(state); - self.text.hash(state); - self.text_elem.hash(state); - (self.text_str as usize).hash(state); - self.smart_quote.hash(state); - self.parbreak.hash(state); - self.strong.hash(state); - self.emph.hash(state); - self.raw.hash(state); - self.raw_languages.hash(state); - self.link.hash(state); - self.reference.hash(state); - (self.bibliography_keys as usize).hash(state); - self.heading.hash(state); - self.heading_elem.hash(state); - self.list_item.hash(state); - self.enum_item.hash(state); - self.term_item.hash(state); - self.equation.hash(state); - self.math_align_point.hash(state); - self.math_delimited.hash(state); - self.math_attach.hash(state); - self.math_accent.hash(state); - self.math_frac.hash(state); - self.math_root.hash(state); - } -} - -/// Global storage for lang items. -#[doc(hidden)] -pub static LANG_ITEMS: OnceLock<LangItems> = OnceLock::new(); - -/// Set the lang items. -/// -/// This is a hack :( -/// -/// Passing the lang items everywhere they are needed (especially text related -/// things) is very painful. By storing them globally, in theory, we break -/// incremental, but only when different sets of lang items are used in the same -/// program. For this reason, if this function is called multiple times, the -/// items must be the same (and this is enforced). -pub fn set_lang_items(items: LangItems) { - if let Err(items) = LANG_ITEMS.set(items) { - let first = hash128(LANG_ITEMS.get().unwrap()); - let second = hash128(&items); - assert_eq!(first, second, "set differing lang items"); - } -} - -/// Access a lang item. -#[macro_export] -#[doc(hidden)] -macro_rules! __item { - ($name:ident) => { - $crate::eval::LANG_ITEMS.get().unwrap().$name - }; -} - -#[doc(inline)] -pub use crate::__item as item; diff --git a/crates/typst/src/eval/markup.rs b/crates/typst/src/eval/markup.rs new file mode 100644 index 00000000..16ea9eef --- /dev/null +++ b/crates/typst/src/eval/markup.rs @@ -0,0 +1,272 @@ +use crate::diag::{warning, SourceResult}; +use crate::eval::{Eval, Vm}; +use crate::foundations::{Content, Label, NativeElement, Smart, Unlabellable, Value}; +use crate::math::EquationElem; +use crate::model::{ + EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem, + StrongElem, Supplement, TermItem, +}; +use crate::symbols::Symbol; +use crate::syntax::ast::{self, AstNode}; +use crate::text::{LinebreakElem, RawElem, SmartQuoteElem, SpaceElem, TextElem}; + +impl Eval for ast::Markup<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + eval_markup(vm, &mut self.exprs()) + } +} + +/// Evaluate a stream of markup. +fn eval_markup<'a>( + vm: &mut Vm, + exprs: &mut impl Iterator<Item = ast::Expr<'a>>, +) -> SourceResult<Content> { + let flow = vm.flow.take(); + let mut seq = Vec::with_capacity(exprs.size_hint().1.unwrap_or_default()); + + while let Some(expr) = exprs.next() { + match expr { + ast::Expr::Set(set) => { + let styles = set.eval(vm)?; + if vm.flow.is_some() { + break; + } + + seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) + } + ast::Expr::Show(show) => { + let recipe = show.eval(vm)?; + if vm.flow.is_some() { + break; + } + + let tail = eval_markup(vm, exprs)?; + seq.push(tail.styled_with_recipe(vm, recipe)?) + } + expr => match expr.eval(vm)? { + Value::Label(label) => { + if let Some(elem) = + seq.iter_mut().rev().find(|node| !node.can::<dyn Unlabellable>()) + { + *elem = std::mem::take(elem).labelled(label); + } + } + value => seq.push(value.display().spanned(expr.span())), + }, + } + + if vm.flow.is_some() { + break; + } + } + + if flow.is_some() { + vm.flow = flow; + } + + Ok(Content::sequence(seq)) +} + +impl Eval for ast::Text<'_> { + type Output = Content; + + #[tracing::instrument(name = "Text::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(TextElem::packed(self.get().clone())) + } +} + +impl Eval for ast::Space<'_> { + type Output = Content; + + #[tracing::instrument(name = "Space::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(SpaceElem::new().pack()) + } +} + +impl Eval for ast::Linebreak<'_> { + type Output = Content; + + #[tracing::instrument(name = "Linebreak::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(LinebreakElem::new().pack()) + } +} + +impl Eval for ast::Parbreak<'_> { + type Output = Content; + + #[tracing::instrument(name = "Parbreak::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(ParbreakElem::new().pack()) + } +} + +impl Eval for ast::Escape<'_> { + type Output = Value; + + #[tracing::instrument(name = "Escape::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Symbol(Symbol::single(self.get()))) + } +} + +impl Eval for ast::Shorthand<'_> { + type Output = Value; + + #[tracing::instrument(name = "Shorthand::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Symbol(Symbol::single(self.get()))) + } +} + +impl Eval for ast::SmartQuote<'_> { + type Output = Content; + + #[tracing::instrument(name = "SmartQuote::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(SmartQuoteElem::new().with_double(self.double()).pack()) + } +} + +impl Eval for ast::Strong<'_> { + type Output = Content; + + #[tracing::instrument(name = "Strong::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let body = self.body(); + if body.exprs().next().is_none() { + vm.vt + .tracer + .warn(warning!(self.span(), "no text within stars").with_hint( + "using multiple consecutive stars (e.g. **) has no additional effect", + )); + } + + Ok(StrongElem::new(body.eval(vm)?).pack()) + } +} + +impl Eval for ast::Emph<'_> { + type Output = Content; + + #[tracing::instrument(name = "Emph::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let body = self.body(); + if body.exprs().next().is_none() { + vm.vt + .tracer + .warn(warning!(self.span(), "no text within underscores").with_hint( + "using multiple consecutive underscores (e.g. __) has no additional effect" + )); + } + + Ok(EmphElem::new(body.eval(vm)?).pack()) + } +} + +impl Eval for ast::Raw<'_> { + type Output = Content; + + #[tracing::instrument(name = "Raw::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + let mut elem = RawElem::new(self.text()).with_block(self.block()); + if let Some(lang) = self.lang() { + elem.push_lang(Some(lang.into())); + } + Ok(elem.pack()) + } +} + +impl Eval for ast::Link<'_> { + type Output = Content; + + #[tracing::instrument(name = "Link::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(LinkElem::from_url(self.get().clone()).pack()) + } +} + +impl Eval for ast::Label<'_> { + type Output = Value; + + #[tracing::instrument(name = "Label::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(Value::Label(Label::new(self.get()))) + } +} + +impl Eval for ast::Ref<'_> { + type Output = Content; + + #[tracing::instrument(name = "Ref::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let target = Label::new(self.target()); + let mut elem = RefElem::new(target); + if let Some(supplement) = self.supplement() { + elem.push_supplement(Smart::Custom(Some(Supplement::Content( + supplement.eval(vm)?, + )))); + } + Ok(elem.pack()) + } +} + +impl Eval for ast::Heading<'_> { + type Output = Content; + + #[tracing::instrument(name = "Heading::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let level = self.level(); + let body = self.body().eval(vm)?; + Ok(HeadingElem::new(body).with_level(level).pack()) + } +} + +impl Eval for ast::ListItem<'_> { + type Output = Content; + + #[tracing::instrument(name = "ListItem::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok(ListItem::new(self.body().eval(vm)?).pack()) + } +} + +impl Eval for ast::EnumItem<'_> { + type Output = Content; + + #[tracing::instrument(name = "EnumItem::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let body = self.body().eval(vm)?; + let mut elem = EnumItem::new(body); + if let Some(number) = self.number() { + elem.push_number(Some(number)); + } + Ok(elem.pack()) + } +} + +impl Eval for ast::TermItem<'_> { + type Output = Content; + + #[tracing::instrument(name = "TermItem::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let term = self.term().eval(vm)?; + let description = self.description().eval(vm)?; + Ok(TermItem::new(term, description).pack()) + } +} + +impl Eval for ast::Equation<'_> { + type Output = Content; + + #[tracing::instrument(name = "Equation::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let body = self.body().eval(vm)?; + let block = self.block(); + Ok(EquationElem::new(body).with_block(block).pack()) + } +} diff --git a/crates/typst/src/eval/math.rs b/crates/typst/src/eval/math.rs new file mode 100644 index 00000000..2e9a7ead --- /dev/null +++ b/crates/typst/src/eval/math.rs @@ -0,0 +1,113 @@ +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::syntax::ast::{self, AstNode}; +use crate::text::TextElem; + +impl Eval for ast::Math<'_> { + type Output = Content; + + #[tracing::instrument(name = "Math::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + Ok(Content::sequence( + self.exprs() + .map(|expr| expr.eval_display(vm)) + .collect::<SourceResult<Vec<_>>>()?, + )) + } +} + +impl Eval for ast::MathIdent<'_> { + type Output = Value; + + #[tracing::instrument(name = "MathIdent::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + vm.scopes.get_in_math(&self).cloned().at(self.span()) + } +} + +impl Eval for ast::MathAlignPoint<'_> { + type Output = Content; + + #[tracing::instrument(name = "MathAlignPoint::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(AlignPointElem::new().pack()) + } +} + +impl Eval for ast::MathDelimited<'_> { + type Output = Content; + + #[tracing::instrument(name = "MathDelimited::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let open = self.open().eval_display(vm)?; + let body = self.body().eval(vm)?; + let close = self.close().eval_display(vm)?; + Ok(LrElem::new(open + body + close).pack()) + } +} + +impl Eval for ast::MathAttach<'_> { + type Output = Content; + + #[tracing::instrument(name = "MathAttach::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let base = self.base().eval_display(vm)?; + let mut elem = AttachElem::new(base); + + if let Some(expr) = self.top() { + elem.push_t(Some(expr.eval_display(vm)?)); + } else if let Some(primes) = self.primes() { + elem.push_t(Some(primes.eval(vm)?)); + } + + if let Some(expr) = self.bottom() { + elem.push_b(Some(expr.eval_display(vm)?)); + } + + Ok(elem.pack()) + } +} + +impl Eval for ast::MathPrimes<'_> { + type Output = Content; + + #[tracing::instrument(name = "MathPrimes::eval", skip_all)] + fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { + Ok(PrimesElem::new(self.count()).pack()) + } +} + +impl Eval for ast::MathFrac<'_> { + type Output = Content; + + #[tracing::instrument(name = "MathFrac::eval", skip_all)] + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let num = self.num().eval_display(vm)?; + let denom = self.denom().eval_display(vm)?; + Ok(FracElem::new(num, denom).pack()) + } +} + +impl Eval for ast::MathRoot<'_> { + type Output = Content; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let index = self.index().map(|i| TextElem::packed(eco_format!("{i}"))); + let radicand = self.radicand().eval_display(vm)?; + Ok(RootElem::new(radicand).with_index(index).pack()) + } +} + +trait ExprExt { + fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content>; +} + +impl ExprExt for ast::Expr<'_> { + fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content> { + Ok(self.eval(vm)?.display().spanned(self.span())) + } +} diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index ce055f1e..f1bd691b 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -1,102 +1,38 @@ -//! Evaluation of markup into modules. - -#[macro_use] -mod library; -#[macro_use] -mod cast; -#[macro_use] -mod array; -#[macro_use] -mod dict; -#[macro_use] -mod str; -#[macro_use] -mod value; -mod args; -mod auto; -mod bool; -mod bytes; -mod datetime; -mod duration; -mod fields; -mod float; -mod func; -mod int; -mod methods; -mod module; -mod none; -pub mod ops; -mod plugin; -pub mod repr; -mod scope; -mod symbol; +//! Evaluation of markup and code. + +pub(crate) mod ops; + +mod access; +mod binding; +mod call; +mod code; +mod flow; +mod import; +mod markup; +mod math; +mod rules; mod tracer; -mod ty; -mod version; - -#[doc(hidden)] -pub use { - self::library::LANG_ITEMS, - ecow::{eco_format, eco_vec}, - indexmap::IndexMap, - once_cell::sync::Lazy, -}; +mod vm; -pub use self::args::{Arg, Args}; -pub use self::array::{array, Array}; -pub use self::auto::{AutoValue, Smart}; -pub use self::bytes::Bytes; -pub use self::cast::{ - cast, Cast, CastInfo, Container, FromValue, IntoResult, IntoValue, Never, Reflect, -}; -pub use self::datetime::Datetime; -pub use self::dict::{dict, Dict}; -pub use self::duration::Duration; -pub use self::fields::fields_on; -pub use self::func::{ - func, CapturesVisitor, Func, NativeFunc, NativeFuncData, ParamInfo, -}; -pub use self::library::{item, set_lang_items, LangItems, Library}; -pub use self::methods::mutable_methods_on; -pub use self::module::Module; -pub use self::none::NoneValue; -pub use self::plugin::Plugin; -pub use self::repr::Repr; -pub use self::scope::{NativeScope, Scope, Scopes}; -pub use self::str::{format_str, Regex, Str}; -pub use self::symbol::{symbols, Symbol}; -pub use self::tracer::Tracer; -pub use self::ty::{scope, ty, NativeType, NativeTypeData, Type}; -pub use self::value::{Dynamic, Value}; -pub use self::version::Version; +pub use self::call::*; +pub use self::import::*; +pub use self::tracer::*; +pub use self::vm::*; -use std::collections::HashSet; -use std::mem; +pub(crate) use self::access::*; +pub(crate) use self::binding::*; +pub(crate) use self::flow::*; -use comemo::{Track, Tracked, TrackedMut, Validate}; -use ecow::{EcoString, EcoVec}; -use serde::{Deserialize, Serialize}; -use unicode_segmentation::UnicodeSegmentation; +use comemo::{Track, Tracked, TrackedMut}; -use self::func::Closure; -use crate::diag::{ - bail, error, warning, At, FileError, Hint, SourceDiagnostic, SourceResult, StrResult, - Trace, Tracepoint, -}; -use crate::model::{ - Content, DelayedErrors, Introspector, Label, Locator, Recipe, ShowableSelector, - Styles, Transform, Unlabellable, Vt, -}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::{ - parse, parse_code, parse_math, FileId, PackageSpec, PackageVersion, Source, Span, - Spanned, SyntaxKind, SyntaxNode, VirtualPath, -}; +use crate::diag::{bail, DelayedErrors, SourceResult}; +use crate::foundations::{Cast, Module, NativeElement, Scope, Scopes, Value}; +use crate::introspection::{Introspector, Locator}; +use crate::layout::Vt; +use crate::math::EquationElem; +use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span}; use crate::World; -const MAX_ITERATIONS: usize = 10_000; -const MAX_CALL_DEPTH: usize = 64; - /// Evaluate a source file and return the resulting module. #[comemo::memoize] #[tracing::instrument(skip_all)] @@ -112,10 +48,6 @@ pub fn eval( panic!("Tried to cyclicly evaluate {:?}", id.vpath()); } - // Hook up the lang items. - let library = world.library(); - set_lang_items(library.items.clone()); - // Prepare VT. let mut locator = Locator::new(); let introspector = Introspector::default(); @@ -130,7 +62,7 @@ pub fn eval( // Prepare VM. let route = Route::insert(route, id); - let scopes = Scopes::new(Some(library)); + let scopes = Scopes::new(Some(world.library())); let mut vm = Vm::new(vt, route.track(), Some(id), scopes); let root = source.root(); @@ -208,10 +140,11 @@ pub fn eval_string( EvalMode::Markup => { Value::Content(root.cast::<ast::Markup>().unwrap().eval(&mut vm)?) } - EvalMode::Math => Value::Content((vm.items.equation)( - root.cast::<ast::Math>().unwrap().eval(&mut vm)?, - false, - )), + EvalMode::Math => Value::Content( + EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?) + .with_block(false) + .pack(), + ), }; // Handle control flow. @@ -233,1862 +166,11 @@ pub enum EvalMode { Math, } -/// A virtual machine. -/// -/// Holds the state needed to [evaluate](eval) Typst sources. A new -/// virtual machine is created for each module evaluation and function call. -pub struct Vm<'a> { - /// The underlying virtual typesetter. - pub vt: Vt<'a>, - /// The language items. - items: LangItems, - /// The route of source ids the VM took to reach its current location. - route: Tracked<'a, Route<'a>>, - /// The id of the currently evaluated file. - file: Option<FileId>, - /// A control flow event that is currently happening. - flow: Option<FlowEvent>, - /// The stack of scopes. - scopes: Scopes<'a>, - /// The current call depth. - depth: usize, - /// A span that is currently under inspection. - inspected: Option<Span>, -} - -impl<'a> Vm<'a> { - /// Create a new virtual machine. - pub fn new( - vt: Vt<'a>, - route: Tracked<'a, Route>, - file: Option<FileId>, - scopes: Scopes<'a>, - ) -> Self { - let inspected = file.and_then(|id| vt.tracer.inspected(id)); - let items = vt.world.library().items.clone(); - Self { - vt, - items, - route, - file, - flow: None, - scopes, - depth: 0, - inspected, - } - } - - /// Access the underlying world. - pub fn world(&self) -> Tracked<'a, dyn World + 'a> { - self.vt.world - } - - /// The id of the currently evaluated file. - /// - /// Returns `None` if the VM is in a detached context, e.g. when evaluating - /// a user-provided string. - pub fn file(&self) -> Option<FileId> { - self.file - } - - /// Resolve a path relative to the currently evaluated file. - pub fn resolve_path(&self, path: &str) -> StrResult<FileId> { - let Some(file) = self.file else { - bail!("cannot access file system from here"); - }; - - Ok(file.join(path)) - } - - /// Define a variable in the current scope. - #[tracing::instrument(skip_all)] - pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { - let value = value.into_value(); - if self.inspected == Some(var.span()) { - self.vt.tracer.value(value.clone()); - } - self.scopes.top.define(var.get().clone(), value); - } -} - -/// A control flow event that occurred during evaluation. -#[derive(Debug, Clone, PartialEq)] -pub enum FlowEvent { - /// Stop iteration in a loop. - Break(Span), - /// Skip the remainder of the current iteration in a loop. - Continue(Span), - /// Stop execution of a function early, optionally returning an explicit - /// value. - Return(Span, Option<Value>), -} - -impl FlowEvent { - /// Return an error stating that this control flow is forbidden. - pub fn forbidden(&self) -> SourceDiagnostic { - match *self { - Self::Break(span) => { - error!(span, "cannot break outside of loop") - } - Self::Continue(span) => { - error!(span, "cannot continue outside of loop") - } - Self::Return(span, _) => { - error!(span, "cannot return outside of function") - } - } - } -} - -/// A route of source ids. -#[derive(Default)] -pub struct Route<'a> { - // We need to override the constraint's lifetime here so that `Tracked` is - // covariant over the constraint. If it becomes invariant, we're in for a - // world of lifetime pain. - outer: Option<Tracked<'a, Self, <Route<'static> as Validate>::Constraint>>, - id: Option<FileId>, -} - -impl<'a> Route<'a> { - /// Create a new route with just one entry. - pub fn new(id: Option<FileId>) -> Self { - Self { id, outer: None } - } - - /// Insert a new id into the route. - /// - /// You must guarantee that `outer` lives longer than the resulting - /// route is ever used. - pub fn insert(outer: Tracked<'a, Self>, id: FileId) -> Self { - Route { outer: Some(outer), id: Some(id) } - } - - /// Start tracking this locator. - /// - /// In comparison to [`Track::track`], this method skips this chain link - /// if it does not contribute anything. - pub fn track(&self) -> Tracked<'_, Self> { - match self.outer { - Some(outer) if self.id.is_none() => outer, - _ => Track::track(self), - } - } -} - -#[comemo::track] -impl<'a> Route<'a> { - /// Whether the given id is part of the route. - fn contains(&self, id: FileId) -> bool { - self.id == Some(id) || self.outer.map_or(false, |outer| outer.contains(id)) - } -} - /// Evaluate an expression. -pub(super) trait Eval { +pub trait Eval { /// The output of evaluating the expression. type Output; /// Evaluate the expression to the output value. fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output>; } - -impl Eval for ast::Markup<'_> { - type Output = Content; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - eval_markup(vm, &mut self.exprs()) - } -} - -/// Evaluate a stream of markup. -fn eval_markup<'a>( - vm: &mut Vm, - exprs: &mut impl Iterator<Item = ast::Expr<'a>>, -) -> SourceResult<Content> { - let flow = vm.flow.take(); - let mut seq = Vec::with_capacity(exprs.size_hint().1.unwrap_or_default()); - - while let Some(expr) = exprs.next() { - match expr { - ast::Expr::Set(set) => { - let styles = set.eval(vm)?; - if vm.flow.is_some() { - break; - } - - seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) - } - ast::Expr::Show(show) => { - let recipe = show.eval(vm)?; - if vm.flow.is_some() { - break; - } - - let tail = eval_markup(vm, exprs)?; - seq.push(tail.styled_with_recipe(vm, recipe)?) - } - expr => match expr.eval(vm)? { - Value::Label(label) => { - if let Some(elem) = - seq.iter_mut().rev().find(|node| !node.can::<dyn Unlabellable>()) - { - *elem = mem::take(elem).labelled(label); - } - } - value => seq.push(value.display().spanned(expr.span())), - }, - } - - if vm.flow.is_some() { - break; - } - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(Content::sequence(seq)) -} - -impl Eval for ast::Expr<'_> { - type Output = Value; - - #[tracing::instrument(name = "Expr::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let span = self.span(); - let forbidden = |name| { - error!(span, "{} is only allowed directly in code and content blocks", name) - }; - - let v = match self { - Self::Text(v) => v.eval(vm).map(Value::Content), - Self::Space(v) => v.eval(vm).map(Value::Content), - Self::Linebreak(v) => v.eval(vm).map(Value::Content), - Self::Parbreak(v) => v.eval(vm).map(Value::Content), - Self::Escape(v) => v.eval(vm), - Self::Shorthand(v) => v.eval(vm), - Self::SmartQuote(v) => v.eval(vm).map(Value::Content), - Self::Strong(v) => v.eval(vm).map(Value::Content), - Self::Emph(v) => v.eval(vm).map(Value::Content), - Self::Raw(v) => v.eval(vm).map(Value::Content), - Self::Link(v) => v.eval(vm).map(Value::Content), - Self::Label(v) => v.eval(vm), - Self::Ref(v) => v.eval(vm).map(Value::Content), - Self::Heading(v) => v.eval(vm).map(Value::Content), - Self::List(v) => v.eval(vm).map(Value::Content), - Self::Enum(v) => v.eval(vm).map(Value::Content), - Self::Term(v) => v.eval(vm).map(Value::Content), - Self::Equation(v) => v.eval(vm).map(Value::Content), - Self::Math(v) => v.eval(vm).map(Value::Content), - Self::MathIdent(v) => v.eval(vm), - Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), - Self::MathDelimited(v) => v.eval(vm).map(Value::Content), - Self::MathAttach(v) => v.eval(vm).map(Value::Content), - Self::MathPrimes(v) => v.eval(vm).map(Value::Content), - Self::MathFrac(v) => v.eval(vm).map(Value::Content), - Self::MathRoot(v) => v.eval(vm).map(Value::Content), - Self::Ident(v) => v.eval(vm), - Self::None(v) => v.eval(vm), - Self::Auto(v) => v.eval(vm), - Self::Bool(v) => v.eval(vm), - Self::Int(v) => v.eval(vm), - Self::Float(v) => v.eval(vm), - Self::Numeric(v) => v.eval(vm), - Self::Str(v) => v.eval(vm), - Self::Code(v) => v.eval(vm), - Self::Content(v) => v.eval(vm).map(Value::Content), - Self::Array(v) => v.eval(vm).map(Value::Array), - Self::Dict(v) => v.eval(vm).map(Value::Dict), - Self::Parenthesized(v) => v.eval(vm), - Self::FieldAccess(v) => v.eval(vm), - Self::FuncCall(v) => v.eval(vm), - Self::Closure(v) => v.eval(vm), - Self::Unary(v) => v.eval(vm), - Self::Binary(v) => v.eval(vm), - Self::Let(v) => v.eval(vm), - Self::DestructAssign(v) => v.eval(vm), - Self::Set(_) => bail!(forbidden("set")), - Self::Show(_) => bail!(forbidden("show")), - Self::Conditional(v) => v.eval(vm), - Self::While(v) => v.eval(vm), - Self::For(v) => v.eval(vm), - Self::Import(v) => v.eval(vm), - Self::Include(v) => v.eval(vm).map(Value::Content), - Self::Break(v) => v.eval(vm), - Self::Continue(v) => v.eval(vm), - Self::Return(v) => v.eval(vm), - }? - .spanned(span); - - if vm.inspected == Some(span) { - vm.vt.tracer.value(v.clone()); - } - - Ok(v) - } -} - -trait ExprExt { - fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content>; -} - -impl ExprExt for ast::Expr<'_> { - fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content> { - Ok(self.eval(vm)?.display().spanned(self.span())) - } -} - -impl Eval for ast::Text<'_> { - type Output = Content; - - #[tracing::instrument(name = "Text::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.text)(self.get().clone())) - } -} - -impl Eval for ast::Space<'_> { - type Output = Content; - - #[tracing::instrument(name = "Space::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.space)()) - } -} - -impl Eval for ast::Linebreak<'_> { - type Output = Content; - - #[tracing::instrument(name = "Linebreak::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.linebreak)()) - } -} - -impl Eval for ast::Parbreak<'_> { - type Output = Content; - - #[tracing::instrument(name = "Parbreak::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.parbreak)()) - } -} - -impl Eval for ast::Escape<'_> { - type Output = Value; - - #[tracing::instrument(name = "Escape::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Symbol(Symbol::single(self.get()))) - } -} - -impl Eval for ast::Shorthand<'_> { - type Output = Value; - - #[tracing::instrument(name = "Shorthand::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Symbol(Symbol::single(self.get()))) - } -} - -impl Eval for ast::SmartQuote<'_> { - type Output = Content; - - #[tracing::instrument(name = "SmartQuote::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.smart_quote)(self.double())) - } -} - -impl Eval for ast::Strong<'_> { - type Output = Content; - - #[tracing::instrument(name = "Strong::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let body = self.body(); - if body.exprs().next().is_none() { - vm.vt - .tracer - .warn(warning!(self.span(), "no text within stars").with_hint( - "using multiple consecutive stars (e.g. **) has no additional effect", - )); - } - - Ok((vm.items.strong)(body.eval(vm)?)) - } -} - -impl Eval for ast::Emph<'_> { - type Output = Content; - - #[tracing::instrument(name = "Emph::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let body = self.body(); - if body.exprs().next().is_none() { - vm.vt - .tracer - .warn(warning!(self.span(), "no text within underscores").with_hint( - "using multiple consecutive underscores (e.g. __) has no additional effect" - )); - } - - Ok((vm.items.emph)(body.eval(vm)?)) - } -} - -impl Eval for ast::Raw<'_> { - type Output = Content; - - #[tracing::instrument(name = "Raw::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let text = self.text(); - let lang = self.lang().map(Into::into); - let block = self.block(); - Ok((vm.items.raw)(text, lang, block)) - } -} - -impl Eval for ast::Link<'_> { - type Output = Content; - - #[tracing::instrument(name = "Link::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.link)(self.get().clone())) - } -} - -impl Eval for ast::Label<'_> { - type Output = Value; - - #[tracing::instrument(name = "Label::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Label(Label::new(self.get()))) - } -} - -impl Eval for ast::Ref<'_> { - type Output = Content; - - #[tracing::instrument(name = "Ref::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let label = Label::new(self.target()); - let supplement = self.supplement().map(|block| block.eval(vm)).transpose()?; - Ok((vm.items.reference)(label, supplement)) - } -} - -impl Eval for ast::Heading<'_> { - type Output = Content; - - #[tracing::instrument(name = "Heading::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let level = self.level(); - let body = self.body().eval(vm)?; - Ok((vm.items.heading)(level, body)) - } -} - -impl Eval for ast::ListItem<'_> { - type Output = Content; - - #[tracing::instrument(name = "ListItem::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.list_item)(self.body().eval(vm)?)) - } -} - -impl Eval for ast::EnumItem<'_> { - type Output = Content; - - #[tracing::instrument(name = "EnumItem::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let number = self.number(); - let body = self.body().eval(vm)?; - Ok((vm.items.enum_item)(number, body)) - } -} - -impl Eval for ast::TermItem<'_> { - type Output = Content; - - #[tracing::instrument(name = "TermItem::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let term = self.term().eval(vm)?; - let description = self.description().eval(vm)?; - Ok((vm.items.term_item)(term, description)) - } -} - -impl Eval for ast::Equation<'_> { - type Output = Content; - - #[tracing::instrument(name = "Equation::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let body = self.body().eval(vm)?; - let block = self.block(); - Ok((vm.items.equation)(body, block)) - } -} - -impl Eval for ast::Math<'_> { - type Output = Content; - - #[tracing::instrument(name = "Math::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok(Content::sequence( - self.exprs() - .map(|expr| expr.eval_display(vm)) - .collect::<SourceResult<Vec<_>>>()?, - )) - } -} - -impl Eval for ast::MathIdent<'_> { - type Output = Value; - - #[tracing::instrument(name = "MathIdent::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - vm.scopes.get_in_math(&self).cloned().at(self.span()) - } -} - -impl Eval for ast::MathAlignPoint<'_> { - type Output = Content; - - #[tracing::instrument(name = "MathAlignPoint::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.math_align_point)()) - } -} - -impl Eval for ast::MathDelimited<'_> { - type Output = Content; - - #[tracing::instrument(name = "MathDelimited::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let open = self.open().eval_display(vm)?; - let body = self.body().eval(vm)?; - let close = self.close().eval_display(vm)?; - Ok((vm.items.math_delimited)(open, body, close)) - } -} - -impl Eval for ast::MathAttach<'_> { - type Output = Content; - - #[tracing::instrument(name = "MathAttach::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let base = self.base().eval_display(vm)?; - - let mut top = self.top().map(|expr| expr.eval_display(vm)).transpose()?; - if top.is_none() { - if let Some(primes) = self.primes() { - top = Some(primes.eval(vm)?); - } - } - - let bottom = self.bottom().map(|expr| expr.eval_display(vm)).transpose()?; - Ok((vm.items.math_attach)(base, top, bottom, None, None, None, None)) - } -} - -impl Eval for ast::MathPrimes<'_> { - type Output = Content; - - #[tracing::instrument(name = "MathPrimes::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - Ok((vm.items.math_primes)(self.count())) - } -} - -impl Eval for ast::MathFrac<'_> { - type Output = Content; - - #[tracing::instrument(name = "MathFrac::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let num = self.num().eval_display(vm)?; - let denom = self.denom().eval_display(vm)?; - Ok((vm.items.math_frac)(num, denom)) - } -} - -impl Eval for ast::MathRoot<'_> { - type Output = Content; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let index = self.index().map(|i| (vm.items.text)(eco_format!("{i}"))); - let radicand = self.radicand().eval_display(vm)?; - Ok((vm.items.math_root)(index, radicand)) - } -} - -impl Eval for ast::Ident<'_> { - type Output = Value; - - #[tracing::instrument(name = "Ident::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - vm.scopes.get(&self).cloned().at(self.span()) - } -} - -impl Eval for ast::None<'_> { - type Output = Value; - - #[tracing::instrument(name = "None::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::None) - } -} - -impl Eval for ast::Auto<'_> { - type Output = Value; - - #[tracing::instrument(name = "Auto::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Auto) - } -} - -impl Eval for ast::Bool<'_> { - type Output = Value; - - #[tracing::instrument(name = "Bool::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Bool(self.get())) - } -} - -impl Eval for ast::Int<'_> { - type Output = Value; - - #[tracing::instrument(name = "Int::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Int(self.get())) - } -} - -impl Eval for ast::Float<'_> { - type Output = Value; - - #[tracing::instrument(name = "Float::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Float(self.get())) - } -} - -impl Eval for ast::Numeric<'_> { - type Output = Value; - - #[tracing::instrument(name = "Numeric::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::numeric(self.get())) - } -} - -impl Eval for ast::Str<'_> { - type Output = Value; - - #[tracing::instrument(name = "Str::eval", skip_all)] - fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { - Ok(Value::Str(self.get().into())) - } -} - -impl Eval for ast::CodeBlock<'_> { - type Output = Value; - - #[tracing::instrument(name = "CodeBlock::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - vm.scopes.enter(); - let output = self.body().eval(vm)?; - vm.scopes.exit(); - Ok(output) - } -} - -impl Eval for ast::Code<'_> { - type Output = Value; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - eval_code(vm, &mut self.exprs()) - } -} - -/// Evaluate a stream of expressions. -fn eval_code<'a>( - vm: &mut Vm, - exprs: &mut impl Iterator<Item = ast::Expr<'a>>, -) -> SourceResult<Value> { - let flow = vm.flow.take(); - let mut output = Value::None; - - while let Some(expr) = exprs.next() { - let span = expr.span(); - let value = match expr { - ast::Expr::Set(set) => { - let styles = set.eval(vm)?; - if vm.flow.is_some() { - break; - } - - let tail = eval_code(vm, exprs)?.display(); - Value::Content(tail.styled_with_map(styles)) - } - ast::Expr::Show(show) => { - let recipe = show.eval(vm)?; - if vm.flow.is_some() { - break; - } - - let tail = eval_code(vm, exprs)?.display(); - Value::Content(tail.styled_with_recipe(vm, recipe)?) - } - _ => expr.eval(vm)?, - }; - - output = ops::join(output, value).at(span)?; - - if vm.flow.is_some() { - break; - } - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(output) -} - -impl Eval for ast::ContentBlock<'_> { - type Output = Content; - - #[tracing::instrument(name = "ContentBlock::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - vm.scopes.enter(); - let content = self.body().eval(vm)?; - vm.scopes.exit(); - Ok(content) - } -} - -impl Eval for ast::Parenthesized<'_> { - type Output = Value; - - #[tracing::instrument(name = "Parenthesized::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - self.expr().eval(vm) - } -} - -impl Eval for ast::Array<'_> { - type Output = Array; - - #[tracing::instrument(skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let items = self.items(); - - let mut vec = EcoVec::with_capacity(items.size_hint().0); - for item in items { - match item { - ast::ArrayItem::Pos(expr) => vec.push(expr.eval(vm)?), - ast::ArrayItem::Spread(expr) => match expr.eval(vm)? { - Value::None => {} - Value::Array(array) => vec.extend(array.into_iter()), - v => bail!(expr.span(), "cannot spread {} into array", v.ty()), - }, - } - } - - Ok(vec.into()) - } -} - -impl Eval for ast::Dict<'_> { - type Output = Dict; - - #[tracing::instrument(skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let mut map = indexmap::IndexMap::new(); - - let mut invalid_keys = eco_vec![]; - - for item in self.items() { - match item { - ast::DictItem::Named(named) => { - map.insert(named.name().get().clone().into(), named.expr().eval(vm)?); - } - ast::DictItem::Keyed(keyed) => { - let raw_key = keyed.key(); - let key = raw_key.eval(vm)?; - let key = key.cast::<Str>().unwrap_or_else(|error| { - let error = SourceDiagnostic::error(raw_key.span(), error); - invalid_keys.push(error); - Str::default() - }); - map.insert(key, keyed.expr().eval(vm)?); - } - ast::DictItem::Spread(expr) => match expr.eval(vm)? { - Value::None => {} - Value::Dict(dict) => map.extend(dict.into_iter()), - v => bail!(expr.span(), "cannot spread {} into dictionary", v.ty()), - }, - } - } - - if !invalid_keys.is_empty() { - return Err(invalid_keys); - } - - Ok(map.into()) - } -} - -impl Eval for ast::Unary<'_> { - type Output = Value; - - #[tracing::instrument(name = "Unary::eval", skip_all)] - 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; - - #[tracing::instrument(name = "Binary::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - match self.op() { - ast::BinOp::Add => apply_binary_expr(self, vm, ops::add), - ast::BinOp::Sub => apply_binary_expr(self, vm, ops::sub), - ast::BinOp::Mul => apply_binary_expr(self, vm, ops::mul), - ast::BinOp::Div => apply_binary_expr(self, vm, ops::div), - ast::BinOp::And => apply_binary_expr(self, vm, ops::and), - ast::BinOp::Or => apply_binary_expr(self, vm, ops::or), - ast::BinOp::Eq => apply_binary_expr(self, vm, ops::eq), - ast::BinOp::Neq => apply_binary_expr(self, vm, ops::neq), - ast::BinOp::Lt => apply_binary_expr(self, vm, ops::lt), - ast::BinOp::Leq => apply_binary_expr(self, vm, ops::leq), - ast::BinOp::Gt => apply_binary_expr(self, vm, ops::gt), - ast::BinOp::Geq => apply_binary_expr(self, vm, ops::geq), - ast::BinOp::In => apply_binary_expr(self, vm, ops::in_), - ast::BinOp::NotIn => apply_binary_expr(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_expr( - binary: ast::Binary, - vm: &mut Vm, - op: fn(Value, Value) -> StrResult<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) -> StrResult<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) -} - -impl Eval for ast::FieldAccess<'_> { - type Output = Value; - - #[tracing::instrument(name = "FieldAccess::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let value = self.target().eval(vm)?; - let field = self.field(); - value.field(&field).at(field.span()) - } -} - -impl Eval for ast::FuncCall<'_> { - type Output = Value; - - #[tracing::instrument(name = "FuncCall::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let span = self.span(); - if vm.depth >= MAX_CALL_DEPTH { - bail!(span, "maximum function call depth exceeded"); - } - - let callee = self.callee(); - let in_math = in_math(callee); - let callee_span = callee.span(); - let args = self.args(); - - // Try to evaluate as a call to an associated function or field. - let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { - let target = access.target(); - let target_span = target.span(); - let field = access.field(); - let field_span = field.span(); - - let target = if methods::is_mutating(&field) { - let mut args = args.eval(vm)?; - let target = target.access(vm)?; - - // Only arrays and dictionaries have mutable methods. - if matches!(target, Value::Array(_) | Value::Dict(_)) { - args.span = span; - let point = || Tracepoint::Call(Some(field.get().clone())); - return methods::call_mut(target, &field, args, span).trace( - vm.world(), - point, - span, - ); - } - - target.clone() - } else { - access.target().eval(vm)? - }; - - let mut args = args.eval(vm)?; - - // Handle plugins. - if let Value::Plugin(plugin) = &target { - let bytes = args.all::<Bytes>()?; - args.finish()?; - return Ok(plugin.call(&field, bytes).at(span)?.into_value()); - } - - // Prioritize associated functions on the value's type (i.e., - // methods) over its fields. A function call on a field is only - // allowed for functions, types, modules (because they are scopes), - // and symbols (because they have modifiers). - // - // For dictionaries, it is not allowed because it would be ambiguous - // (prioritizing associated functions would make an addition of a - // new associated function a breaking change and prioritizing fields - // would break associated functions for certain dictionaries). - if let Some(callee) = target.ty().scope().get(&field) { - let this = Arg { - span: target_span, - name: None, - value: Spanned::new(target, target_span), - }; - args.span = span; - args.items.insert(0, this); - (callee.clone(), args) - } else if matches!( - target, - Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) - ) { - (target.field(&field).at(field_span)?, args) - } else { - let mut error = error!( - field_span, - "type {} has no method `{}`", - target.ty(), - field.as_str() - ); - - if let Value::Dict(dict) = target { - if matches!(dict.get(&field), Ok(Value::Func(_))) { - error.hint( - "to call the function stored in the dictionary, \ - surround the field access with parentheses", - ); - } - } - - bail!(error); - } - } else { - (callee.eval(vm)?, args.eval(vm)?) - }; - - // Handle math special cases for non-functions: - // Combining accent symbols apply themselves while everything else - // simply displays the arguments verbatim. - if in_math && !matches!(callee, Value::Func(_)) { - if let Value::Symbol(sym) = &callee { - let c = sym.get(); - if let Some(accent) = Symbol::combining_accent(c) { - let base = args.expect("base")?; - args.finish()?; - return Ok(Value::Content((vm.items.math_accent)(base, accent))); - } - } - let mut body = Content::empty(); - for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { - if i > 0 { - body += (vm.items.text)(','.into()); - } - body += arg; - } - return Ok(Value::Content( - callee.display().spanned(callee_span) - + (vm.items.math_delimited)( - (vm.items.text)('('.into()), - body, - (vm.items.text)(')'.into()), - ), - )); - } - - let callee = callee.cast::<Func>().at(callee_span)?; - let point = || Tracepoint::Call(callee.name().map(Into::into)); - let f = || callee.call_vm(vm, args).trace(vm.world(), point, span); - - // Stacker is broken on WASM. - #[cfg(target_arch = "wasm32")] - return f(); - - #[cfg(not(target_arch = "wasm32"))] - stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, f) - } -} - -fn in_math(expr: ast::Expr) -> bool { - match expr { - ast::Expr::MathIdent(_) => true, - ast::Expr::FieldAccess(access) => in_math(access.target()), - _ => false, - } -} - -impl Eval for ast::Args<'_> { - type Output = Args; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let mut items = EcoVec::with_capacity(self.items().count()); - - for arg in self.items() { - let span = arg.span(); - match arg { - ast::Arg::Pos(expr) => { - items.push(Arg { - span, - name: None, - value: Spanned::new(expr.eval(vm)?, expr.span()), - }); - } - ast::Arg::Named(named) => { - items.push(Arg { - span, - name: Some(named.name().get().clone().into()), - value: Spanned::new(named.expr().eval(vm)?, named.expr().span()), - }); - } - ast::Arg::Spread(expr) => match expr.eval(vm)? { - Value::None => {} - Value::Array(array) => { - items.extend(array.into_iter().map(|value| Arg { - span, - name: None, - value: Spanned::new(value, span), - })); - } - Value::Dict(dict) => { - items.extend(dict.into_iter().map(|(key, value)| Arg { - span, - name: Some(key), - value: Spanned::new(value, span), - })); - } - Value::Args(args) => items.extend(args.items), - v => bail!(expr.span(), "cannot spread {}", v.ty()), - }, - } - } - - Ok(Args { span: self.span(), items }) - } -} - -impl Eval for ast::Closure<'_> { - type Output = Value; - - #[tracing::instrument(name = "Closure::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - // Evaluate default values of named parameters. - let mut defaults = Vec::new(); - for param in self.params().children() { - if let ast::Param::Named(named) = param { - defaults.push(named.expr().eval(vm)?); - } - } - - // Collect captured variables. - let captured = { - let mut visitor = CapturesVisitor::new(Some(&vm.scopes)); - visitor.visit(self.to_untyped()); - visitor.finish() - }; - - // Define the closure. - let closure = Closure { - node: self.to_untyped().clone(), - file: vm.file, - defaults, - captured, - }; - - Ok(Value::Func(Func::from(closure).spanned(self.params().span()))) - } -} - -/// Destruct the value into the pattern by binding. -fn define_pattern(vm: &mut Vm, pattern: ast::Pattern, value: Value) -> SourceResult<()> { - destructure(vm, pattern, value, |vm, expr, value| match expr { - ast::Expr::Ident(ident) => { - vm.define(ident, value); - Ok(()) - } - _ => bail!(expr.span(), "nested patterns are currently not supported"), - }) -} - -/// Destruct the value into the pattern by assignment. -fn assign_pattern(vm: &mut Vm, pattern: ast::Pattern, value: Value) -> SourceResult<()> { - destructure(vm, pattern, value, |vm, expr, value| { - let location = expr.access(vm)?; - *location = value; - Ok(()) - }) -} - -/// Destruct the given value into the pattern and apply the function to each binding. -#[tracing::instrument(skip_all)] -fn destructure<T>( - vm: &mut Vm, - pattern: ast::Pattern, - value: Value, - f: T, -) -> SourceResult<()> -where - T: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>, -{ - match pattern { - ast::Pattern::Normal(expr) => { - f(vm, expr, value)?; - } - ast::Pattern::Placeholder(_) => {} - ast::Pattern::Destructuring(destruct) => match value { - Value::Array(value) => destructure_array(vm, pattern, value, f, destruct)?, - Value::Dict(value) => destructure_dict(vm, value, f, destruct)?, - _ => bail!(pattern.span(), "cannot destructure {}", value.ty()), - }, - } - Ok(()) -} - -fn destructure_array<F>( - vm: &mut Vm, - pattern: ast::Pattern, - value: Array, - f: F, - destruct: ast::Destructuring, -) -> SourceResult<()> -where - F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>, -{ - let mut i = 0; - let len = value.as_slice().len(); - for p in destruct.bindings() { - match p { - ast::DestructuringKind::Normal(expr) => { - let Ok(v) = value.at(i as i64, None) else { - bail!(expr.span(), "not enough elements to destructure"); - }; - f(vm, expr, v)?; - i += 1; - } - ast::DestructuringKind::Sink(spread) => { - let sink_size = (1 + len).checked_sub(destruct.bindings().count()); - let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s)); - if let (Some(sink_size), Some(sink)) = (sink_size, sink) { - if let Some(expr) = spread.expr() { - f(vm, expr, Value::Array(sink.into()))?; - } - i += sink_size; - } else { - bail!(pattern.span(), "not enough elements to destructure") - } - } - ast::DestructuringKind::Named(named) => { - bail!(named.span(), "cannot destructure named elements from an array") - } - ast::DestructuringKind::Placeholder(underscore) => { - if i < len { - i += 1 - } else { - bail!(underscore.span(), "not enough elements to destructure") - } - } - } - } - if i < len { - bail!(pattern.span(), "too many elements to destructure"); - } - - Ok(()) -} - -fn destructure_dict<F>( - vm: &mut Vm, - dict: Dict, - f: F, - destruct: ast::Destructuring, -) -> SourceResult<()> -where - F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>, -{ - let mut sink = None; - let mut used = HashSet::new(); - for p in destruct.bindings() { - match p { - ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => { - let v = dict.get(&ident).at(ident.span())?; - f(vm, ast::Expr::Ident(ident), v.clone())?; - used.insert(ident.as_str()); - } - ast::DestructuringKind::Sink(spread) => sink = spread.expr(), - ast::DestructuringKind::Named(named) => { - let name = named.name(); - let v = dict.get(&name).at(name.span())?; - f(vm, named.expr(), v.clone())?; - used.insert(name.as_str()); - } - ast::DestructuringKind::Placeholder(_) => {} - ast::DestructuringKind::Normal(expr) => { - bail!(expr.span(), "expected key, found expression"); - } - } - } - - if let Some(expr) = sink { - let mut sink = Dict::new(); - for (key, value) in dict { - if !used.contains(key.as_str()) { - sink.insert(key, value); - } - } - f(vm, expr, Value::Dict(sink))?; - } - - Ok(()) -} - -impl Eval for ast::LetBinding<'_> { - type Output = Value; - - #[tracing::instrument(name = "LetBinding::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let value = match self.init() { - Some(expr) => expr.eval(vm)?, - None => Value::None, - }; - if vm.flow.is_some() { - return Ok(Value::None); - } - - match self.kind() { - ast::LetBindingKind::Normal(pattern) => define_pattern(vm, pattern, value)?, - ast::LetBindingKind::Closure(ident) => { - vm.define(ident, value); - } - } - - Ok(Value::None) - } -} - -impl Eval for ast::DestructAssignment<'_> { - type Output = Value; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let value = self.value().eval(vm)?; - assign_pattern(vm, self.pattern(), value)?; - Ok(Value::None) - } -} - -impl Eval for ast::SetRule<'_> { - type Output = Styles; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - if let Some(condition) = self.condition() { - if !condition.eval(vm)?.cast::<bool>().at(condition.span())? { - return Ok(Styles::new()); - } - } - - let target = self.target(); - let target = target - .eval(vm)? - .cast::<Func>() - .and_then(|func| { - func.element().ok_or_else(|| { - "only element functions can be used in set rules".into() - }) - }) - .at(target.span())?; - let args = self.args().eval(vm)?; - Ok(target.set(vm, args)?.spanned(self.span())) - } -} - -impl Eval for ast::ShowRule<'_> { - type Output = Recipe; - - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let selector = self - .selector() - .map(|sel| sel.eval(vm)?.cast::<ShowableSelector>().at(sel.span())) - .transpose()? - .map(|selector| selector.0); - - let transform = self.transform(); - let span = transform.span(); - - let transform = match transform { - ast::Expr::Set(set) => Transform::Style(set.eval(vm)?), - expr => expr.eval(vm)?.cast::<Transform>().at(span)?, - }; - - Ok(Recipe { span, selector, transform }) - } -} - -impl Eval for ast::Conditional<'_> { - type Output = Value; - - #[tracing::instrument(name = "Conditional::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let condition = self.condition(); - if condition.eval(vm)?.cast::<bool>().at(condition.span())? { - self.if_body().eval(vm) - } else if let Some(else_body) = self.else_body() { - else_body.eval(vm) - } else { - Ok(Value::None) - } - } -} - -impl Eval for ast::WhileLoop<'_> { - type Output = Value; - - #[tracing::instrument(name = "WhileLoop::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let flow = vm.flow.take(); - let mut output = Value::None; - let mut i = 0; - - let condition = self.condition(); - let body = self.body(); - - while condition.eval(vm)?.cast::<bool>().at(condition.span())? { - if i == 0 - && is_invariant(condition.to_untyped()) - && !can_diverge(body.to_untyped()) - { - bail!(condition.span(), "condition is always true"); - } else if i >= MAX_ITERATIONS { - bail!(self.span(), "loop seems to be infinite"); - } - - let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; - - match vm.flow { - Some(FlowEvent::Break(_)) => { - vm.flow = None; - break; - } - Some(FlowEvent::Continue(_)) => vm.flow = None, - Some(FlowEvent::Return(..)) => break, - None => {} - } - - i += 1; - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(output) - } -} - -/// Whether the expression always evaluates to the same value. -fn is_invariant(expr: &SyntaxNode) -> bool { - match expr.cast() { - Some(ast::Expr::Ident(_)) => false, - Some(ast::Expr::MathIdent(_)) => false, - Some(ast::Expr::FieldAccess(access)) => { - is_invariant(access.target().to_untyped()) - } - Some(ast::Expr::FuncCall(call)) => { - is_invariant(call.callee().to_untyped()) - && is_invariant(call.args().to_untyped()) - } - _ => expr.children().all(is_invariant), - } -} - -/// Whether the expression contains a break or return. -fn can_diverge(expr: &SyntaxNode) -> bool { - matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return) - || expr.children().any(can_diverge) -} - -impl Eval for ast::ForLoop<'_> { - type Output = Value; - - #[tracing::instrument(name = "ForLoop::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let flow = vm.flow.take(); - let mut output = Value::None; - - macro_rules! iter { - (for $pat:ident in $iter:expr) => {{ - vm.scopes.enter(); - - #[allow(unused_parens)] - for value in $iter { - define_pattern(vm, $pat, value.into_value())?; - - let body = self.body(); - let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; - - match vm.flow { - Some(FlowEvent::Break(_)) => { - vm.flow = None; - break; - } - Some(FlowEvent::Continue(_)) => vm.flow = None, - Some(FlowEvent::Return(..)) => break, - None => {} - } - } - - vm.scopes.exit(); - }}; - } - - let iter = self.iter().eval(vm)?; - let pattern = self.pattern(); - - match (&pattern, iter.clone()) { - (ast::Pattern::Normal(_), Value::Str(string)) => { - // Iterate over graphemes of string. - iter!(for pattern in string.as_str().graphemes(true)); - } - (_, Value::Dict(dict)) => { - // Iterate over pairs of dict. - iter!(for pattern in dict.pairs()); - } - (_, Value::Array(array)) => { - // Iterate over values of array. - iter!(for pattern in array); - } - (ast::Pattern::Normal(_), _) => { - bail!(self.iter().span(), "cannot loop over {}", iter.ty()); - } - (_, _) => { - bail!(pattern.span(), "cannot destructure values of {}", iter.ty()) - } - } - - if flow.is_some() { - vm.flow = flow; - } - - Ok(output) - } -} - -impl Eval for ast::ModuleImport<'_> { - type Output = Value; - - #[tracing::instrument(name = "ModuleImport::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let source = self.source(); - let source_span = source.span(); - let mut source = source.eval(vm)?; - let new_name = self.new_name(); - let imports = self.imports(); - - match &source { - Value::Func(func) => { - if func.scope().is_none() { - bail!(source_span, "cannot import from user-defined functions"); - } - } - Value::Type(_) => {} - other => { - source = Value::Module(import(vm, other.clone(), source_span, true)?); - } - } - - if let Some(new_name) = &new_name { - if let ast::Expr::Ident(ident) = self.source() { - if ident.as_str() == new_name.as_str() { - // Warn on `import x as x` - vm.vt.tracer.warn(warning!( - new_name.span(), - "unnecessary import rename to same name", - )); - } - } - - // Define renamed module on the scope. - vm.scopes.top.define(new_name.as_str(), source.clone()); - } - - let scope = source.scope().unwrap(); - match imports { - None => { - // Only import here if there is no rename. - if new_name.is_none() { - let name: EcoString = source.name().unwrap().into(); - vm.scopes.top.define(name, source); - } - } - Some(ast::Imports::Wildcard) => { - for (var, value) in scope.iter() { - vm.scopes.top.define(var.clone(), value.clone()); - } - } - Some(ast::Imports::Items(items)) => { - let mut errors = eco_vec![]; - for item in items.iter() { - let original_ident = item.original_name(); - if let Some(value) = scope.get(&original_ident) { - // Warn on `import ...: x as x` - if let ast::ImportItem::Renamed(renamed_item) = &item { - if renamed_item.original_name().as_str() - == renamed_item.new_name().as_str() - { - vm.vt.tracer.warn(warning!( - renamed_item.new_name().span(), - "unnecessary import rename to same name", - )); - } - } - - vm.define(item.bound_name(), value.clone()); - } else { - errors.push(error!(original_ident.span(), "unresolved import")); - } - } - if !errors.is_empty() { - return Err(errors); - } - } - } - - Ok(Value::None) - } -} - -impl Eval for ast::ModuleInclude<'_> { - type Output = Content; - - #[tracing::instrument(name = "ModuleInclude::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let span = self.source().span(); - let source = self.source().eval(vm)?; - let module = import(vm, source, span, false)?; - Ok(module.content()) - } -} - -/// Process an import of a module relative to the current location. -pub fn import( - vm: &mut Vm, - source: Value, - span: Span, - allow_scopes: bool, -) -> SourceResult<Module> { - let path = match source { - Value::Str(path) => path, - Value::Module(module) => return Ok(module), - v if allow_scopes => { - bail!(span, "expected path, module, function, or type, found {}", v.ty()) - } - v => bail!(span, "expected path or module, found {}", v.ty()), - }; - - // Handle package and file imports. - let path = path.as_str(); - if path.starts_with('@') { - let spec = path.parse::<PackageSpec>().at(span)?; - import_package(vm, spec, span) - } else { - import_file(vm, path, span) - } -} - -/// Import an external package. -fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> { - // Evaluate the manifest. - let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); - let bytes = vm.world().file(manifest_id).at(span)?; - let manifest = PackageManifest::parse(&bytes).at(span)?; - manifest.validate(&spec).at(span)?; - - // Evaluate the entry point. - let entrypoint_id = manifest_id.join(&manifest.package.entrypoint); - let source = vm.world().source(entrypoint_id).at(span)?; - let point = || Tracepoint::Import; - Ok(eval(vm.world(), vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) - .trace(vm.world(), point, span)? - .with_name(manifest.package.name)) -} - -/// Import a file from a path. -fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> { - // Load the source file. - let world = vm.world(); - let id = vm.resolve_path(path).at(span)?; - let source = world.source(id).at(span)?; - - // Prevent cyclic importing. - if vm.route.contains(source.id()) { - bail!(span, "cyclic import"); - } - - // Evaluate the file. - let point = || Tracepoint::Import; - eval(world, vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source) - .trace(world, point, span) -} - -/// A parsed package manifest. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -struct PackageManifest { - /// Details about the package itself. - package: PackageInfo, -} - -impl PackageManifest { - /// Parse the manifest from raw bytes. - fn parse(bytes: &[u8]) -> StrResult<Self> { - let string = std::str::from_utf8(bytes).map_err(FileError::from)?; - toml::from_str(string).map_err(|err| { - eco_format!("package manifest is malformed: {}", err.message()) - }) - } - - /// Ensure that this manifest is indeed for the specified package. - fn validate(&self, spec: &PackageSpec) -> StrResult<()> { - if self.package.name != spec.name { - bail!("package manifest contains mismatched name `{}`", self.package.name); - } - - if self.package.version != spec.version { - bail!( - "package manifest contains mismatched version {}", - self.package.version - ); - } - - if let Some(compiler) = self.package.compiler { - let current = PackageVersion::compiler(); - if current < compiler { - bail!( - "package requires typst {compiler} or newer \ - (current version is {current})" - ); - } - } - - Ok(()) - } -} - -/// The `package` key in the manifest. -/// -/// More fields are specified, but they are not relevant to the compiler. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -struct PackageInfo { - /// The name of the package within its namespace. - name: EcoString, - /// The package's version. - version: PackageVersion, - /// The path of the entrypoint into the package. - entrypoint: EcoString, - /// The minimum required compiler version for the package. - compiler: Option<PackageVersion>, -} - -impl Eval for ast::LoopBreak<'_> { - type Output = Value; - - #[tracing::instrument(name = "LoopBreak::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - if vm.flow.is_none() { - vm.flow = Some(FlowEvent::Break(self.span())); - } - Ok(Value::None) - } -} - -impl Eval for ast::LoopContinue<'_> { - type Output = Value; - - #[tracing::instrument(name = "LoopContinue::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - if vm.flow.is_none() { - vm.flow = Some(FlowEvent::Continue(self.span())); - } - Ok(Value::None) - } -} - -impl Eval for ast::FuncReturn<'_> { - type Output = Value; - - #[tracing::instrument(name = "FuncReturn::eval", skip_all)] - fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { - let value = self.body().map(|body| body.eval(vm)).transpose()?; - if vm.flow.is_none() { - vm.flow = Some(FlowEvent::Return(self.span(), value)); - } - Ok(Value::None) - } -} - -/// Access an expression mutably. -trait Access { - /// Access the value. - fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value>; -} - -impl Access for ast::Expr<'_> { - fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - match self { - Self::Ident(v) => v.access(vm), - Self::Parenthesized(v) => v.access(vm), - Self::FieldAccess(v) => v.access(vm), - Self::FuncCall(v) => v.access(vm), - _ => { - let _ = self.eval(vm)?; - bail!(self.span(), "cannot mutate a temporary value"); - } - } - } -} - -impl Access for ast::Ident<'_> { - fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - let span = self.span(); - let value = vm.scopes.get_mut(&self).at(span)?; - if vm.inspected == Some(span) { - vm.vt.tracer.value(value.clone()); - } - Ok(value) - } -} - -impl Access for ast::Parenthesized<'_> { - fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - self.expr().access(vm) - } -} - -impl Access for ast::FieldAccess<'_> { - fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - access_dict(vm, self)?.at_mut(self.field().get()).at(self.span()) - } -} - -fn access_dict<'a>( - vm: &'a mut Vm, - access: ast::FieldAccess, -) -> SourceResult<&'a mut Dict> { - match access.target().access(vm)? { - Value::Dict(dict) => Ok(dict), - value => { - let ty = value.ty(); - let span = access.target().span(); - if matches!( - value, // those types have their own field getters - Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_) - ) { - bail!(span, "cannot mutate fields on {ty}"); - } else if fields::fields_on(ty).is_empty() { - bail!(span, "{ty} does not have accessible fields"); - } else { - // type supports static fields, which don't yet have - // setters - Err(eco_format!("fields on {ty} are not yet mutable")) - .hint(eco_format!( - "try creating a new {ty} with the updated field value instead" - )) - .at(span) - } - } - } -} - -impl Access for ast::FuncCall<'_> { - fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { - if let ast::Expr::FieldAccess(access) = self.callee() { - let method = access.field(); - if methods::is_accessor(&method) { - let span = self.span(); - let world = vm.world(); - let args = self.args().eval(vm)?; - let value = access.target().access(vm)?; - let result = methods::call_access(value, &method, args, span); - let point = || Tracepoint::Call(Some(method.get().clone())); - return result.trace(world, point, span); - } - } - - let _ = self.eval(vm)?; - bail!(self.span(), "cannot mutate a temporary value"); - } -} diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs index 64f6bb0a..8f8e128a 100644 --- a/crates/typst/src/eval/ops.rs +++ b/crates/typst/src/eval/ops.rs @@ -4,10 +4,102 @@ use std::cmp::Ordering; use ecow::eco_format; -use crate::diag::{bail, StrResult}; -use crate::eval::{format_str, item, IntoValue, Regex, Repr, Smart, Value}; -use crate::geom::{Align, Length, Numeric, Rel, Stroke}; -use Value::*; +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::eval::{access_dict, Access, Eval, Vm}; +use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Smart, Value}; +use crate::layout::{Align, Length, Rel}; +use crate::syntax::ast::{self, AstNode}; +use crate::text::TextElem; +use crate::util::Numeric; +use crate::visualize::Stroke; + +impl Eval for ast::Unary<'_> { + type Output = Value; + + #[tracing::instrument(name = "Unary::eval", skip_all)] + 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; + + #[tracing::instrument(name = "Binary::eval", skip_all)] + 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) -> StrResult<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) -> StrResult<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 { @@ -18,6 +110,7 @@ macro_rules! mismatch { /// Join a value with another value. pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { + use Value::*; Ok(match (lhs, rhs) { (a, None) => a, (None, b) => b, @@ -27,10 +120,10 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), - (Content(a), Str(b)) => Content(a + item!(text)(b.into())), - (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), - (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), + (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Str(b)) => Content(a + TextElem::packed(b)), + (Str(a), Content(b)) => Content(TextElem::packed(a) + b), + (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), @@ -44,6 +137,7 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { /// Apply the unary plus operator to a value. pub fn pos(value: Value) -> StrResult<Value> { + use Value::*; Ok(match value { Int(v) => Int(v), Float(v) => Float(v), @@ -68,6 +162,7 @@ pub fn pos(value: Value) -> StrResult<Value> { /// Compute the negation of a value. pub fn neg(value: Value) -> StrResult<Value> { + use Value::*; Ok(match value { Int(v) => Int(v.checked_neg().ok_or_else(too_large)?), Float(v) => Float(-v), @@ -84,6 +179,7 @@ pub fn neg(value: Value) -> StrResult<Value> { /// Compute the sum of two values. pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { + use Value::*; Ok(match (lhs, rhs) { (a, None) => a, (None, b) => b, @@ -115,10 +211,10 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())), - (Content(a), Str(b)) => Content(a + item!(text)(b.into())), - (Str(a), Content(b)) => Content(item!(text)(a.into()) + b), - (Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b), + (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Str(b)) => Content(a + TextElem::packed(b)), + (Str(a), Content(b)) => Content(TextElem::packed(a) + b), + (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), @@ -161,6 +257,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { /// Compute the difference of two values. pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> { + use Value::*; Ok(match (lhs, rhs) { (Int(a), Int(b)) => Int(a.checked_sub(b).ok_or_else(too_large)?), (Int(a), Float(b)) => Float(a as f64 - b), @@ -193,6 +290,7 @@ pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> { /// Compute the product of two values. pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { + use Value::*; Ok(match (lhs, rhs) { (Int(a), Int(b)) => Int(a.checked_mul(b).ok_or_else(too_large)?), (Int(a), Float(b)) => Float(a as f64 * b), @@ -251,6 +349,7 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { /// Compute the quotient of two values. pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> { + use Value::*; if is_zero(&rhs) { bail!("cannot divide by zero"); } @@ -295,6 +394,7 @@ pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> { /// Whether a value is a numeric zero. fn is_zero(v: &Value) -> bool { + use Value::*; match *v { Int(v) => v == 0, Float(v) => v == 0.0, @@ -322,7 +422,7 @@ fn try_div_relative(a: Rel<Length>, b: Rel<Length>) -> StrResult<f64> { /// Compute the logical "not" of a value. pub fn not(value: Value) -> StrResult<Value> { match value { - Bool(b) => Ok(Bool(!b)), + Value::Bool(b) => Ok(Value::Bool(!b)), v => mismatch!("cannot apply 'not' to {}", v), } } @@ -330,7 +430,7 @@ pub fn not(value: Value) -> StrResult<Value> { /// Compute the logical "and" of two values. pub fn and(lhs: Value, rhs: Value) -> StrResult<Value> { match (lhs, rhs) { - (Bool(a), Bool(b)) => Ok(Bool(a && b)), + (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a && b)), (a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b), } } @@ -338,19 +438,19 @@ pub fn and(lhs: Value, rhs: Value) -> StrResult<Value> { /// Compute the logical "or" of two values. pub fn or(lhs: Value, rhs: Value) -> StrResult<Value> { match (lhs, rhs) { - (Bool(a), Bool(b)) => Ok(Bool(a || b)), + (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a || b)), (a, b) => mismatch!("cannot apply 'or' to {} and {}", a, b), } } /// Compute whether two values are equal. pub fn eq(lhs: Value, rhs: Value) -> StrResult<Value> { - Ok(Bool(equal(&lhs, &rhs))) + Ok(Value::Bool(equal(&lhs, &rhs))) } /// Compute whether two values are unequal. pub fn neq(lhs: Value, rhs: Value) -> StrResult<Value> { - Ok(Bool(!equal(&lhs, &rhs))) + Ok(Value::Bool(!equal(&lhs, &rhs))) } macro_rules! comparison { @@ -358,7 +458,7 @@ macro_rules! comparison { /// Compute how a value compares with another value. pub fn $name(lhs: Value, rhs: Value) -> StrResult<Value> { let ordering = compare(&lhs, &rhs)?; - Ok(Bool(matches!(ordering, $($pat)*))) + Ok(Value::Bool(matches!(ordering, $($pat)*))) } }; } @@ -370,6 +470,7 @@ comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); /// Determine whether two values are equal. pub fn equal(lhs: &Value, rhs: &Value) -> bool { + use Value::*; match (lhs, rhs) { // Compare reflexively. (None, None) => true, @@ -418,6 +519,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { /// Compare two values. pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> { + use Value::*; Ok(match (lhs, rhs) { (Bool(a), Bool(b)) => a.cmp(b), (Int(a), Int(b)) => a.cmp(b), @@ -452,7 +554,7 @@ fn try_cmp_values<T: PartialOrd + Repr>(a: &T, b: &T) -> StrResult<Ordering> { } /// Try to compare two datetimes. -fn try_cmp_datetimes(a: &super::Datetime, b: &super::Datetime) -> StrResult<Ordering> { +fn try_cmp_datetimes(a: &Datetime, b: &Datetime) -> StrResult<Ordering> { a.partial_cmp(b) .ok_or_else(|| eco_format!("cannot compare {} and {}", a.kind(), b.kind())) } @@ -460,7 +562,7 @@ fn try_cmp_datetimes(a: &super::Datetime, b: &super::Datetime) -> StrResult<Orde /// Test whether one value is "in" another one. pub fn in_(lhs: Value, rhs: Value) -> StrResult<Value> { if let Some(b) = contains(&lhs, &rhs) { - Ok(Bool(b)) + Ok(Value::Bool(b)) } else { mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) } @@ -469,7 +571,7 @@ pub fn in_(lhs: Value, rhs: Value) -> StrResult<Value> { /// Test whether one value is "not in" another one. pub fn not_in(lhs: Value, rhs: Value) -> StrResult<Value> { if let Some(b) = contains(&lhs, &rhs) { - Ok(Bool(!b)) + Ok(Value::Bool(!b)) } else { mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) } @@ -477,6 +579,7 @@ pub fn not_in(lhs: Value, rhs: Value) -> StrResult<Value> { /// Test for containment. pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> { + use Value::*; match (lhs, rhs) { (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), diff --git a/crates/typst/src/eval/rules.rs b/crates/typst/src/eval/rules.rs new file mode 100644 index 00000000..c85b747b --- /dev/null +++ b/crates/typst/src/eval/rules.rs @@ -0,0 +1,51 @@ +use crate::diag::{At, SourceResult}; +use crate::eval::{Eval, Vm}; +use crate::foundations::{Func, Recipe, ShowableSelector, Styles, Transformation}; +use crate::syntax::ast::{self, AstNode}; + +impl Eval for ast::SetRule<'_> { + type Output = Styles; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + if let Some(condition) = self.condition() { + if !condition.eval(vm)?.cast::<bool>().at(condition.span())? { + return Ok(Styles::new()); + } + } + + let target = self.target(); + let target = target + .eval(vm)? + .cast::<Func>() + .and_then(|func| { + func.element().ok_or_else(|| { + "only element functions can be used in set rules".into() + }) + }) + .at(target.span())?; + let args = self.args().eval(vm)?; + Ok(target.set(vm, args)?.spanned(self.span())) + } +} + +impl Eval for ast::ShowRule<'_> { + type Output = Recipe; + + fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { + let selector = self + .selector() + .map(|sel| sel.eval(vm)?.cast::<ShowableSelector>().at(sel.span())) + .transpose()? + .map(|selector| selector.0); + + let transform = self.transform(); + let span = transform.span(); + + let transform = match transform { + ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?), + expr => expr.eval(vm)?.cast::<Transformation>().at(span)?, + }; + + Ok(Recipe { span, selector, transform }) + } +} diff --git a/crates/typst/src/eval/tracer.rs b/crates/typst/src/eval/tracer.rs index 46784036..af53a945 100644 --- a/crates/typst/src/eval/tracer.rs +++ b/crates/typst/src/eval/tracer.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use ecow::EcoVec; use crate::diag::SourceDiagnostic; -use crate::eval::Value; +use crate::foundations::Value; use crate::syntax::{FileId, Span}; use crate::util::hash128; @@ -44,7 +44,7 @@ impl Tracer { #[comemo::track] impl Tracer { - /// The inspeted span if it is part of the given source file. + /// The inspected span if it is part of the given source file. pub fn inspected(&self, id: FileId) -> Option<Span> { if self.inspected.and_then(Span::id) == Some(id) { self.inspected diff --git a/crates/typst/src/eval/vm.rs b/crates/typst/src/eval/vm.rs new file mode 100644 index 00000000..c34c1070 --- /dev/null +++ b/crates/typst/src/eval/vm.rs @@ -0,0 +1,127 @@ +use comemo::{Track, Tracked, Validate}; + +use crate::diag::{bail, StrResult}; +use crate::eval::FlowEvent; +use crate::foundations::{IntoValue, Scopes}; +use crate::layout::Vt; +use crate::syntax::ast::{self, AstNode}; +use crate::syntax::{FileId, Span}; +use crate::World; + +/// A virtual machine. +/// +/// Holds the state needed to [evaluate](crate::eval::eval()) Typst sources. A new +/// virtual machine is created for each module evaluation and function call. +pub struct Vm<'a> { + /// The underlying virtual typesetter. + pub(crate) vt: Vt<'a>, + /// The route of source ids the VM took to reach its current location. + pub(crate) route: Tracked<'a, Route<'a>>, + /// The id of the currently evaluated file. + pub(crate) file: Option<FileId>, + /// A control flow event that is currently happening. + pub(crate) flow: Option<FlowEvent>, + /// The stack of scopes. + pub(crate) scopes: Scopes<'a>, + /// The current call depth. + pub(crate) depth: usize, + /// A span that is currently under inspection. + pub(crate) inspected: Option<Span>, +} + +impl<'a> Vm<'a> { + /// Create a new virtual machine. + pub fn new( + vt: Vt<'a>, + route: Tracked<'a, Route>, + file: Option<FileId>, + scopes: Scopes<'a>, + ) -> Self { + let inspected = file.and_then(|id| vt.tracer.inspected(id)); + Self { + vt, + route, + file, + flow: None, + scopes, + depth: 0, + inspected, + } + } + + /// Access the underlying world. + pub fn world(&self) -> Tracked<'a, dyn World + 'a> { + self.vt.world + } + + /// The id of the currently evaluated file. + /// + /// Returns `None` if the VM is in a detached context, e.g. when evaluating + /// a user-provided string. + pub fn file(&self) -> Option<FileId> { + self.file + } + + /// Resolve a path relative to the currently evaluated file. + pub fn resolve_path(&self, path: &str) -> StrResult<FileId> { + let Some(file) = self.file else { + bail!("cannot access file system from here"); + }; + + Ok(file.join(path)) + } + + /// Define a variable in the current scope. + #[tracing::instrument(skip_all)] + pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { + let value = value.into_value(); + if self.inspected == Some(var.span()) { + self.vt.tracer.value(value.clone()); + } + self.scopes.top.define(var.get().clone(), value); + } +} + +/// A route of source ids. +#[derive(Default)] +pub struct Route<'a> { + // We need to override the constraint's lifetime here so that `Tracked` is + // covariant over the constraint. If it becomes invariant, we're in for a + // world of lifetime pain. + outer: Option<Tracked<'a, Self, <Route<'static> as Validate>::Constraint>>, + id: Option<FileId>, +} + +impl<'a> Route<'a> { + /// Create a new route with just one entry. + pub fn new(id: Option<FileId>) -> Self { + Self { id, outer: None } + } + + /// Insert a new id into the route. + /// + /// You must guarantee that `outer` lives longer than the resulting + /// route is ever used. + pub fn insert(outer: Tracked<'a, Self>, id: FileId) -> Self { + Route { outer: Some(outer), id: Some(id) } + } + + /// Start tracking this locator. + /// + /// In comparison to [`Track::track`], this method skips this chain link + /// if it does not contribute anything. + pub fn track(&self) -> Tracked<'_, Self> { + match self.outer { + Some(outer) if self.id.is_none() => outer, + _ => Track::track(self), + } + } +} + +#[comemo::track] +impl<'a> Route<'a> { + /// Whether the given id is part of the route. + pub fn contains(&self, id: FileId) -> bool { + self.id == Some(id) || self.outer.map_or(false, |outer| outer.contains(id)) + } +} diff --git a/crates/typst/src/eval/args.rs b/crates/typst/src/foundations/args.rs index 1cb175e3..af5d07b1 100644 --- a/crates/typst/src/eval/args.rs +++ b/crates/typst/src/foundations/args.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use crate::diag::{bail, At, SourceDiagnostic, SourceResult}; -use crate::eval::{ +use crate::foundations::{ func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value, }; use crate::syntax::{Span, Spanned}; diff --git a/crates/typst/src/eval/array.rs b/crates/typst/src/foundations/array.rs index 29e99766..47afa9e2 100644 --- a/crates/typst/src/eval/array.rs +++ b/crates/typst/src/foundations/array.rs @@ -8,9 +8,10 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use crate::diag::{At, SourceResult, StrResult}; -use crate::eval::{ - cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue, - Reflect, Repr, Value, Version, Vm, +use crate::eval::{ops, Vm}; +use crate::foundations::{ + cast, func, repr, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue, + Reflect, Repr, Value, Version, }; use crate::syntax::Span; @@ -19,15 +20,15 @@ use crate::syntax::Span; #[doc(hidden)] macro_rules! __array { ($value:expr; $count:expr) => { - $crate::eval::Array::from($crate::eval::eco_vec![ - $crate::eval::IntoValue::into_value($value); + $crate::foundations::Array::from($crate::foundations::eco_vec![ + $crate::foundations::IntoValue::into_value($value); $count ]) }; ($($value:expr),* $(,)?) => { - $crate::eval::Array::from($crate::eval::eco_vec![$( - $crate::eval::IntoValue::into_value($value) + $crate::foundations::Array::from($crate::foundations::eco_vec![$( + $crate::foundations::IntoValue::into_value($value) ),*]) }; } @@ -35,9 +36,6 @@ macro_rules! __array { #[doc(inline)] pub use crate::__array as array; -#[doc(hidden)] -pub use ecow::eco_vec; - /// A sequence of values. /// /// You can construct an array by enclosing a comma-separated sequence of values @@ -736,7 +734,7 @@ impl Array { vec.make_mut().sort_by(|a, b| { // Until we get `try` blocks :) match (key_of(a.clone()), key_of(b.clone())) { - (Ok(a), Ok(b)) => super::ops::compare(&a, &b).unwrap_or_else(|err| { + (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { if result.is_ok() { result = Err(err).at(span); } @@ -790,7 +788,7 @@ impl Array { } for second in out.iter() { - if super::ops::equal(&key, &key_of(second.clone())?) { + if ops::equal(&key, &key_of(second.clone())?) { continue 'outer; } } diff --git a/crates/typst/src/eval/auto.rs b/crates/typst/src/foundations/auto.rs index 0dcf79c7..dd36e814 100644 --- a/crates/typst/src/eval/auto.rs +++ b/crates/typst/src/foundations/auto.rs @@ -2,8 +2,10 @@ use ecow::EcoString; use std::fmt::{self, Debug, Formatter}; use crate::diag::StrResult; -use crate::eval::{ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value}; -use crate::model::{Fold, Resolve, StyleChain}; +use crate::foundations::{ + ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type, + Value, +}; /// A value that indicates a smart default. /// diff --git a/crates/typst/src/eval/bool.rs b/crates/typst/src/foundations/bool.rs index 1dca260c..4c85a741 100644 --- a/crates/typst/src/eval/bool.rs +++ b/crates/typst/src/foundations/bool.rs @@ -1,6 +1,6 @@ use ecow::EcoString; -use super::{ty, Repr}; +use crate::foundations::{ty, Repr}; /// A type with two states. /// diff --git a/crates/typst/src/eval/bytes.rs b/crates/typst/src/foundations/bytes.rs index 0b074fac..4e894241 100644 --- a/crates/typst/src/eval/bytes.rs +++ b/crates/typst/src/foundations/bytes.rs @@ -8,7 +8,7 @@ use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; use crate::diag::{bail, StrResult}; -use crate::eval::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value}; +use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value}; /// A sequence of bytes. /// diff --git a/crates/typst-library/src/compute/calc.rs b/crates/typst/src/foundations/calc.rs index 6276905c..597bf156 100644 --- a/crates/typst-library/src/compute/calc.rs +++ b/crates/typst/src/foundations/calc.rs @@ -4,20 +4,15 @@ use std::cmp; use std::cmp::Ordering; use std::ops::{Div, Rem}; -use typst::eval::{Module, Scope}; - -use crate::prelude::*; - -/// Hook up all calculation definitions. -pub(super) fn define(global: &mut Scope) { - global.category("calculate"); - global.define_module(module()); -} +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::eval::ops; +use crate::foundations::{cast, func, IntoValue, Module, Scope, Value}; +use crate::layout::{Angle, Fr, Length, Ratio}; +use crate::syntax::{Span, Spanned}; /// A module with calculation definitions. -fn module() -> Module { +pub fn module() -> Module { let mut scope = Scope::new(); - scope.category("calculate"); scope.define_func::<abs>(); scope.define_func::<pow>(); scope.define_func::<exp>(); @@ -747,7 +742,7 @@ fn minmax( }; for Spanned { v, span } in iter { - let ordering = typst::eval::ops::compare(&v, &extremum).at(span)?; + let ordering = ops::compare(&v, &extremum).at(span)?; if ordering == goal { extremum = v; } diff --git a/crates/typst/src/eval/cast.rs b/crates/typst/src/foundations/cast.rs index cbe5149c..fbd5ab14 100644 --- a/crates/typst/src/eval/cast.rs +++ b/crates/typst/src/foundations/cast.rs @@ -1,5 +1,3 @@ -pub use typst_macros::{cast, Cast}; - use std::borrow::Cow; use std::fmt::Write; use std::hash::Hash; @@ -11,9 +9,12 @@ use smallvec::SmallVec; use unicode_math_class::MathClass; use crate::diag::{At, SourceResult, StrResult}; -use crate::eval::{repr, Repr, Type, Value}; +use crate::foundations::{repr, Repr, Type, Value}; use crate::syntax::{Span, Spanned}; +#[doc(inline)] +pub use typst_macros::{cast, Cast}; + /// Determine details of a type. /// /// Type casting works as follows: diff --git a/crates/typst/src/model/content.rs b/crates/typst/src/foundations/content.rs index 48f84c35..7a402629 100644 --- a/crates/typst/src/model/content.rs +++ b/crates/typst/src/foundations/content.rs @@ -7,19 +7,20 @@ use std::sync::Arc; use comemo::Prehashed; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; -use smallvec::SmallVec; -use typst_macros::elem; +use smallvec::smallvec; use crate::diag::{SourceResult, StrResult}; -use crate::doc::Meta; -use crate::eval::{ - func, repr, scope, ty, Dict, FromValue, IntoValue, Repr, Str, Value, Vm, -}; -use crate::model::{ - Behave, Behaviour, Element, Guard, Label, Location, NativeElement, Recipe, Selector, - Style, Styles, +use crate::eval::Vm; +use crate::foundations::{ + elem, func, scope, ty, Dict, Element, FromValue, Guard, IntoValue, Label, + NativeElement, Recipe, Repr, Selector, Str, Style, Styles, Value, }; +use crate::introspection::{Location, Meta, MetaElem}; +use crate::layout::{Align, AlignElem, Axes, Length, MoveElem, PadElem, Rel, Sides}; +use crate::model::{Destination, EmphElem, StrongElem}; use crate::syntax::Span; +use crate::text::UnderlineElem; +use crate::util::fat; /// A piece of document content. /// @@ -457,6 +458,57 @@ impl Content { } } +impl Content { + /// Strongly emphasize this content. + pub fn strong(self) -> Self { + StrongElem::new(self).pack() + } + + /// Emphasize this content. + pub fn emph(self) -> Self { + EmphElem::new(self).pack() + } + + /// Underline this content. + pub fn underlined(self) -> Self { + UnderlineElem::new(self).pack() + } + + /// Link the content somewhere. + pub fn linked(self, dest: Destination) -> Self { + self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)])) + } + + /// Make the content linkable by `.linked(Destination::Location(loc))`. + /// + /// Should be used in combination with [`Location::variant`]. + pub fn backlinked(self, loc: Location) -> Self { + let mut backlink = Content::empty(); + backlink.set_location(loc); + self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)])) + } + + /// Set alignments for this content. + pub fn aligned(self, align: Align) -> Self { + self.styled(AlignElem::set_alignment(align)) + } + + /// Pad this content at the sides. + pub fn padded(self, padding: Sides<Rel<Length>>) -> Self { + PadElem::new(self) + .with_left(padding.left) + .with_top(padding.top) + .with_right(padding.right) + .with_bottom(padding.bottom) + .pack() + } + + /// Transform this content's contents without affecting layout. + pub fn moved(self, delta: Axes<Rel<Length>>) -> Self { + MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack() + } +} + #[scope] impl Content { /// The content's element function. This function can be used to create the element @@ -690,7 +742,7 @@ impl Repr for SequenceElem { } else { eco_format!( "[{}]", - repr::pretty_array_like( + crate::foundations::repr::pretty_array_like( &self.children.iter().map(|c| c.0.repr()).collect::<Vec<_>>(), false ) @@ -720,21 +772,6 @@ impl Repr for StyledElem { } } -/// Hosts metadata and ensures metadata is produced even for empty elements. -#[elem(Behave)] -pub struct MetaElem { - /// Metadata that should be attached to all elements affected by this style - /// property. - #[fold] - pub data: SmallVec<[Meta; 1]>, -} - -impl Behave for MetaElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Invisible - } -} - /// Tries to extract the plain-text representation of the element. pub trait PlainText { /// Write this element's plain text into the given buffer. @@ -743,7 +780,7 @@ pub trait PlainText { /// The missing field access error message. #[cold] -pub fn missing_field(field: &str) -> EcoString { +fn missing_field(field: &str) -> EcoString { eco_format!("content does not contain field {}", field.repr()) } @@ -756,66 +793,3 @@ fn missing_field_no_default(field: &str) -> EcoString { field.repr() ) } - -/// Fat pointer handling. -/// -/// This assumes the memory representation of fat pointers. Although it is not -/// guaranteed by Rust, it's improbable that it will change. Still, when the -/// pointer metadata APIs are stable, we should definitely move to them: -/// <https://github.com/rust-lang/rust/issues/81513> -pub mod fat { - use std::alloc::Layout; - use std::mem; - - /// Create a fat pointer from a data address and a vtable address. - /// - /// # Safety - /// Must only be called when `T` is a `dyn Trait`. The data address must point - /// to a value whose type implements the trait of `T` and the `vtable` must have - /// been extracted with [`vtable`]. - #[track_caller] - pub unsafe fn from_raw_parts<T: ?Sized>( - data: *const (), - vtable: *const (), - ) -> *const T { - let fat = FatPointer { data, vtable }; - debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>()); - mem::transmute_copy::<FatPointer, *const T>(&fat) - } - - /// Create a mutable fat pointer from a data address and a vtable address. - /// - /// # Safety - /// Must only be called when `T` is a `dyn Trait`. The data address must point - /// to a value whose type implements the trait of `T` and the `vtable` must have - /// been extracted with [`vtable`]. - #[track_caller] - pub unsafe fn from_raw_parts_mut<T: ?Sized>( - data: *mut (), - vtable: *const (), - ) -> *mut T { - let fat = FatPointer { data, vtable }; - debug_assert_eq!(Layout::new::<*mut T>(), Layout::new::<FatPointer>()); - mem::transmute_copy::<FatPointer, *mut T>(&fat) - } - - /// Extract the address to a trait object's vtable. - /// - /// # Safety - /// Must only be called when `T` is a `dyn Trait`. - #[track_caller] - pub unsafe fn vtable<T: ?Sized>(ptr: *const T) -> *const () { - debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>()); - mem::transmute_copy::<*const T, FatPointer>(&ptr).vtable - } - - /// The memory representation of a trait object pointer. - /// - /// Although this is not guaranteed by Rust, it's improbable that it will - /// change. - #[repr(C)] - struct FatPointer { - data: *const (), - vtable: *const (), - } -} diff --git a/crates/typst/src/eval/datetime.rs b/crates/typst/src/foundations/datetime.rs index 26117f85..f50daf72 100644 --- a/crates/typst/src/eval/datetime.rs +++ b/crates/typst/src/foundations/datetime.rs @@ -1,6 +1,4 @@ use std::cmp::Ordering; - -use std::fmt::Debug; use std::hash::Hash; use std::ops::{Add, Sub}; @@ -10,8 +8,9 @@ use time::macros::format_description; use time::{format_description, Month, PrimitiveDateTime}; use crate::diag::{bail, StrResult}; -use crate::eval::{ - cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value, Vm, +use crate::eval::Vm; +use crate::foundations::{ + cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value, }; use crate::World; @@ -238,26 +237,26 @@ impl Datetime { pub fn construct( /// The year of the datetime. #[named] - year: Option<YearComponent>, + year: Option<i32>, /// The month of the datetime. #[named] - month: Option<MonthComponent>, + month: Option<Month>, /// The day of the datetime. #[named] - day: Option<DayComponent>, + day: Option<u8>, /// The hour of the datetime. #[named] - hour: Option<HourComponent>, + hour: Option<u8>, /// The minute of the datetime. #[named] - minute: Option<MinuteComponent>, + minute: Option<u8>, /// The second of the datetime. #[named] - second: Option<SecondComponent>, + second: Option<u8>, ) -> StrResult<Datetime> { let time = match (hour, minute, second) { (Some(hour), Some(minute), Some(second)) => { - match time::Time::from_hms(hour.0, minute.0, second.0) { + match time::Time::from_hms(hour, minute, second) { Ok(time) => Some(time), Err(_) => bail!("time is invalid"), } @@ -268,7 +267,7 @@ impl Datetime { let date = match (year, month, day) { (Some(year), Some(month), Some(day)) => { - match time::Date::from_calendar_date(year.0, month.0, day.0) { + match time::Date::from_calendar_date(year, month, day) { Ok(date) => Some(date), Err(_) => bail!("date is invalid"), } @@ -493,43 +492,6 @@ impl Sub for Datetime { } } -pub struct YearComponent(i32); -pub struct MonthComponent(Month); -pub struct DayComponent(u8); -pub struct HourComponent(u8); -pub struct MinuteComponent(u8); -pub struct SecondComponent(u8); - -cast! { - YearComponent, - v: i32 => Self(v), -} - -cast! { - MonthComponent, - v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?) -} - -cast! { - DayComponent, - v: u8 => Self(v), -} - -cast! { - HourComponent, - v: u8 => Self(v), -} - -cast! { - MinuteComponent, - v: u8 => Self(v), -} - -cast! { - SecondComponent, - v: u8 => Self(v), -} - /// A format in which a datetime can be displayed. pub struct DisplayPattern(Str, format_description::OwnedFormatItem); @@ -543,6 +505,11 @@ cast! { } } +cast! { + Month, + v: u8 => Self::try_from(v).map_err(|_| "month is invalid")? +} + /// Format the `Format` error of the time crate in an appropriate way. fn format_time_format_error(error: Format) -> EcoString { match error { diff --git a/crates/typst/src/eval/dict.rs b/crates/typst/src/foundations/dict.rs index 9b99f6b7..b43c5428 100644 --- a/crates/typst/src/eval/dict.rs +++ b/crates/typst/src/foundations/dict.rs @@ -8,7 +8,7 @@ use indexmap::IndexMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::diag::StrResult; -use crate::eval::{array, func, repr, scope, ty, Array, Repr, Str, Value}; +use crate::foundations::{array, func, repr, scope, ty, Array, Repr, Str, Value}; use crate::syntax::is_ident; use crate::util::ArcExt; @@ -18,9 +18,9 @@ use crate::util::ArcExt; macro_rules! __dict { ($($key:expr => $value:expr),* $(,)?) => {{ #[allow(unused_mut)] - let mut map = $crate::eval::IndexMap::new(); - $(map.insert($key.into(), $crate::eval::IntoValue::into_value($value));)* - $crate::eval::Dict::from(map) + let mut map = $crate::foundations::IndexMap::new(); + $(map.insert($key.into(), $crate::foundations::IntoValue::into_value($value));)* + $crate::foundations::Dict::from(map) }}; } diff --git a/crates/typst/src/eval/duration.rs b/crates/typst/src/foundations/duration.rs index 35f41603..b6a48f3b 100644 --- a/crates/typst/src/eval/duration.rs +++ b/crates/typst/src/foundations/duration.rs @@ -1,10 +1,10 @@ -use ecow::{eco_format, EcoString}; - use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, Div, Mul, Neg, Sub}; + +use ecow::{eco_format, EcoString}; use time::ext::NumericalDuration; -use crate::eval::{func, repr, scope, ty, Repr}; +use crate::foundations::{func, repr, scope, ty, Repr}; /// Represents a positive or negative span of time. #[ty(scope)] diff --git a/crates/typst/src/model/element.rs b/crates/typst/src/foundations/element.rs index 95e2befb..64b47b78 100644 --- a/crates/typst/src/model/element.rs +++ b/crates/typst/src/foundations/element.rs @@ -1,4 +1,5 @@ use std::any::{Any, TypeId}; +use std::borrow::Cow; use std::cmp::Ordering; use std::fmt::{self, Debug}; use std::hash::Hasher; @@ -8,14 +9,21 @@ use ecow::EcoString; use once_cell::sync::Lazy; use smallvec::SmallVec; -use super::{Content, Selector, Styles}; use crate::diag::{SourceResult, StrResult}; -use crate::doc::{Lang, Region}; -use crate::eval::{cast, Args, Dict, Func, ParamInfo, Repr, Scope, Value, Vm}; -use crate::model::{Guard, Label, Location}; +use crate::eval::Vm; +use crate::foundations::{ + cast, Args, Content, Dict, Func, Label, ParamInfo, Repr, Scope, Selector, StyleChain, + Styles, Value, +}; +use crate::introspection::Location; +use crate::layout::Vt; use crate::syntax::Span; +use crate::text::{Lang, Region}; use crate::util::Static; +#[doc(inline)] +pub use typst_macros::elem; + /// A document element. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Element(Static<NativeElementData>); @@ -93,8 +101,8 @@ impl Element { Selector::Elem(self, None) } - /// Create a selector for this element, filtering for those - /// that [fields](super::Content::field) match the given argument. + /// Create a selector for this element, filtering for those that + /// [fields](crate::foundations::Content::field) match the given argument. pub fn where_(self, fields: SmallVec<[(u8, Value); 1]>) -> Selector { Selector::Elem(self, Some(fields)) } @@ -308,8 +316,67 @@ cast! { self => Element::from(self).into_value(), } -/// The named with which an element is referenced. -pub trait LocalName { - /// Get the name in the given language and (optionally) region. - fn local_name(lang: Lang, region: Option<Region>) -> &'static str; +/// Synthesize fields on an element. This happens before execution of any show +/// rule. +pub trait Synthesize { + /// Prepare the element for show rule application. + fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()>; +} + +/// The base recipe for an element. +pub trait Show { + /// Execute the base recipe for this element. + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>; +} + +/// Post-process an element after it was realized. +pub trait Finalize { + /// Finalize the fully realized form of the element. Use this for effects + /// that should work even in the face of a user-defined show rule. + fn finalize(&self, realized: Content, styles: StyleChain) -> Content; +} + +/// How the element interacts with other elements. +pub trait Behave { + /// The element's interaction behaviour. + fn behaviour(&self) -> Behaviour; + + /// Whether this weak element is larger than a previous one and thus picked + /// as the maximum when the levels are the same. + #[allow(unused_variables)] + fn larger( + &self, + prev: &(Cow<Content>, Behaviour, StyleChain), + styles: StyleChain, + ) -> bool { + false + } +} + +/// How an element interacts with other elements in a stream. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Behaviour { + /// A weak element which only survives when a supportive element is before + /// and after it. Furthermore, per consecutive run of weak elements, only + /// one survives: The one with the lowest weakness level (or the larger one + /// if there is a tie). + Weak(usize), + /// An element that enables adjacent weak elements to exist. The default. + Supportive, + /// An element that destroys adjacent weak elements. + Destructive, + /// An element that does not interact at all with other elements, having the + /// same effect as if it didn't exist, but has a visual representation. + Ignorant, + /// An element that does not have a visual representation. + Invisible, +} + +/// Guards content against being affected by the same show rule multiple times. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Guard { + /// The nth recipe from the top of the chain. + Nth(usize), + /// The [base recipe](Show) for a kind of element. + Base(Element), } diff --git a/crates/typst/src/eval/fields.rs b/crates/typst/src/foundations/fields.rs index a810d543..94b331e3 100644 --- a/crates/typst/src/eval/fields.rs +++ b/crates/typst/src/foundations/fields.rs @@ -1,8 +1,11 @@ +//! Fields on values. + use ecow::{eco_format, EcoString}; use crate::diag::StrResult; -use crate::eval::{IntoValue, Type, Value, Version}; -use crate::geom::{Align, Length, Rel, Stroke}; +use crate::foundations::{IntoValue, Type, Value, Version}; +use crate::layout::{Align, Length, Rel}; +use crate::visualize::Stroke; /// Try to access a field on a value. /// diff --git a/crates/typst/src/eval/float.rs b/crates/typst/src/foundations/float.rs index 0e665c80..3408d1f5 100644 --- a/crates/typst/src/eval/float.rs +++ b/crates/typst/src/foundations/float.rs @@ -2,8 +2,8 @@ use std::num::ParseFloatError; use ecow::{eco_format, EcoString}; -use crate::eval::{cast, func, repr, scope, ty, Repr, Str}; -use crate::geom::Ratio; +use crate::foundations::{cast, func, repr, scope, ty, Repr, Str}; +use crate::layout::Ratio; /// A floating-point number. /// diff --git a/crates/typst/src/eval/func.rs b/crates/typst/src/foundations/func.rs index 8bcb7e46..7d6b94ba 100644 --- a/crates/typst/src/eval/func.rs +++ b/crates/typst/src/foundations/func.rs @@ -1,22 +1,20 @@ use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; -use comemo::{Prehashed, Tracked, TrackedMut}; +use comemo::{Prehashed, TrackedMut}; use ecow::{eco_format, EcoString}; use once_cell::sync::Lazy; -use crate::diag::{bail, HintedStrResult, SourceResult, StrResult}; -use crate::eval::{ - cast, scope, ty, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes, - Tracer, Type, Value, Vm, +use crate::diag::{bail, SourceResult, StrResult}; +use crate::eval::{Route, Vm}; +use crate::foundations::{ + cast, repr, scope, ty, Args, CastInfo, Content, Element, IntoValue, Scope, Scopes, + Selector, Type, Value, }; -use crate::model::{ - Content, DelayedErrors, Element, Introspector, Locator, Selector, Vt, -}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::{FileId, Span, SyntaxNode}; +use crate::introspection::Locator; +use crate::layout::Vt; +use crate::syntax::{ast, FileId, Span, SyntaxNode}; use crate::util::Static; -use crate::World; #[doc(inline)] pub use typst_macros::func; @@ -276,9 +274,9 @@ impl Func { // Determine the route inside the closure. let fresh = Route::new(closure.file); let route = if vm.file.is_none() { fresh.track() } else { vm.route }; - - Closure::call( + crate::eval::call_closure( self, + closure, vm.world(), route, vm.vt.introspector, @@ -396,7 +394,7 @@ impl Debug for Func { } } -impl super::Repr for Func { +impl repr::Repr for Func { fn repr(&self) -> EcoString { match self.name() { Some(name) => name.into(), @@ -496,7 +494,7 @@ pub struct ParamInfo { /// A user-defined closure. #[derive(Debug, Hash)] -pub(super) struct Closure { +pub struct Closure { /// The closure's syntax node. Must be castable to `ast::Closure`. pub node: SyntaxNode, /// The source file where the closure was defined. @@ -516,116 +514,6 @@ impl Closure { .name() .map(|ident| ident.as_str()) } - - /// Call the function in the context with the arguments. - #[comemo::memoize] - #[tracing::instrument(skip_all)] - #[allow(clippy::too_many_arguments)] - fn call( - func: &Func, - world: Tracked<dyn World + '_>, - route: Tracked<Route>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - depth: usize, - mut args: Args, - ) -> SourceResult<Value> { - let Repr::Closure(this) = &func.repr else { - panic!("`this` must be a closure"); - }; - let closure = this.node.cast::<ast::Closure>().unwrap(); - - // Don't leak the scopes from the call site. Instead, we use the scope - // of captured variables we collected earlier. - let mut scopes = Scopes::new(None); - scopes.top = this.captured.clone(); - - // Prepare VT. - let mut locator = Locator::chained(locator); - let vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - - // Prepare VM. - let mut vm = Vm::new(vt, route, this.file, scopes); - vm.depth = depth; - - // Provide the closure itself for recursive calls. - if let Some(name) = closure.name() { - vm.define(name, Value::Func(func.clone())); - } - - // Parse the arguments according to the parameter list. - let num_pos_params = closure - .params() - .children() - .filter(|p| matches!(p, ast::Param::Pos(_))) - .count(); - let num_pos_args = args.to_pos().len(); - let sink_size = num_pos_args.checked_sub(num_pos_params); - - let mut sink = None; - let mut sink_pos_values = None; - let mut defaults = this.defaults.iter(); - for p in closure.params().children() { - match p { - ast::Param::Pos(pattern) => match pattern { - ast::Pattern::Normal(ast::Expr::Ident(ident)) => { - vm.define(ident, args.expect::<Value>(&ident)?) - } - ast::Pattern::Normal(_) => unreachable!(), - pattern => { - super::define_pattern( - &mut vm, - pattern, - args.expect::<Value>("pattern parameter")?, - )?; - } - }, - ast::Param::Sink(ident) => { - sink = ident.name(); - if let Some(sink_size) = sink_size { - sink_pos_values = Some(args.consume(sink_size)?); - } - } - ast::Param::Named(named) => { - let name = named.name(); - let default = defaults.next().unwrap(); - let value = - args.named::<Value>(&name)?.unwrap_or_else(|| default.clone()); - vm.define(name, value); - } - } - } - - if let Some(sink) = sink { - let mut remaining_args = args.take(); - if let Some(sink_pos_values) = sink_pos_values { - remaining_args.items.extend(sink_pos_values); - } - vm.define(sink, remaining_args); - } - - // Ensure all arguments have been used. - args.finish()?; - - // Handle control flow. - let output = closure.body().eval(&mut vm)?; - match vm.flow { - Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit), - Some(FlowEvent::Return(_, None)) => {} - Some(flow) => bail!(flow.forbidden()), - None => {} - } - - Ok(output) - } } impl From<Closure> for Func { @@ -638,233 +526,3 @@ cast! { Closure, self => Value::Func(self.into()), } - -/// A visitor that determines which variables to capture for a closure. -pub struct CapturesVisitor<'a> { - external: Option<&'a Scopes<'a>>, - internal: Scopes<'a>, - captures: Scope, -} - -impl<'a> CapturesVisitor<'a> { - /// Create a new visitor for the given external scopes. - pub fn new(external: Option<&'a Scopes<'a>>) -> Self { - Self { - external, - internal: Scopes::new(None), - captures: Scope::new(), - } - } - - /// Return the scope of captured variables. - pub fn finish(self) -> Scope { - self.captures - } - - /// Visit any node and collect all captured variables. - #[tracing::instrument(skip_all)] - pub fn visit(&mut self, node: &SyntaxNode) { - match node.cast() { - // Every identifier is a potential variable that we need to capture. - // Identifiers that shouldn't count as captures because they - // actually bind a new name are handled below (individually through - // the expressions that contain them). - Some(ast::Expr::Ident(ident)) => self.capture(&ident, Scopes::get), - Some(ast::Expr::MathIdent(ident)) => { - self.capture(&ident, Scopes::get_in_math) - } - - // Code and content blocks create a scope. - Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { - self.internal.enter(); - for child in node.children() { - self.visit(child); - } - self.internal.exit(); - } - - // Don't capture the field of a field access. - Some(ast::Expr::FieldAccess(access)) => { - self.visit(access.target().to_untyped()); - } - - // A closure contains parameter bindings, which are bound before the - // body is evaluated. Care must be taken so that the default values - // of named parameters cannot access previous parameter bindings. - Some(ast::Expr::Closure(expr)) => { - for param in expr.params().children() { - if let ast::Param::Named(named) = param { - self.visit(named.expr().to_untyped()); - } - } - - self.internal.enter(); - if let Some(name) = expr.name() { - self.bind(name); - } - - for param in expr.params().children() { - match param { - ast::Param::Pos(pattern) => { - for ident in pattern.idents() { - self.bind(ident); - } - } - ast::Param::Named(named) => self.bind(named.name()), - ast::Param::Sink(spread) => { - self.bind(spread.name().unwrap_or_default()) - } - } - } - - self.visit(expr.body().to_untyped()); - self.internal.exit(); - } - - // A let expression contains a binding, but that binding is only - // active after the body is evaluated. - Some(ast::Expr::Let(expr)) => { - if let Some(init) = expr.init() { - self.visit(init.to_untyped()); - } - - for ident in expr.kind().idents() { - self.bind(ident); - } - } - - // A for loop contains one or two bindings in its pattern. These are - // active after the iterable is evaluated but before the body is - // evaluated. - Some(ast::Expr::For(expr)) => { - self.visit(expr.iter().to_untyped()); - self.internal.enter(); - - let pattern = expr.pattern(); - for ident in pattern.idents() { - self.bind(ident); - } - - self.visit(expr.body().to_untyped()); - self.internal.exit(); - } - - // An import contains items, but these are active only after the - // path is evaluated. - Some(ast::Expr::Import(expr)) => { - self.visit(expr.source().to_untyped()); - if let Some(ast::Imports::Items(items)) = expr.imports() { - for item in items.iter() { - self.bind(item.bound_name()); - } - } - } - - _ => { - // Never capture the name part of a named pair. - if let Some(named) = node.cast::<ast::Named>() { - self.visit(named.expr().to_untyped()); - return; - } - - // Everything else is traversed from left to right. - for child in node.children() { - self.visit(child); - } - } - } - } - - /// Bind a new internal variable. - fn bind(&mut self, ident: ast::Ident) { - self.internal.top.define(ident.get().clone(), Value::None); - } - - /// Capture a variable if it isn't internal. - #[inline] - fn capture( - &mut self, - ident: &str, - getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>, - ) { - if self.internal.get(ident).is_err() { - let Some(value) = self - .external - .map(|external| getter(external, ident).ok()) - .unwrap_or(Some(&Value::None)) - else { - return; - }; - - self.captures.define_captured(ident, value.clone()); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::syntax::parse; - - #[track_caller] - fn test(text: &str, result: &[&str]) { - let mut scopes = Scopes::new(None); - scopes.top.define("f", 0); - scopes.top.define("x", 0); - scopes.top.define("y", 0); - scopes.top.define("z", 0); - - let mut visitor = CapturesVisitor::new(Some(&scopes)); - let root = parse(text); - visitor.visit(&root); - - let captures = visitor.finish(); - let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect(); - names.sort(); - - assert_eq!(names, result); - } - - #[test] - fn test_captures() { - // Let binding and function definition. - test("#let x = x", &["x"]); - test("#let x; #(x + y)", &["y"]); - test("#let f(x, y) = x + y", &[]); - test("#let f(x, y) = f", &[]); - test("#let f = (x, y) => f", &["f"]); - - // Closure with different kinds of params. - test("#((x, y) => x + z)", &["z"]); - test("#((x: y, z) => x + z)", &["y"]); - test("#((..x) => x + y)", &["y"]); - test("#((x, y: x + z) => x + y)", &["x", "z"]); - test("#{x => x; x}", &["x"]); - - // Show rule. - test("#show y: x => x", &["y"]); - test("#show y: x => x + z", &["y", "z"]); - test("#show x: x => x", &["x"]); - - // For loop. - test("#for x in y { x + z }", &["y", "z"]); - test("#for (x, y) in y { x + y }", &["y"]); - test("#for x in y {} #x", &["x", "y"]); - - // Import. - test("#import z: x, y", &["z"]); - test("#import x + y: x, y, z", &["x", "y"]); - - // Blocks. - test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); - test("#[#let x = 1]#x", &["x"]); - - // Field access. - test("#foo(body: 1)", &[]); - test("#(body: 1)", &[]); - test("#(body = 1)", &[]); - test("#(body += y)", &["y"]); - test("#{ (body, a) = (y, 1) }", &["y"]); - test("#(x.at(y) = 5)", &["x", "y"]) - } -} diff --git a/crates/typst/src/eval/int.rs b/crates/typst/src/foundations/int.rs index 1e43db28..41b13484 100644 --- a/crates/typst/src/eval/int.rs +++ b/crates/typst/src/foundations/int.rs @@ -2,7 +2,7 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError use ecow::{eco_format, EcoString}; -use crate::eval::{cast, func, repr, scope, ty, Repr, Str, Value}; +use crate::foundations::{cast, func, repr, scope, ty, Repr, Str, Value}; /// A whole number. /// diff --git a/crates/typst/src/model/label.rs b/crates/typst/src/foundations/label.rs index ae2a15c7..b54db6fe 100644 --- a/crates/typst/src/model/label.rs +++ b/crates/typst/src/foundations/label.rs @@ -1,8 +1,6 @@ -use std::fmt::Debug; - use ecow::{eco_format, EcoString}; -use crate::eval::{func, scope, ty, Repr}; +use crate::foundations::{func, scope, ty, Repr}; use crate::util::PicoStr; /// A label for an element. diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/foundations/methods.rs index ef3c4d84..287a49c6 100644 --- a/crates/typst/src/eval/methods.rs +++ b/crates/typst/src/foundations/methods.rs @@ -1,19 +1,9 @@ //! Handles special built-in methods on values. use crate::diag::{At, SourceResult}; -use crate::eval::{Args, Array, Dict, Str, Type, Value}; +use crate::foundations::{Args, Array, Dict, Str, Type, Value}; use crate::syntax::Span; -/// Whether a specific method is mutating. -pub fn is_mutating(method: &str) -> bool { - matches!(method, "push" | "pop" | "insert" | "remove") -} - -/// Whether a specific method is an accessor. -pub fn is_accessor(method: &str) -> bool { - matches!(method, "first" | "last" | "at") -} - /// List the available methods for a type and whether they take arguments. pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] { if ty == Type::of::<Array>() { @@ -33,8 +23,18 @@ pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] { } } +/// Whether a specific method is mutating. +pub(crate) fn is_mutating_method(method: &str) -> bool { + matches!(method, "push" | "pop" | "insert" | "remove") +} + +/// Whether a specific method is an accessor. +pub(crate) fn is_accessor_method(method: &str) -> bool { + matches!(method, "first" | "last" | "at") +} + /// Call a mutating method on a value. -pub fn call_mut( +pub(crate) fn call_method_mut( value: &mut Value, method: &str, mut args: Args, @@ -76,7 +76,7 @@ pub fn call_mut( } /// Call an accessor method on a value. -pub fn call_access<'a>( +pub(crate) fn call_method_access<'a>( value: &'a mut Value, method: &str, mut args: Args, diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst/src/foundations/mod.rs index fef00269..b2c35194 100644 --- a/crates/typst-library/src/compute/foundations.rs +++ b/crates/typst/src/foundations/mod.rs @@ -1,16 +1,93 @@ -use typst::eval::{ - Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex, Repr, Version, +//! Foundational types and functions. + +pub mod calc; +pub mod repr; +pub mod sys; + +mod args; +mod array; +mod auto; +mod bool; +mod bytes; +mod cast; +mod content; +mod datetime; +mod dict; +mod duration; +mod element; +mod fields; +mod float; +mod func; +mod int; +mod label; +mod methods; +mod module; +mod none; +mod plugin; +mod scope; +mod selector; +mod str; +mod styles; +mod ty; +mod value; +mod version; + +pub use self::args::*; +pub use self::array::*; +pub use self::auto::*; +pub use self::bytes::*; +pub use self::cast::*; +pub use self::content::*; +pub use self::datetime::*; +pub use self::dict::*; +pub use self::duration::*; +pub use self::element::*; +pub use self::fields::*; +pub use self::float::*; +pub use self::func::*; +pub use self::int::*; +pub use self::label::*; +pub use self::methods::*; +pub use self::module::*; +pub use self::none::*; +pub use self::plugin::*; +pub use self::repr::Repr; +pub use self::scope::*; +pub use self::selector::*; +pub use self::str::*; +pub use self::styles::*; +pub use self::ty::*; +pub use self::value::*; +pub use self::version::*; + +#[doc(hidden)] +pub use { + ecow::{eco_format, eco_vec}, + indexmap::IndexMap, + once_cell::sync::Lazy, }; -use crate::prelude::*; +use ecow::EcoString; -/// Hook up all foundational definitions. +use crate::diag::{bail, SourceResult, StrResult}; +use crate::eval::{EvalMode, Vm}; +use crate::syntax::Spanned; + +/// Foundational types and functions. +/// +/// Here, you'll find documentation for basic data types like [integers]($int) +/// and [strings]($str) as well as details about core computational functions. +#[category] +pub static FOUNDATIONS: Category; + +/// Hook up all `foundations` definitions. pub(super) fn define(global: &mut Scope) { - global.category("foundations"); + global.category(FOUNDATIONS); global.define_type::<bool>(); global.define_type::<i64>(); global.define_type::<f64>(); global.define_type::<Str>(); + global.define_type::<Label>(); global.define_type::<Bytes>(); global.define_type::<Content>(); global.define_type::<Array>(); @@ -20,38 +97,18 @@ pub(super) fn define(global: &mut Scope) { global.define_type::<Type>(); global.define_type::<Module>(); global.define_type::<Regex>(); + global.define_type::<Selector>(); global.define_type::<Datetime>(); global.define_type::<Duration>(); global.define_type::<Version>(); global.define_type::<Plugin>(); - global.define_func::<repr>(); + global.define_func::<repr::repr>(); global.define_func::<panic>(); global.define_func::<assert>(); global.define_func::<eval>(); -} - -/// Returns the string representation of a value. -/// -/// When inserted into content, most values are displayed as this representation -/// in monospace with syntax-highlighting. The exceptions are `{none}`, -/// integers, floats, strings, content, and functions. -/// -/// **Note:** This function is for debugging purposes. Its output should not be -/// considered stable and may change at any time! -/// -/// # Example -/// ```example -/// #none vs #repr(none) \ -/// #"hello" vs #repr("hello") \ -/// #(1, 2) vs #repr((1, 2)) \ -/// #[*Hi*] vs #repr([*Hi*]) -/// ``` -#[func(title = "Representation")] -pub fn repr( - /// The value whose string representation to produce. - value: Value, -) -> Str { - value.repr().into() + global.define_func::<style>(); + global.define_module(calc::module()); + global.define_module(sys::module()); } /// Fails with an error. @@ -232,5 +289,5 @@ pub fn eval( for (key, value) in dict { scope.define(key, value); } - typst::eval::eval_string(vm.world(), &text, span, mode, scope) + crate::eval::eval_string(vm.world(), &text, span, mode, scope) } diff --git a/crates/typst/src/eval/module.rs b/crates/typst/src/foundations/module.rs index 26bf537d..140618bc 100644 --- a/crates/typst/src/eval/module.rs +++ b/crates/typst/src/foundations/module.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use crate::diag::StrResult; -use crate::eval::{ty, Content, Scope, Value}; +use crate::foundations::{repr, ty, Content, Scope, Value}; /// An evaluated module, either built-in or resulting from a file. /// @@ -110,7 +110,7 @@ impl Debug for Module { } } -impl super::Repr for Module { +impl repr::Repr for Module { fn repr(&self) -> EcoString { eco_format!("<module {}>", self.name()) } diff --git a/crates/typst/src/eval/none.rs b/crates/typst/src/foundations/none.rs index a022e578..d03ca8fc 100644 --- a/crates/typst/src/eval/none.rs +++ b/crates/typst/src/foundations/none.rs @@ -1,10 +1,12 @@ -use ecow::EcoString; use std::fmt::{self, Debug, Formatter}; +use ecow::EcoString; use serde::{Serialize, Serializer}; use crate::diag::StrResult; -use crate::eval::{cast, ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value}; +use crate::foundations::{ + cast, ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value, +}; /// A value that indicates the absence of any other value. /// diff --git a/crates/typst/src/eval/plugin.rs b/crates/typst/src/foundations/plugin.rs index cadb6846..6071b3bd 100644 --- a/crates/typst/src/eval/plugin.rs +++ b/crates/typst/src/foundations/plugin.rs @@ -1,12 +1,13 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; +use std::sync::{Arc, Mutex}; use ecow::{eco_format, EcoString}; -use std::sync::{Arc, Mutex}; use wasmi::{AsContext, AsContextMut, Caller, Engine, Linker, Module}; use crate::diag::{bail, At, SourceResult, StrResult}; -use crate::eval::{func, scope, ty, Bytes, Vm}; +use crate::eval::Vm; +use crate::foundations::{func, repr, scope, ty, Bytes}; use crate::syntax::Spanned; use crate::World; @@ -303,7 +304,7 @@ impl Debug for Plugin { } } -impl super::Repr for Plugin { +impl repr::Repr for Plugin { fn repr(&self) -> EcoString { "plugin(..)".into() } diff --git a/crates/typst/src/eval/repr.rs b/crates/typst/src/foundations/repr.rs index e6b1c1a7..420cb63a 100644 --- a/crates/typst/src/eval/repr.rs +++ b/crates/typst/src/foundations/repr.rs @@ -1,7 +1,36 @@ +//! Debug representation of values. + use ecow::{eco_format, EcoString}; +use crate::foundations::{func, Str, Value}; + +/// The Unicode minus sign. pub const MINUS_SIGN: &str = "\u{2212}"; +/// Returns the string representation of a value. +/// +/// When inserted into content, most values are displayed as this representation +/// in monospace with syntax-highlighting. The exceptions are `{none}`, +/// integers, floats, strings, content, and functions. +/// +/// **Note:** This function is for debugging purposes. Its output should not be +/// considered stable and may change at any time! +/// +/// # Example +/// ```example +/// #none vs #repr(none) \ +/// #"hello" vs #repr("hello") \ +/// #(1, 2) vs #repr((1, 2)) \ +/// #[*Hi*] vs #repr([*Hi*]) +/// ``` +#[func(title = "Representation")] +pub fn repr( + /// The value whose string representation to produce. + value: Value, +) -> Str { + value.repr().into() +} + /// A trait that defines the `repr` of a Typst value. pub trait Repr { /// Return the debug representation of the value. diff --git a/crates/typst/src/eval/scope.rs b/crates/typst/src/foundations/scope.rs index 924629ac..0567b5b0 100644 --- a/crates/typst/src/eval/scope.rs +++ b/crates/typst/src/foundations/scope.rs @@ -5,10 +5,15 @@ use ecow::{eco_format, EcoString}; use indexmap::IndexMap; use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; -use crate::eval::{ - Func, IntoValue, Library, Module, NativeFunc, NativeFuncData, NativeType, Type, Value, +use crate::foundations::{ + Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, + NativeType, Type, Value, }; -use crate::model::{Element, NativeElement}; +use crate::util::Static; +use crate::Library; + +#[doc(inline)] +pub use typst_macros::category; /// A stack of scopes. #[derive(Debug, Default, Clone)] @@ -97,7 +102,7 @@ fn unknown_variable(var: &str) -> HintedString { pub struct Scope { map: IndexMap<EcoString, Slot>, deduplicate: bool, - category: Option<&'static str>, + category: Option<Category>, } impl Scope { @@ -112,8 +117,8 @@ impl Scope { } /// Enter a new category. - pub fn category(&mut self, name: &'static str) { - self.category = Some(name); + pub fn category(&mut self, category: Category) { + self.category = Some(category); } /// Reset the category. @@ -185,7 +190,7 @@ impl Scope { } /// Get the category of a definition. - pub fn get_category(&self, var: &str) -> Option<&'static str> { + pub fn get_category(&self, var: &str) -> Option<Category> { self.map.get(var)?.category } @@ -215,6 +220,15 @@ impl Hash for Scope { } } +/// Defines the associated scope of a Rust type. +pub trait NativeScope { + /// The constructor function for the type, if any. + fn constructor() -> Option<&'static NativeFuncData>; + + /// Get the associated scope for the type. + fn scope() -> Scope; +} + /// A slot where a value is stored. #[derive(Clone, Hash)] struct Slot { @@ -223,7 +237,7 @@ struct Slot { /// The kind of slot, determines how the value can be accessed. kind: Kind, /// The category of the slot. - category: Option<&'static str>, + category: Option<Category>, } /// The different kinds of slots. @@ -237,7 +251,7 @@ enum Kind { impl Slot { /// Create a new slot. - fn new(value: Value, kind: Kind, category: Option<&'static str>) -> Self { + fn new(value: Value, kind: Kind, category: Option<Category>) -> Self { Self { value, kind, category } } @@ -260,11 +274,42 @@ impl Slot { } } -/// Defines the associated scope of a Rust type. -pub trait NativeScope { - /// The constructor function for the type, if any. - fn constructor() -> Option<&'static NativeFuncData>; +/// A group of related definitions. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct Category(Static<CategoryData>); - /// Get the associated scope for the type. - fn scope() -> Scope; +impl Category { + /// Create a new category from raw data. + pub const fn from_data(data: &'static CategoryData) -> Self { + Self(Static(data)) + } + + /// The category's name. + pub fn name(&self) -> &'static str { + self.0.name + } + + /// The type's title case name, for use in documentation (e.g. `String`). + pub fn title(&self) -> &'static str { + self.0.title + } + + /// Documentation for the category. + pub fn docs(&self) -> &'static str { + self.0.docs + } +} + +impl Debug for Category { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Category({})", self.name()) + } +} + +/// Defines a category. +#[derive(Debug)] +pub struct CategoryData { + pub name: &'static str, + pub title: &'static str, + pub docs: &'static str, } diff --git a/crates/typst/src/model/selector.rs b/crates/typst/src/foundations/selector.rs index 4148261f..f0ab90ee 100644 --- a/crates/typst/src/model/selector.rs +++ b/crates/typst/src/foundations/selector.rs @@ -1,16 +1,17 @@ use std::any::{Any, TypeId}; -use std::fmt::Debug; use std::sync::Arc; use ecow::{eco_format, EcoString, EcoVec}; use smallvec::SmallVec; use crate::diag::{bail, StrResult}; -use crate::eval::{ - cast, func, item, repr, scope, ty, CastInfo, Dict, FromValue, Func, Reflect, Regex, - Repr, Str, Symbol, Type, Value, +use crate::foundations::{ + cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func, + Label, Reflect, Regex, Repr, Str, Type, Value, }; -use crate::model::{Content, Element, Label, Locatable, Location}; +use crate::introspection::{Locatable, Location}; +use crate::symbols::Symbol; +use crate::text::TextElem; /// A helper macro to create a field selector used in [`Selector::Elem`] /// @@ -25,12 +26,12 @@ macro_rules! __select_where { let mut fields = ::smallvec::SmallVec::new(); $( fields.push(( - <$ty as $crate::model::ElementFields>::Fields::$field as u8, - $crate::eval::IntoValue::into_value($value), + <$ty as $crate::foundations::ElementFields>::Fields::$field as u8, + $crate::foundations::IntoValue::into_value($value), )); )* - $crate::model::Selector::Elem( - <$ty as $crate::model::NativeElement>::elem(), + $crate::foundations::Selector::Elem( + <$ty as $crate::foundations::NativeElement>::elem(), Some(fields), ) }}; @@ -138,10 +139,9 @@ impl Selector { .all(|(id, value)| target.get(*id).as_ref() == Some(value)) } Self::Label(label) => target.label() == Some(*label), - Self::Regex(regex) => { - target.func() == item!(text_elem) - && item!(text_str)(target).map_or(false, |text| regex.is_match(text)) - } + Self::Regex(regex) => target + .to::<TextElem>() + .map_or(false, |elem| regex.is_match(elem.text())), Self::Can(cap) => target.func().can_type_id(*cap), Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), diff --git a/crates/typst/src/eval/str.rs b/crates/typst/src/foundations/str.rs index 70feae22..4b56971a 100644 --- a/crates/typst/src/eval/str.rs +++ b/crates/typst/src/foundations/str.rs @@ -8,12 +8,12 @@ use serde::{Deserialize, Serialize}; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, At, SourceResult, StrResult}; -use crate::eval::{ - cast, dict, func, repr, scope, ty, Args, Array, Bytes, Dict, Func, IntoValue, Repr, - Type, Value, Version, Vm, +use crate::eval::Vm; +use crate::foundations::{ + cast, dict, func, repr, scope, ty, Args, Array, Bytes, Dict, Func, IntoValue, Label, + Repr, Type, Value, Version, }; -use crate::geom::Align; -use crate::model::Label; +use crate::layout::Align; use crate::syntax::{Span, Spanned}; /// Create a new [`Str`] from a format string. @@ -21,7 +21,7 @@ use crate::syntax::{Span, Spanned}; #[doc(hidden)] macro_rules! __format_str { ($($tts:tt)*) => {{ - $crate::eval::Str::from($crate::eval::eco_format!($($tts)*)) + $crate::foundations::Str::from($crate::foundations::eco_format!($($tts)*)) }}; } diff --git a/crates/typst/src/model/styles.rs b/crates/typst/src/foundations/styles.rs index a943472e..78bd24d6 100644 --- a/crates/typst/src/model/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -2,9 +2,7 @@ use std::any::Any; use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; -use std::iter; -use std::mem; -use std::ptr; +use std::{iter, mem, ptr}; use comemo::Prehashed; use ecow::{eco_vec, EcoString, EcoVec}; @@ -12,9 +10,58 @@ use once_cell::sync::Lazy; use smallvec::SmallVec; use crate::diag::{SourceResult, Trace, Tracepoint}; -use crate::eval::{cast, ty, Args, Func, Repr, Value, Vm}; -use crate::model::{Content, Element, NativeElement, Selector, Vt}; +use crate::eval::Vm; +use crate::foundations::{ + cast, elem, func, ty, Args, Content, Element, Func, NativeElement, Repr, Selector, + Show, Value, +}; +use crate::layout::Vt; use crate::syntax::Span; +use crate::text::{FontFamily, FontList, TextElem}; + +/// Provides access to active styles. +/// +/// The styles are currently opaque and only useful in combination with the +/// [`measure`]($measure) function. See its documentation for more details. In +/// the future, the provided styles might also be directly accessed to look up +/// styles defined by [set rules]($styling/#set-rules). +/// +/// ```example +/// #let thing(body) = style(styles => { +/// let size = measure(body, styles) +/// [Width of "#body" is #size.width] +/// }) +/// +/// #thing[Hey] \ +/// #thing[Welcome] +/// ``` +#[func] +pub fn style( + /// A function to call with the styles. Its return value is displayed + /// in the document. + /// + /// This function is called once for each time the content returned by + /// `style` appears in the document. That makes it possible to generate + /// content that depends on the style context it appears in. + func: Func, +) -> Content { + StyleElem::new(func).pack() +} + +/// Executes a style access. +#[elem(Show)] +struct StyleElem { + /// The function to call with the styles. + #[required] + func: Func, +} + +impl Show for StyleElem { + #[tracing::instrument(name = "StyleElem::show", skip_all)] + fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self.func().call_vt(vt, [styles.to_map()])?.display()) + } +} /// A list of style properties. #[ty] @@ -83,6 +130,16 @@ impl Styles { Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.span)), }) } + + /// Set a font family composed of a preferred family and existing families + /// from a style chain. + pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { + self.set(TextElem::set_font(FontList( + std::iter::once(preferred) + .chain(TextElem::font_in(existing).into_iter().cloned()) + .collect(), + ))); + } } impl From<Style> for Styles { @@ -279,7 +336,7 @@ pub struct Recipe { /// Determines whether the recipe applies to an element. pub selector: Option<Selector>, /// The transformation to perform on the match. - pub transform: Transform, + pub transform: Transformation, } impl Recipe { @@ -301,8 +358,8 @@ impl Recipe { /// Apply the recipe to the given content. pub fn apply_vm(&self, vm: &mut Vm, content: Content) -> SourceResult<Content> { match &self.transform { - Transform::Content(content) => Ok(content.clone()), - Transform::Func(func) => { + Transformation::Content(content) => Ok(content.clone()), + Transformation::Func(func) => { let args = Args::new(self.span, [Value::Content(content.clone())]); let mut result = func.call_vm(vm, args); // For selector-less show rules, a tracepoint makes no sense. @@ -312,15 +369,15 @@ impl Recipe { } Ok(result?.display()) } - Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())), + Transformation::Style(styles) => Ok(content.styled_with_map(styles.clone())), } } /// Apply the recipe to the given content. pub fn apply_vt(&self, vt: &mut Vt, content: Content) -> SourceResult<Content> { match &self.transform { - Transform::Content(content) => Ok(content.clone()), - Transform::Func(func) => { + Transformation::Content(content) => Ok(content.clone()), + Transformation::Func(func) => { let mut result = func.call_vt(vt, [Value::Content(content.clone())]); if self.selector.is_some() { let point = || Tracepoint::Show(content.func().name().into()); @@ -328,7 +385,7 @@ impl Recipe { } Ok(result?.display()) } - Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())), + Transformation::Style(styles) => Ok(content.styled_with_map(styles.clone())), } } } @@ -346,7 +403,7 @@ impl Debug for Recipe { /// A show rule transformation that can be applied to a match. #[derive(Clone, PartialEq, Hash)] -pub enum Transform { +pub enum Transformation { /// Replacement content. Content(Content), /// A function to apply to the match. @@ -355,7 +412,7 @@ pub enum Transform { Style(Styles), } -impl Debug for Transform { +impl Debug for Transformation { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Content(content) => content.fmt(f), @@ -366,7 +423,7 @@ impl Debug for Transform { } cast! { - Transform, + Transformation, content: Content => Self::Content(content), func: Func => Self::Func(func), } diff --git a/crates/typst-library/src/compute/sys.rs b/crates/typst/src/foundations/sys.rs index 6404e625..3561842e 100644 --- a/crates/typst-library/src/compute/sys.rs +++ b/crates/typst/src/foundations/sys.rs @@ -1,17 +1,10 @@ //! System-related things. -use typst::eval::{Module, Scope, Version}; - -/// Hook up all calculation definitions. -pub(super) fn define(global: &mut Scope) { - global.category("sys"); - global.define_module(module()); -} +use crate::foundations::{Module, Scope, Version}; /// A module with system-related things. -fn module() -> Module { +pub fn module() -> Module { let mut scope = Scope::deduplicating(); - scope.category("sys"); scope.define( "version", Version::from_iter([ diff --git a/crates/typst/src/eval/ty.rs b/crates/typst/src/foundations/ty.rs index 3dc8f555..47add6ec 100644 --- a/crates/typst/src/eval/ty.rs +++ b/crates/typst/src/foundations/ty.rs @@ -5,7 +5,7 @@ use ecow::{eco_format, EcoString}; use once_cell::sync::Lazy; use crate::diag::StrResult; -use crate::eval::{cast, func, Func, NativeFuncData, Repr, Scope, Value}; +use crate::foundations::{cast, func, Func, NativeFuncData, Repr, Scope, Value}; use crate::util::Static; #[doc(inline)] @@ -93,7 +93,7 @@ impl Type { .constructor .as_ref() .map(|lazy| Func::from(*lazy)) - .ok_or_else(|| eco_format!("type {self} does not have a constructor")) + .ok_or_else(|| eco_format!("type self does not have a constructor")) } /// The type's associated scope of sub-definition. @@ -105,7 +105,7 @@ impl Type { pub fn field(&self, field: &str) -> StrResult<&'static Value> { self.scope() .get(field) - .ok_or_else(|| eco_format!("type {self} does not contain field `{}`", field)) + .ok_or_else(|| eco_format!("type self does not contain field `{}`", field)) } } diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/foundations/value.rs index b88a293e..b3141a16 100644 --- a/crates/typst/src/eval/value.rs +++ b/crates/typst/src/foundations/value.rs @@ -11,14 +11,17 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use siphasher::sip128::{Hasher128, SipHasher13}; use crate::diag::StrResult; -use crate::eval::{ - fields, item, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, - Dict, Duration, FromValue, Func, IntoValue, Module, NativeType, NoneValue, Plugin, - Reflect, Repr, Scope, Str, Symbol, Type, Version, +use crate::eval::ops; +use crate::foundations::{ + fields, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Dict, + Duration, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType, + NoneValue, Plugin, Reflect, Repr, Scope, Str, Styles, Type, Version, }; -use crate::geom::{Abs, Angle, Color, Em, Fr, Gradient, Length, Ratio, Rel}; -use crate::model::{Label, Styles}; +use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; +use crate::symbols::Symbol; use crate::syntax::{ast, Span}; +use crate::text::{RawElem, TextElem}; +use crate::visualize::{Color, Gradient}; /// A computational value. #[derive(Default, Clone)] @@ -196,14 +199,17 @@ impl Value { pub fn display(self) -> Content { match self { Self::None => Content::empty(), - Self::Int(v) => item!(text)(repr::format_int_with_base(v, 10)), - Self::Float(v) => item!(text)(repr::format_float(v, None, "")), - Self::Str(v) => item!(text)(v.into()), - Self::Version(v) => item!(text)(eco_format!("{v}")), - Self::Symbol(v) => item!(text)(v.get().into()), + Self::Int(v) => TextElem::packed(repr::format_int_with_base(v, 10)), + Self::Float(v) => TextElem::packed(repr::format_float(v, None, "")), + Self::Str(v) => TextElem::packed(v), + Self::Version(v) => TextElem::packed(eco_format!("{v}")), + Self::Symbol(v) => TextElem::packed(v.get()), Self::Content(v) => v, Self::Module(module) => module.content(), - _ => item!(raw)(self.repr(), Some("typc".into()), false), + _ => RawElem::new(self.repr()) + .with_lang(Some("typc".into())) + .with_block(false) + .pack(), } } @@ -643,8 +649,8 @@ primitive! { Duration: "duration", Duration } primitive! { Content: "content", Content, None => Content::empty(), - Symbol(v) => item!(text)(v.get().into()), - Str(v) => item!(text)(v.into()) + Symbol(v) => TextElem::packed(v.get()), + Str(v) => TextElem::packed(v) } primitive! { Styles: "styles", Styles } primitive! { Array: "array", Array } @@ -662,7 +668,7 @@ primitive! { Plugin: "plugin", Plugin } #[cfg(test)] mod tests { use super::*; - use crate::eval::{array, dict}; + use crate::foundations::{array, dict}; #[track_caller] fn test(value: impl IntoValue, exp: &str) { diff --git a/crates/typst/src/eval/version.rs b/crates/typst/src/foundations/version.rs index b05773cb..80a39fe4 100644 --- a/crates/typst/src/eval/version.rs +++ b/crates/typst/src/foundations/version.rs @@ -6,7 +6,7 @@ use std::iter::repeat; use ecow::{eco_format, EcoString, EcoVec}; use crate::diag::{bail, error, StrResult}; -use crate::eval::{cast, func, repr, scope, ty, Repr}; +use crate::foundations::{cast, func, repr, scope, ty, Repr}; /// A version with an arbitrary number of components. /// diff --git a/crates/typst/src/geom/ellipse.rs b/crates/typst/src/geom/ellipse.rs deleted file mode 100644 index 36046d95..00000000 --- a/crates/typst/src/geom/ellipse.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::*; - -/// Produce a shape that approximates an axis-aligned ellipse. -pub 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 } -} diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs deleted file mode 100644 index 6df0e148..00000000 --- a/crates/typst/src/geom/mod.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Geometrical primitives. - -#[macro_use] -mod macros; -mod abs; -mod align; -mod angle; -mod axes; -mod color; -mod corners; -mod dir; -mod ellipse; -mod em; -mod fr; -mod gradient; -mod length; -mod paint; -mod path; -mod point; -mod ratio; -mod rect; -mod rel; -mod scalar; -mod shape; -mod sides; -mod size; -mod stroke; -mod transform; - -pub use self::abs::{Abs, AbsUnit}; -pub use self::align::{Align, FixedAlign, HAlign, VAlign}; -pub use self::angle::{Angle, AngleUnit, Quadrant}; -pub use self::axes::{Axes, Axis}; -pub use self::color::{Color, ColorSpace, WeightedColor}; -pub use self::corners::{Corner, Corners}; -pub use self::dir::Dir; -pub use self::ellipse::ellipse; -pub use self::em::Em; -pub use self::fr::Fr; -pub use self::gradient::{ - ConicGradient, Gradient, LinearGradient, RatioOrAngle, Relative, -}; -pub use self::length::Length; -pub use self::paint::Paint; -pub use self::path::{Path, PathItem}; -pub use self::point::Point; -pub use self::ratio::Ratio; -pub use self::rect::{clip_rect, styled_rect}; -pub use self::rel::Rel; -pub use self::scalar::Scalar; -pub use self::shape::{Geometry, Shape}; -pub use self::sides::{Side, Sides}; -pub use self::size::Size; -pub use self::stroke::{DashLength, DashPattern, FixedStroke, LineCap, LineJoin, Stroke}; -pub use self::transform::Transform; - -use std::cmp::Ordering; -use std::f64::consts::PI; -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::iter::Sum; -use std::ops::*; - -use ecow::{eco_format, EcoString}; - -use crate::diag::{bail, StrResult}; -use crate::eval::repr::format_float; -use crate::eval::{array, cast, func, item, scope, ty, Array, Dict, Repr, Smart, Value}; -use crate::model::{Fold, Resolve, StyleChain}; - -/// Generic access to a structure's components. -pub trait Get<Index> { - /// The structure's component type. - type Component; - - /// Borrow the component for the specified index. - fn get_ref(&self, index: Index) -> &Self::Component; - - /// Borrow the component for the specified index mutably. - fn get_mut(&mut self, index: Index) -> &mut Self::Component; - - /// Convenience method for getting a copy of a component. - fn get(self, index: Index) -> Self::Component - where - Self: Sized, - Self::Component: Copy, - { - *self.get_ref(index) - } - - /// Convenience method for setting a component. - fn set(&mut self, index: Index, component: Self::Component) { - *self.get_mut(index) = component; - } -} - -/// A numeric type. -pub trait Numeric: - Sized - + Debug - + Copy - + PartialEq - + Neg<Output = Self> - + Add<Output = Self> - + Sub<Output = Self> - + Mul<f64, Output = Self> - + Div<f64, Output = Self> -{ - /// The identity element for addition. - fn zero() -> Self; - - /// Whether `self` is zero. - fn is_zero(self) -> bool { - self == Self::zero() - } - - /// Whether `self` consists only of finite parts. - fn is_finite(self) -> bool; -} - -/// Round a float to two decimal places. -pub fn round_2(value: f64) -> f64 { - (value * 100.0).round() / 100.0 -} diff --git a/crates/typst/src/geom/path.rs b/crates/typst/src/geom/path.rs deleted file mode 100644 index 177c4f65..00000000 --- a/crates/typst/src/geom/path.rs +++ /dev/null @@ -1,102 +0,0 @@ -use kurbo::Shape; - -use super::*; - -/// A bezier path. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct Path(pub Vec<PathItem>); - -/// An item in a bezier path. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum PathItem { - MoveTo(Point), - LineTo(Point), - CubicTo(Point, Point, Point), - ClosePath, -} - -impl Path { - /// Create an empty path. - pub const fn new() -> Self { - Self(vec![]) - } - - /// Create a path that describes a rectangle. - pub fn rect(size: Size) -> Self { - let z = Abs::zero(); - let point = Point::new; - let mut path = Self::new(); - path.move_to(point(z, z)); - path.line_to(point(size.x, z)); - path.line_to(point(size.x, size.y)); - path.line_to(point(z, size.y)); - path.close_path(); - path - } - - /// Push a [`MoveTo`](PathItem::MoveTo) item. - pub fn move_to(&mut self, p: Point) { - self.0.push(PathItem::MoveTo(p)); - } - - /// Push a [`LineTo`](PathItem::LineTo) item. - pub fn line_to(&mut self, p: Point) { - self.0.push(PathItem::LineTo(p)); - } - - /// Push a [`CubicTo`](PathItem::CubicTo) item. - pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) { - self.0.push(PathItem::CubicTo(p1, p2, p3)); - } - - /// Push a [`ClosePath`](PathItem::ClosePath) item. - pub fn close_path(&mut self) { - self.0.push(PathItem::ClosePath); - } - - /// Computes the size of bounding box of this path. - pub fn bbox_size(&self) -> Size { - let mut min_x = Abs::inf(); - let mut min_y = Abs::inf(); - let mut max_x = -Abs::inf(); - let mut max_y = -Abs::inf(); - - let mut cursor = Point::zero(); - for item in self.0.iter() { - match item { - PathItem::MoveTo(to) => { - min_x = min_x.min(cursor.x); - min_y = min_y.min(cursor.y); - max_x = max_x.max(cursor.x); - max_y = max_y.max(cursor.y); - cursor = *to; - } - PathItem::LineTo(to) => { - min_x = min_x.min(cursor.x); - min_y = min_y.min(cursor.y); - max_x = max_x.max(cursor.x); - max_y = max_y.max(cursor.y); - cursor = *to; - } - PathItem::CubicTo(c0, c1, end) => { - let cubic = kurbo::CubicBez::new( - kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()), - kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()), - kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()), - kurbo::Point::new(end.x.to_pt(), end.y.to_pt()), - ); - - let bbox = cubic.bounding_box(); - min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1)); - min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1)); - max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1)); - max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1)); - cursor = *end; - } - PathItem::ClosePath => (), - } - } - - Size::new(max_x - min_x, max_y - min_y) - } -} diff --git a/crates/typst/src/geom/rect.rs b/crates/typst/src/geom/rect.rs deleted file mode 100644 index 0c7595c3..00000000 --- a/crates/typst/src/geom/rect.rs +++ /dev/null @@ -1,599 +0,0 @@ -use super::*; - -/// Creates a new rectangle as a path. -pub fn clip_rect( - size: Size, - radius: Corners<Rel<Abs>>, - stroke: &Sides<Option<FixedStroke>>, -) -> Path { - let stroke_widths = stroke - .as_ref() - .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0)); - - let max_radius = (size.x.min(size.y)) / 2.0 - + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); - - let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius)); - - let corners = corners_control_points(size, radius, stroke, stroke_widths); - - let mut path = Path::new(); - if corners.top_left.arc_inner() { - path.arc_move( - corners.top_left.start_inner(), - corners.top_left.center_inner(), - corners.top_left.end_inner(), - ); - } else { - path.move_to(corners.top_left.center_inner()); - } - for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] { - if corner.arc_inner() { - path.arc_line(corner.start_inner(), corner.center_inner(), corner.end_inner()) - } else { - path.line_to(corner.center_inner()); - } - } - path.close_path(); - path -} - -/// Create a styled rectangle with shapes. -/// - use rect primitive for simple rectangles -/// - stroke sides if possible -/// - use fill for sides for best looks -pub fn styled_rect( - size: Size, - radius: Corners<Rel<Abs>>, - fill: Option<Paint>, - stroke: Sides<Option<FixedStroke>>, -) -> Vec<Shape> { - if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) { - simple_rect(size, fill, stroke.top) - } else { - segmented_rect(size, radius, fill, stroke) - } -} - -/// Use rect primitive for the rectangle -fn simple_rect( - size: Size, - fill: Option<Paint>, - stroke: Option<FixedStroke>, -) -> Vec<Shape> { - vec![Shape { geometry: Geometry::Rect(size), fill, stroke }] -} - -fn corners_control_points( - size: Size, - radius: Corners<Abs>, - strokes: &Sides<Option<FixedStroke>>, - stroke_widths: Sides<Abs>, -) -> Corners<ControlPoints> { - Corners { - top_left: Corner::TopLeft, - top_right: Corner::TopRight, - bottom_right: Corner::BottomRight, - bottom_left: Corner::BottomLeft, - } - .map(|corner| ControlPoints { - radius: radius.get(corner), - stroke_before: stroke_widths.get(corner.side_ccw()), - stroke_after: stroke_widths.get(corner.side_cw()), - corner, - size, - same: match ( - strokes.get_ref(corner.side_ccw()), - strokes.get_ref(corner.side_cw()), - ) { - (Some(a), Some(b)) => a.paint == b.paint && a.dash_pattern == b.dash_pattern, - (None, None) => true, - _ => false, - }, - }) -} - -/// Use stroke and fill for the rectangle -fn segmented_rect( - size: Size, - radius: Corners<Rel<Abs>>, - fill: Option<Paint>, - strokes: Sides<Option<FixedStroke>>, -) -> Vec<Shape> { - let mut res = vec![]; - let stroke_widths = strokes - .as_ref() - .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0)); - - let max_radius = (size.x.min(size.y)) / 2.0 - + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); - - let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius)); - - let corners = corners_control_points(size, radius, &strokes, stroke_widths); - - // insert stroked sides below filled sides - let mut stroke_insert = 0; - - // fill shape with inner curve - if let Some(fill) = fill { - let mut path = Path::new(); - let c = corners.get_ref(Corner::TopLeft); - if c.arc() { - path.arc_move(c.start(), c.center(), c.end()); - } else { - path.move_to(c.center()); - }; - - for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] { - let c = corners.get_ref(corner); - if c.arc() { - path.arc_line(c.start(), c.center(), c.end()); - } else { - path.line_to(c.center()); - } - } - path.close_path(); - res.push(Shape { - geometry: Geometry::Path(path), - fill: Some(fill), - stroke: None, - }); - stroke_insert += 1; - } - - let current = corners.iter().find(|c| !c.same).map(|c| c.corner); - if let Some(mut current) = current { - // multiple segments - // start at a corner with a change between sides and iterate clockwise all other corners - let mut last = current; - for _ in 0..4 { - current = current.next_cw(); - if corners.get_ref(current).same { - continue; - } - // create segment - let start = last; - let end = current; - last = current; - let stroke = match strokes.get_ref(start.side_cw()) { - None => continue, - Some(stroke) => stroke.clone(), - }; - let (shape, ontop) = segment(start, end, &corners, stroke); - if ontop { - res.push(shape); - } else { - res.insert(stroke_insert, shape); - stroke_insert += 1; - } - } - } else if let Some(stroke) = strokes.top { - // single segment - let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke); - res.push(shape); - } - res -} - -fn path_segment( - start: Corner, - end: Corner, - corners: &Corners<ControlPoints>, - path: &mut Path, -) { - // create start corner - let c = corners.get_ref(start); - if start == end || !c.arc() { - path.move_to(c.end()); - } else { - path.arc_move(c.mid(), c.center(), c.end()); - } - - // create corners between start and end - let mut current = start.next_cw(); - while current != end { - let c = corners.get_ref(current); - if c.arc() { - path.arc_line(c.start(), c.center(), c.end()); - } else { - path.line_to(c.end()); - } - current = current.next_cw(); - } - - // create end corner - let c = corners.get_ref(end); - if !c.arc() { - path.line_to(c.start()); - } else if start == end { - path.arc_line(c.start(), c.center(), c.end()); - } else { - path.arc_line(c.start(), c.center(), c.mid()); - } -} - -/// Returns the shape for the segment and whether the shape should be drawn on top. -fn segment( - start: Corner, - end: Corner, - corners: &Corners<ControlPoints>, - stroke: FixedStroke, -) -> (Shape, bool) { - fn fill_corner(corner: &ControlPoints) -> bool { - corner.stroke_before != corner.stroke_after - || corner.radius() < corner.stroke_before - } - - fn fill_corners( - start: Corner, - end: Corner, - corners: &Corners<ControlPoints>, - ) -> bool { - if fill_corner(corners.get_ref(start)) { - return true; - } - if fill_corner(corners.get_ref(end)) { - return true; - } - let mut current = start.next_cw(); - while current != end { - if fill_corner(corners.get_ref(current)) { - return true; - } - current = current.next_cw(); - } - false - } - - let solid = stroke - .dash_pattern - .as_ref() - .map(|pattern| pattern.array.is_empty()) - .unwrap_or(true); - - let use_fill = solid && fill_corners(start, end, corners); - - let shape = if use_fill { - fill_segment(start, end, corners, stroke) - } else { - stroke_segment(start, end, corners, stroke) - }; - (shape, use_fill) -} - -/// Stroke the sides from `start` to `end` clockwise. -fn stroke_segment( - start: Corner, - end: Corner, - corners: &Corners<ControlPoints>, - stroke: FixedStroke, -) -> Shape { - // create start corner - let mut path = Path::new(); - path_segment(start, end, corners, &mut path); - - Shape { - geometry: Geometry::Path(path), - stroke: Some(stroke), - fill: None, - } -} - -/// Fill the sides from `start` to `end` clockwise. -fn fill_segment( - start: Corner, - end: Corner, - corners: &Corners<ControlPoints>, - stroke: FixedStroke, -) -> Shape { - let mut path = Path::new(); - - // create the start corner - // begin on the inside and finish on the outside - // no corner if start and end are equal - // half corner if different - if start == end { - let c = corners.get_ref(start); - path.move_to(c.end_inner()); - path.line_to(c.end_outer()); - } else { - let c = corners.get_ref(start); - - if c.arc_inner() { - path.arc_move(c.end_inner(), c.center_inner(), c.mid_inner()); - } else { - path.move_to(c.end_inner()); - } - - if c.arc_outer() { - path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); - } else { - path.line_to(c.outer()); - path.line_to(c.end_outer()); - } - } - - // create the clockwise outside path for the corners between start and end - let mut current = start.next_cw(); - while current != end { - let c = corners.get_ref(current); - if c.arc_outer() { - path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); - } else { - path.line_to(c.outer()); - } - current = current.next_cw(); - } - - // create the end corner - // begin on the outside and finish on the inside - // full corner if start and end are equal - // half corner if different - if start == end { - let c = corners.get_ref(end); - if c.arc_outer() { - path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); - } else { - path.line_to(c.outer()); - path.line_to(c.end_outer()); - } - if c.arc_inner() { - path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); - } else { - path.line_to(c.center_inner()); - } - } else { - let c = corners.get_ref(end); - if c.arc_outer() { - path.arc_line(c.start_outer(), c.center_outer(), c.mid_outer()); - } else { - path.line_to(c.outer()); - } - if c.arc_inner() { - path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); - } else { - path.line_to(c.center_inner()); - } - } - - // create the counterclockwise inside path for the corners between start and end - let mut current = end.next_ccw(); - while current != start { - let c = corners.get_ref(current); - if c.arc_inner() { - path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); - } else { - path.line_to(c.center_inner()); - } - current = current.next_ccw(); - } - - path.close_path(); - - Shape { - geometry: Geometry::Path(path), - stroke: None, - fill: Some(stroke.paint), - } -} - -/// Helper to calculate different control points for the corners. -/// Clockwise orientation from start to end. -/// ```text -/// O-------------------EO --- - Z: Zero/Origin ({x: 0, y: 0} for top left corner) -/// |\ ___----''' | | - O: Outer: intersection between the straight outer lines -/// | \ / | | - S_: start -/// | MO | | - M_: midpoint -/// | /Z\ __-----------E | - E_: end -/// |/ \M | ro - r_: radius -/// | /\ | | - middle of the stroke -/// | / \ | | - arc from S through M to E with center C and radius r -/// | | MI--EI------- | - outer curve -/// | | / \ | - arc from SO through MO to EO with center CO and radius ro -/// SO | | \ CO --- - inner curve -/// | | | \ - arc from SI through MI to EI with center CI and radius ri -/// |--S-SI-----CI C -/// |--ri--| -/// |-------r--------| -/// ``` -struct ControlPoints { - radius: Abs, - stroke_after: Abs, - stroke_before: Abs, - corner: Corner, - size: Size, - same: bool, -} - -impl ControlPoints { - /// Move and rotate the point from top-left to the required corner. - fn rotate(&self, point: Point) -> Point { - match self.corner { - Corner::TopLeft => point, - Corner::TopRight => Point { x: self.size.x - point.y, y: point.x }, - Corner::BottomRight => { - Point { x: self.size.x - point.x, y: self.size.y - point.y } - } - Corner::BottomLeft => Point { x: point.y, y: self.size.y - point.x }, - } - } - - /// Outside intersection of the sides. - pub fn outer(&self) -> Point { - self.rotate(Point { x: -self.stroke_before, y: -self.stroke_after }) - } - - /// Center for the outer arc. - pub fn center_outer(&self) -> Point { - let r = self.radius_outer(); - self.rotate(Point { - x: r - self.stroke_before, - y: r - self.stroke_after, - }) - } - - /// Center for the middle arc. - pub fn center(&self) -> Point { - let r = self.radius(); - self.rotate(Point { x: r, y: r }) - } - - /// Center for the inner arc. - pub fn center_inner(&self) -> Point { - let r = self.radius_inner(); - - self.rotate(Point { - x: self.stroke_before + r, - y: self.stroke_after + r, - }) - } - - /// Radius of the outer arc. - pub fn radius_outer(&self) -> Abs { - self.radius - } - - /// Radius of the middle arc. - pub fn radius(&self) -> Abs { - (self.radius - self.stroke_before.min(self.stroke_after)).max(Abs::zero()) - } - - /// Radius of the inner arc. - pub fn radius_inner(&self) -> Abs { - (self.radius - 2.0 * self.stroke_before.max(self.stroke_after)).max(Abs::zero()) - } - - /// Middle of the corner on the outside of the stroke. - pub fn mid_outer(&self) -> Point { - let c_i = self.center_inner(); - let c_o = self.center_outer(); - let o = self.outer(); - let r = self.radius_outer(); - - // https://math.stackexchange.com/a/311956 - // intersection between the line from inner center to outside and the outer arc - let a = (o.x - c_i.x).to_raw().powi(2) + (o.y - c_i.y).to_raw().powi(2); - let b = 2.0 * (o.x - c_i.x).to_raw() * (c_i.x - c_o.x).to_raw() - + 2.0 * (o.y - c_i.y).to_raw() * (c_i.y - c_o.y).to_raw(); - let c = (c_i.x - c_o.x).to_raw().powi(2) + (c_i.y - c_o.y).to_raw().powi(2) - - r.to_raw().powi(2); - let t = (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a); - c_i + t * (o - c_i) - } - - /// Middle of the corner in the middle of the stroke. - pub fn mid(&self) -> Point { - let center = self.center_outer(); - let outer = self.outer(); - let diff = outer - center; - center + diff / diff.hypot().to_raw() * self.radius().to_raw() - } - - /// Middle of the corner on the inside of the stroke. - pub fn mid_inner(&self) -> Point { - let center = self.center_inner(); - let outer = self.outer(); - let diff = outer - center; - center + diff / diff.hypot().to_raw() * self.radius_inner().to_raw() - } - - /// If an outer arc is required. - pub fn arc_outer(&self) -> bool { - self.radius_outer() > Abs::zero() - } - - pub fn arc(&self) -> bool { - self.radius() > Abs::zero() - } - - /// If an inner arc is required. - pub fn arc_inner(&self) -> bool { - self.radius_inner() > Abs::zero() - } - - /// Start of the corner on the outside of the stroke. - pub fn start_outer(&self) -> Point { - self.rotate(Point { - x: -self.stroke_before, - y: self.radius_outer() - self.stroke_after, - }) - } - - /// Start of the corner in the center of the stroke. - pub fn start(&self) -> Point { - self.rotate(Point::with_y(self.radius())) - } - - /// Start of the corner on the inside of the stroke. - pub fn start_inner(&self) -> Point { - self.rotate(Point { - x: self.stroke_before, - y: self.stroke_after + self.radius_inner(), - }) - } - - /// End of the corner on the outside of the stroke. - pub fn end_outer(&self) -> Point { - self.rotate(Point { - x: self.radius_outer() - self.stroke_before, - y: -self.stroke_after, - }) - } - - /// End of the corner in the center of the stroke. - pub fn end(&self) -> Point { - self.rotate(Point::with_x(self.radius())) - } - - /// End of the corner on the inside of the stroke. - pub fn end_inner(&self) -> Point { - self.rotate(Point { - x: self.stroke_before + self.radius_inner(), - y: self.stroke_after, - }) - } -} - -/// Helper to draw arcs with bezier curves. -trait PathExt { - fn arc(&mut self, start: Point, center: Point, end: Point); - fn arc_move(&mut self, start: Point, center: Point, end: Point); - fn arc_line(&mut self, start: Point, center: Point, end: Point); -} - -impl PathExt for Path { - fn arc(&mut self, start: Point, center: Point, end: Point) { - let arc = bezier_arc_control(start, center, end); - self.cubic_to(arc[0], arc[1], end); - } - - fn arc_move(&mut self, start: Point, center: Point, end: Point) { - self.move_to(start); - self.arc(start, center, end); - } - - fn arc_line(&mut self, start: Point, center: Point, end: Point) { - self.line_to(start); - self.arc(start, center, end); - } -} - -/// Get the control points for a bezier curve that approximates a circular arc for -/// a start point, an end point and a center of the circle whose arc connects -/// the two. -fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { - // https://stackoverflow.com/a/44829356/1567835 - let a = start - center; - let b = end - center; - - let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); - let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); - let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) - / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); - - let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); - let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); - - [control_1, control_2] -} diff --git a/crates/typst/src/geom/shape.rs b/crates/typst/src/geom/shape.rs deleted file mode 100644 index 9d4296c8..00000000 --- a/crates/typst/src/geom/shape.rs +++ /dev/null @@ -1,44 +0,0 @@ -use super::*; - -/// 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 border stroke. - pub stroke: Option<FixedStroke>, -} - -/// 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), stroke: None } - } - - /// Stroke the geometry without a fill. - pub fn stroked(self, stroke: FixedStroke) -> Shape { - Shape { geometry: self, fill: None, 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/geom/transform.rs b/crates/typst/src/geom/transform.rs deleted file mode 100644 index 52400f7d..00000000 --- a/crates/typst/src/geom/transform.rs +++ /dev/null @@ -1,126 +0,0 @@ -use super::*; - -/// A scale-skew-translate transformation. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Transform { - pub sx: Ratio, - pub ky: Ratio, - pub kx: Ratio, - pub sy: Ratio, - pub tx: Abs, - pub ty: Abs, -} - -impl Transform { - /// The identity transformation. - pub const fn identity() -> Self { - Self { - sx: Ratio::one(), - ky: Ratio::zero(), - kx: Ratio::zero(), - sy: Ratio::one(), - tx: Abs::zero(), - ty: Abs::zero(), - } - } - - /// A translate transform. - pub const fn translate(tx: Abs, ty: Abs) -> Self { - Self { tx, ty, ..Self::identity() } - } - - /// A scale transform. - pub const fn scale(sx: Ratio, sy: Ratio) -> Self { - Self { sx, sy, ..Self::identity() } - } - - /// A rotate transform. - pub fn rotate(angle: Angle) -> Self { - let cos = Ratio::new(angle.cos()); - let sin = Ratio::new(angle.sin()); - Self { - sx: cos, - ky: sin, - kx: -sin, - sy: cos, - ..Self::default() - } - } - - /// Whether this is the identity transformation. - pub fn is_identity(self) -> bool { - self == Self::identity() - } - - /// Pre-concatenate another transformation. - pub fn pre_concat(self, prev: Self) -> Self { - Transform { - sx: self.sx * prev.sx + self.kx * prev.ky, - ky: self.ky * prev.sx + self.sy * prev.ky, - kx: self.sx * prev.kx + self.kx * prev.sy, - sy: self.ky * prev.kx + self.sy * prev.sy, - tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx, - ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty, - } - } - - /// Post-concatenate another transformation. - pub fn post_concat(self, next: Self) -> Self { - next.pre_concat(self) - } - - /// Inverts the transformation. - /// - /// Returns `None` if the determinant of the matrix is zero. - pub fn invert(self) -> Option<Self> { - // Allow the trivial case to be inlined. - if self.is_identity() { - return Some(self); - } - - // Fast path for scale-translate-only transforms. - if self.kx.is_zero() && self.ky.is_zero() { - if self.sx.is_zero() || self.sy.is_zero() { - return Some(Self::translate(-self.tx, -self.ty)); - } - - let inv_x = 1.0 / self.sx; - let inv_y = 1.0 / self.sy; - return Some(Self { - sx: Ratio::new(inv_x), - ky: Ratio::zero(), - kx: Ratio::zero(), - sy: Ratio::new(inv_y), - tx: -self.tx * inv_x, - ty: -self.ty * inv_y, - }); - } - - let det = self.sx * self.sy - self.kx * self.ky; - if det.get().abs() < 1e-12 { - return None; - } - - let inv_det = 1.0 / det; - Some(Self { - sx: (self.sy * inv_det), - ky: (-self.ky * inv_det), - kx: (-self.kx * inv_det), - sy: (self.sx * inv_det), - tx: Abs::pt( - (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt()) - * inv_det, - ), - ty: Abs::pt( - (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt()) - * inv_det, - ), - }) - } -} - -impl Default for Transform { - fn default() -> Self { - Self::identity() - } -} diff --git a/crates/typst/src/image/mod.rs b/crates/typst/src/image/mod.rs deleted file mode 100644 index 793e2a75..00000000 --- a/crates/typst/src/image/mod.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! Image handling. - -mod raster; -mod svg; - -pub use self::raster::{RasterFormat, RasterImage}; -pub use self::svg::SvgImage; - -use std::fmt::{self, Debug, Formatter}; -use std::sync::Arc; - -use comemo::{Prehashed, Tracked}; -use ecow::EcoString; -use typst_macros::{cast, Cast}; - -use crate::diag::StrResult; -use crate::eval::Bytes; -use crate::World; - -/// A raster or vector image. -/// -/// Values of this type are cheap to clone and hash. -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Image(Arc<Prehashed<Repr>>); - -/// The internal representation. -#[derive(Hash)] -struct Repr { - /// The raw, undecoded image data. - kind: ImageKind, - /// A text describing the image. - alt: Option<EcoString>, -} - -/// A kind of image. -#[derive(Hash)] -pub enum ImageKind { - /// A raster image. - Raster(RasterImage), - /// An SVG image. - Svg(SvgImage), -} - -impl Image { - /// Create an image from a buffer and a format. - #[comemo::memoize] - pub fn new( - data: Bytes, - format: ImageFormat, - alt: Option<EcoString>, - ) -> StrResult<Self> { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::new(data)?) - } - }; - - Ok(Self(Arc::new(Prehashed::new(Repr { kind, alt })))) - } - - /// Create a possibly font-dependant image from a buffer and a format. - #[comemo::memoize] - pub fn with_fonts( - data: Bytes, - format: ImageFormat, - alt: Option<EcoString>, - world: Tracked<dyn World + '_>, - families: &[String], - ) -> StrResult<Self> { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::with_fonts(data, world, families)?) - } - }; - - Ok(Self(Arc::new(Prehashed::new(Repr { kind, alt })))) - } - - /// The raw image data. - pub fn data(&self) -> &Bytes { - match &self.0.kind { - ImageKind::Raster(raster) => raster.data(), - ImageKind::Svg(svg) => svg.data(), - } - } - - /// The format of the image. - pub fn format(&self) -> ImageFormat { - match &self.0.kind { - ImageKind::Raster(raster) => raster.format().into(), - ImageKind::Svg(_) => VectorFormat::Svg.into(), - } - } - - /// The width of the image in pixels. - pub fn width(&self) -> u32 { - match &self.0.kind { - ImageKind::Raster(raster) => raster.width(), - ImageKind::Svg(svg) => svg.width(), - } - } - - /// The height of the image in pixels. - pub fn height(&self) -> u32 { - match &self.0.kind { - ImageKind::Raster(raster) => raster.height(), - ImageKind::Svg(svg) => svg.height(), - } - } - - /// A text describing the image. - pub fn alt(&self) -> Option<&str> { - self.0.alt.as_deref() - } - - /// The decoded image. - pub fn kind(&self) -> &ImageKind { - &self.0.kind - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("format", &self.format()) - .field("width", &self.width()) - .field("height", &self.height()) - .field("alt", &self.alt()) - .finish() - } -} - -/// A raster or vector image format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ImageFormat { - /// A raster graphics format. - Raster(RasterFormat), - /// A vector graphics format. - Vector(VectorFormat), -} - -/// A vector graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum VectorFormat { - /// The vector graphics format of the web. - Svg, -} - -impl From<RasterFormat> for ImageFormat { - fn from(format: RasterFormat) -> Self { - Self::Raster(format) - } -} - -impl From<VectorFormat> for ImageFormat { - fn from(format: VectorFormat) -> Self { - Self::Vector(format) - } -} - -cast! { - ImageFormat, - self => match self { - Self::Raster(v) => v.into_value(), - Self::Vector(v) => v.into_value() - }, - v: RasterFormat => Self::Raster(v), - v: VectorFormat => Self::Vector(v), -} diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst/src/introspection/counter.rs index 0e733efb..0af65563 100644 --- a/crates/typst-library/src/meta/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -1,14 +1,23 @@ +use std::num::NonZeroUsize; use std::str::FromStr; -use ecow::{eco_vec, EcoVec}; +use comemo::{Tracked, TrackedMut}; +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use smallvec::{smallvec, SmallVec}; -use typst::eval::{Repr, Tracer}; -use typst::model::DelayedErrors; -use crate::layout::PageElem; +use crate::diag::{At, DelayedErrors, SourceResult, StrResult}; +use crate::eval::Tracer; +use crate::foundations::{ + cast, elem, func, scope, select_where, ty, Array, Content, Element, Func, IntoValue, + Label, LocatableSelector, NativeElement, Repr, Selector, Show, Str, StyleChain, + Value, +}; +use crate::introspection::{Introspector, Locatable, Location, Locator, Meta}; +use crate::layout::{Frame, FrameItem, PageElem, Vt}; use crate::math::EquationElem; -use crate::meta::{FigureElem, HeadingElem, Numbering, NumberingPattern}; -use crate::prelude::*; +use crate::model::{FigureElem, HeadingElem, Numbering, NumberingPattern}; +use crate::util::NonZeroExt; +use crate::World; /// Counts through pages, elements, and more. /// diff --git a/crates/typst/src/model/introspect.rs b/crates/typst/src/introspection/introspector.rs index 439e316d..80374aca 100644 --- a/crates/typst/src/model/introspect.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -3,127 +3,17 @@ use std::collections::{BTreeSet, HashMap}; use std::hash::Hash; use std::num::NonZeroUsize; -use comemo::{Prehashed, Track, Tracked, Validate}; +use comemo::Prehashed; use ecow::{eco_format, EcoVec}; use indexmap::IndexMap; use crate::diag::{bail, StrResult}; -use crate::doc::{Frame, FrameItem, Meta, Position}; -use crate::eval::{Repr, Value}; -use crate::geom::{Point, Transform}; -use crate::model::{Content, Label, Location, Selector}; +use crate::foundations::{Content, Label, Repr, Selector}; +use crate::introspection::{Location, Meta}; +use crate::layout::{Frame, FrameItem, Point, Position, Transform}; +use crate::model::Numbering; use crate::util::NonZeroExt; -/// Provides locations for elements in the document. -/// -/// A [`Location`] consists of an element's hash plus a disambiguator. Just the -/// hash is not enough because we can have multiple equal elements with the same -/// hash (not a hash collision, just equal elements!). Between these, we -/// disambiguate with an increasing number. In principle, the disambiguator -/// could just be counted up. However, counting is an impure operation and as -/// such we can't count across a memoization boundary. [^1] -/// -/// Instead, we only mutate within a single "layout run" and combine the results -/// with disambiguators from an outer tracked locator. Thus, the locators form a -/// "tracked chain". When a layout run ends, its mutations are discarded and, on -/// the other side of the memoization boundary, we -/// [reconstruct](Self::visit_frame) them from the resulting [frames](Frame). -/// -/// [^1]: Well, we could with [`TrackedMut`](comemo::TrackedMut), but the -/// overhead is quite high, especially since we need to save & undo the counting -/// when only measuring. -#[derive(Default, Clone)] -pub struct Locator<'a> { - /// Maps from a hash to the maximum number we've seen for this hash. This - /// number becomes the `disambiguator`. - hashes: RefCell<HashMap<u128, usize>>, - /// An outer `Locator`, from which we can get disambiguator for hashes - /// outside of the current "layout run". - /// - /// We need to override the constraint's lifetime here so that `Tracked` is - /// covariant over the constraint. If it becomes invariant, we're in for a - /// world of lifetime pain. - outer: Option<Tracked<'a, Self, <Locator<'static> as Validate>::Constraint>>, -} - -impl<'a> Locator<'a> { - /// Create a new locator. - pub fn new() -> Self { - Self::default() - } - - /// Create a new chained locator. - pub fn chained(outer: Tracked<'a, Self>) -> Self { - Self { outer: Some(outer), ..Default::default() } - } - - /// Start tracking this locator. - /// - /// In comparison to [`Track::track`], this method skips this chain link - /// if it does not contribute anything. - pub fn track(&self) -> Tracked<'_, Self> { - match self.outer { - Some(outer) if self.hashes.borrow().is_empty() => outer, - _ => Track::track(self), - } - } - - /// Produce a stable identifier for this call site. - pub fn locate(&mut self, hash: u128) -> Location { - // Get the current disambiguator for this hash. - let disambiguator = self.disambiguator_impl(hash); - - // Bump the next disambiguator up by one. - self.hashes.borrow_mut().insert(hash, disambiguator + 1); - - // Create the location in its default variant. - Location { hash, disambiguator, variant: 0 } - } - - /// Advance past a frame. - pub fn visit_frame(&mut self, frame: &Frame) { - for (_, item) in frame.items() { - match item { - FrameItem::Group(group) => self.visit_frame(&group.frame), - FrameItem::Meta(Meta::Elem(elem), _) => { - let mut hashes = self.hashes.borrow_mut(); - let loc = elem.location().unwrap(); - let entry = hashes.entry(loc.hash).or_default(); - - // Next disambiguator needs to be at least one larger than - // the maximum we've seen so far. - *entry = (*entry).max(loc.disambiguator + 1); - } - _ => {} - } - } - } - - /// Advance past a number of frames. - pub fn visit_frames<'b>(&mut self, frames: impl IntoIterator<Item = &'b Frame>) { - for frame in frames { - self.visit_frame(frame); - } - } - - /// The current disambiguator for the given hash. - fn disambiguator_impl(&self, hash: u128) -> usize { - *self - .hashes - .borrow_mut() - .entry(hash) - .or_insert_with(|| self.outer.map_or(0, |outer| outer.disambiguator(hash))) - } -} - -#[comemo::track] -impl<'a> Locator<'a> { - /// The current disambiguator for the hash. - fn disambiguator(&self, hash: u128) -> usize { - self.disambiguator_impl(hash) - } -} - /// Can be queried for elements and their positions. pub struct Introspector { /// The number of pages in the document. @@ -131,7 +21,7 @@ pub struct Introspector { /// All introspectable elements. elems: IndexMap<Location, (Prehashed<Content>, Position)>, /// The page numberings, indexed by page number minus 1. - page_numberings: Vec<Value>, + page_numberings: Vec<Option<Numbering>>, /// Caches queries done on the introspector. This is important because /// even if all top-level queries are distinct, they often have shared /// subqueries. Example: Individual counter queries with `before` that @@ -324,9 +214,11 @@ impl Introspector { } /// Gets the page numbering for the given location, if any. - pub fn page_numbering(&self, location: Location) -> Value { + pub fn page_numbering(&self, location: Location) -> Option<&Numbering> { let page = self.page(location); - self.page_numberings.get(page.get() - 1).cloned().unwrap_or_default() + self.page_numberings + .get(page.get() - 1) + .and_then(|slot| slot.as_ref()) } /// Find the page number for the given location. diff --git a/crates/typst/src/introspection/locate.rs b/crates/typst/src/introspection/locate.rs new file mode 100644 index 00000000..1c4f0e5a --- /dev/null +++ b/crates/typst/src/introspection/locate.rs @@ -0,0 +1,47 @@ +use crate::diag::SourceResult; +use crate::foundations::{elem, func, Content, Func, NativeElement, Show, StyleChain}; +use crate::introspection::Locatable; +use crate::layout::Vt; + +/// Provides access to the location of content. +/// +/// This is useful in combination with [queries]($query), [counters]($counter), +/// [state]($state), and [links]($link). See their documentation for more +/// details. +/// +/// ```example +/// #locate(loc => [ +/// My location: \ +/// #loc.position()! +/// ]) +/// ``` +#[func] +pub fn locate( + /// A function that receives a [`location`]($location). Its return value is + /// displayed in the document. + /// + /// This function is called once for each time the content returned by + /// `locate` appears in the document. That makes it possible to generate + /// content that depends on its own location in the document. + func: Func, +) -> Content { + LocateElem::new(func).pack() +} + +/// Executes a `locate` call. +#[elem(Locatable, Show)] +struct LocateElem { + /// The function to call with the location. + #[required] + func: Func, +} + +impl Show for LocateElem { + #[tracing::instrument(name = "LocateElem::show", skip(self, vt))] + fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(vt.delayed(|vt| { + let location = self.location().unwrap(); + Ok(self.func().call_vt(vt, [location])?.display()) + })) + } +} diff --git a/crates/typst/src/model/location.rs b/crates/typst/src/introspection/location.rs index 8303a5f0..b70dc4ad 100644 --- a/crates/typst/src/model/location.rs +++ b/crates/typst/src/introspection/location.rs @@ -2,7 +2,9 @@ use std::num::NonZeroUsize; use ecow::EcoString; -use crate::eval::{cast, func, scope, ty, Dict, Repr, Value, Vm}; +use crate::eval::Vm; +use crate::foundations::{cast, func, scope, ty, Dict, Repr}; +use crate::model::Numbering; /// Identifies an element in the document. /// @@ -66,8 +68,8 @@ impl Location { /// If the page numbering is set to `none` at that location, this function /// returns `none`. #[func] - pub fn page_numbering(self, vm: &mut Vm) -> Value { - vm.vt.introspector.page_numbering(self) + pub fn page_numbering(self, vm: &mut Vm) -> Option<Numbering> { + vm.vt.introspector.page_numbering(self).cloned() } } @@ -80,3 +82,6 @@ impl Repr for Location { cast! { type Location, } + +/// Makes this element locatable through `vt.locate`. +pub trait Locatable {} diff --git a/crates/typst/src/introspection/locator.rs b/crates/typst/src/introspection/locator.rs new file mode 100644 index 00000000..c9c34b9c --- /dev/null +++ b/crates/typst/src/introspection/locator.rs @@ -0,0 +1,117 @@ +use std::cell::RefCell; +use std::collections::HashMap; + +use comemo::{Track, Tracked, Validate}; + +use crate::introspection::{Location, Meta}; +use crate::layout::{Frame, FrameItem}; + +/// Provides locations for elements in the document. +/// +/// A [`Location`] consists of an element's hash plus a disambiguator. Just the +/// hash is not enough because we can have multiple equal elements with the same +/// hash (not a hash collision, just equal elements!). Between these, we +/// disambiguate with an increasing number. In principle, the disambiguator +/// could just be counted up. However, counting is an impure operation and as +/// such we can't count across a memoization boundary. [^1] +/// +/// Instead, we only mutate within a single "layout run" and combine the results +/// with disambiguators from an outer tracked locator. Thus, the locators form a +/// "tracked chain". When a layout run ends, its mutations are discarded and, on +/// the other side of the memoization boundary, we +/// [reconstruct](Self::visit_frame) them from the resulting [frames](Frame). +/// +/// [^1]: Well, we could with [`TrackedMut`](comemo::TrackedMut), but the +/// overhead is quite high, especially since we need to save & undo the counting +/// when only measuring. +#[derive(Default, Clone)] +pub struct Locator<'a> { + /// Maps from a hash to the maximum number we've seen for this hash. This + /// number becomes the `disambiguator`. + hashes: RefCell<HashMap<u128, usize>>, + /// An outer `Locator`, from which we can get disambiguator for hashes + /// outside of the current "layout run". + /// + /// We need to override the constraint's lifetime here so that `Tracked` is + /// covariant over the constraint. If it becomes invariant, we're in for a + /// world of lifetime pain. + outer: Option<Tracked<'a, Self, <Locator<'static> as Validate>::Constraint>>, +} + +impl<'a> Locator<'a> { + /// Create a new locator. + pub fn new() -> Self { + Self::default() + } + + /// Create a new chained locator. + pub fn chained(outer: Tracked<'a, Self>) -> Self { + Self { outer: Some(outer), ..Default::default() } + } + + /// Start tracking this locator. + /// + /// In comparison to [`Track::track`], this method skips this chain link + /// if it does not contribute anything. + pub fn track(&self) -> Tracked<'_, Self> { + match self.outer { + Some(outer) if self.hashes.borrow().is_empty() => outer, + _ => Track::track(self), + } + } + + /// Produce a stable identifier for this call site. + pub fn locate(&mut self, hash: u128) -> Location { + // Get the current disambiguator for this hash. + let disambiguator = self.disambiguator_impl(hash); + + // Bump the next disambiguator up by one. + self.hashes.borrow_mut().insert(hash, disambiguator + 1); + + // Create the location in its default variant. + Location { hash, disambiguator, variant: 0 } + } + + /// Advance past a frame. + pub fn visit_frame(&mut self, frame: &Frame) { + for (_, item) in frame.items() { + match item { + FrameItem::Group(group) => self.visit_frame(&group.frame), + FrameItem::Meta(Meta::Elem(elem), _) => { + let mut hashes = self.hashes.borrow_mut(); + let loc = elem.location().unwrap(); + let entry = hashes.entry(loc.hash).or_default(); + + // Next disambiguator needs to be at least one larger than + // the maximum we've seen so far. + *entry = (*entry).max(loc.disambiguator + 1); + } + _ => {} + } + } + } + + /// Advance past a number of frames. + pub fn visit_frames<'b>(&mut self, frames: impl IntoIterator<Item = &'b Frame>) { + for frame in frames { + self.visit_frame(frame); + } + } + + /// The current disambiguator for the given hash. + fn disambiguator_impl(&self, hash: u128) -> usize { + *self + .hashes + .borrow_mut() + .entry(hash) + .or_insert_with(|| self.outer.map_or(0, |outer| outer.disambiguator(hash))) + } +} + +#[comemo::track] +impl<'a> Locator<'a> { + /// The current disambiguator for the hash. + fn disambiguator(&self, hash: u128) -> usize { + self.disambiguator_impl(hash) + } +} diff --git a/crates/typst-library/src/meta/metadata.rs b/crates/typst/src/introspection/metadata.rs index b4ae64cb..4042bc04 100644 --- a/crates/typst-library/src/meta/metadata.rs +++ b/crates/typst/src/introspection/metadata.rs @@ -1,4 +1,7 @@ -use crate::prelude::*; +use crate::diag::SourceResult; +use crate::foundations::{elem, Behave, Behaviour, Content, Show, StyleChain, Value}; +use crate::introspection::Locatable; +use crate::layout::Vt; /// Exposes a value to the query system without producing visible content. /// diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst/src/introspection/mod.rs new file mode 100644 index 00000000..49a1c53c --- /dev/null +++ b/crates/typst/src/introspection/mod.rs @@ -0,0 +1,109 @@ +//! Interaction between document parts. + +mod counter; +mod introspector; +#[path = "locate.rs"] +mod locate_; +mod location; +mod locator; +mod metadata; +#[path = "query.rs"] +mod query_; +mod state; + +pub use self::counter::*; +pub use self::introspector::*; +pub use self::locate_::*; +pub use self::location::*; +pub use self::locator::*; +pub use self::metadata::*; +pub use self::query_::*; +pub use self::state::*; + +use std::fmt::{self, Debug, Formatter}; + +use ecow::{eco_format, EcoString}; +use smallvec::SmallVec; + +use crate::foundations::{ + cast, category, elem, ty, Behave, Behaviour, Category, Content, Repr, Scope, +}; +use crate::layout::PdfPageLabel; +use crate::model::{Destination, Numbering}; + +/// Interactions between document parts. +/// +/// This category is home to Typst's introspection capabilities: With the +/// `counter` function, you can access and manipulate page, section, figure, and +/// equation counters or create custom ones. Meanwhile, the `query` function +/// lets you search for elements in the document to construct things like a list +/// of figures or headers which show the current chapter title. +#[category] +pub static INTROSPECTION: Category; + +/// Hook up all `introspection` definitions. +pub fn define(global: &mut Scope) { + global.category(INTROSPECTION); + global.define_type::<Location>(); + global.define_type::<Counter>(); + global.define_type::<State>(); + global.define_elem::<MetadataElem>(); + global.define_func::<locate>(); + global.define_func::<query>(); +} + +/// Hosts metadata and ensures metadata is produced even for empty elements. +#[elem(Behave)] +pub struct MetaElem { + /// Metadata that should be attached to all elements affected by this style + /// property. + #[fold] + pub data: SmallVec<[Meta; 1]>, +} + +impl Behave for MetaElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Invisible + } +} + +/// Meta information that isn't visible or renderable. +#[ty] +#[derive(Clone, PartialEq, Hash)] +pub enum Meta { + /// An internal or external link to a destination. + Link(Destination), + /// An identifiable element that produces something within the area this + /// metadata is attached to. + Elem(Content), + /// The numbering of the current page. + PageNumbering(Option<Numbering>), + /// A PDF page label of the current page. + PdfPageLabel(PdfPageLabel), + /// Indicates that content should be hidden. This variant doesn't appear + /// in the final frames as it is removed alongside the content that should + /// be hidden. + Hide, +} + +cast! { + type Meta, +} + +impl Debug for Meta { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Link(dest) => write!(f, "Link({dest:?})"), + Self::Elem(content) => write!(f, "Elem({:?})", content.func()), + Self::PageNumbering(value) => write!(f, "PageNumbering({value:?})"), + Self::PdfPageLabel(label) => write!(f, "PdfPageLabel({label:?})"), + Self::Hide => f.pad("Hide"), + } + } +} + +impl Repr for Meta { + fn repr(&self) -> EcoString { + eco_format!("{self:?}") + } +} diff --git a/crates/typst-library/src/meta/query.rs b/crates/typst/src/introspection/query.rs index d6c600d7..1024238e 100644 --- a/crates/typst-library/src/meta/query.rs +++ b/crates/typst/src/introspection/query.rs @@ -1,4 +1,6 @@ -use crate::prelude::*; +use crate::eval::Vm; +use crate::foundations::{func, Array, LocatableSelector, Value}; +use crate::introspection::Location; /// Finds elements in the document. /// diff --git a/crates/typst-library/src/meta/state.rs b/crates/typst/src/introspection/state.rs index 4f04628f..4b559b3e 100644 --- a/crates/typst-library/src/meta/state.rs +++ b/crates/typst/src/introspection/state.rs @@ -1,10 +1,15 @@ -use std::fmt::Debug; +use comemo::{Tracked, TrackedMut}; +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; -use ecow::{eco_vec, EcoVec}; -use typst::eval::{Repr, Tracer}; -use typst::model::DelayedErrors; - -use crate::prelude::*; +use crate::diag::{DelayedErrors, SourceResult}; +use crate::eval::Tracer; +use crate::foundations::{ + cast, elem, func, scope, select_where, ty, Content, Func, NativeElement, Repr, + Selector, Show, Str, StyleChain, Value, +}; +use crate::introspection::{Introspector, Locatable, Location, Locator}; +use crate::layout::Vt; +use crate::World; /// Manages stateful parts of your document. /// diff --git a/crates/typst/src/geom/abs.rs b/crates/typst/src/layout/abs.rs index 2a57e368..92b4bf2b 100644 --- a/crates/typst/src/geom/abs.rs +++ b/crates/typst/src/layout/abs.rs @@ -1,4 +1,11 @@ -use super::*; +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg, Rem}; + +use ecow::EcoString; + +use crate::foundations::{cast, repr, Repr, Value}; +use crate::util::{Numeric, Scalar}; /// An absolute length. #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -141,7 +148,7 @@ impl Debug for Abs { impl Repr for Abs { fn repr(&self) -> EcoString { - format_float(self.to_pt(), Some(2), "pt") + repr::format_float(self.to_pt(), Some(2), "pt") } } diff --git a/crates/typst/src/geom/align.rs b/crates/typst/src/layout/align.rs index 59e608b1..74db4373 100644 --- a/crates/typst/src/geom/align.rs +++ b/crates/typst/src/layout/align.rs @@ -1,4 +1,58 @@ -use super::*; +use std::ops::Add; + +use ecow::{eco_format, EcoString}; + +use crate::diag::{bail, SourceResult, StrResult}; +use crate::foundations::{ + cast, elem, func, scope, ty, Content, Fold, Repr, Resolve, Show, StyleChain, +}; +use crate::layout::{Abs, Axes, Axis, Dir, Side, Vt}; +use crate::text::TextElem; + +/// Aligns content horizontally and vertically. +/// +/// # Example +/// ```example +/// #set align(center) +/// +/// Centered text, a sight to see \ +/// In perfect balance, visually \ +/// Not left nor right, it stands alone \ +/// A work of art, a visual throne +/// ``` +#[elem(Show)] +pub struct AlignElem { + /// The [alignment]($alignment) along both axes. + /// + /// ```example + /// #set page(height: 6cm) + /// #set text(lang: "ar") + /// + /// مثال + /// #align( + /// end + horizon, + /// rect(inset: 12pt)[ركن] + /// ) + /// ``` + #[positional] + #[fold] + #[default] + pub alignment: Align, + + /// The content to align. + #[required] + pub body: Content, +} + +impl Show for AlignElem { + #[tracing::instrument(name = "AlignElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self + .body() + .clone() + .styled(Self::set_alignment(self.alignment(styles)))) + } +} /// Where to [align]($align) something along an axis. /// @@ -175,7 +229,7 @@ impl Resolve for Align { type Output = Axes<FixedAlign>; fn resolve(self, styles: StyleChain) -> Self::Output { - self.fix(item!(dir)(styles)) + self.fix(TextElem::dir_in(styles)) } } @@ -259,7 +313,7 @@ impl Resolve for HAlign { type Output = FixedAlign; fn resolve(self, styles: StyleChain) -> Self::Output { - self.fix(item!(dir)(styles)) + self.fix(TextElem::dir_in(styles)) } } diff --git a/crates/typst/src/geom/angle.rs b/crates/typst/src/layout/angle.rs index b2f29b75..4120cbc1 100644 --- a/crates/typst/src/geom/angle.rs +++ b/crates/typst/src/layout/angle.rs @@ -1,4 +1,12 @@ -use super::*; +use std::f64::consts::PI; +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; + +use crate::foundations::{func, repr, scope, ty, Repr}; +use crate::util::{Numeric, Scalar}; /// An angle describing a rotation. /// @@ -127,7 +135,7 @@ impl Debug for Angle { impl Repr for Angle { fn repr(&self) -> EcoString { - format_float(self.to_deg(), Some(2), "deg") + repr::format_float(self.to_deg(), Some(2), "deg") } } diff --git a/crates/typst/src/geom/axes.rs b/crates/typst/src/layout/axes.rs index 2439182a..585e6698 100644 --- a/crates/typst/src/geom/axes.rs +++ b/crates/typst/src/layout/axes.rs @@ -1,7 +1,11 @@ use std::any::Any; -use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not}; -use super::*; +use crate::diag::bail; +use crate::foundations::{array, cast, Array, Fold, Resolve, Smart, StyleChain}; +use crate::layout::{Abs, Dir, Length, Ratio, Rel}; +use crate::util::Get; /// A container with a horizontal and vertical component. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst/src/layout/columns.rs index bf111506..852ef8f8 100644 --- a/crates/typst-library/src/layout/columns.rs +++ b/crates/typst/src/layout/columns.rs @@ -1,5 +1,12 @@ -use crate::prelude::*; +use std::num::NonZeroUsize; + +use crate::diag::SourceResult; +use crate::foundations::{elem, Behave, Behaviour, Content, StyleChain}; +use crate::layout::{ + Abs, Axes, Dir, Fragment, Frame, Layout, Length, Point, Ratio, Regions, Rel, Size, Vt, +}; use crate::text::TextElem; +use crate::util::Numeric; /// Separates a region into multiple equally sized columns. /// diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst/src/layout/container.rs index 9268f8df..cb65d4d8 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst/src/layout/container.rs @@ -1,7 +1,13 @@ -use typst::eval::AutoValue; - -use crate::layout::{Spacing, VElem}; -use crate::prelude::*; +use crate::diag::SourceResult; +use crate::foundations::{ + cast, elem, AutoValue, Content, NativeElement, Resolve, Smart, StyleChain, Value, +}; +use crate::layout::{ + Abs, Axes, Corners, Em, Fr, Fragment, FrameKind, Layout, Length, Ratio, Regions, Rel, + Sides, Size, Spacing, VElem, Vt, +}; +use crate::util::Numeric; +use crate::visualize::{clip_rect, Paint, Stroke}; /// An inline-level container that sizes content. /// diff --git a/crates/typst/src/geom/corners.rs b/crates/typst/src/layout/corners.rs index e0b45314..e014201c 100644 --- a/crates/typst/src/geom/corners.rs +++ b/crates/typst/src/layout/corners.rs @@ -1,5 +1,11 @@ -use super::*; -use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; +use std::fmt::{self, Debug, Formatter}; + +use crate::diag::StrResult; +use crate::foundations::{ + CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value, +}; +use crate::layout::Side; +use crate::util::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/geom/dir.rs b/crates/typst/src/layout/dir.rs index dc622d3a..569c1880 100644 --- a/crates/typst/src/geom/dir.rs +++ b/crates/typst/src/layout/dir.rs @@ -1,4 +1,7 @@ -use super::*; +use ecow::EcoString; + +use crate::foundations::{cast, func, scope, ty, Repr}; +use crate::layout::{Axis, Side}; /// The four directions into which content can be laid out. /// diff --git a/crates/typst/src/geom/em.rs b/crates/typst/src/layout/em.rs index 81c4a350..b3b416f9 100644 --- a/crates/typst/src/geom/em.rs +++ b/crates/typst/src/layout/em.rs @@ -1,4 +1,13 @@ -use super::*; +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; + +use crate::foundations::{cast, repr, Repr, Resolve, StyleChain, Value}; +use crate::layout::Abs; +use crate::text::TextElem; +use crate::util::{Numeric, Scalar}; /// A length that is relative to the font size. /// @@ -76,7 +85,7 @@ impl Debug for Em { impl Repr for Em { fn repr(&self) -> EcoString { - format_float(self.get(), None, "em") + repr::format_float(self.get(), None, "em") } } @@ -153,7 +162,7 @@ impl Resolve for Em { if self.is_zero() { Abs::zero() } else { - self.at(item!(em)(styles)) + self.at(TextElem::size_in(styles)) } } } diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index 17a39254..1c97c110 100644 --- a/crates/typst-library/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -1,12 +1,15 @@ -use std::mem; - use comemo::Prehashed; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{elem, Content, NativeElement, Resolve, Smart, StyleChain}; +use crate::introspection::{Meta, MetaElem}; use crate::layout::{ - AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem, + Abs, AlignElem, Axes, BlockElem, ColbreakElem, ColumnsElem, FixedAlign, Fr, Fragment, + Frame, FrameItem, Layout, PlaceElem, Point, Regions, Rel, Size, Spacing, VAlign, + VElem, Vt, }; -use crate::meta::{FootnoteElem, FootnoteEntry}; -use crate::prelude::*; +use crate::model::{FootnoteElem, FootnoteEntry, ParElem}; +use crate::util::Numeric; use crate::visualize::{ CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, SquareElem, @@ -32,10 +35,10 @@ impl Layout for FlowElem { regions: Regions, ) -> SourceResult<Fragment> { if !regions.size.x.is_finite() && regions.expand.x { - bail!(error!(self.span(), "cannot expand into infinite width")); + bail!(self.span(), "cannot expand into infinite width"); } if !regions.size.y.is_finite() && regions.expand.y { - bail!(error!(self.span(), "cannot expand into infinite height")); + bail!(self.span(), "cannot expand into infinite height"); } let mut layouter = FlowLayouter::new(regions, styles); @@ -165,7 +168,7 @@ impl<'a> FlowLayouter<'a> { // Disable vertical expansion & root for children. regions.expand.y = false; - let root = mem::replace(&mut regions.root, false); + let root = std::mem::replace(&mut regions.root, false); Self { root, @@ -563,7 +566,7 @@ impl<'a> FlowLayouter<'a> { self.has_footnotes = false; // Try to place floats. - for item in mem::take(&mut self.pending_floats) { + for item in std::mem::take(&mut self.pending_floats) { self.layout_item(vt, item)?; } diff --git a/crates/typst/src/geom/fr.rs b/crates/typst/src/layout/fr.rs index fe4fed49..8be70582 100644 --- a/crates/typst/src/geom/fr.rs +++ b/crates/typst/src/layout/fr.rs @@ -1,4 +1,12 @@ -use super::*; +use std::fmt::{self, Debug, Formatter}; +use std::iter::Sum; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; + +use crate::foundations::{repr, ty, Repr}; +use crate::layout::Abs; +use crate::util::{Numeric, Scalar}; /// Defines how the the remaining space in a layout is distributed. /// @@ -71,7 +79,7 @@ impl Debug for Fr { impl Repr for Fr { fn repr(&self) -> EcoString { - format_float(self.get(), Some(2), "fr") + repr::format_float(self.get(), Some(2), "fr") } } diff --git a/crates/typst-library/src/layout/fragment.rs b/crates/typst/src/layout/fragment.rs index 3550df2a..ce8f17d1 100644 --- a/crates/typst-library/src/layout/fragment.rs +++ b/crates/typst/src/layout/fragment.rs @@ -1,4 +1,6 @@ -use crate::prelude::*; +use std::fmt::{self, Debug, Formatter}; + +use crate::layout::Frame; /// A partial layout result. #[derive(Clone)] diff --git a/crates/typst/src/doc.rs b/crates/typst/src/layout/frame.rs index 854219e4..3abc8623 100644 --- a/crates/typst/src/doc.rs +++ b/crates/typst/src/layout/frame.rs @@ -2,36 +2,21 @@ use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; -use std::ops::Range; -use std::str::FromStr; use std::sync::Arc; use ecow::{eco_format, EcoString}; -use crate::eval::{cast, dict, ty, Datetime, Dict, Repr, Smart, Value}; -use crate::font::Font; -use crate::geom::{ - self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke, - Geometry, Length, Numeric, Paint, Path, Point, Rel, Shape, Sides, Size, Transform, +use crate::foundations::{cast, dict, Dict, Repr, StyleChain, Value}; +use crate::introspection::{Meta, MetaElem}; +use crate::layout::{ + Abs, Axes, Corners, FixedAlign, Length, Point, Rel, Sides, Size, Transform, }; -use crate::image::Image; -use crate::model::{Content, Location, MetaElem, StyleChain}; use crate::syntax::Span; - -/// A finished document with metadata and page frames. -#[derive(Debug, Default, Clone, Hash)] -pub struct Document { - /// The page frames. - pub pages: Vec<Frame>, - /// The document's title. - pub title: Option<EcoString>, - /// The document's author. - pub author: Vec<EcoString>, - /// The document's keywords. - pub keywords: Vec<EcoString>, - /// The document's creation date. - pub date: Smart<Option<Datetime>>, -} +use crate::text::TextItem; +use crate::util::Numeric; +use crate::visualize::{ + ellipse, styled_rect, Color, FixedStroke, Geometry, Image, Paint, Path, Shape, +}; /// A finished layout with items at fixed positions. #[derive(Default, Clone, Hash)] @@ -415,7 +400,7 @@ impl Frame { self.push( pos - Point::splat(radius), FrameItem::Shape( - geom::ellipse(Size::splat(2.0 * radius), Some(Color::GREEN.into()), None), + ellipse(Size::splat(2.0 * radius), Some(Color::GREEN.into()), None), Span::detached(), ), ); @@ -531,278 +516,6 @@ impl Debug for GroupItem { } } -/// A run of shaped text. -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct TextItem { - /// The font the glyphs are contained in. - pub font: Font, - /// The font size. - pub size: Abs, - /// Glyph color. - pub fill: Paint, - /// The natural language of the text. - pub lang: Lang, - /// The item's plain text. - pub text: EcoString, - /// The glyphs. - pub glyphs: Vec<Glyph>, -} - -impl TextItem { - /// The width of the text run. - pub fn width(&self) -> Abs { - self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size) - } -} - -impl Debug for TextItem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write_str("Text(")?; - self.text.fmt(f)?; - f.write_str(")") - } -} - -/// A glyph in a run of shaped text. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Glyph { - /// The glyph's index in the font. - pub id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The range of the glyph in its item's text. - pub range: Range<u16>, - /// The source code location of the text. - pub span: (Span, u16), -} - -impl Glyph { - /// The range of the glyph in its item's text. - pub fn range(&self) -> Range<usize> { - usize::from(self.range.start)..usize::from(self.range.end) - } -} - -/// An ISO 15924-type script identifier -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct WritingScript([u8; 4], u8); - -impl WritingScript { - /// Return the script as an all lowercase string slice. - pub fn as_str(&self) -> &str { - std::str::from_utf8(&self.0[..usize::from(self.1)]).unwrap_or_default() - } - - /// Return the description of the script as raw bytes. - pub fn as_bytes(&self) -> &[u8; 4] { - &self.0 - } -} - -impl FromStr for WritingScript { - type Err = &'static str; - - /// Construct a region from its ISO 15924 code. - fn from_str(iso: &str) -> Result<Self, Self::Err> { - let len = iso.len(); - if matches!(len, 3..=4) && iso.is_ascii() { - let mut bytes = [b' '; 4]; - bytes[..len].copy_from_slice(iso.as_bytes()); - bytes.make_ascii_lowercase(); - Ok(Self(bytes, len as u8)) - } else { - Err("expected three or four letter script code (ISO 15924 or 'math')") - } - } -} - -cast! { - WritingScript, - self => self.as_str().into_value(), - string: EcoString => Self::from_str(&string)?, -} - -/// An identifier for a natural language. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Lang([u8; 3], u8); - -impl Lang { - pub const ALBANIAN: Self = Self(*b"sq ", 2); - pub const ARABIC: Self = Self(*b"ar ", 2); - pub const BOKMÅL: Self = Self(*b"nb ", 2); - pub const CHINESE: Self = Self(*b"zh ", 2); - pub const CZECH: Self = Self(*b"cs ", 2); - pub const DANISH: Self = Self(*b"da ", 2); - pub const DUTCH: Self = Self(*b"nl ", 2); - pub const ENGLISH: Self = Self(*b"en ", 2); - pub const FILIPINO: Self = Self(*b"tl ", 2); - pub const FINNISH: Self = Self(*b"fi ", 2); - pub const FRENCH: Self = Self(*b"fr ", 2); - pub const GERMAN: Self = Self(*b"de ", 2); - pub const GREEK: Self = Self(*b"gr ", 2); - pub const ITALIAN: Self = Self(*b"it ", 2); - pub const JAPANESE: Self = Self(*b"ja ", 2); - pub const NYNORSK: Self = Self(*b"nn ", 2); - pub const POLISH: Self = Self(*b"pl ", 2); - pub const PORTUGUESE: Self = Self(*b"pt ", 2); - pub const RUSSIAN: Self = Self(*b"ru ", 2); - pub const SLOVENIAN: Self = Self(*b"sl ", 2); - pub const SPANISH: Self = Self(*b"es ", 2); - pub const SWEDISH: Self = Self(*b"sv ", 2); - pub const TURKISH: Self = Self(*b"tr ", 2); - pub const UKRAINIAN: Self = Self(*b"ua ", 2); - pub const VIETNAMESE: Self = Self(*b"vi ", 2); - pub const HUNGARIAN: Self = Self(*b"hu ", 2); - pub const ROMANIAN: Self = Self(*b"ro ", 2); - - /// Return the language code as an all lowercase string slice. - pub fn as_str(&self) -> &str { - std::str::from_utf8(&self.0[..usize::from(self.1)]).unwrap_or_default() - } - - /// The default direction for the language. - pub fn dir(self) -> Dir { - match self.as_str() { - "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur" - | "yi" => Dir::RTL, - _ => Dir::LTR, - } - } -} - -impl FromStr for Lang { - type Err = &'static str; - - /// Construct a language from a two- or three-byte ISO 639-1/2/3 code. - fn from_str(iso: &str) -> Result<Self, Self::Err> { - let len = iso.len(); - if matches!(len, 2..=3) && iso.is_ascii() { - let mut bytes = [b' '; 3]; - bytes[..len].copy_from_slice(iso.as_bytes()); - bytes.make_ascii_lowercase(); - Ok(Self(bytes, len as u8)) - } else { - Err("expected two or three letter language code (ISO 639-1/2/3)") - } - } -} - -cast! { - Lang, - self => self.as_str().into_value(), - string: EcoString => Self::from_str(&string)?, -} - -/// An identifier for a region somewhere in the world. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Region([u8; 2]); - -impl Region { - /// Return the region code as an all uppercase string slice. - pub fn as_str(&self) -> &str { - std::str::from_utf8(&self.0).unwrap_or_default() - } -} - -impl PartialEq<&str> for Region { - fn eq(&self, other: &&str) -> bool { - self.as_str() == *other - } -} - -impl FromStr for Region { - type Err = &'static str; - - /// Construct a region from its two-byte ISO 3166-1 alpha-2 code. - fn from_str(iso: &str) -> Result<Self, Self::Err> { - if iso.len() == 2 && iso.is_ascii() { - let mut bytes: [u8; 2] = iso.as_bytes().try_into().unwrap(); - bytes.make_ascii_uppercase(); - Ok(Self(bytes)) - } else { - Err("expected two letter region code (ISO 3166-1 alpha-2)") - } - } -} - -cast! { - Region, - self => self.as_str().into_value(), - string: EcoString => Self::from_str(&string)?, -} - -/// Meta information that isn't visible or renderable. -#[ty] -#[derive(Clone, PartialEq, Hash)] -pub enum Meta { - /// An internal or external link to a destination. - Link(Destination), - /// An identifiable element that produces something within the area this - /// metadata is attached to. - Elem(Content), - /// The numbering of the current page. - PageNumbering(Value), - /// A PDF page label of the current page. - PdfPageLabel(PdfPageLabel), - /// Indicates that content should be hidden. This variant doesn't appear - /// in the final frames as it is removed alongside the content that should - /// be hidden. - Hide, -} - -cast! { - type Meta, -} - -impl Debug for Meta { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Link(dest) => write!(f, "Link({dest:?})"), - Self::Elem(content) => write!(f, "Elem({:?})", content.func()), - Self::PageNumbering(value) => write!(f, "PageNumbering({value:?})"), - Self::PdfPageLabel(label) => write!(f, "PdfPageLabel({label:?})"), - Self::Hide => f.pad("Hide"), - } - } -} - -impl Repr for Meta { - fn repr(&self) -> EcoString { - eco_format!("{self:?}") - } -} - -/// A link destination. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum Destination { - /// A link to a URL. - Url(EcoString), - /// A link to a point on a page. - Position(Position), - /// An unresolved link to a location in the document. - Location(Location), -} - -impl Repr for Destination { - fn repr(&self) -> EcoString { - eco_format!("{self:?}") - } -} - -cast! { - Destination, - self => match self { - Self::Url(v) => v.into_value(), - Self::Position(v) => v.into_value(), - Self::Location(v) => v.into_value(), - }, - v: EcoString => Self::Url(v), - v: Position => Self::Position(v), - v: Location => Self::Location(v), -} - /// A physical position in a document. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Position { @@ -872,22 +585,3 @@ pub enum PdfPageLabelStyle { /// `AA` to `ZZ` and so on for the next). UpperAlpha, } - -#[cfg(test)] -mod tests { - use super::*; - use crate::util::option_eq; - - #[test] - fn test_region_option_eq() { - let region = Some(Region([b'U', b'S'])); - assert!(option_eq(region, "US")); - assert!(!option_eq(region, "AB")); - } - - #[test] - fn test_document_is_send() { - fn ensure_send<T: Send>() {} - ensure_send::<Document>(); - } -} diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst/src/layout/grid.rs index 1cde6564..cc7d1d25 100644 --- a/crates/typst-library/src/layout/grid.rs +++ b/crates/typst/src/layout/grid.rs @@ -1,8 +1,18 @@ +use std::num::NonZeroUsize; + use smallvec::{smallvec, SmallVec}; -use crate::layout::Sizing; -use crate::prelude::*; +use crate::diag::{bail, SourceResult, StrResult}; +use crate::foundations::{ + cast, elem, Array, Content, NativeElement, Resolve, StyleChain, Value, +}; +use crate::layout::{ + Abs, Axes, Dir, Fr, Fragment, Frame, Layout, Length, Point, Regions, Rel, Size, + Sizing, Vt, +}; +use crate::syntax::Span; use crate::text::TextElem; +use crate::util::Numeric; /// Arranges content in a grid. /// @@ -589,7 +599,7 @@ impl<'a> GridLayouter<'a> { y: usize, ) -> SourceResult<Frame> { if !height.is_finite() { - bail!(error!(self.span, "cannot create grid with infinite height")); + bail!(self.span, "cannot create grid with infinite height"); } let mut output = Frame::soft(Size::new(self.width, height)); diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst/src/layout/hide.rs index af3d0631..b72f2ad2 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst/src/layout/hide.rs @@ -1,6 +1,9 @@ use smallvec::smallvec; -use crate::prelude::*; +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, Show, StyleChain}; +use crate::introspection::{Meta, MetaElem}; +use crate::layout::Vt; /// Hides content without affecting layout. /// diff --git a/crates/typst-library/src/text/linebreak.rs b/crates/typst/src/layout/inline/linebreak.rs index 8a69e0a8..1f00dc20 100644 --- a/crates/typst-library/src/text/linebreak.rs +++ b/crates/typst/src/layout/inline/linebreak.rs @@ -5,11 +5,10 @@ use icu_provider_adapters::fork::ForkByKeyProvider; use icu_provider_blob::BlobDataProvider; use icu_segmenter::LineSegmenter; use once_cell::sync::Lazy; -use typst::doc::Lang; -use typst::syntax::link_prefix; -use crate::layout::Preparation; -use crate::text::TextElem; +use super::Preparation; +use crate::syntax::link_prefix; +use crate::text::{Lang, TextElem}; /// Generated by the following command: /// @@ -22,7 +21,7 @@ use crate::text::TextElem; /// ``` /// /// Install icu_datagen with `cargo install icu_datagen`. -static ICU_DATA: &[u8] = include_bytes!("../../assets/icudata.postcard"); +static ICU_DATA: &[u8] = include_bytes!("../../../assets/icudata.postcard"); /// Generated by the following command: /// @@ -37,7 +36,7 @@ static ICU_DATA: &[u8] = include_bytes!("../../assets/icudata.postcard"); /// The used icu_datagen should be patched by /// https://github.com/peng1999/icu4x/commit/b9beb6cbf633d61fc3d7983e5baf7f4449fbfae5 static CJ_LINEBREAK_DATA: &[u8] = - include_bytes!("../../assets/cj_linebreak_data.postcard"); + include_bytes!("../../../assets/cj_linebreak_data.postcard"); /// The general line break segmenter. static SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| { @@ -62,7 +61,7 @@ static LINEBREAK_DATA: Lazy<CodePointMapData<LineBreak>> = Lazy::new(|| { /// A line break opportunity. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub(crate) enum Breakpoint { +pub(super) enum Breakpoint { /// Just a normal opportunity (e.g. after a space). Normal, /// A mandatory breakpoint (after '\n' or at the end of the text). @@ -80,7 +79,7 @@ pub(crate) enum Breakpoint { /// This is an internal instead of an external iterator because it makes the /// code much simpler and the consumers of this function don't need the /// composability and flexibility of external iteration anyway. -pub(crate) fn breakpoints<'a>( +pub(super) fn breakpoints<'a>( p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint), ) { diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst/src/layout/inline/mod.rs index 2d7c9080..35d332d6 100644 --- a/crates/typst-library/src/layout/par.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -1,235 +1,96 @@ -use comemo::Prehashed; -use typst::eval::Tracer; -use typst::model::DelayedErrors; +mod linebreak; +mod shaping; + +use comemo::{Prehashed, Tracked, TrackedMut}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; -use crate::layout::{AlignElem, BoxElem, HElem, Sizing, Spacing}; +use self::linebreak::{breakpoints, Breakpoint}; +use self::shaping::{ + is_gb_style, is_of_cjk_script, shape, ShapedGlyph, ShapedText, BEGIN_PUNCT_PAT, + END_PUNCT_PAT, +}; +use crate::diag::{bail, DelayedErrors, SourceResult}; +use crate::eval::Tracer; +use crate::foundations::{Content, Resolve, Smart, StyleChain}; +use crate::introspection::{Introspector, Locator, MetaElem}; +use crate::layout::{ + Abs, AlignElem, Axes, BoxElem, Dir, Em, FixedAlign, Fr, Fragment, Frame, HElem, + Layout, Point, Regions, Size, Sizing, Spacing, Vt, +}; use crate::math::EquationElem; -use crate::prelude::*; +use crate::model::{Linebreaks, ParElem}; +use crate::syntax::Span; use crate::text::{ - breakpoints, char_is_cjk_script, is_gb_style, shape, Breakpoint, LinebreakElem, - Quoter, Quotes, ShapedGlyph, ShapedText, SmartquoteElem, SpaceElem, TextElem, - BEGIN_PUNCT_PAT, END_PUNCT_PAT, + Lang, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, }; +use crate::util::Numeric; +use crate::World; -/// Arranges text, spacing and inline-level elements into a paragraph. -/// -/// Although this function is primarily used in set rules to affect paragraph -/// properties, it can also be used to explicitly render its argument onto a -/// paragraph of its own. -/// -/// # Example -/// ```example -/// #show par: set block(spacing: 0.65em) -/// #set par( -/// first-line-indent: 1em, -/// justify: true, -/// ) -/// -/// We proceed by contradiction. -/// Suppose that there exists a set -/// of positive integers $a$, $b$, and -/// $c$ that satisfies the equation -/// $a^n + b^n = c^n$ for some -/// integer value of $n > 2$. -/// -/// Without loss of generality, -/// let $a$ be the smallest of the -/// three integers. Then, we ... -/// ``` -#[elem(title = "Paragraph", Construct)] -pub struct ParElem { - /// The spacing between lines. - #[resolve] - #[ghost] - #[default(Em::new(0.65).into())] - pub leading: Length, - - /// Whether to justify text in its line. - /// - /// Hyphenation will be enabled for justified paragraphs if the - /// [text function's `hyphenate` property]($text.hyphenate) is set to - /// `{auto}` and the current language is known. - /// - /// Note that the current [alignment]($align) still has an effect on the - /// placement of the last line except if it ends with a - /// [justified line break]($linebreak.justify). - #[ghost] - #[default(false)] - pub justify: bool, - - /// How to determine line breaks. - /// - /// When this property is set to `{auto}`, its default value, optimized line - /// breaks will be used for justified paragraphs. Enabling optimized line - /// breaks for ragged paragraphs may also be worthwhile to improve the - /// appearance of the text. - /// - /// ```example - /// #set page(width: 207pt) - /// #set par(linebreaks: "simple") - /// Some texts feature many longer - /// words. Those are often exceedingly - /// challenging to break in a visually - /// pleasing way. - /// - /// #set par(linebreaks: "optimized") - /// Some texts feature many longer - /// words. Those are often exceedingly - /// challenging to break in a visually - /// pleasing way. - /// ``` - #[ghost] - pub linebreaks: Smart<Linebreaks>, - - /// The indent the first line of a paragraph should have. - /// - /// Only the first line of a consecutive paragraph will be indented (not - /// the first one in a block or on the page). - /// - /// By typographic convention, paragraph breaks are indicated either by some - /// space between paragraphs or by indented first lines. Consider reducing - /// the [paragraph spacing]($block.spacing) to the [`leading`] when - /// using this property (e.g. using - /// `[#show par: set block(spacing: 0.65em)]`). - #[ghost] - pub first_line_indent: Length, - - /// The indent all but the first line of a paragraph should have. - #[ghost] - #[resolve] - pub hanging_indent: Length, - - /// The contents of the paragraph. - #[external] - #[required] - pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: Vec<Prehashed<Content>>, -} - -impl Construct for ParElem { - fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> { - // The paragraph constructor is special: It doesn't create a paragraph - // element. Instead, it just ensures that the passed content lives in a - // separate paragraph and styles it. - let styles = Self::set(vm, args)?; - let body = args.expect::<Content>("body")?; - Ok(Content::sequence([ - ParbreakElem::new().pack(), - body.styled_with_map(styles), - ParbreakElem::new().pack(), - ])) - } -} - -impl ParElem { - /// Layout the paragraph into a collection of lines. - #[tracing::instrument(name = "ParElement::layout", skip_all)] - pub fn layout( - &self, - vt: &mut Vt, +/// Layout's content inline. +pub(crate) fn layout_inline( + children: &[Prehashed<Content>], + vt: &mut Vt, + styles: StyleChain, + consecutive: bool, + region: Size, + expand: bool, +) -> SourceResult<Fragment> { + #[comemo::memoize] + #[allow(clippy::too_many_arguments)] + fn cached( + children: &[Prehashed<Content>], + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, styles: StyleChain, consecutive: bool, region: Size, expand: bool, ) -> SourceResult<Fragment> { - #[comemo::memoize] - #[allow(clippy::too_many_arguments)] - fn cached( - par: &ParElem, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let children = par.children(); - - // Collect all text into one string for BiDi analysis. - let (text, segments, spans) = collect(children, &styles, consecutive)?; + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; - // Perform BiDi analysis and then prepare paragraph layout by building a - // representation on which we can do line breaking without layouting - // each and every line from scratch. - let p = prepare(&mut vt, children, &text, segments, spans, styles, region)?; + // Collect all text into one string for BiDi analysis. + let (text, segments, spans) = collect(children, &styles, consecutive)?; - // Break the paragraph into lines. - let lines = linebreak(&vt, &p, region.x - p.hang); + // Perform BiDi analysis and then prepare paragraph layout by building a + // representation on which we can do line breaking without layouting + // each and every line from scratch. + let p = prepare(&mut vt, children, &text, segments, spans, styles, region)?; - // Stack the lines into one frame per region. - finalize(&mut vt, &p, &lines, region, expand) - } + // Break the paragraph into lines. + let lines = linebreak(&vt, &p, region.x - p.hang); - let fragment = cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - consecutive, - region, - expand, - )?; - - vt.locator.visit_frames(&fragment); - Ok(fragment) + // Stack the lines into one frame per region. + finalize(&mut vt, &p, &lines, region, expand) } -} -/// How to determine line breaks in a paragraph. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Linebreaks { - /// Determine the line breaks in a simple first-fit style. - Simple, - /// Optimize the line breaks for the whole paragraph. - /// - /// Typst will try to produce more evenly filled lines of text by - /// considering the whole paragraph when calculating line breaks. - Optimized, + let fragment = cached( + children, + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + styles, + consecutive, + region, + expand, + )?; + + vt.locator.visit_frames(&fragment); + Ok(fragment) } -/// A paragraph break. -/// -/// This starts a new paragraph. Especially useful when used within code like -/// [for loops]($scripting/#loops). Multiple consecutive -/// paragraph breaks collapse into a single one. -/// -/// # Example -/// ```example -/// #for i in range(3) { -/// [Blind text #i: ] -/// lorem(5) -/// parbreak() -/// } -/// ``` -/// -/// # Syntax -/// Instead of calling this function, you can insert a blank line into your -/// markup to create a paragraph break. -#[elem(title = "Paragraph Break", Unlabellable)] -pub struct ParbreakElem {} - -impl Unlabellable for ParbreakElem {} - /// Range of a substring of text. type Range = std::ops::Range<usize>; @@ -244,38 +105,38 @@ const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character /// In many cases, we can directly reuse these results when constructing a line. /// Only when a line break falls onto a text index that is not safe-to-break per /// rustybuzz, we have to reshape that portion. -pub(crate) struct Preparation<'a> { +struct Preparation<'a> { /// Bidirectional text embedding levels for the paragraph. - pub bidi: BidiInfo<'a>, + bidi: BidiInfo<'a>, /// Text runs, spacing and layouted elements. - pub items: Vec<Item<'a>>, + items: Vec<Item<'a>>, /// The span mapper. - pub spans: SpanMapper, + spans: SpanMapper, /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option<bool>, + hyphenate: Option<bool>, /// The text language if it's the same for all children. - pub lang: Option<Lang>, + lang: Option<Lang>, /// The paragraph's resolved horizontal alignment. - pub align: FixedAlign, + align: FixedAlign, /// Whether to justify the paragraph. - pub justify: bool, + justify: bool, /// The paragraph's hanging indent. - pub hang: Abs, + hang: Abs, /// Whether to add spacing between CJK and Latin characters. - pub cjk_latin_spacing: bool, + cjk_latin_spacing: bool, /// Whether font fallback is enabled for this paragraph. - pub fallback: bool, + fallback: bool, /// The leading of the paragraph. - pub leading: Abs, + leading: Abs, /// How to determine line breaks. - pub linebreaks: Smart<Linebreaks>, + linebreaks: Smart<Linebreaks>, /// The text size. - pub size: Abs, + size: Abs, } impl<'a> Preparation<'a> { /// Find the item that contains the given `text_offset`. - pub fn find(&self, text_offset: usize) -> Option<&Item<'a>> { + fn find(&self, text_offset: usize) -> Option<&Item<'a>> { let mut cursor = 0; for item in &self.items { let end = cursor + item.len(); @@ -290,7 +151,7 @@ impl<'a> Preparation<'a> { /// Return the items that intersect the given `text_range`. /// /// Returns the expanded range around the items and the items. - pub fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { + fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { let mut cursor = 0; let mut start = 0; let mut end = 0; @@ -348,7 +209,7 @@ impl Segment<'_> { /// A prepared item in a paragraph layout. #[derive(Debug)] -pub(crate) enum Item<'a> { +enum Item<'a> { /// A shaped text run with consistent style and direction. Text(ShapedText<'a>), /// Absolute spacing between other items. @@ -363,14 +224,14 @@ pub(crate) enum Item<'a> { impl<'a> Item<'a> { /// If this a text item, return it. - pub fn text(&self) -> Option<&ShapedText<'a>> { + fn text(&self) -> Option<&ShapedText<'a>> { match self { Self::Text(shaped) => Some(shaped), _ => None, } } - pub fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> { + fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> { match self { Self::Text(shaped) => Some(shaped), _ => None, @@ -378,7 +239,8 @@ impl<'a> Item<'a> { } /// The text length of the item. - pub fn len(&self) -> usize { + #[allow(clippy::len_without_is_empty)] + fn len(&self) -> usize { match self { Self::Text(shaped) => shaped.text.len(), Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(), @@ -388,7 +250,7 @@ impl<'a> Item<'a> { } /// The natural layouted width of the item. - pub fn width(&self) -> Abs { + fn width(&self) -> Abs { match self { Self::Text(shaped) => shaped.width, Self::Absolute(v) => *v, @@ -400,23 +262,23 @@ impl<'a> Item<'a> { /// Maps byte offsets back to spans. #[derive(Default)] -pub struct SpanMapper(Vec<(usize, Span)>); +struct SpanMapper(Vec<(usize, Span)>); impl SpanMapper { /// Create a new span mapper. - pub fn new() -> Self { + fn new() -> Self { Self::default() } /// Push a span for a segment with the given length. - pub fn push(&mut self, len: usize, span: Span) { + fn push(&mut self, len: usize, span: Span) { self.0.push((len, span)); } /// Determine the span at the given byte offset. /// /// May return a detached span. - pub fn span_at(&self, offset: usize) -> (Span, u16) { + fn span_at(&self, offset: usize) -> (Span, u16) { let mut cursor = 0; for &(len, span) in &self.0 { if (cursor..=cursor + len).contains(&offset) { @@ -541,7 +403,7 @@ fn collect<'a>( consecutive: bool, ) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> { let mut full = String::new(); - let mut quoter = Quoter::new(); + let mut quoter = SmartQuoter::new(); let mut segments = Vec::with_capacity(2 + children.len()); let mut spans = SpanMapper::new(); let mut iter = children.iter().map(|c| &**c).peekable(); @@ -592,17 +454,17 @@ fn collect<'a>( let c = if elem.justify(styles) { '\u{2028}' } else { '\n' }; full.push(c); Segment::Text(c.len_utf8()) - } else if let Some(elem) = child.to::<SmartquoteElem>() { + } else if let Some(elem) = child.to::<SmartQuoteElem>() { let prev = full.len(); - if SmartquoteElem::enabled_in(styles) { - let quotes = SmartquoteElem::quotes_in(styles); + if SmartQuoteElem::enabled_in(styles) { + let quotes = SmartQuoteElem::quotes_in(styles); let lang = TextElem::lang_in(styles); let region = TextElem::region_in(styles); - let quotes = Quotes::new( + let quotes = SmartQuotes::new( quotes, lang, region, - SmartquoteElem::alternative_in(styles), + SmartQuoteElem::alternative_in(styles), ); let peeked = iter.peek().and_then(|child| { let child = if let Some((child, _)) = child.to_styled() { @@ -612,7 +474,7 @@ fn collect<'a>( }; if let Some(elem) = child.to::<TextElem>() { elem.text().chars().next() - } else if child.is::<SmartquoteElem>() { + } else if child.is::<SmartQuoteElem>() { Some('"') } else if child.is::<SpaceElem>() || child.is::<HElem>() @@ -643,7 +505,7 @@ fn collect<'a>( }; if let Some(last) = full.chars().last() { - quoter.last(last, child.is::<SmartquoteElem>()); + quoter.last(last, child.is::<SmartQuoteElem>()); } spans.push(segment.len(), child.span()); @@ -1149,7 +1011,7 @@ fn line<'a>( // Deal with CJK punctuation at line ends. let gb_style = is_gb_style(shaped.lang, shaped.region); let maybe_adjust_last_glyph = trimmed.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && trimmed.ends_with(char_is_cjk_script)); + || (p.cjk_latin_spacing && trimmed.ends_with(is_of_cjk_script)); // Usually, we don't want to shape an empty string because: // - We don't want the height of trimmed whitespace in a different @@ -1202,7 +1064,7 @@ fn line<'a>( // Deal with CJK characters at line starts. let text = &p.bidi.text[range.start..end]; let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(char_is_cjk_script)); + || (p.cjk_latin_spacing && text.starts_with(is_of_cjk_script)); // Reshape the start item if it's split in half. let mut first = None; diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst/src/layout/inline/shaping.rs index 2d820c64..69e70967 100644 --- a/crates/typst-library/src/text/shaping.rs +++ b/crates/typst/src/layout/inline/shaping.rs @@ -1,23 +1,30 @@ use std::borrow::Cow; +use std::fmt::{self, Debug, Formatter}; use std::ops::Range; use std::str::FromStr; use az::SaturatingAs; -use rustybuzz::{Feature, Tag, UnicodeBuffer}; -use typst::font::{Font, FontStyle, FontVariant}; -use typst::util::SliceExt; +use ecow::EcoString; +use rustybuzz::{Tag, UnicodeBuffer}; use unicode_script::{Script, UnicodeScript}; -use crate::layout::SpanMapper; -use crate::prelude::*; -use crate::text::{decorate, NumberType, NumberWidth, TextElem}; +use super::SpanMapper; +use crate::foundations::StyleChain; +use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size, Vt}; +use crate::syntax::Span; +use crate::text::{ + decorate, families, features, variant, Font, FontVariant, Glyph, Lang, Region, + TextElem, TextItem, +}; +use crate::util::SliceExt; +use crate::World; /// The result of shaping text. /// /// This type contains owned or borrowed shaped text runs, which can be /// measured, used to reshape substrings more quickly and converted into a /// frame. -pub struct ShapedText<'a> { +pub(super) struct ShapedText<'a> { /// The start of the text in the full paragraph. pub base: usize, /// The text that was shaped. @@ -42,7 +49,7 @@ pub struct ShapedText<'a> { /// A single glyph resulting from shaping. #[derive(Debug, Clone)] -pub struct ShapedGlyph { +pub(super) struct ShapedGlyph { /// The font the glyph is contained in. pub font: Font, /// The glyph's index in the font. @@ -80,7 +87,7 @@ pub struct ShapedGlyph { } #[derive(Debug, Clone, Default)] -pub struct Adjustability { +pub(super) struct Adjustability { /// The left and right strechability pub stretchability: (Em, Em), /// The left and right shrinkability @@ -553,14 +560,14 @@ struct ShapingContext<'a, 'v> { styles: StyleChain<'a>, size: Abs, variant: FontVariant, - tags: Vec<rustybuzz::Feature>, + features: Vec<rustybuzz::Feature>, fallback: bool, dir: Dir, } /// Shape text into [`ShapedText`]. #[allow(clippy::too_many_arguments)] -pub fn shape<'a>( +pub(super) fn shape<'a>( vt: &Vt, base: usize, text: &'a str, @@ -579,7 +586,7 @@ pub fn shape<'a>( used: vec![], styles, variant: variant(styles), - tags: tags(styles), + features: features(styles), fallback: TextElem::fallback_in(styles), dir, }; @@ -666,7 +673,7 @@ fn shape_segment<'a>( }); // Shape! - let buffer = rustybuzz::shape(font.rusty(), &ctx.tags, buffer); + let buffer = rustybuzz::shape(font.rusty(), &ctx.features, buffer); let infos = buffer.glyph_infos(); let pos = buffer.glyph_positions(); let ltr = ctx.dir.is_positive(); @@ -817,13 +824,6 @@ fn track_and_space(ctx: &mut ShapingContext) { } } -pub fn is_gb_style(lang: Lang, region: Option<Region>) -> bool { - // Most CJK variants, including zh-CN, ja-JP, zh-SG, zh-MY use GB-style punctuation, - // while zh-HK and zh-TW use alternative style. We default to use GB-style. - !(lang == Lang::CHINESE - && matches!(region.as_ref().map(Region::as_str), Some("TW" | "HK"))) -} - /// Calculate stretchability and shrinkability of each glyph, /// and CJK punctuation adjustments according to Chinese Layout Requirements. fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<Region>) { @@ -864,114 +864,6 @@ fn nbsp_delta(font: &Font) -> Option<Em> { Some(font.advance(nbsp)? - font.advance(space)?) } -/// Resolve the font variant. -pub fn variant(styles: StyleChain) -> FontVariant { - let mut variant = FontVariant::new( - TextElem::style_in(styles), - TextElem::weight_in(styles), - TextElem::stretch_in(styles), - ); - - let delta = TextElem::delta_in(styles); - variant.weight = variant - .weight - .thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16); - - if TextElem::emph_in(styles) { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - variant -} - -/// Resolve a prioritized iterator over the font families. -pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { - const FALLBACKS: &[&str] = &[ - "linux libertine", - "twitter color emoji", - "noto color emoji", - "apple color emoji", - "segoe ui emoji", - ]; - - let tail = if TextElem::fallback_in(styles) { FALLBACKS } else { &[] }; - TextElem::font_in(styles) - .into_iter() - .map(|family| family.as_str()) - .chain(tail.iter().copied()) -} - -/// Collect the tags of the OpenType features to apply. -pub fn tags(styles: StyleChain) -> Vec<Feature> { - let mut tags = vec![]; - let mut feat = |tag, value| { - tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); - }; - - // Features that are on by default in Harfbuzz are only added if disabled. - if !TextElem::kerning_in(styles) { - feat(b"kern", 0); - } - - // Features that are off by default in Harfbuzz are only added if enabled. - if TextElem::smallcaps_in(styles) { - feat(b"smcp", 1); - } - - if TextElem::alternates_in(styles) { - feat(b"salt", 1); - } - - let storage; - if let Some(set) = TextElem::stylistic_set_in(styles) { - storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; - feat(&storage, 1); - } - - if !TextElem::ligatures_in(styles) { - feat(b"liga", 0); - feat(b"clig", 0); - } - - if TextElem::discretionary_ligatures_in(styles) { - feat(b"dlig", 1); - } - - if TextElem::historical_ligatures_in(styles) { - feat(b"hilg", 1); - } - - match TextElem::number_type_in(styles) { - Smart::Auto => {} - Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), - Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), - } - - match TextElem::number_width_in(styles) { - Smart::Auto => {} - Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), - Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), - } - - if TextElem::slashed_zero_in(styles) { - feat(b"zero", 1); - } - - if TextElem::fractions_in(styles) { - feat(b"frac", 1); - } - - for (tag, value) in TextElem::features_in(styles).0 { - tags.push(Feature::new(tag, value, ..)) - } - - tags -} - /// Process the language and and region of a style chain into a /// rustybuzz-compatible BCP 47 language. fn language(styles: StyleChain) -> rustybuzz::Language { @@ -1021,13 +913,20 @@ fn assert_glyph_ranges_in_order(glyphs: &[ShapedGlyph], dir: Dir) { } // The CJK punctuation that can appear at the beginning or end of a line. -pub(crate) const BEGIN_PUNCT_PAT: &[char] = +pub(super) const BEGIN_PUNCT_PAT: &[char] = &['“', '‘', '《', '〈', '(', '『', '「', '【', '〖', '〔', '[', '{']; -pub(crate) const END_PUNCT_PAT: &[char] = &[ +pub(super) const END_PUNCT_PAT: &[char] = &[ '”', '’', ',', '.', '。', '、', ':', ';', '》', '〉', ')', '』', '」', '】', '〗', '〕', ']', '}', '?', '!', ]; +pub(super) fn is_gb_style(lang: Lang, region: Option<Region>) -> bool { + // Most CJK variants, including zh-CN, ja-JP, zh-SG, zh-MY use GB-style punctuation, + // while zh-HK and zh-TW use alternative style. We default to use GB-style. + !(lang == Lang::CHINESE + && matches!(region.as_ref().map(Region::as_str), Some("TW" | "HK"))) +} + /// Whether the glyph is a space. #[inline] fn is_space(c: char) -> bool { @@ -1036,7 +935,7 @@ fn is_space(c: char) -> bool { /// Whether the glyph is part of a CJK script. #[inline] -pub fn char_is_cjk_script(c: char) -> bool { +pub(super) fn is_of_cjk_script(c: char) -> bool { is_cjk_script(c, c.script()) } diff --git a/crates/typst-library/src/meta/context.rs b/crates/typst/src/layout/layout.rs index 59b35577..e2c0fa2b 100644 --- a/crates/typst-library/src/meta/context.rs +++ b/crates/typst/src/layout/layout.rs @@ -1,91 +1,6 @@ -use crate::prelude::*; - -/// Provides access to the location of content. -/// -/// This is useful in combination with [queries]($query), [counters]($counter), -/// [state]($state), and [links]($link). See their documentation for more -/// details. -/// -/// ```example -/// #locate(loc => [ -/// My location: \ -/// #loc.position()! -/// ]) -/// ``` -#[func] -pub fn locate( - /// A function that receives a [`location`]($location). Its return value is - /// displayed in the document. - /// - /// This function is called once for each time the content returned by - /// `locate` appears in the document. That makes it possible to generate - /// content that depends on its own location in the document. - func: Func, -) -> Content { - LocateElem::new(func).pack() -} - -/// Executes a `locate` call. -#[elem(Locatable, Show)] -struct LocateElem { - /// The function to call with the location. - #[required] - func: Func, -} - -impl Show for LocateElem { - #[tracing::instrument(name = "LocateElem::show", skip(self, vt))] - fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(vt.delayed(|vt| { - let location = self.location().unwrap(); - Ok(self.func().call_vt(vt, [location])?.display()) - })) - } -} - -/// Provides access to active styles. -/// -/// The styles are currently opaque and only useful in combination with the -/// [`measure`]($measure) function. See its documentation for more details. In -/// the future, the provided styles might also be directly accessed to look up -/// styles defined by [set rules]($styling/#set-rules). -/// -/// ```example -/// #let thing(body) = style(styles => { -/// let size = measure(body, styles) -/// [Width of "#body" is #size.width] -/// }) -/// -/// #thing[Hey] \ -/// #thing[Welcome] -/// ``` -#[func] -pub fn style( - /// A function to call with the styles. Its return value is displayed - /// in the document. - /// - /// This function is called once for each time the content returned by - /// `style` appears in the document. That makes it possible to generate - /// content that depends on the style context it appears in. - func: Func, -) -> Content { - StyleElem::new(func).pack() -} - -/// Executes a style access. -#[elem(Show)] -struct StyleElem { - /// The function to call with the styles. - #[required] - func: Func, -} - -impl Show for StyleElem { - #[tracing::instrument(name = "StyleElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.func().call_vt(vt, [styles.to_map()])?.display()) - } -} +use crate::diag::SourceResult; +use crate::foundations::{dict, elem, func, Content, Func, NativeElement, StyleChain}; +use crate::layout::{Fragment, Layout, Regions, Size, Vt}; /// Provides access to the current outer container's (or page's, if none) size /// (width and height). diff --git a/crates/typst/src/geom/length.rs b/crates/typst/src/layout/length.rs index 3027fbea..587085d3 100644 --- a/crates/typst/src/geom/length.rs +++ b/crates/typst/src/layout/length.rs @@ -1,6 +1,14 @@ -use super::*; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::{eco_format, EcoString}; + use crate::diag::{At, Hint, SourceResult}; +use crate::foundations::{func, scope, ty, Repr, Resolve, StyleChain}; +use crate::layout::{Abs, Em}; use crate::syntax::Span; +use crate::util::Numeric; /// A size or distance, possibly expressed with contextual units. /// diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst/src/layout/measure.rs index d41b7f95..2b82c58b 100644 --- a/crates/typst-library/src/layout/measure.rs +++ b/crates/typst/src/layout/measure.rs @@ -1,4 +1,7 @@ -use crate::prelude::*; +use crate::diag::SourceResult; +use crate::eval::Vm; +use crate::foundations::{dict, func, Content, Dict, StyleChain, Styles}; +use crate::layout::{Abs, Axes, Layout, Regions, Size}; /// Measures the layouted size of content. /// diff --git a/crates/typst/src/layout/mod.rs b/crates/typst/src/layout/mod.rs new file mode 100644 index 00000000..1627b8e7 --- /dev/null +++ b/crates/typst/src/layout/mod.rs @@ -0,0 +1,256 @@ +//! Composable layouts. + +mod abs; +mod align; +mod angle; +mod axes; +mod columns; +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; +#[path = "measure.rs"] +mod measure_; +mod pad; +mod page; +mod place; +mod point; +mod ratio; +mod regions; +mod rel; +mod repeat; +mod sides; +mod size; +mod spacing; +mod stack; +mod transform; +mod vt; + +pub use self::abs::*; +pub use self::align::*; +pub use self::angle::*; +pub use self::axes::*; +pub use self::columns::*; +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::*; +pub use self::grid::*; +pub use self::hide::*; +pub use self::layout_::*; +pub use self::length::*; +pub use self::measure_::*; +pub use self::pad::*; +pub use self::page::*; +pub use self::place::*; +pub use self::point::*; +pub use self::ratio::*; +pub use self::regions::Regions; +pub use self::rel::*; +pub use self::repeat::*; +pub use self::sides::*; +pub use self::size::*; +pub use self::spacing::*; +pub use self::stack::*; +pub use self::transform::*; +pub use self::vt::*; + +pub(crate) use self::inline::*; + +use comemo::{Tracked, TrackedMut}; + +use crate::diag::DelayedErrors; +use crate::diag::SourceResult; +use crate::eval::Tracer; +use crate::foundations::{category, Category, Content, Scope, StyleChain}; +use crate::introspection::{Introspector, Locator}; +use crate::model::Document; +use crate::realize::{realize_block, realize_root, Scratch}; +use crate::World; + +/// Arranging elements on the page in different ways. +/// +/// By combining layout functions, you can create complex and automatic layouts. +#[category] +pub static LAYOUT: Category; + +/// Hook up all `layout` definitions. +pub fn define(global: &mut Scope) { + global.category(LAYOUT); + global.define_type::<Length>(); + global.define_type::<Angle>(); + global.define_type::<Ratio>(); + global.define_type::<Rel<Length>>(); + global.define_type::<Fr>(); + global.define_type::<Dir>(); + global.define_type::<Align>(); + global.define_elem::<PageElem>(); + global.define_elem::<PagebreakElem>(); + global.define_elem::<VElem>(); + global.define_elem::<HElem>(); + global.define_elem::<BoxElem>(); + global.define_elem::<BlockElem>(); + global.define_elem::<StackElem>(); + global.define_elem::<GridElem>(); + global.define_elem::<ColumnsElem>(); + global.define_elem::<ColbreakElem>(); + global.define_elem::<PlaceElem>(); + global.define_elem::<AlignElem>(); + global.define_elem::<PadElem>(); + global.define_elem::<RepeatElem>(); + global.define_elem::<MoveElem>(); + global.define_elem::<ScaleElem>(); + global.define_elem::<RotateElem>(); + global.define_elem::<HideElem>(); + global.define_func::<measure>(); + global.define_func::<layout>(); +} + +/// Root-level layout. +pub trait LayoutRoot { + /// Layout into one frame per page. + fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>; +} + +/// Layout into regions. +pub trait Layout { + /// Layout into one frame per region. + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment>; + + /// Layout without side effects. + /// + /// This element must be layouted again in the same order for the results to + /// be valid. + #[tracing::instrument(name = "Layout::measure", skip_all)] + fn measure( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let mut locator = Locator::chained(vt.locator.track()); + let mut vt = Vt { + world: vt.world, + introspector: vt.introspector, + locator: &mut locator, + tracer: TrackedMut::reborrow_mut(&mut vt.tracer), + delayed: TrackedMut::reborrow_mut(&mut vt.delayed), + }; + self.layout(&mut vt, styles, regions) + } +} + +impl LayoutRoot for Content { + #[tracing::instrument(name = "Content::layout_root", skip_all)] + fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { + #[comemo::memoize] + fn cached( + content: &Content, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + styles: StyleChain, + ) -> SourceResult<Document> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let scratch = Scratch::default(); + let (realized, styles) = realize_root(&mut vt, &scratch, content, styles)?; + realized + .with::<dyn LayoutRoot>() + .unwrap() + .layout_root(&mut vt, styles) + } + + tracing::info!("Starting layout"); + cached( + self, + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + styles, + ) + } +} + +impl Layout for Content { + #[tracing::instrument(name = "Content::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + #[allow(clippy::too_many_arguments)] + #[comemo::memoize] + fn cached( + content: &Content, + world: Tracked<dyn World + '_>, + introspector: Tracked<Introspector>, + locator: Tracked<Locator>, + delayed: TrackedMut<DelayedErrors>, + tracer: TrackedMut<Tracer>, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + let mut locator = Locator::chained(locator); + let mut vt = Vt { + world, + introspector, + locator: &mut locator, + delayed, + tracer, + }; + let scratch = Scratch::default(); + let (realized, styles) = realize_block(&mut vt, &scratch, content, styles)?; + realized + .with::<dyn Layout>() + .unwrap() + .layout(&mut vt, styles, regions) + } + + tracing::info!("Layouting `Content`"); + + let fragment = cached( + self, + vt.world, + vt.introspector, + vt.locator.track(), + TrackedMut::reborrow_mut(&mut vt.delayed), + TrackedMut::reborrow_mut(&mut vt.tracer), + styles, + regions, + )?; + + vt.locator.visit_frames(&fragment); + Ok(fragment) + } +} diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst/src/layout/pad.rs index d1b0cb1f..f5762409 100644 --- a/crates/typst-library/src/layout/pad.rs +++ b/crates/typst/src/layout/pad.rs @@ -1,4 +1,8 @@ -use crate::prelude::*; +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, Resolve, StyleChain}; +use crate::layout::{ + Abs, Fragment, Layout, Length, Point, Regions, Rel, Sides, Size, Vt, +}; /// Adds spacing around content. /// diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst/src/layout/page.rs index 53646c7d..411f9769 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -1,13 +1,24 @@ use std::borrow::Cow; +use std::num::NonZeroUsize; use std::ptr; use std::str::FromStr; -use typst::eval::AutoValue; - -use crate::layout::{AlignElem, ColumnsElem}; -use crate::meta::{Counter, CounterKey, ManualPageCounter, Numbering}; -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + cast, elem, AutoValue, Cast, Content, Dict, Fold, Func, NativeElement, Resolve, + Smart, StyleChain, Value, +}; +use crate::introspection::{Counter, CounterKey, ManualPageCounter, Meta}; +use crate::layout::{ + Abs, Align, AlignElem, Axes, ColumnsElem, Dir, Fragment, Frame, HAlign, Layout, + Length, Point, Ratio, Regions, Rel, Sides, Size, VAlign, Vt, +}; + +use crate::model::Numbering; +use crate::syntax::Spanned; use crate::text::TextElem; +use crate::util::{NonZeroExt, Numeric, Scalar}; +use crate::visualize::Paint; /// Layouts its child onto one or multiple pages. /// @@ -400,7 +411,7 @@ impl PageElem { let header_ascent = self.header_ascent(styles); let footer_descent = self.footer_descent(styles); let numbering = self.numbering(styles); - let numbering_meta = Meta::PageNumbering(numbering.clone().into_value()); + let numbering_meta = Meta::PageNumbering(numbering.clone()); let number_align = self.number_align(styles); let mut header = Cow::Borrowed(self.header(styles)); let mut footer = Cow::Borrowed(self.footer(styles)); diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst/src/layout/place.rs index c8e83383..537b47ed 100644 --- a/crates/typst-library/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -1,4 +1,10 @@ -use crate::prelude::*; +use crate::diag::{bail, At, Hint, SourceResult}; +use crate::foundations::{ + elem, Behave, Behaviour, Content, NativeElement, Smart, StyleChain, +}; +use crate::layout::{ + Align, Axes, Em, Fragment, Layout, Length, Regions, Rel, VAlign, Vt, +}; /// Places content at an absolute position. /// diff --git a/crates/typst/src/geom/point.rs b/crates/typst/src/layout/point.rs index e7811e1e..239a6fe7 100644 --- a/crates/typst/src/geom/point.rs +++ b/crates/typst/src/layout/point.rs @@ -1,4 +1,8 @@ -use super::*; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg}; + +use crate::layout::{Abs, Axis, Size, Transform}; +use crate::util::{Get, Numeric}; /// A point in 2D. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst/src/geom/ratio.rs b/crates/typst/src/layout/ratio.rs index 670eeb75..c19245b4 100644 --- a/crates/typst/src/geom/ratio.rs +++ b/crates/typst/src/layout/ratio.rs @@ -1,4 +1,10 @@ -use super::*; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, Div, Mul, Neg}; + +use ecow::EcoString; + +use crate::foundations::{repr, ty, Repr}; +use crate::util::{Numeric, Scalar}; /// A ratio of a whole. /// @@ -70,7 +76,7 @@ impl Debug for Ratio { impl Repr for Ratio { fn repr(&self) -> EcoString { - format_float(self.get() * 100.0, Some(2), "%") + repr::format_float(self.get() * 100.0, Some(2), "%") } } diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst/src/layout/regions.rs index 6dd549b0..46e85e68 100644 --- a/crates/typst-library/src/layout/regions.rs +++ b/crates/typst/src/layout/regions.rs @@ -1,6 +1,6 @@ use std::fmt::{self, Debug, Formatter}; -use typst::geom::{Abs, Axes, Size}; +use crate::layout::{Abs, Axes, Size}; /// A sequence of regions to layout into. #[derive(Copy, Clone, Hash)] diff --git a/crates/typst/src/geom/rel.rs b/crates/typst/src/layout/rel.rs index a2c8643e..7769f858 100644 --- a/crates/typst/src/geom/rel.rs +++ b/crates/typst/src/layout/rel.rs @@ -1,4 +1,12 @@ -use super::*; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use ecow::{eco_format, EcoString}; + +use crate::foundations::{cast, ty, Fold, Repr, Resolve, StyleChain}; +use crate::layout::{Abs, Em, Length, Ratio}; +use crate::util::Numeric; /// A length in relation to some known length. /// diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst/src/layout/repeat.rs index ce31164e..29e34e18 100644 --- a/crates/typst-library/src/layout/repeat.rs +++ b/crates/typst/src/layout/repeat.rs @@ -1,5 +1,9 @@ -use crate::layout::AlignElem; -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{elem, Content, NativeElement, Resolve, StyleChain}; +use crate::layout::{ + Abs, AlignElem, Axes, Fragment, Frame, Layout, Point, Regions, Size, Vt, +}; +use crate::util::Numeric; /// Repeats content to the available space. /// diff --git a/crates/typst/src/geom/sides.rs b/crates/typst/src/layout/sides.rs index 2ca7ca6a..01b1f426 100644 --- a/crates/typst/src/geom/sides.rs +++ b/crates/typst/src/layout/sides.rs @@ -1,7 +1,12 @@ use std::fmt::{self, Debug, Formatter}; - -use super::*; -use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; +use std::ops::Add; + +use crate::diag::{bail, StrResult}; +use crate::foundations::{ + cast, CastInfo, Dict, Fold, FromValue, IntoValue, Reflect, Resolve, StyleChain, Value, +}; +use crate::layout::{Abs, Align, Axes, Axis, Corner, Rel, Size}; +use crate::util::Get; /// A container with left, top, right and bottom components. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/crates/typst/src/geom/size.rs b/crates/typst/src/layout/size.rs index 21ffeb2e..92f26db0 100644 --- a/crates/typst/src/geom/size.rs +++ b/crates/typst/src/layout/size.rs @@ -1,4 +1,7 @@ -use super::*; +use std::ops::{Add, Div, Mul, Neg}; + +use crate::layout::{Abs, Axes, Point, Ratio}; +use crate::util::Numeric; /// A size in 2D. pub type Size = Axes<Abs>; diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst/src/layout/spacing.rs index 88b6e2cd..1620c139 100644 --- a/crates/typst-library/src/layout/spacing.rs +++ b/crates/typst/src/layout/spacing.rs @@ -1,6 +1,8 @@ use std::borrow::Cow; -use crate::prelude::*; +use crate::foundations::{cast, elem, Behave, Behaviour, Content, Resolve, StyleChain}; +use crate::layout::{Abs, Em, Fr, Length, Ratio, Rel}; +use crate::util::Numeric; /// Inserts horizontal spacing into a paragraph. /// diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst/src/layout/stack.rs index c12d2048..e4dd029a 100644 --- a/crates/typst-library/src/layout/stack.rs +++ b/crates/typst/src/layout/stack.rs @@ -1,5 +1,12 @@ -use crate::layout::{AlignElem, Spacing}; -use crate::prelude::*; +use std::fmt::{self, Debug, Formatter}; + +use crate::diag::SourceResult; +use crate::foundations::{cast, elem, Content, Resolve, StyleChain}; +use crate::layout::{ + Abs, AlignElem, Axes, Axis, Dir, FixedAlign, Fr, Fragment, Frame, Layout, Point, + Regions, Size, Spacing, Vt, +}; +use crate::util::{Get, Numeric}; /// Arranges content and spacing horizontally or vertically. /// diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst/src/layout/transform.rs index 012a146d..51cc5d54 100644 --- a/crates/typst-library/src/layout/transform.rs +++ b/crates/typst/src/layout/transform.rs @@ -1,6 +1,9 @@ -use typst::geom::Transform; - -use crate::prelude::*; +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, Resolve, StyleChain}; +use crate::layout::{ + Abs, Align, Angle, Axes, FixedAlign, Fragment, HAlign, Layout, Length, Ratio, + Regions, Rel, VAlign, Vt, +}; /// Moves content without affecting layout. /// @@ -185,3 +188,128 @@ impl Layout for ScaleElem { Ok(Fragment::frame(frame)) } } + +/// A scale-skew-translate transformation. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Transform { + pub sx: Ratio, + pub ky: Ratio, + pub kx: Ratio, + pub sy: Ratio, + pub tx: Abs, + pub ty: Abs, +} + +impl Transform { + /// The identity transformation. + pub const fn identity() -> Self { + Self { + sx: Ratio::one(), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::one(), + tx: Abs::zero(), + ty: Abs::zero(), + } + } + + /// A translate transform. + pub const fn translate(tx: Abs, ty: Abs) -> Self { + Self { tx, ty, ..Self::identity() } + } + + /// A scale transform. + pub const fn scale(sx: Ratio, sy: Ratio) -> Self { + Self { sx, sy, ..Self::identity() } + } + + /// A rotate transform. + pub fn rotate(angle: Angle) -> Self { + let cos = Ratio::new(angle.cos()); + let sin = Ratio::new(angle.sin()); + Self { + sx: cos, + ky: sin, + kx: -sin, + sy: cos, + ..Self::default() + } + } + + /// Whether this is the identity transformation. + pub fn is_identity(self) -> bool { + self == Self::identity() + } + + /// Pre-concatenate another transformation. + pub fn pre_concat(self, prev: Self) -> Self { + Transform { + sx: self.sx * prev.sx + self.kx * prev.ky, + ky: self.ky * prev.sx + self.sy * prev.ky, + kx: self.sx * prev.kx + self.kx * prev.sy, + sy: self.ky * prev.kx + self.sy * prev.sy, + tx: self.sx.of(prev.tx) + self.kx.of(prev.ty) + self.tx, + ty: self.ky.of(prev.tx) + self.sy.of(prev.ty) + self.ty, + } + } + + /// Post-concatenate another transformation. + pub fn post_concat(self, next: Self) -> Self { + next.pre_concat(self) + } + + /// Inverts the transformation. + /// + /// Returns `None` if the determinant of the matrix is zero. + pub fn invert(self) -> Option<Self> { + // Allow the trivial case to be inlined. + if self.is_identity() { + return Some(self); + } + + // Fast path for scale-translate-only transforms. + if self.kx.is_zero() && self.ky.is_zero() { + if self.sx.is_zero() || self.sy.is_zero() { + return Some(Self::translate(-self.tx, -self.ty)); + } + + let inv_x = 1.0 / self.sx; + let inv_y = 1.0 / self.sy; + return Some(Self { + sx: Ratio::new(inv_x), + ky: Ratio::zero(), + kx: Ratio::zero(), + sy: Ratio::new(inv_y), + tx: -self.tx * inv_x, + ty: -self.ty * inv_y, + }); + } + + let det = self.sx * self.sy - self.kx * self.ky; + if det.get().abs() < 1e-12 { + return None; + } + + let inv_det = 1.0 / det; + Some(Self { + sx: (self.sy * inv_det), + ky: (-self.ky * inv_det), + kx: (-self.kx * inv_det), + sy: (self.sx * inv_det), + tx: Abs::pt( + (self.kx.get() * self.ty.to_pt() - self.sy.get() * self.tx.to_pt()) + * inv_det, + ), + ty: Abs::pt( + (self.ky.get() * self.tx.to_pt() - self.sx.get() * self.ty.to_pt()) + * inv_det, + ), + }) + } +} + +impl Default for Transform { + fn default() -> Self { + Self::identity() + } +} diff --git a/crates/typst/src/layout/vt.rs b/crates/typst/src/layout/vt.rs new file mode 100644 index 00000000..7b316a28 --- /dev/null +++ b/crates/typst/src/layout/vt.rs @@ -0,0 +1,43 @@ +use comemo::{Tracked, TrackedMut}; + +use crate::diag::{DelayedErrors, SourceResult}; +use crate::eval::Tracer; +use crate::introspection::{Introspector, Locator}; +use crate::World; + +/// A virtual typesetter. +/// +/// Holds the state needed during compilation. +pub struct Vt<'a> { + /// The compilation environment. + pub world: Tracked<'a, dyn World + 'a>, + /// Provides access to information about the document. + pub introspector: Tracked<'a, Introspector>, + /// Provides stable identities to elements. + pub locator: &'a mut Locator<'a>, + /// Delayed errors that do not immediately terminate execution. + pub delayed: TrackedMut<'a, DelayedErrors>, + /// The tracer for inspection of the values an expression produces. + pub tracer: TrackedMut<'a, Tracer>, +} + +impl Vt<'_> { + /// Perform a fallible operation that does not immediately terminate further + /// execution. Instead it produces a delayed error that is only promoted to + /// a fatal one if it remains at the end of the introspection loop. + pub fn delayed<F, T>(&mut self, f: F) -> T + where + F: FnOnce(&mut Self) -> SourceResult<T>, + T: Default, + { + match f(self) { + Ok(value) => value, + Err(errors) => { + for error in errors { + self.delayed.push(error); + } + T::default() + } + } + } +} diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 9bd0f728..92f82492 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -12,7 +12,7 @@ //! the source file. The elements of the content tree are well structured and //! order-independent and thus much better suited for further processing than //! the raw markup. -//! - **Typesetting:** +//! - **Layouting:** //! Next, the content is [layouted] into a [document] containing one [frame] //! per page with items at fixed positions. //! - **Exporting:** @@ -24,27 +24,33 @@ //! [syntax tree]: syntax::SyntaxNode //! [AST]: syntax::ast //! [evaluate]: eval::eval -//! [module]: eval::Module -//! [content]: model::Content -//! [layouted]: model::layout -//! [document]: doc::Document -//! [frame]: doc::Frame +//! [module]: foundations::Module +//! [content]: foundations::Content +//! [layouted]: layout::LayoutRoot +//! [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; #[macro_use] pub mod util; -#[macro_use] -pub mod eval; pub mod diag; -pub mod doc; -pub mod font; -pub mod geom; -pub mod image; +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; #[doc(inline)] pub use typst_syntax as syntax; @@ -52,14 +58,20 @@ pub use typst_syntax as syntax; use std::collections::HashSet; use std::ops::Range; -use comemo::{Prehashed, Track, TrackedMut}; -use ecow::EcoString; +use comemo::{Prehashed, Track, Tracked, Validate}; +use ecow::{EcoString, EcoVec}; -use crate::diag::{FileResult, SourceResult}; -use crate::doc::Document; -use crate::eval::{Bytes, Datetime, Library, Route, Tracer}; -use crate::font::{Font, FontBook}; +use crate::diag::{warning, DelayedErrors, FileResult, SourceDiagnostic, SourceResult}; +use crate::eval::{Route, Tracer}; +use crate::foundations::{ + Array, Bytes, Content, Datetime, Module, Scope, StyleChain, Styles, +}; +use crate::introspection::{Introspector, Locator}; +use crate::layout::{Align, Dir, LayoutRoot, Vt}; +use crate::model::Document; use crate::syntax::{FileId, PackageSpec, Source, Span}; +use crate::text::{Font, FontBook}; +use crate::visualize::Color; /// Compile a source file into a fully layouted document. /// @@ -71,33 +83,88 @@ use crate::syntax::{FileId, PackageSpec, Source, Span}; /// `tracer.warnings()` after compilation will return all compiler warnings. #[tracing::instrument(skip_all)] pub fn compile(world: &dyn World, tracer: &mut Tracer) -> SourceResult<Document> { - let route = Route::default(); - - // Call `track` just once to keep comemo's ID stable. + // Call `track` on the world just once to keep comemo's ID stable. let world = world.track(); - let mut tracer = tracer.track_mut(); // Try to evaluate the source file into a module. - let module = eval::eval( + let module = crate::eval::eval( world, - route.track(), - TrackedMut::reborrow_mut(&mut tracer), + Route::default().track(), + tracer.track_mut(), &world.main(), - ); - - // Try to typeset it. - let res = module.and_then(|module| model::layout(world, tracer, &module.content())); - - // Deduplicate errors. - res.map_err(|err| { - let mut unique = HashSet::new(); - err.into_iter() - .filter(|diagnostic| { - let hash = util::hash128(&(&diagnostic.span, &diagnostic.message)); - unique.insert(hash) - }) - .collect() - }) + ) + .map_err(deduplicate)?; + + // Typeset the module's content, relayouting until convergence. + typeset(world, tracer, &module.content()).map_err(deduplicate) +} + +/// Relayout until introspection converges. +fn typeset( + world: Tracked<dyn World + '_>, + tracer: &mut Tracer, + content: &Content, +) -> SourceResult<Document> { + let library = world.library(); + let styles = StyleChain::new(&library.styles); + + let mut iter = 0; + let mut document; + let mut delayed; + let mut introspector = Introspector::new(&[]); + + // Relayout until all introspections stabilize. + // If that doesn't happen within five attempts, we give up. + loop { + tracing::info!("Layout iteration {iter}"); + + // Clear delayed errors. + delayed = DelayedErrors::new(); + + let constraint = <Introspector as Validate>::Constraint::new(); + let mut locator = Locator::new(); + let mut vt = Vt { + world, + tracer: tracer.track_mut(), + locator: &mut locator, + delayed: delayed.track_mut(), + introspector: introspector.track_with(&constraint), + }; + + // Layout! + document = content.layout_root(&mut vt, styles)?; + introspector = Introspector::new(&document.pages); + iter += 1; + + if introspector.validate(&constraint) { + break; + } + + if iter >= 5 { + tracer.warn( + warning!(Span::detached(), "layout did not converge within 5 attempts",) + .with_hint("check if any states or queries are updating themselves"), + ); + break; + } + } + + // Promote delayed errors. + if !delayed.0.is_empty() { + return Err(delayed.0); + } + + Ok(document) +} + +/// Deduplicate diagnostics. +fn deduplicate(mut diags: EcoVec<SourceDiagnostic>) -> EcoVec<SourceDiagnostic> { + let mut unique = HashSet::new(); + diags.retain(|diag| { + let hash = crate::util::hash128(&(&diag.span, &diag.message)); + unique.insert(hash) + }); + diags } /// The environment in which typesetting occurs. @@ -111,12 +178,15 @@ pub fn compile(world: &dyn World, tracer: &mut Tracer) -> SourceResult<Document> /// 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. Advanced clients like -/// language servers can also retain the source files and [edit](Source::edit) -/// them in-place to benefit from better incremental performance. +/// 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 { /// The standard library. + /// + /// Can be created through `Library::build()`. fn library(&self) -> &Prehashed<Library>; /// Metadata about all known fonts. @@ -126,11 +196,6 @@ pub trait World { fn main(&self) -> Source; /// Try to access the specified source file. - /// - /// The returned `Source` file's [id](Source::id) does not have to match the - /// given `id`. Due to symlinks, two different file id's can point to the - /// same on-disk file. Implementors can deduplicate and return the same - /// `Source` if they want to, but do not have to. fn source(&self, id: FileId) -> FileResult<Source>; /// Try to access the specified file. @@ -172,3 +237,88 @@ impl<T: World> WorldExt for T { 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, +} + +impl Library { + /// Construct the standard library. + pub fn build() -> Self { + let math = math::module(); + let global = global(math.clone()); + Self { global, math, styles: Styles::new() } + } +} + +impl Default for Library { + fn default() -> Self { + Self::build() + } +} + +/// Construct the module with global definitions. +#[tracing::instrument(skip_all)] +fn global(math: Module) -> Module { + let mut global = Scope::deduplicating(); + self::foundations::define(&mut global); + self::model::define(&mut global); + self::text::define(&mut global); + 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", Align::START); + global.define("left", Align::LEFT); + global.define("center", Align::CENTER); + global.define("right", Align::RIGHT); + global.define("end", Align::END); + global.define("top", Align::TOP); + global.define("horizon", Align::HORIZON); + global.define("bottom", Align::BOTTOM); +} diff --git a/crates/typst/src/loading/cbor.rs b/crates/typst/src/loading/cbor.rs new file mode 100644 index 00000000..a1189dbe --- /dev/null +++ b/crates/typst/src/loading/cbor.rs @@ -0,0 +1,57 @@ +use ecow::{eco_format, EcoString}; + +use crate::diag::{At, SourceResult}; +use crate::eval::Vm; +use crate::foundations::{func, scope, Bytes, Value}; +use crate::syntax::Spanned; +use crate::World; + +/// Reads structured data from a CBOR file. +/// +/// The file must contain a valid CBOR serialization. Mappings will be +/// converted into Typst dictionaries, and sequences will be converted into +/// Typst arrays. Strings and booleans will be converted into the Typst +/// equivalents, null-values (`null`, `~` or empty ``) will be converted into +/// `{none}`, and numbers will be converted to floats or integers depending on +/// whether they are whole numbers. +#[func(scope, title = "CBOR")] +pub fn cbor( + /// The virtual machine. + vm: &mut Vm, + /// Path to a CBOR file. + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + cbor::decode(Spanned::new(data, span)) +} + +#[scope] +impl cbor { + /// Reads structured data from CBOR bytes. + #[func(title = "Decode CBOR")] + pub fn decode( + /// cbor data. + data: Spanned<Bytes>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + ciborium::from_reader(data.as_slice()) + .map_err(|err| eco_format!("failed to parse CBOR ({err})")) + .at(span) + } + + /// Encode structured data into CBOR bytes. + #[func(title = "Encode CBOR")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + ) -> SourceResult<Bytes> { + let Spanned { v: value, span } = value; + let mut res = Vec::new(); + ciborium::into_writer(&value, &mut res) + .map(|_| res.into()) + .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) + .at(span) + } +} diff --git a/crates/typst/src/loading/csv.rs b/crates/typst/src/loading/csv.rs new file mode 100644 index 00000000..e195f190 --- /dev/null +++ b/crates/typst/src/loading/csv.rs @@ -0,0 +1,118 @@ +use ecow::{eco_format, EcoString}; + +use crate::diag::{bail, At, SourceResult}; +use crate::eval::Vm; +use crate::foundations::{cast, func, scope, Array, IntoValue, Value}; +use crate::loading::Readable; +use crate::syntax::Spanned; +use crate::World; + +/// Reads structured data from a CSV file. +/// +/// The CSV file will be read and parsed into a 2-dimensional array of strings: +/// Each row in the CSV file will be represented as an array of strings, and all +/// rows will be collected into a single array. Header rows will not be +/// stripped. +/// +/// # Example +/// ```example +/// #let results = csv("data.csv") +/// +/// #table( +/// columns: 2, +/// [*Condition*], [*Result*], +/// ..results.flatten(), +/// ) +/// ``` +#[func(scope, title = "CSV")] +pub fn csv( + /// The virtual machine. + vm: &mut Vm, + /// Path to a CSV file. + path: Spanned<EcoString>, + /// The delimiter that separates columns in the CSV file. + /// Must be a single ASCII character. + #[named] + #[default] + delimiter: Delimiter, +) -> SourceResult<Array> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter) +} + +#[scope] +impl csv { + /// Reads structured data from a CSV string/bytes. + #[func(title = "Decode CSV")] + pub fn decode( + /// CSV data. + data: Spanned<Readable>, + /// The delimiter that separates columns in the CSV file. + /// Must be a single ASCII character. + #[named] + #[default] + delimiter: Delimiter, + ) -> SourceResult<Array> { + let Spanned { v: data, span } = data; + let mut builder = ::csv::ReaderBuilder::new(); + builder.has_headers(false); + builder.delimiter(delimiter.0 as u8); + let mut reader = builder.from_reader(data.as_slice()); + let mut array = Array::new(); + + for (line, result) in reader.records().enumerate() { + // Original solution use line from error, but that is incorrect with + // `has_headers` set to `false`. See issue: + // https://github.com/BurntSushi/rust-csv/issues/184 + let line = line + 1; // Counting lines from 1 + let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; + let sub = row.into_iter().map(|field| field.into_value()).collect(); + array.push(Value::Array(sub)) + } + + Ok(array) + } +} + +/// The delimiter to use when parsing CSV files. +pub struct Delimiter(char); + +impl Default for Delimiter { + fn default() -> Self { + Self(',') + } +} + +cast! { + Delimiter, + self => self.0.into_value(), + v: EcoString => { + let mut chars = v.chars(); + let first = chars.next().ok_or("delimiter must not be empty")?; + if chars.next().is_some() { + bail!("delimiter must be a single character"); + } + + if !first.is_ascii() { + bail!("delimiter must be an ASCII character"); + } + + Self(first) + }, +} + +/// Format the user-facing CSV error message. +fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString { + match err.kind() { + ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), + ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { + eco_format!( + "failed to parse CSV (found {len} instead of \ + {expected_len} fields in line {line})" + ) + } + _ => eco_format!("failed to parse CSV ({err})"), + } +} diff --git a/crates/typst/src/loading/json.rs b/crates/typst/src/loading/json.rs new file mode 100644 index 00000000..cf209971 --- /dev/null +++ b/crates/typst/src/loading/json.rs @@ -0,0 +1,94 @@ +use ecow::{eco_format, EcoString}; + +use crate::diag::{At, SourceResult}; +use crate::eval::Vm; +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. +/// +/// The file must contain a valid JSON object or array. JSON objects will be +/// converted into Typst dictionaries, and JSON arrays will be converted into +/// Typst arrays. Strings and booleans will be converted into the Typst +/// equivalents, `null` will be converted into `{none}`, and numbers will be +/// converted to floats or integers depending on whether they are whole numbers. +/// +/// The function returns a dictionary or an array, depending on the JSON file. +/// +/// The JSON files in the example contain objects with the keys `temperature`, +/// `unit`, and `weather`. +/// +/// # Example +/// ```example +/// #let forecast(day) = block[ +/// #box(square( +/// width: 2cm, +/// inset: 8pt, +/// fill: if day.weather == "sunny" { +/// yellow +/// } else { +/// aqua +/// }, +/// align( +/// bottom + right, +/// strong(day.weather), +/// ), +/// )) +/// #h(6pt) +/// #set text(22pt, baseline: -8pt) +/// #day.temperature °#day.unit +/// ] +/// +/// #forecast(json("monday.json")) +/// #forecast(json("tuesday.json")) +/// ``` +#[func(scope, title = "JSON")] +pub fn json( + /// The virtual machine. + vm: &mut Vm, + /// Path to a JSON file. + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + json::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl json { + /// Reads structured data from a JSON string/bytes. + #[func(title = "Decode JSON")] + pub fn decode( + /// JSON data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + serde_json::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse JSON ({err})")) + .at(span) + } + + /// Encodes structured data into a JSON string. + #[func(title = "Encode JSON")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + /// Whether to pretty print the JSON with newlines and indentation. + #[named] + #[default(true)] + pretty: bool, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + if pretty { + serde_json::to_string_pretty(&value) + } else { + serde_json::to_string(&value) + } + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as JSON ({err})")) + .at(span) + } +} diff --git a/crates/typst/src/loading/mod.rs b/crates/typst/src/loading/mod.rs new file mode 100644 index 00000000..dcfafb9e --- /dev/null +++ b/crates/typst/src/loading/mod.rs @@ -0,0 +1,82 @@ +//! Data loading. + +#[path = "cbor.rs"] +mod cbor_; +#[path = "csv.rs"] +mod csv_; +#[path = "json.rs"] +mod json_; +#[path = "read.rs"] +mod read_; +#[path = "toml.rs"] +mod toml_; +#[path = "xml.rs"] +mod xml_; +#[path = "yaml.rs"] +mod yaml_; + +pub use self::cbor_::*; +pub use self::csv_::*; +pub use self::json_::*; +pub use self::read_::*; +pub use self::toml_::*; +pub use self::xml_::*; +pub use self::yaml_::*; + +use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; + +/// Data loading from external files. +/// +/// These functions help you with loading and embedding data, for example from +/// the results of an experiment. +#[category] +pub static DATA_LOADING: Category; + +/// Hook up all `data-loading` definitions. +pub(super) fn define(global: &mut Scope) { + global.category(DATA_LOADING); + global.define_func::<read>(); + global.define_func::<csv>(); + global.define_func::<json>(); + global.define_func::<toml>(); + global.define_func::<yaml>(); + global.define_func::<cbor>(); + global.define_func::<xml>(); +} + +/// A value that can be read from a file. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum Readable { + /// A decoded string. + Str(Str), + /// Raw bytes. + Bytes(Bytes), +} + +impl Readable { + fn as_slice(&self) -> &[u8] { + match self { + Readable::Bytes(v) => v, + Readable::Str(v) => v.as_bytes(), + } + } +} + +cast! { + Readable, + self => match self { + Self::Str(v) => v.into_value(), + Self::Bytes(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Bytes => Self::Bytes(v), +} + +impl From<Readable> for Bytes { + fn from(value: Readable) -> Self { + match value { + Readable::Bytes(v) => v, + Readable::Str(v) => v.as_bytes().into(), + } + } +} diff --git a/crates/typst/src/loading/read.rs b/crates/typst/src/loading/read.rs new file mode 100644 index 00000000..49a86699 --- /dev/null +++ b/crates/typst/src/loading/read.rs @@ -0,0 +1,57 @@ +use ecow::EcoString; + +use crate::diag::{At, SourceResult}; +use crate::eval::Vm; +use crate::foundations::{func, Cast}; +use crate::loading::Readable; +use crate::syntax::Spanned; +use crate::World; + +/// Reads plain text or data from a file. +/// +/// By default, the file will be read as UTF-8 and returned as a [string]($str). +/// +/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead. +/// +/// # Example +/// ```example +/// An example for a HTML file: \ +/// #let text = read("data.html") +/// #raw(text, lang: "html") +/// +/// Raw bytes: +/// #read("tiger.jpg", encoding: none) +/// ``` +#[func] +pub fn read( + /// The virtual machine. + vm: &mut Vm, + /// Path to a file. + path: Spanned<EcoString>, + /// The encoding to read the file with. + /// + /// If set to `{none}`, this function returns raw bytes. + #[named] + #[default(Some(Encoding::Utf8))] + encoding: Option<Encoding>, +) -> SourceResult<Readable> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + Ok(match encoding { + None => Readable::Bytes(data), + Some(Encoding::Utf8) => Readable::Str( + std::str::from_utf8(&data) + .map_err(|_| "file is not valid utf-8") + .at(span)? + .into(), + ), + }) +} + +/// An encoding of a file. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Encoding { + /// The Unicode UTF-8 encoding. + Utf8, +} diff --git a/crates/typst/src/loading/toml.rs b/crates/typst/src/loading/toml.rs new file mode 100644 index 00000000..a4388f74 --- /dev/null +++ b/crates/typst/src/loading/toml.rs @@ -0,0 +1,90 @@ +use ecow::{eco_format, EcoString}; + +use crate::diag::{At, SourceResult}; +use crate::eval::Vm; +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. +/// +/// The file must contain a valid TOML table. TOML tables will be converted into +/// Typst dictionaries, and TOML arrays will be converted into Typst arrays. +/// Strings, booleans and datetimes will be converted into the Typst equivalents +/// and numbers will be converted to floats or integers depending on whether +/// they are whole numbers. +/// +/// The TOML file in the example consists of a table with the keys `title`, +/// `version`, and `authors`. +/// +/// # Example +/// ```example +/// #let details = toml("details.toml") +/// +/// Title: #details.title \ +/// Version: #details.version \ +/// Authors: #(details.authors +/// .join(", ", last: " and ")) +/// ``` +#[func(scope, title = "TOML")] +pub fn toml( + /// The virtual machine. + vm: &mut Vm, + /// Path to a TOML file. + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + toml::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl toml { + /// Reads structured data from a TOML string/bytes. + #[func(title = "Decode TOML")] + pub fn decode( + /// TOML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + let raw = std::str::from_utf8(data.as_slice()) + .map_err(|_| "file is not valid utf-8") + .at(span)?; + ::toml::from_str(raw) + .map_err(|err| format_toml_error(err, raw)) + .at(span) + } + + /// Encodes structured data into a TOML string. + #[func(title = "Encode TOML")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + /// Whether to pretty-print the resulting TOML. + #[named] + #[default(true)] + pretty: bool, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) } + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as TOML ({err})")) + .at(span) + } +} + +/// Format the user-facing TOML error message. +fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString { + if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) { + let line = head.lines().count(); + let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count(); + eco_format!( + "failed to parse TOML ({} at line {line} column {column})", + error.message(), + ) + } else { + eco_format!("failed to parse TOML ({})", error.message()) + } +} diff --git a/crates/typst/src/loading/xml.rs b/crates/typst/src/loading/xml.rs new file mode 100644 index 00000000..73aeb32a --- /dev/null +++ b/crates/typst/src/loading/xml.rs @@ -0,0 +1,116 @@ +use ecow::EcoString; + +use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; +use crate::eval::Vm; +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. +/// +/// The XML file is parsed into an array of dictionaries and strings. XML nodes +/// can be elements or strings. Elements are represented as dictionaries with +/// the the following keys: +/// +/// - `tag`: The name of the element as a string. +/// - `attrs`: A dictionary of the element's attributes as strings. +/// - `children`: An array of the element's child nodes. +/// +/// The XML file in the example contains a root `news` tag with multiple +/// `article` tags. Each article has a `title`, `author`, and `content` tag. The +/// `content` tag contains one or more paragraphs, which are represented as `p` +/// tags. +/// +/// # Example +/// ```example +/// #let find-child(elem, tag) = { +/// elem.children +/// .find(e => "tag" in e and e.tag == tag) +/// } +/// +/// #let article(elem) = { +/// let title = find-child(elem, "title") +/// let author = find-child(elem, "author") +/// let pars = find-child(elem, "content") +/// +/// heading(title.children.first()) +/// text(10pt, weight: "medium")[ +/// Published by +/// #author.children.first() +/// ] +/// +/// for p in pars.children { +/// if (type(p) == "dictionary") { +/// parbreak() +/// p.children.first() +/// } +/// } +/// } +/// +/// #let data = xml("example.xml") +/// #for elem in data.first().children { +/// if (type(elem) == "dictionary") { +/// article(elem) +/// } +/// } +/// ``` +#[func(scope, title = "XML")] +pub fn xml( + /// The virtual machine. + vm: &mut Vm, + /// Path to an XML file. + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + xml::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl xml { + /// Reads structured data from an XML string/bytes. + #[func(title = "Decode XML")] + pub fn decode( + /// XML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + let text = std::str::from_utf8(data.as_slice()) + .map_err(FileError::from) + .at(span)?; + let document = + roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?; + Ok(convert_xml(document.root())) + } +} + +/// Convert an XML node to a Typst value. +fn convert_xml(node: roxmltree::Node) -> Value { + if node.is_text() { + return node.text().unwrap_or_default().into_value(); + } + + let children: Array = node.children().map(convert_xml).collect(); + if node.is_root() { + return Value::Array(children); + } + + let tag: Str = node.tag_name().name().into(); + let attrs: Dict = node + .attributes() + .map(|attr| (attr.name().into(), attr.value().into_value())) + .collect(); + + Value::Dict(dict! { + "tag" => tag, + "attrs" => attrs, + "children" => children, + }) +} + +/// Format the user-facing XML error message. +fn format_xml_error(error: roxmltree::Error) -> EcoString { + format_xml_like_error("XML", error) +} diff --git a/crates/typst/src/loading/yaml.rs b/crates/typst/src/loading/yaml.rs new file mode 100644 index 00000000..03dd2a2c --- /dev/null +++ b/crates/typst/src/loading/yaml.rs @@ -0,0 +1,78 @@ +use ecow::{eco_format, EcoString}; + +use crate::diag::{At, SourceResult}; +use crate::eval::Vm; +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. +/// +/// The file must contain a valid YAML object or array. YAML mappings will be +/// converted into Typst dictionaries, and YAML sequences will be converted into +/// Typst arrays. Strings and booleans will be converted into the Typst +/// equivalents, null-values (`null`, `~` or empty ``) will be converted into +/// `{none}`, and numbers will be converted to floats or integers depending on +/// whether they are whole numbers. Custom YAML tags are ignored, though the +/// loaded value will still be present. +/// +/// The YAML files in the example contain objects with authors as keys, +/// each with a sequence of their own submapping with the keys +/// "title" and "published" +/// +/// # Example +/// ```example +/// #let bookshelf(contents) = { +/// for (author, works) in contents { +/// author +/// for work in works [ +/// - #work.title (#work.published) +/// ] +/// } +/// } +/// +/// #bookshelf( +/// yaml("scifi-authors.yaml") +/// ) +/// ``` +#[func(scope, title = "YAML")] +pub fn yaml( + /// The virtual machine. + vm: &mut Vm, + /// Path to a YAML file. + path: Spanned<EcoString>, +) -> SourceResult<Value> { + let Spanned { v: path, span } = path; + let id = vm.resolve_path(&path).at(span)?; + let data = vm.world().file(id).at(span)?; + yaml::decode(Spanned::new(Readable::Bytes(data), span)) +} + +#[scope] +impl yaml { + /// Reads structured data from a YAML string/bytes. + #[func(title = "Decode YAML")] + pub fn decode( + /// YAML data. + data: Spanned<Readable>, + ) -> SourceResult<Value> { + let Spanned { v: data, span } = data; + serde_yaml::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse YAML ({err})")) + .at(span) + } + + /// Encode structured data into a YAML string. + #[func(title = "Encode YAML")] + pub fn encode( + /// Value to be encoded. + value: Spanned<Value>, + ) -> SourceResult<Str> { + let Spanned { v: value, span } = value; + serde_yaml::to_string(&value) + .map(|v| v.into()) + .map_err(|err| eco_format!("failed to encode value as YAML ({err})")) + .at(span) + } +} diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst/src/math/accent.rs index 1b2d4793..0480567c 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst/src/math/accent.rs @@ -1,4 +1,14 @@ -use crate::math::*; +use ttf_parser::GlyphId; +use unicode_math_class::MathClass; + +use crate::diag::{bail, SourceResult}; +use crate::foundations::{cast, elem, Content, NativeElement, Value}; +use crate::layout::{Abs, Em, Frame, Point, Size}; +use crate::math::{ + FrameFragment, GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, +}; +use crate::symbols::Symbol; +use crate::text::TextElem; /// How much the accent can be shorter than the base. const ACCENT_SHORT_FALL: Em = Em::new(0.5); diff --git a/crates/typst-library/src/math/align.rs b/crates/typst/src/math/align.rs index 4192e97b..5147134e 100644 --- a/crates/typst-library/src/math/align.rs +++ b/crates/typst/src/math/align.rs @@ -1,4 +1,7 @@ -use crate::math::*; +use crate::diag::SourceResult; +use crate::foundations::elem; +use crate::layout::Abs; +use crate::math::{LayoutMath, MathContext, MathFragment, MathRow}; /// A math alignment point: `&`, `&&`. #[elem(title = "Alignment Point", LayoutMath)] diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst/src/math/attach.rs index 3e6b69f2..71074edc 100644 --- a/crates/typst-library/src/math/attach.rs +++ b/crates/typst/src/math/attach.rs @@ -1,4 +1,12 @@ -use super::*; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, StyleChain}; +use crate::layout::{Abs, Frame, FrameItem, Point, Size}; +use crate::math::{ + FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, +}; +use crate::text::TextElem; /// A base with optional attachments. /// diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst/src/math/cancel.rs index 455750f7..7a72f44a 100644 --- a/crates/typst-library/src/math/cancel.rs +++ b/crates/typst/src/math/cancel.rs @@ -1,4 +1,14 @@ -use super::*; +use unicode_math_class::MathClass; + +use crate::diag::{At, SourceResult}; +use crate::foundations::{cast, elem, Content, Func, NativeElement, Resolve, Smart}; +use crate::layout::{ + Abs, Angle, Frame, FrameItem, Length, Point, Ratio, Rel, Size, Transform, +}; +use crate::math::{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. /// diff --git a/crates/typst-library/src/math/class.rs b/crates/typst/src/math/class.rs index d2c5192d..88424eed 100644 --- a/crates/typst-library/src/math/class.rs +++ b/crates/typst/src/math/class.rs @@ -1,4 +1,8 @@ -use super::*; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::foundations::{elem, Content}; +use crate::math::{LayoutMath, MathContext}; /// Forced use of a certain math class. /// diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst/src/math/ctx.rs index 789bd332..0f7ad933 100644 --- a/crates/typst-library/src/math/ctx.rs +++ b/crates/typst/src/math/ctx.rs @@ -1,13 +1,27 @@ use comemo::Prehashed; -use ttf_parser::gsub::SubstitutionSubtable; +use ecow::EcoString; +use rustybuzz::Feature; +use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::math::MathValue; -use typst::font::{FontStyle, FontWeight}; -use typst::model::realize; -use typst::syntax::is_newline; +use ttf_parser::opentype_layout::LayoutTable; +use ttf_parser::GlyphId; +use unicode_math_class::MathClass; use unicode_segmentation::UnicodeSegmentation; -use super::*; -use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; +use crate::diag::SourceResult; +use crate::foundations::{Content, NativeElement, Smart, StyleChain, Styles}; +use crate::layout::{Abs, Axes, BoxElem, Em, Frame, Layout, Regions, Size, Vt}; +use crate::math::{ + FrameFragment, GlyphFragment, LayoutMath, MathFragment, MathRow, MathSize, MathStyle, + MathVariant, THICK, +}; +use crate::model::ParElem; +use crate::realize::realize; +use crate::syntax::{is_newline, Span}; +use crate::text::{ + features, variant, BottomEdge, BottomEdgeMetric, Font, FontStyle, FontWeight, + TextElem, TextSize, TopEdge, TopEdgeMetric, +}; macro_rules! scaled { ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => { @@ -71,7 +85,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { _ => None, }); - let features = tags(styles); + let features = features(styles); let glyphwise_tables = gsub_table.map(|gsub| { features .into_iter() @@ -333,3 +347,51 @@ impl Scaled for MathValue<'_> { self.value.scaled(ctx) } } + +/// 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-library/src/math/mod.rs b/crates/typst/src/math/equation.rs index 7ced638b..fb58a5b7 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst/src/math/equation.rs @@ -1,116 +1,23 @@ -//! Mathematical formulas. - -#[macro_use] -mod ctx; -mod accent; -mod align; -mod attach; -mod cancel; -mod class; -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::*; -pub use self::align::*; -pub use self::attach::*; -pub use self::cancel::*; -pub use self::class::*; -pub use self::frac::*; -pub use self::lr::*; -pub use self::matrix::*; -pub use self::op::*; -pub use self::root::*; -pub use self::style::*; -pub use self::underover::*; - -use std::borrow::Cow; - -use ttf_parser::{GlyphId, Rect}; -use typst::eval::{Module, Scope}; -use typst::font::{Font, FontWeight}; -use typst::model::Guard; -use typst::util::option_eq; -use unicode_math_class::MathClass; - -use self::ctx::*; -use self::fragment::*; -use self::row::*; -use self::spacing::*; -use crate::layout::{AlignElem, BoxElem, HElem, ParElem, Spacing}; -use crate::meta::{ - Count, Counter, CounterUpdate, LocalNameIn, Numbering, Outlinable, Refable, - Supplement, +use std::num::NonZeroUsize; + +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + elem, Content, Finalize, Guard, NativeElement, Resolve, Show, Smart, StyleChain, + Synthesize, +}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::layout::{ + Abs, Align, AlignElem, Axes, Dir, Em, FixedAlign, Fragment, Layout, Point, Regions, + Size, Vt, }; -use crate::prelude::*; -use crate::shared::BehavedBuilder; +use crate::math::{LayoutMath, MathContext}; +use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; use crate::text::{ - families, variant, FontFamily, FontList, LinebreakElem, SpaceElem, TextElem, TextSize, + families, variant, FontFamily, FontList, FontWeight, Lang, LocalName, Region, + TextElem, }; - -/// Create a module with all math definitions. -pub fn module() -> Module { - let mut math = Scope::deduplicating(); - math.category("math"); - math.define_elem::<EquationElem>(); - math.define_elem::<TextElem>(); - math.define_elem::<LrElem>(); - math.define_elem::<AttachElem>(); - math.define_elem::<ScriptsElem>(); - math.define_elem::<LimitsElem>(); - math.define_elem::<AccentElem>(); - math.define_elem::<UnderlineElem>(); - math.define_elem::<OverlineElem>(); - math.define_elem::<UnderbraceElem>(); - math.define_elem::<OverbraceElem>(); - math.define_elem::<UnderbracketElem>(); - math.define_elem::<OverbracketElem>(); - math.define_elem::<CancelElem>(); - math.define_elem::<FracElem>(); - math.define_elem::<BinomElem>(); - math.define_elem::<VecElem>(); - math.define_elem::<MatElem>(); - math.define_elem::<CasesElem>(); - math.define_elem::<RootElem>(); - math.define_elem::<ClassElem>(); - math.define_elem::<OpElem>(); - math.define_func::<abs>(); - math.define_func::<norm>(); - math.define_func::<floor>(); - math.define_func::<ceil>(); - math.define_func::<round>(); - math.define_func::<sqrt>(); - math.define_func::<upright>(); - math.define_func::<bold>(); - math.define_func::<italic>(); - math.define_func::<serif>(); - math.define_func::<sans>(); - math.define_func::<cal>(); - math.define_func::<frak>(); - math.define_func::<mono>(); - math.define_func::<bb>(); - math.define_func::<display>(); - math.define_func::<inline>(); - math.define_func::<script>(); - math.define_func::<sscript>(); - - // Text operators, spacings, and symbols. - op::define(&mut math); - spacing::define(&mut math); - for (name, symbol) in crate::symbols::SYM { - math.define(*name, symbol.clone()); - } - - Module::new("math", math) -} +use crate::util::{option_eq, NonZeroExt, Numeric}; +use crate::World; /// A mathematical equation. /// @@ -394,107 +301,9 @@ impl Outlinable for EquationElem { } } -pub trait LayoutMath { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>; -} - impl LayoutMath for EquationElem { #[tracing::instrument(skip(ctx))] fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { self.body().layout_math(ctx) } } - -impl LayoutMath for Content { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - // Directly layout the body of nested equations instead of handling it - // like a normal equation so that things like this work: - // ``` - // #let my = $pi$ - // $ my r^2 $ - // ``` - if let Some(elem) = self.to::<EquationElem>() { - return elem.layout_math(ctx); - } - - if let Some(realized) = ctx.realize(self)? { - return realized.layout_math(ctx); - } - - if self.is_sequence() { - let mut bb = BehavedBuilder::new(); - self.sequence_recursive_for_each(&mut |child: &Content| { - bb.push(Cow::Owned(child.clone()), StyleChain::default()) - }); - - for (child, _) in bb.finish().0.iter() { - child.layout_math(ctx)?; - } - return Ok(()); - } - - if let Some((elem, styles)) = self.to_styled() { - if TextElem::font_in(ctx.styles().chain(styles)) - != TextElem::font_in(ctx.styles()) - { - let frame = ctx.layout_content(self)?; - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - return Ok(()); - } - - let prev_map = std::mem::replace(&mut ctx.local, styles.clone()); - let prev_size = ctx.size; - ctx.local.apply(prev_map.clone()); - ctx.size = TextElem::size_in(ctx.styles()); - elem.layout_math(ctx)?; - ctx.size = prev_size; - ctx.local = prev_map; - return Ok(()); - } - - if self.is::<SpaceElem>() { - ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx))); - return Ok(()); - } - - if self.is::<LinebreakElem>() { - ctx.push(MathFragment::Linebreak); - return Ok(()); - } - - if let Some(elem) = self.to::<HElem>() { - if let Spacing::Rel(rel) = elem.amount() { - if rel.rel.is_zero() { - ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles()))); - } - } - return Ok(()); - } - - if let Some(elem) = self.to::<TextElem>() { - let fragment = ctx.layout_text(elem)?; - ctx.push(fragment); - return Ok(()); - } - - if let Some(boxed) = self.to::<BoxElem>() { - let frame = ctx.layout_box(boxed)?; - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - return Ok(()); - } - - if let Some(elem) = self.with::<dyn LayoutMath>() { - return elem.layout_math(ctx); - } - - let mut frame = ctx.layout_content(self)?; - if !frame.has_baseline() { - let axis = scaled!(ctx, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - } - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - - Ok(()) - } -} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst/src/math/frac.rs index c3014178..442cc18e 100644 --- a/crates/typst-library/src/math/frac.rs +++ b/crates/typst/src/math/frac.rs @@ -1,4 +1,13 @@ -use super::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{elem, Content, NativeElement, Value}; +use crate::layout::{Em, Frame, FrameItem, Point, Size}; +use crate::math::{ + FrameFragment, GlyphFragment, LayoutMath, MathContext, MathSize, Scaled, + 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); diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst/src/math/fragment.rs index 76ee2512..fb1420bf 100644 --- a/crates/typst-library/src/math/fragment.rs +++ b/crates/typst/src/math/fragment.rs @@ -1,10 +1,17 @@ -use rustybuzz::Feature; -use ttf_parser::gsub::{ - AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable, -}; -use ttf_parser::opentype_layout::LayoutTable; +use std::fmt::{self, Debug, Formatter}; -use super::*; +use smallvec::SmallVec; +use ttf_parser::gsub::AlternateSet; +use ttf_parser::{GlyphId, Rect}; +use unicode_math_class::MathClass; + +use crate::foundations::Smart; +use crate::introspection::{Meta, MetaElem}; +use crate::layout::{Abs, Corner, Em, Frame, FrameItem, Point, Size}; +use crate::math::{Limits, MathContext, MathStyle, Scaled}; +use crate::syntax::Span; +use crate::text::{Font, Glyph, Lang, TextElem, TextItem}; +use crate::visualize::Paint; #[derive(Debug, Clone)] pub enum MathFragment { @@ -463,51 +470,3 @@ fn kern_at_height( Some(kern.kern(i)?.scaled(ctx)) } - -/// 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-library/src/math/lr.rs b/crates/typst/src/math/lr.rs index 39143620..93e5a43c 100644 --- a/crates/typst-library/src/math/lr.rs +++ b/crates/typst/src/math/lr.rs @@ -1,4 +1,10 @@ -use super::*; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::foundations::{elem, func, Content, NativeElement, Resolve, Smart}; +use crate::layout::{Abs, Em, Length, Rel}; +use crate::math::{GlyphFragment, 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); diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst/src/math/matrix.rs index b5d21ed6..a6b30f3c 100644 --- a/crates/typst-library/src/math/matrix.rs +++ b/crates/typst/src/math/matrix.rs @@ -1,9 +1,25 @@ -use super::*; +use smallvec::{smallvec, SmallVec}; + +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::foundations::{ + cast, dict, elem, Array, Cast, Content, Dict, Fold, NativeElement, Resolve, Smart, + StyleChain, Value, +}; +use crate::layout::{ + Abs, Axes, Em, FixedAlign, Frame, FrameItem, Length, Point, Ratio, Rel, Size, +}; +use crate::math::{ + alignments, stack, AlignmentResult, FrameFragment, GlyphFragment, LayoutMath, + MathContext, Scaled, DELIM_SHORT_FALL, +}; +use crate::syntax::{Span, Spanned}; +use crate::text::TextElem; +use crate::util::Numeric; +use crate::visualize::{FixedStroke, Geometry, LineCap, Shape, Stroke}; const DEFAULT_ROW_GAP: Em = Em::new(0.5); 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. @@ -407,7 +423,7 @@ fn layout_mat_body( (v.hline, v.vline, stroke) } - _ => (Offsets::default(), Offsets::default(), default_stroke), + _ => (AugmentOffsets::default(), AugmentOffsets::default(), default_stroke), }; let ncols = rows.first().map_or(0, |row| row.len()); @@ -562,8 +578,8 @@ fn layout_delimiters( /// should be drawn on a matrix. #[derive(Debug, Default, Clone, PartialEq, Hash)] pub struct Augment<T: Numeric = Length> { - pub hline: Offsets, - pub vline: Offsets, + pub hline: AugmentOffsets, + pub vline: AugmentOffsets, pub stroke: Smart<Stroke<T>>, } @@ -620,15 +636,15 @@ cast! { d.into_value() }, v: isize => Augment { - hline: Offsets::default(), - vline: Offsets(smallvec![v]), + hline: AugmentOffsets::default(), + vline: AugmentOffsets(smallvec![v]), stroke: Smart::Auto, }, mut dict: Dict => { // need the transpose for the defaults to work - let hline = dict.take("hline").ok().map(Offsets::from_value) + let hline = dict.take("hline").ok().map(AugmentOffsets::from_value) .transpose().unwrap_or_default().unwrap_or_default(); - let vline = dict.take("vline").ok().map(Offsets::from_value) + let vline = dict.take("vline").ok().map(AugmentOffsets::from_value) .transpose().unwrap_or_default().unwrap_or_default(); let stroke = dict.take("stroke").ok().map(Stroke::from_value) @@ -645,10 +661,10 @@ cast! { /// The offsets at which augmentation lines should be drawn on a matrix. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct Offsets(SmallVec<[isize; 1]>); +pub struct AugmentOffsets(SmallVec<[isize; 1]>); cast! { - Offsets, + AugmentOffsets, self => self.0.into_value(), v: isize => Self(smallvec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs new file mode 100644 index 00000000..d84d9b40 --- /dev/null +++ b/crates/typst/src/math/mod.rs @@ -0,0 +1,311 @@ +//! Mathematical formulas. + +#[macro_use] +mod ctx; +mod accent; +mod align; +mod attach; +mod cancel; +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::*; +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::style::*; +pub use self::underover::*; + +use self::ctx::*; +use self::fragment::*; +use self::row::*; +use self::spacing::*; + +use std::borrow::Cow; + +use crate::diag::SourceResult; +use crate::foundations::{ + category, Category, Content, Module, Resolve, Scope, StyleChain, +}; +use crate::layout::{BoxElem, HElem, Spacing}; +use crate::realize::BehavedBuilder; +use crate::text::{LinebreakElem, SpaceElem, TextElem}; + +/// Typst has special [syntax]($syntax/#math) and library functions to typeset +/// mathematical formulas. Math formulas can be displayed inline with text or as +/// separate blocks. They will be typeset into their own block if they start and +/// end with at least one space (e.g. `[$ x^2 $]`). +/// +/// # Variables +/// In math, single letters are always displayed as is. Multiple letters, +/// however, are interpreted as variables and functions. To display multiple +/// letters verbatim, you can place them into quotes and to access single letter +/// variables, you can use the [hash syntax]($scripting/#expressions). +/// +/// ```example +/// $ A = pi r^2 $ +/// $ "area" = pi dot "radius"^2 $ +/// $ cal(A) := +/// { x in RR | x "is natural" } $ +/// #let x = 5 +/// $ #x < 17 $ +/// ``` +/// +/// # Symbols +/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like +/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in +/// different variants. You can select between different variants by applying +/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of +/// shorthand sequences like `=>` that approximate a symbol. When such a +/// shorthand exists, the symbol's documentation lists it. +/// +/// ```example +/// $ x < y => x gt.eq.not y $ +/// ``` +/// +/// # Line Breaks +/// Formulas can also contain line breaks. Each line can contain one or multiple +/// _alignment points_ (`&`) which are then aligned. +/// +/// ```example +/// $ sum_(k=0)^n k +/// &= 1 + ... + n \ +/// &= (n(n+1)) / 2 $ +/// ``` +/// +/// # Function calls +/// Math mode supports special function calls without the hash prefix. In these +/// "math calls", the argument list works a little differently than in code: +/// +/// - Within them, Typst is still in "math mode". Thus, you can write math +/// directly into them, but need to use hash syntax to pass code expressions +/// (except for strings, which are available in the math syntax). +/// - They support positional and named arguments, but don't support trailing +/// content blocks and argument spreading. +/// - They provide additional syntax for 2-dimensional argument lists. The +/// semicolon (`;`) merges preceding arguments separated by commas into an +/// array argument. +/// +/// ```example +/// $ frac(a^2, 2) $ +/// $ vec(1, 2, delim: "[") $ +/// $ mat(1, 2; 3, 4) $ +/// $ lim_x = +/// op("lim", limits: #true)_x $ +/// ``` +/// +/// To write a verbatim comma or semicolon in a math call, escape it with a +/// backslash. The colon on the other hand is only recognized in a special way +/// if directly preceded by an identifier, so to display it verbatim in those +/// cases, you can just insert a space before it. +/// +/// Functions calls preceded by a hash are normal code function calls and not +/// affected by these rules. +/// +/// # Alignment +/// When equations include multiple _alignment points_ (`&`), this creates +/// blocks of alternatingly right- and left-aligned columns. In the example +/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is +/// left-aligned. The word "given" is also left-aligned because `&&` creates two +/// alignment points in a row, alternating the alignment twice. `& &` and `&&` +/// behave exactly the same way. Meanwhile, "multiply by 7" is left-aligned +/// because just one `&` precedes it. Each alignment point simply alternates +/// between right-aligned/left-aligned. +/// +/// ```example +/// $ (3x + y) / 7 &= 9 && "given" \ +/// 3x + y &= 63 & "multiply by 7" \ +/// 3x &= 63 - y && "subtract y" \ +/// x &= 21 - y/3 & "divide by 3" $ +/// ``` +/// +/// # Math fonts +/// You can set the math font by with a [show-set rule]($styling/#show-rules) as +/// demonstrated below. Note that only special OpenType math fonts are suitable +/// for typesetting maths. +/// +/// ```example +/// #show math.equation: set text(font: "Fira Math") +/// $ sum_(i in NN) 1 + i $ +/// ``` +/// +/// # Math module +/// All math functions are part of the `math` [module]($scripting/#modules), +/// which is available by default in equations. Outside of equations, they can +/// be accessed with the `math.` prefix. +#[category] +pub static MATH: Category; + +/// Create a module with all math definitions. +pub fn module() -> Module { + let mut math = Scope::deduplicating(); + math.category(MATH); + math.define_elem::<EquationElem>(); + math.define_elem::<TextElem>(); + math.define_elem::<LrElem>(); + math.define_elem::<AttachElem>(); + math.define_elem::<ScriptsElem>(); + math.define_elem::<LimitsElem>(); + math.define_elem::<AccentElem>(); + math.define_elem::<UnderlineElem>(); + math.define_elem::<OverlineElem>(); + math.define_elem::<UnderbraceElem>(); + math.define_elem::<OverbraceElem>(); + math.define_elem::<UnderbracketElem>(); + math.define_elem::<OverbracketElem>(); + math.define_elem::<CancelElem>(); + math.define_elem::<FracElem>(); + math.define_elem::<BinomElem>(); + math.define_elem::<VecElem>(); + math.define_elem::<MatElem>(); + math.define_elem::<CasesElem>(); + math.define_elem::<RootElem>(); + math.define_elem::<ClassElem>(); + math.define_elem::<OpElem>(); + math.define_func::<abs>(); + math.define_func::<norm>(); + math.define_func::<floor>(); + math.define_func::<ceil>(); + math.define_func::<round>(); + math.define_func::<sqrt>(); + math.define_func::<upright>(); + math.define_func::<bold>(); + math.define_func::<italic>(); + math.define_func::<serif>(); + math.define_func::<sans>(); + math.define_func::<cal>(); + math.define_func::<frak>(); + math.define_func::<mono>(); + math.define_func::<bb>(); + math.define_func::<display>(); + math.define_func::<inline>(); + math.define_func::<script>(); + math.define_func::<sscript>(); + + // Text operators, spacings, and symbols. + op::define(&mut math); + spacing::define(&mut math); + for (name, symbol) in crate::symbols::SYM { + math.define(*name, symbol.clone()); + } + + 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) -> SourceResult<()>; +} + +impl LayoutMath for Content { + #[tracing::instrument(skip(ctx))] + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + // Directly layout the body of nested equations instead of handling it + // like a normal equation so that things like this work: + // ``` + // #let my = $pi$ + // $ my r^2 $ + // ``` + if let Some(elem) = self.to::<EquationElem>() { + return elem.layout_math(ctx); + } + + if let Some(realized) = ctx.realize(self)? { + return realized.layout_math(ctx); + } + + if self.is_sequence() { + let mut bb = BehavedBuilder::new(); + self.sequence_recursive_for_each(&mut |child: &Content| { + bb.push(Cow::Owned(child.clone()), StyleChain::default()) + }); + + for (child, _) in bb.finish().0.iter() { + child.layout_math(ctx)?; + } + return Ok(()); + } + + if let Some((elem, styles)) = self.to_styled() { + if TextElem::font_in(ctx.styles().chain(styles)) + != TextElem::font_in(ctx.styles()) + { + let frame = ctx.layout_content(self)?; + ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); + return Ok(()); + } + + let prev_map = std::mem::replace(&mut ctx.local, styles.clone()); + let prev_size = ctx.size; + ctx.local.apply(prev_map.clone()); + ctx.size = TextElem::size_in(ctx.styles()); + elem.layout_math(ctx)?; + ctx.size = prev_size; + ctx.local = prev_map; + return Ok(()); + } + + if self.is::<SpaceElem>() { + ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx))); + return Ok(()); + } + + if self.is::<LinebreakElem>() { + ctx.push(MathFragment::Linebreak); + return Ok(()); + } + + if let Some(elem) = self.to::<HElem>() { + if let Spacing::Rel(rel) = elem.amount() { + if rel.rel.is_zero() { + ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles()))); + } + } + return Ok(()); + } + + if let Some(elem) = self.to::<TextElem>() { + let fragment = ctx.layout_text(elem)?; + ctx.push(fragment); + return Ok(()); + } + + if let Some(boxed) = self.to::<BoxElem>() { + let frame = ctx.layout_box(boxed)?; + ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); + return Ok(()); + } + + if let Some(elem) = self.with::<dyn LayoutMath>() { + return elem.layout_math(ctx); + } + + let mut frame = ctx.layout_content(self)?; + if !frame.has_baseline() { + let axis = scaled!(ctx, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + } + ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); + + Ok(()) + } +} diff --git a/crates/typst-library/src/math/op.rs b/crates/typst/src/math/op.rs index 9e35d207..2670e26d 100644 --- a/crates/typst-library/src/math/op.rs +++ b/crates/typst/src/math/op.rs @@ -1,4 +1,11 @@ -use super::*; +use ecow::EcoString; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, NativeElement, Scope}; +use crate::layout::HElem; +use crate::math::{FrameFragment, LayoutMath, Limits, MathContext, MathStyleElem, THIN}; +use crate::text::TextElem; /// A text operator in an equation. /// diff --git a/crates/typst-library/src/math/root.rs b/crates/typst/src/math/root.rs index ba918ea9..68df6b24 100644 --- a/crates/typst-library/src/math/root.rs +++ b/crates/typst/src/math/root.rs @@ -1,4 +1,12 @@ -use super::*; +use crate::diag::SourceResult; +use crate::foundations::{elem, func, Content, NativeElement}; +use crate::layout::{Abs, Frame, FrameItem, Point, Size}; +use crate::math::{ + FrameFragment, GlyphFragment, LayoutMath, MathContext, MathSize, Scaled, +}; +use crate::syntax::Span; +use crate::text::TextElem; +use crate::visualize::{FixedStroke, Geometry}; /// A square root. /// diff --git a/crates/typst-library/src/math/row.rs b/crates/typst/src/math/row.rs index 70813598..cd75e7c3 100644 --- a/crates/typst-library/src/math/row.rs +++ b/crates/typst/src/math/row.rs @@ -1,8 +1,14 @@ use std::iter::once; -use crate::layout::AlignElem; - -use super::*; +use unicode_math_class::MathClass; + +use crate::foundations::Resolve; +use crate::layout::{Abs, AlignElem, Em, FixedAlign, Frame, Point, Size}; +use crate::math::{ + alignments, spacing, AlignmentResult, FrameFragment, MathContext, MathFragment, + MathSize, Scaled, +}; +use crate::model::ParElem; pub const TIGHT_LEADING: Em = Em::new(0.25); diff --git a/crates/typst-library/src/math/spacing.rs b/crates/typst/src/math/spacing.rs index 3dfce024..f358aa50 100644 --- a/crates/typst-library/src/math/spacing.rs +++ b/crates/typst/src/math/spacing.rs @@ -1,4 +1,8 @@ -use super::*; +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); diff --git a/crates/typst-library/src/math/stretch.rs b/crates/typst/src/math/stretch.rs index e9bf6890..a8e22af4 100644 --- a/crates/typst-library/src/math/stretch.rs +++ b/crates/typst/src/math/stretch.rs @@ -1,7 +1,8 @@ use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::LazyArray16; -use super::*; +use crate::layout::{Abs, Frame, Point, Size}; +use crate::math::{GlyphFragment, MathContext, Scaled, VariantFragment}; /// Maximum number of times extenders can be repeated. const MAX_REPEATS: usize = 1024; diff --git a/crates/typst-library/src/math/style.rs b/crates/typst/src/math/style.rs index 774fadac..631fe967 100644 --- a/crates/typst-library/src/math/style.rs +++ b/crates/typst/src/math/style.rs @@ -1,4 +1,8 @@ -use super::*; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::foundations::{elem, func, Cast, Content, NativeElement, Smart, StyleChain}; +use crate::math::{LayoutMath, MathContext}; /// Bold font style in math. /// diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst/src/math/underover.rs index 6fc76830..f94549ea 100644 --- a/crates/typst-library/src/math/underover.rs +++ b/crates/typst/src/math/underover.rs @@ -1,4 +1,15 @@ -use super::*; +use unicode_math_class::MathClass; + +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, NativeElement}; +use crate::layout::{Abs, Em, FixedAlign, Frame, FrameItem, Point, Size}; +use crate::math::{ + alignments, AlignmentResult, FrameFragment, GlyphFragment, LayoutMath, MathContext, + MathRow, 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); diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst/src/model/bibliography.rs index b84c330a..5c260300 100644 --- a/crates/typst-library/src/meta/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -2,34 +2,43 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; +use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; -use comemo::Prehashed; -use ecow::EcoVec; -use hayagriva::citationberg; +use comemo::{Prehashed, Tracked}; +use ecow::{eco_format, EcoString, EcoVec}; use hayagriva::io::BibLaTeXError; use hayagriva::{ - BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, + citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, SpecificLocator, }; use indexmap::IndexMap; use once_cell::sync::Lazy; -use smallvec::SmallVec; +use smallvec::{smallvec, SmallVec}; use typed_arena::Arena; -use typst::diag::FileError; -use typst::eval::{eval_string, Bytes, CastInfo, EvalMode, Reflect}; -use typst::font::FontStyle; -use typst::util::{option_eq, PicoStr}; +use crate::diag::{bail, error, At, FileError, SourceResult, StrResult}; +use crate::eval::{eval_string, EvalMode, Vm}; +use crate::foundations::{ + cast, elem, ty, Args, Array, Bytes, CastInfo, Content, Finalize, FromValue, + IntoValue, Label, NativeElement, Reflect, Repr, Scope, Show, Smart, Str, StyleChain, + Synthesize, Type, Value, +}; +use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ - BlockElem, GridElem, HElem, PadElem, ParElem, Sizing, TrackSizings, VElem, + BlockElem, Em, GridElem, HElem, PadElem, Sizing, TrackSizings, VElem, Vt, +}; +use crate::model::{ + CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, }; -use crate::meta::{ - CitationForm, CiteGroup, FootnoteElem, HeadingElem, LinkElem, LocalName, LocalNameIn, + +use crate::syntax::{Span, Spanned}; +use crate::text::{ + FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta, }; -use crate::prelude::*; -use crate::text::{Delta, SubElem, SuperElem, TextElem}; +use crate::util::{option_eq, NonZeroExt, PicoStr}; +use crate::World; /// A bibliography / reference listing. /// @@ -81,7 +90,7 @@ pub struct BibliographyElem { let (paths, bibliography) = Bibliography::parse(vm, args)?; paths )] - pub path: BibPaths, + pub path: BibliographyPaths, /// The title of the bibliography. /// @@ -133,10 +142,10 @@ pub struct BibliographyElem { /// A list of bibliography file paths. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct BibPaths(Vec<EcoString>); +pub struct BibliographyPaths(Vec<EcoString>); cast! { - BibPaths, + BibliographyPaths, self => self.0.into_value(), v: EcoString => Self(vec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), @@ -305,9 +314,12 @@ pub struct Bibliography { impl Bibliography { /// Parse the bibliography argument. - fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult<(BibPaths, Bibliography)> { + fn parse( + vm: &mut Vm, + args: &mut Args, + ) -> SourceResult<(BibliographyPaths, Bibliography)> { let Spanned { v: paths, span } = - args.expect::<Spanned<BibPaths>>("path to bibliography file")?; + args.expect::<Spanned<BibliographyPaths>>("path to bibliography file")?; // Load bibliography files. let data = paths @@ -327,7 +339,7 @@ impl Bibliography { /// Load bibliography entries from paths. #[comemo::memoize] - fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult<Bibliography> { + fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult<Bibliography> { let mut map = IndexMap::new(); let mut duplicates = Vec::<EcoString>::new(); @@ -366,7 +378,7 @@ impl Bibliography { Ok(Bibliography { map: Arc::new(map), - hash: typst::util::hash128(data), + hash: crate::util::hash128(data), }) } @@ -997,10 +1009,10 @@ fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Con match format.font_weight { citationberg::FontWeight::Normal => {} citationberg::FontWeight::Bold => { - content = content.styled(TextElem::set_delta(Delta(300))); + content = content.styled(TextElem::set_delta(WeightDelta(300))); } citationberg::FontWeight::Light => { - content = content.styled(TextElem::set_delta(Delta(-100))); + content = content.styled(TextElem::set_delta(WeightDelta(-100))); } } diff --git a/crates/typst-library/src/meta/cite.rs b/crates/typst/src/model/cite.rs index c0bd71ae..7ead5907 100644 --- a/crates/typst-library/src/meta/cite.rs +++ b/crates/typst/src/model/cite.rs @@ -1,7 +1,12 @@ -use crate::meta::bibliography::Works; -use crate::meta::CslStyle; -use crate::prelude::*; -use crate::text::TextElem; +use crate::diag::{bail, At, SourceResult}; +use crate::foundations::{ + cast, elem, Cast, Content, Label, NativeElement, Show, Smart, StyleChain, Synthesize, +}; +use crate::introspection::Locatable; +use crate::layout::Vt; +use crate::model::bibliography::Works; +use crate::model::CslStyle; +use crate::text::{Lang, Region, TextElem}; /// Cite a work from the bibliography. /// diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst/src/model/document.rs index 581be60c..f6ea7d24 100644 --- a/crates/typst-library/src/meta/document.rs +++ b/crates/typst/src/model/document.rs @@ -1,9 +1,13 @@ use comemo::Prehashed; -use typst::eval::Datetime; +use ecow::EcoString; -use crate::layout::{LayoutRoot, PageElem}; -use crate::meta::ManualPageCounter; -use crate::prelude::*; +use crate::diag::{bail, SourceResult, StrResult}; +use crate::eval::Vm; +use crate::foundations::{ + cast, elem, Args, Array, Construct, Content, Datetime, Smart, StyleChain, Value, +}; +use crate::introspection::ManualPageCounter; +use crate::layout::{Frame, LayoutRoot, PageElem, Vt}; /// The root element of a document and its metadata. /// @@ -122,3 +126,29 @@ cast! { v: EcoString => Self(vec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), } + +/// A finished document with metadata and page frames. +#[derive(Debug, Default, Clone, Hash)] +pub struct Document { + /// The page frames. + pub pages: Vec<Frame>, + /// The document's title. + pub title: Option<EcoString>, + /// The document's author. + pub author: Vec<EcoString>, + /// The document's keywords. + pub keywords: Vec<EcoString>, + /// The document's creation date. + pub date: Smart<Option<Datetime>>, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_document_is_send() { + fn ensure_send<T: Send>() {} + ensure_send::<Document>(); + } +} diff --git a/crates/typst/src/model/emph.rs b/crates/typst/src/model/emph.rs new file mode 100644 index 00000000..1b7f654c --- /dev/null +++ b/crates/typst/src/model/emph.rs @@ -0,0 +1,41 @@ +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, Show, StyleChain}; +use crate::layout::Vt; +use crate::text::{ItalicToggle, TextElem}; + +/// Emphasizes content by setting it in italics. +/// +/// - If the current [text style]($text.style) is `{"normal"}`, this turns it +/// into `{"italic"}`. +/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to +/// `{"normal"}`. +/// +/// # Example +/// ```example +/// This is _emphasized._ \ +/// This is #emph[too.] +/// +/// #show emph: it => { +/// text(blue, it.body) +/// } +/// +/// This is _emphasized_ differently. +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: To emphasize content, simply +/// enclose it in underscores (`_`). Note that this only works at word +/// boundaries. To emphasize part of a word, you have to use the function. +#[elem(title = "Emphasis", Show)] +pub struct EmphElem { + /// The content to emphasize. + #[required] + pub body: Content, +} + +impl Show for EmphElem { + #[tracing::instrument(name = "EmphElem::show", skip(self))] + fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { + Ok(self.body().clone().styled(TextElem::set_emph(ItalicToggle))) + } +} diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst/src/model/enum.rs index 0c98c18a..f440bc39 100644 --- a/crates/typst-library/src/layout/enum.rs +++ b/crates/typst/src/model/enum.rs @@ -1,8 +1,14 @@ use std::str::FromStr; -use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing}; -use crate::meta::{Numbering, NumberingPattern}; -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + cast, elem, scope, Array, Content, Fold, NativeElement, Smart, StyleChain, +}; +use crate::layout::{ + Axes, BlockElem, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions, Sizing, + Spacing, VAlign, Vt, +}; +use crate::model::{Numbering, NumberingPattern, ParElem}; use crate::text::TextElem; /// A numbered list. diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst/src/model/figure.rs index e96b076c..26a96db1 100644 --- a/crates/typst-library/src/meta/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -1,13 +1,22 @@ use std::borrow::Cow; +use std::num::NonZeroUsize; use std::str::FromStr; -use crate::layout::{BlockElem, PlaceElem, VElem}; -use crate::meta::{ - Count, Counter, CounterKey, CounterUpdate, Numbering, NumberingPattern, Outlinable, - Refable, Supplement, +use ecow::EcoString; + +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + cast, elem, scope, select_where, Content, Element, Finalize, NativeElement, Selector, + Show, Smart, StyleChain, Synthesize, +}; +use crate::introspection::{ + Count, Counter, CounterKey, CounterUpdate, Locatable, Location, }; -use crate::prelude::*; -use crate::text::TextElem; +use crate::layout::{Align, BlockElem, Em, HAlign, Length, PlaceElem, VAlign, VElem, Vt}; +use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; +use crate::syntax::Spanned; +use crate::text::{Lang, Region, TextElem}; +use crate::util::NonZeroExt; use crate::visualize::ImageElem; /// A figure with an optional caption. diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst/src/model/footnote.rs index 189dbcb8..d51ee226 100644 --- a/crates/typst-library/src/meta/footnote.rs +++ b/crates/typst/src/model/footnote.rs @@ -1,11 +1,19 @@ -use comemo::Prehashed; +use std::num::NonZeroUsize; use std::str::FromStr; -use crate::layout::{HElem, ParElem}; -use crate::meta::{Count, Counter, CounterUpdate, Numbering, NumberingPattern}; -use crate::prelude::*; +use comemo::Prehashed; + +use crate::diag::{bail, error, At, SourceResult, StrResult}; +use crate::foundations::{ + cast, elem, scope, Content, Finalize, Label, NativeElement, Show, Smart, StyleChain, + Synthesize, +}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location}; +use crate::layout::{Abs, Em, HElem, Length, Ratio, Vt}; +use crate::model::{Destination, Numbering, NumberingPattern, ParElem}; use crate::text::{SuperElem, TextElem, TextSize}; -use crate::visualize::LineElem; +use crate::util::NonZeroExt; +use crate::visualize::{LineElem, Stroke}; /// A footnote. /// diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst/src/model/heading.rs index 0f814dc9..3365f00c 100644 --- a/crates/typst-library/src/meta/heading.rs +++ b/crates/typst/src/model/heading.rs @@ -1,11 +1,15 @@ -use typst::font::FontWeight; -use typst::util::option_eq; +use std::num::NonZeroUsize; -use super::{Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable}; -use crate::layout::{BlockElem, HElem, VElem}; -use crate::meta::{Count, LocalNameIn, Supplement}; -use crate::prelude::*; -use crate::text::{SpaceElem, TextElem, TextSize}; +use crate::diag::SourceResult; +use crate::foundations::{ + cast, elem, Content, Finalize, NativeElement, Show, Smart, StyleChain, Styles, + Synthesize, +}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::layout::{BlockElem, Em, HElem, VElem, Vt}; +use crate::model::{Numbering, Outlinable, Refable, Supplement}; +use crate::text::{FontWeight, Lang, LocalName, Region, SpaceElem, TextElem, TextSize}; +use crate::util::{option_eq, NonZeroExt}; /// A section heading. /// diff --git a/crates/typst-library/src/meta/link.rs b/crates/typst/src/model/link.rs index 74857f1b..27a05737 100644 --- a/crates/typst-library/src/meta/link.rs +++ b/crates/typst/src/model/link.rs @@ -1,4 +1,11 @@ -use crate::prelude::*; +use ecow::{eco_format, EcoString}; + +use crate::diag::{At, SourceResult}; +use crate::foundations::{ + cast, elem, Content, Label, NativeElement, Repr, Show, Smart, StyleChain, +}; +use crate::introspection::Location; +use crate::layout::{Position, Vt}; use crate::text::{Hyphenate, TextElem}; /// Links to a URL or a location in the document. @@ -131,3 +138,32 @@ impl From<Destination> for LinkTarget { Self::Dest(dest) } } + +/// A link destination. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Destination { + /// A link to a URL. + Url(EcoString), + /// A link to a point on a page. + Position(Position), + /// An unresolved link to a location in the document. + Location(Location), +} + +impl Repr for Destination { + fn repr(&self) -> EcoString { + eco_format!("{self:?}") + } +} + +cast! { + Destination, + self => match self { + Self::Url(v) => v.into_value(), + Self::Position(v) => v.into_value(), + Self::Location(v) => v.into_value(), + }, + v: EcoString => Self::Url(v), + v: Position => Self::Position(v), + v: Location => Self::Location(v), +} diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst/src/model/list.rs index 7c089760..e9275f03 100644 --- a/crates/typst-library/src/layout/list.rs +++ b/crates/typst/src/model/list.rs @@ -1,5 +1,13 @@ -use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing}; -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + cast, elem, scope, Array, Content, Fold, Func, NativeElement, Smart, StyleChain, + Value, +}; +use crate::layout::{ + Axes, BlockElem, Em, Fragment, GridLayouter, HAlign, Layout, Length, Regions, Sizing, + Spacing, VAlign, Vt, +}; +use crate::model::ParElem; use crate::text::TextElem; /// A bullet list. diff --git a/crates/typst/src/model/mod.rs b/crates/typst/src/model/mod.rs index ec4c8bef..7dad51c3 100644 --- a/crates/typst/src/model/mod.rs +++ b/crates/typst/src/model/mod.rs @@ -1,157 +1,75 @@ -//! The document model. - -mod content; -mod element; -mod introspect; -mod label; -mod location; -mod realize; -mod selector; -mod styles; - -use ecow::EcoVec; -#[doc(inline)] -pub use typst_macros::elem; - -pub use self::content::{fat, Content, MetaElem, PlainText}; -pub use self::element::{ - Construct, Element, ElementFields, LocalName, NativeElement, NativeElementData, Set, -}; -pub use self::introspect::{Introspector, Locator}; -pub use self::label::{Label, Unlabellable}; -pub use self::location::Location; -pub use self::realize::{ - applicable, realize, Behave, Behaviour, Finalize, Guard, Locatable, Show, Synthesize, -}; -pub use self::selector::{select_where, LocatableSelector, Selector, ShowableSelector}; -pub use self::styles::{ - Fold, Property, Recipe, Resolve, Style, StyleChain, StyleVec, StyleVecBuilder, - Styles, Transform, -}; - -use comemo::{Track, Tracked, TrackedMut, Validate}; - -use crate::diag::{warning, SourceDiagnostic, SourceResult}; -use crate::doc::Document; -use crate::eval::Tracer; -use crate::syntax::Span; -use crate::World; - -/// Layout content. -#[comemo::memoize] -#[tracing::instrument(skip(world, tracer, content))] -pub fn layout( - world: Tracked<dyn World + '_>, - mut tracer: TrackedMut<Tracer>, - content: &Content, -) -> SourceResult<Document> { - tracing::info!("Starting typesetting"); - - let library = world.library(); - let styles = StyleChain::new(&library.styles); - - let mut iter = 0; - let mut document; - let mut delayed; - - let mut introspector = Introspector::new(&[]); - - // Relayout until all introspections stabilize. - // If that doesn't happen within five attempts, we give up. - loop { - tracing::info!("Layout iteration {iter}"); - - delayed = DelayedErrors::new(); - - let constraint = <Introspector as Validate>::Constraint::new(); - let mut locator = Locator::new(); - let mut vt = Vt { - world, - tracer: TrackedMut::reborrow_mut(&mut tracer), - locator: &mut locator, - introspector: introspector.track_with(&constraint), - delayed: delayed.track_mut(), - }; - - // Layout! - document = (library.items.layout)(&mut vt, content, styles)?; - - introspector = Introspector::new(&document.pages); - iter += 1; - - if introspector.validate(&constraint) { - break; - } - - if iter >= 5 { - tracer.warn( - warning!(Span::detached(), "layout did not converge within 5 attempts",) - .with_hint("check if any states or queries are updating themselves"), - ); - break; - } - } - - // Promote delayed errors. - if !delayed.0.is_empty() { - return Err(delayed.0); - } - - Ok(document) -} - -/// A virtual typesetter. +//! Structuring elements that define the document model. + +mod bibliography; +mod cite; +mod document; +mod emph; +#[path = "enum.rs"] +mod enum_; +mod figure; +mod footnote; +mod heading; +mod link; +mod list; +#[path = "numbering.rs"] +mod numbering_; +mod outline; +mod par; +mod quote; +mod reference; +mod strong; +mod table; +mod terms; + +pub use self::bibliography::*; +pub use self::cite::*; +pub use self::document::*; +pub use self::emph::*; +pub use self::enum_::*; +pub use self::figure::*; +pub use self::footnote::*; +pub use self::heading::*; +pub use self::link::*; +pub use self::list::*; +pub use self::numbering_::*; +pub use self::outline::*; +pub use self::par::*; +pub use self::quote::*; +pub use self::reference::*; +pub use self::strong::*; +pub use self::table::*; +pub use self::terms::*; + +use crate::foundations::{category, Category, Scope}; + +/// Document structuring. /// -/// Holds the state needed to [layout] content. -pub struct Vt<'a> { - /// The compilation environment. - pub world: Tracked<'a, dyn World + 'a>, - /// Provides access to information about the document. - pub introspector: Tracked<'a, Introspector>, - /// Provides stable identities to elements. - pub locator: &'a mut Locator<'a>, - /// Delayed errors that do not immediately terminate execution. - pub delayed: TrackedMut<'a, DelayedErrors>, - /// The tracer for inspection of the values an expression produces. - pub tracer: TrackedMut<'a, Tracer>, -} - -impl Vt<'_> { - /// Perform a fallible operation that does not immediately terminate further - /// execution. Instead it produces a delayed error that is only promoted to - /// a fatal one if it remains at the end of the introspection loop. - pub fn delayed<F, T>(&mut self, f: F) -> T - where - F: FnOnce(&mut Self) -> SourceResult<T>, - T: Default, - { - match f(self) { - Ok(value) => value, - Err(errors) => { - for error in errors { - self.delayed.push(error); - } - T::default() - } - } - } -} - -/// Holds delayed errors. -#[derive(Default, Clone)] -pub struct DelayedErrors(EcoVec<SourceDiagnostic>); - -impl DelayedErrors { - /// Create an empty list of delayed errors. - pub fn new() -> Self { - Self::default() - } -} - -#[comemo::track] -impl DelayedErrors { - /// Push a delayed error. - fn push(&mut self, error: SourceDiagnostic) { - self.0.push(error); - } +/// Here, you can find functions to structure your document and interact with +/// that structure. This includes section headings, figures, bibliography +/// management, cross-referencing and more. +#[category] +pub static MODEL: Category; + +/// Hook up all `model` definitions. +pub fn define(global: &mut Scope) { + global.category(MODEL); + global.define_elem::<DocumentElem>(); + global.define_elem::<RefElem>(); + global.define_elem::<LinkElem>(); + global.define_elem::<OutlineElem>(); + global.define_elem::<HeadingElem>(); + global.define_elem::<FigureElem>(); + global.define_elem::<FootnoteElem>(); + global.define_elem::<QuoteElem>(); + global.define_elem::<CiteElem>(); + global.define_elem::<BibliographyElem>(); + global.define_elem::<EnumElem>(); + global.define_elem::<ListElem>(); + global.define_elem::<ParbreakElem>(); + global.define_elem::<ParElem>(); + global.define_elem::<TableElem>(); + global.define_elem::<TermsElem>(); + global.define_elem::<EmphElem>(); + global.define_elem::<StrongElem>(); + global.define_func::<numbering>(); } diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst/src/model/numbering.rs index 72906ff6..fa7a88c4 100644 --- a/crates/typst-library/src/meta/numbering.rs +++ b/crates/typst/src/model/numbering.rs @@ -1,10 +1,13 @@ +use std::num::NonZeroUsize; use std::str::FromStr; use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese}; -use ecow::EcoVec; -use typst::doc::{PdfPageLabel, PdfPageLabelStyle}; +use ecow::{eco_format, EcoString, EcoVec}; -use crate::prelude::*; +use crate::diag::SourceResult; +use crate::eval::Vm; +use crate::foundations::{cast, func, Args, Func, Str, Value}; +use crate::layout::{PdfPageLabel, PdfPageLabelStyle, Vt}; use crate::text::Case; /// Applies a numbering to a sequence of numbers. @@ -112,8 +115,7 @@ impl Numbering { // since PDF does not provide a suffix field. let mut style = None; if pat.suffix.is_empty() { - use NumberingKind as Kind; - use PdfPageLabelStyle as Style; + use {NumberingKind as Kind, PdfPageLabelStyle as Style}; match (kind, case) { (Kind::Arabic, _) => style = Some(Style::Arabic), (Kind::Roman, Case::Lower) => style = Some(Style::LowerRoman), diff --git a/crates/typst-library/src/meta/outline.rs b/crates/typst/src/model/outline.rs index ebd6c4b0..3b23098e 100644 --- a/crates/typst-library/src/meta/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -1,14 +1,17 @@ +use std::num::NonZeroUsize; use std::str::FromStr; -use typst::util::option_eq; - -use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem, Spacing}; -use crate::meta::{ - Counter, CounterKey, HeadingElem, LocalName, LocalNameIn, Numbering, - NumberingPattern, Refable, +use crate::diag::{bail, error, At, SourceResult}; +use crate::foundations::{ + cast, elem, scope, select_where, Content, Finalize, Func, LocatableSelector, + NativeElement, Show, Smart, StyleChain, }; -use crate::prelude::*; -use crate::text::{LinebreakElem, SpaceElem, TextElem}; +use crate::introspection::{Counter, CounterKey, Locatable}; +use crate::layout::{BoxElem, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing, Vt}; +use crate::model::{Destination, HeadingElem, NumberingPattern, ParbreakElem, Refable}; +use crate::syntax::Span; +use crate::text::{Lang, LinebreakElem, LocalName, Region, SpaceElem, TextElem}; +use crate::util::{option_eq, NonZeroExt}; /// A table of contents, figures, or other elements. /// @@ -297,6 +300,7 @@ pub trait Outlinable: Refable { } } +/// Defines how an outline is indented. #[derive(Debug, Clone, PartialEq, Hash)] pub enum OutlineIndent { Bool(bool), @@ -468,11 +472,8 @@ impl OutlineEntry { let page_numbering = vt .introspector .page_numbering(location) - .cast::<Option<Numbering>>() - .unwrap() - .unwrap_or_else(|| { - Numbering::Pattern(NumberingPattern::from_str("1").unwrap()) - }); + .cloned() + .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); let page = Counter::new(CounterKey::Page) .at(vt, location)? diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs new file mode 100644 index 00000000..84274661 --- /dev/null +++ b/crates/typst/src/model/par.rs @@ -0,0 +1,180 @@ +use comemo::Prehashed; + +use crate::diag::SourceResult; +use crate::eval::Vm; +use crate::foundations::{ + elem, Args, Cast, Construct, Content, NativeElement, Set, Smart, StyleChain, + Unlabellable, +}; +use crate::layout::{Em, Fragment, Length, Size, Vt}; + +/// Arranges text, spacing and inline-level elements into a paragraph. +/// +/// Although this function is primarily used in set rules to affect paragraph +/// properties, it can also be used to explicitly render its argument onto a +/// paragraph of its own. +/// +/// # Example +/// ```example +/// #show par: set block(spacing: 0.65em) +/// #set par( +/// first-line-indent: 1em, +/// justify: true, +/// ) +/// +/// We proceed by contradiction. +/// Suppose that there exists a set +/// of positive integers $a$, $b$, and +/// $c$ that satisfies the equation +/// $a^n + b^n = c^n$ for some +/// integer value of $n > 2$. +/// +/// Without loss of generality, +/// let $a$ be the smallest of the +/// three integers. Then, we ... +/// ``` +#[elem(title = "Paragraph", Construct)] +pub struct ParElem { + /// The spacing between lines. + #[resolve] + #[ghost] + #[default(Em::new(0.65).into())] + pub leading: Length, + + /// Whether to justify text in its line. + /// + /// Hyphenation will be enabled for justified paragraphs if the + /// [text function's `hyphenate` property]($text.hyphenate) is set to + /// `{auto}` and the current language is known. + /// + /// Note that the current [alignment]($align) still has an effect on the + /// placement of the last line except if it ends with a + /// [justified line break]($linebreak.justify). + #[ghost] + #[default(false)] + pub justify: bool, + + /// How to determine line breaks. + /// + /// When this property is set to `{auto}`, its default value, optimized line + /// breaks will be used for justified paragraphs. Enabling optimized line + /// breaks for ragged paragraphs may also be worthwhile to improve the + /// appearance of the text. + /// + /// ```example + /// #set page(width: 207pt) + /// #set par(linebreaks: "simple") + /// Some texts feature many longer + /// words. Those are often exceedingly + /// challenging to break in a visually + /// pleasing way. + /// + /// #set par(linebreaks: "optimized") + /// Some texts feature many longer + /// words. Those are often exceedingly + /// challenging to break in a visually + /// pleasing way. + /// ``` + #[ghost] + pub linebreaks: Smart<Linebreaks>, + + /// The indent the first line of a paragraph should have. + /// + /// Only the first line of a consecutive paragraph will be indented (not + /// the first one in a block or on the page). + /// + /// By typographic convention, paragraph breaks are indicated either by some + /// space between paragraphs or by indented first lines. Consider reducing + /// the [paragraph spacing]($block.spacing) to the [`leading`] when + /// using this property (e.g. using + /// `[#show par: set block(spacing: 0.65em)]`). + #[ghost] + pub first_line_indent: Length, + + /// The indent all but the first line of a paragraph should have. + #[ghost] + #[resolve] + pub hanging_indent: Length, + + /// The contents of the paragraph. + #[external] + #[required] + pub body: Content, + + /// The paragraph's children. + #[internal] + #[variadic] + pub children: Vec<Prehashed<Content>>, +} + +impl Construct for ParElem { + fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> { + // The paragraph constructor is special: It doesn't create a paragraph + // element. Instead, it just ensures that the passed content lives in a + // separate paragraph and styles it. + let styles = Self::set(vm, args)?; + let body = args.expect::<Content>("body")?; + Ok(Content::sequence([ + ParbreakElem::new().pack(), + body.styled_with_map(styles), + ParbreakElem::new().pack(), + ])) + } +} + +impl ParElem { + /// Layout the paragraph into a collection of lines. + #[tracing::instrument(name = "ParElement::layout", skip_all)] + pub fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + consecutive: bool, + region: Size, + expand: bool, + ) -> SourceResult<Fragment> { + crate::layout::layout_inline( + self.children(), + vt, + styles, + consecutive, + region, + expand, + ) + } +} + +/// How to determine line breaks in a paragraph. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Linebreaks { + /// Determine the line breaks in a simple first-fit style. + Simple, + /// Optimize the line breaks for the whole paragraph. + /// + /// Typst will try to produce more evenly filled lines of text by + /// considering the whole paragraph when calculating line breaks. + Optimized, +} + +/// A paragraph break. +/// +/// This starts a new paragraph. Especially useful when used within code like +/// [for loops]($scripting/#loops). Multiple consecutive +/// paragraph breaks collapse into a single one. +/// +/// # Example +/// ```example +/// #for i in range(3) { +/// [Blind text #i: ] +/// lorem(5) +/// parbreak() +/// } +/// ``` +/// +/// # Syntax +/// Instead of calling this function, you can insert a blank line into your +/// markup to create a paragraph break. +#[elem(title = "Paragraph Break", Unlabellable)] +pub struct ParbreakElem {} + +impl Unlabellable for ParbreakElem {} diff --git a/crates/typst-library/src/text/quote.rs b/crates/typst/src/model/quote.rs index 8d7bd15b..489e2657 100644 --- a/crates/typst-library/src/text/quote.rs +++ b/crates/typst/src/model/quote.rs @@ -1,7 +1,11 @@ -use crate::layout::{BlockElem, HElem, PadElem, Spacing, VElem}; -use crate::meta::{CitationForm, CiteElem}; -use crate::prelude::*; -use crate::text::{SmartquoteElem, SpaceElem, TextElem}; +use crate::diag::SourceResult; +use crate::foundations::{ + cast, elem, Content, Finalize, Label, NativeElement, Show, Smart, StyleChain, + Synthesize, +}; +use crate::layout::{Align, BlockElem, Em, HElem, PadElem, Spacing, VElem, Vt}; +use crate::model::{CitationForm, CiteElem}; +use crate::text::{SmartQuoteElem, SpaceElem, TextElem}; /// Displays a quote alongside an optional attribution. /// @@ -123,6 +127,7 @@ pub struct QuoteElem { body: Content, } +/// Attribution for a [quote](QuoteElem). #[derive(Debug, Clone, PartialEq, Hash)] pub enum Attribution { Content(Content), @@ -155,7 +160,7 @@ impl Show for QuoteElem { if self.quotes(styles) == Smart::Custom(true) || !block { // Add zero-width weak spacing to make the quotes "sticky". let hole = HElem::hole().pack(); - let quote = SmartquoteElem::new().with_double(true).pack(); + let quote = SmartQuoteElem::new().with_double(true).pack(); realized = Content::sequence([quote.clone(), hole.clone(), realized, hole, quote]); } diff --git a/crates/typst/src/model/realize.rs b/crates/typst/src/model/realize.rs deleted file mode 100644 index fc74ef29..00000000 --- a/crates/typst/src/model/realize.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::borrow::Cow; - -use smallvec::smallvec; - -use crate::diag::SourceResult; -use crate::doc::Meta; -use crate::eval::item; -use crate::model::{ - Content, Element, MetaElem, NativeElement, Recipe, Selector, StyleChain, Vt, -}; -use crate::util::hash128; - -/// Whether the target is affected by show rules in the given style chain. -pub fn applicable(target: &Content, styles: StyleChain) -> bool { - if target.needs_preparation() { - return true; - } - - if target.can::<dyn Show>() && target.is_pristine() { - return true; - } - - // Find out how many recipes there are. - let mut n = styles.recipes().count(); - - // Find out whether any recipe matches and is unguarded. - for recipe in styles.recipes() { - if recipe.applicable(target) && !target.is_guarded(Guard::Nth(n)) { - return true; - } - n -= 1; - } - - false -} - -/// Apply the show rules in the given style chain to a target. -pub fn realize( - vt: &mut Vt, - target: &Content, - styles: StyleChain, -) -> SourceResult<Option<Content>> { - // Pre-process. - if target.needs_preparation() { - let mut elem = target.clone(); - if target.can::<dyn Locatable>() || target.label().is_some() { - let location = vt.locator.locate(hash128(target)); - elem.set_location(location); - } - - if let Some(elem) = elem.with_mut::<dyn Synthesize>() { - elem.synthesize(vt, styles)?; - } - - elem.mark_prepared(); - - if elem.location().is_some() { - let span = elem.span(); - let meta = Meta::Elem(elem.clone()); - return Ok(Some( - (elem + MetaElem::new().pack().spanned(span)) - .styled(MetaElem::set_data(smallvec![meta])), - )); - } - - return Ok(Some(elem)); - } - - // Find out how many recipes there are. - let mut n = styles.recipes().count(); - - // Find an applicable recipe. - let mut realized = None; - for recipe in styles.recipes() { - let guard = Guard::Nth(n); - if recipe.applicable(target) && !target.is_guarded(guard) { - if let Some(content) = try_apply(vt, target, recipe, guard)? { - realized = Some(content); - break; - } - } - n -= 1; - } - - // Realize if there was no matching recipe. - if let Some(showable) = target.with::<dyn Show>() { - let guard = Guard::Base(target.func()); - if realized.is_none() && !target.is_guarded(guard) { - realized = Some(showable.show(vt, styles)?); - } - } - - // Finalize only if this is the first application for this element. - if let Some(elem) = target.with::<dyn Finalize>() { - if target.is_pristine() { - if let Some(already) = realized { - realized = Some(elem.finalize(already, styles)); - } - } - } - - Ok(realized) -} - -/// Try to apply a recipe to the target. -fn try_apply( - vt: &mut Vt, - target: &Content, - recipe: &Recipe, - guard: Guard, -) -> SourceResult<Option<Content>> { - match &recipe.selector { - Some(Selector::Elem(element, _)) => { - if target.func() != *element { - return Ok(None); - } - - recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some) - } - - Some(Selector::Label(label)) => { - if target.label() != Some(*label) { - return Ok(None); - } - - recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some) - } - - Some(Selector::Regex(regex)) => { - let Some(text) = item!(text_str)(target) else { - return Ok(None); - }; - - // We know we are on a `TextElem` and the `text` is always at ID = 0. - let make = |s: &str| target.clone().with_field(0, s); - let mut result = vec![]; - let mut cursor = 0; - - for m in regex.find_iter(text) { - let start = m.start(); - if cursor < start { - result.push(make(&text[cursor..start])); - } - - let piece = make(m.as_str()).guarded(guard); - let transformed = recipe.apply_vt(vt, piece)?; - result.push(transformed); - cursor = m.end(); - } - - if result.is_empty() { - return Ok(None); - } - - if cursor < text.len() { - result.push(make(&text[cursor..])); - } - - Ok(Some(Content::sequence(result))) - } - - // Not supported here. - Some( - Selector::Or(_) - | Selector::And(_) - | Selector::Location(_) - | Selector::Can(_) - | Selector::Before { .. } - | Selector::After { .. }, - ) => Ok(None), - - None => Ok(None), - } -} - -/// Makes this element locatable through `vt.locate`. -pub trait Locatable {} - -/// Synthesize fields on an element. This happens before execution of any show -/// rule. -pub trait Synthesize { - /// Prepare the element for show rule application. - fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()>; -} - -/// The base recipe for an element. -pub trait Show { - /// Execute the base recipe for this element. - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>; -} - -/// Post-process an element after it was realized. -pub trait Finalize { - /// Finalize the fully realized form of the element. Use this for effects - /// that should work even in the face of a user-defined show rule. - fn finalize(&self, realized: Content, styles: StyleChain) -> Content; -} - -/// How the element interacts with other elements. -pub trait Behave { - /// The element's interaction behaviour. - fn behaviour(&self) -> Behaviour; - - /// Whether this weak element is larger than a previous one and thus picked - /// as the maximum when the levels are the same. - #[allow(unused_variables)] - fn larger( - &self, - prev: &(Cow<Content>, Behaviour, StyleChain), - styles: StyleChain, - ) -> bool { - false - } -} - -/// How an element interacts with other elements in a stream. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Behaviour { - /// A weak element which only survives when a supportive element is before - /// and after it. Furthermore, per consecutive run of weak elements, only - /// one survives: The one with the lowest weakness level (or the larger one - /// if there is a tie). - Weak(usize), - /// An element that enables adjacent weak elements to exist. The default. - Supportive, - /// An element that destroys adjacent weak elements. - Destructive, - /// An element that does not interact at all with other elements, having the - /// same effect as if it didn't exist, but has a visual representation. - Ignorant, - /// An element that does not have a visual representation. - Invisible, -} - -/// Guards content against being affected by the same show rule multiple times. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Guard { - /// The nth recipe from the top of the chain. - Nth(usize), - /// The [base recipe](Show) for a kind of element. - Base(Element), -} diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst/src/model/reference.rs index 3a463c80..7f328e28 100644 --- a/crates/typst-library/src/meta/reference.rs +++ b/crates/typst/src/model/reference.rs @@ -1,7 +1,16 @@ -use super::{BibliographyElem, CiteElem, Counter, Figurable, Numbering}; +use ecow::eco_format; + +use crate::diag::{bail, At, Hint, SourceResult}; +use crate::foundations::{ + cast, elem, Content, Func, IntoValue, Label, NativeElement, Show, Smart, StyleChain, + Synthesize, +}; +use crate::introspection::{Counter, Locatable}; +use crate::layout::Vt; use crate::math::EquationElem; -use crate::meta::FootnoteElem; -use crate::prelude::*; +use crate::model::{ + BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, Numbering, +}; use crate::text::TextElem; /// A reference to a label or bibliography. diff --git a/crates/typst/src/model/strong.rs b/crates/typst/src/model/strong.rs new file mode 100644 index 00000000..40b99b2f --- /dev/null +++ b/crates/typst/src/model/strong.rs @@ -0,0 +1,48 @@ +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, Show, StyleChain}; +use crate::layout::Vt; +use crate::text::{TextElem, WeightDelta}; + +/// Strongly emphasizes content by increasing the font weight. +/// +/// Increases the current font weight by a given `delta`. +/// +/// # Example +/// ```example +/// This is *strong.* \ +/// This is #strong[too.] \ +/// +/// #show strong: set text(red) +/// And this is *evermore.* +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: To strongly emphasize content, +/// simply enclose it in stars/asterisks (`*`). Note that this only works at +/// word boundaries. To strongly emphasize part of a word, you have to use the +/// function. +#[elem(title = "Strong Emphasis", Show)] +pub struct StrongElem { + /// The delta to apply on the font weight. + /// + /// ```example + /// #set strong(delta: 0) + /// No *effect!* + /// ``` + #[default(300)] + pub delta: i64, + + /// The content to strongly emphasize. + #[required] + pub body: Content, +} + +impl Show for StrongElem { + #[tracing::instrument(name = "StrongElem::show", skip_all)] + fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { + Ok(self + .body() + .clone() + .styled(TextElem::set_delta(WeightDelta(self.delta(styles))))) + } +} diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst/src/model/table.rs index 0eff7a44..278b898c 100644 --- a/crates/typst-library/src/layout/table.rs +++ b/crates/typst/src/model/table.rs @@ -1,8 +1,15 @@ -use typst::eval::{CastInfo, Reflect}; - -use crate::layout::{AlignElem, GridLayouter, TrackSizings}; -use crate::meta::Figurable; -use crate::prelude::*; +use crate::diag::{At, SourceResult, StrResult}; +use crate::foundations::{ + elem, Array, CastInfo, Content, FromValue, Func, IntoValue, NativeElement, Reflect, + Smart, StyleChain, Value, +}; +use crate::layout::{ + Abs, Align, AlignElem, Axes, Fragment, FrameItem, GridLayouter, Layout, Length, + Point, Regions, Rel, Sides, Size, TrackSizings, Vt, +}; +use crate::model::Figurable; +use crate::text::{Lang, LocalName, Region}; +use crate::visualize::{Geometry, Paint, Stroke}; /// A table of items. /// diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst/src/model/terms.rs index d373768d..1d256626 100644 --- a/crates/typst-library/src/layout/terms.rs +++ b/crates/typst/src/model/terms.rs @@ -1,5 +1,12 @@ -use crate::layout::{BlockElem, HElem, ParElem, Spacing, VElem}; -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + cast, elem, scope, Array, Content, NativeElement, Smart, StyleChain, +}; +use crate::layout::{ + BlockElem, Em, Fragment, HElem, Layout, Length, Regions, Spacing, VElem, Vt, +}; +use crate::model::ParElem; +use crate::util::Numeric; /// A list of terms and their descriptions. /// diff --git a/crates/typst-library/src/shared/behave.rs b/crates/typst/src/realize/behave.rs index e152fbb5..bc900994 100644 --- a/crates/typst-library/src/shared/behave.rs +++ b/crates/typst/src/realize/behave.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; -use typst::model::{Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder}; +use crate::foundations::{ + Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder, +}; /// A wrapper around a [`StyleVecBuilder`] that allows elements to interact. #[derive(Debug)] @@ -82,11 +84,6 @@ impl<'a> BehavedBuilder<'a> { } } - /// Iterate over the contained elements. - pub fn elems(&self) -> impl DoubleEndedIterator<Item = &Cow<'a, Content>> { - self.builder.elems().chain(self.staged.iter().map(|(item, ..)| item)) - } - /// Return the finish style vec and the common prefix chain. pub fn finish(mut self) -> (StyleVec<Cow<'a, Content>>, StyleChain<'a>) { self.flush(false); diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst/src/realize/mod.rs index bcbe3b2b..2b5ce22d 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -1,244 +1,210 @@ -//! Composable layouts. - -mod align; -mod columns; -mod container; -#[path = "enum.rs"] -mod enum_; -mod flow; -mod fragment; -mod grid; -mod hide; -mod list; -#[path = "measure.rs"] -mod measure_; -mod pad; -mod page; -mod par; -mod place; -mod regions; -mod repeat; -mod spacing; -mod stack; -mod table; -mod terms; -mod transform; - -pub use self::align::*; -pub use self::columns::*; -pub use self::container::*; -pub use self::enum_::*; -pub use self::flow::*; -pub use self::fragment::*; -pub use self::grid::*; -pub use self::hide::*; -pub use self::list::*; -pub use self::measure_::*; -pub use self::pad::*; -pub use self::page::*; -pub use self::par::*; -pub use self::place::*; -pub use self::regions::*; -pub use self::repeat::*; -pub use self::spacing::*; -pub use self::stack::*; -pub use self::table::*; -pub use self::terms::*; -pub use self::transform::*; +//! Realization of content. + +mod behave; + +pub use self::behave::BehavedBuilder; use std::borrow::Cow; use std::mem; +use smallvec::smallvec; use typed_arena::Arena; -use typst::diag::SourceResult; -use typst::eval::Tracer; -use typst::model::{applicable, realize, DelayedErrors, StyleVecBuilder}; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + Content, Finalize, Guard, NativeElement, Recipe, Selector, Show, StyleChain, + StyleVecBuilder, Styles, Synthesize, +}; +use crate::introspection::{Locatable, Meta, MetaElem}; +use crate::layout::{ + AlignElem, BlockElem, BoxElem, ColbreakElem, FlowElem, HElem, Layout, LayoutRoot, + PageElem, PagebreakElem, Parity, PlaceElem, VElem, Vt, +}; use crate::math::{EquationElem, LayoutMath}; -use crate::meta::{CiteElem, CiteGroup, DocumentElem}; -use crate::prelude::*; -use crate::shared::BehavedBuilder; -use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem}; +use crate::model::{ + CiteElem, CiteGroup, DocumentElem, EnumElem, EnumItem, ListElem, ListItem, ParElem, + ParbreakElem, TermItem, TermsElem, +}; +use crate::syntax::Span; +use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; +use crate::util::hash128; use crate::visualize::{ CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, SquareElem, }; -/// Hook up all layout definitions. -pub(super) fn define(global: &mut Scope) { - global.category("layout"); - global.define_type::<Length>(); - global.define_type::<Angle>(); - global.define_type::<Ratio>(); - global.define_type::<Rel<Length>>(); - global.define_type::<Fr>(); - global.define_type::<Dir>(); - global.define_type::<Align>(); - global.define_elem::<PageElem>(); - global.define_elem::<PagebreakElem>(); - global.define_elem::<VElem>(); - global.define_elem::<ParElem>(); - global.define_elem::<ParbreakElem>(); - global.define_elem::<HElem>(); - global.define_elem::<BoxElem>(); - global.define_elem::<BlockElem>(); - global.define_elem::<ListElem>(); - global.define_elem::<EnumElem>(); - global.define_elem::<TermsElem>(); - global.define_elem::<TableElem>(); - global.define_elem::<StackElem>(); - global.define_elem::<GridElem>(); - global.define_elem::<ColumnsElem>(); - global.define_elem::<ColbreakElem>(); - global.define_elem::<PlaceElem>(); - global.define_elem::<AlignElem>(); - global.define_elem::<PadElem>(); - global.define_elem::<RepeatElem>(); - global.define_elem::<MoveElem>(); - global.define_elem::<ScaleElem>(); - global.define_elem::<RotateElem>(); - global.define_elem::<HideElem>(); - global.define_func::<measure>(); -} +/// Whether the target is affected by show rules in the given style chain. +pub fn applicable(target: &Content, styles: StyleChain) -> bool { + if target.needs_preparation() { + return true; + } -/// Root-level layout. -pub trait LayoutRoot { - /// Layout into one frame per page. - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>; -} + if target.can::<dyn Show>() && target.is_pristine() { + return true; + } -impl LayoutRoot for Content { - #[tracing::instrument(name = "Content::layout_root", skip_all)] - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - ) -> SourceResult<Document> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let scratch = Scratch::default(); - let (realized, styles) = realize_root(&mut vt, &scratch, content, styles)?; - realized - .with::<dyn LayoutRoot>() - .unwrap() - .layout_root(&mut vt, styles) - } - - tracing::info!("Starting layout"); - cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - ) + // Find out how many recipes there are. + let mut n = styles.recipes().count(); + + // Find out whether any recipe matches and is unguarded. + for recipe in styles.recipes() { + if recipe.applicable(target) && !target.is_guarded(Guard::Nth(n)) { + return true; + } + n -= 1; } + + false } -/// Layout into regions. -pub trait Layout { - /// Layout into one frame per region. - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment>; - - /// Layout without side effects. - /// - /// This element must be layouted again in the same order for the results to - /// be valid. - #[tracing::instrument(name = "Layout::measure", skip_all)] - fn measure( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(vt.locator.track()); - let mut vt = Vt { - world: vt.world, - introspector: vt.introspector, - locator: &mut locator, - tracer: TrackedMut::reborrow_mut(&mut vt.tracer), - delayed: TrackedMut::reborrow_mut(&mut vt.delayed), - }; - self.layout(&mut vt, styles, regions) +/// Apply the show rules in the given style chain to a target. +pub fn realize( + vt: &mut Vt, + target: &Content, + styles: StyleChain, +) -> SourceResult<Option<Content>> { + // Pre-process. + if target.needs_preparation() { + let mut elem = target.clone(); + if target.can::<dyn Locatable>() || target.label().is_some() { + let location = vt.locator.locate(hash128(target)); + elem.set_location(location); + } + + if let Some(elem) = elem.with_mut::<dyn Synthesize>() { + elem.synthesize(vt, styles)?; + } + + elem.mark_prepared(); + + if elem.location().is_some() { + let span = elem.span(); + let meta = Meta::Elem(elem.clone()); + return Ok(Some( + (elem + MetaElem::new().pack().spanned(span)) + .styled(MetaElem::set_data(smallvec![meta])), + )); + } + + return Ok(Some(elem)); + } + + // Find out how many recipes there are. + let mut n = styles.recipes().count(); + + // Find an applicable recipe. + let mut realized = None; + for recipe in styles.recipes() { + let guard = Guard::Nth(n); + if recipe.applicable(target) && !target.is_guarded(guard) { + if let Some(content) = try_apply(vt, target, recipe, guard)? { + realized = Some(content); + break; + } + } + n -= 1; + } + + // Realize if there was no matching recipe. + if let Some(showable) = target.with::<dyn Show>() { + let guard = Guard::Base(target.func()); + if realized.is_none() && !target.is_guarded(guard) { + realized = Some(showable.show(vt, styles)?); + } + } + + // Finalize only if this is the first application for this element. + if let Some(elem) = target.with::<dyn Finalize>() { + if target.is_pristine() { + if let Some(already) = realized { + realized = Some(elem.finalize(already, styles)); + } + } } + + Ok(realized) } -impl Layout for Content { - #[tracing::instrument(name = "Content::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - #[allow(clippy::too_many_arguments)] - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, +/// Try to apply a recipe to the target. +fn try_apply( + vt: &mut Vt, + target: &Content, + recipe: &Recipe, + guard: Guard, +) -> SourceResult<Option<Content>> { + match &recipe.selector { + Some(Selector::Elem(element, _)) => { + if target.func() != *element { + return Ok(None); + } + + recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some) + } + + Some(Selector::Label(label)) => { + if target.label() != Some(*label) { + return Ok(None); + } + + recipe.apply_vt(vt, target.clone().guarded(guard)).map(Some) + } + + Some(Selector::Regex(regex)) => { + let Some(elem) = target.to::<TextElem>() else { + return Ok(None); }; - let scratch = Scratch::default(); - let (realized, styles) = realize_block(&mut vt, &scratch, content, styles)?; - realized - .with::<dyn Layout>() - .unwrap() - .layout(&mut vt, styles, regions) - } - - tracing::info!("Layouting `Content`"); - - let fragment = cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - regions, - )?; - - vt.locator.visit_frames(&fragment); - Ok(fragment) + + let make = |s: &str| { + let mut fresh = elem.clone(); + fresh.push_text(s.into()); + fresh.pack() + }; + + let mut result = vec![]; + let mut cursor = 0; + + let text = elem.text(); + + for m in regex.find_iter(elem.text()) { + let start = m.start(); + if cursor < start { + result.push(make(&text[cursor..start])); + } + + let piece = make(m.as_str()).guarded(guard); + let transformed = recipe.apply_vt(vt, piece)?; + result.push(transformed); + cursor = m.end(); + } + + if result.is_empty() { + return Ok(None); + } + + if cursor < text.len() { + result.push(make(&text[cursor..])); + } + + Ok(Some(Content::sequence(result))) + } + + // Not supported here. + Some( + Selector::Or(_) + | Selector::And(_) + | Selector::Location(_) + | Selector::Can(_) + | Selector::Before { .. } + | Selector::After { .. }, + ) => Ok(None), + + None => Ok(None), } } /// Realize into an element that is capable of root-level layout. #[tracing::instrument(skip_all)] -fn realize_root<'a>( +pub fn realize_root<'a>( vt: &mut Vt, scratch: &'a Scratch<'a>, content: &'a Content, @@ -257,7 +223,7 @@ fn realize_root<'a>( /// Realize into an element that is capable of block-level layout. #[tracing::instrument(skip_all)] -fn realize_block<'a>( +pub fn realize_block<'a>( vt: &mut Vt, scratch: &'a Scratch<'a>, content: &'a Content, @@ -308,7 +274,7 @@ struct Builder<'a, 'v, 't> { /// Temporary storage arenas for building. #[derive(Default)] -struct Scratch<'a> { +pub struct Scratch<'a> { /// An arena where intermediate style chains are stored. styles: Arena<StyleChain<'a>>, /// An arena where intermediate content resulting from show rules is stored. @@ -624,7 +590,7 @@ impl<'a> ParBuilder<'a> { || content.is::<TextElem>() || content.is::<HElem>() || content.is::<LinebreakElem>() - || content.is::<SmartquoteElem>() + || content.is::<SmartQuoteElem>() || content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles)) || content.is::<BoxElem>() { diff --git a/crates/typst-library/src/symbols/emoji.rs b/crates/typst/src/symbols/emoji.rs index b35dfcaa..092c5100 100644 --- a/crates/typst-library/src/symbols/emoji.rs +++ b/crates/typst/src/symbols/emoji.rs @@ -1,4 +1,5 @@ -use typst::eval::{symbols, Module, Scope, Symbol}; +use crate::foundations::{Module, Scope}; +use crate::symbols::{symbols, Symbol}; /// A module with all emoji. pub fn emoji() -> Module { diff --git a/crates/typst/src/symbols/mod.rs b/crates/typst/src/symbols/mod.rs new file mode 100644 index 00000000..711ab149 --- /dev/null +++ b/crates/typst/src/symbols/mod.rs @@ -0,0 +1,27 @@ +//! Modifiable symbols. + +mod emoji; +mod sym; +mod symbol; + +pub use self::emoji::*; +pub use self::sym::*; +pub use self::symbol::*; + +use crate::foundations::{category, Category, Scope}; + +/// These two modules give names to symbols and emoji to make them easy to +/// insert with a normal keyboard. Alternatively, you can also always directly +/// enter Unicode symbols into your text and formulas. In addition to the +/// symbols listed below, math mode defines `dif` and `Dif`. These are not +/// normal symbol values because they also affect spacing and font style. +#[category] +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-library/src/symbols/sym.rs b/crates/typst/src/symbols/sym.rs index 79696a38..b7bf167a 100644 --- a/crates/typst-library/src/symbols/sym.rs +++ b/crates/typst/src/symbols/sym.rs @@ -1,4 +1,5 @@ -use typst::eval::{symbols, Module, Scope, Symbol}; +use crate::foundations::{Module, Scope}; +use crate::symbols::{symbols, Symbol}; /// A module with all general symbols. pub fn sym() -> Module { diff --git a/crates/typst/src/eval/symbol.rs b/crates/typst/src/symbols/symbol.rs index 8219ec58..8a7707cd 100644 --- a/crates/typst/src/eval/symbol.rs +++ b/crates/typst/src/symbols/symbol.rs @@ -7,7 +7,7 @@ use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; use crate::diag::{bail, SourceResult, StrResult}; -use crate::eval::{cast, func, scope, ty, Array}; +use crate::foundations::{cast, func, scope, ty, Array}; use crate::syntax::{Span, Spanned}; #[doc(inline)] @@ -192,7 +192,7 @@ impl Symbol { /// displaying a symbol, Typst selects the first from the variants that have /// all attached modifiers and the minimum number of other modifiers. #[variadic] - variants: Vec<Spanned<Variant>>, + variants: Vec<Spanned<SymbolVariant>>, ) -> SourceResult<Symbol> { let mut list = Vec::new(); if variants.is_empty() { @@ -233,7 +233,7 @@ impl Debug for List { } } -impl super::Repr for Symbol { +impl crate::foundations::Repr for Symbol { fn repr(&self) -> EcoString { eco_format!("\"{}\"", self.get()) } @@ -259,10 +259,10 @@ impl List { } /// A value that can be cast to a symbol. -pub struct Variant(EcoString, char); +pub struct SymbolVariant(EcoString, char); cast! { - Variant, + SymbolVariant, c: char => Self(EcoString::new(), c), array: Array => { let mut iter = array.into_iter(); diff --git a/crates/typst/src/text/case.rs b/crates/typst/src/text/case.rs new file mode 100644 index 00000000..69dbf5e1 --- /dev/null +++ b/crates/typst/src/text/case.rs @@ -0,0 +1,79 @@ +use crate::foundations::{cast, func, Cast, Content, Str}; +use crate::text::TextElem; + +/// Converts a string or content to lowercase. +/// +/// # Example +/// ```example +/// #lower("ABC") \ +/// #lower[*My Text*] \ +/// #lower[already low] +/// ``` +#[func(title = "Lowercase")] +pub fn lower( + /// The text to convert to lowercase. + text: Caseable, +) -> Caseable { + case(text, Case::Lower) +} + +/// Converts a string or content to uppercase. +/// +/// # Example +/// ```example +/// #upper("abc") \ +/// #upper[*my text*] \ +/// #upper[ALREADY HIGH] +/// ``` +#[func(title = "Uppercase")] +pub fn upper( + /// The text to convert to uppercase. + text: Caseable, +) -> Caseable { + case(text, Case::Upper) +} + +/// Change the case of text. +fn case(text: Caseable, case: Case) -> Caseable { + match text { + Caseable::Str(v) => Caseable::Str(case.apply(&v).into()), + Caseable::Content(v) => { + Caseable::Content(v.styled(TextElem::set_case(Some(case)))) + } + } +} + +/// A value whose case can be changed. +pub enum Caseable { + Str(Str), + Content(Content), +} + +cast! { + Caseable, + self => match self { + Self::Str(v) => v.into_value(), + Self::Content(v) => v.into_value(), + }, + v: Str => Self::Str(v), + v: Content => Self::Content(v), +} + +/// A case transformation on text. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Case { + /// Everything is lowercased. + Lower, + /// Everything is uppercased. + Upper, +} + +impl Case { + /// Apply the case to a string. + pub fn apply(self, text: &str) -> String { + match self { + Self::Lower => text.to_lowercase(), + Self::Upper => text.to_uppercase(), + } + } +} diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst/src/text/deco.rs index aecff401..f7ee8cbc 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst/src/text/deco.rs @@ -1,8 +1,16 @@ use kurbo::{BezPath, Line, ParamCurve}; use ttf_parser::{GlyphId, OutlineBuilder}; -use crate::prelude::*; -use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; +use ecow::{eco_format, EcoString}; + +use crate::diag::SourceResult; +use crate::foundations::{cast, elem, ty, Content, Fold, Repr, Show, Smart, StyleChain}; +use crate::layout::{Abs, Em, Frame, FrameItem, Length, Point, Size, Vt}; +use crate::syntax::Span; +use crate::text::{ + BottomEdge, BottomEdgeMetric, TextElem, TextItem, TopEdge, TopEdgeMetric, +}; +use crate::visualize::{Color, FixedStroke, Geometry, Paint, Stroke}; /// Underlines text. /// @@ -328,8 +336,10 @@ impl Show for HighlightElem { } } -/// Defines a line-based decoration that is positioned over, under or on top of text, -/// or highlights the text with a background. +/// A text decoration. +/// +/// Can be positioned over, under, or on top of text, or highlight the text with +/// a background. #[ty] #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Decoration { @@ -366,7 +376,7 @@ enum DecoLine { } /// Add line decorations to a single run of shaped text. -pub(super) fn decorate( +pub(crate) fn decorate( frame: &mut Frame, deco: &Decoration, text: &TextItem, diff --git a/crates/typst/src/font/book.rs b/crates/typst/src/text/font/book.rs index dbdfab15..c875ab17 100644 --- a/crates/typst/src/font/book.rs +++ b/crates/typst/src/text/font/book.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use ttf_parser::{name_id, PlatformId, Tag}; use unicode_segmentation::UnicodeSegmentation; -use crate::font::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; +use crate::text::{Font, FontStretch, FontStyle, FontVariant, FontWeight}; /// Metadata about a collection of fonts. #[derive(Debug, Default, Clone, Hash)] diff --git a/crates/typst/src/font/mod.rs b/crates/typst/src/text/font/mod.rs index 953a5122..d9eb044b 100644 --- a/crates/typst/src/font/mod.rs +++ b/crates/typst/src/text/font/mod.rs @@ -13,8 +13,8 @@ use std::sync::Arc; use ttf_parser::GlyphId; use self::book::find_name; -use crate::eval::{Bytes, Cast}; -use crate::geom::Em; +use crate::foundations::{Bytes, Cast}; +use crate::layout::Em; /// An OpenType font. /// diff --git a/crates/typst/src/font/variant.rs b/crates/typst/src/text/font/variant.rs index d3c1f953..f96f648d 100644 --- a/crates/typst/src/font/variant.rs +++ b/crates/typst/src/text/font/variant.rs @@ -1,10 +1,10 @@ -use ecow::EcoString; use std::fmt::{self, Debug, Formatter}; +use ecow::EcoString; use serde::{Deserialize, Serialize}; -use crate::eval::{cast, Cast, IntoValue, Repr}; -use crate::geom::Ratio; +use crate::foundations::{cast, Cast, IntoValue, Repr}; +use crate::layout::Ratio; /// Properties that distinguish a font from other fonts in the same family. #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] diff --git a/crates/typst/src/text/item.rs b/crates/typst/src/text/item.rs new file mode 100644 index 00000000..081b06d7 --- /dev/null +++ b/crates/typst/src/text/item.rs @@ -0,0 +1,63 @@ +use std::fmt::{self, Debug, Formatter}; +use std::ops::Range; + +use ecow::EcoString; + +use crate::layout::{Abs, Em}; +use crate::syntax::Span; +use crate::text::{Font, Lang}; +use crate::visualize::Paint; + +/// A run of shaped text. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct TextItem { + /// The font the glyphs are contained in. + pub font: Font, + /// The font size. + pub size: Abs, + /// Glyph color. + pub fill: Paint, + /// The natural language of the text. + pub lang: Lang, + /// The item's plain text. + pub text: EcoString, + /// The glyphs. + pub glyphs: Vec<Glyph>, +} + +impl TextItem { + /// The width of the text run. + pub fn width(&self) -> Abs { + self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size) + } +} + +impl Debug for TextItem { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("Text(")?; + self.text.fmt(f)?; + f.write_str(")") + } +} + +/// A glyph in a run of shaped text. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Glyph { + /// The glyph's index in the font. + pub id: u16, + /// The advance width of the glyph. + pub x_advance: Em, + /// The horizontal offset of the glyph. + pub x_offset: Em, + /// The range of the glyph in its item's text. + pub range: Range<u16>, + /// The source code location of the text. + pub span: (Span, u16), +} + +impl Glyph { + /// The range of the glyph in its item's text. + pub fn range(&self) -> Range<usize> { + usize::from(self.range.start)..usize::from(self.range.end) + } +} diff --git a/crates/typst/src/text/lang.rs b/crates/typst/src/text/lang.rs new file mode 100644 index 00000000..35c3422f --- /dev/null +++ b/crates/typst/src/text/lang.rs @@ -0,0 +1,182 @@ +use std::str::FromStr; + +use ecow::EcoString; + +use crate::foundations::{cast, StyleChain}; +use crate::layout::Dir; +use crate::text::TextElem; + +/// An identifier for a natural language. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Lang([u8; 3], u8); + +impl Lang { + pub const ALBANIAN: Self = Self(*b"sq ", 2); + pub const ARABIC: Self = Self(*b"ar ", 2); + pub const BOKMÅL: Self = Self(*b"nb ", 2); + pub const CHINESE: Self = Self(*b"zh ", 2); + pub const CZECH: Self = Self(*b"cs ", 2); + pub const DANISH: Self = Self(*b"da ", 2); + pub const DUTCH: Self = Self(*b"nl ", 2); + pub const ENGLISH: Self = Self(*b"en ", 2); + pub const FILIPINO: Self = Self(*b"tl ", 2); + pub const FINNISH: Self = Self(*b"fi ", 2); + pub const FRENCH: Self = Self(*b"fr ", 2); + pub const GERMAN: Self = Self(*b"de ", 2); + pub const GREEK: Self = Self(*b"gr ", 2); + pub const ITALIAN: Self = Self(*b"it ", 2); + pub const JAPANESE: Self = Self(*b"ja ", 2); + pub const NYNORSK: Self = Self(*b"nn ", 2); + pub const POLISH: Self = Self(*b"pl ", 2); + pub const PORTUGUESE: Self = Self(*b"pt ", 2); + pub const RUSSIAN: Self = Self(*b"ru ", 2); + pub const SLOVENIAN: Self = Self(*b"sl ", 2); + pub const SPANISH: Self = Self(*b"es ", 2); + pub const SWEDISH: Self = Self(*b"sv ", 2); + pub const TURKISH: Self = Self(*b"tr ", 2); + pub const UKRAINIAN: Self = Self(*b"ua ", 2); + pub const VIETNAMESE: Self = Self(*b"vi ", 2); + pub const HUNGARIAN: Self = Self(*b"hu ", 2); + pub const ROMANIAN: Self = Self(*b"ro ", 2); + + /// Return the language code as an all lowercase string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0[..usize::from(self.1)]).unwrap_or_default() + } + + /// The default direction for the language. + pub fn dir(self) -> Dir { + match self.as_str() { + "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur" + | "yi" => Dir::RTL, + _ => Dir::LTR, + } + } +} + +impl FromStr for Lang { + type Err = &'static str; + + /// Construct a language from a two- or three-byte ISO 639-1/2/3 code. + fn from_str(iso: &str) -> Result<Self, Self::Err> { + let len = iso.len(); + if matches!(len, 2..=3) && iso.is_ascii() { + let mut bytes = [b' '; 3]; + bytes[..len].copy_from_slice(iso.as_bytes()); + bytes.make_ascii_lowercase(); + Ok(Self(bytes, len as u8)) + } else { + Err("expected two or three letter language code (ISO 639-1/2/3)") + } + } +} + +cast! { + Lang, + self => self.as_str().into_value(), + string: EcoString => Self::from_str(&string)?, +} + +/// An identifier for a region somewhere in the world. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Region([u8; 2]); + +impl Region { + /// Return the region code as an all uppercase string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0).unwrap_or_default() + } +} + +impl PartialEq<&str> for Region { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl FromStr for Region { + type Err = &'static str; + + /// Construct a region from its two-byte ISO 3166-1 alpha-2 code. + fn from_str(iso: &str) -> Result<Self, Self::Err> { + if iso.len() == 2 && iso.is_ascii() { + let mut bytes: [u8; 2] = iso.as_bytes().try_into().unwrap(); + bytes.make_ascii_uppercase(); + Ok(Self(bytes)) + } else { + Err("expected two letter region code (ISO 3166-1 alpha-2)") + } + } +} + +cast! { + Region, + self => self.as_str().into_value(), + string: EcoString => Self::from_str(&string)?, +} + +/// An ISO 15924-type script identifier. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct WritingScript([u8; 4], u8); + +impl WritingScript { + /// Return the script as an all lowercase string slice. + pub fn as_str(&self) -> &str { + std::str::from_utf8(&self.0[..usize::from(self.1)]).unwrap_or_default() + } + + /// Return the description of the script as raw bytes. + pub fn as_bytes(&self) -> &[u8; 4] { + &self.0 + } +} + +impl FromStr for WritingScript { + type Err = &'static str; + + /// Construct a region from its ISO 15924 code. + fn from_str(iso: &str) -> Result<Self, Self::Err> { + let len = iso.len(); + if matches!(len, 3..=4) && iso.is_ascii() { + let mut bytes = [b' '; 4]; + bytes[..len].copy_from_slice(iso.as_bytes()); + bytes.make_ascii_lowercase(); + Ok(Self(bytes, len as u8)) + } else { + Err("expected three or four letter script code (ISO 15924 or 'math')") + } + } +} + +cast! { + WritingScript, + self => self.as_str().into_value(), + string: EcoString => Self::from_str(&string)?, +} + +/// The name with which an element is referenced. +pub trait LocalName { + /// Get the name in the given language and (optionally) region. + fn local_name(lang: Lang, region: Option<Region>) -> &'static str; + + /// Gets the local name from the style chain. + fn local_name_in(styles: StyleChain) -> &'static str + where + Self: Sized, + { + Self::local_name(TextElem::lang_in(styles), TextElem::region_in(styles)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::option_eq; + + #[test] + fn test_region_option_eq() { + let region = Some(Region([b'U', b'S'])); + assert!(option_eq(region, "US")); + assert!(!option_eq(region, "AB")); + } +} diff --git a/crates/typst/src/text/linebreak.rs b/crates/typst/src/text/linebreak.rs new file mode 100644 index 00000000..d9802942 --- /dev/null +++ b/crates/typst/src/text/linebreak.rs @@ -0,0 +1,43 @@ +use crate::foundations::{elem, Behave, Behaviour}; + +/// Inserts a line break. +/// +/// Advances the paragraph to the next line. A single trailing line break at the +/// end of a paragraph is ignored, but more than one creates additional empty +/// lines. +/// +/// # Example +/// ```example +/// *Date:* 26.12.2022 \ +/// *Topic:* Infrastructure Test \ +/// *Severity:* High \ +/// ``` +/// +/// # Syntax +/// This function also has dedicated syntax: To insert a line break, simply write +/// a backslash followed by whitespace. This always creates an unjustified +/// break. +#[elem(title = "Line Break", Behave)] +pub struct LinebreakElem { + /// Whether to justify the line before the break. + /// + /// This is useful if you found a better line break opportunity in your + /// justified text than Typst did. + /// + /// ```example + /// #set par(justify: true) + /// #let jb = linebreak(justify: true) + /// + /// I have manually tuned the #jb + /// line breaks in this paragraph #jb + /// for an _interesting_ result. #jb + /// ``` + #[default(false)] + pub justify: bool, +} + +impl Behave for LinebreakElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Destructive + } +} diff --git a/crates/typst/src/text/lorem.rs b/crates/typst/src/text/lorem.rs new file mode 100644 index 00000000..5d01a550 --- /dev/null +++ b/crates/typst/src/text/lorem.rs @@ -0,0 +1,24 @@ +use crate::foundations::{func, Str}; + +/// Creates blind text. +/// +/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given +/// number of words. The sequence of words generated by the function is always +/// the same but randomly chosen. As usual for blind texts, it does not make any +/// sense. Use it as a placeholder to try layouts. +/// +/// # Example +/// ```example +/// = Blind Text +/// #lorem(30) +/// +/// = More Blind Text +/// #lorem(15) +/// ``` +#[func(keywords = ["Blind Text"])] +pub fn lorem( + /// The length of the blind text in words. + words: usize, +) -> Str { + lipsum::lipsum(words).replace("--", "–").into() +} diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst/src/text/mod.rs index 202ab2c5..b2a5a840 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -1,47 +1,68 @@ //! Text handling. +mod case; mod deco; +mod font; +mod item; +mod lang; mod linebreak; -mod misc; -mod quote; -mod quotes; +#[path = "lorem.rs"] +mod lorem_; mod raw; -mod shaping; mod shift; +#[path = "smallcaps.rs"] +mod smallcaps_; +mod smartquote; +mod space; +pub use self::case::*; pub use self::deco::*; -pub(crate) use self::linebreak::*; -pub use self::misc::*; -pub use self::quote::*; -pub use self::quotes::*; +pub use self::font::*; +pub use self::item::*; +pub use self::lang::*; +pub use self::linebreak::*; +pub use self::lorem_::*; pub use self::raw::*; -pub use self::shaping::*; pub use self::shift::*; +pub use self::smallcaps_::*; +pub use self::smartquote::*; +pub use self::space::*; -use rustybuzz::Tag; +use std::fmt::{self, Debug, Formatter}; + +use ecow::{eco_format, EcoString}; +use rustybuzz::{Feature, Tag}; use ttf_parser::Rect; -use typst::diag::{bail, error, SourceResult}; -use typst::eval::Never; -use typst::font::{Font, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; -use crate::layout::ParElem; -use crate::prelude::*; +use crate::diag::{bail, error, SourceResult, StrResult}; +use crate::eval::Vm; +use crate::foundations::{ + cast, category, elem, Args, Array, Cast, Category, Construct, Content, Dict, Fold, + NativeElement, Never, PlainText, Repr, Resolve, Scope, Set, Smart, StyleChain, Value, +}; +use crate::layout::{Abs, Axis, Dir, Length, Rel}; +use crate::model::ParElem; +use crate::syntax::Spanned; +use crate::visualize::{Color, GradientRelative, Paint}; + +/// Text styling. +/// +/// The [text function]($text) is of particular interest. +#[category] +pub static TEXT: Category; -/// Hook up all text definitions. +/// Hook up all `text` definitions. pub(super) fn define(global: &mut Scope) { - global.category("text"); + global.category(TEXT); global.define_elem::<TextElem>(); global.define_elem::<LinebreakElem>(); - global.define_elem::<SmartquoteElem>(); - global.define_elem::<StrongElem>(); - global.define_elem::<EmphElem>(); + global.define_elem::<SmartQuoteElem>(); global.define_elem::<SubElem>(); global.define_elem::<SuperElem>(); global.define_elem::<UnderlineElem>(); global.define_elem::<OverlineElem>(); global.define_elem::<StrikeElem>(); global.define_elem::<HighlightElem>(); - global.define_elem::<QuoteElem>(); global.define_elem::<RawElem>(); global.define_func::<lower>(); global.define_func::<upper>(); @@ -206,7 +227,7 @@ pub struct TextElem { let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?; if let Some(paint) = &paint { if let Paint::Gradient(gradient) = &paint.v { - if gradient.relative() == Smart::Custom(Relative::Self_) { + if gradient.relative() == Smart::Custom(GradientRelative::Self_) { bail!( error!( paint.span, @@ -590,21 +611,20 @@ pub struct TextElem { /// The text. #[required] - #[variant(0)] pub text: EcoString, /// A delta to apply on the font weight. #[internal] #[fold] #[ghost] - pub delta: Delta, + pub delta: WeightDelta, /// Whether the font style should be inverted. #[internal] #[fold] #[default(false)] #[ghost] - pub emph: Toggle, + pub emph: ItalicToggle, /// Decorative lines. #[internal] @@ -706,6 +726,47 @@ cast! { values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?), } +/// Resolve a prioritized iterator over the font families. +pub(crate) fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { + const FALLBACKS: &[&str] = &[ + "linux libertine", + "twitter color emoji", + "noto color emoji", + "apple color emoji", + "segoe ui emoji", + ]; + + let tail = if TextElem::fallback_in(styles) { FALLBACKS } else { &[] }; + TextElem::font_in(styles) + .into_iter() + .map(|family| family.as_str()) + .chain(tail.iter().copied()) +} + +/// Resolve the font variant. +pub(crate) fn variant(styles: StyleChain) -> FontVariant { + let mut variant = FontVariant::new( + TextElem::style_in(styles), + TextElem::weight_in(styles), + TextElem::stretch_in(styles), + ); + + let delta = TextElem::delta_in(styles); + variant.weight = variant + .weight + .thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16); + + if TextElem::emph_in(styles) { + variant.style = match variant.style { + FontStyle::Normal => FontStyle::Italic, + FontStyle::Italic => FontStyle::Normal, + FontStyle::Oblique => FontStyle::Normal, + } + } + + variant +} + /// The size of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct TextSize(pub Length); @@ -990,3 +1051,106 @@ impl Fold for FontFeatures { self } } + +/// Collect the OpenType features to apply. +pub(crate) fn features(styles: StyleChain) -> Vec<Feature> { + let mut tags = vec![]; + let mut feat = |tag, value| { + tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); + }; + + // Features that are on by default in Harfbuzz are only added if disabled. + if !TextElem::kerning_in(styles) { + feat(b"kern", 0); + } + + // Features that are off by default in Harfbuzz are only added if enabled. + if TextElem::smallcaps_in(styles) { + feat(b"smcp", 1); + } + + if TextElem::alternates_in(styles) { + feat(b"salt", 1); + } + + let storage; + if let Some(set) = TextElem::stylistic_set_in(styles) { + storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; + feat(&storage, 1); + } + + if !TextElem::ligatures_in(styles) { + feat(b"liga", 0); + feat(b"clig", 0); + } + + if TextElem::discretionary_ligatures_in(styles) { + feat(b"dlig", 1); + } + + if TextElem::historical_ligatures_in(styles) { + feat(b"hilg", 1); + } + + match TextElem::number_type_in(styles) { + Smart::Auto => {} + Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), + Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), + } + + match TextElem::number_width_in(styles) { + Smart::Auto => {} + Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), + Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), + } + + if TextElem::slashed_zero_in(styles) { + feat(b"zero", 1); + } + + if TextElem::fractions_in(styles) { + feat(b"frac", 1); + } + + for (tag, value) in TextElem::features_in(styles).0 { + tags.push(Feature::new(tag, value, ..)) + } + + tags +} + +/// A toggle that turns on and off alternatingly if folded. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct ItalicToggle; + +cast! { + ItalicToggle, + self => Value::None, + _: Value => Self, +} + +impl Fold for ItalicToggle { + type Output = bool; + + fn fold(self, outer: Self::Output) -> Self::Output { + !outer + } +} + +/// A delta that is summed up when folded. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct WeightDelta(pub i64); + +cast! { + WeightDelta, + self => self.0.into_value(), + v: i64 => Self(v), +} + +impl Fold for WeightDelta { + type Output = i64; + + fn fold(self, outer: Self::Output) -> Self::Output { + outer + self.0 + } +} diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst/src/text/raw.rs index c5e152cf..1142281c 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst/src/text/raw.rs @@ -2,23 +2,29 @@ use std::hash::Hash; use std::ops::Range; use std::sync::Arc; -use ecow::EcoVec; +use ecow::{eco_format, EcoString, EcoVec}; use once_cell::sync::Lazy; use once_cell::unsync::Lazy as UnsyncLazy; use syntect::highlighting as synt; use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; -use typst::diag::FileError; -use typst::eval::Bytes; -use typst::syntax::{self, split_newlines, LinkedNode}; -use typst::util::option_eq; use unicode_segmentation::UnicodeSegmentation; -use crate::layout::BlockElem; -use crate::meta::Figurable; -use crate::prelude::*; +use crate::diag::{At, FileError, SourceResult, StrResult}; +use crate::eval::Vm; +use crate::foundations::{ + cast, elem, scope, Args, Array, Bytes, Content, Finalize, Fold, NativeElement, + PlainText, Show, Smart, StyleChain, Styles, Synthesize, Value, +}; +use crate::layout::{BlockElem, Em, HAlign, Vt}; +use crate::model::Figurable; +use crate::syntax::{split_newlines, LinkedNode, Spanned}; use crate::text::{ - FontFamily, FontList, Hyphenate, LinebreakElem, SmartquoteElem, TextElem, TextSize, + FontFamily, FontList, Hyphenate, Lang, LinebreakElem, LocalName, Region, + SmartQuoteElem, TextElem, TextSize, }; +use crate::util::option_eq; +use crate::visualize::Color; +use crate::{syntax, World}; // Shorthand for highlighter closures. type StyleFn<'a> = &'a mut dyn FnMut(&LinkedNode, Range<usize>, synt::Style) -> Content; @@ -267,7 +273,7 @@ impl RawElem { impl RawElem { /// The supported language names and tags. pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> { - SYNTAXES + RAW_SYNTAXES .syntaxes() .iter() .map(|syntax| { @@ -310,7 +316,7 @@ impl Synthesize for RawElem { .unwrap() }); - let theme = theme.as_deref().unwrap_or(&THEME); + let theme = theme.as_deref().unwrap_or(&RAW_THEME); let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK); let mut seq = vec![]; @@ -339,9 +345,9 @@ impl Synthesize for RawElem { ) .highlight(); } else if let Some((syntax_set, syntax)) = lang.and_then(|token| { - SYNTAXES + RAW_SYNTAXES .find_syntax_by_token(&token) - .map(|syntax| (&*SYNTAXES, syntax)) + .map(|syntax| (&*RAW_SYNTAXES, syntax)) .or_else(|| { extra_syntaxes .find_syntax_by_token(&token) @@ -416,7 +422,7 @@ impl Finalize for RawElem { styles.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); styles .set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); - styles.set(SmartquoteElem::set_enabled(false)); + styles.set(SmartQuoteElem::set_enabled(false)); realized.styled_with_map(styles) } } @@ -587,7 +593,7 @@ impl<'a> ThemedHighlighter<'a> { for child in self.node.children() { let mut scopes = self.scopes.clone(); - if let Some(tag) = typst::syntax::highlight(&child) { + if let Some(tag) = crate::syntax::highlight(&child) { scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap()) } @@ -631,7 +637,7 @@ fn to_syn(color: Color) -> synt::Color { synt::Color { r, g, b, a } } -/// A list of bibliography file paths. +/// A list of raw syntax file paths. #[derive(Debug, Default, Clone, PartialEq, Hash)] pub struct SyntaxPaths(Vec<EcoString>); @@ -750,11 +756,11 @@ fn parse_theme( /// syntaxes/02_Extra/VimHelp.sublime-syntax /// syntaxes/02_Extra/cmd-help/syntaxes/cmd-help.sublime-syntax /// ``` -pub static SYNTAXES: Lazy<syntect::parsing::SyntaxSet> = +pub static RAW_SYNTAXES: Lazy<syntect::parsing::SyntaxSet> = Lazy::new(|| syntect::dumps::from_binary(include_bytes!("../../assets/syntect.bin"))); /// The default theme used for syntax highlighting. -pub static THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme { +pub static RAW_THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme { name: Some("Typst Light".into()), author: Some("The Typst Project Developers".into()), settings: synt::ThemeSettings::default(), diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst/src/text/shift.rs index a1862098..73f28343 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst/src/text/shift.rs @@ -1,5 +1,10 @@ -use crate::prelude::*; +use ecow::EcoString; + +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, Show, StyleChain}; +use crate::layout::{Em, Length, Vt}; use crate::text::{variant, SpaceElem, TextElem, TextSize}; +use crate::World; /// Renders text in subscript. /// diff --git a/crates/typst/src/text/smallcaps.rs b/crates/typst/src/text/smallcaps.rs new file mode 100644 index 00000000..b41fdf02 --- /dev/null +++ b/crates/typst/src/text/smallcaps.rs @@ -0,0 +1,32 @@ +use crate::foundations::{func, Content}; +use crate::text::TextElem; + +/// Displays text in small capitals. +/// +/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts +/// support this feature. Sometimes smallcaps are part of a dedicated font and +/// sometimes they are not available at all. In the future, this function will +/// support selecting a dedicated smallcaps font as well as synthesizing +/// smallcaps from normal letters, but this is not yet implemented. +/// +/// # Example +/// ```example +/// #set par(justify: true) +/// #set heading(numbering: "I.") +/// +/// #show heading: it => { +/// set block(below: 10pt) +/// set text(weight: "regular") +/// align(center, smallcaps(it)) +/// } +/// +/// = Introduction +/// #lorem(40) +/// ``` +#[func(title = "Small Capitals")] +pub fn smallcaps( + /// The text to display to small capitals. + body: Content, +) -> Content { + body.styled(TextElem::set_smallcaps(true)) +} diff --git a/crates/typst-library/src/text/quotes.rs b/crates/typst/src/text/smartquote.rs index 7a19b9dc..1ea39c9e 100644 --- a/crates/typst-library/src/text/quotes.rs +++ b/crates/typst/src/text/smartquote.rs @@ -1,7 +1,11 @@ -use typst::syntax::is_newline; +use ecow::EcoString; use unicode_segmentation::UnicodeSegmentation; -use crate::prelude::*; +use crate::diag::{bail, StrResult}; +use crate::foundations::{array, cast, dict, elem, Array, Dict, FromValue, 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. /// @@ -22,8 +26,8 @@ use crate::prelude::*; /// # Syntax /// This function also has dedicated syntax: The normal quote characters /// (`'` and `"`). Typst automatically makes your quotes smart. -#[elem] -pub struct SmartquoteElem { +#[elem(name = "smartquote")] +pub struct SmartQuoteElem { /// Whether this should be a double quote. #[default(true)] pub double: bool, @@ -79,12 +83,12 @@ pub struct SmartquoteElem { /// 'Das sind eigene Anführungszeichen.' /// ``` #[borrowed] - pub quotes: Smart<QuoteDict>, + pub quotes: Smart<SmartQuoteDict>, } /// State machine for smart quote substitution. #[derive(Debug, Clone)] -pub struct Quoter { +pub struct SmartQuoter { /// How many quotes have been opened. quote_depth: usize, /// Whether an opening quote might follow. @@ -95,7 +99,7 @@ pub struct Quoter { prev_quote_type: Option<bool>, } -impl Quoter { +impl SmartQuoter { /// Start quoting. pub fn new() -> Self { Self { @@ -118,7 +122,7 @@ impl Quoter { /// Process and substitute a quote. pub fn quote<'a>( &mut self, - quotes: &Quotes<'a>, + quotes: &SmartQuotes<'a>, double: bool, peeked: Option<char>, ) -> &'a str { @@ -147,7 +151,7 @@ impl Quoter { } } -impl Default for Quoter { +impl Default for SmartQuoter { fn default() -> Self { Self::new() } @@ -162,7 +166,7 @@ fn is_opening_bracket(c: char) -> bool { } /// Decides which quotes to substitute smart quotes with. -pub struct Quotes<'s> { +pub struct SmartQuotes<'s> { /// The opening single quote. pub single_open: &'s str, /// The closing single quote. @@ -173,7 +177,7 @@ pub struct Quotes<'s> { pub double_close: &'s str, } -impl<'s> Quotes<'s> { +impl<'s> SmartQuotes<'s> { /// Create a new `Quotes` struct with the given quotes, optionally falling /// back to the defaults for a language and region. /// @@ -188,7 +192,7 @@ impl<'s> Quotes<'s> { /// /// For unknown languages, the English quotes are used as fallback. pub fn new( - quotes: &'s Smart<QuoteDict>, + quotes: &'s Smart<SmartQuoteDict>, lang: Lang, region: Option<Region>, alternative: bool, @@ -219,13 +223,13 @@ impl<'s> Quotes<'s> { }; fn inner_or_default<'s>( - quotes: Smart<&'s QuoteDict>, - f: impl FnOnce(&'s QuoteDict) -> Smart<&'s QuoteSet>, + quotes: Smart<&'s SmartQuoteDict>, + f: impl FnOnce(&'s SmartQuoteDict) -> Smart<&'s SmartQuoteSet>, default: [&'s str; 2], ) -> [&'s str; 2] { match quotes.and_then(f) { Smart::Auto => default, - Smart::Custom(QuoteSet { open, close }) => { + Smart::Custom(SmartQuoteSet { open, close }) => { [open, close].map(|s| s.as_str()) } } @@ -284,13 +288,13 @@ impl<'s> Quotes<'s> { /// An opening and closing quote. #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct QuoteSet { +pub struct SmartQuoteSet { open: EcoString, close: EcoString, } cast! { - QuoteSet, + SmartQuoteSet, self => array![self.open, self.close].into_value(), value: Array => { let [open, close] = array_to_set(value)?; @@ -334,13 +338,13 @@ fn array_to_set(value: Array) -> StrResult<[EcoString; 2]> { /// A dict of single and double quotes. #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct QuoteDict { - double: Smart<QuoteSet>, - single: Smart<QuoteSet>, +pub struct SmartQuoteDict { + double: Smart<SmartQuoteSet>, + single: Smart<SmartQuoteSet>, } cast! { - QuoteDict, + SmartQuoteDict, self => dict! { "double" => self.double, "single" => self.single }.into_value(), mut value: Dict => { let keys = ["double", "single"]; @@ -362,7 +366,7 @@ cast! { Self { single, double } }, - value: QuoteSet => Self { + value: SmartQuoteSet => Self { double: Smart::Custom(value), single: Smart::Auto, }, diff --git a/crates/typst/src/text/space.rs b/crates/typst/src/text/space.rs new file mode 100644 index 00000000..02463e76 --- /dev/null +++ b/crates/typst/src/text/space.rs @@ -0,0 +1,26 @@ +use crate::foundations::{elem, Behave, Behaviour, PlainText, Repr, Unlabellable}; +use ecow::EcoString; + +/// A text space. +#[elem(Behave, Unlabellable, PlainText, Repr)] +pub struct SpaceElem {} + +impl Repr for SpaceElem { + fn repr(&self) -> EcoString { + EcoString::inline("[ ]") + } +} + +impl Behave for SpaceElem { + fn behaviour(&self) -> Behaviour { + Behaviour::Weak(2) + } +} + +impl Unlabellable for SpaceElem {} + +impl PlainText for SpaceElem { + fn plain_text(&self, text: &mut EcoString) { + text.push(' '); + } +} diff --git a/crates/typst/src/util/deferred.rs b/crates/typst/src/util/deferred.rs index 5a902e23..46cd6e88 100644 --- a/crates/typst/src/util/deferred.rs +++ b/crates/typst/src/util/deferred.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use once_cell::sync::OnceCell; -/// A deferred value. +/// A value that is lazily executed on another thread. /// -/// This is a value that is being executed in parallel and can be waited on. +/// Execution will be started in the background and can be waited on. pub struct Deferred<T>(Arc<OnceCell<T>>); impl<T: Send + Sync + 'static> Deferred<T> { @@ -32,9 +32,16 @@ impl<T: Send + Sync + 'static> Deferred<T> { /// immediately. Otherwise, this will block until the value is /// initialized in another thread. pub fn wait(&self) -> &T { + // Fast path if the value is already available. We don't want to yield + // to rayon in that case. + if let Some(value) = self.0.get() { + return value; + } + // Ensure that we yield to give the deferred value a chance to compute // single-threaded platforms (for WASM compatibility). while let Some(rayon::Yield::Executed) = rayon::yield_now() {} + self.0.wait() } } diff --git a/crates/typst/src/util/fat.rs b/crates/typst/src/util/fat.rs new file mode 100644 index 00000000..d3c9bb20 --- /dev/null +++ b/crates/typst/src/util/fat.rs @@ -0,0 +1,55 @@ +//! Fat pointer handling. +//! +//! This assumes the memory representation of fat pointers. Although it is not +//! guaranteed by Rust, it's improbable that it will change. Still, when the +//! pointer metadata APIs are stable, we should definitely move to them: +//! <https://github.com/rust-lang/rust/issues/81513> + +use std::alloc::Layout; +use std::mem; + +/// Create a fat pointer from a data address and a vtable address. +/// +/// # Safety +/// Must only be called when `T` is a `dyn Trait`. The data address must point +/// to a value whose type implements the trait of `T` and the `vtable` must have +/// been extracted with [`vtable`]. +#[track_caller] +pub unsafe fn from_raw_parts<T: ?Sized>(data: *const (), vtable: *const ()) -> *const T { + let fat = FatPointer { data, vtable }; + debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>()); + mem::transmute_copy::<FatPointer, *const T>(&fat) +} + +/// Create a mutable fat pointer from a data address and a vtable address. +/// +/// # Safety +/// Must only be called when `T` is a `dyn Trait`. The data address must point +/// to a value whose type implements the trait of `T` and the `vtable` must have +/// been extracted with [`vtable`]. +#[track_caller] +pub unsafe fn from_raw_parts_mut<T: ?Sized>(data: *mut (), vtable: *const ()) -> *mut T { + let fat = FatPointer { data, vtable }; + debug_assert_eq!(Layout::new::<*mut T>(), Layout::new::<FatPointer>()); + mem::transmute_copy::<FatPointer, *mut T>(&fat) +} + +/// Extract the address to a trait object's vtable. +/// +/// # Safety +/// Must only be called when `T` is a `dyn Trait`. +#[track_caller] +pub unsafe fn vtable<T: ?Sized>(ptr: *const T) -> *const () { + debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>()); + mem::transmute_copy::<*const T, FatPointer>(&ptr).vtable +} + +/// The memory representation of a trait object pointer. +/// +/// Although this is not guaranteed by Rust, it's improbable that it will +/// change. +#[repr(C)] +struct FatPointer { + data: *const (), + vtable: *const (), +} diff --git a/crates/typst/src/geom/macros.rs b/crates/typst/src/util/macros.rs index b1b50e22..b1b50e22 100644 --- a/crates/typst/src/geom/macros.rs +++ b/crates/typst/src/util/macros.rs diff --git a/crates/typst/src/util/mod.rs b/crates/typst/src/util/mod.rs index 33cadb3b..a57c6f08 100644 --- a/crates/typst/src/util/mod.rs +++ b/crates/typst/src/util/mod.rs @@ -1,15 +1,21 @@ //! Utilities. +pub mod fat; + +#[macro_use] +mod macros; mod deferred; mod pico; +mod scalar; pub use self::deferred::Deferred; pub use self::pico::PicoStr; +pub use self::scalar::Scalar; use std::fmt::{Debug, Formatter}; use std::hash::Hash; use std::num::NonZeroUsize; -use std::ops::Deref; +use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher13}; @@ -149,3 +155,58 @@ impl<T> Hash for Static<T> { state.write_usize(self.0 as *const _ as _); } } + +/// Generic access to a structure's components. +pub trait Get<Index> { + /// The structure's component type. + type Component; + + /// Borrow the component for the specified index. + fn get_ref(&self, index: Index) -> &Self::Component; + + /// Borrow the component for the specified index mutably. + fn get_mut(&mut self, index: Index) -> &mut Self::Component; + + /// Convenience method for getting a copy of a component. + fn get(self, index: Index) -> Self::Component + where + Self: Sized, + Self::Component: Copy, + { + *self.get_ref(index) + } + + /// Convenience method for setting a component. + fn set(&mut self, index: Index, component: Self::Component) { + *self.get_mut(index) = component; + } +} + +/// A numeric type. +pub trait Numeric: + Sized + + Debug + + Copy + + PartialEq + + Neg<Output = Self> + + Add<Output = Self> + + Sub<Output = Self> + + Mul<f64, Output = Self> + + Div<f64, Output = Self> +{ + /// The identity element for addition. + fn zero() -> Self; + + /// Whether `self` is zero. + fn is_zero(self) -> bool { + self == Self::zero() + } + + /// Whether `self` consists only of finite parts. + fn is_finite(self) -> bool; +} + +/// Round a float to two decimal places. +pub fn round_2(value: f64) -> f64 { + (value * 100.0).round() / 100.0 +} diff --git a/crates/typst/src/util/pico.rs b/crates/typst/src/util/pico.rs index 398392f8..9b0e46f1 100644 --- a/crates/typst/src/util/pico.rs +++ b/crates/typst/src/util/pico.rs @@ -3,7 +3,8 @@ use std::fmt::{self, Debug, Formatter}; use ecow::EcoString; use lasso::{Spur, ThreadedRodeo}; use once_cell::sync::Lazy; -use typst_macros::cast; + +use crate::foundations::cast; /// The global string interner. static INTERNER: Lazy<ThreadedRodeo> = Lazy::new(ThreadedRodeo::new); diff --git a/crates/typst/src/geom/scalar.rs b/crates/typst/src/util/scalar.rs index a2b966da..fbd2a28e 100644 --- a/crates/typst/src/geom/scalar.rs +++ b/crates/typst/src/util/scalar.rs @@ -1,4 +1,12 @@ -use super::*; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::iter::Sum; +use std::ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, +}; + +use crate::util::Numeric; /// A 64-bit float that implements `Eq`, `Ord` and `Hash`. /// @@ -6,23 +14,21 @@ use super::*; #[derive(Default, Copy, Clone)] pub struct Scalar(f64); -// We have to detect NaNs this way since `f64::is_nan` isn’t const -// on stable yet: -// ([tracking issue](https://github.com/rust-lang/rust/issues/57241)) -#[allow(clippy::unusual_byte_groupings)] -const fn is_nan_const(x: f64) -> bool { - // Safety: all bit patterns are valid for u64, and f64 has no padding bits. - // We cannot use `f64::to_bits` because it is not const. - let x_bits = unsafe { std::mem::transmute::<f64, u64>(x) }; - (x_bits << 1 >> (64 - 12 + 1)) == 0b0_111_1111_1111 && (x_bits << 12) != 0 -} - impl Scalar { + /// The scalar containing `0.0`. + pub const ZERO: Self = Self(0.0); + + /// The scalar containing `1.0`. + pub const ONE: Self = Self(1.0); + + /// The scalar containing `f64::INFINITY`. + pub const INFINITY: Self = Self(f64::INFINITY); + /// Creates a [`Scalar`] with the given value. /// /// If the value is NaN, then it is set to `0.0` in the result. pub const fn new(x: f64) -> Self { - Self(if is_nan_const(x) { 0.0 } else { x }) + Self(if is_nan(x) { 0.0 } else { x }) } /// Gets the value of this [`Scalar`]. @@ -30,13 +36,17 @@ impl Scalar { pub const fn get(self) -> f64 { self.0 } +} - /// The scalar containing `0.0`. - pub const ZERO: Self = Self(0.0); - /// The scalar containing `1.0`. - pub const ONE: Self = Self(1.0); - /// The scalar containing `f64::INFINITY`. - pub const INFINITY: Self = Self(f64::INFINITY); +// We have to detect NaNs this way since `f64::is_nan` isn’t const +// on stable yet: +// ([tracking issue](https://github.com/rust-lang/rust/issues/57241)) +#[allow(clippy::unusual_byte_groupings)] +const fn is_nan(x: f64) -> bool { + // Safety: all bit patterns are valid for u64, and f64 has no padding bits. + // We cannot use `f64::to_bits` because it is not const. + let x_bits = unsafe { std::mem::transmute::<f64, u64>(x) }; + (x_bits << 1 >> (64 - 12 + 1)) == 0b0_111_1111_1111 && (x_bits << 12) != 0 } impl Numeric for Scalar { @@ -55,12 +65,6 @@ impl Debug for Scalar { } } -impl Repr for Scalar { - fn repr(&self) -> EcoString { - self.0.repr() - } -} - impl Eq for Scalar {} impl PartialEq for Scalar { diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/visualize/color.rs index 3f5ef479..e3c858ff 100644 --- a/crates/typst/src/geom/color.rs +++ b/crates/typst/src/visualize/color.rs @@ -1,6 +1,8 @@ +use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::str::FromStr; -use ecow::EcoVec; +use ecow::{eco_format, EcoString, EcoVec}; use once_cell::sync::Lazy; use palette::convert::FromColorUnclamped; use palette::encoding::{self, Linear}; @@ -8,9 +10,12 @@ use palette::{ Darken, Desaturate, FromColor, Lighten, Okhsva, OklabHue, RgbHue, Saturate, ShiftHue, }; -use super::*; -use crate::diag::{error, At, SourceResult}; -use crate::eval::{Args, IntoValue, Module, Scope, Str}; +use crate::diag::{bail, error, At, SourceResult, StrResult}; +use crate::foundations::{ + array, cast, func, repr, scope, ty, Args, Array, IntoValue, Module, Repr, Scope, Str, + Value, +}; +use crate::layout::{Angle, Ratio}; use crate::syntax::{Span, Spanned}; // Type aliases for `palette` internal types in f32. @@ -1411,15 +1416,15 @@ impl Repr for Color { eco_format!( "oklab({}, {}, {})", Ratio::new(c.l as _).repr(), - format_float(c.a as _, Some(3), ""), - format_float(c.b as _, Some(3), ""), + repr::format_float(c.a as _, Some(3), ""), + repr::format_float(c.b as _, Some(3), ""), ) } else { eco_format!( "oklab({}, {}, {}, {})", Ratio::new(c.l as _).repr(), - format_float(c.a as _, Some(3), ""), - format_float(c.b as _, Some(3), ""), + repr::format_float(c.a as _, Some(3), ""), + repr::format_float(c.b as _, Some(3), ""), Ratio::new(c.alpha as _).repr(), ) } @@ -1429,14 +1434,14 @@ impl Repr for Color { eco_format!( "oklch({}, {}, {})", Ratio::new(c.l as _).repr(), - format_float(c.chroma as _, Some(3), ""), + repr::format_float(c.chroma as _, Some(3), ""), hue_angle(c.hue.into_degrees()).repr(), ) } else { eco_format!( "oklch({}, {}, {}, {})", Ratio::new(c.l as _).repr(), - format_float(c.chroma as _, Some(3), ""), + repr::format_float(c.chroma as _, Some(3), ""), hue_angle(c.hue.into_degrees()).repr(), Ratio::new(c.alpha as _).repr(), ) @@ -1781,7 +1786,9 @@ cast! { }, } -/// A component that must either be: +/// A chroma color component. +/// +/// Must either be: /// - a ratio, in which case it is relative to 0.4. /// - a float, in which case it is taken literally. pub struct ChromaComponent(f32); diff --git a/crates/typst/src/geom/gradient.rs b/crates/typst/src/visualize/gradient.rs index f4a0c7e8..bdd26c0d 100644 --- a/crates/typst/src/geom/gradient.rs +++ b/crates/typst/src/visualize/gradient.rs @@ -1,16 +1,19 @@ use std::f64::consts::{FRAC_PI_2, PI, TAU}; -use std::f64::{EPSILON, NEG_INFINITY}; +use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::sync::Arc; +use ecow::EcoString; use kurbo::Vec2; -use super::*; use crate::diag::{bail, error, SourceResult}; -use crate::eval::{array, cast, func, scope, ty, Args, Array, Cast, Func, IntoValue}; -use crate::geom::color::{Hsl, Hsv}; -use crate::geom::{ColorSpace, Smart}; +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::{Hsl, Hsv}; +use crate::visualize::{Color, ColorSpace, WeightedColor}; /// A color gradient. /// @@ -189,7 +192,7 @@ impl Gradient { span: Span, /// The color [stops](#stops) of the gradient. #[variadic] - stops: Vec<Spanned<Stop>>, + stops: Vec<Spanned<GradientStop>>, /// The color space in which to interpolate the gradient. /// /// Defaults to a perceptually uniform color space called @@ -205,7 +208,7 @@ impl Gradient { /// element. #[named] #[default(Smart::Auto)] - relative: Smart<Relative>, + relative: Smart<GradientRelative>, /// The direction of the gradient. #[external] #[default(Dir::LTR)] @@ -277,7 +280,7 @@ impl Gradient { span: Span, /// The color [stops](#stops) of the gradient. #[variadic] - stops: Vec<Spanned<Stop>>, + stops: Vec<Spanned<GradientStop>>, /// The color space in which to interpolate the gradient. /// /// Defaults to a perceptually uniform color space called @@ -292,7 +295,7 @@ impl Gradient { /// box, column, grid, or stack that contains the element. #[named] #[default(Smart::Auto)] - relative: Smart<Relative>, + relative: Smart<GradientRelative>, /// The center of the end circle of the gradient. /// /// A value of `{(50%, 50%)}` means that the end circle is @@ -387,7 +390,7 @@ impl Gradient { span: Span, /// The color [stops](#stops) of the gradient. #[variadic] - stops: Vec<Spanned<Stop>>, + stops: Vec<Spanned<GradientStop>>, /// The angle of the gradient. #[named] #[default(Angle::zero())] @@ -406,7 +409,7 @@ impl Gradient { /// box, column, grid, or stack that contains the element. #[named] #[default(Smart::Auto)] - relative: Smart<Relative>, + relative: Smart<GradientRelative>, /// The center of the last circle of the gradient. /// /// A value of `{(50%, 50%)}` means that the end circle is @@ -621,22 +624,31 @@ impl Gradient { /// Returns the stops of this gradient. #[func] - pub fn stops(&self) -> Vec<Stop> { + pub fn stops(&self) -> Vec<GradientStop> { match self { Self::Linear(linear) => linear .stops .iter() - .map(|(color, offset)| Stop { color: *color, offset: Some(*offset) }) + .map(|(color, offset)| GradientStop { + color: *color, + offset: Some(*offset), + }) .collect(), Self::Radial(radial) => radial .stops .iter() - .map(|(color, offset)| Stop { color: *color, offset: Some(*offset) }) + .map(|(color, offset)| GradientStop { + color: *color, + offset: Some(*offset), + }) .collect(), Self::Conic(conic) => conic .stops .iter() - .map(|(color, offset)| Stop { color: *color, offset: Some(*offset) }) + .map(|(color, offset)| GradientStop { + color: *color, + offset: Some(*offset), + }) .collect(), } } @@ -653,7 +665,7 @@ impl Gradient { /// Returns the relative placement of this gradient. #[func] - pub fn relative(&self) -> Smart<Relative> { + pub fn relative(&self) -> Smart<GradientRelative> { match self { Self::Linear(linear) => linear.relative, Self::Radial(radial) => radial.relative, @@ -706,7 +718,7 @@ impl Gradient { impl Gradient { /// Clones this gradient, but with a different relative placement. - pub fn with_relative(mut self, relative: Relative) -> Self { + pub fn with_relative(mut self, relative: GradientRelative) -> Self { match &mut self { Self::Linear(linear) => { Arc::make_mut(linear).relative = Smart::Custom(relative); @@ -803,12 +815,12 @@ impl Gradient { /// Returns the relative placement of this gradient, handling /// the special case of `auto`. - pub fn unwrap_relative(&self, on_text: bool) -> Relative { + pub fn unwrap_relative(&self, on_text: bool) -> GradientRelative { self.relative().unwrap_or_else(|| { if on_text { - Relative::Parent + GradientRelative::Parent } else { - Relative::Self_ + GradientRelative::Self_ } }) } @@ -858,7 +870,7 @@ pub struct LinearGradient { /// The color space in which to interpolate the gradient. pub space: ColorSpace, /// The relative placement of the gradient. - pub relative: Smart<Relative>, + pub relative: Smart<GradientRelative>, /// Whether to anti-alias the gradient (used for sharp gradients). pub anti_alias: bool, } @@ -868,13 +880,13 @@ impl Repr for LinearGradient { let mut r = EcoString::from("gradient.linear("); let angle = self.angle.to_rad().rem_euclid(TAU); - if angle.abs() < EPSILON { + if angle.abs() < f64::EPSILON { // Default value, do nothing - } else if (angle - FRAC_PI_2).abs() < EPSILON { + } else if (angle - FRAC_PI_2).abs() < f64::EPSILON { r.push_str("dir: rtl, "); - } else if (angle - PI).abs() < EPSILON { + } else if (angle - PI).abs() < f64::EPSILON { r.push_str("dir: ttb, "); - } else if (angle - 3.0 * FRAC_PI_2).abs() < EPSILON { + } else if (angle - 3.0 * FRAC_PI_2).abs() < f64::EPSILON { r.push_str("dir: btt, "); } else { r.push_str("angle: "); @@ -926,7 +938,7 @@ pub struct RadialGradient { /// The color space in which to interpolate the gradient. pub space: ColorSpace, /// The relative placement of the gradient. - pub relative: Smart<Relative>, + pub relative: Smart<GradientRelative>, /// Whether to anti-alias the gradient (used for sharp gradients). pub anti_alias: bool, } @@ -1004,7 +1016,7 @@ pub struct ConicGradient { /// The color space in which to interpolate the gradient. pub space: ColorSpace, /// The relative placement of the gradient. - pub relative: Smart<Relative>, + pub relative: Smart<GradientRelative>, /// Whether to anti-alias the gradient (used for sharp gradients). pub anti_alias: bool, } @@ -1014,7 +1026,7 @@ impl Repr for ConicGradient { let mut r = EcoString::from("gradient.conic("); let angle = self.angle.to_rad().rem_euclid(TAU); - if angle.abs() > EPSILON { + if angle.abs() > f64::EPSILON { r.push_str("angle: "); r.push_str(&self.angle.repr()); r.push_str(", "); @@ -1058,7 +1070,7 @@ impl Repr for ConicGradient { /// What is the gradient relative to. #[derive(Cast, Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Relative { +pub enum GradientRelative { /// The gradient is relative to itself (its own bounding box). Self_, /// The gradient is relative to its parent (the parent's bounding box). @@ -1067,14 +1079,14 @@ pub enum Relative { /// A color stop. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct Stop { +pub struct GradientStop { /// The color for this stop. pub color: Color, /// The offset of the stop along the gradient. pub offset: Option<Ratio>, } -impl Stop { +impl GradientStop { /// Create a new stop from a `color` and an `offset`. pub fn new(color: Color, offset: Ratio) -> Self { Self { color, offset: Some(offset) } @@ -1082,7 +1094,7 @@ impl Stop { } cast! { - Stop, + GradientStop, self => if let Some(offset) = self.offset { array![self.color.into_value(), offset].into_value() } else { @@ -1136,10 +1148,10 @@ cast! { /// This is split into its own function because it is used by all of the /// different gradient types. #[comemo::memoize] -fn process_stops(stops: &[Spanned<Stop>]) -> SourceResult<Vec<(Color, Ratio)>> { +fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ratio)>> { let has_offset = stops.iter().any(|stop| stop.v.offset.is_some()); if has_offset { - let mut last_stop = NEG_INFINITY; + let mut last_stop = f64::NEG_INFINITY; for Spanned { v: stop, span } in stops.iter() { let Some(stop) = stop.offset else { bail!(error!( @@ -1158,7 +1170,7 @@ fn process_stops(stops: &[Spanned<Stop>]) -> SourceResult<Vec<(Color, Ratio)>> { let out = stops .iter() - .map(|Spanned { v: Stop { color, offset }, span }| { + .map(|Spanned { v: GradientStop { color, offset }, span }| { if offset.unwrap().get() > 1.0 || offset.unwrap().get() < 0.0 { bail!(*span, "offset must be between 0 and 1"); } diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst/src/visualize/image/mod.rs index 0996ae7c..722e77a6 100644 --- a/crates/typst-library/src/visualize/image.rs +++ b/crates/typst/src/visualize/image/mod.rs @@ -1,12 +1,34 @@ +//! Image handling. + +mod raster; +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 typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; -use typst::util::option_eq; +use comemo::{Prehashed, Tracked}; +use ecow::EcoString; -use crate::compute::Readable; -use crate::meta::Figurable; -use crate::prelude::*; -use crate::text::families; +use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::foundations::{ + cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Resolve, Smart, + StyleChain, +}; +use crate::layout::{ + Abs, Axes, FixedAlign, Fragment, Frame, FrameItem, Layout, Length, Point, Regions, + Rel, Size, Vt, +}; +use crate::loading::Readable; +use crate::model::Figurable; +use crate::syntax::Spanned; +use crate::text::{families, Lang, LocalName, Region}; +use crate::util::{option_eq, Numeric}; +use crate::visualize::Path; +use crate::World; /// A raster or vector graphic. /// @@ -269,3 +291,160 @@ pub enum ImageFit { /// this means that the image will be distorted. Stretch, } + +/// A loaded raster or vector image. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Image(Arc<Prehashed<Repr>>); + +/// The internal representation. +#[derive(Hash)] +struct Repr { + /// The raw, undecoded image data. + kind: ImageKind, + /// A text describing the image. + alt: Option<EcoString>, +} + +/// A kind of image. +#[derive(Hash)] +pub enum ImageKind { + /// A raster image. + Raster(RasterImage), + /// An SVG image. + Svg(SvgImage), +} + +impl Image { + /// Create an image from a buffer and a format. + #[comemo::memoize] + pub fn new( + data: Bytes, + format: ImageFormat, + alt: Option<EcoString>, + ) -> StrResult<Self> { + let kind = match format { + ImageFormat::Raster(format) => { + ImageKind::Raster(RasterImage::new(data, format)?) + } + ImageFormat::Vector(VectorFormat::Svg) => { + ImageKind::Svg(SvgImage::new(data)?) + } + }; + + Ok(Self(Arc::new(Prehashed::new(Repr { kind, alt })))) + } + + /// Create a possibly font-dependant image from a buffer and a format. + #[comemo::memoize] + pub fn with_fonts( + data: Bytes, + format: ImageFormat, + alt: Option<EcoString>, + world: Tracked<dyn World + '_>, + families: &[String], + ) -> StrResult<Self> { + let kind = match format { + ImageFormat::Raster(format) => { + ImageKind::Raster(RasterImage::new(data, format)?) + } + ImageFormat::Vector(VectorFormat::Svg) => { + ImageKind::Svg(SvgImage::with_fonts(data, world, families)?) + } + }; + + Ok(Self(Arc::new(Prehashed::new(Repr { kind, alt })))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + match &self.0.kind { + ImageKind::Raster(raster) => raster.data(), + ImageKind::Svg(svg) => svg.data(), + } + } + + /// The format of the image. + pub fn format(&self) -> ImageFormat { + match &self.0.kind { + ImageKind::Raster(raster) => raster.format().into(), + ImageKind::Svg(_) => VectorFormat::Svg.into(), + } + } + + /// The width of the image in pixels. + pub fn width(&self) -> u32 { + match &self.0.kind { + ImageKind::Raster(raster) => raster.width(), + ImageKind::Svg(svg) => svg.width(), + } + } + + /// The height of the image in pixels. + pub fn height(&self) -> u32 { + match &self.0.kind { + ImageKind::Raster(raster) => raster.height(), + ImageKind::Svg(svg) => svg.height(), + } + } + + /// A text describing the image. + pub fn alt(&self) -> Option<&str> { + self.0.alt.as_deref() + } + + /// The decoded image. + pub fn kind(&self) -> &ImageKind { + &self.0.kind + } +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Image") + .field("format", &self.format()) + .field("width", &self.width()) + .field("height", &self.height()) + .field("alt", &self.alt()) + .finish() + } +} + +/// A raster or vector image format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ImageFormat { + /// A raster graphics format. + Raster(RasterFormat), + /// A vector graphics format. + Vector(VectorFormat), +} + +/// A vector graphics format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum VectorFormat { + /// The vector graphics format of the web. + Svg, +} + +impl From<RasterFormat> for ImageFormat { + fn from(format: RasterFormat) -> Self { + Self::Raster(format) + } +} + +impl From<VectorFormat> for ImageFormat { + fn from(format: VectorFormat) -> Self { + Self::Vector(format) + } +} + +cast! { + ImageFormat, + self => match self { + Self::Raster(v) => v.into_value(), + Self::Vector(v) => v.into_value() + }, + v: RasterFormat => Self::Raster(v), + v: VectorFormat => Self::Vector(v), +} diff --git a/crates/typst/src/image/raster.rs b/crates/typst/src/visualize/image/raster.rs index d8235039..5bd1ad10 100644 --- a/crates/typst/src/image/raster.rs +++ b/crates/typst/src/visualize/image/raster.rs @@ -8,10 +8,9 @@ use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; use image::io::Limits; use image::{guess_format, ImageDecoder, ImageResult}; -use typst_macros::Cast; use crate::diag::{bail, StrResult}; -use crate::eval::Bytes; +use crate::foundations::{Bytes, Cast}; /// A decoded raster image. #[derive(Clone, Hash)] diff --git a/crates/typst/src/image/svg.rs b/crates/typst/src/visualize/image/svg.rs index c8db63a5..d7289367 100644 --- a/crates/typst/src/image/svg.rs +++ b/crates/typst/src/visualize/image/svg.rs @@ -8,9 +8,9 @@ use siphasher::sip128::Hasher128; use usvg::{NodeExt, TreeParsing, TreeTextToPath}; use crate::diag::{format_xml_like_error, StrResult}; -use crate::eval::Bytes; -use crate::font::{FontVariant, FontWeight}; -use crate::geom::Axes; +use crate::foundations::Bytes; +use crate::layout::Axes; +use crate::text::{FontVariant, FontWeight}; use crate::World; /// A decoded SVG. diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst/src/visualize/line.rs index da497cf4..118395ec 100644 --- a/crates/typst-library/src/visualize/line.rs +++ b/crates/typst/src/visualize/line.rs @@ -1,4 +1,10 @@ -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{elem, NativeElement, StyleChain}; +use crate::layout::{ + Abs, Angle, Axes, Fragment, Frame, FrameItem, Layout, Length, Regions, Rel, Size, Vt, +}; +use crate::util::Numeric; +use crate::visualize::{Geometry, Stroke}; /// A line from one point to another. /// @@ -76,8 +82,9 @@ impl Layout for LineElem { let target = regions.expand.select(regions.size, size); if !target.is_finite() { - bail!(error!(self.span(), "cannot create line with infinite length")); + bail!(self.span(), "cannot create line with infinite length"); } + let mut frame = Frame::soft(target); let shape = Geometry::Line(delta.to_point()).stroked(stroke); frame.push(start.to_point(), FrameItem::Shape(shape, self.span())); diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst/src/visualize/mod.rs index e39d50bc..e733e5a4 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst/src/visualize/mod.rs @@ -1,22 +1,38 @@ //! Drawing and visualization. +mod color; +mod gradient; mod image; mod line; +mod paint; mod path; mod polygon; mod shape; +mod stroke; +pub use self::color::*; +pub use self::gradient::*; pub use self::image::*; pub use self::line::*; +pub use self::paint::*; pub use self::path::*; pub use self::polygon::*; pub use self::shape::*; +pub use self::stroke::*; -use crate::prelude::*; +use crate::foundations::{category, Category, Scope}; + +/// Drawing and data visualization. +/// +/// If you want to create more advanced drawings or plots, also have a look at +/// the [CetZ](https://github.com/johannes-wolf/cetz) package as well as more +/// specialized [packages]($packages) for your use case. +#[category] +pub static VISUALIZE: Category; /// Hook up all visualize definitions. pub(super) fn define(global: &mut Scope) { - global.category("visualize"); + global.category(VISUALIZE); global.define_type::<Color>(); global.define_type::<Gradient>(); global.define_type::<Stroke>(); diff --git a/crates/typst/src/geom/paint.rs b/crates/typst/src/visualize/paint.rs index 2bedfe13..ca5d0d40 100644 --- a/crates/typst/src/geom/paint.rs +++ b/crates/typst/src/visualize/paint.rs @@ -1,4 +1,9 @@ -use super::*; +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +use crate::foundations::{cast, Repr}; +use crate::visualize::{Color, Gradient, GradientRelative}; /// How a fill or stroke should be painted. #[derive(Clone, Eq, PartialEq, Hash)] @@ -21,12 +26,12 @@ impl Paint { /// Turns this paint into a paint for a text decoration. /// /// If this paint is a gradient, it will be converted to a gradient with - /// relative set to [`Relative::Parent`]. + /// relative set to [`GradientRelative::Parent`]. pub fn as_decoration(&self) -> Self { match self { Self::Solid(color) => Self::Solid(*color), Self::Gradient(gradient) => { - Self::Gradient(gradient.clone().with_relative(Relative::Parent)) + Self::Gradient(gradient.clone().with_relative(GradientRelative::Parent)) } } } @@ -65,8 +70,8 @@ impl From<Gradient> for Paint { cast! { Paint, self => match self { - Self::Solid(color) => Value::Color(color), - Self::Gradient(gradient) => Value::Gradient(gradient), + Self::Solid(color) => color.into_value(), + Self::Gradient(gradient) => gradient.into_value(), }, color: Color => Self::Solid(color), gradient: Gradient => Self::Gradient(gradient), diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst/src/visualize/path.rs index 43b11526..f2e1466f 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst/src/visualize/path.rs @@ -1,7 +1,13 @@ use kurbo::{CubicBez, ParamCurveExtrema}; -use typst::eval::Reflect; -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + array, cast, elem, Array, NativeElement, Reflect, Resolve, Smart, StyleChain, +}; +use crate::layout::{ + Abs, Axes, Fragment, Frame, FrameItem, Layout, Length, Point, Regions, Rel, Size, Vt, +}; +use crate::visualize::{FixedStroke, Geometry, Paint, Shape, Stroke}; use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; @@ -146,6 +152,7 @@ impl Layout for PathElem { } } +/// A component used for path creation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum PathVertex { Vertex(Axes<Rel<Length>>), @@ -206,3 +213,102 @@ cast! { } }, } + +/// A bezier path. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct Path(pub Vec<PathItem>); + +/// An item in a bezier path. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum PathItem { + MoveTo(Point), + LineTo(Point), + CubicTo(Point, Point, Point), + ClosePath, +} + +impl Path { + /// Create an empty path. + pub const fn new() -> Self { + Self(vec![]) + } + + /// Create a path that describes a rectangle. + pub fn rect(size: Size) -> Self { + let z = Abs::zero(); + let point = Point::new; + let mut path = Self::new(); + path.move_to(point(z, z)); + path.line_to(point(size.x, z)); + path.line_to(point(size.x, size.y)); + path.line_to(point(z, size.y)); + path.close_path(); + path + } + + /// Push a [`MoveTo`](PathItem::MoveTo) item. + pub fn move_to(&mut self, p: Point) { + self.0.push(PathItem::MoveTo(p)); + } + + /// Push a [`LineTo`](PathItem::LineTo) item. + pub fn line_to(&mut self, p: Point) { + self.0.push(PathItem::LineTo(p)); + } + + /// Push a [`CubicTo`](PathItem::CubicTo) item. + pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) { + self.0.push(PathItem::CubicTo(p1, p2, p3)); + } + + /// Push a [`ClosePath`](PathItem::ClosePath) item. + pub fn close_path(&mut self) { + self.0.push(PathItem::ClosePath); + } + + /// Computes the size of bounding box of this path. + pub fn bbox_size(&self) -> Size { + let mut min_x = Abs::inf(); + let mut min_y = Abs::inf(); + let mut max_x = -Abs::inf(); + let mut max_y = -Abs::inf(); + + let mut cursor = Point::zero(); + for item in self.0.iter() { + match item { + PathItem::MoveTo(to) => { + min_x = min_x.min(cursor.x); + min_y = min_y.min(cursor.y); + max_x = max_x.max(cursor.x); + max_y = max_y.max(cursor.y); + cursor = *to; + } + PathItem::LineTo(to) => { + min_x = min_x.min(cursor.x); + min_y = min_y.min(cursor.y); + max_x = max_x.max(cursor.x); + max_y = max_y.max(cursor.y); + cursor = *to; + } + PathItem::CubicTo(c0, c1, end) => { + let cubic = kurbo::CubicBez::new( + kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()), + kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()), + kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()), + kurbo::Point::new(end.x.to_pt(), end.y.to_pt()), + ); + + let bbox = cubic.bounding_box(); + min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1)); + min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1)); + max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1)); + max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1)); + cursor = *end; + } + PathItem::ClosePath => (), + } + } + + Size::new(max_x - min_x, max_y - min_y) + } +} diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst/src/visualize/polygon.rs index b1ed9eaa..3b818fa4 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst/src/visualize/polygon.rs @@ -1,6 +1,14 @@ use std::f64::consts::PI; -use crate::prelude::*; +use crate::diag::{bail, SourceResult}; +use crate::foundations::{ + elem, func, scope, Content, NativeElement, Resolve, Smart, StyleChain, +}; +use crate::layout::{ + Axes, Em, Fragment, Frame, FrameItem, Layout, Length, Point, Regions, Rel, Vt, +}; +use crate::util::Numeric; +use crate::visualize::{FixedStroke, Geometry, Paint, Path, Shape, Stroke}; /// A closed polygon. /// @@ -131,8 +139,9 @@ impl Layout for PolygonElem { let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size(); if !size.is_finite() { - bail!(error!(self.span(), "cannot create polygon with infinite size")); + bail!(self.span(), "cannot create polygon with infinite size"); } + let mut frame = Frame::hard(size); // Only create a path if there are more than zero points. diff --git a/crates/typst/src/visualize/shape.rs b/crates/typst/src/visualize/shape.rs new file mode 100644 index 00000000..d1a2a155 --- /dev/null +++ b/crates/typst/src/visualize/shape.rs @@ -0,0 +1,1221 @@ +use std::f64::consts::SQRT_2; + +use crate::diag::SourceResult; +use crate::foundations::{elem, Content, NativeElement, Resolve, Smart, StyleChain}; +use crate::layout::{ + Abs, Axes, Corner, Corners, Fragment, Frame, FrameItem, Layout, Length, Point, Ratio, + Regions, Rel, Sides, Size, Vt, +}; +use crate::syntax::Span; +use crate::util::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", Layout)] +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: Smart<Rel<Length>>, + + /// 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]($stroke) + /// - A dictionary describing the stroke for each side inidvidually. 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(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] + pub body: Option<Content>, +} + +impl Layout for RectElem { + #[tracing::instrument(name = "RectElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Rect, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles), + self.inset(styles), + self.outset(styles), + self.radius(styles), + self.span(), + ) + } +} + +/// A square with optional content. +/// +/// # Example +/// ```example +/// // Without content. +/// #square(size: 40pt) +/// +/// // With content. +/// #square[ +/// Automatically \ +/// sized to fit. +/// ] +/// ``` +#[elem(Layout)] +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, + })] + pub height: Smart<Rel<Length>>, + + /// 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(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] + pub body: Option<Content>, +} + +impl Layout for SquareElem { + #[tracing::instrument(name = "SquareElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Square, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles), + self.inset(styles), + self.outset(styles), + self.radius(styles), + 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(Layout)] +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: Smart<Rel<Length>>, + + /// 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(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] + pub body: Option<Content>, +} + +impl Layout for EllipseElem { + #[tracing::instrument(name = "EllipseElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Ellipse, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles).map(Sides::splat), + self.inset(styles), + self.outset(styles), + Corners::splat(Rel::zero()), + 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(Layout)] +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, + })] + pub height: Smart<Rel<Length>>, + + /// 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(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] + pub body: Option<Content>, +} + +impl Layout for CircleElem { + #[tracing::instrument(name = "CircleElem::layout", skip_all)] + fn layout( + &self, + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + ) -> SourceResult<Fragment> { + layout( + vt, + styles, + regions, + ShapeKind::Circle, + &self.body(styles), + Axes::new(self.width(styles), self.height(styles)), + self.fill(styles), + self.stroke(styles).map(Sides::splat), + self.inset(styles), + self.outset(styles), + Corners::splat(Rel::zero()), + self.span(), + ) + } +} + +/// Layout a shape. +#[tracing::instrument(name = "shape::layout", skip_all)] +#[allow(clippy::too_many_arguments)] +fn layout( + vt: &mut Vt, + styles: StyleChain, + regions: Regions, + kind: ShapeKind, + body: &Option<Content>, + sizing: Axes<Smart<Rel<Length>>>, + fill: Option<Paint>, + stroke: Smart<Sides<Option<Stroke<Abs>>>>, + mut inset: Sides<Rel<Abs>>, + outset: Sides<Rel<Abs>>, + radius: Corners<Rel<Abs>>, + span: Span, +) -> SourceResult<Fragment> { + let resolved = sizing + .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r))); + + let mut frame; + if let Some(child) = body { + let region = resolved.unwrap_or(regions.base()); + if kind.is_round() { + inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0)); + } + + // Pad the child. + let child = child.clone().padded(inset.map(|side| side.map(Length::from))); + let expand = sizing.as_ref().map(Smart::is_custom); + let pod = Regions::one(region, expand); + frame = child.layout(vt, styles, pod)?.into_frame(); + + // Enforce correct size. + *frame.size_mut() = expand.select(region, frame.size()); + + // Relayout with full expansion into square region to make sure + // the result is really a square or circle. + if kind.is_quadratic() { + frame.set_size(Size::splat(frame.size().max_by_side())); + let length = frame.size().max_by_side().min(region.min_by_side()); + let pod = Regions::one(Size::splat(length), Axes::splat(true)); + frame = child.layout(vt, styles, pod)?.into_frame(); + } + + // Enforce correct size again. + *frame.size_mut() = expand.select(region, frame.size()); + if kind.is_quadratic() { + frame.set_size(Size::splat(frame.size().max_by_side())); + } + } else { + // The default size that a shape takes on if it has no child and + // enough space. + let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)); + let mut size = resolved.unwrap_or(default.min(regions.base())); + if kind.is_quadratic() { + size = Size::splat(size.min_by_side()); + } + frame = Frame::soft(size); + } + + // Prepare stroke. + let stroke = match stroke { + Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())), + Smart::Auto => Sides::splat(None), + Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)), + }; + + // Add fill and/or stroke. + if fill.is_some() || stroke.iter().any(Option::is_some) { + if kind.is_round() { + let outset = outset.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); + frame.prepend(pos, FrameItem::Shape(shape, span)); + } else { + frame.fill_and_stroke(fill, stroke, outset, radius, span); + } + } + + // Apply metadata. + frame.meta(styles, false); + + Ok(Fragment::frame(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 border stroke. + pub stroke: Option<FixedStroke>, +} + +/// 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), stroke: None } + } + + /// Stroke the geometry without a fill. + pub fn stroked(self, stroke: FixedStroke) -> Shape { + Shape { geometry: self, fill: None, 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 } +} + +/// Creates a new rectangle as a path. +pub(crate) fn clip_rect( + size: Size, + radius: Corners<Rel<Abs>>, + stroke: &Sides<Option<FixedStroke>>, +) -> Path { + let stroke_widths = stroke + .as_ref() + .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0)); + + let max_radius = (size.x.min(size.y)) / 2.0 + + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); + + let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius)); + + let corners = corners_control_points(size, radius, stroke, stroke_widths); + + let mut path = Path::new(); + if corners.top_left.arc_inner() { + path.arc_move( + corners.top_left.start_inner(), + corners.top_left.center_inner(), + corners.top_left.end_inner(), + ); + } else { + path.move_to(corners.top_left.center_inner()); + } + for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] { + if corner.arc_inner() { + path.arc_line(corner.start_inner(), corner.center_inner(), corner.end_inner()) + } else { + path.line_to(corner.center_inner()); + } + } + path.close_path(); + path +} + +/// 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( + size: Size, + radius: Corners<Rel<Abs>>, + fill: Option<Paint>, + stroke: Sides<Option<FixedStroke>>, +) -> Vec<Shape> { + if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) { + simple_rect(size, fill, stroke.top) + } else { + segmented_rect(size, radius, fill, stroke) + } +} + +/// Use rect primitive for the rectangle +fn simple_rect( + size: Size, + fill: Option<Paint>, + stroke: Option<FixedStroke>, +) -> Vec<Shape> { + vec![Shape { geometry: Geometry::Rect(size), fill, stroke }] +} + +fn corners_control_points( + size: Size, + radius: Corners<Abs>, + strokes: &Sides<Option<FixedStroke>>, + stroke_widths: Sides<Abs>, +) -> Corners<ControlPoints> { + Corners { + top_left: Corner::TopLeft, + top_right: Corner::TopRight, + bottom_right: Corner::BottomRight, + bottom_left: Corner::BottomLeft, + } + .map(|corner| ControlPoints { + radius: radius.get(corner), + stroke_before: stroke_widths.get(corner.side_ccw()), + stroke_after: stroke_widths.get(corner.side_cw()), + corner, + size, + same: match ( + strokes.get_ref(corner.side_ccw()), + strokes.get_ref(corner.side_cw()), + ) { + (Some(a), Some(b)) => a.paint == b.paint && a.dash_pattern == b.dash_pattern, + (None, None) => true, + _ => false, + }, + }) +} + +/// Use stroke and fill for the rectangle +fn segmented_rect( + size: Size, + radius: Corners<Rel<Abs>>, + fill: Option<Paint>, + strokes: Sides<Option<FixedStroke>>, +) -> Vec<Shape> { + let mut res = vec![]; + let stroke_widths = strokes + .as_ref() + .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0)); + + let max_radius = (size.x.min(size.y)) / 2.0 + + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); + + let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius)); + + let corners = corners_control_points(size, radius, &strokes, stroke_widths); + + // insert stroked sides below filled sides + let mut stroke_insert = 0; + + // fill shape with inner curve + if let Some(fill) = fill { + let mut path = Path::new(); + let c = corners.get_ref(Corner::TopLeft); + if c.arc() { + path.arc_move(c.start(), c.center(), c.end()); + } else { + path.move_to(c.center()); + }; + + for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] { + let c = corners.get_ref(corner); + if c.arc() { + path.arc_line(c.start(), c.center(), c.end()); + } else { + path.line_to(c.center()); + } + } + path.close_path(); + res.push(Shape { + geometry: Geometry::Path(path), + fill: Some(fill), + stroke: None, + }); + stroke_insert += 1; + } + + let current = corners.iter().find(|c| !c.same).map(|c| c.corner); + if let Some(mut current) = current { + // multiple segments + // start at a corner with a change between sides and iterate clockwise all other corners + let mut last = current; + for _ in 0..4 { + current = current.next_cw(); + if corners.get_ref(current).same { + continue; + } + // create segment + let start = last; + let end = current; + last = current; + let stroke = match strokes.get_ref(start.side_cw()) { + None => continue, + Some(stroke) => stroke.clone(), + }; + let (shape, ontop) = segment(start, end, &corners, stroke); + if ontop { + res.push(shape); + } else { + res.insert(stroke_insert, shape); + stroke_insert += 1; + } + } + } else if let Some(stroke) = strokes.top { + // single segment + let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke); + res.push(shape); + } + res +} + +fn path_segment( + start: Corner, + end: Corner, + corners: &Corners<ControlPoints>, + path: &mut Path, +) { + // create start corner + let c = corners.get_ref(start); + if start == end || !c.arc() { + path.move_to(c.end()); + } else { + path.arc_move(c.mid(), c.center(), c.end()); + } + + // create corners between start and end + let mut current = start.next_cw(); + while current != end { + let c = corners.get_ref(current); + if c.arc() { + path.arc_line(c.start(), c.center(), c.end()); + } else { + path.line_to(c.end()); + } + current = current.next_cw(); + } + + // create end corner + let c = corners.get_ref(end); + if !c.arc() { + path.line_to(c.start()); + } else if start == end { + path.arc_line(c.start(), c.center(), c.end()); + } else { + path.arc_line(c.start(), c.center(), c.mid()); + } +} + +/// Returns the shape for the segment and whether the shape should be drawn on top. +fn segment( + start: Corner, + end: Corner, + corners: &Corners<ControlPoints>, + stroke: FixedStroke, +) -> (Shape, bool) { + fn fill_corner(corner: &ControlPoints) -> bool { + corner.stroke_before != corner.stroke_after + || corner.radius() < corner.stroke_before + } + + fn fill_corners( + start: Corner, + end: Corner, + corners: &Corners<ControlPoints>, + ) -> bool { + if fill_corner(corners.get_ref(start)) { + return true; + } + if fill_corner(corners.get_ref(end)) { + return true; + } + let mut current = start.next_cw(); + while current != end { + if fill_corner(corners.get_ref(current)) { + return true; + } + current = current.next_cw(); + } + false + } + + let solid = stroke + .dash_pattern + .as_ref() + .map(|pattern| pattern.array.is_empty()) + .unwrap_or(true); + + let use_fill = solid && fill_corners(start, end, corners); + + let shape = if use_fill { + fill_segment(start, end, corners, stroke) + } else { + stroke_segment(start, end, corners, stroke) + }; + (shape, use_fill) +} + +/// Stroke the sides from `start` to `end` clockwise. +fn stroke_segment( + start: Corner, + end: Corner, + corners: &Corners<ControlPoints>, + stroke: FixedStroke, +) -> Shape { + // create start corner + let mut path = Path::new(); + path_segment(start, end, corners, &mut path); + + Shape { + geometry: Geometry::Path(path), + stroke: Some(stroke), + fill: None, + } +} + +/// Fill the sides from `start` to `end` clockwise. +fn fill_segment( + start: Corner, + end: Corner, + corners: &Corners<ControlPoints>, + stroke: FixedStroke, +) -> Shape { + let mut path = Path::new(); + + // create the start corner + // begin on the inside and finish on the outside + // no corner if start and end are equal + // half corner if different + if start == end { + let c = corners.get_ref(start); + path.move_to(c.end_inner()); + path.line_to(c.end_outer()); + } else { + let c = corners.get_ref(start); + + if c.arc_inner() { + path.arc_move(c.end_inner(), c.center_inner(), c.mid_inner()); + } else { + path.move_to(c.end_inner()); + } + + if c.arc_outer() { + path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); + } else { + path.line_to(c.outer()); + path.line_to(c.end_outer()); + } + } + + // create the clockwise outside path for the corners between start and end + let mut current = start.next_cw(); + while current != end { + let c = corners.get_ref(current); + if c.arc_outer() { + path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); + } else { + path.line_to(c.outer()); + } + current = current.next_cw(); + } + + // create the end corner + // begin on the outside and finish on the inside + // full corner if start and end are equal + // half corner if different + if start == end { + let c = corners.get_ref(end); + if c.arc_outer() { + path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); + } else { + path.line_to(c.outer()); + path.line_to(c.end_outer()); + } + if c.arc_inner() { + path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); + } else { + path.line_to(c.center_inner()); + } + } else { + let c = corners.get_ref(end); + if c.arc_outer() { + path.arc_line(c.start_outer(), c.center_outer(), c.mid_outer()); + } else { + path.line_to(c.outer()); + } + if c.arc_inner() { + path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); + } else { + path.line_to(c.center_inner()); + } + } + + // create the counterclockwise inside path for the corners between start and end + let mut current = end.next_ccw(); + while current != start { + let c = corners.get_ref(current); + if c.arc_inner() { + path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); + } else { + path.line_to(c.center_inner()); + } + current = current.next_ccw(); + } + + path.close_path(); + + Shape { + geometry: Geometry::Path(path), + stroke: None, + fill: Some(stroke.paint), + } +} + +/// Helper to calculate different control points for the corners. +/// Clockwise orientation from start to end. +/// ```text +/// O-------------------EO --- - Z: Zero/Origin ({x: 0, y: 0} for top left corner) +/// |\ ___----''' | | - O: Outer: intersection between the straight outer lines +/// | \ / | | - S_: start +/// | MO | | - M_: midpoint +/// | /Z\ __-----------E | - E_: end +/// |/ \M | ro - r_: radius +/// | /\ | | - middle of the stroke +/// | / \ | | - arc from S through M to E with center C and radius r +/// | | MI--EI------- | - outer curve +/// | | / \ | - arc from SO through MO to EO with center CO and radius ro +/// SO | | \ CO --- - inner curve +/// | | | \ - arc from SI through MI to EI with center CI and radius ri +/// |--S-SI-----CI C +/// |--ri--| +/// |-------r--------| +/// ``` +struct ControlPoints { + radius: Abs, + stroke_after: Abs, + stroke_before: Abs, + corner: Corner, + size: Size, + same: bool, +} + +impl ControlPoints { + /// Move and rotate the point from top-left to the required corner. + fn rotate(&self, point: Point) -> Point { + match self.corner { + Corner::TopLeft => point, + Corner::TopRight => Point { x: self.size.x - point.y, y: point.x }, + Corner::BottomRight => { + Point { x: self.size.x - point.x, y: self.size.y - point.y } + } + Corner::BottomLeft => Point { x: point.y, y: self.size.y - point.x }, + } + } + + /// Outside intersection of the sides. + pub fn outer(&self) -> Point { + self.rotate(Point { x: -self.stroke_before, y: -self.stroke_after }) + } + + /// Center for the outer arc. + pub fn center_outer(&self) -> Point { + let r = self.radius_outer(); + self.rotate(Point { + x: r - self.stroke_before, + y: r - self.stroke_after, + }) + } + + /// Center for the middle arc. + pub fn center(&self) -> Point { + let r = self.radius(); + self.rotate(Point { x: r, y: r }) + } + + /// Center for the inner arc. + pub fn center_inner(&self) -> Point { + let r = self.radius_inner(); + + self.rotate(Point { + x: self.stroke_before + r, + y: self.stroke_after + r, + }) + } + + /// Radius of the outer arc. + pub fn radius_outer(&self) -> Abs { + self.radius + } + + /// Radius of the middle arc. + pub fn radius(&self) -> Abs { + (self.radius - self.stroke_before.min(self.stroke_after)).max(Abs::zero()) + } + + /// Radius of the inner arc. + pub fn radius_inner(&self) -> Abs { + (self.radius - 2.0 * self.stroke_before.max(self.stroke_after)).max(Abs::zero()) + } + + /// Middle of the corner on the outside of the stroke. + pub fn mid_outer(&self) -> Point { + let c_i = self.center_inner(); + let c_o = self.center_outer(); + let o = self.outer(); + let r = self.radius_outer(); + + // https://math.stackexchange.com/a/311956 + // intersection between the line from inner center to outside and the outer arc + let a = (o.x - c_i.x).to_raw().powi(2) + (o.y - c_i.y).to_raw().powi(2); + let b = 2.0 * (o.x - c_i.x).to_raw() * (c_i.x - c_o.x).to_raw() + + 2.0 * (o.y - c_i.y).to_raw() * (c_i.y - c_o.y).to_raw(); + let c = (c_i.x - c_o.x).to_raw().powi(2) + (c_i.y - c_o.y).to_raw().powi(2) + - r.to_raw().powi(2); + let t = (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a); + c_i + t * (o - c_i) + } + + /// Middle of the corner in the middle of the stroke. + pub fn mid(&self) -> Point { + let center = self.center_outer(); + let outer = self.outer(); + let diff = outer - center; + center + diff / diff.hypot().to_raw() * self.radius().to_raw() + } + + /// Middle of the corner on the inside of the stroke. + pub fn mid_inner(&self) -> Point { + let center = self.center_inner(); + let outer = self.outer(); + let diff = outer - center; + center + diff / diff.hypot().to_raw() * self.radius_inner().to_raw() + } + + /// If an outer arc is required. + pub fn arc_outer(&self) -> bool { + self.radius_outer() > Abs::zero() + } + + pub fn arc(&self) -> bool { + self.radius() > Abs::zero() + } + + /// If an inner arc is required. + pub fn arc_inner(&self) -> bool { + self.radius_inner() > Abs::zero() + } + + /// Start of the corner on the outside of the stroke. + pub fn start_outer(&self) -> Point { + self.rotate(Point { + x: -self.stroke_before, + y: self.radius_outer() - self.stroke_after, + }) + } + + /// Start of the corner in the center of the stroke. + pub fn start(&self) -> Point { + self.rotate(Point::with_y(self.radius())) + } + + /// Start of the corner on the inside of the stroke. + pub fn start_inner(&self) -> Point { + self.rotate(Point { + x: self.stroke_before, + y: self.stroke_after + self.radius_inner(), + }) + } + + /// End of the corner on the outside of the stroke. + pub fn end_outer(&self) -> Point { + self.rotate(Point { + x: self.radius_outer() - self.stroke_before, + y: -self.stroke_after, + }) + } + + /// End of the corner in the center of the stroke. + pub fn end(&self) -> Point { + self.rotate(Point::with_x(self.radius())) + } + + /// End of the corner on the inside of the stroke. + pub fn end_inner(&self) -> Point { + self.rotate(Point { + x: self.stroke_before + self.radius_inner(), + y: self.stroke_after, + }) + } +} + +/// Helper to draw arcs with bezier curves. +trait PathExt { + fn arc(&mut self, start: Point, center: Point, end: Point); + fn arc_move(&mut self, start: Point, center: Point, end: Point); + fn arc_line(&mut self, start: Point, center: Point, end: Point); +} + +impl PathExt for Path { + fn arc(&mut self, start: Point, center: Point, end: Point) { + let arc = bezier_arc_control(start, center, end); + self.cubic_to(arc[0], arc[1], end); + } + + fn arc_move(&mut self, start: Point, center: Point, end: Point) { + self.move_to(start); + self.arc(start, center, end); + } + + fn arc_line(&mut self, start: Point, center: Point, end: Point) { + self.line_to(start); + self.arc(start, center, end); + } +} + +/// Get the control points for a bezier curve that approximates a circular arc for +/// a start point, an end point and a center of the circle whose arc connects +/// the two. +fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { + // https://stackoverflow.com/a/44829356/1567835 + let a = start - center; + let b = end - center; + + let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw(); + let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw(); + let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2) + / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw()); + + let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x); + let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x); + + [control_1, control_2] +} diff --git a/crates/typst/src/geom/stroke.rs b/crates/typst/src/visualize/stroke.rs index 8d9aec5e..3a90c3b9 100644 --- a/crates/typst/src/geom/stroke.rs +++ b/crates/typst/src/visualize/stroke.rs @@ -1,6 +1,13 @@ -use super::*; -use crate::diag::SourceResult; -use crate::eval::{dict, Args, Cast, FromValue, NoneValue}; +use ecow::EcoString; + +use crate::diag::{SourceResult, StrResult}; +use crate::foundations::{ + cast, dict, func, scope, ty, Args, Cast, Dict, Fold, FromValue, NoneValue, Repr, + Resolve, Smart, StyleChain, Value, +}; +use crate::layout::{Abs, Length}; +use crate::util::{Numeric, Scalar}; +use crate::visualize::{Color, Gradient, Paint}; /// Defines how to draw a line. /// @@ -322,7 +329,7 @@ impl<T: Numeric + Repr> Repr for Stroke<T> { if let Smart::Custom(miter_limit) = &miter_limit { r.push_str(sep); r.push_str("miter-limit: "); - r.push_str(&miter_limit.repr()); + r.push_str(&miter_limit.get().repr()); } r.push(')'); } |
