diff options
| -rw-r--r-- | Cargo.lock | 61 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -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 | ||||
| -rw-r--r-- | docs/dev/architecture.md | 23 | ||||
| -rw-r--r-- | docs/reference/categories.yml | 178 | ||||
| -rw-r--r-- | docs/reference/groups.yml | 82 | ||||
| -rw-r--r-- | docs/reference/packages.md | 6 | ||||
| -rw-r--r-- | tests/Cargo.toml | 1 | ||||
| -rw-r--r-- | tests/src/benches.rs | 27 | ||||
| -rw-r--r-- | tests/src/tests.rs | 28 |
250 files changed, 9574 insertions, 9035 deletions
@@ -2704,14 +2704,26 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" name = "typst" version = "0.9.0" dependencies = [ + "az", "bitflags 2.4.1", + "chinese-number", + "ciborium", "comemo", + "csv", "ecow", "fontdb", + "hayagriva", + "hypher", + "icu_properties", + "icu_provider", + "icu_provider_adapters", + "icu_provider_blob", + "icu_segmenter", "image", "indexmap 2.0.2", "kurbo", "lasso", + "lipsum", "log", "once_cell", "palette", @@ -2720,16 +2732,22 @@ dependencies = [ "roxmltree", "rustybuzz", "serde", + "serde_json", + "serde_yaml 0.9.27", "siphasher", "smallvec", "stacker", + "syntect", "time", "toml", "tracing", "ttf-parser", + "typed-arena", "typst-macros", "typst-syntax", + "unicode-bidi", "unicode-math-class", + "unicode-script", "unicode-segmentation", "usvg", "wasmi", @@ -2771,7 +2789,6 @@ dependencies = [ "tracing-flame", "tracing-subscriber", "typst", - "typst-library", "typst-pdf", "typst-render", "typst-svg", @@ -2795,7 +2812,6 @@ dependencies = [ "syntect", "typed-arena", "typst", - "typst-library", "unicode_names2", "unscanny", "yaml-front-matter", @@ -2815,46 +2831,6 @@ dependencies = [ ] [[package]] -name = "typst-library" -version = "0.9.0" -dependencies = [ - "az", - "chinese-number", - "ciborium", - "comemo", - "csv", - "ecow", - "hayagriva", - "hypher", - "icu_properties", - "icu_provider", - "icu_provider_adapters", - "icu_provider_blob", - "icu_segmenter", - "indexmap 2.0.2", - "kurbo", - "lipsum", - "log", - "once_cell", - "roxmltree", - "rustybuzz", - "serde_json", - "serde_yaml 0.9.27", - "smallvec", - "syntect", - "time", - "toml", - "tracing", - "ttf-parser", - "typed-arena", - "typst", - "unicode-bidi", - "unicode-math-class", - "unicode-script", - "unicode-segmentation", -] - -[[package]] name = "typst-macros" version = "0.9.0" dependencies = [ @@ -2948,7 +2924,6 @@ dependencies = [ "tiny-skia", "ttf-parser", "typst", - "typst-library", "typst-pdf", "typst-render", "typst-svg", @@ -20,7 +20,6 @@ typst = { path = "crates/typst" } typst-cli = { path = "crates/typst-cli" } typst-docs = { path = "crates/typst-docs" } typst-ide = { path = "crates/typst-ide" } -typst-library = { path = "crates/typst-library" } typst-macros = { path = "crates/typst-macros" } typst-pdf = { path = "crates/typst-pdf" } typst-render = { path = "crates/typst-render" } @@ -98,7 +97,7 @@ tar = "0.4" tempfile = "3.7.0" time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] } tiny-skia = "0.11" -toml = { version = "0.8", default-features = false, features = ["parse"] } +toml = { version = "0.8", default-features = false, features = ["parse", "display"] } tracing = "0.1.37" tracing-error = "0.2" tracing-flame = "0.2.0" 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(')'); } diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 3dd27a41..947796af 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -3,20 +3,24 @@ Wondering how to contribute or just curious how Typst works? This document covers the general structure and architecture of Typst's compiler, so you get an understanding of what's where and how everything fits together. + ## Directories Let's start with a broad overview of the directories in this repository: -- `crates/typst`: The main compiler crate which is home to the parser, - interpreter, exporters, IDE tooling, and more. -- `crates/typst-library`: Typst's standard library with all global definitions - available in Typst. Also contains the layout and text handling pipeline. +- `crates/typst`: The main compiler crate which defines the complete language + and library. - `crates/typst-cli`: Typst's command line interface. This is a relatively small - layer on top of `typst` and `typst-library`. + layer on top of the compiler and the exporters. - `crates/typst-docs`: Generates the content of the official [documentation][docs] from the content of the `docs` folder and the inline Rust documentation. Only generates the content and structure, not the concrete HTML (that part is currently closed source). -- `crates/typst-macros`: Procedural macros for the compiler and library. +- `crates/typst-ide`: Exposes IDE functionality. +- `crates/typst-macros`: Procedural macros for the compiler. +- `crates/typst-pdf`: The PDF exporter. +- `crates/typst-render`: A renderer for Typst frames. +- `crates/typst-svg`: The SVG exporter. +- `crates/typst-syntax`: Home to the parser and syntax tree definition. - `docs`: Source files for longer-form parts of the documentation. Individual elements and functions are documented inline with the Rust source code. - `assets`: Fonts and files used for tests and the documentation. @@ -137,10 +141,11 @@ reuse as much as possible. ## Export -Exporters live in `crates/typst/src/export`. They turn layouted frames into an -output file format. +Exporters live in separate crates. They turn layouted frames into an output file +format. - The PDF exporter takes layouted frames and turns them into a PDF file. +- The SVG exporter takes a frame and turns it into an SVG. - The built-in renderer takes a frame and turns it into a pixel buffer. - HTML export does not exist yet, but will in the future. However, this requires some complex compiler work because the export will start with `Content` @@ -148,7 +153,7 @@ output file format. ## IDE -The `crates/typst/src/ide` module implements IDE functionality for Typst. It +The `crates/typst-ide` crate implements IDE functionality for Typst. It builds heavily on the other modules (most importantly, `syntax` and `eval`). **Syntactic:** diff --git a/docs/reference/categories.yml b/docs/reference/categories.yml deleted file mode 100644 index 468c9f21..00000000 --- a/docs/reference/categories.yml +++ /dev/null @@ -1,178 +0,0 @@ -# Descriptions of the documentation categories. - -foundations: | - 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. - -text: | - Text styling. - - The [text function]($text) is of particular interest. - -math: | - 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. - -layout: | - Arranging elements on the page in different ways. - - By combining layout functions, you can create complex and automatic layouts. - -visualize: | - 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. - -meta: | - Document structuring, introspection, and metadata configuration. - - Here, you can find functions to structure your document and interact with that - structure. This includes section headings and figures, bibliography - management, cross-referencing and more. - - Moreover, 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. And 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. - -symbols: | - 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. - -sym: | - Named general symbols. - - For example, `#sym.arrow` produces the → symbol. Within - [formulas]($category/math), these symbols can be used without the `#sym.` - prefix. - - The `d` in an integral's `dx` can be written as `[$dif x$]`. - Outside math formulas, `dif` can be accessed as `math.dif`. - -emoji: | - Named emoji. - - For example, `#emoji.face` produces the 😀 emoji. If you frequently use - certain emojis, you can also import them from the `emoji` module (`[#import - emoji: face]`) to use them without the `#emoji.` prefix. - -data-loading: | - Data loading from external files. - - These functions help you with loading and embedding data, for example from - the results of an experiment. - -packages: | - Typst [packages]($scripting/#packages) encapsulate reusable building blocks - and make them reusable across projects. Below is a list of Typst packages - created by the community. Due to the early and experimental nature of Typst's - package management, they all live in a `preview` namespace. Click on a - package's name to view its documentation and use the copy button on the right - to get a full import statement for it. diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index c6ec4533..fc2b845d 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -2,30 +2,33 @@ # together into one documentation page although they are not part of any scope. - name: variants - display: Variants + title: Variants category: math - functions: ["serif", "sans", "frak", "mono", "bb", "cal"] - description: | + path: ["math"] + filter: ["serif", "sans", "frak", "mono", "bb", "cal"] + details: | Alternate typefaces within formulas. These functions are distinct from the [`text`]($text) function because math fonts contain multiple variants of each letter. - name: styles - display: Styles + title: Styles category: math - functions: ["upright", "italic", "bold"] - description: | + path: ["math"] + filter: ["upright", "italic", "bold"] + details: | Alternate letterforms within formulas. These functions are distinct from the [`text`]($text) function because math fonts contain multiple variants of each letter. - name: sizes - display: Sizes + title: Sizes category: math - functions: ["display", "inline", "script", "sscript"] - description: | + path: ["math"] + filter: ["display", "inline", "script", "sscript"] + details: | Forced size styles for expressions within formulas. These functions allow manual configuration of the size of equation elements @@ -33,9 +36,10 @@ sub/superscripts. - name: underover - display: Under/Over + title: Under/Over category: math - functions: [ + path: ["math"] + filter: [ "underline", "overline", "underbrace", @@ -43,17 +47,18 @@ "underbracket", "overbracket", ] - description: | + details: | Delimiters above or below parts of an equation. The braces and brackets further allow you to add an optional annotation below or above themselves. - name: roots - display: Roots + title: Roots category: math - functions: ["root", "sqrt"] - description: | + path: ["math"] + filter: ["root", "sqrt"] + details: | Square and non-square roots. # Example @@ -63,10 +68,11 @@ ``` - name: attach - display: Attach + title: Attach category: math - functions: ["attach", "scripts", "limits"] - description: | + path: ["math"] + filter: ["attach", "scripts", "limits"] + details: | Subscript, superscripts, and limits. Attachments can be displayed either as sub/superscripts, or limits. Typst @@ -84,10 +90,11 @@ hat (`^`) to indicate a superscript i.e. top attachment. - name: lr - display: Left/Right + title: Left/Right category: math - functions: ["lr", "abs", "norm", "floor", "ceil", "round"] - description: | + path: ["math"] + filter: ["lr", "abs", "norm", "floor", "ceil", "round"] + details: | Delimiter matching. The `lr` function allows you to match two delimiters and scale them with the @@ -105,10 +112,10 @@ ``` - name: calc - display: Calculation + title: Calculation category: foundations path: ["calc"] - description: | + details: | Module for calculations and processing of numeric values. These definitions are part of the `calc` module and not imported by default. @@ -116,12 +123,37 @@ the constants `pi`, `tau`, `e`, `inf`, and `nan`. - name: sys - display: System + title: System category: foundations path: ["sys"] - description: | + details: | Module for system interactions. Currently, this module defines a single item: The `sys.version` constant (of type [`version`]($version)), that specifies the currently active Typst compiler version. + +- name: sym + title: General + category: symbols + path: ["sym"] + details: | + Named general symbols. + + For example, `#sym.arrow` produces the → symbol. Within + [formulas]($category/math), these symbols can be used without the `#sym.` + prefix. + + The `d` in an integral's `dx` can be written as `[$dif x$]`. + Outside math formulas, `dif` can be accessed as `math.dif`. + +- name: emoji + title: Emoji + category: symbols + path: ["emoji"] + details: | + Named emoji. + + For example, `#emoji.face` produces the 😀 emoji. If you frequently use + certain emojis, you can also import them from the `emoji` module (`[#import + emoji: face]`) to use them without the `#emoji.` prefix. diff --git a/docs/reference/packages.md b/docs/reference/packages.md new file mode 100644 index 00000000..bfd1ef58 --- /dev/null +++ b/docs/reference/packages.md @@ -0,0 +1,6 @@ +Typst [packages]($scripting/#packages) encapsulate reusable building blocks +and make them reusable across projects. Below is a list of Typst packages +created by the community. Due to the early and experimental nature of Typst's +package management, they all live in a `preview` namespace. Click on a package's +name to view its documentation and use the copy button on the right to get a +full import statement for it. diff --git a/tests/Cargo.toml b/tests/Cargo.toml index ca8c1c0c..386b1073 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -8,7 +8,6 @@ publish = false [dev-dependencies] typst = { workspace = true } -typst-library = { workspace = true } typst-pdf = { workspace = true } typst-render = { workspace = true } typst-svg = { workspace = true } diff --git a/tests/src/benches.rs b/tests/src/benches.rs index 8ef17353..16122bd4 100644 --- a/tests/src/benches.rs +++ b/tests/src/benches.rs @@ -1,11 +1,12 @@ use comemo::{Prehashed, Track, Tracked}; use iai::{black_box, main, Iai}; use typst::diag::FileResult; -use typst::eval::{Bytes, Datetime, Library, Tracer}; -use typst::font::{Font, FontBook}; -use typst::geom::Color; +use typst::eval::Tracer; +use typst::foundations::{Bytes, Datetime}; use typst::syntax::{FileId, Source}; -use typst::World; +use typst::text::{Font, FontBook}; +use typst::visualize::Color; +use typst::{Library, World}; use unscanny::Scanner; const TEXT: &str = include_str!("../typ/compiler/bench.typ"); @@ -17,7 +18,6 @@ main!( bench_parse, bench_edit, bench_eval, - bench_layout, bench_compile, bench_render, ); @@ -65,21 +65,6 @@ fn bench_eval(iai: &mut Iai) { }); } -fn bench_layout(iai: &mut Iai) { - let world = BenchWorld::new(); - let route = typst::eval::Route::default(); - let mut tracer = typst::eval::Tracer::new(); - let module = typst::eval::eval( - world.track(), - route.track(), - tracer.track_mut(), - &world.source, - ) - .unwrap(); - let content = module.content(); - iai.run(|| typst::model::layout(world.track(), tracer.track_mut(), &content)); -} - fn bench_compile(iai: &mut Iai) { let world = BenchWorld::new(); let mut tracer = Tracer::new(); @@ -106,7 +91,7 @@ impl BenchWorld { let book = FontBook::from_fonts([&font]); Self { - library: Prehashed::new(typst_library::build()), + library: Prehashed::new(Library::build()), book: Prehashed::new(book), font, source: Source::detached(TEXT), diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 42663e6c..0a936a8d 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -2,13 +2,12 @@ use std::cell::{RefCell, RefMut}; use std::collections::{HashMap, HashSet}; -use std::env; use std::ffi::OsStr; use std::fmt::{self, Display, Formatter, Write as _}; -use std::fs; use std::io::{self, Write}; use std::ops::Range; use std::path::{Path, PathBuf}; +use std::{env, fs}; use clap::Parser; use comemo::{Prehashed, Track}; @@ -17,20 +16,20 @@ use oxipng::{InFile, Options, OutFile}; use rayon::iter::{ParallelBridge, ParallelIterator}; use std::cell::OnceCell; use tiny_skia as sk; -use unscanny::Scanner; -use walkdir::WalkDir; - use typst::diag::{bail, FileError, FileResult, Severity, StrResult}; -use typst::doc::{Document, Frame, FrameItem, Meta}; -use typst::eval::{ - eco_format, func, Bytes, Datetime, Library, NoneValue, Repr, Smart, Tracer, Value, +use typst::eval::Tracer; +use typst::foundations::{ + eco_format, func, Bytes, Datetime, NoneValue, Repr, Smart, Value, }; -use typst::font::{Font, FontBook}; -use typst::geom::{Abs, Color, Transform}; +use typst::introspection::Meta; +use typst::layout::{Abs, Frame, FrameItem, Margin, PageElem, Transform}; +use typst::model::Document; use typst::syntax::{FileId, PackageVersion, Source, SyntaxNode, VirtualPath}; -use typst::{World, WorldExt}; -use typst_library::layout::{Margin, PageElem}; -use typst_library::text::{TextElem, TextSize}; +use typst::text::{Font, FontBook, TextElem, TextSize}; +use typst::visualize::Color; +use typst::{Library, World, WorldExt}; +use unscanny::Scanner; +use walkdir::WalkDir; const TYP_DIR: &str = "typ"; const REF_DIR: &str = "ref"; @@ -183,11 +182,10 @@ fn library() -> Library { NoneValue } - let mut lib = typst_library::build(); - // Set page width to 120pt with 10pt margins, so that the inner page is // exactly 100pt wide. Page height is unbounded and font size is 10pt so // that it multiplies to nice round numbers. + let mut lib = Library::build(); lib.styles .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); lib.styles.set(PageElem::set_height(Smart::Auto)); |
