summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-11-23 16:25:49 +0100
committerLaurenz <laurmaedje@gmail.com>2023-11-24 12:30:02 +0100
commit7eebafa7837ec173a7b2064ae60fd45b5413d17c (patch)
treeb63b302b6d7747bcbb28571713745b9ca1aa83a4 /crates
parent76e173b78b511b506b928c27ac360f75fa455747 (diff)
Merge `typst` and `typst-library`
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-cli/Cargo.toml1
-rw-r--r--crates/typst-cli/src/args.rs3
-rw-r--r--crates/typst-cli/src/compile.rs8
-rw-r--r--crates/typst-cli/src/fonts.rs4
-rw-r--r--crates/typst-cli/src/query.rs9
-rw-r--r--crates/typst-cli/src/tracing.rs5
-rw-r--r--crates/typst-cli/src/update.rs5
-rw-r--r--crates/typst-cli/src/watch.rs2
-rw-r--r--crates/typst-cli/src/world.rs11
-rw-r--r--crates/typst-docs/Cargo.toml1
-rw-r--r--crates/typst-docs/src/contribs.rs2
-rw-r--r--crates/typst-docs/src/html.rs13
-rw-r--r--crates/typst-docs/src/lib.rs252
-rw-r--r--crates/typst-docs/src/link.rs30
-rw-r--r--crates/typst-docs/src/model.rs6
-rw-r--r--crates/typst-ide/src/analyze.rs17
-rw-r--r--crates/typst-ide/src/complete.rs26
-rw-r--r--crates/typst-ide/src/jump.rs7
-rw-r--r--crates/typst-ide/src/lib.rs4
-rw-r--r--crates/typst-ide/src/tooltip.rs21
-rw-r--r--crates/typst-library/Cargo.toml53
-rw-r--r--crates/typst-library/src/compute/data.rs609
-rw-r--r--crates/typst-library/src/compute/mod.rs20
-rw-r--r--crates/typst-library/src/layout/align.rs46
-rw-r--r--crates/typst-library/src/lib.rs170
-rw-r--r--crates/typst-library/src/meta/mod.rs76
-rw-r--r--crates/typst-library/src/prelude.rs42
-rw-r--r--crates/typst-library/src/shared/ext.rs92
-rw-r--r--crates/typst-library/src/shared/mod.rs7
-rw-r--r--crates/typst-library/src/symbols/mod.rs17
-rw-r--r--crates/typst-library/src/text/misc.rs315
-rw-r--r--crates/typst-library/src/visualize/shape.rs547
-rw-r--r--crates/typst-macros/src/cast.rs59
-rw-r--r--crates/typst-macros/src/category.rs57
-rw-r--r--crates/typst-macros/src/elem.rs234
-rw-r--r--crates/typst-macros/src/func.rs50
-rw-r--r--crates/typst-macros/src/lib.rs23
-rw-r--r--crates/typst-macros/src/scope.rs18
-rw-r--r--crates/typst-macros/src/symbols.rs11
-rw-r--r--crates/typst-macros/src/ty.rs30
-rw-r--r--crates/typst-macros/src/util.rs39
-rw-r--r--crates/typst-pdf/src/color.rs2
-rw-r--r--crates/typst-pdf/src/font.rs2
-rw-r--r--crates/typst-pdf/src/gradient.rs15
-rw-r--r--crates/typst-pdf/src/image.rs5
-rw-r--r--crates/typst-pdf/src/lib.rs12
-rw-r--r--crates/typst-pdf/src/outline.rs8
-rw-r--r--crates/typst-pdf/src/page.rs29
-rw-r--r--crates/typst-render/src/lib.rs32
-rw-r--r--crates/typst-svg/src/lib.rs32
-rw-r--r--crates/typst-syntax/src/reparser.rs2
-rw-r--r--crates/typst-syntax/src/span.rs4
-rw-r--r--crates/typst/Cargo.toml18
-rw-r--r--crates/typst/assets/cj_linebreak_data.postcard (renamed from crates/typst-library/assets/cj_linebreak_data.postcard)bin18848 -> 18848 bytes
-rw-r--r--crates/typst/assets/icudata.postcard (renamed from crates/typst-library/assets/icudata.postcard)bin352005 -> 352005 bytes
-rw-r--r--crates/typst/assets/syntect.bin (renamed from crates/typst-library/assets/syntect.bin)bin687378 -> 687378 bytes
-rw-r--r--crates/typst/src/diag.rs29
-rw-r--r--crates/typst/src/eval/access.rs99
-rw-r--r--crates/typst/src/eval/binding.rs179
-rw-r--r--crates/typst/src/eval/call.rs587
-rw-r--r--crates/typst/src/eval/code.rs317
-rw-r--r--crates/typst/src/eval/flow.rs227
-rw-r--r--crates/typst/src/eval/import.rs227
-rw-r--r--crates/typst/src/eval/library.rs179
-rw-r--r--crates/typst/src/eval/markup.rs272
-rw-r--r--crates/typst/src/eval/math.rs113
-rw-r--r--crates/typst/src/eval/mod.rs1988
-rw-r--r--crates/typst/src/eval/ops.rs145
-rw-r--r--crates/typst/src/eval/rules.rs51
-rw-r--r--crates/typst/src/eval/tracer.rs4
-rw-r--r--crates/typst/src/eval/vm.rs127
-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.rs22
-rw-r--r--crates/typst/src/geom/mod.rs124
-rw-r--r--crates/typst/src/geom/path.rs102
-rw-r--r--crates/typst/src/geom/rect.rs599
-rw-r--r--crates/typst/src/geom/shape.rs44
-rw-r--r--crates/typst/src/geom/transform.rs126
-rw-r--r--crates/typst/src/image/mod.rs175
-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.rs47
-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.rs117
-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.rs109
-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.rs256
-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.rs43
-rw-r--r--crates/typst/src/lib.rs246
-rw-r--r--crates/typst/src/loading/cbor.rs57
-rw-r--r--crates/typst/src/loading/csv.rs118
-rw-r--r--crates/typst/src/loading/json.rs94
-rw-r--r--crates/typst/src/loading/mod.rs82
-rw-r--r--crates/typst/src/loading/read.rs57
-rw-r--r--crates/typst/src/loading/toml.rs90
-rw-r--r--crates/typst/src/loading/xml.rs116
-rw-r--r--crates/typst/src/loading/yaml.rs78
-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.rs311
-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.rs41
-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.rs228
-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.rs180
-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.rs242
-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.rs48
-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.rs27
-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.rs79
-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.rs63
-rw-r--r--crates/typst/src/text/lang.rs182
-rw-r--r--crates/typst/src/text/linebreak.rs43
-rw-r--r--crates/typst/src/text/lorem.rs24
-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.rs32
-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.rs26
-rw-r--r--crates/typst/src/util/deferred.rs11
-rw-r--r--crates/typst/src/util/fat.rs55
-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.rs63
-rw-r--r--crates/typst/src/util/pico.rs3
-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.rs1221
-rw-r--r--crates/typst/src/visualize/stroke.rs (renamed from crates/typst/src/geom/stroke.rs)15
241 files changed, 9459 insertions, 8741 deletions
diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml
index 7ac4ee5e..3819469e 100644
--- a/crates/typst-cli/Cargo.toml
+++ b/crates/typst-cli/Cargo.toml
@@ -21,7 +21,6 @@ doc = false
[dependencies]
typst = { workspace = true }
-typst-library = { workspace = true }
typst-pdf = { workspace = true }
typst-render = { workspace = true }
typst-svg = { workspace = true }
diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs
index cfd1ae08..075412cd 100644
--- a/crates/typst-cli/src/args.rs
+++ b/crates/typst-cli/src/args.rs
@@ -1,9 +1,8 @@
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
-use semver::Version;
-
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
+use semver::Version;
/// The character typically used to separate path components
/// in environment variables.
diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs
index 48d6401c..73115924 100644
--- a/crates/typst-cli/src/compile.rs
+++ b/crates/typst-cli/src/compile.rs
@@ -4,12 +4,14 @@ use std::path::{Path, PathBuf};
use chrono::{Datelike, Timelike};
use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term::{self, termcolor};
+use ecow::eco_format;
use termcolor::{ColorChoice, StandardStream};
use typst::diag::{bail, Severity, SourceDiagnostic, StrResult};
-use typst::doc::Document;
-use typst::eval::{eco_format, Datetime, Tracer};
-use typst::geom::Color;
+use typst::eval::Tracer;
+use typst::foundations::Datetime;
+use typst::model::Document;
use typst::syntax::{FileId, Source, Span};
+use typst::visualize::Color;
use typst::{World, WorldExt};
use crate::args::{CompileCommand, DiagnosticFormat, OutputFormat};
diff --git a/crates/typst-cli/src/fonts.rs b/crates/typst-cli/src/fonts.rs
index 7c785716..f4711b82 100644
--- a/crates/typst-cli/src/fonts.rs
+++ b/crates/typst-cli/src/fonts.rs
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use fontdb::{Database, Source};
use typst::diag::StrResult;
-use typst::font::{Font, FontBook, FontInfo, FontVariant};
+use typst::text::{Font, FontBook, FontInfo, FontVariant};
use crate::args::FontsCommand;
@@ -106,7 +106,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) {
let mut process = |bytes: &'static [u8]| {
- let buffer = typst::eval::Bytes::from_static(bytes);
+ let buffer = typst::foundations::Bytes::from_static(bytes);
for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone());
self.fonts.push(FontSlot {
diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs
index cc9cfc23..a84cef79 100644
--- a/crates/typst-cli/src/query.rs
+++ b/crates/typst-cli/src/query.rs
@@ -1,10 +1,13 @@
use comemo::Track;
+use ecow::{eco_format, EcoString};
use serde::Serialize;
use typst::diag::{bail, StrResult};
use typst::eval::{eval_string, EvalMode, Tracer};
-use typst::model::Introspector;
+use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
+use typst::introspection::Introspector;
+use typst::model::Document;
+use typst::syntax::Span;
use typst::World;
-use typst_library::prelude::*;
use crate::args::{QueryCommand, SerializationFormat};
use crate::compile::print_diagnostics;
@@ -95,7 +98,7 @@ fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> {
.collect();
if command.one {
- let Some(value) = mapped.get(0) else {
+ let Some(value) = mapped.first() else {
bail!("no such field found for element");
};
serialize(value, command.format)
diff --git a/crates/typst-cli/src/tracing.rs b/crates/typst-cli/src/tracing.rs
index c01efd6d..331c6327 100644
--- a/crates/typst-cli/src/tracing.rs
+++ b/crates/typst-cli/src/tracing.rs
@@ -6,8 +6,9 @@ use inferno::flamegraph::Options;
use tracing::metadata::LevelFilter;
use tracing_error::ErrorLayer;
use tracing_flame::{FlameLayer, FlushGuard};
-use tracing_subscriber::fmt;
-use tracing_subscriber::prelude::*;
+use tracing_subscriber::layer::SubscriberExt;
+use tracing_subscriber::util::SubscriberInitExt;
+use tracing_subscriber::{fmt, Layer};
use crate::args::{CliArguments, Command};
diff --git a/crates/typst-cli/src/update.rs b/crates/typst-cli/src/update.rs
index c94515b6..3cd65c9d 100644
--- a/crates/typst-cli/src/update.rs
+++ b/crates/typst-cli/src/update.rs
@@ -1,13 +1,12 @@
-use std::env;
-use std::fs;
use std::io::{Cursor, Read, Write};
use std::path::PathBuf;
+use std::{env, fs};
+use ecow::eco_format;
use semver::Version;
use serde::Deserialize;
use tempfile::NamedTempFile;
use typst::diag::{bail, StrResult};
-use typst::eval::eco_format;
use xz2::bufread::XzDecoder;
use zip::ZipArchive;
diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs
index 138f473a..aea3ca48 100644
--- a/crates/typst-cli/src/watch.rs
+++ b/crates/typst-cli/src/watch.rs
@@ -3,11 +3,11 @@ use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use codespan_reporting::term::{self, termcolor};
+use ecow::eco_format;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use same_file::is_same_file;
use termcolor::WriteColor;
use typst::diag::StrResult;
-use typst::eval::eco_format;
use crate::args::CompileCommand;
use crate::color_stream;
diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs
index 3b774f1d..f375c648 100644
--- a/crates/typst-cli/src/world.rs
+++ b/crates/typst-cli/src/world.rs
@@ -5,13 +5,14 @@ use std::path::{Path, PathBuf};
use chrono::{DateTime, Datelike, Local};
use comemo::Prehashed;
+use ecow::eco_format;
use typst::diag::{FileError, FileResult, StrResult};
-use typst::doc::Frame;
-use typst::eval::{eco_format, Bytes, Datetime, Library};
-use typst::font::{Font, FontBook};
+use typst::foundations::{Bytes, Datetime};
+use typst::layout::Frame;
use typst::syntax::{FileId, Source, VirtualPath};
+use typst::text::{Font, FontBook};
use typst::util::hash128;
-use typst::World;
+use typst::{Library, World};
use crate::args::SharedArgs;
use crate::fonts::{FontSearcher, FontSlot};
@@ -75,7 +76,7 @@ impl SystemWorld {
input,
root,
main: FileId::new(None, main_path),
- library: Prehashed::new(typst_library::build()),
+ library: Prehashed::new(Library::build()),
book: Prehashed::new(searcher.book),
fonts: searcher.fonts,
slots: RefCell::default(),
diff --git a/crates/typst-docs/Cargo.toml b/crates/typst-docs/Cargo.toml
index b2e82e43..152f5e79 100644
--- a/crates/typst-docs/Cargo.toml
+++ b/crates/typst-docs/Cargo.toml
@@ -12,7 +12,6 @@ bench = false
[dependencies]
typst = { workspace = true }
-typst-library = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
heck = { workspace = true }
diff --git a/crates/typst-docs/src/contribs.rs b/crates/typst-docs/src/contribs.rs
index cbd87dc6..58a730e2 100644
--- a/crates/typst-docs/src/contribs.rs
+++ b/crates/typst-docs/src/contribs.rs
@@ -4,7 +4,7 @@ use std::fmt::Write;
use serde::{Deserialize, Serialize};
-use super::{Html, Resolver};
+use crate::{Html, Resolver};
/// Build HTML detailing the contributors between two tags.
pub fn contributors(resolver: &dyn Resolver, from: &str, to: &str) -> Option<Html> {
diff --git a/crates/typst-docs/src/html.rs b/crates/typst-docs/src/html.rs
index 1210545f..7481b050 100644
--- a/crates/typst-docs/src/html.rs
+++ b/crates/typst-docs/src/html.rs
@@ -8,11 +8,12 @@ use pulldown_cmark as md;
use serde::{Deserialize, Serialize};
use typed_arena::Arena;
use typst::diag::{FileResult, StrResult};
-use typst::eval::{Bytes, Datetime, Library, Tracer};
-use typst::font::{Font, FontBook};
-use typst::geom::{Abs, Point, Size};
+use typst::eval::Tracer;
+use typst::foundations::{Bytes, Datetime};
+use typst::layout::{Abs, Point, Size};
use typst::syntax::{FileId, Source, VirtualPath};
-use typst::World;
+use typst::text::{Font, FontBook};
+use typst::{Library, World};
use unscanny::Scanner;
use yaml_front_matter::YamlFrontMatter;
@@ -360,13 +361,13 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
buf.push_str("</pre>");
return Html::new(buf);
} else if !matches!(lang, "example" | "typ" | "preview") {
- let set = &*typst_library::text::SYNTAXES;
+ let set = &*typst::text::RAW_SYNTAXES;
let buf = syntect::html::highlighted_html_for_string(
&display,
set,
set.find_syntax_by_token(lang)
.unwrap_or_else(|| panic!("unsupported highlighting language: {lang}")),
- &typst_library::text::THEME,
+ &typst::text::RAW_THEME,
)
.expect("failed to highlight code");
return Html::new(buf);
diff --git a/crates/typst-docs/src/lib.rs b/crates/typst-docs/src/lib.rs
index d58734c6..444dda32 100644
--- a/crates/typst-docs/src/lib.rs
+++ b/crates/typst-docs/src/lib.rs
@@ -5,9 +5,9 @@ mod html;
mod link;
mod model;
-pub use contribs::{contributors, Author, Commit};
-pub use html::Html;
-pub use model::*;
+pub use self::contribs::*;
+pub use self::html::*;
+pub use self::model::*;
use std::path::Path;
@@ -20,30 +20,48 @@ use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_yaml as yaml;
use typst::diag::{bail, StrResult};
-use typst::doc::Frame;
-use typst::eval::{
- CastInfo, Func, Library, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
+use typst::foundations::{
+ CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
+ FOUNDATIONS,
};
-use typst::font::{Font, FontBook};
-use typst::geom::Abs;
-use typst_library::layout::{Margin, PageElem};
+use typst::introspection::INTROSPECTION;
+use typst::layout::{Abs, Frame, Margin, PageElem, LAYOUT};
+use typst::loading::DATA_LOADING;
+use typst::math::MATH;
+use typst::model::MODEL;
+use typst::symbols::SYMBOLS;
+use typst::text::{Font, FontBook, TEXT};
+use typst::visualize::VISUALIZE;
+use typst::Library;
static DOCS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs");
static FILE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/files");
static FONT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/fonts");
-static CATEGORIES: Lazy<yaml::Mapping> = Lazy::new(|| yaml("reference/categories.yml"));
-static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| yaml("reference/groups.yml"));
+static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| {
+ let mut groups: Vec<GroupData> = yaml("reference/groups.yml");
+ for group in &mut groups {
+ if group.filter.is_empty() {
+ group.filter = group
+ .module()
+ .scope()
+ .iter()
+ .filter(|(_, v)| matches!(v, Value::Func(_)))
+ .map(|(k, _)| k.clone())
+ .collect();
+ }
+ }
+ groups
+});
static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
- let mut lib = typst_library::build();
+ let mut lib = Library::build();
lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto));
lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
Abs::pt(15.0).into(),
)))));
- typst::eval::set_lang_items(lib.items.clone());
Prehashed::new(lib)
});
@@ -128,14 +146,15 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel {
.with_part("Language"),
markdown_page(resolver, "/docs/reference/", "reference/styling.md"),
markdown_page(resolver, "/docs/reference/", "reference/scripting.md"),
- category_page(resolver, "foundations").with_part("Library"),
- category_page(resolver, "text"),
- category_page(resolver, "math"),
- category_page(resolver, "layout"),
- category_page(resolver, "visualize"),
- category_page(resolver, "meta"),
- category_page(resolver, "symbols"),
- category_page(resolver, "data-loading"),
+ category_page(resolver, FOUNDATIONS).with_part("Library"),
+ category_page(resolver, MODEL),
+ category_page(resolver, TEXT),
+ category_page(resolver, MATH),
+ category_page(resolver, SYMBOLS),
+ category_page(resolver, LAYOUT),
+ category_page(resolver, VISUALIZE),
+ category_page(resolver, INTROSPECTION),
+ category_page(resolver, DATA_LOADING),
];
page
}
@@ -152,50 +171,73 @@ fn guide_pages(resolver: &dyn Resolver) -> PageModel {
/// Build the packages section.
fn packages_page(resolver: &dyn Resolver) -> PageModel {
+ let md = DOCS_DIR
+ .get_file("reference/packages.md")
+ .unwrap()
+ .contents_utf8()
+ .unwrap();
PageModel {
route: "/docs/packages/".into(),
title: "Packages".into(),
description: "Packages for Typst.".into(),
part: None,
outline: vec![],
- body: BodyModel::Packages(Html::markdown(
- resolver,
- category_details("packages"),
- Some(1),
- )),
+ body: BodyModel::Packages(Html::markdown(resolver, md, Some(1))),
children: vec![],
}
}
/// Create a page for a category.
#[track_caller]
-fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel {
- let route = eco_format!("/docs/reference/{category}/");
+fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
+ let route = eco_format!("/docs/reference/{}/", category.name());
let mut children = vec![];
let mut items = vec![];
+ let mut shorthands = None;
+ let mut markup = vec![];
+ let mut math = vec![];
- let (module, path): (&Module, &[&str]) = match category {
- "math" => (&LIBRARY.math, &["math"]),
- _ => (&LIBRARY.global, &[]),
+ let (module, path): (&Module, &[&str]) = if category == MATH {
+ (&LIBRARY.math, &["math"])
+ } else {
+ (&LIBRARY.global, &[])
};
// Add groups.
- for mut group in GROUPS.iter().filter(|g| g.category == category).cloned() {
- let mut focus = module;
- if matches!(group.name.as_str(), "calc" | "sys") {
- focus = get_module(focus, &group.name).unwrap();
- group.functions = focus
- .scope()
- .iter()
- .filter(|(_, v)| matches!(v, Value::Func(_)))
- .map(|(k, _)| k.clone())
- .collect();
+ for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() {
+ if matches!(group.name.as_str(), "sym" | "emoji") {
+ let subpage = symbols_page(resolver, &route, &group);
+ let BodyModel::Symbols(model) = &subpage.body else { continue };
+ let list = &model.list;
+ markup.extend(
+ list.iter()
+ .filter(|symbol| symbol.markup_shorthand.is_some())
+ .cloned(),
+ );
+ math.extend(
+ list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(),
+ );
+
+ items.push(CategoryItem {
+ name: group.name.clone(),
+ route: subpage.route.clone(),
+ oneliner: oneliner(category.docs()).into(),
+ code: true,
+ });
+ children.push(subpage);
+ continue;
}
- let (child, item) = group_page(resolver, &route, &group, focus.scope());
+
+ let (child, item) = group_page(resolver, &route, &group);
children.push(child);
items.push(item);
}
+ // Add symbol pages. These are ordered manually.
+ if category == SYMBOLS {
+ shorthands = Some(ShorthandsModel { markup, math });
+ }
+
// Add functions.
let scope = module.scope();
for (name, value) in scope.iter() {
@@ -203,9 +245,9 @@ fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel {
continue;
}
- if category == "math" {
+ if category == MATH {
// Skip grouped functions.
- if GROUPS.iter().flat_map(|group| &group.functions).any(|f| f == name) {
+ if GROUPS.iter().flat_map(|group| &group.filter).any(|f| f == name) {
continue;
}
@@ -242,54 +284,35 @@ fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel {
}
}
- children.sort_by_cached_key(|child| child.title.clone());
- items.sort_by_cached_key(|item| item.name.clone());
-
- // Add symbol pages. These are ordered manually.
- let mut shorthands = None;
- if category == "symbols" {
- let mut markup = vec![];
- let mut math = vec![];
- for module in ["sym", "emoji"] {
- let subpage = symbols_page(resolver, &route, module);
- let BodyModel::Symbols(model) = &subpage.body else { continue };
- let list = &model.list;
- markup.extend(
- list.iter()
- .filter(|symbol| symbol.markup_shorthand.is_some())
- .cloned(),
- );
- math.extend(
- list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(),
- );
-
- items.push(CategoryItem {
- name: module.into(),
- route: subpage.route.clone(),
- oneliner: oneliner(category_details(module)).into(),
- code: true,
- });
- children.push(subpage);
- }
- shorthands = Some(ShorthandsModel { markup, math });
+ if category != SYMBOLS {
+ children.sort_by_cached_key(|child| child.title.clone());
+ items.sort_by_cached_key(|item| item.name.clone());
}
- let name: EcoString = category.to_title_case().into();
-
- let details = Html::markdown(resolver, category_details(category), Some(1));
+ let name = category.title();
+ let details = Html::markdown(resolver, category.docs(), Some(1));
let mut outline = vec![OutlineItem::from_name("Summary")];
outline.extend(details.outline());
outline.push(OutlineItem::from_name("Definitions"));
+ if shorthands.is_some() {
+ outline.push(OutlineItem::from_name("Shorthands"));
+ }
PageModel {
route,
- title: name.clone(),
+ title: name.into(),
description: eco_format!(
"Documentation for functions related to {name} in Typst."
),
part: None,
outline,
- body: BodyModel::Category(CategoryModel { name, details, items, shorthands }),
+ body: BodyModel::Category(CategoryModel {
+ name: category.name(),
+ title: category.title(),
+ details,
+ items,
+ shorthands,
+ }),
children,
}
}
@@ -498,18 +521,17 @@ fn group_page(
resolver: &dyn Resolver,
parent: &str,
group: &GroupData,
- scope: &Scope,
) -> (PageModel, CategoryItem) {
let mut functions = vec![];
let mut outline = vec![OutlineItem::from_name("Summary")];
let path: Vec<_> = group.path.iter().map(|s| s.as_str()).collect();
- let details = Html::markdown(resolver, &group.description, Some(1));
+ let details = Html::markdown(resolver, &group.details, Some(1));
outline.extend(details.outline());
let mut outline_items = vec![];
- for name in &group.functions {
- let value = scope.get(name).unwrap();
+ for name in &group.filter {
+ let value = group.module().scope().get(name).unwrap();
let Value::Func(func) = value else { panic!("not a function") };
let func = func_model(resolver, func, &path, true);
let id_base = urlify(&eco_format!("functions-{}", func.name));
@@ -530,13 +552,13 @@ fn group_page(
let model = PageModel {
route: eco_format!("{parent}{}", group.name),
- title: group.display.clone(),
+ title: group.title.clone(),
description: eco_format!("Documentation for the {} functions.", group.name),
part: None,
outline,
body: BodyModel::Group(GroupModel {
name: group.name.clone(),
- title: group.display.clone(),
+ title: group.title.clone(),
details,
functions,
}),
@@ -546,7 +568,7 @@ fn group_page(
let item = CategoryItem {
name: group.name.clone(),
route: model.route.clone(),
- oneliner: oneliner(&group.description).into(),
+ oneliner: oneliner(&group.details).into(),
code: false,
};
@@ -601,19 +623,12 @@ fn type_outline(model: &TypeModel) -> Vec<OutlineItem> {
}
/// Create a page for symbols.
-fn symbols_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel {
- let module = get_module(&LIBRARY.global, name).unwrap();
- let title = match name {
- "sym" => "General",
- "emoji" => "Emoji",
- _ => unreachable!(),
- };
-
- let model = symbols_model(resolver, name, title, module.scope());
+fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> PageModel {
+ let model = symbols_model(resolver, group);
PageModel {
- route: eco_format!("{parent}{name}/"),
- title: title.into(),
- description: eco_format!("Documentation for the `{name}` module."),
+ route: eco_format!("{parent}{}/", group.name),
+ title: group.title.clone(),
+ description: eco_format!("Documentation for the `{}` module.", group.name),
part: None,
outline: vec![],
body: BodyModel::Symbols(model),
@@ -622,14 +637,9 @@ fn symbols_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel
}
/// Produce a symbol list's model.
-fn symbols_model(
- resolver: &dyn Resolver,
- name: &str,
- title: &'static str,
- scope: &Scope,
-) -> SymbolsModel {
+fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
let mut list = vec![];
- for (name, value) in scope.iter() {
+ for (name, value) in group.module().scope().iter() {
let Value::Symbol(symbol) = value else { continue };
let complete = |variant: &str| {
if variant.is_empty() {
@@ -649,7 +659,7 @@ fn symbols_model(
markup_shorthand: shorthand(typst::syntax::ast::Shorthand::MARKUP_LIST),
math_shorthand: shorthand(typst::syntax::ast::Shorthand::MATH_LIST),
codepoint: c as u32,
- accent: typst::eval::Symbol::combining_accent(c).is_some(),
+ accent: typst::symbols::Symbol::combining_accent(c).is_some(),
unicode_name: unicode_names2::name(c)
.map(|s| s.to_string().to_title_case().into()),
alternates: symbol
@@ -662,8 +672,9 @@ fn symbols_model(
}
SymbolsModel {
- name: title,
- details: Html::markdown(resolver, category_details(name), Some(1)),
+ name: group.name.clone(),
+ title: group.title.clone(),
+ details: Html::markdown(resolver, &group.details, Some(1)),
list,
}
}
@@ -684,15 +695,6 @@ fn yaml<T: DeserializeOwned>(path: &str) -> T {
yaml::from_slice(file.contents()).unwrap()
}
-/// Load details for an identifying key.
-#[track_caller]
-fn category_details(key: &str) -> &str {
- CATEGORIES
- .get(&yaml::Value::String(key.into()))
- .and_then(|value| value.as_str())
- .unwrap_or_else(|| panic!("missing details for {key}"))
-}
-
/// Turn a title into an URL fragment.
pub fn urlify(title: &str) -> EcoString {
title
@@ -752,13 +754,23 @@ const TYPE_ORDER: &[&str] = &[
#[derive(Debug, Clone, Deserialize)]
struct GroupData {
name: EcoString,
+ title: EcoString,
category: EcoString,
- display: EcoString,
#[serde(default)]
path: Vec<EcoString>,
#[serde(default)]
- functions: Vec<EcoString>,
- description: EcoString,
+ filter: Vec<EcoString>,
+ details: EcoString,
+}
+
+impl GroupData {
+ fn module(&self) -> &'static Module {
+ let mut focus = &LIBRARY.global;
+ for path in &self.path {
+ focus = get_module(focus, path).unwrap();
+ }
+ focus
+ }
}
#[cfg(test)]
diff --git a/crates/typst-docs/src/link.rs b/crates/typst-docs/src/link.rs
index 38730c32..e2721b95 100644
--- a/crates/typst-docs/src/link.rs
+++ b/crates/typst-docs/src/link.rs
@@ -1,5 +1,5 @@
use typst::diag::{bail, StrResult};
-use typst::eval::Func;
+use typst::foundations::Func;
use crate::{get_module, GROUPS, LIBRARY};
@@ -55,6 +55,15 @@ fn resolve_known(head: &str) -> Option<&'static str> {
fn resolve_definition(head: &str) -> StrResult<String> {
let mut parts = head.trim_start_matches('$').split('.').peekable();
let mut focus = &LIBRARY.global;
+
+ let Some(name) = parts.peek() else {
+ bail!("missing first link component");
+ };
+
+ let Some(category) = focus.scope().get_category(name) else {
+ bail!("{name} has no category");
+ };
+
while let Some(m) = parts.peek().and_then(|&name| get_module(focus, name).ok()) {
focus = m;
parts.next();
@@ -62,18 +71,15 @@ fn resolve_definition(head: &str) -> StrResult<String> {
let name = parts.next().ok_or("link is missing first part")?;
let value = focus.field(name)?;
- let Some(category) = focus.scope().get_category(name) else {
- bail!("{name} has no category");
- };
// Handle grouped functions.
- if let Some(group) = GROUPS
- .iter()
- .filter(|_| category == "math")
- .find(|group| group.functions.iter().any(|func| func == name))
- {
- let mut route =
- format!("/docs/reference/math/{}/#functions-{}", group.name, name);
+ if let Some(group) = GROUPS.iter().find(|group| {
+ group.category == category.name() && group.filter.iter().any(|func| func == name)
+ }) {
+ let mut route = format!(
+ "/docs/reference/{}/{}/#functions-{}",
+ group.category, group.name, name
+ );
if let Some(param) = parts.next() {
route.push('-');
route.push_str(param);
@@ -81,7 +87,7 @@ fn resolve_definition(head: &str) -> StrResult<String> {
return Ok(route);
}
- let mut route = format!("/docs/reference/{category}/{name}/");
+ let mut route = format!("/docs/reference/{}/{name}/", category.name());
if let Some(next) = parts.next() {
if value.field(next).is_ok() {
route.push_str("#definitions-");
diff --git a/crates/typst-docs/src/model.rs b/crates/typst-docs/src/model.rs
index 580ae0d3..93742825 100644
--- a/crates/typst-docs/src/model.rs
+++ b/crates/typst-docs/src/model.rs
@@ -62,7 +62,8 @@ pub enum BodyModel {
/// Details about a category.
#[derive(Debug, Serialize)]
pub struct CategoryModel {
- pub name: EcoString,
+ pub name: &'static str,
+ pub title: &'static str,
pub details: Html,
pub items: Vec<CategoryItem>,
pub shorthands: Option<ShorthandsModel>,
@@ -144,7 +145,8 @@ pub struct TypeModel {
/// A collection of symbols.
#[derive(Debug, Serialize)]
pub struct SymbolsModel {
- pub name: &'static str,
+ pub name: EcoString,
+ pub title: EcoString,
pub details: Html,
pub list: Vec<SymbolModel>,
}
diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs
index 4d12e1c5..2d48039b 100644
--- a/crates/typst-ide/src/analyze.rs
+++ b/crates/typst-ide/src/analyze.rs
@@ -1,8 +1,11 @@
use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec};
-use typst::doc::Frame;
-use typst::eval::{Route, Scopes, Tracer, Value, Vm};
-use typst::model::{DelayedErrors, Introspector, Label, Locator, Vt};
+use typst::diag::DelayedErrors;
+use typst::eval::{Route, Tracer, Vm};
+use typst::foundations::{Label, Scopes, Value};
+use typst::introspection::{Introspector, Locator};
+use typst::layout::{Frame, Vt};
+use typst::model::BibliographyElem;
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use typst::World;
@@ -75,13 +78,9 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
/// - All labels and descriptions for them, if available
/// - A split offset: All labels before this offset belong to nodes, all after
/// belong to a bibliography.
-pub fn analyze_labels(
- world: &dyn World,
- frames: &[Frame],
-) -> (Vec<(Label, Option<EcoString>)>, usize) {
+pub fn analyze_labels(frames: &[Frame]) -> (Vec<(Label, Option<EcoString>)>, usize) {
let mut output = vec![];
let introspector = Introspector::new(frames);
- let items = &world.library().items;
// Labels in the document.
for elem in introspector.all() {
@@ -102,7 +101,7 @@ pub fn analyze_labels(
let split = output.len();
// Bibliography keys.
- for (key, detail) in (items.bibliography_keys)(introspector.track()) {
+ for (key, detail) in BibliographyElem::keys(introspector.track()) {
output.push((Label::new(&key), detail));
}
diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs
index 71ae95db..5cff5a81 100644
--- a/crates/typst-ide/src/complete.rs
+++ b/crates/typst-ide/src/complete.rs
@@ -4,21 +4,21 @@ use std::collections::{BTreeSet, HashSet};
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use serde::{Deserialize, Serialize};
-use typst::doc::Frame;
-use typst::eval::{
- format_str, repr, AutoValue, CastInfo, Func, Library, NoneValue, Repr, Scope, Type,
- Value,
+use typst::foundations::{
+ fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label,
+ NoneValue, Repr, Scope, Type, Value,
};
-use typst::geom::Color;
-use typst::model::Label;
+use typst::layout::Frame;
use typst::syntax::{
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind,
};
+use typst::text::RawElem;
+use typst::visualize::Color;
use typst::World;
use unscanny::Scanner;
-use crate::analyze::analyze_labels;
-use crate::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family};
+use crate::analyze::{analyze_expr, analyze_import, analyze_labels};
+use crate::{plain_docs_sentence, summarize_font_family};
/// Autocomplete a cursor position in a source file.
///
@@ -367,7 +367,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
}
}
- for &(method, args) in typst::eval::mutable_methods_on(value.ty()) {
+ for &(method, args) in mutable_methods_on(value.ty()) {
ctx.completions.push(Completion {
kind: CompletionKind::Func,
label: method.into(),
@@ -380,7 +380,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
})
}
- for &field in typst::eval::fields_on(value.ty()) {
+ for &field in fields_on(value.ty()) {
// Complete the field name along with its value. Notes:
// 1. No parentheses since function fields cannot currently be called
// with method syntax;
@@ -967,7 +967,6 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
struct CompletionContext<'a> {
world: &'a (dyn World + 'a),
frames: &'a [Frame],
- library: &'a Library,
global: &'a Scope,
math: &'a Scope,
text: &'a str,
@@ -996,7 +995,6 @@ impl<'a> CompletionContext<'a> {
Some(Self {
world,
frames,
- library,
global: library.global.scope(),
math: library.math.scope(),
text,
@@ -1074,7 +1072,7 @@ impl<'a> CompletionContext<'a> {
/// Add completions for raw block tags.
fn raw_completions(&mut self) {
- for (name, mut tags) in (self.library.items.raw_languages)() {
+ for (name, mut tags) in RawElem::languages() {
let lower = name.to_lowercase();
if !tags.contains(&lower.as_str()) {
tags.push(lower.as_str());
@@ -1096,7 +1094,7 @@ impl<'a> CompletionContext<'a> {
/// Add completions for labels and references.
fn label_completions(&mut self) {
- let (labels, split) = analyze_labels(self.world, self.frames);
+ let (labels, split) = analyze_labels(self.frames);
let head = &self.text[..self.from];
let at = head.ends_with('@');
diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs
index a33e743c..700f475f 100644
--- a/crates/typst-ide/src/jump.rs
+++ b/crates/typst-ide/src/jump.rs
@@ -1,10 +1,11 @@
use std::num::NonZeroUsize;
use ecow::EcoString;
-use typst::doc::{Destination, Frame, FrameItem, Meta, Position};
-use typst::geom::{Geometry, Point, Size};
-use typst::model::Introspector;
+use typst::introspection::{Introspector, Meta};
+use typst::layout::{Frame, FrameItem, Point, Position, Size};
+use typst::model::Destination;
use typst::syntax::{FileId, LinkedNode, Source, Span, SyntaxKind};
+use typst::visualize::Geometry;
use typst::World;
/// Where to [jump](jump_from_click) to.
diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs
index 3ab367dc..173a4264 100644
--- a/crates/typst-ide/src/lib.rs
+++ b/crates/typst-ide/src/lib.rs
@@ -13,9 +13,7 @@ pub use self::tooltip::{tooltip, Tooltip};
use std::fmt::Write;
use ecow::{eco_format, EcoString};
-use typst::font::{FontInfo, FontStyle};
-
-use self::analyze::*;
+use typst::text::{FontInfo, FontStyle};
/// Extract the first sentence of plain text of a piece of documentation.
///
diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs
index d3f040e7..4f079166 100644
--- a/crates/typst-ide/src/tooltip.rs
+++ b/crates/typst-ide/src/tooltip.rs
@@ -2,14 +2,15 @@ use std::fmt::Write;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
-use typst::doc::Frame;
-use typst::eval::{repr, CapturesVisitor, CastInfo, Repr, Tracer, Value};
-use typst::geom::{round_2, Length, Numeric};
+use typst::eval::{CapturesVisitor, Tracer};
+use typst::foundations::{repr, CastInfo, Repr, Value};
+use typst::layout::{Frame, Length};
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
+use typst::util::{round_2, Numeric};
use typst::World;
-use crate::analyze::analyze_labels;
-use crate::{analyze_expr, plain_docs_sentence, summarize_font_family};
+use crate::analyze::{analyze_expr, analyze_labels};
+use crate::{plain_docs_sentence, summarize_font_family};
/// Describe the item under the cursor.
pub fn tooltip(
@@ -25,7 +26,7 @@ pub fn tooltip(
named_param_tooltip(world, &leaf)
.or_else(|| font_tooltip(world, &leaf))
- .or_else(|| label_tooltip(world, frames, &leaf))
+ .or_else(|| label_tooltip(frames, &leaf))
.or_else(|| expr_tooltip(world, &leaf))
.or_else(|| closure_tooltip(&leaf))
}
@@ -144,18 +145,14 @@ fn length_tooltip(length: Length) -> Option<Tooltip> {
}
/// Tooltip for a hovered reference or label.
-fn label_tooltip(
- world: &dyn World,
- frames: &[Frame],
- leaf: &LinkedNode,
-) -> Option<Tooltip> {
+fn label_tooltip(frames: &[Frame], leaf: &LinkedNode) -> Option<Tooltip> {
let target = match leaf.kind() {
SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'),
SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'),
_ => return None,
};
- for (label, detail) in analyze_labels(world, frames).0 {
+ for (label, detail) in analyze_labels(frames).0 {
if label.as_str() == target {
return Some(Tooltip::Text(detail?));
}
diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml
deleted file mode 100644
index 426c56f3..00000000
--- a/crates/typst-library/Cargo.toml
+++ /dev/null
@@ -1,53 +0,0 @@
-[package]
-name = "typst-library"
-description = "The standard library for Typst."
-version.workspace = true
-rust-version.workspace = true
-authors.workspace = true
-edition.workspace = true
-homepage.workspace = true
-repository.workspace = true
-license.workspace = true
-categories.workspace = true
-keywords.workspace = true
-
-[lib]
-test = false
-doctest = false
-bench = false
-
-[dependencies]
-typst = { workspace = true }
-az = { workspace = true }
-chinese-number = { workspace = true }
-ciborium = { workspace = true }
-comemo = { workspace = true }
-csv = { workspace = true }
-ecow = { workspace = true }
-hayagriva = { workspace = true }
-hypher = { workspace = true }
-icu_properties = { workspace = true }
-icu_provider = { workspace = true }
-icu_provider_adapters = { workspace = true }
-icu_provider_blob = { workspace = true }
-icu_segmenter = { workspace = true }
-indexmap = { workspace = true }
-kurbo = { workspace = true }
-lipsum = { workspace = true }
-log = { workspace = true }
-once_cell = { workspace = true }
-roxmltree = { workspace = true }
-rustybuzz = { workspace = true }
-serde_json = { workspace = true }
-serde_yaml = { workspace = true }
-smallvec = { workspace = true }
-syntect = { workspace = true }
-time = { workspace = true }
-toml = { workspace = true, features = ["display"] }
-tracing = { workspace = true }
-ttf-parser = { workspace = true }
-typed-arena = { workspace = true }
-unicode-bidi = { workspace = true }
-unicode-math-class = { workspace = true }
-unicode-script = { workspace = true }
-unicode-segmentation = { workspace = true }
diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs
deleted file mode 100644
index e4767ebf..00000000
--- a/crates/typst-library/src/compute/data.rs
+++ /dev/null
@@ -1,609 +0,0 @@
-use typst::diag::{format_xml_like_error, FileError};
-use typst::eval::Bytes;
-use typst::syntax::is_newline;
-
-use crate::prelude::*;
-
-/// Hook up all data loading definitions.
-pub(super) fn define(global: &mut Scope) {
- global.category("data-loading");
- global.define_func::<read>();
- global.define_func::<csv>();
- global.define_func::<json>();
- global.define_func::<toml>();
- global.define_func::<yaml>();
- global.define_func::<cbor>();
- global.define_func::<xml>();
-}
-
-/// Reads plain text or data from a file.
-///
-/// By default, the file will be read as UTF-8 and returned as a [string]($str).
-///
-/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead.
-///
-/// # Example
-/// ```example
-/// An example for a HTML file: \
-/// #let text = read("data.html")
-/// #raw(text, lang: "html")
-///
-/// Raw bytes:
-/// #read("tiger.jpg", encoding: none)
-/// ```
-#[func]
-pub fn read(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to a file.
- path: Spanned<EcoString>,
- /// The encoding to read the file with.
- ///
- /// If set to `{none}`, this function returns raw bytes.
- #[named]
- #[default(Some(Encoding::Utf8))]
- encoding: Option<Encoding>,
-) -> SourceResult<Readable> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- Ok(match encoding {
- None => Readable::Bytes(data),
- Some(Encoding::Utf8) => Readable::Str(
- std::str::from_utf8(&data)
- .map_err(|_| "file is not valid utf-8")
- .at(span)?
- .into(),
- ),
- })
-}
-
-/// An encoding of a file.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Encoding {
- /// The Unicode UTF-8 encoding.
- Utf8,
-}
-
-/// A value that can be read from a file.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Readable {
- /// A decoded string.
- Str(Str),
- /// Raw bytes.
- Bytes(Bytes),
-}
-
-impl Readable {
- fn as_slice(&self) -> &[u8] {
- match self {
- Readable::Bytes(v) => v,
- Readable::Str(v) => v.as_bytes(),
- }
- }
-}
-
-cast! {
- Readable,
- self => match self {
- Self::Str(v) => v.into_value(),
- Self::Bytes(v) => v.into_value(),
- },
- v: Str => Self::Str(v),
- v: Bytes => Self::Bytes(v),
-}
-
-impl From<Readable> for Bytes {
- fn from(value: Readable) -> Self {
- match value {
- Readable::Bytes(v) => v,
- Readable::Str(v) => v.as_bytes().into(),
- }
- }
-}
-
-/// Reads structured data from a CSV file.
-///
-/// The CSV file will be read and parsed into a 2-dimensional array of strings:
-/// Each row in the CSV file will be represented as an array of strings, and all
-/// rows will be collected into a single array. Header rows will not be
-/// stripped.
-///
-/// # Example
-/// ```example
-/// #let results = csv("data.csv")
-///
-/// #table(
-/// columns: 2,
-/// [*Condition*], [*Result*],
-/// ..results.flatten(),
-/// )
-/// ```
-#[func(scope, title = "CSV")]
-pub fn csv(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to a CSV file.
- path: Spanned<EcoString>,
- /// The delimiter that separates columns in the CSV file.
- /// Must be a single ASCII character.
- #[named]
- #[default]
- delimiter: Delimiter,
-) -> SourceResult<Array> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter)
-}
-
-#[scope]
-impl csv {
- /// Reads structured data from a CSV string/bytes.
- #[func(title = "Decode CSV")]
- pub fn decode(
- /// CSV data.
- data: Spanned<Readable>,
- /// The delimiter that separates columns in the CSV file.
- /// Must be a single ASCII character.
- #[named]
- #[default]
- delimiter: Delimiter,
- ) -> SourceResult<Array> {
- let Spanned { v: data, span } = data;
- let mut builder = ::csv::ReaderBuilder::new();
- builder.has_headers(false);
- builder.delimiter(delimiter.0 as u8);
- let mut reader = builder.from_reader(data.as_slice());
- let mut array = Array::new();
-
- for (line, result) in reader.records().enumerate() {
- // Original solution use line from error, but that is incorrect with
- // `has_headers` set to `false`. See issue:
- // https://github.com/BurntSushi/rust-csv/issues/184
- let line = line + 1; // Counting lines from 1
- let row = result.map_err(|err| format_csv_error(err, line)).at(span)?;
- let sub = row.into_iter().map(|field| field.into_value()).collect();
- array.push(Value::Array(sub))
- }
-
- Ok(array)
- }
-}
-
-/// The delimiter to use when parsing CSV files.
-pub struct Delimiter(char);
-
-impl Default for Delimiter {
- fn default() -> Self {
- Self(',')
- }
-}
-
-cast! {
- Delimiter,
- self => self.0.into_value(),
- v: EcoString => {
- let mut chars = v.chars();
- let first = chars.next().ok_or("delimiter must not be empty")?;
- if chars.next().is_some() {
- bail!("delimiter must be a single character");
- }
-
- if !first.is_ascii() {
- bail!("delimiter must be an ASCII character");
- }
-
- Self(first)
- },
-}
-
-/// Format the user-facing CSV error message.
-fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString {
- match err.kind() {
- ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
- ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => {
- eco_format!(
- "failed to parse CSV (found {len} instead of \
- {expected_len} fields in line {line})"
- )
- }
- _ => eco_format!("failed to parse CSV ({err})"),
- }
-}
-
-/// Reads structured data from a JSON file.
-///
-/// The file must contain a valid JSON object or array. JSON objects will be
-/// converted into Typst dictionaries, and JSON arrays will be converted into
-/// Typst arrays. Strings and booleans will be converted into the Typst
-/// equivalents, `null` will be converted into `{none}`, and numbers will be
-/// converted to floats or integers depending on whether they are whole numbers.
-///
-/// The function returns a dictionary or an array, depending on the JSON file.
-///
-/// The JSON files in the example contain objects with the keys `temperature`,
-/// `unit`, and `weather`.
-///
-/// # Example
-/// ```example
-/// #let forecast(day) = block[
-/// #box(square(
-/// width: 2cm,
-/// inset: 8pt,
-/// fill: if day.weather == "sunny" {
-/// yellow
-/// } else {
-/// aqua
-/// },
-/// align(
-/// bottom + right,
-/// strong(day.weather),
-/// ),
-/// ))
-/// #h(6pt)
-/// #set text(22pt, baseline: -8pt)
-/// #day.temperature °#day.unit
-/// ]
-///
-/// #forecast(json("monday.json"))
-/// #forecast(json("tuesday.json"))
-/// ```
-#[func(scope, title = "JSON")]
-pub fn json(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to a JSON file.
- path: Spanned<EcoString>,
-) -> SourceResult<Value> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- json::decode(Spanned::new(Readable::Bytes(data), span))
-}
-
-#[scope]
-impl json {
- /// Reads structured data from a JSON string/bytes.
- #[func(title = "Decode JSON")]
- pub fn decode(
- /// JSON data.
- data: Spanned<Readable>,
- ) -> SourceResult<Value> {
- let Spanned { v: data, span } = data;
- serde_json::from_slice(data.as_slice())
- .map_err(|err| eco_format!("failed to parse JSON ({err})"))
- .at(span)
- }
-
- /// Encodes structured data into a JSON string.
- #[func(title = "Encode JSON")]
- pub fn encode(
- /// Value to be encoded.
- value: Spanned<Value>,
- /// Whether to pretty print the JSON with newlines and indentation.
- #[named]
- #[default(true)]
- pretty: bool,
- ) -> SourceResult<Str> {
- let Spanned { v: value, span } = value;
- if pretty {
- serde_json::to_string_pretty(&value)
- } else {
- serde_json::to_string(&value)
- }
- .map(|v| v.into())
- .map_err(|err| eco_format!("failed to encode value as JSON ({err})"))
- .at(span)
- }
-}
-
-/// Reads structured data from a TOML file.
-///
-/// The file must contain a valid TOML table. TOML tables will be converted into
-/// Typst dictionaries, and TOML arrays will be converted into Typst arrays.
-/// Strings, booleans and datetimes will be converted into the Typst equivalents
-/// and numbers will be converted to floats or integers depending on whether
-/// they are whole numbers.
-///
-/// The TOML file in the example consists of a table with the keys `title`,
-/// `version`, and `authors`.
-///
-/// # Example
-/// ```example
-/// #let details = toml("details.toml")
-///
-/// Title: #details.title \
-/// Version: #details.version \
-/// Authors: #(details.authors
-/// .join(", ", last: " and "))
-/// ```
-#[func(scope, title = "TOML")]
-pub fn toml(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to a TOML file.
- path: Spanned<EcoString>,
-) -> SourceResult<Value> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- toml::decode(Spanned::new(Readable::Bytes(data), span))
-}
-
-#[scope]
-impl toml {
- /// Reads structured data from a TOML string/bytes.
- #[func(title = "Decode TOML")]
- pub fn decode(
- /// TOML data.
- data: Spanned<Readable>,
- ) -> SourceResult<Value> {
- let Spanned { v: data, span } = data;
- let raw = std::str::from_utf8(data.as_slice())
- .map_err(|_| "file is not valid utf-8")
- .at(span)?;
- ::toml::from_str(raw)
- .map_err(|err| format_toml_error(err, raw))
- .at(span)
- }
-
- /// Encodes structured data into a TOML string.
- #[func(title = "Encode TOML")]
- pub fn encode(
- /// Value to be encoded.
- value: Spanned<Value>,
- /// Whether to pretty-print the resulting TOML.
- #[named]
- #[default(true)]
- pretty: bool,
- ) -> SourceResult<Str> {
- let Spanned { v: value, span } = value;
- if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) }
- .map(|v| v.into())
- .map_err(|err| eco_format!("failed to encode value as TOML ({err})"))
- .at(span)
- }
-}
-
-/// Format the user-facing TOML error message.
-fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString {
- if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) {
- let line = head.lines().count();
- let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count();
- eco_format!(
- "failed to parse TOML ({} at line {line} column {column})",
- error.message(),
- )
- } else {
- eco_format!("failed to parse TOML ({})", error.message())
- }
-}
-
-/// Reads structured data from a YAML file.
-///
-/// The file must contain a valid YAML object or array. YAML mappings will be
-/// converted into Typst dictionaries, and YAML sequences will be converted into
-/// Typst arrays. Strings and booleans will be converted into the Typst
-/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
-/// `{none}`, and numbers will be converted to floats or integers depending on
-/// whether they are whole numbers. Custom YAML tags are ignored, though the
-/// loaded value will still be present.
-///
-/// The YAML files in the example contain objects with authors as keys,
-/// each with a sequence of their own submapping with the keys
-/// "title" and "published"
-///
-/// # Example
-/// ```example
-/// #let bookshelf(contents) = {
-/// for (author, works) in contents {
-/// author
-/// for work in works [
-/// - #work.title (#work.published)
-/// ]
-/// }
-/// }
-///
-/// #bookshelf(
-/// yaml("scifi-authors.yaml")
-/// )
-/// ```
-#[func(scope, title = "YAML")]
-pub fn yaml(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to a YAML file.
- path: Spanned<EcoString>,
-) -> SourceResult<Value> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- yaml::decode(Spanned::new(Readable::Bytes(data), span))
-}
-
-#[scope]
-impl yaml {
- /// Reads structured data from a YAML string/bytes.
- #[func(title = "Decode YAML")]
- pub fn decode(
- /// YAML data.
- data: Spanned<Readable>,
- ) -> SourceResult<Value> {
- let Spanned { v: data, span } = data;
- serde_yaml::from_slice(data.as_slice())
- .map_err(|err| eco_format!("failed to parse YAML ({err})"))
- .at(span)
- }
-
- /// Encode structured data into a YAML string.
- #[func(title = "Encode YAML")]
- pub fn encode(
- /// Value to be encoded.
- value: Spanned<Value>,
- ) -> SourceResult<Str> {
- let Spanned { v: value, span } = value;
- serde_yaml::to_string(&value)
- .map(|v| v.into())
- .map_err(|err| eco_format!("failed to encode value as YAML ({err})"))
- .at(span)
- }
-}
-
-/// Reads structured data from a CBOR file.
-///
-/// The file must contain a valid cbor serialization. Mappings will be
-/// converted into Typst dictionaries, and sequences will be converted into
-/// Typst arrays. Strings and booleans will be converted into the Typst
-/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
-/// `{none}`, and numbers will be converted to floats or integers depending on
-/// whether they are whole numbers.
-#[func(scope, title = "CBOR")]
-pub fn cbor(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to a CBOR file.
- path: Spanned<EcoString>,
-) -> SourceResult<Value> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- cbor::decode(Spanned::new(data, span))
-}
-
-#[scope]
-impl cbor {
- /// Reads structured data from CBOR bytes.
- #[func(title = "Decode CBOR")]
- pub fn decode(
- /// cbor data.
- data: Spanned<Bytes>,
- ) -> SourceResult<Value> {
- let Spanned { v: data, span } = data;
- ciborium::from_reader(data.as_slice())
- .map_err(|err| eco_format!("failed to parse CBOR ({err})"))
- .at(span)
- }
-
- /// Encode structured data into CBOR bytes.
- #[func(title = "Encode CBOR")]
- pub fn encode(
- /// Value to be encoded.
- value: Spanned<Value>,
- ) -> SourceResult<Bytes> {
- let Spanned { v: value, span } = value;
- let mut res = Vec::new();
- ciborium::into_writer(&value, &mut res)
- .map(|_| res.into())
- .map_err(|err| eco_format!("failed to encode value as CBOR ({err})"))
- .at(span)
- }
-}
-
-/// Reads structured data from an XML file.
-///
-/// The XML file is parsed into an array of dictionaries and strings. XML nodes
-/// can be elements or strings. Elements are represented as dictionaries with
-/// the the following keys:
-///
-/// - `tag`: The name of the element as a string.
-/// - `attrs`: A dictionary of the element's attributes as strings.
-/// - `children`: An array of the element's child nodes.
-///
-/// The XML file in the example contains a root `news` tag with multiple
-/// `article` tags. Each article has a `title`, `author`, and `content` tag. The
-/// `content` tag contains one or more paragraphs, which are represented as `p`
-/// tags.
-///
-/// # Example
-/// ```example
-/// #let find-child(elem, tag) = {
-/// elem.children
-/// .find(e => "tag" in e and e.tag == tag)
-/// }
-///
-/// #let article(elem) = {
-/// let title = find-child(elem, "title")
-/// let author = find-child(elem, "author")
-/// let pars = find-child(elem, "content")
-///
-/// heading(title.children.first())
-/// text(10pt, weight: "medium")[
-/// Published by
-/// #author.children.first()
-/// ]
-///
-/// for p in pars.children {
-/// if (type(p) == "dictionary") {
-/// parbreak()
-/// p.children.first()
-/// }
-/// }
-/// }
-///
-/// #let data = xml("example.xml")
-/// #for elem in data.first().children {
-/// if (type(elem) == "dictionary") {
-/// article(elem)
-/// }
-/// }
-/// ```
-#[func(scope, title = "XML")]
-pub fn xml(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to an XML file.
- path: Spanned<EcoString>,
-) -> SourceResult<Value> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- xml::decode(Spanned::new(Readable::Bytes(data), span))
-}
-
-#[scope]
-impl xml {
- /// Reads structured data from an XML string/bytes.
- #[func(title = "Decode XML")]
- pub fn decode(
- /// XML data.
- data: Spanned<Readable>,
- ) -> SourceResult<Value> {
- let Spanned { v: data, span } = data;
- let text = std::str::from_utf8(data.as_slice())
- .map_err(FileError::from)
- .at(span)?;
- let document =
- roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?;
- Ok(convert_xml(document.root()))
- }
-}
-
-/// Convert an XML node to a Typst value.
-fn convert_xml(node: roxmltree::Node) -> Value {
- if node.is_text() {
- return node.text().unwrap_or_default().into_value();
- }
-
- let children: Array = node.children().map(convert_xml).collect();
- if node.is_root() {
- return Value::Array(children);
- }
-
- let tag: Str = node.tag_name().name().into();
- let attrs: Dict = node
- .attributes()
- .map(|attr| (attr.name().into(), attr.value().into_value()))
- .collect();
-
- Value::Dict(dict! {
- "tag" => tag,
- "attrs" => attrs,
- "children" => children,
- })
-}
-
-/// Format the user-facing XML error message.
-fn format_xml_error(error: roxmltree::Error) -> EcoString {
- format_xml_like_error("XML", error)
-}
diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs
deleted file mode 100644
index f1af24c5..00000000
--- a/crates/typst-library/src/compute/mod.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-//! Computational functions.
-
-pub mod calc;
-pub mod sys;
-
-mod data;
-mod foundations;
-
-pub use self::data::*;
-pub use self::foundations::*;
-
-use crate::prelude::*;
-
-/// Hook up all compute definitions.
-pub(super) fn define(global: &mut Scope) {
- self::foundations::define(global);
- self::data::define(global);
- self::calc::define(global);
- self::sys::define(global);
-}
diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs
deleted file mode 100644
index 9c18266d..00000000
--- a/crates/typst-library/src/layout/align.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use crate::prelude::*;
-
-/// Aligns content horizontally and vertically.
-///
-/// # Example
-/// ```example
-/// #set align(center)
-///
-/// Centered text, a sight to see \
-/// In perfect balance, visually \
-/// Not left nor right, it stands alone \
-/// A work of art, a visual throne
-/// ```
-#[elem(Show)]
-pub struct AlignElem {
- /// The [alignment]($alignment) along both axes.
- ///
- /// ```example
- /// #set page(height: 6cm)
- /// #set text(lang: "ar")
- ///
- /// مثال
- /// #align(
- /// end + horizon,
- /// rect(inset: 12pt)[ركن]
- /// )
- /// ```
- #[positional]
- #[fold]
- #[default]
- pub alignment: Align,
-
- /// The content to align.
- #[required]
- pub body: Content,
-}
-
-impl Show for AlignElem {
- #[tracing::instrument(name = "AlignElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self
- .body()
- .clone()
- .styled(Self::set_alignment(self.alignment(styles))))
- }
-}
diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs
deleted file mode 100644
index 212debb0..00000000
--- a/crates/typst-library/src/lib.rs
+++ /dev/null
@@ -1,170 +0,0 @@
-//! Typst's standard library.
-
-#![allow(clippy::wildcard_in_or_patterns)]
-#![allow(clippy::manual_range_contains)]
-#![allow(clippy::comparison_chain)]
-
-pub mod compute;
-pub mod layout;
-pub mod math;
-pub mod meta;
-pub mod prelude;
-pub mod shared;
-pub mod symbols;
-pub mod text;
-pub mod visualize;
-
-use typst::eval::{Array, LangItems, Library, Module, Scope, Smart};
-use typst::geom::{Align, Color, Dir};
-use typst::model::{NativeElement, Styles};
-
-use self::layout::LayoutRoot;
-
-/// Construct the standard library.
-pub fn build() -> Library {
- let math = math::module();
- let global = global(math.clone());
- Library { global, math, styles: styles(), items: items() }
-}
-
-/// Construct the module with global definitions.
-#[tracing::instrument(skip_all)]
-fn global(math: Module) -> Module {
- let mut global = Scope::deduplicating();
- text::define(&mut global);
- global.define_module(math);
- layout::define(&mut global);
- visualize::define(&mut global);
- meta::define(&mut global);
- symbols::define(&mut global);
- compute::define(&mut global);
- prelude(&mut global);
- Module::new("global", global)
-}
-
-/// Defines scoped values that are globally available, too.
-fn prelude(global: &mut Scope) {
- global.reset_category();
- global.define("black", Color::BLACK);
- global.define("gray", Color::GRAY);
- global.define("silver", Color::SILVER);
- global.define("white", Color::WHITE);
- global.define("navy", Color::NAVY);
- global.define("blue", Color::BLUE);
- global.define("aqua", Color::AQUA);
- global.define("teal", Color::TEAL);
- global.define("eastern", Color::EASTERN);
- global.define("purple", Color::PURPLE);
- global.define("fuchsia", Color::FUCHSIA);
- global.define("maroon", Color::MAROON);
- global.define("red", Color::RED);
- global.define("orange", Color::ORANGE);
- global.define("yellow", Color::YELLOW);
- global.define("olive", Color::OLIVE);
- global.define("green", Color::GREEN);
- global.define("lime", Color::LIME);
- global.define("luma", Color::luma_data());
- global.define("oklab", Color::oklab_data());
- global.define("oklch", Color::oklch_data());
- global.define("rgb", Color::rgb_data());
- global.define("cmyk", Color::cmyk_data());
- global.define("range", Array::range_data());
- global.define("ltr", Dir::LTR);
- global.define("rtl", Dir::RTL);
- global.define("ttb", Dir::TTB);
- global.define("btt", Dir::BTT);
- global.define("start", Align::START);
- global.define("left", Align::LEFT);
- global.define("center", Align::CENTER);
- global.define("right", Align::RIGHT);
- global.define("end", Align::END);
- global.define("top", Align::TOP);
- global.define("horizon", Align::HORIZON);
- global.define("bottom", Align::BOTTOM);
-}
-
-/// Construct the standard style map.
-fn styles() -> Styles {
- Styles::new()
-}
-
-/// Construct the standard lang item mapping.
-fn items() -> LangItems {
- LangItems {
- layout: |world, content, styles| content.layout_root(world, styles),
- em: text::TextElem::size_in,
- dir: text::TextElem::dir_in,
- space: || text::SpaceElem::new().pack(),
- linebreak: || text::LinebreakElem::new().pack(),
- text: |text| text::TextElem::new(text).pack(),
- text_elem: text::TextElem::elem(),
- text_str: |content| Some(content.to::<text::TextElem>()?.text()),
- smart_quote: |double| text::SmartquoteElem::new().with_double(double).pack(),
- parbreak: || layout::ParbreakElem::new().pack(),
- strong: |body| text::StrongElem::new(body).pack(),
- emph: |body| text::EmphElem::new(body).pack(),
- raw: |text, lang, block| {
- let mut elem = text::RawElem::new(text).with_block(block);
- if let Some(lang) = lang {
- elem.push_lang(Some(lang));
- }
- elem.pack()
- },
- raw_languages: text::RawElem::languages,
- link: |url| meta::LinkElem::from_url(url).pack(),
- reference: |target, supplement| {
- let mut elem = meta::RefElem::new(target);
- if let Some(supplement) = supplement {
- elem.push_supplement(Smart::Custom(Some(meta::Supplement::Content(
- supplement,
- ))));
- }
- elem.pack()
- },
- bibliography_keys: meta::BibliographyElem::keys,
- heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(),
- heading_elem: meta::HeadingElem::elem(),
- list_item: |body| layout::ListItem::new(body).pack(),
- enum_item: |number, body| {
- let mut elem = layout::EnumItem::new(body);
- if let Some(number) = number {
- elem.push_number(Some(number));
- }
- elem.pack()
- },
- term_item: |term, description| layout::TermItem::new(term, description).pack(),
- equation: |body, block| math::EquationElem::new(body).with_block(block).pack(),
- math_align_point: || math::AlignPointElem::new().pack(),
- math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(),
- math_attach: |base, t, b, tl, bl, tr, br| {
- let mut elem = math::AttachElem::new(base);
- if let Some(t) = t {
- elem.push_t(Some(t));
- }
- if let Some(b) = b {
- elem.push_b(Some(b));
- }
- if let Some(tl) = tl {
- elem.push_tl(Some(tl));
- }
- if let Some(bl) = bl {
- elem.push_bl(Some(bl));
- }
- if let Some(tr) = tr {
- elem.push_tr(Some(tr));
- }
- if let Some(br) = br {
- elem.push_br(Some(br));
- }
- elem.pack()
- },
- math_primes: |count| math::PrimesElem::new(count).pack(),
- math_accent: |base, accent| {
- math::AccentElem::new(base, math::Accent::new(accent)).pack()
- },
- math_frac: |num, denom| math::FracElem::new(num, denom).pack(),
- math_root: |index, radicand| {
- math::RootElem::new(radicand).with_index(index).pack()
- },
- }
-}
diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs
deleted file mode 100644
index ffe861ab..00000000
--- a/crates/typst-library/src/meta/mod.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-//! Interaction between document parts.
-
-mod bibliography;
-mod cite;
-mod context;
-mod counter;
-mod document;
-mod figure;
-mod footnote;
-mod heading;
-mod link;
-mod metadata;
-#[path = "numbering.rs"]
-mod numbering_;
-mod outline;
-#[path = "query.rs"]
-mod query_;
-mod reference;
-mod state;
-
-pub use self::bibliography::*;
-pub use self::cite::*;
-pub use self::context::*;
-pub use self::counter::*;
-pub use self::document::*;
-pub use self::figure::*;
-pub use self::footnote::*;
-pub use self::heading::*;
-pub use self::link::*;
-pub use self::metadata::*;
-pub use self::numbering_::*;
-pub use self::outline::*;
-pub use self::query_::*;
-pub use self::reference::*;
-pub use self::state::*;
-
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Hook up all meta definitions.
-pub(super) fn define(global: &mut Scope) {
- global.category("meta");
- global.define_type::<Label>();
- global.define_type::<Selector>();
- global.define_type::<Location>();
- global.define_type::<Counter>();
- global.define_type::<State>();
- global.define_elem::<DocumentElem>();
- global.define_elem::<RefElem>();
- global.define_elem::<LinkElem>();
- global.define_elem::<OutlineElem>();
- global.define_elem::<HeadingElem>();
- global.define_elem::<FigureElem>();
- global.define_elem::<FootnoteElem>();
- global.define_elem::<CiteElem>();
- global.define_elem::<BibliographyElem>();
- global.define_elem::<MetadataElem>();
- global.define_func::<locate>();
- global.define_func::<style>();
- global.define_func::<layout>();
- global.define_func::<numbering>();
- global.define_func::<query>();
-}
-
-/// An element that has a local name.
-pub trait LocalNameIn: LocalName {
- /// Gets the local name from the style chain.
- fn local_name_in(styles: StyleChain) -> &'static str
- where
- Self: Sized,
- {
- Self::local_name(TextElem::lang_in(styles), TextElem::region_in(styles))
- }
-}
-
-impl<T: LocalName> LocalNameIn for T {}
diff --git a/crates/typst-library/src/prelude.rs b/crates/typst-library/src/prelude.rs
deleted file mode 100644
index 00700540..00000000
--- a/crates/typst-library/src/prelude.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-//! Helpful imports for creating library functionality.
-
-#[doc(no_inline)]
-pub use std::fmt::{self, Debug, Formatter};
-#[doc(no_inline)]
-pub use std::num::NonZeroUsize;
-
-#[doc(no_inline)]
-pub use comemo::{Track, Tracked, TrackedMut};
-#[doc(no_inline)]
-pub use ecow::{eco_format, EcoString};
-#[doc(no_inline)]
-pub use smallvec::{smallvec, SmallVec};
-#[doc(no_inline)]
-pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult};
-#[doc(no_inline)]
-pub use typst::doc::*;
-#[doc(no_inline)]
-pub use typst::eval::{
- array, cast, dict, format_str, func, scope, ty, Args, Array, Bytes, Cast, Dict,
- FromValue, Func, IntoValue, Repr, Scope, Smart, Str, Symbol, Type, Value, Vm,
-};
-#[doc(no_inline)]
-pub use typst::geom::*;
-#[doc(no_inline)]
-pub use typst::model::{
- elem, select_where, Behave, Behaviour, Construct, Content, Element, ElementFields,
- Finalize, Fold, Introspector, Label, LocalName, Locatable, LocatableSelector,
- Location, Locator, MetaElem, NativeElement, PlainText, Resolve, Selector, Set, Show,
- StyleChain, StyleVec, Styles, Synthesize, Unlabellable, Vt,
-};
-#[doc(no_inline)]
-pub use typst::syntax::{FileId, Span, Spanned};
-#[doc(no_inline)]
-pub use typst::util::NonZeroExt;
-#[doc(no_inline)]
-pub use typst::World;
-
-#[doc(no_inline)]
-pub use crate::layout::{Fragment, Layout, Regions};
-#[doc(no_inline)]
-pub use crate::shared::{ContentExt, StylesExt};
diff --git a/crates/typst-library/src/shared/ext.rs b/crates/typst-library/src/shared/ext.rs
deleted file mode 100644
index 60614820..00000000
--- a/crates/typst-library/src/shared/ext.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-//! Extension traits.
-
-use crate::layout::{AlignElem, MoveElem, PadElem};
-use crate::prelude::*;
-use crate::text::{EmphElem, FontFamily, FontList, StrongElem, TextElem, UnderlineElem};
-
-/// Additional methods on content.
-pub trait ContentExt {
- /// Make this content strong.
- fn strong(self) -> Self;
-
- /// Make this content emphasized.
- fn emph(self) -> Self;
-
- /// Underline this content.
- fn underlined(self) -> Self;
-
- /// Link the content somewhere.
- fn linked(self, dest: Destination) -> Self;
-
- /// Make the content linkable by `.linked(Destination::Location(loc))`.
- ///
- /// Should be used in combination with [`Location::variant`].
- fn backlinked(self, loc: Location) -> Self;
-
- /// Set alignments for this content.
- fn aligned(self, align: Align) -> Self;
-
- /// Pad this content at the sides.
- fn padded(self, padding: Sides<Rel<Length>>) -> Self;
-
- /// Transform this content's contents without affecting layout.
- fn moved(self, delta: Axes<Rel<Length>>) -> Self;
-}
-
-impl ContentExt for Content {
- fn strong(self) -> Self {
- StrongElem::new(self).pack()
- }
-
- fn emph(self) -> Self {
- EmphElem::new(self).pack()
- }
-
- fn underlined(self) -> Self {
- UnderlineElem::new(self).pack()
- }
-
- fn linked(self, dest: Destination) -> Self {
- self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)]))
- }
-
- fn backlinked(self, loc: Location) -> Self {
- let mut backlink = Content::empty();
- backlink.set_location(loc);
- self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)]))
- }
-
- fn aligned(self, align: Align) -> Self {
- self.styled(AlignElem::set_alignment(align))
- }
-
- fn padded(self, padding: Sides<Rel<Length>>) -> Self {
- PadElem::new(self)
- .with_left(padding.left)
- .with_top(padding.top)
- .with_right(padding.right)
- .with_bottom(padding.bottom)
- .pack()
- }
-
- fn moved(self, delta: Axes<Rel<Length>>) -> Self {
- MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack()
- }
-}
-
-/// Additional methods for style lists.
-pub trait StylesExt {
- /// Set a font family composed of a preferred family and existing families
- /// from a style chain.
- fn set_family(&mut self, preferred: FontFamily, existing: StyleChain);
-}
-
-impl StylesExt for Styles {
- fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) {
- self.set(TextElem::set_font(FontList(
- std::iter::once(preferred)
- .chain(TextElem::font_in(existing).into_iter().cloned())
- .collect(),
- )));
- }
-}
diff --git a/crates/typst-library/src/shared/mod.rs b/crates/typst-library/src/shared/mod.rs
deleted file mode 100644
index f54241cf..00000000
--- a/crates/typst-library/src/shared/mod.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-//! Shared definitions for the standard library.
-
-mod behave;
-mod ext;
-
-pub use behave::*;
-pub use ext::*;
diff --git a/crates/typst-library/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs
deleted file mode 100644
index 0d288c3b..00000000
--- a/crates/typst-library/src/symbols/mod.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-//! Modifiable symbols.
-
-mod emoji;
-mod sym;
-
-pub use emoji::*;
-pub use sym::*;
-
-use crate::prelude::*;
-
-/// Hook up all symbol definitions.
-pub(super) fn define(global: &mut Scope) {
- global.category("symbols");
- global.define_type::<Symbol>();
- global.define_module(sym());
- global.define_module(emoji());
-}
diff --git a/crates/typst-library/src/text/misc.rs b/crates/typst-library/src/text/misc.rs
deleted file mode 100644
index 1bf28a34..00000000
--- a/crates/typst-library/src/text/misc.rs
+++ /dev/null
@@ -1,315 +0,0 @@
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// A text space.
-#[elem(Behave, Unlabellable, PlainText, Repr)]
-pub struct SpaceElem {}
-
-impl Repr for SpaceElem {
- fn repr(&self) -> EcoString {
- EcoString::inline("[ ]")
- }
-}
-
-impl Behave for SpaceElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Weak(2)
- }
-}
-
-impl Unlabellable for SpaceElem {}
-
-impl PlainText for SpaceElem {
- fn plain_text(&self, text: &mut EcoString) {
- text.push(' ');
- }
-}
-
-/// Inserts a line break.
-///
-/// Advances the paragraph to the next line. A single trailing line break at the
-/// end of a paragraph is ignored, but more than one creates additional empty
-/// lines.
-///
-/// # Example
-/// ```example
-/// *Date:* 26.12.2022 \
-/// *Topic:* Infrastructure Test \
-/// *Severity:* High \
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: To insert a line break, simply write
-/// a backslash followed by whitespace. This always creates an unjustified
-/// break.
-#[elem(title = "Line Break", Behave)]
-pub struct LinebreakElem {
- /// Whether to justify the line before the break.
- ///
- /// This is useful if you found a better line break opportunity in your
- /// justified text than Typst did.
- ///
- /// ```example
- /// #set par(justify: true)
- /// #let jb = linebreak(justify: true)
- ///
- /// I have manually tuned the #jb
- /// line breaks in this paragraph #jb
- /// for an _interesting_ result. #jb
- /// ```
- #[default(false)]
- pub justify: bool,
-}
-
-impl Behave for LinebreakElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Destructive
- }
-}
-
-/// Strongly emphasizes content by increasing the font weight.
-///
-/// Increases the current font weight by a given `delta`.
-///
-/// # Example
-/// ```example
-/// This is *strong.* \
-/// This is #strong[too.] \
-///
-/// #show strong: set text(red)
-/// And this is *evermore.*
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: To strongly emphasize content,
-/// simply enclose it in stars/asterisks (`*`). Note that this only works at
-/// word boundaries. To strongly emphasize part of a word, you have to use the
-/// function.
-#[elem(title = "Strong Emphasis", Show)]
-pub struct StrongElem {
- /// The delta to apply on the font weight.
- ///
- /// ```example
- /// #set strong(delta: 0)
- /// No *effect!*
- /// ```
- #[default(300)]
- pub delta: i64,
-
- /// The content to strongly emphasize.
- #[required]
- pub body: Content,
-}
-
-impl Show for StrongElem {
- #[tracing::instrument(name = "StrongElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self
- .body()
- .clone()
- .styled(TextElem::set_delta(Delta(self.delta(styles)))))
- }
-}
-
-/// A delta that is summed up when folded.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Delta(pub i64);
-
-cast! {
- Delta,
- self => self.0.into_value(),
- v: i64 => Self(v),
-}
-
-impl Fold for Delta {
- type Output = i64;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- outer + self.0
- }
-}
-
-/// Emphasizes content by setting it in italics.
-///
-/// - If the current [text style]($text.style) is `{"normal"}`, this turns it
-/// into `{"italic"}`.
-/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to
-/// `{"normal"}`.
-///
-/// # Example
-/// ```example
-/// This is _emphasized._ \
-/// This is #emph[too.]
-///
-/// #show emph: it => {
-/// text(blue, it.body)
-/// }
-///
-/// This is _emphasized_ differently.
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: To emphasize content, simply
-/// enclose it in underscores (`_`). Note that this only works at word
-/// boundaries. To emphasize part of a word, you have to use the function.
-#[elem(title = "Emphasis", Show)]
-pub struct EmphElem {
- /// The content to emphasize.
- #[required]
- pub body: Content,
-}
-
-impl Show for EmphElem {
- #[tracing::instrument(name = "EmphElem::show", skip(self))]
- fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(TextElem::set_emph(Toggle)))
- }
-}
-
-/// A toggle that turns on and off alternatingly if folded.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Toggle;
-
-cast! {
- Toggle,
- self => Value::None,
- _: Value => Self,
-}
-
-impl Fold for Toggle {
- type Output = bool;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- !outer
- }
-}
-
-/// Converts text or content to lowercase.
-///
-/// # Example
-/// ```example
-/// #lower("ABC") \
-/// #lower[*My Text*] \
-/// #lower[already low]
-/// ```
-#[func(title = "Lowercase")]
-pub fn lower(
- /// The text to convert to lowercase.
- text: Caseable,
-) -> Caseable {
- case(text, Case::Lower)
-}
-
-/// Converts text or content to uppercase.
-///
-/// # Example
-/// ```example
-/// #upper("abc") \
-/// #upper[*my text*] \
-/// #upper[ALREADY HIGH]
-/// ```
-#[func(title = "Uppercase")]
-pub fn upper(
- /// The text to convert to uppercase.
- text: Caseable,
-) -> Caseable {
- case(text, Case::Upper)
-}
-
-/// Change the case of text.
-fn case(text: Caseable, case: Case) -> Caseable {
- match text {
- Caseable::Str(v) => Caseable::Str(case.apply(&v).into()),
- Caseable::Content(v) => {
- Caseable::Content(v.styled(TextElem::set_case(Some(case))))
- }
- }
-}
-
-/// A value whose case can be changed.
-pub enum Caseable {
- Str(Str),
- Content(Content),
-}
-
-cast! {
- Caseable,
- self => match self {
- Self::Str(v) => v.into_value(),
- Self::Content(v) => v.into_value(),
- },
- v: Str => Self::Str(v),
- v: Content => Self::Content(v),
-}
-
-/// A case transformation on text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Case {
- /// Everything is lowercased.
- Lower,
- /// Everything is uppercased.
- Upper,
-}
-
-impl Case {
- /// Apply the case to a string.
- pub fn apply(self, text: &str) -> String {
- match self {
- Self::Lower => text.to_lowercase(),
- Self::Upper => text.to_uppercase(),
- }
- }
-}
-
-/// Displays text in small capitals.
-///
-/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts
-/// support this feature. Sometimes smallcaps are part of a dedicated font and
-/// sometimes they are not available at all. In the future, this function will
-/// support selecting a dedicated smallcaps font as well as synthesizing
-/// smallcaps from normal letters, but this is not yet implemented.
-///
-/// # Example
-/// ```example
-/// #set par(justify: true)
-/// #set heading(numbering: "I.")
-///
-/// #show heading: it => {
-/// set block(below: 10pt)
-/// set text(weight: "regular")
-/// align(center, smallcaps(it))
-/// }
-///
-/// = Introduction
-/// #lorem(40)
-/// ```
-#[func(title = "Small Capitals")]
-pub fn smallcaps(
- /// The text to display to small capitals.
- body: Content,
-) -> Content {
- body.styled(TextElem::set_smallcaps(true))
-}
-
-/// Creates blind text.
-///
-/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given
-/// number of words. The sequence of words generated by the function is always
-/// the same but randomly chosen. As usual for blind texts, it does not make any
-/// sense. Use it as a placeholder to try layouts.
-///
-/// # Example
-/// ```example
-/// = Blind Text
-/// #lorem(30)
-///
-/// = More Blind Text
-/// #lorem(15)
-/// ```
-#[func(keywords = ["Blind Text"])]
-pub fn lorem(
- /// The length of the blind text in words.
- words: usize,
-) -> Str {
- lipsum::lipsum(words).replace("--", "–").into()
-}
diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs
deleted file mode 100644
index 779b07ff..00000000
--- a/crates/typst-library/src/visualize/shape.rs
+++ /dev/null
@@ -1,547 +0,0 @@
-use std::f64::consts::SQRT_2;
-
-use crate::prelude::*;
-
-/// A rectangle with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #rect(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #rect[
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-#[elem(title = "Rectangle", Layout)]
-pub struct RectElem {
- /// The rectangle's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The rectangle's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the rectangle.
- ///
- /// When setting a fill, the default stroke disappears. To create a
- /// rectangle with both fill and stroke, you have to configure both.
- ///
- /// ```example
- /// #rect(fill: blue)
- /// ```
- pub fill: Option<Paint>,
-
- /// How to stroke the rectangle. This can be:
- ///
- /// - `{none}` to disable stroking
- /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
- /// given.
- /// - Any kind of [stroke]($stroke)
- /// - A dictionary describing the stroke for each side inidvidually. The
- /// dictionary can contain the following keys in order of precedence:
- /// - `top`: The top stroke.
- /// - `right`: The right stroke.
- /// - `bottom`: The bottom stroke.
- /// - `left`: The left stroke.
- /// - `x`: The horizontal stroke.
- /// - `y`: The vertical stroke.
- /// - `rest`: The stroke on all sides except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #stack(
- /// dir: ltr,
- /// spacing: 1fr,
- /// rect(stroke: red),
- /// rect(stroke: 2pt),
- /// rect(stroke: 2pt + red),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
-
- /// How much to round the rectangle's corners, relative to the minimum of
- /// the width and height divided by two. This can be:
- ///
- /// - A relative length for a uniform corner radius.
- /// - A dictionary: With a dictionary, the stroke for each side can be set
- /// individually. The dictionary can contain the following keys in order
- /// of precedence:
- /// - `top-left`: The top-left corner radius.
- /// - `top-right`: The top-right corner radius.
- /// - `bottom-right`: The bottom-right corner radius.
- /// - `bottom-left`: The bottom-left corner radius.
- /// - `left`: The top-left and bottom-left corner radii.
- /// - `top`: The top-left and top-right corner radii.
- /// - `right`: The top-right and bottom-right corner radii.
- /// - `bottom`: The bottom-left and bottom-right corner radii.
- /// - `rest`: The radii for all corners except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #set rect(stroke: 4pt)
- /// #rect(
- /// radius: (
- /// left: 5pt,
- /// top-right: 20pt,
- /// bottom-right: 10pt,
- /// ),
- /// stroke: (
- /// left: red,
- /// top: yellow,
- /// right: green,
- /// bottom: blue,
- /// ),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub radius: Corners<Option<Rel<Length>>>,
-
- /// How much to pad the rectangle's content.
- /// See the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the rectangle's size without affecting the layout.
- /// See the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the rectangle.
- ///
- /// When this is omitted, the rectangle takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for RectElem {
- #[tracing::instrument(name = "RectElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Rect,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// A square with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #square(size: 40pt)
-///
-/// // With content.
-/// #square[
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct SquareElem {
- /// The square's side length. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub size: Smart<Length>,
-
- /// The square's width. This is mutually exclusive with `size` and `height`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The square's height. This is mutually exclusive with `size` and `width`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the square. See the [rectangle's documentation]($rect.fill)
- /// for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the square. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
-
- /// How much to round the square's corners. See the
- /// [rectangle's documentation]($rect.radius) for more details.
- #[resolve]
- #[fold]
- pub radius: Corners<Option<Rel<Length>>>,
-
- /// How much to pad the square's content. See the
- /// [box's documentation]($box.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the square's size without affecting the layout. See
- /// the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the square. The square expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- ///
- /// When this is omitted, the square takes on a default size of at most
- /// `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for SquareElem {
- #[tracing::instrument(name = "SquareElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Square,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// An ellipse with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #ellipse(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #ellipse[
-/// #set align(center)
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct EllipseElem {
- /// The ellipse's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The ellipse's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
- /// for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the ellipse. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<Stroke>>,
-
- /// How much to pad the ellipse's content. See the
- /// [box's documentation]($box.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the ellipse's size without affecting the layout. See
- /// the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the ellipse.
- ///
- /// When this is omitted, the ellipse takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for EllipseElem {
- #[tracing::instrument(name = "EllipseElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Ellipse,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// A circle with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #circle(radius: 25pt)
-///
-/// // With content.
-/// #circle[
-/// #set align(center + horizon)
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct CircleElem {
- /// The circle's radius. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub radius: Length,
-
- /// The circle's width. This is mutually exclusive with `radius` and
- /// `height`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args
- .named::<Smart<Length>>("radius")?
- .map(|s| s.map(|r| 2.0 * Rel::from(r)));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The circle's height. This is mutually exclusive with `radius` and
- /// `width`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the circle. See the [rectangle's documentation]($rect.fill)
- /// for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the circle. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- #[default(Smart::Auto)]
- pub stroke: Smart<Option<Stroke>>,
-
- /// How much to pad the circle's content. See the
- /// [box's documentation]($box.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the circle's size without affecting the layout. See
- /// the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the circle. The circle expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for CircleElem {
- #[tracing::instrument(name = "CircleElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Circle,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// Layout a shape.
-#[tracing::instrument(name = "shape::layout", skip_all)]
-#[allow(clippy::too_many_arguments)]
-fn layout(
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- kind: ShapeKind,
- body: &Option<Content>,
- sizing: Axes<Smart<Rel<Length>>>,
- fill: Option<Paint>,
- stroke: Smart<Sides<Option<Stroke<Abs>>>>,
- mut inset: Sides<Rel<Abs>>,
- outset: Sides<Rel<Abs>>,
- radius: Corners<Rel<Abs>>,
- span: Span,
-) -> SourceResult<Fragment> {
- let resolved = sizing
- .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)));
-
- let mut frame;
- if let Some(child) = body {
- let region = resolved.unwrap_or(regions.base());
- if kind.is_round() {
- inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
- }
-
- // Pad the child.
- let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
- let expand = sizing.as_ref().map(Smart::is_custom);
- let pod = Regions::one(region, expand);
- frame = child.layout(vt, styles, pod)?.into_frame();
-
- // Enforce correct size.
- *frame.size_mut() = expand.select(region, frame.size());
-
- // Relayout with full expansion into square region to make sure
- // the result is really a square or circle.
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- let length = frame.size().max_by_side().min(region.min_by_side());
- let pod = Regions::one(Size::splat(length), Axes::splat(true));
- frame = child.layout(vt, styles, pod)?.into_frame();
- }
-
- // Enforce correct size again.
- *frame.size_mut() = expand.select(region, frame.size());
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- }
- } else {
- // The default size that a shape takes on if it has no child and
- // enough space.
- let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
- let mut size = resolved.unwrap_or(default.min(regions.base()));
- if kind.is_quadratic() {
- size = Size::splat(size.min_by_side());
- }
- frame = Frame::soft(size);
- }
-
- // Prepare stroke.
- let stroke = match stroke {
- Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())),
- Smart::Auto => Sides::splat(None),
- Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)),
- };
-
- // Add fill and/or stroke.
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- if kind.is_round() {
- let outset = outset.relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let pos = Point::new(-outset.left, -outset.top);
- let shape = ellipse(size, fill, stroke.left);
- frame.prepend(pos, FrameItem::Shape(shape, span));
- } else {
- frame.fill_and_stroke(fill, stroke, outset, radius, span);
- }
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
-}
-
-/// A category of shape.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum ShapeKind {
- /// A rectangle with equal side lengths.
- Square,
- /// A quadrilateral with four right angles.
- Rect,
- /// An ellipse with coinciding foci.
- Circle,
- /// A curve around two focal points.
- Ellipse,
-}
-
-impl ShapeKind {
- /// Whether this shape kind is curvy.
- fn is_round(self) -> bool {
- matches!(self, Self::Circle | Self::Ellipse)
- }
-
- /// Whether this shape kind has equal side length.
- fn is_quadratic(self) -> bool {
- matches!(self, Self::Square | Self::Circle)
- }
-}
diff --git a/crates/typst-macros/src/cast.rs b/crates/typst-macros/src/cast.rs
index b83dbedd..f2115aa9 100644
--- a/crates/typst-macros/src/cast.rs
+++ b/crates/typst-macros/src/cast.rs
@@ -1,4 +1,11 @@
-use super::*;
+use heck::ToKebabCase;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::{DeriveInput, Ident, Result, Token};
+
+use crate::util::{documentation, foundations};
/// Expand the `#[derive(Cast)]` macro.
pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> {
@@ -43,9 +50,9 @@ pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> {
});
Ok(quote! {
- ::typst::eval::cast! {
+ #foundations::cast! {
#ty,
- self => ::typst::eval::IntoValue::into_value(match self {
+ self => #foundations::IntoValue::into_value(match self {
#(#variants_to_strs),*
}),
#(#strs_to_variants),*
@@ -62,8 +69,6 @@ struct Variant {
/// Expand the `cast!` macro.
pub fn cast(stream: TokenStream) -> Result<TokenStream> {
- let eval = quote! { ::typst::eval };
-
let input: CastInput = syn::parse2(stream)?;
let ty = &input.ty;
let castable_body = create_castable_body(&input);
@@ -74,16 +79,16 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let reflect = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! {
- impl #eval::Reflect for #ty {
- fn input() -> #eval::CastInfo {
+ impl #foundations::Reflect for #ty {
+ fn input() -> #foundations::CastInfo {
#input_body
}
- fn output() -> #eval::CastInfo {
+ fn output() -> #foundations::CastInfo {
#output_body
}
- fn castable(value: &#eval::Value) -> bool {
+ fn castable(value: &#foundations::Value) -> bool {
#castable_body
}
}
@@ -92,8 +97,8 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let into_value = (input.into_value.is_some() || input.dynamic).then(|| {
quote! {
- impl #eval::IntoValue for #ty {
- fn into_value(self) -> #eval::Value {
+ impl #foundations::IntoValue for #ty {
+ fn into_value(self) -> #foundations::Value {
#into_value_body
}
}
@@ -102,8 +107,8 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let from_value = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! {
- impl #eval::FromValue for #ty {
- fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> {
+ impl #foundations::FromValue for #ty {
+ fn from_value(value: #foundations::Value) -> ::typst::diag::StrResult<Self> {
#from_value_body
}
}
@@ -196,7 +201,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
}
Pattern::Ty(_, ty) => {
casts.push(quote! {
- if <#ty as ::typst::eval::Reflect>::castable(value) {
+ if <#ty as #foundations::Reflect>::castable(value) {
return true;
}
});
@@ -206,7 +211,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
let dynamic_check = input.dynamic.then(|| {
quote! {
- if let ::typst::eval::Value::Dyn(dynamic) = &value {
+ if let #foundations::Value::Dyn(dynamic) = &value {
if dynamic.is::<Self>() {
return true;
}
@@ -216,7 +221,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
let str_check = (!strings.is_empty()).then(|| {
quote! {
- if let ::typst::eval::Value::Str(string) = &value {
+ if let #foundations::Value::Str(string) = &value {
match string.as_str() {
#(#strings,)*
_ => {}
@@ -241,21 +246,21 @@ fn create_input_body(input: &CastInput) -> TokenStream {
infos.push(match &cast.pattern {
Pattern::Str(lit) => {
quote! {
- ::typst::eval::CastInfo::Value(
- ::typst::eval::IntoValue::into_value(#lit),
+ #foundations::CastInfo::Value(
+ #foundations::IntoValue::into_value(#lit),
#docs,
)
}
}
Pattern::Ty(_, ty) => {
- quote! { <#ty as ::typst::eval::Reflect>::input() }
+ quote! { <#ty as #foundations::Reflect>::input() }
}
});
}
if input.dynamic {
infos.push(quote! {
- ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>())
+ #foundations::CastInfo::Type(#foundations::Type::of::<Self>())
});
}
@@ -266,7 +271,7 @@ fn create_input_body(input: &CastInput) -> TokenStream {
fn create_output_body(input: &CastInput) -> TokenStream {
if input.dynamic {
- quote! { ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>()) }
+ quote! { #foundations::CastInfo::Type(#foundations::Type::of::<Self>()) }
} else {
quote! { Self::input() }
}
@@ -276,7 +281,7 @@ fn create_into_value_body(input: &CastInput) -> TokenStream {
if let Some(expr) = &input.into_value {
quote! { #expr }
} else {
- quote! { ::typst::eval::Value::dynamic(self) }
+ quote! { #foundations::Value::dynamic(self) }
}
}
@@ -292,8 +297,8 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
}
Pattern::Ty(binding, ty) => {
cast_checks.push(quote! {
- if <#ty as ::typst::eval::Reflect>::castable(&value) {
- let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?;
+ if <#ty as #foundations::Reflect>::castable(&value) {
+ let #binding = <#ty as #foundations::FromValue>::from_value(value)?;
return Ok(#expr);
}
});
@@ -303,7 +308,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
let dynamic_check = input.dynamic.then(|| {
quote! {
- if let ::typst::eval::Value::Dyn(dynamic) = &value {
+ if let #foundations::Value::Dyn(dynamic) = &value {
if let Some(concrete) = dynamic.downcast::<Self>() {
return Ok(concrete.clone());
}
@@ -313,7 +318,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
let str_check = (!string_arms.is_empty()).then(|| {
quote! {
- if let ::typst::eval::Value::Str(string) = &value {
+ if let #foundations::Value::Str(string) = &value {
match string.as_str() {
#(#string_arms,)*
_ => {}
@@ -326,6 +331,6 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
#dynamic_check
#str_check
#(#cast_checks)*
- Err(<Self as ::typst::eval::Reflect>::error(&value))
+ Err(<Self as #foundations::Reflect>::error(&value))
}
}
diff --git a/crates/typst-macros/src/category.rs b/crates/typst-macros/src/category.rs
new file mode 100644
index 00000000..399a0510
--- /dev/null
+++ b/crates/typst-macros/src/category.rs
@@ -0,0 +1,57 @@
+use heck::{ToKebabCase, ToTitleCase};
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Attribute, Ident, Result, Token, Type, Visibility};
+
+use crate::util::{documentation, foundations};
+
+/// Expand the `#[category]` macro.
+pub fn category(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
+ let syn::Item::Verbatim(stream) = item else {
+ bail!(item, "expected bare static");
+ };
+
+ let BareStatic { attrs, vis, ident, ty, .. } = syn::parse2(stream)?;
+
+ let name = ident.to_string().to_kebab_case();
+ let title = name.to_title_case();
+ let docs = documentation(&attrs);
+
+ Ok(quote! {
+ #(#attrs)*
+ #vis static #ident: #ty = {
+ static DATA: #foundations::CategoryData = #foundations::CategoryData {
+ name: #name,
+ title: #title,
+ docs: #docs,
+ };
+ #foundations::Category::from_data(&DATA)
+ };
+ })
+}
+
+/// Parse a bare `pub static CATEGORY: Category;` item.
+pub struct BareStatic {
+ pub attrs: Vec<Attribute>,
+ pub vis: Visibility,
+ pub static_token: Token![static],
+ pub ident: Ident,
+ pub colon_token: Token![:],
+ pub ty: Type,
+ pub semi_token: Token![;],
+}
+
+impl Parse for BareStatic {
+ fn parse(input: ParseStream) -> Result<Self> {
+ Ok(Self {
+ attrs: input.call(Attribute::parse_outer)?,
+ vis: input.parse()?,
+ static_token: input.parse()?,
+ ident: input.parse()?,
+ colon_token: input.parse()?,
+ ty: input.parse()?,
+ semi_token: input.parse()?,
+ })
+ }
+}
diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs
index 2de0adf2..9791427b 100644
--- a/crates/typst-macros/src/elem.rs
+++ b/crates/typst-macros/src/elem.rs
@@ -1,4 +1,15 @@
-use super::*;
+use heck::{ToKebabCase, ToShoutySnakeCase, ToUpperCamelCase};
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::{parse_quote, Ident, Result, Token};
+
+use crate::util::{
+ determine_name_and_title, documentation, foundations, has_attr, kw, parse_attr,
+ parse_flag, parse_string, parse_string_array, quote_option, validate_attrs,
+ BlockWithReturn,
+};
/// Expand the `#[elem]` macro.
pub fn elem(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> {
@@ -136,7 +147,6 @@ struct Field {
synthesized: bool,
borrowed: bool,
ghost: bool,
- forced_variant: Option<usize>,
parse: Option<BlockWithReturn>,
default: Option<syn::Expr>,
}
@@ -226,10 +236,6 @@ fn parse_field(field: &syn::Field) -> Result<Field> {
docs: documentation(&attrs),
internal: has_attr(&mut attrs, "internal"),
external: has_attr(&mut attrs, "external"),
- forced_variant: parse_attr::<syn::LitInt>(&mut attrs, "variant")?
- .flatten()
- .map(|lit| lit.base10_parse())
- .transpose()?,
positional,
required,
variadic,
@@ -262,12 +268,12 @@ fn parse_field(field: &syn::Field) -> Result<Field> {
if field.resolve {
let output = &field.output;
- field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output };
+ field.output = parse_quote! { <#output as #foundations::Resolve>::Output };
}
if field.fold {
let output = &field.output;
- field.output = parse_quote! { <#output as ::typst::model::Fold>::Output };
+ field.output = parse_quote! { <#output as #foundations::Fold>::Output };
}
validate_attrs(&attrs)?;
@@ -317,8 +323,8 @@ fn create(element: &Elem) -> Result<TokenStream> {
let label_and_location = element.unless_capability("Unlabellable", || {
quote! {
- location: Option<::typst::model::Location>,
- label: Option<::typst::model::Label>,
+ location: Option<::typst::introspection::Location>,
+ label: Option<#foundations::Label>,
prepared: bool,
}
});
@@ -330,7 +336,7 @@ fn create(element: &Elem) -> Result<TokenStream> {
#vis struct #ident {
span: ::typst::syntax::Span,
#label_and_location
- guards: ::std::vec::Vec<::typst::model::Guard>,
+ guards: ::std::vec::Vec<#foundations::Guard>,
#(#fields,)*
}
@@ -353,9 +359,9 @@ fn create(element: &Elem) -> Result<TokenStream> {
#partial_eq_impl
#repr_impl
- impl ::typst::eval::IntoValue for #ident {
- fn into_value(self) -> ::typst::eval::Value {
- ::typst::eval::Value::Content(::typst::model::Content::new(self))
+ impl #foundations::IntoValue for #ident {
+ fn into_value(self) -> #foundations::Value {
+ #foundations::Value::Content(#foundations::Content::new(self))
}
}
})
@@ -382,12 +388,9 @@ fn create_field(field: &Field) -> TokenStream {
/// Creates the element's enum for field identifiers.
fn create_fields_enum(element: &Elem) -> TokenStream {
- let model = quote! { ::typst::model };
let Elem { ident, enum_ident, .. } = element;
- let mut fields = element.real_fields().collect::<Vec<_>>();
- fields.sort_by_key(|field| field.forced_variant.unwrap_or(usize::MAX));
-
+ let fields = element.real_fields().collect::<Vec<_>>();
let field_names = fields.iter().map(|Field { name, .. }| name).collect::<Vec<_>>();
let field_consts = fields
.iter()
@@ -399,20 +402,14 @@ fn create_fields_enum(element: &Elem) -> TokenStream {
.map(|Field { enum_ident, .. }| enum_ident)
.collect::<Vec<_>>();
- let definitions =
- fields.iter().map(|Field { forced_variant, enum_ident, .. }| {
- if let Some(variant) = forced_variant {
- let variant = proc_macro2::Literal::u8_unsuffixed(*variant as _);
- quote! { #enum_ident = #variant }
- } else {
- quote! { #enum_ident }
- }
- });
+ let definitions = fields.iter().map(|Field { enum_ident, .. }| {
+ quote! { #enum_ident }
+ });
quote! {
// To hide the private type
const _: () = {
- impl #model::ElementFields for #ident {
+ impl #foundations::ElementFields for #ident {
type Fields = #enum_ident;
}
@@ -578,16 +575,15 @@ fn create_push_field_method(field: &Field) -> TokenStream {
/// Create a setter method for a field.
fn create_set_field_method(element: &Elem, field: &Field) -> TokenStream {
- let model = quote! { ::typst::model };
let elem = &element.ident;
let Field { vis, ident, set_ident, enum_ident, ty, name, .. } = field;
let doc = format!("Create a style property for the `{}` field.", name);
quote! {
#[doc = #doc]
- #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style {
- ::typst::model::Style::Property(::typst::model::Property::new(
- <Self as ::typst::model::NativeElement>::elem(),
- <#elem as #model::ElementFields>::Fields::#enum_ident as u8,
+ #vis fn #set_ident(#ident: #ty) -> #foundations::Style {
+ #foundations::Style::Property(#foundations::Property::new(
+ <Self as #foundations::NativeElement>::elem(),
+ <#elem as #foundations::ElementFields>::Fields::#enum_ident as u8,
#ident,
))
}
@@ -608,7 +604,7 @@ fn create_field_in_method(element: &Elem, field: &Field) -> TokenStream {
quote! {
#[doc = #doc]
- #vis fn #ident_in(styles: ::typst::model::StyleChain) -> #output {
+ #vis fn #ident_in(styles: #foundations::StyleChain) -> #output {
#access
}
}
@@ -646,7 +642,7 @@ fn create_field_method(element: &Elem, field: &Field) -> TokenStream {
quote! {
#[doc = #docs]
- #vis fn #ident<'a>(&'a self, styles: ::typst::model::StyleChain<'a>) -> &'a #output {
+ #vis fn #ident<'a>(&'a self, styles: #foundations::StyleChain<'a>) -> &'a #output {
#access
}
}
@@ -655,7 +651,7 @@ fn create_field_method(element: &Elem, field: &Field) -> TokenStream {
quote! {
#[doc = #docs]
- #vis fn #ident(&self, styles: ::typst::model::StyleChain) -> #output {
+ #vis fn #ident(&self, styles: #foundations::StyleChain) -> #output {
#access
}
}
@@ -668,7 +664,6 @@ fn create_style_chain_access(
field: &Field,
inherent: TokenStream,
) -> TokenStream {
- let model = quote! { ::typst::model };
let elem = &element.ident;
let Field { ty, default, enum_ident, .. } = field;
@@ -693,8 +688,8 @@ fn create_style_chain_access(
quote! {
#init
styles.#getter::<#ty>(
- <Self as ::typst::model::NativeElement>::elem(),
- <#elem as #model::ElementFields>::Fields::#enum_ident as u8,
+ <Self as #foundations::NativeElement>::elem(),
+ <#elem as #foundations::ElementFields>::Fields::#enum_ident as u8,
#inherent,
#default,
)
@@ -703,9 +698,6 @@ fn create_style_chain_access(
/// Creates the element's `Pack` implementation.
fn create_native_elem_impl(element: &Elem) -> TokenStream {
- let eval = quote! { ::typst::eval };
- let model = quote! { ::typst::model };
-
let Elem { name, ident, title, scope, keywords, docs, .. } = element;
let vtable_func = create_vtable_func(element);
@@ -716,9 +708,9 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
.map(create_param_info);
let scope = if *scope {
- quote! { <#ident as #eval::NativeScope>::scope() }
+ quote! { <#ident as #foundations::NativeScope>::scope() }
} else {
- quote! { #eval::Scope::new() }
+ quote! { #foundations::Scope::new() }
};
// Fields that can be accessed using the `field` method.
@@ -729,37 +721,39 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
if field.ghost {
quote! {
- <#elem as #model::ElementFields>::Fields::#name => None,
+ <#elem as #foundations::ElementFields>::Fields::#name => None,
}
} else if field.inherent() {
quote! {
- <#elem as #model::ElementFields>::Fields::#name => Some(
- ::typst::eval::IntoValue::into_value(self.#field_ident.clone())
+ <#elem as #foundations::ElementFields>::Fields::#name => Some(
+ #foundations::IntoValue::into_value(self.#field_ident.clone())
),
}
} else {
quote! {
- <#elem as #model::ElementFields>::Fields::#name => {
- self.#field_ident.clone().map(::typst::eval::IntoValue::into_value)
+ <#elem as #foundations::ElementFields>::Fields::#name => {
+ self.#field_ident.clone().map(#foundations::IntoValue::into_value)
}
}
}
});
// Fields that can be set using the `set_field` method.
- let field_set_matches = element.visible_fields()
- .filter(|field| field.settable() && !field.synthesized && !field.ghost).map(|field| {
- let elem = &element.ident;
- let name = &field.enum_ident;
- let field_ident = &field.ident;
+ let field_set_matches = element
+ .visible_fields()
+ .filter(|field| field.settable() && !field.synthesized && !field.ghost)
+ .map(|field| {
+ let elem = &element.ident;
+ let name = &field.enum_ident;
+ let field_ident = &field.ident;
- quote! {
- <#elem as #model::ElementFields>::Fields::#name => {
- self.#field_ident = Some(::typst::eval::FromValue::from_value(value)?);
- return Ok(());
+ quote! {
+ <#elem as #foundations::ElementFields>::Fields::#name => {
+ self.#field_ident = Some(#foundations::FromValue::from_value(value)?);
+ return Ok(());
+ }
}
- }
- });
+ });
// Fields that are inherent.
let field_inherent_matches = element
@@ -771,8 +765,8 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let field_ident = &field.ident;
quote! {
- <#elem as #model::ElementFields>::Fields::#name => {
- self.#field_ident = ::typst::eval::FromValue::from_value(value)?;
+ <#elem as #foundations::ElementFields>::Fields::#name => {
+ self.#field_ident = #foundations::FromValue::from_value(value)?;
return Ok(());
}
}
@@ -790,13 +784,13 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
// Internal fields create an error that they are unknown.
let unknown_field = format!("unknown field `{field_name}` on `{name}`");
quote! {
- <#elem as #model::ElementFields>::Fields::#ident => ::typst::diag::bail!(#unknown_field),
+ <#elem as #foundations::ElementFields>::Fields::#ident => ::typst::diag::bail!(#unknown_field),
}
} else {
// Fields that cannot be set create an error that they are not settable.
let not_settable = format!("cannot set `{field_name}` on `{name}`");
quote! {
- <#elem as #model::ElementFields>::Fields::#ident => ::typst::diag::bail!(#not_settable),
+ <#elem as #foundations::ElementFields>::Fields::#ident => ::typst::diag::bail!(#not_settable),
}
}
});
@@ -824,15 +818,15 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let field_ident = &field.ident;
let field_call = if name.len() > 15 {
- quote! { EcoString::from(#name).into() }
+ quote! { ::ecow::EcoString::from(#name).into() }
} else {
- quote! { EcoString::inline(#name).into() }
+ quote! { ::ecow::EcoString::inline(#name).into() }
};
quote! {
fields.insert(
#field_call,
- ::typst::eval::IntoValue::into_value(self.#field_ident.clone())
+ #foundations::IntoValue::into_value(self.#field_ident.clone())
);
}
});
@@ -847,16 +841,16 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let field_ident = &field.ident;
let field_call = if name.len() > 15 {
- quote! { EcoString::from(#name).into() }
+ quote! { ::ecow::EcoString::from(#name).into() }
} else {
- quote! { EcoString::inline(#name).into() }
+ quote! { ::ecow::EcoString::inline(#name).into() }
};
quote! {
if let Some(value) = &self.#field_ident {
fields.insert(
#field_call,
- ::typst::eval::IntoValue::into_value(value.clone())
+ #foundations::IntoValue::into_value(value.clone())
);
}
}
@@ -889,7 +883,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let label_field = element
.unless_capability("Unlabellable", || {
quote! {
- self.label().map(::typst::eval::Value::Label)
+ self.label().map(#foundations::Value::Label)
}
})
.unwrap_or_else(|| quote! { None });
@@ -905,51 +899,51 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let local_name = element
.if_capability(
"LocalName",
- || quote! { Some(<#ident as ::typst::model::LocalName>::local_name) },
+ || quote! { Some(<#ident as ::typst::text::LocalName>::local_name) },
)
.unwrap_or_else(|| quote! { None });
let unknown_field = format!("unknown field {{}} on {}", name);
let label_error = format!("cannot set label on {}", name);
let data = quote! {
- #model::NativeElementData {
+ #foundations::NativeElementData {
name: #name,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
- construct: <#ident as #model::Construct>::construct,
- set: <#ident as #model::Set>::set,
+ construct: <#ident as #foundations::Construct>::construct,
+ set: <#ident as #foundations::Set>::set,
vtable: #vtable_func,
field_id: |name|
<
- <#ident as #model::ElementFields>::Fields as ::std::str::FromStr
+ <#ident as #foundations::ElementFields>::Fields as ::std::str::FromStr
>::from_str(name).ok().map(|id| id as u8),
field_name: |id|
<
- <#ident as #model::ElementFields>::Fields as ::std::convert::TryFrom<u8>
- >::try_from(id).ok().map(<#ident as #model::ElementFields>::Fields::to_str),
+ <#ident as #foundations::ElementFields>::Fields as ::std::convert::TryFrom<u8>
+ >::try_from(id).ok().map(<#ident as #foundations::ElementFields>::Fields::to_str),
local_name: #local_name,
- scope: #eval::Lazy::new(|| #scope),
- params: #eval::Lazy::new(|| ::std::vec![#(#params),*])
+ scope: #foundations::Lazy::new(|| #scope),
+ params: #foundations::Lazy::new(|| ::std::vec![#(#params),*])
}
};
quote! {
- impl #model::NativeElement for #ident {
- fn data() -> &'static #model::NativeElementData {
- static DATA: #model::NativeElementData = #data;
+ impl #foundations::NativeElement for #ident {
+ fn data() -> &'static #foundations::NativeElementData {
+ static DATA: #foundations::NativeElementData = #data;
&DATA
}
- fn dyn_elem(&self) -> #model::Element {
- #model::Element::of::<Self>()
+ fn dyn_elem(&self) -> #foundations::Element {
+ #foundations::Element::of::<Self>()
}
fn dyn_hash(&self, mut hasher: &mut dyn ::std::hash::Hasher) {
<Self as ::std::hash::Hash>::hash(self, &mut hasher);
}
- fn dyn_eq(&self, other: &#model::Content) -> bool {
+ fn dyn_eq(&self, other: &#foundations::Content) -> bool {
if let Some(other) = other.to::<Self>() {
<Self as ::std::cmp::PartialEq>::eq(self, other)
} else {
@@ -957,7 +951,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
}
}
- fn dyn_clone(&self) -> ::std::sync::Arc<dyn #model::NativeElement> {
+ fn dyn_clone(&self) -> ::std::sync::Arc<dyn #foundations::NativeElement> {
::std::sync::Arc::new(Clone::clone(self))
}
@@ -983,27 +977,27 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
}
}
- fn label(&self) -> Option<#model::Label> {
+ fn label(&self) -> Option<#foundations::Label> {
#label
}
- fn set_label(&mut self, label: #model::Label) {
+ fn set_label(&mut self, label: #foundations::Label) {
#set_label
}
- fn location(&self) -> Option<#model::Location> {
+ fn location(&self) -> Option<::typst::introspection::Location> {
#location
}
- fn set_location(&mut self, location: #model::Location) {
+ fn set_location(&mut self, location: ::typst::introspection::Location) {
#set_location
}
- fn push_guard(&mut self, guard: #model::Guard) {
+ fn push_guard(&mut self, guard: #foundations::Guard) {
self.guards.push(guard);
}
- fn is_guarded(&self, guard: #model::Guard) -> bool {
+ fn is_guarded(&self, guard: #foundations::Guard) -> bool {
self.guards.contains(&guard)
}
@@ -1023,30 +1017,30 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
#prepared
}
- fn field(&self, id: u8) -> Option<::typst::eval::Value> {
- let id = <#ident as #model::ElementFields>::Fields::try_from(id).ok()?;
+ fn field(&self, id: u8) -> Option<#foundations::Value> {
+ let id = <#ident as #foundations::ElementFields>::Fields::try_from(id).ok()?;
match id {
- <#ident as #model::ElementFields>::Fields::Label => #label_field,
+ <#ident as #foundations::ElementFields>::Fields::Label => #label_field,
#(#field_matches)*
_ => None,
}
}
- fn fields(&self) -> Dict {
- let mut fields = Dict::new();
+ fn fields(&self) -> #foundations::Dict {
+ let mut fields = #foundations::Dict::new();
#(#field_dict)*
#(#field_opt_dict)*
fields
}
- fn set_field(&mut self, id: u8, value: Value) -> ::typst::diag::StrResult<()> {
- let id = <#ident as #model::ElementFields>::Fields::try_from(id)
+ fn set_field(&mut self, id: u8, value: #foundations::Value) -> ::typst::diag::StrResult<()> {
+ let id = <#ident as #foundations::ElementFields>::Fields::try_from(id)
.map_err(|_| ::ecow::eco_format!(#unknown_field, id))?;
match id {
#(#field_set_matches)*
#(#field_inherent_matches)*
#(#field_not_set_matches)*
- <#ident as #model::ElementFields>::Fields::Label => {
+ <#ident as #foundations::ElementFields>::Fields::Label => {
::typst::diag::bail!(#label_error);
}
}
@@ -1087,18 +1081,18 @@ fn create_construct_impl(element: &Elem) -> TokenStream {
.map(|field| &field.ident);
quote! {
- impl ::typst::model::Construct for #ident {
+ impl #foundations::Construct for #ident {
fn construct(
vm: &mut ::typst::eval::Vm,
- args: &mut ::typst::eval::Args,
- ) -> ::typst::diag::SourceResult<::typst::model::Content> {
+ args: &mut #foundations::Args,
+ ) -> ::typst::diag::SourceResult<#foundations::Content> {
#(#pre)*
let mut element = Self::new(#(#defaults),*);
#(#handlers)*
- Ok(::typst::model::Content::new(element))
+ Ok(#foundations::Content::new(element))
}
}
}
@@ -1119,12 +1113,12 @@ fn create_set_impl(element: &Elem) -> TokenStream {
});
quote! {
- impl ::typst::model::Set for #ident {
+ impl #foundations::Set for #ident {
fn set(
- vm: &mut Vm,
- args: &mut ::typst::eval::Args,
- ) -> ::typst::diag::SourceResult<::typst::model::Styles> {
- let mut styles = ::typst::model::Styles::new();
+ vm: &mut ::typst::eval::Vm,
+ args: &mut #foundations::Args,
+ ) -> ::typst::diag::SourceResult<#foundations::Styles> {
+ let mut styles = #foundations::Styles::new();
#(#handlers)*
Ok(styles)
}
@@ -1135,7 +1129,7 @@ fn create_set_impl(element: &Elem) -> TokenStream {
/// Creates the element's `Locatable` implementation.
fn create_locatable_impl(element: &Elem) -> TokenStream {
let ident = &element.ident;
- quote! { impl ::typst::model::Locatable for #ident {} }
+ quote! { impl ::typst::introspection::Locatable for #ident {} }
}
/// Creates the element's `PartialEq` implementation.
@@ -1159,12 +1153,12 @@ fn create_repr_impl(element: &Elem) -> TokenStream {
let ident = &element.ident;
let repr_format = format!("{}{{}}", element.name);
quote! {
- impl ::typst::eval::Repr for #ident {
+ impl #foundations::Repr for #ident {
fn repr(&self) -> ::ecow::EcoString {
- let fields = self.fields().into_iter()
- .map(|(name, value)| eco_format!("{}: {}", name, value.repr()))
+ let fields = #foundations::NativeElement::fields(self).into_iter()
+ .map(|(name, value)| ::ecow::eco_format!("{}: {}", name, value.repr()))
.collect::<Vec<_>>();
- ::ecow::eco_format!(#repr_format, ::typst::eval::repr::pretty_array_like(&fields, false))
+ ::ecow::eco_format!(#repr_format, #foundations::repr::pretty_array_like(&fields, false))
}
}
}
@@ -1185,7 +1179,7 @@ fn create_vtable_func(element: &Elem) -> TokenStream {
if id == ::std::any::TypeId::of::<dyn #capability>() {
let vtable = unsafe {
let dangling = ::std::ptr::NonNull::<#ident>::dangling().as_ptr() as *const dyn #capability;
- ::typst::model::fat::vtable(dangling)
+ ::typst::util::fat::vtable(dangling)
};
return Some(vtable);
}
@@ -1224,20 +1218,20 @@ fn create_param_info(field: &Field) -> TokenStream {
quote! {
|| {
let typed: #default_ty = #default;
- ::typst::eval::IntoValue::into_value(typed)
+ #foundations::IntoValue::into_value(typed)
}
}
}));
let ty = if *variadic {
- quote! { <#ty as ::typst::eval::Container>::Inner }
+ quote! { <#ty as #foundations::Container>::Inner }
} else {
quote! { #ty }
};
quote! {
- ::typst::eval::ParamInfo {
+ #foundations::ParamInfo {
name: #name,
docs: #docs,
- input: <#ty as ::typst::eval::Reflect>::input(),
+ input: <#ty as #foundations::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,
diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs
index 9040abf9..8537ac4f 100644
--- a/crates/typst-macros/src/func.rs
+++ b/crates/typst-macros/src/func.rs
@@ -1,4 +1,14 @@
-use super::*;
+use heck::ToKebabCase;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{parse_quote, Ident, Result};
+
+use crate::util::{
+ determine_name_and_title, documentation, foundations, has_attr, kw, parse_attr,
+ parse_flag, parse_key_value, parse_string, parse_string_array, quote_option,
+ validate_attrs,
+};
/// Expand the `#[func]` macro.
pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> {
@@ -191,8 +201,6 @@ fn parse_param(
/// Produce the function's definition.
fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
- let eval = quote! { ::typst::eval };
-
let Func { docs, vis, ident, .. } = func;
let item = rewrite_fn_item(item);
let ty = create_func_ty(func);
@@ -200,9 +208,9 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
let creator = if ty.is_some() {
quote! {
- impl #eval::NativeFunc for #ident {
- fn data() -> &'static #eval::NativeFuncData {
- static DATA: #eval::NativeFuncData = #data;
+ impl #foundations::NativeFunc for #ident {
+ fn data() -> &'static #foundations::NativeFuncData {
+ static DATA: #foundations::NativeFuncData = #data;
&DATA
}
}
@@ -211,8 +219,8 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
let ident_data = quote::format_ident!("{ident}_data");
quote! {
#[doc(hidden)]
- #vis fn #ident_data() -> &'static #eval::NativeFuncData {
- static DATA: #eval::NativeFuncData = #data;
+ #vis fn #ident_data() -> &'static #foundations::NativeFuncData {
+ static DATA: #foundations::NativeFuncData = #data;
&DATA
}
}
@@ -231,8 +239,6 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
/// Create native function data for the function.
fn create_func_data(func: &Func) -> TokenStream {
- let eval = quote! { ::typst::eval };
-
let Func {
ident,
name,
@@ -247,30 +253,30 @@ fn create_func_data(func: &Func) -> TokenStream {
} = func;
let scope = if *scope {
- quote! { <#ident as #eval::NativeScope>::scope() }
+ quote! { <#ident as #foundations::NativeScope>::scope() }
} else {
- quote! { #eval::Scope::new() }
+ quote! { #foundations::Scope::new() }
};
let closure = create_wrapper_closure(func);
let params = func.special.self_.iter().chain(&func.params).map(create_param_info);
let name = if *constructor {
- quote! { <#parent as #eval::NativeType>::NAME }
+ quote! { <#parent as #foundations::NativeType>::NAME }
} else {
quote! { #name }
};
quote! {
- #eval::NativeFuncData {
+ #foundations::NativeFuncData {
function: #closure,
name: #name,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
- scope: #eval::Lazy::new(|| #scope),
- params: #eval::Lazy::new(|| ::std::vec![#(#params),*]),
- returns: #eval::Lazy::new(|| <#returns as #eval::Reflect>::output()),
+ scope: #foundations::Lazy::new(|| #scope),
+ params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]),
+ returns: #foundations::Lazy::new(|| <#returns as #foundations::Reflect>::output()),
}
}
}
@@ -335,7 +341,7 @@ fn create_wrapper_closure(func: &Func) -> TokenStream {
#handlers
#finish
let output = #call;
- ::typst::eval::IntoResult::into_result(output, args.span)
+ #foundations::IntoResult::into_result(output, args.span)
}
}
}
@@ -346,7 +352,7 @@ fn create_param_info(param: &Param) -> TokenStream {
let positional = !named;
let required = !named && default.is_none();
let ty = if *variadic || (*named && default.is_none()) {
- quote! { <#ty as ::typst::eval::Container>::Inner }
+ quote! { <#ty as #foundations::Container>::Inner }
} else {
quote! { #ty }
};
@@ -354,15 +360,15 @@ fn create_param_info(param: &Param) -> TokenStream {
quote! {
|| {
let typed: #ty = #default;
- ::typst::eval::IntoValue::into_value(typed)
+ #foundations::IntoValue::into_value(typed)
}
}
}));
quote! {
- ::typst::eval::ParamInfo {
+ #foundations::ParamInfo {
name: #name,
docs: #docs,
- input: <#ty as ::typst::eval::Reflect>::input(),
+ input: <#ty as #foundations::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,
diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs
index 87048ee5..28d02a6e 100644
--- a/crates/typst-macros/src/lib.rs
+++ b/crates/typst-macros/src/lib.rs
@@ -5,22 +5,15 @@ extern crate proc_macro;
#[macro_use]
mod util;
mod cast;
+mod category;
mod elem;
mod func;
mod scope;
mod symbols;
mod ty;
-use heck::*;
use proc_macro::TokenStream as BoundaryStream;
-use proc_macro2::TokenStream;
-use quote::quote;
-use syn::ext::IdentExt;
-use syn::parse::{Parse, ParseStream, Parser};
-use syn::punctuated::Punctuated;
-use syn::{parse_quote, DeriveInput, Ident, Result, Token};
-
-use self::util::*;
+use syn::DeriveInput;
/// Makes a native Rust function usable as a Typst function.
///
@@ -190,9 +183,6 @@ pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
/// - `#[synthesized]`: The field cannot be specified in a constructor or set
/// rule. Instead, it is added to an element before its show rule runs
/// through the `Synthesize` trait.
-/// - `#[variant]`: Allows setting the ID of a field's variant. This is used
-/// for fields that are accessed in `typst` and not `typst-library`. It gives
-/// the field a stable ID that can be used to access it.
/// - `#[ghost]`: Allows creating fields that are only present in the style chain,
/// this means that they *cannot* be accessed by the user, they cannot be set
/// on an individual instantiated element, and must be set via the style chain.
@@ -256,6 +246,15 @@ pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
.into()
}
+/// Defines a category of definitions.
+#[proc_macro_attribute]
+pub fn category(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as syn::Item);
+ category::category(stream.into(), item)
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type.
///
/// - `Reflect` makes Typst's runtime aware of the type's characteristics.
diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs
index 4aa17c6c..07a0efe0 100644
--- a/crates/typst-macros/src/scope.rs
+++ b/crates/typst-macros/src/scope.rs
@@ -1,6 +1,9 @@
use heck::ToKebabCase;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{parse_quote, Result};
-use super::*;
+use crate::util::{foundations, BareType};
/// Expand the `#[scope]` macro.
pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
@@ -8,7 +11,6 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
bail!(item, "expected module or impl item");
};
- let eval = quote! { ::typst::eval };
let self_ty = &item.self_ty;
let mut definitions = vec![];
@@ -43,13 +45,13 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
Ok(quote! {
#base
- impl #eval::NativeScope for #self_ty {
- fn constructor() -> ::std::option::Option<&'static #eval::NativeFuncData> {
+ impl #foundations::NativeScope for #self_ty {
+ fn constructor() -> ::std::option::Option<&'static #foundations::NativeFuncData> {
#constructor
}
- fn scope() -> #eval::Scope {
- let mut scope = #eval::Scope::deduplicating();
+ fn scope() -> #foundations::Scope {
+ let mut scope = #foundations::Scope::deduplicating();
#(#definitions;)*
scope
}
@@ -92,7 +94,7 @@ fn handle_fn(self_ty: &syn::Type, item: &mut syn::ImplItemFn) -> Result<FnKind>
}
syn::Meta::List(list) => {
let tokens = &list.tokens;
- let meta: super::func::Meta = syn::parse2(tokens.clone())?;
+ let meta: crate::func::Meta = syn::parse2(tokens.clone())?;
list.tokens = quote! { #tokens, parent = #self_ty };
if meta.constructor {
return Ok(FnKind::Constructor(quote! { Some(#self_ty::#ident_data()) }));
@@ -135,7 +137,7 @@ fn rewrite_primitive_base(item: &syn::ItemImpl, ident: &syn::Ident) -> TokenStre
let ident_data = quote::format_ident!("{}_data", sig.ident);
sigs.push(quote! { #sig; });
sigs.push(quote! {
- fn #ident_data() -> &'static ::typst::eval::NativeFuncData;
+ fn #ident_data() -> &'static #foundations::NativeFuncData;
});
}
diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs
index 8ab47f08..2ddb922f 100644
--- a/crates/typst-macros/src/symbols.rs
+++ b/crates/typst-macros/src/symbols.rs
@@ -1,4 +1,9 @@
-use super::*;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::ext::IdentExt;
+use syn::parse::{Parse, ParseStream, Parser};
+use syn::punctuated::Punctuated;
+use syn::{Ident, Result, Token};
/// Expand the `symbols!` macro.
pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
@@ -7,7 +12,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
let pairs = list.iter().map(|symbol| {
let name = symbol.name.to_string();
let kind = match &symbol.kind {
- Kind::Single(c) => quote! { typst::eval::Symbol::single(#c), },
+ Kind::Single(c) => quote! { ::typst::symbols::Symbol::single(#c), },
Kind::Multiple(variants) => {
let variants = variants.iter().map(|variant| {
let name = &variant.name;
@@ -15,7 +20,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
quote! { (#name, #c) }
});
quote! {
- typst::eval::Symbol::list(&[#(#variants),*])
+ ::typst::symbols::Symbol::list(&[#(#variants),*])
}
}
};
diff --git a/crates/typst-macros/src/ty.rs b/crates/typst-macros/src/ty.rs
index df60e7bb..23f818bd 100644
--- a/crates/typst-macros/src/ty.rs
+++ b/crates/typst-macros/src/ty.rs
@@ -1,6 +1,12 @@
-use syn::Attribute;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Attribute, Ident, Result};
-use super::*;
+use crate::util::{
+ determine_name_and_title, documentation, foundations, kw, parse_flag, parse_string,
+ parse_string_array, BareType,
+};
/// Expand the `#[ty]` macro.
pub fn ty(stream: TokenStream, item: syn::Item) -> Result<TokenStream> {
@@ -68,44 +74,42 @@ fn parse(meta: Meta, ident: Ident, attrs: &[Attribute]) -> Result<Type> {
/// Produce the output of the macro.
fn create(ty: &Type, item: Option<&syn::Item>) -> TokenStream {
- let eval = quote! { ::typst::eval };
-
let Type {
ident, name, long, title, docs, keywords, scope, ..
} = ty;
let constructor = if *scope {
- quote! { <#ident as #eval::NativeScope>::constructor() }
+ quote! { <#ident as #foundations::NativeScope>::constructor() }
} else {
quote! { None }
};
let scope = if *scope {
- quote! { <#ident as #eval::NativeScope>::scope() }
+ quote! { <#ident as #foundations::NativeScope>::scope() }
} else {
- quote! { #eval::Scope::new() }
+ quote! { #foundations::Scope::new() }
};
let data = quote! {
- #eval::NativeTypeData {
+ #foundations::NativeTypeData {
name: #name,
long_name: #long,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
- constructor: #eval::Lazy::new(|| #constructor),
- scope: #eval::Lazy::new(|| #scope),
+ constructor: #foundations::Lazy::new(|| #constructor),
+ scope: #foundations::Lazy::new(|| #scope),
}
};
quote! {
#item
- impl #eval::NativeType for #ident {
+ impl #foundations::NativeType for #ident {
const NAME: &'static str = #name;
- fn data() -> &'static #eval::NativeTypeData {
- static DATA: #eval::NativeTypeData = #data;
+ fn data() -> &'static #foundations::NativeTypeData {
+ static DATA: #foundations::NativeTypeData = #data;
&DATA
}
}
diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs
index 790db3e1..32c0aa4e 100644
--- a/crates/typst-macros/src/util.rs
+++ b/crates/typst-macros/src/util.rs
@@ -1,8 +1,9 @@
-use quote::ToTokens;
+use heck::{ToKebabCase, ToTitleCase};
+use proc_macro2::TokenStream;
+use quote::{quote, ToTokens};
+use syn::parse::{Parse, ParseStream};
use syn::token::Token;
-use syn::Attribute;
-
-use super::*;
+use syn::{Attribute, Ident, Result, Token};
/// Return an error at the given item.
macro_rules! bail {
@@ -199,6 +200,16 @@ impl<T: Parse> Parse for Array<T> {
}
}
+/// Shorthand for `::typst::foundations`.
+#[allow(non_camel_case_types)]
+pub struct foundations;
+
+impl quote::ToTokens for foundations {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ quote! { ::typst::foundations }.to_tokens(tokens);
+ }
+}
+
/// For parsing attributes of the form:
/// #[attr(
/// statement;
@@ -220,15 +231,6 @@ impl Parse for BlockWithReturn {
}
}
-pub mod kw {
- syn::custom_keyword!(name);
- syn::custom_keyword!(title);
- syn::custom_keyword!(scope);
- syn::custom_keyword!(constructor);
- syn::custom_keyword!(keywords);
- syn::custom_keyword!(parent);
-}
-
/// Parse a bare `type Name;` item.
pub struct BareType {
pub attrs: Vec<Attribute>,
@@ -239,7 +241,7 @@ pub struct BareType {
impl Parse for BareType {
fn parse(input: ParseStream) -> Result<Self> {
- Ok(BareType {
+ Ok(Self {
attrs: input.call(Attribute::parse_outer)?,
type_token: input.parse()?,
ident: input.parse()?,
@@ -247,3 +249,12 @@ impl Parse for BareType {
})
}
}
+
+pub mod kw {
+ syn::custom_keyword!(name);
+ syn::custom_keyword!(title);
+ syn::custom_keyword!(scope);
+ syn::custom_keyword!(constructor);
+ syn::custom_keyword!(keywords);
+ syn::custom_keyword!(parent);
+}
diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs
index e23e7cbe..17c4686a 100644
--- a/crates/typst-pdf/src/color.rs
+++ b/crates/typst-pdf/src/color.rs
@@ -1,7 +1,7 @@
use once_cell::sync::Lazy;
use pdf_writer::types::DeviceNSubtype;
use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref};
-use typst::geom::{Color, ColorSpace, Paint};
+use typst::visualize::{Color, ColorSpace, Paint};
use crate::deflate;
use crate::page::{PageContext, Transforms};
diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs
index a358f9ea..ce3913f7 100644
--- a/crates/typst-pdf/src/font.rs
+++ b/crates/typst-pdf/src/font.rs
@@ -5,7 +5,7 @@ use ecow::{eco_format, EcoString};
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
use pdf_writer::{Filter, Finish, Name, Rect, Str};
use ttf_parser::{name_id, GlyphId, Tag};
-use typst::font::Font;
+use typst::text::Font;
use typst::util::SliceExt;
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs
index ecb01ee6..5e7e5f3d 100644
--- a/crates/typst-pdf/src/gradient.rs
+++ b/crates/typst-pdf/src/gradient.rs
@@ -5,9 +5,10 @@ use ecow::{eco_format, EcoString};
use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType};
use pdf_writer::writers::StreamShadingType;
use pdf_writer::{Filter, Finish, Name, Ref};
-use typst::geom::{
- Abs, Angle, Color, ColorSpace, ConicGradient, Gradient, Numeric, Point, Quadrant,
- Ratio, Relative, Transform, WeightedColor,
+use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform};
+use typst::util::Numeric;
+use typst::visualize::{
+ Color, ColorSpace, ConicGradient, Gradient, GradientRelative, WeightedColor,
};
use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor};
@@ -301,8 +302,8 @@ fn register_gradient(
transforms.size.y = Abs::pt(1.0);
}
let size = match gradient.unwrap_relative(on_text) {
- Relative::Self_ => transforms.size,
- Relative::Parent => transforms.container_size,
+ GradientRelative::Self_ => transforms.size,
+ GradientRelative::Parent => transforms.container_size,
};
let (offset_x, offset_y) = match gradient {
@@ -316,8 +317,8 @@ fn register_gradient(
let rotation = gradient.angle().unwrap_or_else(Angle::zero);
let transform = match gradient.unwrap_relative(on_text) {
- Relative::Self_ => transforms.transform,
- Relative::Parent => transforms.container_transform,
+ GradientRelative::Self_ => transforms.transform,
+ GradientRelative::Parent => transforms.container_transform,
};
let scale_offset = match gradient {
diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs
index 63329b42..2e99eef5 100644
--- a/crates/typst-pdf/src/image.rs
+++ b/crates/typst-pdf/src/image.rs
@@ -3,9 +3,10 @@ use std::io::Cursor;
use image::{DynamicImage, GenericImageView, Rgba};
use pdf_writer::{Chunk, Filter, Finish, Ref};
-use typst::geom::ColorSpace;
-use typst::image::{Image, ImageKind, RasterFormat, RasterImage, SvgImage};
use typst::util::Deferred;
+use typst::visualize::{
+ ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage,
+};
use crate::{deflate, PdfContext};
diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs
index 628129b6..c753315c 100644
--- a/crates/typst-pdf/src/lib.rs
+++ b/crates/typst-pdf/src/lib.rs
@@ -16,13 +16,13 @@ use base64::Engine;
use ecow::{eco_format, EcoString};
use pdf_writer::types::Direction;
use pdf_writer::{Finish, Name, Pdf, Ref, TextStr};
-use typst::doc::{Document, Lang};
-use typst::eval::Datetime;
-use typst::font::Font;
-use typst::geom::{Abs, Dir, Em};
-use typst::image::Image;
-use typst::model::Introspector;
+use typst::foundations::Datetime;
+use typst::introspection::Introspector;
+use typst::layout::{Abs, Dir, Em};
+use typst::model::Document;
+use typst::text::{Font, Lang};
use typst::util::Deferred;
+use typst::visualize::Image;
use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter};
use crate::color::ColorSpaces;
diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs
index 4fd072b6..aafaa5b9 100644
--- a/crates/typst-pdf/src/outline.rs
+++ b/crates/typst-pdf/src/outline.rs
@@ -1,9 +1,9 @@
use std::num::NonZeroUsize;
use pdf_writer::{Finish, Ref, TextStr};
-use typst::eval::{item, Smart};
-use typst::geom::Abs;
-use typst::model::Content;
+use typst::foundations::{Content, NativeElement, Smart};
+use typst::layout::Abs;
+use typst::model::HeadingElem;
use crate::{AbsExt, PdfContext};
@@ -18,7 +18,7 @@ pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
// Therefore, its next descendant must be added at its level, which is
// enforced in the manner shown below.
let mut last_skipped_level = None;
- for heading in ctx.introspector.query(&item!(heading_elem).select()).iter() {
+ for heading in ctx.introspector.query(&HeadingElem::elem().select()).iter() {
let leaf = HeadingNode::leaf((**heading).clone());
if leaf.bookmarked {
diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs
index 74b32302..545380da 100644
--- a/crates/typst-pdf/src/page.rs
+++ b/crates/typst-pdf/src/page.rs
@@ -8,16 +8,17 @@ use pdf_writer::types::{
};
use pdf_writer::writers::PageLabel;
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr};
-use typst::doc::{
- Destination, Frame, FrameItem, GroupItem, Meta, PdfPageLabel, PdfPageLabelStyle,
- TextItem,
+use typst::introspection::Meta;
+use typst::layout::{
+ Abs, Em, Frame, FrameItem, GroupItem, PdfPageLabel, PdfPageLabelStyle, Point, Ratio,
+ Size, Transform,
};
-use typst::font::Font;
-use typst::geom::{
- self, Abs, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint, Point,
- Ratio, Shape, Size, Transform,
+use typst::model::Destination;
+use typst::text::{Font, TextItem};
+use typst::util::Numeric;
+use typst::visualize::{
+ FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
};
-use typst::image::Image;
use crate::color::PaintEncode;
use crate::extg::ExtGState;
@@ -581,7 +582,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
adjustment = Em::zero();
}
- let cid = super::font::glyph_cid(&text.font, glyph.id);
+ let cid = crate::font::glyph_cid(&text.font, glyph.id);
encoded.push((cid >> 8) as u8);
encoded.push((cid & 0xff) as u8);
@@ -656,16 +657,16 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
}
/// Encode a bezier path into the content stream.
-fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) {
+fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &Path) {
for elem in &path.0 {
match elem {
- geom::PathItem::MoveTo(p) => {
+ PathItem::MoveTo(p) => {
ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
}
- geom::PathItem::LineTo(p) => {
+ PathItem::LineTo(p) => {
ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
}
- geom::PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to(
+ PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to(
x + p1.x.to_f32(),
y + p1.y.to_f32(),
x + p2.x.to_f32(),
@@ -673,7 +674,7 @@ fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) {
x + p3.x.to_f32(),
y + p3.y.to_f32(),
),
- geom::PathItem::ClosePath => ctx.content.close_path(),
+ PathItem::ClosePath => ctx.content.close_path(),
};
}
}
diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs
index 0b6edf00..251f647a 100644
--- a/crates/typst-render/src/lib.rs
+++ b/crates/typst-render/src/lib.rs
@@ -9,13 +9,15 @@ use pixglyph::Bitmap;
use resvg::tiny_skia::IntRect;
use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
-use typst::doc::{Frame, FrameItem, FrameKind, GroupItem, Meta, TextItem};
-use typst::font::Font;
-use typst::geom::{
- self, Abs, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
- PathItem, Point, Ratio, Relative, Shape, Size, Transform,
+use typst::introspection::Meta;
+use typst::layout::{
+ Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
+};
+use typst::text::{Font, TextItem};
+use typst::visualize::{
+ Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageKind, LineCap,
+ LineJoin, Paint, Path, PathItem, RasterFormat, Shape,
};
-use typst::image::{Image, ImageKind, RasterFormat};
use usvg::{NodeExt, TreeParsing};
/// Export a frame into a raster image.
@@ -634,7 +636,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
}
/// Convert a Typst path into a tiny-skia path.
-fn convert_path(path: &geom::Path) -> Option<sk::Path> {
+fn convert_path(path: &Path) -> Option<sk::Path> {
let mut builder = sk::PathBuilder::new();
for elem in &path.0 {
match elem {
@@ -773,13 +775,13 @@ impl<'a> GradientSampler<'a> {
) -> Self {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
- Relative::Self_ => item_size,
- Relative::Parent => state.size,
+ GradientRelative::Self_ => item_size,
+ GradientRelative::Parent => state.size,
};
let fill_transform = match relative {
- Relative::Self_ => sk::Transform::identity(),
- Relative::Parent => state.container_transform.invert().unwrap(),
+ GradientRelative::Self_ => sk::Transform::identity(),
+ GradientRelative::Parent => state.container_transform.invert().unwrap(),
};
Self {
@@ -857,13 +859,13 @@ fn to_sk_paint<'a>(
Paint::Gradient(gradient) => {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
- Relative::Self_ => item_size,
- Relative::Parent => state.size,
+ GradientRelative::Self_ => item_size,
+ GradientRelative::Parent => state.size,
};
let fill_transform = match relative {
- Relative::Self_ => fill_transform.unwrap_or_default(),
- Relative::Parent => state
+ GradientRelative::Self_ => fill_transform.unwrap_or_default(),
+ GradientRelative::Parent => state
.container_transform
.post_concat(state.transform.invert().unwrap()),
};
diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs
index ed34f3dc..7d3a773d 100644
--- a/crates/typst-svg/src/lib.rs
+++ b/crates/typst-svg/src/lib.rs
@@ -6,16 +6,18 @@ use std::io::Read;
use base64::Engine;
use ecow::{eco_format, EcoString};
use ttf_parser::{GlyphId, OutlineBuilder};
-use typst::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
-use typst::eval::Repr;
-use typst::font::Font;
-use typst::geom::{
- self, Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin,
- Paint, PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size,
- Transform,
+use typst::foundations::Repr;
+use typst::layout::{
+ Abs, Angle, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Quadrant, Ratio,
+ Size, Transform,
};
-use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
+use typst::text::{Font, TextItem};
use typst::util::hash128;
+use typst::visualize::{
+ Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageFormat,
+ LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RatioOrAngle, Shape,
+ VectorFormat,
+};
use xmlwriter::XmlWriter;
/// The number of segments in a conic gradient.
@@ -432,8 +434,8 @@ impl SVGRenderer {
};
match gradient.unwrap_relative(true) {
- Relative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
- Relative::Parent => Transform::scale(
+ GradientRelative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
+ GradientRelative::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
@@ -488,11 +490,11 @@ impl SVGRenderer {
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
- Relative::Self_ => Transform::scale(
+ GradientRelative::Self_ => Transform::scale(
Ratio::new(shape_size.x.to_pt()),
Ratio::new(shape_size.y.to_pt()),
),
- Relative::Parent => Transform::scale(
+ GradientRelative::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
@@ -517,8 +519,8 @@ impl SVGRenderer {
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
- Relative::Self_ => shape_size,
- Relative::Parent => state.size,
+ GradientRelative::Self_ => shape_size,
+ GradientRelative::Parent => state.size,
}
} else {
shape_size
@@ -1047,7 +1049,7 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
builder.0
}
-fn convert_path(path: &geom::Path) -> EcoString {
+fn convert_path(path: &Path) -> EcoString {
let mut builder = SvgPathBuilder::default();
for item in &path.0 {
match item {
diff --git a/crates/typst-syntax/src/reparser.rs b/crates/typst-syntax/src/reparser.rs
index 1374bde0..1b7dc42f 100644
--- a/crates/typst-syntax/src/reparser.rs
+++ b/crates/typst-syntax/src/reparser.rs
@@ -11,7 +11,7 @@ use crate::{
/// ultimately reparsed.
///
/// The high-level API for this function is
-/// [`Source::edit`](super::Source::edit).
+/// [`Source::edit`](crate::Source::edit).
pub fn reparse(
root: &mut SyntaxNode,
text: &str,
diff --git a/crates/typst-syntax/src/span.rs b/crates/typst-syntax/src/span.rs
index 5b8fd693..14e5e216 100644
--- a/crates/typst-syntax/src/span.rs
+++ b/crates/typst-syntax/src/span.rs
@@ -7,7 +7,7 @@ use crate::FileId;
/// A unique identifier for a syntax node.
///
/// This is used throughout the compiler to track which source section an error
-/// or element stems from. Can be [mapped back](super::Source::range) to a byte
+/// or element stems from. Can be [mapped back](crate::Source::range) to a byte
/// range for user facing display.
///
/// During editing, the span values stay mostly stable, even for nodes behind an
@@ -76,7 +76,7 @@ impl Span {
Some(FileId::from_raw(bits))
}
- /// The unique number of the span within its [`Source`](super::Source).
+ /// The unique number of the span within its [`Source`](crate::Source).
pub const fn number(self) -> u64 {
self.0.get() & ((1 << Self::BITS) - 1)
}
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index 53512d90..c9c19f8e 100644
--- a/crates/typst/Cargo.toml
+++ b/crates/typst/Cargo.toml
@@ -18,14 +18,26 @@ bench = false
[dependencies]
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
+az = { workspace = true }
bitflags = { workspace = true }
+chinese-number = { workspace = true }
+ciborium = { workspace = true }
comemo = { workspace = true }
+csv = { workspace = true }
ecow = { workspace = true}
fontdb = { workspace = true }
+hayagriva = { workspace = true }
+hypher = { workspace = true }
+icu_properties = { workspace = true }
+icu_provider = { workspace = true }
+icu_provider_adapters = { workspace = true }
+icu_provider_blob = { workspace = true }
+icu_segmenter = { workspace = true }
image = { workspace = true }
indexmap = { workspace = true }
kurbo = { workspace = true }
lasso = { workspace = true }
+lipsum = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
palette = { workspace = true }
@@ -34,13 +46,19 @@ regex = { workspace = true }
roxmltree = { workspace = true }
rustybuzz = { workspace = true }
serde = { workspace = true }
+serde_json = { workspace = true }
+serde_yaml = { workspace = true }
siphasher = { workspace = true }
smallvec = { workspace = true }
+syntect = { workspace = true }
time = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
ttf-parser = { workspace = true }
+typed-arena = { workspace = true }
+unicode-bidi = { workspace = true }
unicode-math-class = { workspace = true }
+unicode-script = { workspace = true }
unicode-segmentation = { workspace = true }
usvg = { workspace = true }
wasmi = { workspace = true }
diff --git a/crates/typst-library/assets/cj_linebreak_data.postcard b/crates/typst/assets/cj_linebreak_data.postcard
index 910dd167..910dd167 100644
--- a/crates/typst-library/assets/cj_linebreak_data.postcard
+++ b/crates/typst/assets/cj_linebreak_data.postcard
Binary files differ
diff --git a/crates/typst-library/assets/icudata.postcard b/crates/typst/assets/icudata.postcard
index a1fdbd48..a1fdbd48 100644
--- a/crates/typst-library/assets/icudata.postcard
+++ b/crates/typst/assets/icudata.postcard
Binary files differ
diff --git a/crates/typst-library/assets/syntect.bin b/crates/typst/assets/syntect.bin
index 043602a4..043602a4 100644
--- a/crates/typst-library/assets/syntect.bin
+++ b/crates/typst/assets/syntect.bin
Binary files differ
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(')');
}