diff --git a/packages/preview/typsy/0.2.1/LICENSE b/packages/preview/typsy/0.2.1/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/preview/typsy/0.2.1/README.md b/packages/preview/typsy/0.2.1/README.md
new file mode 100644
index 0000000000..9224ff13ec
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/README.md
@@ -0,0 +1,221 @@
+
typsy
+Classes/structs, pattern matching, safe counters... and more!
Your one-stop library for programming tools not already in core Typst.
+
+---
+
+_Pronounced 'tipsy', because I think that's funny and it's a nice pun on 'Typst'._ 😄
+
+Provides tools for programming geeks:
+
+- classes (i.e. structs, custom types)
+- pattern matching
+- enums
+- safe `counter`s and `state`s (no need to choose a unique string)
+- trees-of-counters (i.e. subcounters)
+- string formatting
+- namespaces of objects that can be mutually referential
+- runtime type checking
+
+## Installation
+
+Typst will autodownload packages on import:
+```typst
+#import "@preview/typsy:0.2.1"
+```
+
+## What's in the box?
+
+### Classes
+
+Classes with fields and methods:
+```typst
+#import "@preview/typsy:0.2.1": class, Int
+
+#let Adder = class(
+ fields: (x: Int),
+ methods: (
+ add: (self, y) => {self.x + y}
+ )
+)
+#let add_three = (Adder.new)(x: 3)
+#let five = (add_three.add)(2)
+```
+
+### Pattern-matching
+
+Simple type checking:
+```typst
+#import "@preview/typsy:0.2.1": Array, Int, matches
+
+// Fixed-length case.
+#matches(Array(Int, Int), (3, 4)) // true
+// Variable-length case.
+#matches(Array(..Int), (3, 4, 5, "not an int")) // false
+```
+
+More complicated match-case statements:
+```typst
+#import "@preview/typsy:0.2.1": Arguments, Int, Str, case, match, matches
+
+// Option 1: if/else
+#let fn-with-multiple-signatures(..args) = {
+ if matches(Arguments(Int), args) {
+ // ...
+ } else if matches(Arguments(Str), args) {
+ // ...
+ } else if matches(Arguments(Str, level: Int), args) {
+ // ...
+ } else {
+ panic
+ }
+}
+
+// Option 2: match/case
+#let fn-with-multiple-signatures(..args) = {
+ match(args,
+ case(Arguments(Int), ()=>{
+ // ...
+ }),
+ case(Arguments(Str), ()=>{
+ // ...
+ }),
+ case(Arguments(Str, level: Int), ()=>{
+ // ...
+ }),
+ )
+}
+```
+
+Observe the capitalisation. All patterns are capitalised to distinguish them from their corresponding type.
+
+### Enums
+
+Also using the same pattern-matching capabilities as above:
+```typst
+#import "@preview/typsy:0.2.1": case, class, enumeration, match, Int
+
+#let Shape = enumeration(
+ Rectangle: class(fields: (height: Int, width: Int)),
+ Circle: class(fields: (radius: Int)),
+)
+#let area(x) = {
+ match(x,
+ case(Shape.Rectangle, ()=>{
+ x.height * x.width
+ }),
+ case(Shape.Circle, ()=>{
+ calc.pi * calc.pow(x.radius, 2)
+ }),
+ )
+}
+```
+
+### Safe counters and states
+
+Counters without needing to cross your fingers and hope that you're using a unique string each time:
+```typst
+#import "@preview/typsy:0.2.1": safe-counter
+
+#let my-counter1 = safe-counter(()=>{})
+#let my-counter2 = safe-counter(()=>{})
+// ...these are different counters!
+// (All anonymous functions have different identities to the compiler.)
+```
+
+Likewise safe states:
+```typst
+#import "@preview/typsy:0.2.1": safe-state
+#let my-state1 = safe-state(()=>{}, "hello")
+#let my-state2 = safe-state(()=>{}, "world")
+// These are different states. The second argument is the default value.
+```
+
+### Tree counters / subcounters
+
+Create trees of counters, including using existing counters as starting points. This is particularly useful for creating theorem counters that increment with the heading.
+```typst
+#import "@preview/typsy:0.2.1": tree-counter
+
+// Set up counters
+#let heading-counter = tree-counter(heading, level: 1)
+#let theorem-counter = (heading-counter.subcounter)(()=>{}) // uses safe-counter internally!
+#let corollary-counter = (theorem-counter.subcounter)(()=>{})
+
+// Usage
+#set heading(numbering: "1")
+#let theorem(doc) = [Theorem #(theorem-counter.take)(): #doc]
+#let corollary(doc) = [Corollary #(corollary-counter.take)(): #doc]
+= First Section
+#theorem[Let ...] // Theorem 1.1: Let ...
+#theorem[Let ...] // Theorem 1.2: Let ...
+#corollary[Let ...] // Corollary 1.2.1: Let ...
+= Second Section
+#theorem[Let ...] // Theorem 2.1: Let ...
+```
+
+### String formatting
+
+Rust-like string formatting:
+```typst
+#import "@preview/typsy:0.2.1": fmt, panic-fmt
+
+#let msg = fmt("Invalid input `{}`, expected `{}`.", foo, bar)
+
+// shorthand for `panic(fmt(...))`
+#panic-fmt("Invalid input `{}`, expected `{}`.", foo, bar)
+```
+
+### Runtime type checking
+
+Wrap functions to check their inputs and outputs. This builds on top of the pattern-matching capablities above.
+```typst
+#import "@preview/typsy:0.2.1": Arguments, typecheck
+
+#let add_integers = typecheck(Arguments(Int, Int), Int, (x, y) => x + y)
+#let five = add_integers(2, 3) // ok
+#add_integers("hello ", "world") // panic!
+```
+
+### Namespaces of mutually-referential objects
+
+Build a namespace by providing lambda functions which return their object. Access any object in a namespace via `ns(object-name)`:
+```typst
+#import "@preview/typsy:0.2.1": namespace
+
+#let ns = namespace(
+ foo: ns => {
+ let foo(x) = if x == 0 {"FOO"} else {ns("bar")(x - 1)}
+ foo
+ },
+ bar: ns => {
+ let bar(x) = if x == 0 {"BAR"} else {ns("foo")(x - 1)}
+ bar
+ },
+)
+#let foo = ns("foo")
+#assert.eq(foo(3), "BAR")
+#assert.eq(foo(4), "FOO")
+```
+For example, this can be used to implement mutually-recursive functions.
+
+## Documentation
+
+All objects have detailed docstrings indicating their usage; see those for details.
+
+The examples above demonstrate nearly every object in the public API. In addition to those above, the list of patterns that can be used for pattern-matching are:
+```typst
+Any, Arguments, Array, Bool, Bytes, Class, Content, Counter, Datetime,
+Decimal, Dictionary, Duration, Float, Function, Int, Label, Literal,
+Location, Module, Named, Never, None, Pattern, Pos, Ratio, Refine,
+Regex, Selector, State, Str, Symbol, Type, Union, Version
+```
+(They are capitalised to distinguish them from the underlying type.)
+
+## FAQ
+
+**Similar libraries:**
+
+- [elembic](https://github.com/pgbiel/elembic) offers a very different way to create custom classes.
+- [valkyrie](https://github.com/typst-community/valkyrie) offers object parsing that is somewhat similar to our type matching.
+- [headcount](https://github.com/jbirnick/typst-headcount) and [rich-counters](https://typst.app/universe/package/rich-counters/) also offer tree counters. (Though I find our approach a bit simpler, and safer due to our `()=>{}`-using safe counters.)
+- [oxifmt](https://github.com/PgBiel/typst-oxifmt) offers Rust-like string formatting. Theirs is far more developed and better than what we have; I just like avoiding dependencies.
diff --git a/packages/preview/typsy/0.2.1/src/classes.typ b/packages/preview/typsy/0.2.1/src/classes.typ
new file mode 100644
index 0000000000..f2dfa7add0
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/classes.typ
@@ -0,0 +1,308 @@
+#import "./format.typ": panic-fmt
+#import "./match.typ": Any, Class, Dictionary, Function, Int, Literal, Pattern, Str, matches, pattern-repr
+
+#let _checktype(name, value, pattern) = {
+ if not matches(pattern, value) {
+ panic-fmt(
+ "For `{}`, expected `{}`, received `{}` of type `{}`.",
+ name,
+ pattern-repr(pattern),
+ repr(value),
+ repr(type(value)),
+ )
+ }
+}
+
+/// - fn (function): str->any
+/// - names (array):
+/// -> dictionary str=>any
+#let _fn2dict(fn, names) = {
+ let out = (:)
+ for name in names {
+ out.insert(name, fn(name))
+ }
+ out
+}
+
+#let _make_cls(new, name, fields, methods, tag) = {
+ let out = (new: new, name: name, fields: fields, methods: methods, tag: tag, __typsy_sentinel_is_class: true)
+ // Make it possible to use classes in pattern-matching.
+ // We inspect specifically the `new` field as that should be enough to get uniqueness; in particular it closes over
+ // `tag`.
+ out + Dictionary(meta: Dictionary(cls: Dictionary(new: Literal(new), ..Any), ..Any), ..Any)
+}
+
+#let _class_or_namespace(name: none, fields: none, methods: none, tag: none, call_on_dict: none) = {
+ if name != none {
+ _checktype("name", name, Str)
+ }
+ _checktype("fields", fields, Dictionary(..Any))
+ _checktype("methods", methods, Dictionary(..Any))
+ let methods_keys = methods.keys()
+ let reserved_fields = ("meta",)
+ for (argname, pattern) in fields.pairs() {
+ _checktype(argname, argname, Str)
+ if type(pattern) == type {
+ panic-fmt(
+ "For `{}`, received a type annotation `{}`, but expected a pattern. For example, you should write `class(fields: (x: Int))` rather than `class(fields: (x: int))`. This was a breaking change between typsy:0.1.0 and typsy:0.2.0",
+ argname,
+ repr(pattern),
+ )
+ }
+ _checktype(argname, pattern, Pattern)
+ if methods_keys.contains(argname) {
+ panic-fmt("`{}` is present in both `fields` and `methods`", argname)
+ }
+ if reserved_fields.contains(argname) {
+ panic-fmt("`{}` is reserved and cannot be used in `fields`.", argname)
+ }
+ }
+ for (methodname, method) in methods.pairs() {
+ _checktype(methodname, methodname, Str)
+ _checktype(methodname, method, Function)
+ if reserved_fields.contains(methodname) {
+ panic-fmt("`{}` is reserved and cannot be used in `methods`.", methodname)
+ }
+ }
+ let new(..init_args) = {
+ if init_args.pos().len() != 0 {
+ panic-fmt("Do not call type constructors with positional arguments. Got `{}`", repr(init_args.pos()))
+ }
+
+ let self_dict = (:)
+ for (initname, value) in init_args.named().pairs() {
+ let expected = fields.at(initname, default: auto)
+ if expected == auto {
+ panic-fmt("Got unexpected init argument `{}`.", initname)
+ }
+ _checktype(initname, value, expected)
+ self_dict.insert(initname, value)
+ }
+ for (initname, method) in methods.pairs() {
+ self_dict.insert(initname, method)
+ }
+ let repr_pieces = ()
+ if name != none {
+ repr_pieces.push(name)
+ }
+ repr_pieces.push(repr(self_dict))
+ let meta = (
+ // Provide `cls` to allow easy self-recursion.
+ // Mutual recursion should be handled by using a `namespace`.
+ cls: _make_cls(new, name, fields, methods, tag),
+ repr: repr_pieces.join(""),
+ )
+ self_dict.insert("meta", meta)
+ call_on_dict(self_dict)
+ }
+ _make_cls(new, name, fields, methods, tag)
+}
+
+#let _call_on_dict(self_dict) = {
+ // This is a sneaky trick. The only kind of recursive data structure in Typst seems to be self-recursive
+ // functions. In particular this means that if were to have done e.g.
+ // `self_dict.insert(name, (..args)=>method(self, ..args))`
+ // above then this would not have worked! We would have captured the *old* version of `self`, which still
+ // only has some methods filled in.
+ let self_call(attrname) = {
+ let value = self_dict.at(attrname)
+ if type(value) == function {
+ (..args) => value(_fn2dict(self_call, self_dict.keys()), ..args)
+ } else {
+ value
+ }
+ }
+ _fn2dict(self_call, self_dict.keys())
+}
+
+/// Defines a class with attributes and methods. (Similar to Rust or Python.)
+///
+/// *Example*
+///
+/// ```typst
+/// #{
+/// let Adder = class(
+/// fields: (x: Int),
+/// methods: (
+/// add: (self, y) => {self.x + y}
+/// )
+/// )
+/// let add_three = (Adder.new)(x: 3)
+/// let five = (add_three.add)(2)
+/// }
+///
+/// ```
+///
+/// *Notes:*
+///
+/// - Method lookup (but not field lookup) requires brackets around the access.
+/// - To access the class object (`Adder` itself in the above example) from within a method, then use `self.meta.cls`.
+/// For example, this means that a new instance of the class can be instantiated via `(self.meta.cls.new)(...)`.
+/// _Simply using the name of the class object will not work, as it does not yet exist whilst the methods are being
+/// defined. (In this way `self.meta.cls` handles the case of simple recursion. And if you need mutual recursion
+/// between two different classes/functions/etc, then see `namespace`.)_
+///
+/// *Returns:*
+///
+/// The class object, which may later be instantiated via its `.new` method.
+///
+/// *Arguments:*
+///
+/// - name (none, str): an optional name for the class. Used in error messages.
+/// - fields (dictionary): a mapping str=>type defining the types of the arguments that must be passed at initialisation.
+/// - methods (dictionary): a mapping str=>function defining the methods available.
+/// - tag (function): an optional place to add `class(..., tag: ()=>{})`. If not provided then all class objects with
+/// the same fields and methods will compare equal. If provided then (as all anonymous functions are distinct), this
+/// will make the class unique.
+#let class(name: none, fields: (:), methods: (:), tag: none) = {
+ _class_or_namespace(name: name, fields: fields, methods: methods, tag: tag, call_on_dict: _call_on_dict)
+}
+
+#let test-doc() = {
+ let Adder = class(
+ fields: (x: Int),
+ methods: (
+ add: (self, y) => { self.x + y },
+ ),
+ )
+ let add_three = (Adder.new)(x: 3)
+ let five = (add_three.add)(2)
+ assert.eq(five, 5)
+}
+
+#let test-basic() = {
+ let ArgTest = class(fields: (x: Int))
+ let foo = (ArgTest.new)(x: 3)
+ assert.eq(foo.x, 3)
+}
+
+#let panic-on-basic() = {
+ let ArgTest = class(fields: (x: Int))
+ let foo = (ArgTest.new)(x: "not an int")
+}
+
+#let test-self-recursive() = {
+ let self_recursive_construct_test = class(
+ fields: (x: Int),
+ methods: (
+ add_one: self => {
+ (self.meta.cls.new)(x: self.x + 1)
+ },
+ ),
+ )
+ assert.eq(((self_recursive_construct_test.new)(x: 3).add_one)().x, 4)
+}
+
+#let test-mutually-recursive-methods() = {
+ let mutally_recursive_methods_test = class(
+ fields: (x: Int),
+ methods: (
+ baz: (self, x) => {
+ (self.bar)(x)
+ },
+ bar: (self, x) => {
+ if x == 0 {
+ 5
+ } else {
+ (self.baz)(x - 1)
+ }
+ },
+ ),
+ )
+ assert.eq(((mutally_recursive_methods_test.new)(x: 3).baz)(4), 5)
+}
+
+#let test-tag() = {
+ let Foo1 = class()
+ let Foo2 = class()
+ let Foo3 = class(tag: () => {})
+ let Foo4 = class(tag: () => {})
+ assert.eq(Foo1, Foo2)
+ assert.ne(Foo3, Foo4)
+
+ let foo1 = (Foo1.new)()
+ let foo2 = (Foo2.new)()
+ let foo3 = (Foo3.new)()
+ let foo4 = (Foo4.new)()
+ assert.eq(foo1, foo2)
+ assert.ne(foo3, foo4)
+}
+
+#let test-unsugared-ns() = {
+ let basic_ns_test = class(
+ methods: (
+ Foo: ns => class(
+ name: "Foo",
+ fields: (x: Int),
+ methods: (
+ to_bar: self => ((ns.Bar)().new)(y: self.x),
+ ),
+ ),
+ Bar: ns => class(
+ name: "Bar",
+ fields: (y: Int),
+ methods: (
+ to_foo: self => ((ns.Foo)().new)(x: self.y),
+ ),
+ ),
+ ),
+ )
+ let foo = (((basic_ns_test.new)().Foo)().new)(x: 3)
+ assert.eq(foo.meta.repr, "Foo(x: 3, to_bar: (..) => ..)")
+ assert.eq((foo.to_bar)().meta.repr, "Bar(y: 3, to_foo: (..) => ..)")
+
+ let bar = (((basic_ns_test.new)().Bar)().new)(y: 2)
+ assert.eq(bar.meta.repr, "Bar(y: 2, to_foo: (..) => ..)")
+ assert.eq((bar.to_foo)().meta.repr, "Foo(x: 2, to_bar: (..) => ..)")
+}
+
+#let test-pattern-match-instances() = {
+ let Foo = class(fields: (x: Int))
+ let Bar1 = class(fields: (y: Int), tag: () => {})
+ let Bar2 = class(fields: (y: Int), tag: () => {})
+ let Bar3 = class(fields: (y: Int))
+
+ let x = (Foo.new)(x: 3)
+ let y1 = (Bar1.new)(y: 4)
+ let y2 = (Bar2.new)(y: 4)
+ let y3 = (Bar3.new)(y: 4)
+
+ assert(matches(Foo, x))
+ assert(not matches(Bar1, x))
+ assert(not matches(Bar2, x))
+ assert(not matches(Bar3, x))
+
+ assert(not matches(Foo, y1))
+ assert(matches(Bar1, y1))
+ assert(not matches(Bar2, y1))
+ assert(not matches(Bar3, y1))
+
+ assert(not matches(Foo, y2))
+ assert(not matches(Bar1, y2))
+ assert(matches(Bar2, y2))
+ assert(not matches(Bar3, y2))
+
+ assert(not matches(Foo, y3))
+ assert(not matches(Bar1, y3))
+ assert(not matches(Bar2, y3))
+ assert(matches(Bar3, y3))
+
+ // Identical classes
+ let bar4 = class(fields: (y: Int))
+ assert(matches(bar4, y3))
+}
+
+#let test-pattern-match-class-object() = {
+ assert(matches(Class, class()))
+ assert(matches(Class, class(name: "hi")))
+ assert(matches(Class, class(fields: (x: Int))))
+ assert(not matches(Class, 4.0))
+ assert(not matches(Class, (name: "hi")))
+}
+
+#let test-pattern-match-class-doc() = {
+ let Adder = class(fields: (x: Int), methods: (foo: (self, y) => self.x + y))
+ assert(matches(Class, Adder))
+ let adder = (Adder.new)(x: 3)
+ assert(matches(Adder, adder))
+}
diff --git a/packages/preview/typsy/0.2.1/src/enumeration.typ b/packages/preview/typsy/0.2.1/src/enumeration.typ
new file mode 100644
index 0000000000..89441e1722
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/enumeration.typ
@@ -0,0 +1,88 @@
+#import "./match.typ": case, match, matches
+#import "./format.typ": panic-fmt
+#import "./classes.typ": Class, Int, class
+
+/// An enumeration over different classes.
+///
+/// *Example:*
+///
+/// ```typst
+/// #let Shape = enumeration(
+/// Rectangle: class(fields: (height: Int, width: Int)),
+/// Circle: class(fields: (radius: Int)),
+/// )
+///
+/// #let area(x) = {
+/// match(x,
+/// case(Shape.Rectangle, ()=>{
+/// x.height * x.width
+/// }),
+/// case(Shape.Circle, ()=>{
+/// calc.pi * calc.pow(x.radius, 2)
+/// }),
+/// )
+/// }
+///
+/// #let circ = (Shape.Circle.new)(radius: 3)
+/// #let area_circ = area(circ)
+/// ```
+///
+/// **Returns:**
+///
+/// A new enumeration (which is basically just a namespace of classes).
+///
+/// **Arguments:**
+///
+/// - items (arguments): any number of named classes.
+#let enumeration(..items) = {
+ if items.pos().len() != 0 {
+ panic-fmt("`enumeration` should not be called with positional arguments. Got {}.", repr(items.pos().len()))
+ }
+ for (key, value) in items.named().pairs() {
+ if not matches(Class, value) {
+ panic-fmt("Every value in an `enumeration` must be a `class`.")
+ }
+ }
+ if items.named().values().dedup().len() != items.named().len() {
+ panic(
+ "Got duplicate entries in the enumeration. Consider adding tags: `class(..., tag: ()=>{})`. This "
+ + "exploits a trick in which all anonymous functions are distinct from each other, so it offers a way "
+ + "to distinguish structurally-identical classes.",
+ )
+ }
+ items.named()
+}
+
+#let test-pattern-match() = {
+ let Shape = enumeration(
+ Rectangle: class(fields: (height: Int, width: Int)),
+ Circle: class(fields: (radius: Int)),
+ )
+ let area(x) = {
+ match(
+ x,
+ case(Shape.Rectangle, () => {
+ x.height * x.width
+ }),
+ case(Shape.Circle, () => {
+ // Good approximation to pi. :D
+ // (Better for testing correctness below.)
+ 3 * calc.pow(x.radius, 2)
+ }),
+ )
+ }
+ assert.eq(area((Shape.Rectangle.new)(height: 3, width: 2)), 6)
+ assert.eq(area((Shape.Circle.new)(radius: 4)), 48)
+}
+
+#let panic-on-not-class() = {
+ enumeration(foo: 3)
+}
+
+#let panic-on-equal-classes() = {
+ enumeration(foo: class(), bar: class())
+}
+
+#let test-no-panic-on-tagged-classes() = {
+ enumeration(foo: class(tag: () => {}), bar: class(tag: () => {}))
+}
diff --git a/packages/preview/typsy/0.2.1/src/format.typ b/packages/preview/typsy/0.2.1/src/format.typ
new file mode 100644
index 0000000000..71138114b0
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/format.typ
@@ -0,0 +1,43 @@
+/// Lightweight Rust-like formatting.
+///
+/// This will simply iterate over the string, replacing each instance of `{}` with one of the provided arguments.
+/// See also the `oxifmt` library for the more heavyweight alternative.
+///
+/// *Usage:*
+///
+/// ```typst
+/// #let foo = "Hello"
+/// #let bar = "world"
+/// #let hello_world = fmt("{} {}, hello!", foo, bar)
+/// #assert(hello_world, "Hello world, hello!")
+/// ```
+///
+/// *Returns:*
+///
+/// The formatted string.
+///
+/// *Arguments:*
+///
+/// - format (str): e.g. "hello {}"
+/// - args (arguments): e.g. "world"
+#let fmt(format, ..args) = {
+ assert.eq(
+ args.named().len(),
+ 0,
+ message: "`fmt` does not consume any named arguments. Got `" + repr(args.named().keys()) + "`",
+ )
+ let pieces = format.split("{}")
+ let out = (pieces.at(0),)
+ for (arg, piece) in args.pos().zip(pieces.slice(1), exact: true) {
+ out.push(arg)
+ out.push(piece)
+ }
+ out.join("")
+}
+
+/// Short for `panic(fmt(fmt, ..args))`
+/// - format (str): e.g. "hello {}"
+/// - args (arguments): e.g. "world"
+#let panic-fmt(format, ..args) = {
+ panic(fmt(format, ..args))
+}
diff --git a/packages/preview/typsy/0.2.1/src/lib.typ b/packages/preview/typsy/0.2.1/src/lib.typ
new file mode 100644
index 0000000000..3475a431b5
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/lib.typ
@@ -0,0 +1,13 @@
+#import "./classes.typ": class
+#import "./enumeration.typ": enumeration
+#import "./format.typ": fmt, panic-fmt
+#import "./match.typ": (
+ Any, Arguments, Array, Bool, Bytes, Class, Content, Counter, Datetime, Decimal, Dictionary, Duration, Float,
+ Function, Int, Label, Literal, Location, Module, Named, Never, None, Pattern, Pos, Ratio, Refine, Regex, Selector,
+ State, Str, Symbol, Type, Union, Version, case, match, matches, pattern-repr,
+)
+#import "./namespace.typ": namespace
+#import "./safe-counters.typ": safe-counter
+#import "./safe-state.typ": safe-state
+#import "./tree-counters.typ": tree-counter
+#import "./typecheck.typ": typecheck
diff --git a/packages/preview/typsy/0.2.1/src/match.typ b/packages/preview/typsy/0.2.1/src/match.typ
new file mode 100644
index 0000000000..610af80f94
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/match.typ
@@ -0,0 +1,810 @@
+#import "./format.typ"
+#import "./format.typ": fmt, panic-fmt
+
+/// Checks if `obj` matches the `pattern`.
+///
+/// *Examples:*
+///
+/// ```typst
+/// matches(Array(Int), (3, 4, 5, "not an int")) // false
+/// ```
+///
+/// ```typst
+/// let fn-with-multiple-signatures(..args) = {
+/// if matches(Arguments(Int), args) {
+/// // ...
+/// } else if matches(Arguments(Str), args) {
+/// // ...
+/// } else if matches(Arguments(Str, level: Int), args) {
+/// // ...
+/// } else {
+/// // ...
+/// }
+/// }
+/// ```
+///
+/// *Returns:*
+///
+/// Either `true` or `false`.
+///
+/// *Arguments:*
+///
+/// - pattern (Pattern): the pattern to check against, e.g. `Array(Dictionary(name: Str, email: Str))`. Note that the
+/// underlying Typst types are not valid here. This because, for example, Typst does not have a way to indicate the
+/// types of the elements of an `array`. In addition, `typsy.class`es can be used directly as patterns.
+/// - obj (any): the object to check, e.g. `((name: "Tim", email: "tim@example.com"),)`.
+#let matches(pattern, obj) = {
+ if type(pattern) == function {
+ panic-fmt(
+ "Cannot pattern-match on a function. This usually occurs when writing e.g. `matches(Array, (1, none))` "
+ + "rather than `matches(Array(Any), (1, none))`. Got pattern `{}`.",
+ repr(pattern),
+ )
+ }
+ if type(pattern) == type {
+ panic-fmt(
+ "Cannot pattern-match on a type. This usually occurs when writing e.g. `matches(int, 3)` instead of "
+ + "`matches(Int, 3)`. Got pattern `{}`.",
+ repr(pattern),
+ )
+ }
+ if type(pattern) != dictionary {
+ panic-fmt(
+ "Cannot pattern-match on a pattern of type `{}`. Got pattern `{}`.",
+ repr(type(pattern)),
+ repr(pattern),
+ )
+ }
+ (pattern.__typsy_sentinel_match.match)(obj)
+}
+
+/// Used with `match`.
+///
+/// *Arguments:*
+///
+/// - pattern (Pattern): any kind of pattern.
+/// - fn (function): a function `()=>any`.
+#let case(pattern, fn) = (pattern: pattern, fn: fn)
+/// Pattern-matching. See also `enumeration` for where this feature really shines.
+///
+/// This is syntactic sugar around the lower-level `matches` function.
+///
+/// *Example:*
+///
+/// ```typst
+/// #let foo = (3, 4)
+/// #let bar = match(foo,
+/// case(Array(Int), ()=>{
+/// foo.at(0)
+/// }),
+/// case(Array(Int, Int), ()=>{
+/// foo.at(0) + foo.at(1)
+/// }),
+/// )
+/// ```
+///
+/// *Returns:*
+///
+/// The output of the first `case` statement that the argument matches.
+///
+/// *Arguments:*
+///
+/// - obj (any): the object to pattern-match.
+/// - cases (arguments): any number of `case`s.
+#let match(obj, ..cases) = {
+ if cases.named().len() != 0 {
+ panic-fmt("`match` does not accept any named arguments, received `{}`.", repr(cases.named().keys()))
+ }
+ for case in cases.pos() {
+ if matches(case.pattern, obj) { return (case.fn)() }
+ }
+ panic-fmt("Did not match any case. Value was `{}`.", repr(obj))
+}
+
+/// Pretty-prints a pattern object.
+///
+/// *Example:*
+///
+/// `pattern-repr(Dictionary(x: Int, ..Any)) == "Dictionary(x: Int, ..Any)`
+#let pattern-repr(pattern) = pattern.__typsy_sentinel_match.repr
+
+#let _match(match, repr) = (__typsy_sentinel_match: (match: match, repr: repr))
+#let _match_type(typ, repr) = _match(obj => type(obj) == typ, repr)
+
+//
+// Basic types
+//
+
+#let Any = _match(obj => true, "Any")
+#let Bool = _match_type(bool, "Bool")
+#let Bytes = _match_type(bytes, "Bytes")
+#let Content = _match_type(content, "Content")
+#let Counter = _match_type(counter, "Counter")
+#let Datetime = _match_type(datetime, "Datetime")
+#let Decimal = _match_type(decimal, "Decimal")
+#let Duration = _match_type(duration, "Duration")
+#let Float = _match_type(float, "Float")
+#let Function = _match_type(function, "Function") // Just a simple type as there is no way to verify its signature.
+#let Int = _match_type(int, "Int")
+#let Label = _match_type(label, "Label")
+#let Location = _match_type(location, "Location")
+#let Module = _match_type(module, "Module")
+#let Never = _match(obj => false, "Never") // Useful extra addition
+#let None = _match(obj => obj == none, "None")
+#let Ratio = _match_type(ratio, "Ratio")
+#let Regex = _match_type(regex, "Regex")
+#let Selector = _match_type(selector, "Selector")
+#let Str = _match_type(str, "Str")
+#let Symbol = _match_type(symbol, "Symbol")
+#let Type = _match_type(type, "Type")
+#let Version = _match_type(version, "Version")
+
+//
+// Generic types
+//
+
+#let _unpack(fn) = x => fn(..x)
+#let _generic-repr(generic, args) = {
+ let out = ()
+ let variadic = none
+ let pos = none
+ let named = none
+ for x in args.pos() {
+ out.push(pattern-repr(x))
+ }
+ for (name, val) in args.named() {
+ if name == "__typsy_sentinel_match" {
+ variadic = val.repr
+ } else if name == "__typsy_sentinel_pos" {
+ pos = pattern-repr(val)
+ } else if name == "__typsy_sentinel_named" {
+ named = pattern-repr(val)
+ } else {
+ out.push(fmt("{}: {}", name, pattern-repr(val)))
+ }
+ }
+ if pos != none {
+ out.push(fmt("..Pos({})", pos))
+ }
+ if named != none {
+ out.push(fmt("..Named({})", named))
+ }
+ if variadic != none {
+ out.push(fmt("..{}", variadic))
+ }
+ if out.len() == 0 {
+ generic + "()"
+ } else {
+ fmt("{}({})", generic, out.join(", "))
+ }
+}
+#let _with-repr(pattern, repr) = {
+ _match(pattern.__typsy_sentinel_match.match, repr)
+}
+
+/// *Usage:*
+///
+/// - `Array(Int)` is a pattern that would accept `(3,)` but not `(3, 4)`.
+/// - `Array(Int, Int)` is a pattern that would accept `(3, 4)` but not `(3,)`.
+/// - `Array(Int, Str)` is a pattern that would accept `(3, "hi")`.
+/// - `Array(..Int)` is a pattern that would accept `(3,)` and `(3, 4)` and `(3, 4, 5)`.
+/// - `Array(Bool, Str, ..Int)` is a pattern that would accept `(true, "hello")` and `(true, "hello", 3, 4, 5)`
+///
+/// So positional arguments correspond to the types of the first elements on an array, and `..` unpacking corresponds to
+/// the types of the tail.
+#let Array(..eltypes) = {
+ let pos = eltypes.pos()
+ let named = eltypes.named()
+ let array-repr = () => _generic-repr("Array", eltypes)
+ if named.len() == 0 {
+ _match(
+ obj => type(obj) == array and pos.len() == obj.len() and pos.zip(obj).all(_unpack(matches)),
+ array-repr(),
+ )
+ } else {
+ if named.keys() == ("__typsy_sentinel_match",) {
+ if pos.len() == 0 and named == Any {
+ _match_type(array, array-repr()) // Fastpath
+ } else {
+ _match(
+ obj => (
+ type(obj) == array
+ and pos.len() <= obj.len()
+ and pos.zip(obj.slice(0, pos.len()), exact: true).all(_unpack(matches))
+ and obj.slice(pos.len()).all(matches.with(named))
+ ),
+ array-repr(),
+ )
+ }
+ } else {
+ panic-fmt(
+ "`Array` should either be called with positional arguments e.g. `Array(Int, Str)`, or with a variadic "
+ + "argument e.g. `Array(..Int)`, or both e.g. `Array(Int, Str, ..Int)`. However received keywords "
+ + "`{}` instead.",
+ repr(named.keys()),
+ )
+ }
+ }
+}
+/// *Usage:*
+///
+/// - `Dictionary(..Int)` is a pattern that would accept `(foo: 3)` and `(foo: 3, bar: 4)` but not `(foo: 3, bar: "x")`.
+/// - `Dictionary(bar: Str, ..Int)` is a pattern that would accept `(foo: 3, bar: "x")` but not `(foo: 3)`
+/// or `(foo: 3, bar: 4)`.
+///
+/// So the named arguments correspond to type of the value in key-value pairs with that specific key, and `..` unpacking
+/// corresponds to the the type of the value for all other keys. (And recall that Typst only allows for string-typed
+/// keys, so we do not need to annotate those.)
+#let Dictionary(..valtypes) = {
+ if valtypes.pos().len() != 0 {
+ panic-fmt(
+ "`Dictionary` should either be called with zero positional arguments. Got {} positional arguments instead.",
+ repr(valtypes.pos().len()),
+ )
+ }
+ let dictionary-repr = () => _generic-repr("Dictionary", valtypes)
+ let named = valtypes.named()
+ let valtype = (
+ __typsy_sentinel_match: named.remove("__typsy_sentinel_match", default: Never.__typsy_sentinel_match),
+ )
+ if valtype == Any and named.len() == 0 { return _match_type(dictionary, dictionary-repr()) } // Fastpath
+ if valtype == Any and named.len() == 1 {
+ // Fastpath for a particularly common case.
+ let ((key, pat),) = named.pairs()
+ return _match(
+ obj => type(obj) == dictionary and obj.keys().contains(key) and matches(pat, obj.at(key)),
+ dictionary-repr(),
+ )
+ }
+ _match(
+ obj => (
+ type(obj) == dictionary
+ and obj.pairs().all(kv => matches(named.at(kv.at(0), default: valtype), kv.at(1)))
+ and named.keys().all(n => obj.keys().contains(n))
+ ),
+ dictionary-repr(),
+ )
+}
+/// For use with `Arguments`.
+#let Pos(eltype) = (__typsy_sentinel_pos: eltype)
+/// For use with `Arguments`.
+#let Named(valtype) = (__typsy_sentinel_named: valtype)
+/// *Usage:*
+///
+/// - `Arguments(Str, Bool)` is a pattern that would match the `arguments` to `some-fn("foo", true)`.
+/// - `Arguments(Str, level: Int)` is a pattern that would match the `arguments` to `some-fn("foo", level: 1)`.
+/// - `Arguments(level: Int, sep: Str)` is a pattern that would match the `arguments` to `some-fn(level: 1, sep: ",")`.
+/// - `Arguments(..Any)` is a pattern that would match all `arguments` to any function.
+/// - `Arguments(..Int)` is a pattern that would match the `arguments` to `some-fn(3, 4, foo: 5, bar: 6)`.
+/// - `Arguments(..Pos(Int))` is a pattern that would match the `arguments` to `some-fn(3, 4)`.
+/// - `Arguments(..Named(Int))` is a pattern that would match the `arguments` to `some-fn(foo: 5, bar: 6)`.
+///
+/// That is, we can specify both positional and named arguments straightforwardly. We can specify variadic positional
+/// arguments using `..Pos(Foo)` and variadic named arguments using `..Named(Foo)`, and variadic positional+named
+/// arguments via `..Foo`.
+#let Arguments(..args) = {
+ let arguments-repr = () => _generic-repr("Arguments", args)
+ let named = args.named()
+
+ let var = named.remove("__typsy_sentinel_match", default: auto)
+ let varpos = named.remove("__typsy_sentinel_pos", default: auto)
+ let varnamed = named.remove("__typsy_sentinel_named", default: auto)
+ let pos = if var == auto {
+ if varpos == auto {
+ Array(..args.pos())
+ } else {
+ Array(..args.pos(), ..varpos)
+ }
+ } else {
+ if varpos == auto {
+ Array(..args.pos(), __typsy_sentinel_match: var)
+ } else {
+ panic-fmt(
+ "`Arguments` called with both variadic and variadic-positional arguments, e.g. "
+ + "`Arguments(..Foo, ..Pos(Bar))`",
+ )
+ }
+ }
+ let named = if var == auto {
+ if varnamed == auto {
+ Dictionary(..named)
+ } else {
+ Dictionary(..named, ..varnamed)
+ }
+ } else {
+ if varnamed == auto {
+ Dictionary(..named, __typsy_sentinel_match: var)
+ } else {
+ panic-fmt(
+ "`Arguments` called with both variadic and variadic-named arguments, e.g. "
+ + "`Arguments(..Foo, ..Named(Bar))`",
+ )
+ }
+ }
+
+ _match(obj => type(obj) == arguments and matches(pos, obj.pos()) and matches(named, obj.named()), arguments-repr())
+}
+/// *Usage:*
+///
+/// - `State(Int)` would match `state("foo", 4)`
+///
+/// That is, the pattern specifies the value of the state.
+#let State(statetype) = _match(
+ obj => type(obj) == state and matches(statetype, obj.get()),
+ fmt("State({})", pattern-repr(statetype)),
+)
+
+//
+// Miscellaneous
+//
+
+/// *Usage:*
+///
+/// - `Literal(3)` is a pattern that would accept `3` but not `4`.
+/// - `Literal(3, 4, "hi")` is a pattern that would accept `3` and `"hi"` but not `"bye"`.
+///
+/// That is, this is a collection of values, at least one of which we must be equal to.
+#let Literal(..values) = {
+ let named = values.named()
+ if named.len() != 0 {
+ panic-fmt("`Literal` should only be called with positional arguments. Got keywords `{}`.", repr(named.keys()))
+ }
+ let literal-repr = ("Literal(",)
+ for (i, val) in values.pos().enumerate() {
+ if i != 0 {
+ literal-repr.push(", ")
+ }
+ literal-repr.push(repr(val))
+ }
+ literal-repr.push(")")
+ _match(obj => values.pos().contains(obj), literal-repr.join(""))
+}
+/// *Usage:*
+///
+/// - `Union(Int, Str)` is a pattern that would accept `3` and `"hi"` but not `(foo: 4)`.
+///
+/// That is, this is a collection of patterns, at least one of which we must match.
+#let Union(..values) = {
+ let named = values.named()
+ if named.len() != 0 {
+ panic-fmt("`Union` should only be called with positional arguments. Got keywords `{}`.", repr(named.keys()))
+ }
+ _match(obj => values.pos().any(v => matches(v, obj)), _generic-repr("Union", values))
+}
+/// Matches pattern objects themselves. Does *not* go via `Dictionary` because we're actually looking for the sentinel
+// key that `Dictionary` uses to identify its default value.
+#let Pattern = _match(
+ x => (
+ type(x) == dictionary
+ and x.keys() == ("__typsy_sentinel_match",)
+ and type(x.__typsy_sentinel_match) == dictionary
+ and x.__typsy_sentinel_match.keys() == ("match", "repr")
+ and type(x.__typsy_sentinel_match.match) == function
+ and type(x.__typsy_sentinel_match.repr) == str
+ ),
+ "Pattern",
+)
+
+/// Used for pattern-matching class objects themselves. Not to be confused with matching class instances.
+///
+/// *Example:*
+///
+/// ```typst
+/// #let Adder = class(fields: (x: Int), methods: (foo: (self, y)=>self.x+y))
+/// #assert(matches(Class, Adder))
+/// #let adder = (Adder.new)(x: 3)
+/// #assert(matches(Adder, adder))
+/// ```
+#let Class = _with-repr(Dictionary(__typsy_sentinel_is_class: Literal(true), ..Any), "Class")
+/// Takes a pattern, and additionally requires that a function must return `true` in order for values to satisfy the
+/// pattern.
+///
+/// *Examples:*
+///
+/// ```typst
+/// #let Email = Refine(Str, x=>x.contains("@"))
+/// #matches(Email, "hello@example.com") // true
+/// #matches(Email, "hello world") // false
+/// ```
+///
+/// This can also be used to create brand-new patterns by refining `Any`, e.g. we could reimplement `Literal` via
+/// ```typst
+/// #let MyLiteral(..values) = Refine(Any, x=>values.pos().contains(x))
+/// #matches(MyLiteral(3), 3) // true
+/// #matches(MyLiteral(3), 4) // false
+/// ```
+///
+/// **Returns:**
+///
+/// A new pattern.
+///
+/// **Arguments:**
+///
+/// - pattern (Pattern): an existing pattern.
+/// - predicate (function): a function ` -> bool`. It will only be called on items that already
+/// match `pattern`.
+#let Refine(pattern, predicate) = {
+ _match(
+ obj => matches(pattern, obj) and predicate(obj),
+ fmt("Refine({}, {})", pattern-repr(pattern), repr(predicate)),
+ )
+}
+
+#let panic-on-type() = {
+ matches(int, 3)
+}
+
+#let panic-on-function() = {
+ matches(Array, (3,))
+}
+
+#let test-basic-types() = {
+ assert(matches(Any, "hi"))
+ assert(matches(Any, 1))
+ assert(matches(Any, [some content]))
+
+ assert(matches(Bool, true))
+ assert(not matches(Bool, "not a bool"))
+
+ assert(matches(Bytes, bytes("hello")))
+ assert(not matches(Bytes, "hello"))
+
+ assert(matches(Content, [hello there]))
+ assert(not matches(Content, "hello there"))
+
+ assert(matches(Datetime, datetime(year: 1, month: 1, day: 1)))
+ assert(matches(Datetime, datetime(hour: 1, minute: 1, second: 1)))
+ assert(not matches(Datetime, "hello"))
+
+ assert(matches(Decimal, decimal("3.0")))
+ assert(not matches(Decimal, 3.0))
+
+ assert(matches(Duration, duration(hours: 1)))
+ assert(not matches(Duration, "duration"))
+
+ assert(matches(Float, 3.0))
+ assert(not matches(Float, 3))
+
+ assert(matches(Function, () => {}))
+ assert(not matches(Function, ()))
+
+ assert(matches(Int, 3))
+ assert(not matches(Int, 3.0))
+ assert(not matches(Int, decimal("3")))
+
+ assert(matches(Label, ))
+ assert(not matches(Label, "hi"))
+
+ assert(matches(Module, format))
+ assert(not matches(Module, "format"))
+
+ assert(not matches(Never, 3))
+ assert(not matches(Never, "3"))
+
+ assert(matches(None, none))
+ assert(not matches(None, "none"))
+
+ assert(matches(Ratio, 150%))
+ assert(not matches(Ratio, 1 / 2))
+
+ assert(matches(Regex, regex("foo")))
+ assert(not matches(Regex, "foo"))
+
+ assert(matches(Selector, heading.where(level: 1)))
+ assert(not matches(Selector, heading))
+
+ assert(matches(Str, "hi"))
+ assert(not matches(Str, bytes("hi")))
+
+ assert(matches(Symbol, symbol("x")))
+ assert(not matches(Symbol, "x"))
+
+ assert(matches(Type, int))
+ assert(matches(Type, str))
+ assert(matches(Type, function))
+ assert(not matches(Type, Int))
+ assert(not matches(Type, Str))
+ assert(not matches(Type, Function))
+
+ assert(matches(Version, version(0, 1, 2)))
+ assert(not matches(Version, (0, 1, 2)))
+
+ assert(matches(Pattern, Any))
+ assert(matches(Pattern, Int))
+ assert(matches(Pattern, Function))
+ assert(matches(Pattern, Dictionary(x: Int)))
+ assert(not matches(Pattern, int))
+ assert(not matches(Pattern, (x: 3)))
+}
+
+#let test-location() = {
+ context assert(matches(Location, here()))
+ assert(not matches(Location, "here"))
+}
+#test-location()
+
+#let test-arguments() = {
+ let id(..args) = args
+ assert(not matches(Arguments(..Any), 3))
+
+ assert(matches(Arguments(Int), id(3)))
+ assert(not matches(Arguments(Int), id(3, 4)))
+ assert(not matches(Arguments(Int), id(3, foo: 4)))
+ assert(not matches(Arguments(Int), id(foo: 4)))
+ assert(not matches(Arguments(Int), id(3.0)))
+ assert(matches(Arguments(Int, Str, Int), id(3, "hi", 4)))
+ assert(not matches(Arguments(Int, Str, Int), id(3, "hi", 4, 5)))
+ assert(not matches(Arguments(Int, Str, Int), id(3, 4, "hi", 5)))
+
+ assert(matches(Arguments(Int, Str, foo: Int), id(3, "hi", foo: 4)))
+ assert(not matches(Arguments(Int, Str, foo: Int), id(3, "hi", foo: "hi")))
+ assert(not matches(Arguments(Int, Str, foo: Int, bar: Array(Int)), id(3, "hi", foo: "hi")))
+ assert(matches(Arguments(Int, Str, foo: Str, bar: Array(Int)), id(3, "hi", foo: "hi", bar: (3,))))
+ assert(not matches(Arguments(Int, Str, foo: Int, bar: Array(Int)), id(3, "hi", foo: "hi", bar: (3.0,))))
+
+ assert(matches(Arguments(..Any), id(3)))
+ assert(matches(Arguments(..Any), id(3, 4)))
+ assert(matches(Arguments(..Any), id(3, 4, foo: "hi")))
+ assert(matches(Arguments(..Any), id(3, 4, foo: "hi", bar: id(5))))
+
+ assert(matches(Arguments(..Int), id()))
+ assert(matches(Arguments(..Int), id(3)))
+ assert(matches(Arguments(..Int), id(3, 4)))
+ assert(not matches(Arguments(..Int), id(3, 4, foo: "hi")))
+ assert(not matches(Arguments(..Int), id(3, "hi", foo: 4)))
+ assert(matches(Arguments(..Int), id(3, 4, foo: 4)))
+
+ assert(matches(Arguments(..Pos(Int)), id()))
+ assert(matches(Arguments(..Pos(Int)), id(3)))
+ assert(matches(Arguments(..Pos(Int)), id(3, 4)))
+ assert(not matches(Arguments(..Pos(Int)), id(3, "4")))
+ assert(not matches(Arguments(..Pos(Int)), id(3, 4, foo: 5)))
+
+ assert(matches(Arguments(..Named(Int)), id()))
+ assert(matches(Arguments(..Named(Int)), id(foo: 3)))
+ assert(matches(Arguments(..Named(Int)), id(foo: 3, bar: 4)))
+ assert(not matches(Arguments(..Named(Int)), id(foo: 3, bar: "4")))
+ assert(not matches(Arguments(..Named(Int)), id(3, 4, foo: 5)))
+
+ assert(matches(Arguments(..Pos(Int), ..Named(Str)), id()))
+ assert(matches(Arguments(..Pos(Int), ..Named(Str)), id(3)))
+ assert(matches(Arguments(..Pos(Int), ..Named(Str)), id(3, foo: "hi")))
+ assert(not matches(Arguments(..Pos(Int), ..Named(Str)), id(3, foo: 3)))
+ assert(not matches(Arguments(..Pos(Int), ..Named(Str)), id("hi", foo: 3)))
+ assert(matches(Arguments(Str, foo: Int, ..Pos(Int), ..Named(Str)), id("hi", foo: 3)))
+ assert(matches(Arguments(..Pos(Int), foo: Int, ..Named(Str)), id(3, foo: 3)))
+
+ let def = Arguments(Str, Int, Float, foo: Int, ..Pos(Int), ..Named(Str))
+ assert(matches(def, id("hi", 3, 3.0, 2, 5, foo: 3, bar: "bye")))
+ assert(not matches(def, id(3, 3, 3.0, 2, 5, foo: 3, bar: "bye")))
+ assert(not matches(def, id("hi", "3", 3.0, 2, 5, foo: 3, bar: "bye")))
+ assert(not matches(def, id("hi", 3, "3.0", 2, 5, foo: 3, bar: "bye")))
+ assert(not matches(def, id("hi", 3, 3.0, 2, 5, foo: "3", bar: "bye")))
+ assert(not matches(def, id("hi", 3, 3.0, "2", 5, foo: 3, bar: 5)))
+ assert(not matches(def, id("hi", 3, 3.0, 2, 5, foo: 3, bar: 5)))
+}
+
+
+#let test-array() = {
+ assert(not matches(Array(Any), 3))
+ assert(not matches(Array(Any), (hi: 3)))
+
+ assert(matches(Array(), ()))
+ assert(not matches(Array(), (3,)))
+ assert(not matches(Array(), (3, 4)))
+ assert(matches(Array(Any), (3,)))
+ assert(matches(Array(Int), (3,)))
+ assert(not matches(Array(Int), ()))
+ assert(not matches(Array(Int), (3, 4)))
+ assert(matches(Array(Any, Any), (1, none)))
+ assert(matches(Array(Any, Any), (1, "hi")))
+ assert(not matches(Array(Any, Any), ()))
+ assert(not matches(Array(Any, Any), (3,)))
+ assert(not matches(Array(Int, Any), (3,)))
+ assert(not matches(Array(Int, Int), (3,)))
+
+ assert(matches(Array(Int, Str), (3, "hi")))
+ assert(not matches(Array(Int, Str), ("hi", 3)))
+
+ assert(matches(Array(Array(Int), Str), ((3,), "hi")))
+ assert(not matches(Array(Array(Int), Str), ((3, 4), "hi")))
+
+ assert(matches(Array(..Int), ()))
+ assert(matches(Array(..Int), (3,)))
+ assert(matches(Array(..Int), (3, 4)))
+ assert(matches(Array(..Int), (3, 4, 5)))
+ assert(matches(Array(..Any), ()))
+ assert(matches(Array(..Any), (3, "4", none)))
+ assert(matches(Array(..Never), ()))
+ assert(not matches(Array(..Never), (3,)))
+ assert(matches(Array(..Array(Int)), ((3,), (4,), (5,))))
+ assert(not matches(Array(..Array(Int)), ((3,), (4,), (5, 6))))
+
+ assert(not matches(Array(Int, Str, ..Int), ()))
+ assert(not matches(Array(Int, Str, ..Int), (3,)))
+ assert(matches(Array(Int, Str, ..Int), (3, "hi")))
+ assert(matches(Array(Int, Str, ..Int), (3, "hi", 4)))
+ assert(matches(Array(Int, Str, ..Int), (3, "hi", 4, 5)))
+ assert(not matches(Array(Int, Str, ..Int), (3, "hi", 4, 5, "hi")))
+
+ assert(matches(Array(Int, Str, ..Any), (3, "hi", 4, 5)))
+ assert(matches(Array(Int, Str, ..Any), (3, "hi", 4, 5, "hi")))
+}
+
+#let panic-on-array-keywords() = {
+ Array(hi: 3)
+}
+
+#let test-dictionary() = {
+ assert(not matches(Dictionary(), 3))
+ assert(not matches(Dictionary(), (3,)))
+
+ assert(matches(Dictionary(), (:)))
+ assert(matches(Dictionary(..Any), (:)))
+ assert(matches(Dictionary(..Int), (:)))
+ assert(matches(Dictionary(..Str), (:)))
+
+ assert(matches(Dictionary(..Int), (hi: 3, bye: 3)))
+ assert(matches(Dictionary(..Any), (hi: 3, bye: 3)))
+ assert(matches(Dictionary(..Any), (hi: 3, bye: "foo")))
+ assert(matches(Dictionary(..Union(Int, Str)), (hi: 3, bye: "foo")))
+ assert(not matches(Dictionary(..Union(Int, Bytes)), (hi: 3, bye: "foo")))
+ assert(not matches(Dictionary(..Union(Int, Str)), (hi: 3, bye: "foo", baz: 3.0)))
+
+ assert(matches(Dictionary(hi: Int, bye: Int), (hi: 3, bye: 3)))
+ assert(not matches(Dictionary(hi: Int, bye: Int), (hi: 3, bye: "foo")))
+ assert(matches(Dictionary(hi: Int, bye: Str), (hi: 3, bye: "foo")))
+ assert(matches(Dictionary(hi: Int, bye: Str), (hi: 3, bye: "foo")))
+
+ assert(matches(Dictionary(hi: Int, bye: Str, ..Str), (hi: 3, bye: "foo", baz: "boom")))
+ assert(not matches(Dictionary(hi: Int, bye: Str, ..Str), (hi: 3, bye: "foo", baz: 3)))
+ assert(matches(Dictionary(hi: Int, bye: Str, ..Any), (hi: 3, bye: "foo", baz: "boom")))
+ assert(not matches(Dictionary(hi: Int, bye: Int, ..Any), (hi: 3, bye: "foo")))
+ assert(not matches(Dictionary(hi: Int, bye: Int, ..Str), (hi: 3, bye: "foo")))
+}
+
+#let test-dictionary-incomplete() = {
+ assert(not matches(Dictionary(foo: Int, ..Any), (:)))
+}
+
+#let test-dictionary-fastpath-single-element() = {
+ let pat = Dictionary(hi: Int, ..Any)
+ assert(matches(pat, (hi: 3)))
+ assert(matches(pat, (hi: 3, bye: 3)))
+ assert(matches(pat, (hi: 3, bye: "foo")))
+ assert(not matches(pat, (:)))
+ assert(not matches(pat, (hi: "foo")))
+ assert(not matches(pat, (bye: "foo")))
+ assert(not matches(pat, (bye: 3)))
+}
+
+#let panic-on-dictionary-args1() = {
+ Dictionary(Int)
+}
+
+#let panic-on-dictionary-args2() = {
+ Dictionary(Int, Str)
+}
+
+#let test-state() = {
+ let foo = state("kidger-match-test-state", 3)
+ context assert(matches(State(Any), foo))
+ context assert(matches(State(Int), foo))
+ context assert(not matches(State(Str), foo))
+}
+#test-state()
+
+#let test-literal() = {
+ assert(matches(Literal(heading), heading))
+ assert(matches(Literal(heading, page), heading))
+ assert(not matches(Literal(page), heading))
+ assert(matches(Literal(int), int))
+ assert(not matches(Literal(int), str))
+}
+
+#let test-union() = {
+ assert(matches(Union(Int, Str), 3))
+ assert(matches(Union(Int, Int), 3))
+ assert(not matches(Union(Bytes, Str), 3))
+ assert(matches(Union(Array(..Int), Array(..Str)), (3,)))
+ assert(not matches(Union(Array(..Int), Array(..Str)), (3.0,)))
+ assert(matches(Union(Array(..Union(Int, Float)), Array(..Str)), (3.0,)))
+}
+
+#let test-refine() = {
+ let Email = Refine(Str, x => x.contains("@"))
+ assert(matches(Email, "hello@example.com"))
+ assert(not matches(Email, "hello world"))
+ let MyLiteral(..values) = Refine(Any, x => values.pos().contains(x))
+ assert(matches(MyLiteral(3), 3))
+ assert(not matches(MyLiteral(3), 4))
+}
+
+#let test-pattern-match() = {
+ assert.eq(match(3, case(Int, () => 4), case(Str, () => 5)), 4)
+}
+
+#let panic-on-no-pattern-match() = {
+ match(3, case(Float, () => 4), case(Str, () => 5))
+}
+
+#let test-pattern-repr-basic() = {
+ assert.eq(pattern-repr(Any), "Any")
+ assert.eq(pattern-repr(Bool), "Bool")
+ assert.eq(pattern-repr(Bytes), "Bytes")
+ assert.eq(pattern-repr(Content), "Content")
+ assert.eq(pattern-repr(Counter), "Counter")
+ assert.eq(pattern-repr(Datetime), "Datetime")
+ assert.eq(pattern-repr(Decimal), "Decimal")
+ assert.eq(pattern-repr(Duration), "Duration")
+ assert.eq(pattern-repr(Float), "Float")
+ assert.eq(pattern-repr(Function), "Function")
+ assert.eq(pattern-repr(Int), "Int")
+ assert.eq(pattern-repr(Label), "Label")
+ assert.eq(pattern-repr(Location), "Location")
+ assert.eq(pattern-repr(Module), "Module")
+ assert.eq(pattern-repr(Never), "Never")
+ assert.eq(pattern-repr(None), "None")
+ assert.eq(pattern-repr(Ratio), "Ratio")
+ assert.eq(pattern-repr(Regex), "Regex")
+ assert.eq(pattern-repr(Selector), "Selector")
+ assert.eq(pattern-repr(Str), "Str")
+ assert.eq(pattern-repr(Symbol), "Symbol")
+ assert.eq(pattern-repr(Type), "Type")
+ assert.eq(pattern-repr(Version), "Version")
+}
+
+#let test-pattern-repr-array() = {
+ assert.eq(pattern-repr(Array()), "Array()")
+ assert.eq(pattern-repr(Array(Int)), "Array(Int)")
+ assert.eq(pattern-repr(Array(Int, Str)), "Array(Int, Str)")
+ assert.eq(pattern-repr(Array(..Any)), "Array(..Any)")
+ assert.eq(pattern-repr(Array(Int, ..Any)), "Array(Int, ..Any)")
+ assert.eq(pattern-repr(Array(Int, Str, ..Any)), "Array(Int, Str, ..Any)")
+}
+
+#let test-pattern-repr-dictionary() = {
+ assert.eq(pattern-repr(Dictionary()), "Dictionary()")
+ assert.eq(pattern-repr(Dictionary(x: Int)), "Dictionary(x: Int)")
+ assert.eq(pattern-repr(Dictionary(x: Int, y: Str)), "Dictionary(x: Int, y: Str)")
+ assert.eq(pattern-repr(Dictionary(..Ratio)), "Dictionary(..Ratio)")
+ assert.eq(pattern-repr(Dictionary(x: Int, ..Ratio)), "Dictionary(x: Int, ..Ratio)")
+ assert.eq(pattern-repr(Dictionary(x: Int, y: Str, ..Ratio)), "Dictionary(x: Int, y: Str, ..Ratio)")
+}
+
+#let test-pattern-repr-arguments() = {
+ assert.eq(pattern-repr(Arguments()), "Arguments()")
+ assert.eq(pattern-repr(Arguments(Int)), "Arguments(Int)")
+ assert.eq(pattern-repr(Arguments(Int, Str)), "Arguments(Int, Str)")
+ assert.eq(pattern-repr(Arguments(x: Int)), "Arguments(x: Int)")
+ assert.eq(pattern-repr(Arguments(x: Int, y: Str)), "Arguments(x: Int, y: Str)")
+ assert.eq(pattern-repr(Arguments(Str, x: Int)), "Arguments(Str, x: Int)")
+ assert.eq(pattern-repr(Arguments(x: Int, ..Pos(Str))), "Arguments(x: Int, ..Pos(Str))")
+ assert.eq(pattern-repr(Arguments(x: Int, ..Named(Str))), "Arguments(x: Int, ..Named(Str))")
+ assert.eq(pattern-repr(Arguments(x: Int, ..Any)), "Arguments(x: Int, ..Any)")
+ assert.eq(
+ pattern-repr(Arguments(Bool, x: Int, ..Pos(Ratio), ..Named(Str))),
+ "Arguments(Bool, x: Int, ..Pos(Ratio), ..Named(Str))",
+ )
+}
+
+#let test-pattern-repr-unusual() = {
+ // state, literal, union, pattern, class, refine
+ assert.eq(pattern-repr(State(Int)), "State(Int)")
+
+ assert.eq(pattern-repr(Literal()), "Literal()")
+ assert.eq(pattern-repr(Literal(3)), "Literal(3)")
+ assert.eq(pattern-repr(Literal(3, "hi")), "Literal(3, \"hi\")")
+
+ assert.eq(pattern-repr(Union()), "Union()")
+ assert.eq(pattern-repr(Union(Int)), "Union(Int)")
+ assert.eq(pattern-repr(Union(Int, Str)), "Union(Int, Str)")
+
+ assert.eq(pattern-repr(Pattern), "Pattern")
+ assert.eq(pattern-repr(Class), "Class")
+
+ let refine-repr = pattern-repr(Refine(Int, x=>x>0))
+ assert(refine-repr.starts-with("Refine(Int, "))
+ assert(refine-repr.ends-with(")"))
+}
+
diff --git a/packages/preview/typsy/0.2.1/src/namespace.typ b/packages/preview/typsy/0.2.1/src/namespace.typ
new file mode 100644
index 0000000000..156e38a5e7
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/namespace.typ
@@ -0,0 +1,97 @@
+#import "./classes.typ": _class_or_namespace, class, Int
+#import "./format.typ": panic-fmt
+
+#let _call_on_dict(self_dict) = {
+ let self_call(attrname) = {
+ let value = self_dict.at(attrname)
+ assert.eq(type(value), function)
+ value(self_call)
+ }
+ self_call
+}
+
+/// Builds a namespace of objects that may be mutually-referential.
+///
+/// This ability to be mutually-referential is what distinguishes this object (and is the reason it exists) as compared
+/// to just normal Typst code.
+///
+/// *Example:*
+///
+/// ```typst
+/// #let ns = namespace(
+/// foo: ns => {
+/// let foo(x) = if x == 0 {"FOO"} else {ns("bar")(x - 1)}
+/// foo
+/// },
+/// bar: ns => {
+/// let bar(x) = if x == 0 {"BAR"} else {ns("foo")(x - 1)}
+/// bar
+/// },
+/// )
+/// #let foo = ns("foo")
+/// #assert.eq(foo(3), "BAR")
+/// #assert.eq(foo(4), "FOO")
+/// ```
+///
+/// *Returns:*
+///
+/// The namespace object.
+///
+/// *Arguments:*
+///
+/// - objs (arguments): a collection of objects in the namespace.
+#let namespace(..objs) = {
+ if objs.pos().len() != 0 {
+ panic-fmt("Cannot pass positional arguments to a namespace.")
+ }
+ (
+ _class_or_namespace(
+ name: "namespace",
+ fields: (:),
+ methods: objs.named(),
+ tag: none,
+ call_on_dict: _call_on_dict,
+ ).new
+ )()
+}
+
+#let test-ns() = {
+ let ns_test = namespace(
+ Foo: ns => class(
+ fields: (x: Int),
+ methods: (
+ to_bar: self => ns("foo_to_bar")(self),
+ ),
+ ),
+ Bar: ns => class(
+ fields: (y: Int),
+ methods: (
+ to_foo: self => (ns("Foo").new)(x: self.y),
+ ),
+ ),
+ foo_to_bar: ns => foo => { (ns("Bar").new)(y: foo.x) },
+ )
+ let foo = (ns_test("Foo").new)(x: 3)
+ assert.eq(foo.meta.repr, "(x: 3, to_bar: (..) => ..)")
+ assert.eq((foo.to_bar)().meta.repr, "(y: 3, to_foo: (..) => ..)")
+
+ let bar = (ns_test("Bar").new)(y: 2)
+ assert.eq(bar.meta.repr, "(y: 2, to_foo: (..) => ..)")
+ assert.eq((bar.to_foo)().meta.repr, "(x: 2, to_bar: (..) => ..)")
+}
+
+#let test-doc() = {
+ let ns = namespace(
+ foo: ns => {
+ let foo(x) = if x == 0 { "FOO" } else { ns("bar")(x - 1) }
+ foo
+ },
+ bar: ns => {
+ let bar(x) = if x == 0 { "BAR" } else { ns("foo")(x - 1) }
+ bar
+ },
+ )
+ let foo = ns("foo")
+ assert.eq(foo(3), "BAR")
+ assert.eq(foo(4), "FOO")
+}
diff --git a/packages/preview/typsy/0.2.1/src/safe-counters.typ b/packages/preview/typsy/0.2.1/src/safe-counters.typ
new file mode 100644
index 0000000000..cf117b0b42
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/safe-counters.typ
@@ -0,0 +1,69 @@
+/// Like the built-in `counter`, but without risk of using the same string twice.
+///
+/// Usage is
+/// ```typst
+/// #let counter1 = safe-counter(()=>{})
+/// #let counter2 = safe-counter(()=>{})
+/// // ...
+/// ```
+/// This relies on the fact that every anonymous function is a unique object, and we can use this to build a unique key
+/// for creating the counter. Note that you cannot wrap this into a `let safe-counter2() = {safe-counter(()=>{})}`
+/// because we need it to be a *different* anonymous function each time.
+///
+/// *Returns:*
+///
+/// A `counter(...)`.
+///
+/// *Arguments:*
+///
+/// - symbol (function): this should be a `()=>{}`.
+#let safe-counter(symbol) = {
+ counter(heading.where(level: symbol))
+}
+
+#let test-basic() = {
+ let c1 = safe-counter(() => {})
+ let c2 = safe-counter(() => {})
+ context {
+ assert.eq(c1.get().at(0), 0)
+ assert.eq(c2.get().at(0), 0)
+ }
+ c1.step()
+ context {
+ assert.eq(c1.get().at(0), 1)
+ assert.eq(c2.get().at(0), 0)
+ }
+ c2.step()
+ context {
+ assert.eq(c1.get().at(0), 1)
+ assert.eq(c2.get().at(0), 1)
+ }
+}
+
+// This trick is used in `tree-counters`.
+#let test-undocumented-sneaky-trick() = {
+ let symbol = ()=>{}
+ let c1 = safe-counter((1, symbol))
+ let c2 = safe-counter((2, symbol))
+ let c1-again = safe-counter((1, symbol))
+ context {
+ assert.eq(c1.get().at(0), 0)
+ assert.eq(c2.get().at(0), 0)
+ assert.eq(c1-again.get().at(0), 0)
+ }
+ c1.step()
+ context {
+ assert.eq(c1.get().at(0), 1)
+ assert.eq(c2.get().at(0), 0)
+ assert.eq(c1-again.get().at(0), 1)
+ }
+ c2.step()
+ context {
+ assert.eq(c1.get().at(0), 1)
+ assert.eq(c2.get().at(0), 1)
+ assert.eq(c1-again.get().at(0), 1)
+ }
+}
+
+#test-basic()
+#test-undocumented-sneaky-trick()
diff --git a/packages/preview/typsy/0.2.1/src/safe-state.typ b/packages/preview/typsy/0.2.1/src/safe-state.typ
new file mode 100644
index 0000000000..140edc297a
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/safe-state.typ
@@ -0,0 +1,151 @@
+#import "./classes.typ": class
+#import "./match.typ": Any, Function
+
+#let _underlying-safe-state = state("--typsy-sentinel-very-secret-state-string", ())
+#let _at(self, iterator) = {
+ // Yay, O(n) lookup! Oh well, I don't see a way around around this. Fortunate that Typst has ubiquitous caching.
+ for (key, val) in iterator {
+ if key == self.symbol {
+ return val
+ }
+ }
+ self.default
+}
+#let _state-cls = class(
+ fields: (symbol: Function, default: Any),
+ methods: (
+ at: (self, loc) => {
+ _at(self, _underlying-safe-state.at(loc))
+ },
+ final: self => {
+ _at(self, _underlying-safe-state.final())
+ },
+ get: self => {
+ _at(self, _underlying-safe-state.get())
+ },
+ update: (self, value) => context {
+ let new = ()
+ let found = false
+ for (key, val) in _underlying-safe-state.at(here()) {
+ if key == self.symbol {
+ new.push((key, value))
+ found = true
+ } else {
+ new.push((key, val))
+ }
+ }
+ if not found {
+ new.push((self.symbol, value))
+ }
+ _underlying-safe-state.update(new)
+ },
+ ),
+)
+
+/// Like the built-in `state`, but without risk of using the same string twice.
+///
+/// Usage is
+/// ```typst
+/// #let state1 = safe-state(()=>{}, "some default value")
+/// #let state2 = safe-state(()=>{}, "another default value")
+/// // ...
+/// ```
+/// This relies on the fact that every anonymous function is a unique object, and we can use this to build a unique key
+/// for creating the state. Note that you cannot wrap this into a `let safe-state2() = {safe-state(()=>{})}` because
+/// we need it to be a *different* anonymous function each time.
+///
+/// *Methods:*
+///
+/// It provides the following of the usual `state` methods. In every case note that they must be surrounded with an
+/// extra pair of brackets, due to Typst limitations.
+///
+/// - `(self.at)(loc)`: as the usual `state.at`.
+/// - `(self.final)()`: as the usual `state.final`.
+/// - `(self.get)()`: as the usual `state.get`.
+/// - `(self.update)(value)`: as the usual `state.update`.
+///
+/// *Returns:*
+///
+/// A `state(...)`.
+///
+/// *Arguments:*
+///
+/// - symbol (function): this should be a `()=>{}`.
+/// - default (any): the default value of the state.
+#let safe-state(symbol, default) = { (_state-cls.new)(symbol: symbol, default: default) }
+
+
+#let test-safe-state() = context {
+ let x = safe-state(()=>{}, 1)
+ let y = safe-state(()=>{}, "hello")
+ let loc = here()
+ context {
+ assert.eq((x.final)(), 9)
+ assert.eq((y.final)(), selector(heading))
+ assert.eq((x.get)(), 1)
+ assert.eq((y.get)(), "hello")
+ }
+
+ (x.update)("foo")
+ context {
+ assert.eq((x.get)(), "foo")
+ assert.eq((y.get)(), "hello")
+ }
+ (y.update)(none)
+ context {
+ assert.eq((x.get)(), "foo")
+ assert.eq((y.get)(), none)
+ }
+
+ (x.update)(9)
+ context {
+ assert.eq((x.get)(), 9)
+ assert.eq((y.get)(), none)
+ }
+ (y.update)(selector(heading))
+ context {
+ assert.eq((x.at)(loc), 1)
+ assert.eq((y.at)(loc), "hello")
+ assert.eq((x.get)(), 9)
+ assert.eq((y.get)(), selector(heading))
+ }
+}
+#test-safe-state()
+
+
+#let test-regular-state-for-comparison() = context {
+ let x = state("--typsy-test-sentinel-string1", 1)
+ let y = state("--typsy-test-sentinel-string2", "hello")
+ let loc = here()
+ context {
+ assert.eq(x.final(), 9)
+ assert.eq(y.final(), selector(heading))
+ assert.eq(x.get(), 1)
+ assert.eq(y.get(), "hello")
+ }
+
+ x.update("foo")
+ context {
+ assert.eq(x.get(), "foo")
+ assert.eq(y.get(), "hello")
+ }
+ y.update(none)
+ context {
+ assert.eq(x.get(), "foo")
+ assert.eq(y.get(), none)
+ }
+
+ x.update(9)
+ context {
+ assert.eq(x.get(), 9)
+ assert.eq(y.get(), none)
+ }
+ y.update(selector(heading))
+ context {
+ assert.eq(x.at(loc), 1)
+ assert.eq(y.at(loc), "hello")
+ assert.eq(x.get(), 9)
+ assert.eq(y.get(), selector(heading))
+ }
+}
+#test-regular-state-for-comparison()
diff --git a/packages/preview/typsy/0.2.1/src/tree-counters.typ b/packages/preview/typsy/0.2.1/src/tree-counters.typ
new file mode 100644
index 0000000000..3e808f9bd4
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/tree-counters.typ
@@ -0,0 +1,714 @@
+#import "./classes.typ": class
+#import "./format.typ": fmt, panic-fmt
+#import "./match.typ": Arguments, Counter, Function, Int, Literal, Str, Union, matches
+#import "./safe-counters.typ": safe-counter
+#import "./typecheck.typ": typecheck
+
+/// - value (int, function)
+/// - default (none, int)
+#let _check_update(value, default) = {
+ if type(value) == function {
+ if default != none {
+ value = x => value(x + default) - default
+ }
+ value = typecheck(Arguments(Int), Int, value, invalid-return: (_, _) => error_msg)
+ } else if type(value) == int {
+ if default != none {
+ value = value - default
+ }
+ } else {
+ panic-fmt("`The `value` in `(counter.update)(value)` must either be an `int`, or a function `int -> int`.")
+ }
+ value
+}
+
+/// - numbers (array<..int>)
+/// - default (none, int)
+#let _offset(numbers, default) = {
+ if default == none {
+ numbers
+ } else {
+ assert.eq(numbers.len(), 1)
+ (numbers.at(0) + default,)
+ }
+}
+
+
+#let TreeCounter = class(
+ name: "TreeCounter",
+ fields: (
+ display: Function, /*self->str*/
+ get: Function, /*self->array*/
+ step: Function, /*self->content*/
+ update: Function, /*(self, union int>>)->content*/
+ ),
+ methods: (
+ subcounter: (self, symbol /*function*/, numbering: ".1", default: 0) => /*Counter*/ {
+ let get_raw_counter() = {
+ let parent_index = (self.get)().map(str).join(".")
+ // This is a sneaky undocumented (but tested) usage of `safe-counter`.
+ safe-counter((parent_index, symbol))
+ }
+ let display(_) = {
+ (self.display)() + if numbering == "" {""} else {get_raw_counter().display(numbering)}
+ }
+ let get(_) = (self.get)() + _offset(get_raw_counter().get(), default)
+ // `step` needs `context` as it needs to resolve its parent counter via `parent.get()`.
+ let step(_) = context get_raw_counter().step()
+ let update(_, value) = context get_raw_counter().update(_check_update(value, default))
+ (self.meta.cls.new)(display: display, get: get, step: step, update: update)
+ },
+ take: self => /*content*/ {
+ (self.step)()
+ context (self.display)()
+ },
+ ),
+)
+
+/// - raw_counter (counter):
+/// - level (int):
+/// - get_numbering (function): ()->union(str,function)
+/// - default (none, int): default value for the counter.
+#let _tree-counter(raw_counter, level, get_numbering, default) = {
+ let display = self => {
+ let n = get_numbering()
+ if n == "" {
+ ""
+ } else {
+ numbering(n, ..(self.get)())
+ }
+ }
+ let get(self) = {
+ let raw_get = _offset(raw_counter.get(), default)
+ let diff = level - raw_get.len()
+ if diff <= 0 {
+ raw_get.slice(0, level)
+ } else {
+ raw_get + (0,) * diff
+ }
+ }
+ let step = self => raw_counter.step(level: level)
+ let update(self, value) = {
+ raw_counter.update(_check_update(value, default))
+ }
+ (TreeCounter.new)(display: display, get: get, step: step, update: update)
+}
+
+#let _get-numbering(named, x) = {
+ () => named.at("numbering", default: if x == none or x.numbering == none { "1.1" } else { x.numbering })
+}
+
+/// Creates the root node of a tree-of-counters. Subcounters are then created using its `.subcounter` method.
+///
+/// *Example:*
+///
+/// ```typst
+/// #let heading-counter = tree-counter(heading, level: 1)
+/// #let theorem-counter = (heading-counter.subcounter)(()=>{}, numbering: ".A")
+/// #(heading-counter.step)()
+/// #(theorem-counter.step)()
+/// #(theorem-counter.step)()
+/// #context assert.eq((theorem-counter.display)(), "1.B")
+/// ```
+///
+/// *Constructors:*
+///
+/// This can be called in a few distinct ways, corresponding to the following call signatures:
+///
+/// 1. To create a brand-new counter: `tree-counter(()=>{})`
+/// - with non-zero default: `tree-counter(()=>{}, default: 3)`.
+/// 2. Track an existing counter at some level: `tree-counter(some-counter)`
+/// - with non-one level: `tree-counter(some-counter, level: 2)`.
+/// 3. To track the `heading` counter: `tree-counter(heading)`.
+/// - with non-one level: `tree-counter(heading, level: 2)`.
+/// 4. To track the `page` counter: `tree-counter(page)`.
+/// 5. To track the `figure` counter: `tree-counter(figure)`.
+///
+/// In all cases then `numbering` can be passed, e.g. `tree-counter(()=>{}, numbering: "A.")`, to use a non-default
+/// numbering.
+///
+/// Notes:
+///
+/// - When creating a brand-new counter, then the first argument should be specifically the anonymous function
+/// `()=>{}`. Like with `safe-counter`, we use the distinctness of all anonymous functions to assign a unique
+/// identity to our counter.
+/// - Wrapping an existing counter, or the `heading` counter, requires `level` as a mandatory keyword argument. This is
+/// because tree-counters do not have a notion of `level`. Just create subcounters instead! Correspondingly when
+/// wrapping an existing counter, then we must provide the level of that counter that we wish to track. (This does
+/// not apply to the `page` counter, which seems to be special and is always a level-1 counter.)
+/// - The `numbering` arguments should be a `str` or `function`, as per what can be passed to the builtin
+/// `numbering(...)` function. This is provided at initialisation time, rather than `display` time, so that child
+/// counters are not required to specify how all their parents should be `display`ed. (And we respect the existing
+/// `heading.numbering` or `page.numbering` if tracking those counters.)
+///
+/// Examples:
+///
+/// 1. `tree-counter(()=>{})` to create a brand new counter.
+/// 2. `tree-counter(counter("foo"), level: 1)` to track an existing counter.
+/// 3. `tree-counter(heading, level: 2)` to track the heading counter (at level 2).
+///
+/// *Methods:*
+///
+/// It provides the following of the usual `counter` methods. In every case note that they must be surrounded with an
+/// extra pair of brackets, due to Typst limitations.
+///
+/// - `(self.display)()`: as the usual `counter.display`.
+/// Note that this does not take a `numbering` argument: provide this at initialisation time instead. (This is so
+/// that each node in the tree can specify how it should be displayed without requiring all children to do so.)
+/// - `(self.get)()`: as the usual `counter.get`.
+/// - `(self.step)()`: as the usual `counter.step`.
+/// Note that this does not take a `level` argument. Create a `.subcounter` instead.
+/// - `(self.subcounter)(()=>{}, numbering: ".1", default : 0)`: the star of the show! This creates a subcounter. The
+/// first argument should be specifically `()=>{}`, in the same way as described for the main constructor. The
+/// numbering is optional and used when `.display`ing the subcounter. The default is the initial value of the
+/// counter.
+/// - `(self.take)()`: this is a useful shorthand for `(self.step)()` followed by `(self.display)()`.
+/// - `(self.update)(value)`: as the usual `counter.update`.
+///
+/// We intentionally do not provide `.at` or `.final` methods, as these are very difficult to reason about when using
+/// trees of counters.
+///
+/// *Returns:*
+///
+/// A `TreeCounter` object.
+///
+/// *Arguments:*
+///
+/// As described in "Constructors' above.
+#let tree-counter(..args) = {
+ let pos = args.pos()
+ let named = args.named()
+ let heading_matches = Union(
+ Arguments(Literal(heading), numbering: Union(Str, Function), level: Int),
+ Arguments(Literal(heading), numbering: Union(Str, Function)),
+ Arguments(Literal(heading), level: Int),
+ Arguments(Literal(heading)),
+ )
+ let page_matches = Union(
+ Arguments(Literal(page), numbering: Union(Str, Function)),
+ Arguments(Literal(page)),
+ )
+ let figure_matches = Union(
+ Arguments(Literal(figure), numbering: Union(Str, Function)),
+ Arguments(Literal(figure)),
+ )
+ let new_matches = Union(
+ Arguments(Function, numbering: Union(Str, Function), default: Int),
+ Arguments(Function, numbering: Union(Str, Function)),
+ Arguments(Function, default: Int),
+ Arguments(Function),
+ )
+ let existing_matches = Union(
+ Arguments(Counter, numbering: Union(Str, Function), level: Int),
+ Arguments(Counter, numbering: Union(Str, Function)),
+ Arguments(Counter, level: Int),
+ Arguments(Counter),
+ )
+
+ if matches(heading_matches, args) {
+ // Track heading counter
+ let get-numbering = _get-numbering(named, heading)
+ _tree-counter(counter(heading), named.at("level", default: 1), get-numbering, none)
+ } else if matches(page_matches, args) {
+ // Track page counter
+ let get-numbering = _get-numbering(named, page)
+ _tree-counter(counter(page), 1, get-numbering, none)
+ } else if matches(figure_matches, args) {
+ // Track figure counter
+ let get-numbering = _get-numbering(named, figure)
+ _tree-counter(counter(figure), 1, get-numbering, none)
+ } else if matches(new_matches, args) {
+ // New counter
+ let (symbol,) = pos
+ let get-numbering = _get-numbering(named, none)
+ let default = named.at("default", default: 0)
+ _tree-counter(safe-counter(symbol), 1, get-numbering, default)
+ } else if matches(existing_matches, args) {
+ // Track existing counter
+ let (count,) = pos
+ let get-numbering = _get-numbering(named, none)
+ _tree-counter(count, named.at("level", default: 1), get-numbering, none)
+ } else {
+ panic-fmt(
+ "Received invalid arguments `{}` that did not match any call signature for `tree-counter`.",
+ repr(args),
+ )
+ }
+}
+
+#let test-new-counter() = {
+ let root = tree-counter(() => {})
+ context assert.eq((root.get)(), (0,))
+ context assert.eq((root.display)(), "0")
+ (root.step)()
+ context assert.eq((root.get)(), (1,))
+ context assert.eq((root.display)(), "1")
+ (root.step)()
+ context assert.eq((root.get)(), (2,))
+ context assert.eq((root.display)(), "2")
+
+ let subcounter1 = (root.subcounter)(() => {})
+ context assert.eq((subcounter1.get)(), (2, 0))
+ context assert.eq((subcounter1.display)(), "2.0")
+ (subcounter1.step)()
+ context assert.eq((subcounter1.get)(), (2, 1))
+ context assert.eq((subcounter1.display)(), "2.1")
+ (root.step)()
+ context assert.eq((subcounter1.get)(), (3, 0))
+ context assert.eq((subcounter1.display)(), "3.0")
+
+ let subcounter2 = (root.subcounter)(() => {}, numbering: ".A")
+ context assert.eq((subcounter2.get)(), (3, 0))
+ context assert.eq((subcounter2.display)(), "3.-")
+ (subcounter2.step)()
+ context assert.eq((subcounter2.get)(), (3, 1))
+ context assert.eq((subcounter2.display)(), "3.A")
+ (root.step)()
+ context assert.eq((subcounter2.get)(), (4, 0))
+ context assert.eq((subcounter2.display)(), "4.-")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9,))
+ context assert.eq((subcounter2.get)(), (9, 0))
+ context (subcounter2.update)(4)
+ context assert.eq((subcounter2.get)(), (9, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19,))
+ context assert.eq((subcounter2.get)(), (19, 0))
+ (subcounter2.update)(x => x + 10)
+ context assert.eq((subcounter2.get)(), (19, 10))
+}
+
+#let test-new-counter-with-numbering() = {
+ let root = tree-counter(() => {}, numbering: "A.")
+ context assert.eq((root.display)(), "-.")
+ (root.step)()
+ context assert.eq((root.get)(), (1,))
+ context assert.eq((root.display)(), "A.")
+ (root.step)()
+ context assert.eq((root.get)(), (2,))
+ context assert.eq((root.display)(), "B.")
+
+ let subcounter1 = (root.subcounter)(() => {})
+ context assert.eq((subcounter1.get)(), (2, 0))
+ context assert.eq((subcounter1.display)(), "B..0")
+ (subcounter1.step)()
+ context assert.eq((subcounter1.get)(), (2, 1))
+ context assert.eq((subcounter1.display)(), "B..1")
+ (root.step)()
+ context assert.eq((subcounter1.get)(), (3, 0))
+ context assert.eq((subcounter1.display)(), "C..0")
+
+ let subcounter2 = (root.subcounter)(() => {}, numbering: "A")
+ context assert.eq((subcounter2.get)(), (3, 0))
+ context assert.eq((subcounter2.display)(), "C.-")
+ (subcounter2.step)()
+ context assert.eq((subcounter2.get)(), (3, 1))
+ context assert.eq((subcounter2.display)(), "C.A")
+ (root.step)()
+ context assert.eq((subcounter2.get)(), (4, 0))
+ context assert.eq((subcounter2.display)(), "D.-")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9,))
+ context assert.eq((subcounter2.get)(), (9, 0))
+ context (subcounter2.update)(4)
+ context assert.eq((subcounter2.get)(), (9, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19,))
+ context assert.eq((subcounter2.get)(), (19, 0))
+ (subcounter2.update)(x => x + 10)
+ context assert.eq((subcounter2.get)(), (19, 10))
+}
+
+#let test-wrap-counter() = {
+ let root = tree-counter(safe-counter(() => {}), level: 2)
+ context assert.eq((root.get)(), (0, 0))
+ context assert.eq((root.display)(), "0.0")
+ (root.step)()
+ context assert.eq((root.get)(), (0, 1))
+ context assert.eq((root.display)(), "0.1")
+ (root.step)()
+ context assert.eq((root.get)(), (0, 2))
+ context assert.eq((root.display)(), "0.2")
+
+ let subcounter1 = (root.subcounter)(() => {})
+ context assert.eq((subcounter1.get)(), (0, 2, 0))
+ context assert.eq((subcounter1.display)(), "0.2.0")
+ (subcounter1.step)()
+ context assert.eq((subcounter1.get)(), (0, 2, 1))
+ context assert.eq((subcounter1.display)(), "0.2.1")
+ (root.step)()
+ context assert.eq((subcounter1.get)(), (0, 3, 0))
+ context assert.eq((subcounter1.display)(), "0.3.0")
+
+ let subcounter2 = (root.subcounter)(() => {}, numbering: ".A")
+ context assert.eq((subcounter2.get)(), (0, 3, 0))
+ context assert.eq((subcounter2.display)(), "0.3.-")
+ (subcounter2.step)()
+ context assert.eq((subcounter2.get)(), (0, 3, 1))
+ context assert.eq((subcounter2.display)(), "0.3.A")
+ (root.step)()
+ context assert.eq((subcounter2.get)(), (0, 4, 0))
+ context assert.eq((subcounter2.display)(), "0.4.-")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9, 0))
+ context assert.eq((subcounter2.get)(), (9, 0, 0))
+ context (subcounter2.update)(4)
+ context assert.eq((subcounter2.get)(), (9, 0, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19, 0))
+ context assert.eq((subcounter2.get)(), (19, 0, 0))
+ (subcounter2.update)(x => x + 10)
+ context assert.eq((subcounter2.get)(), (19, 0, 10))
+}
+
+#let test-wrap-counter-with-numbering() = {
+ let root = tree-counter(safe-counter(() => {}), numbering: "1.", level: 2)
+ context assert.eq((root.get)(), (0, 0))
+ context assert.eq((root.display)(), "0.0.")
+ (root.step)()
+ context assert.eq((root.get)(), (0, 1))
+ context assert.eq((root.display)(), "0.1.")
+ (root.step)()
+ context assert.eq((root.get)(), (0, 2))
+ context assert.eq((root.display)(), "0.2.")
+
+ let subcounter1 = (root.subcounter)(() => {})
+ context assert.eq((subcounter1.get)(), (0, 2, 0))
+ context assert.eq((subcounter1.display)(), "0.2..0")
+ (subcounter1.step)()
+ context assert.eq((subcounter1.get)(), (0, 2, 1))
+ context assert.eq((subcounter1.display)(), "0.2..1")
+ (root.step)()
+ context assert.eq((subcounter1.get)(), (0, 3, 0))
+ context assert.eq((subcounter1.display)(), "0.3..0")
+
+ let subcounter2 = (root.subcounter)(() => {}, numbering: "A")
+ context assert.eq((subcounter2.get)(), (0, 3, 0))
+ context assert.eq((subcounter2.display)(), "0.3.-")
+ (subcounter2.step)()
+ context assert.eq((subcounter2.get)(), (0, 3, 1))
+ context assert.eq((subcounter2.display)(), "0.3.A")
+ (root.step)()
+ context assert.eq((subcounter2.get)(), (0, 4, 0))
+ context assert.eq((subcounter2.display)(), "0.4.-")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9, 0))
+ context assert.eq((subcounter2.get)(), (9, 0, 0))
+ context (subcounter2.update)(4)
+ context assert.eq((subcounter2.get)(), (9, 0, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19, 0))
+ context assert.eq((subcounter2.get)(), (19, 0, 0))
+ (subcounter2.update)(x => x + 10)
+ context assert.eq((subcounter2.get)(), (19, 0, 10))
+}
+
+#let test-heading-counter() = {
+ counter(heading).update(0)
+ // Test that we match the default numbering (when `heading.numbering = none`).
+ context assert.eq(counter(heading).display(), "0")
+ counter(heading).step(level: 2)
+ context assert.eq(counter(heading).display(), "0.1")
+ let root = tree-counter(heading, level: 3)
+ context assert.eq((root.get)(), (0, 1, 0))
+ context assert.eq((root.display)(), "0.1.0")
+ (root.step)()
+ context assert.eq((root.get)(), (0, 1, 1))
+ context assert.eq((root.display)(), "0.1.1")
+ (root.step)()
+ // Test that we track any changes to `heading.numbering`.
+ set heading(numbering: "1.A.")
+ context assert.eq((root.get)(), (0, 1, 2))
+ context assert.eq((root.display)(), "0.A.B.")
+ // Test that we track exogeneous changes to the counter.
+ counter(heading).step(level: 1)
+ context assert.eq((root.get)(), (1, 0, 0))
+ context assert.eq((root.display)(), "1.-.-.")
+
+ let subcounter1 = (root.subcounter)(() => {})
+ context assert.eq((subcounter1.get)(), (1, 0, 0, 0))
+ context assert.eq((subcounter1.display)(), "1.-.-..0")
+ (subcounter1.step)()
+ context assert.eq((subcounter1.get)(), (1, 0, 0, 1))
+ context assert.eq((subcounter1.display)(), "1.-.-..1")
+ (root.step)()
+ context assert.eq((subcounter1.get)(), (1, 0, 1, 0))
+ context assert.eq((subcounter1.display)(), "1.-.A..0")
+
+ let subcounter2 = (root.subcounter)(() => {}, numbering: ".A")
+ context assert.eq((subcounter2.get)(), (1, 0, 1, 0))
+ context assert.eq((subcounter2.display)(), "1.-.A..-")
+ (subcounter2.step)()
+ context assert.eq((subcounter2.get)(), (1, 0, 1, 1))
+ context assert.eq((subcounter2.display)(), "1.-.A..A")
+ (root.step)()
+ context assert.eq((subcounter2.get)(), (1, 0, 2, 0))
+ context assert.eq((subcounter2.display)(), "1.-.B..-")
+
+ // Test that we track any changes to `heading.numbering`.
+ set heading(numbering: "1.1")
+ context assert.eq((subcounter2.display)(), "1.0.2.-")
+
+ // Test that we track exogeneous changes to the counter.
+ counter(heading).step(level: 1)
+ context assert.eq((subcounter2.display)(), "2.0.0.-")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9, 0, 0))
+ context assert.eq((subcounter2.get)(), (9, 0, 0, 0))
+ (subcounter2.update)(4)
+ context assert.eq((subcounter2.get)(), (9, 0, 0, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19, 0, 0))
+ context assert.eq((subcounter2.get)(), (19, 0, 0, 0))
+ (subcounter2.update)(x => x + 10)
+ context assert.eq((subcounter2.get)(), (19, 0, 0, 10))
+}
+
+#let test-heading-counter-without-level() = {
+ counter(heading).update(0)
+ let root = tree-counter(heading)
+ context assert.eq((root.get)(), (0, 1, 0))
+ context assert.eq((root.display)(), "0.1.0")
+ (root.step)()
+ context assert.eq((root.get)(), (0, 1, 1))
+ context assert.eq((root.display)(), "0.1.1")
+ (root.step)()
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9,))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19,))
+}
+
+#let test-page-counter() = {
+ counter(page).update(1)
+ // Test that we match the default numbering (when `page.numbering = none`).
+ context assert.eq(counter(page).display(), "1")
+ counter(page).step()
+ context assert.eq(counter(page).display(), "2")
+ let root = tree-counter(page)
+ context assert.eq((root.get)(), (2,))
+ context assert.eq((root.display)(), "2")
+ (root.step)()
+ context assert.eq((root.get)(), (3,))
+ context assert.eq((root.display)(), "3")
+ (root.step)()
+ // Test that we track any changes to `page.numbering`.
+ set page(numbering: "A.")
+ context assert.eq((root.get)(), (4,))
+ context assert.eq((root.display)(), "D.")
+
+ let subcounter1 = (root.subcounter)(() => {})
+ context assert.eq((subcounter1.get)(), (4, 0))
+ context assert.eq((subcounter1.display)(), "D..0")
+ (subcounter1.step)()
+ context assert.eq((subcounter1.get)(), (4, 1))
+ context assert.eq((subcounter1.display)(), "D..1")
+ (root.step)()
+ context assert.eq((subcounter1.get)(), (5, 0))
+ context assert.eq((subcounter1.display)(), "E..0")
+
+ let subcounter2 = (root.subcounter)(() => {}, numbering: "A")
+ context assert.eq((subcounter2.get)(), (5, 0))
+ context assert.eq((subcounter2.display)(), "E.-")
+ (subcounter2.step)()
+ context assert.eq((subcounter2.get)(), (5, 1))
+ context assert.eq((subcounter2.display)(), "E.A")
+ (root.step)()
+ context assert.eq((subcounter2.get)(), (6, 0))
+ context assert.eq((subcounter2.display)(), "F.-")
+
+ // Test that we track any changes to `heading.numbering`.
+ set page(numbering: "1")
+ context assert.eq((subcounter2.display)(), "6-")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9,))
+ context assert.eq((subcounter2.get)(), (9, 0))
+ context (subcounter2.update)(4)
+ context assert.eq((subcounter2.get)(), (9, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19,))
+ context assert.eq((subcounter2.get)(), (19, 0))
+ (subcounter2.update)(x => x + 10)
+ context assert.eq((subcounter2.get)(), (19, 10))
+}
+
+#let test-figure-counter() = {
+ counter(figure).update(1)
+ // Test that we match the default numbering (when `figure.numbering = none`).
+ context assert.eq(counter(figure).display(), "1")
+ counter(figure).step()
+ context assert.eq(counter(figure).display(), "2")
+ let root = tree-counter(figure)
+ context assert.eq((root.get)(), (2,))
+ context assert.eq((root.display)(), "2")
+ (root.step)()
+ context assert.eq((root.get)(), (3,))
+ context assert.eq((root.display)(), "3")
+ (root.step)()
+ // Test that we track any changes to `figure.numbering`.
+ set figure(numbering: "A.")
+ context assert.eq((root.get)(), (4,))
+ context assert.eq((root.display)(), "D.")
+
+ let subcounter1 = (root.subcounter)(() => {})
+ context assert.eq((subcounter1.get)(), (4, 0))
+ context assert.eq((subcounter1.display)(), "D..0")
+ (subcounter1.step)()
+ context assert.eq((subcounter1.get)(), (4, 1))
+ context assert.eq((subcounter1.display)(), "D..1")
+ (root.step)()
+ context assert.eq((subcounter1.get)(), (5, 0))
+ context assert.eq((subcounter1.display)(), "E..0")
+
+ let subcounter2 = (root.subcounter)(() => {}, numbering: "A")
+ context assert.eq((subcounter2.get)(), (5, 0))
+ context assert.eq((subcounter2.display)(), "E.-")
+ (subcounter2.step)()
+ context assert.eq((subcounter2.get)(), (5, 1))
+ context assert.eq((subcounter2.display)(), "E.A")
+ (root.step)()
+ context assert.eq((subcounter2.get)(), (6, 0))
+ context assert.eq((subcounter2.display)(), "F.-")
+
+ // Test that we track any changes to `heading.numbering`.
+ set figure(numbering: "1")
+ context assert.eq((subcounter2.display)(), "6-")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9,))
+ context assert.eq((subcounter2.get)(), (9, 0))
+ context (subcounter2.update)(4)
+ context assert.eq((subcounter2.get)(), (9, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19,))
+ context assert.eq((subcounter2.get)(), (19, 0))
+ (subcounter2.update)(x => x + 10)
+ context assert.eq((subcounter2.get)(), (19, 10))
+}
+
+#let test-empty-numbering() = {
+ let root = tree-counter(()=>{}, numbering: "")
+ let sub1 = (root.subcounter)(()=>{}, numbering: "(a)")
+ context assert.eq((sub1.display)(), "(-)")
+ (sub1.step)()
+ context assert.eq((sub1.display)(), "(a)")
+}
+
+#let test-sub-subcounter() = {
+ let root = tree-counter(() => {})
+ let sub1 = (root.subcounter)(() => {})
+ let sub2 = (sub1.subcounter)(() => {})
+ context assert.eq((sub2.get)(), (0, 0, 0))
+ context assert.eq((sub2.display)(), "0.0.0")
+ (root.step)()
+ context assert.eq((sub2.display)(), "1.0.0")
+ (sub2.step)()
+ context assert.eq((sub2.display)(), "1.0.1")
+ (sub1.step)()
+ context assert.eq((sub2.display)(), "1.1.0")
+
+ (root.update)(9)
+ context assert.eq((root.get)(), (9,))
+ context assert.eq((sub2.get)(), (9, 0, 0))
+ context (sub2.update)(4)
+ context assert.eq((sub2.get)(), (9, 0, 4))
+ (root.update)(x => x + 10)
+ context assert.eq((root.get)(), (19,))
+ context assert.eq((sub2.get)(), (19, 0, 0))
+ (sub2.update)(x => x + 10)
+ context assert.eq((sub2.get)(), (19, 0, 10))
+}
+
+#let test-default() = {
+ let root = tree-counter(()=>{}, default: 3)
+ context assert.eq((root.get)(), (3,))
+ (root.step)()
+ context assert.eq((root.get)(), (4,))
+ (root.step)()
+ context assert.eq((root.get)(), (5,))
+ (root.update)(7)
+ context assert.eq((root.get)(), (7,))
+ (root.update)(12)
+ context assert.eq((root.get)(), (12,))
+ (root.step)()
+ context assert.eq((root.get)(), (13,))
+ (root.update)(x=>calc.pow(x, 2))
+ context assert.eq((root.get)(), (169,))
+ (root.step)()
+ context assert.eq((root.get)(), (170,))
+
+ let s = (root.subcounter)(()=>{}, default: -3)
+ context assert.eq((s.get)(), (170, -3))
+ (s.step)()
+ context assert.eq((s.get)(), (170, -2))
+ (s.step)()
+ context assert.eq((s.get)(), (170, -1))
+ (s.update)(5)
+ context assert.eq((s.get)(), (170, 5))
+ (s.update)(x=>calc.pow(x, 2))
+ context assert.eq((s.get)(), (170, 25))
+ (root.step)()
+ context assert.eq((s.get)(), (171, -3))
+ (s.update)(x=>calc.pow(x, 2))
+ context assert.eq((s.get)(), (171, 9))
+
+ let ss = (s.subcounter)(()=>{}, default: 4)
+ context assert.eq((ss.get)(), (171, 9, 4))
+ (ss.step)()
+ context assert.eq((ss.get)(), (171, 9, 5))
+ (s.step)()
+ context assert.eq((ss.get)(), (171, 10, 4))
+ (ss.update)(x=>calc.pow(x, 2))
+ context assert.eq((ss.get)(), (171, 10, 16))
+
+}
+
+#let test-doc() = {
+ counter(heading).update(0)
+ let heading-counter = tree-counter(heading, level: 1)
+ let theorem-counter = (heading-counter.subcounter)(() => {}, numbering: ".A")
+ (heading-counter.step)()
+ (theorem-counter.step)()
+ (theorem-counter.step)()
+ context assert.eq((theorem-counter.display)(), "1.B")
+}
+
+#let panic-on-bad-new-counter() = {
+ tree-counter(3)
+}
+
+#let panic-on-bad-new-counter-with-numbering() = {
+ tree-counter(3, numbering: "A.")
+}
+
+#let panic-on-bad-wrap-counter() = {
+ tree-counter(3, level: 1)
+}
+
+#let panic-on-bad-wrap-counter-with-numbering() = {
+ tree-counter(3, level: 1, numbering: "A.")
+}
+
+#let panic-on-bad-keywords() = {
+ tree-counter(() => {}, foo: 3)
+}
+
+// Need to include these as they include `context` asserts: https://github.com/Myriad-Dreamin/tinymist/issues/2128
+#test-new-counter()
+#test-new-counter-with-numbering()
+#test-wrap-counter()
+#test-wrap-counter-with-numbering()
+#test-heading-counter()
+#test-page-counter()
+#test-figure-counter()
+#test-empty-numbering()
+#test-sub-subcounter()
+#test-default()
+#test-doc()
diff --git a/packages/preview/typsy/0.2.1/src/typecheck.typ b/packages/preview/typsy/0.2.1/src/typecheck.typ
new file mode 100644
index 0000000000..509c0dd908
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/src/typecheck.typ
@@ -0,0 +1,62 @@
+#import "./match.typ": Arguments, Int, matches
+#import "./format.typ": fmt
+
+/// Wraps a function with runtime typechecking.
+///
+/// *Example:*
+///
+/// ```typst
+/// #let add_integers = typecheck(Arguments(Int, Int), Int, (x, y) => x + y)
+/// #let five = add_integers(2, 3)
+/// #let will_panic = add_integers("hello ", "world")
+/// ```
+///
+/// *Returns:*
+///
+/// A wrapped version of `fn`, that consumes the same arguments and returns the same output, but which will panic if the
+/// arguments do not match `args-type` or if the return value does not match `return-type`.
+///
+/// *Arguments:*
+///
+/// - args-type (Pattern): typically an `Arguments(...)` object specifying the expected arguments. Can in general be any
+/// pattern, e.g. `Any` to accept anything, `Union(Arguments(...), Arguments(...))` to accept multiple arguments,
+/// etc.
+/// - return-type (Pattern): the pattern to check the return value of `fn` against.
+/// - fn (function): the function to wrap.
+/// - invalid-args (function): a function `args -> str` formatting the error message for invalid arguments.
+/// - invalid-return (function): a function `(args, returnval) -> str` formatting the error message for an invalid
+/// return value.
+#let typecheck(
+ args-type,
+ return-type,
+ fn,
+ invalid-args: args => fmt("Arguments `{}` do not match type annotation.", repr(args)),
+ invalid-return: (args, returnval) => fmt("Return value `{}` does not match type annotation.", repr(returnval)),
+) = {
+ (..args) => {
+ if not matches(args-type, args) {
+ panic(invalid-args(args))
+ }
+ let returnval = fn(..args)
+ if not matches(return-type, returnval) {
+ panic(invalid-return(args, returnval))
+ }
+ returnval
+ }
+}
+
+
+#let _add_integers = typecheck(Arguments(Int, Int), Int, (x, y) => x + y)
+#let test-typecheck() = {
+ let five = _add_integers(2, 3)
+ assert.eq(five, 5)
+}
+
+#let panic-on-typecheck-args() = {
+ _add_integers("hello ", "world")
+}
+
+#let _add_integers_buggy = typecheck(Arguments(Int, Int), Int, (x, y) => repr(x + y))
+#let panic-on-typecheck-return() = {
+ _add_integers_buggy(2, 3)
+}
diff --git a/packages/preview/typsy/0.2.1/typst.toml b/packages/preview/typsy/0.2.1/typst.toml
new file mode 100644
index 0000000000..0fc214b225
--- /dev/null
+++ b/packages/preview/typsy/0.2.1/typst.toml
@@ -0,0 +1,10 @@
+[package]
+name = "typsy"
+version = "0.2.1"
+entrypoint = "src/lib.typ"
+authors = ["Patrick Kidger "]
+license = "Apache-2.0"
+description = "Classes/structs, pattern matching, safe counters... and more! Your one-stop library for programming tools not already in core Typst."
+repository = "https://github.com/patrick-kidger/typsy"
+keywords = ["class", "struct", "pattern", "match", "counter", "namespace", "functional", "typecheck", "type", "enum", "enumeration", "variant", "trait", "abstract", "programmatic", "programming", "scripting"]
+categories = ["scripting"]