diff --git a/.obsidian/plugins/typst-mate/caches/preview_elembic_1.1.1.cache b/.obsidian/plugins/typst-mate/caches/preview_elembic_1.1.1.cache new file mode 100644 index 0000000..cfbc9ab Binary files /dev/null and b/.obsidian/plugins/typst-mate/caches/preview_elembic_1.1.1.cache differ diff --git a/.obsidian/plugins/typst-mate/caches/preview_lilaq_0.5.0.cache b/.obsidian/plugins/typst-mate/caches/preview_lilaq_0.5.0.cache new file mode 100644 index 0000000..2800639 Binary files /dev/null and b/.obsidian/plugins/typst-mate/caches/preview_lilaq_0.5.0.cache differ diff --git a/.obsidian/plugins/typst-mate/caches/preview_mannot_0.3.1.cache b/.obsidian/plugins/typst-mate/caches/preview_mannot_0.3.1.cache new file mode 100644 index 0000000..224cb25 Binary files /dev/null and b/.obsidian/plugins/typst-mate/caches/preview_mannot_0.3.1.cache differ diff --git a/.obsidian/plugins/typst-mate/caches/preview_quick-maths_0.2.1.cache b/.obsidian/plugins/typst-mate/caches/preview_quick-maths_0.2.1.cache new file mode 100644 index 0000000..1697dc2 Binary files /dev/null and b/.obsidian/plugins/typst-mate/caches/preview_quick-maths_0.2.1.cache differ diff --git a/.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.1.cache b/.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.1.cache new file mode 100644 index 0000000..6d48107 Binary files /dev/null and b/.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.1.cache differ diff --git a/.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.2.cache b/.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.2.cache new file mode 100644 index 0000000..895154b Binary files /dev/null and b/.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.2.cache differ diff --git a/.obsidian/plugins/typst-mate/caches/preview_zero_0.5.0.cache b/.obsidian/plugins/typst-mate/caches/preview_zero_0.5.0.cache new file mode 100644 index 0000000..add45d8 Binary files /dev/null and b/.obsidian/plugins/typst-mate/caches/preview_zero_0.5.0.cache differ diff --git a/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE new file mode 100644 index 0000000..03f646f --- /dev/null +++ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE @@ -0,0 +1 @@ +MIT OR Apache-2.0 diff --git a/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-APACHE b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-APACHE new file mode 100644 index 0000000..71b8799 --- /dev/null +++ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-APACHE @@ -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 2024 PgBiel + + 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/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-MIT b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-MIT new file mode 100644 index 0000000..869be70 --- /dev/null +++ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 PgBiel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/README.md b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/README.md new file mode 100644 index 0000000..b4f8429 --- /dev/null +++ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/README.md @@ -0,0 +1,114 @@ +# elembic +Framework for custom elements and types in Typst. **Supports Typst 0.11.0 or later.** + +**Read the book:** https://pgbiel.github.io/elembic + +**Mirrors:** [GitHub (pgbiel/elembic)](https://github.com/PgBiel/elembic); [Codeberg (pgbiel/elembic)](https://codeberg.org/PgBiel/elembic) + +## About + +Elembic lets you create [**custom elements,**](https://pgbiel.github.io/elembic/elements/creating/index.html) which are **reusable and customizable document components,** with support for **typechecked fields, [show and set rules](https://pgbiel.github.io/elembic/elements/styling/index.html)** (without using any `state` by default!), **[reference and outline support,](https://pgbiel.github.io/elembic/elements/creating/labels-refs.html)** as well as features not present in Typst elements today such as **[revokable rules](https://pgbiel.github.io/elembic/elements/styling/revoke.html)** and **[nested element selectors](https://pgbiel.github.io/elembic/elements/filters/within.html)**. In addition, Elembic lets you create [**custom types,**](https://pgbiel.github.io/elembic/types/custom-types/index.html) which also support **typechecked fields** but also [**custom casting**](https://pgbiel.github.io/elembic/types/custom-types/casts.html), and can be used in element fields or by themselves in arbitrary Typst code. + +Elembic's name comes from "element" + ["alembic"](https://en.wikipedia.org/wiki/Alembic), to indicate that one of Elembic's goals is to experiment with different approaches for this functionality, and to help shape a future update to Typst that includes native custom elements, which will eventually remove the need for using this package. + +Its documentation is at the Elembic Handbook: [https://pgbiel.github.io/elembic](https://pgbiel.github.io/elembic) + +This library has a few limitations which are [appropriately noted in the book.](https://pgbiel.github.io/elembic/about/limitations.html) + +## Who is elembic for? + +Elembic is especially suited for: +1. **Packages:** elembic's elements allows creating reusable components which can be freely customized by your package's end users. With typechecking and other features, elembic's got you covered in terms of flexibility. See the ["Simple Theorems" example](https://pgbiel.github.io/elembic/examples/simple-theorems.html) for a sample. +2. **Templates:** elembic's elements can be used for fine-grained configuration of parts of your template. See the ["Simple Thesis" example](https://pgbiel.github.io/elembic/examples/simple-thesis.html) for a sample. + +## Installation + +Just import the latest elembic version from the package manager and you're ready to go! + +```typ +#import "@preview/elembic:1.1.1" as e + +// See the full example below +#show: e.set_(element, stroke: red) +// ... +``` + +## Example + +### Custom element + +```typ +#import "@preview/elembic:1.1.1" as e: field, types + +#let bigbox = e.element.declare( + "bigbox", + prefix: "@preview/my-package,v1", + doc: "A fancy and customizable box.", + display: it => block(fill: it.fill, stroke: it.stroke, inset: 5pt, it.body), + fields: ( + field("body", types.option(content), doc: "Box contents", required: true), + field("fill", types.option(types.paint), doc: "Box fill"), + field("stroke", types.option(stroke), doc: "Box border", default: red) + ) +) + +#bigbox[abc] + +#show: e.set_(bigbox, fill: red) + +#bigbox(stroke: blue + 2pt)[def] +``` + +!['abc' with no fill and a thin red stroke, followed by 'def' with red fill and blue thicker stroke](https://github.com/user-attachments/assets/c852cfcd-c0de-446a-999b-5ecaa44809b7) + +### Custom type + +```typ +#import "@preview/elembic:1.1.1" as e: field, types + +#let person = e.types.declare( + "person", + prefix: "@preview/my-package,v1", + doc: "Relevant data for a person.", + fields: ( + field("name", str, doc: "Person's name", required: true, named: true), + field("age", int, doc: "Person's age", default: 40), + field("preference", types.any, doc: "Anything the person likes", default: none) + ), + casts: ( + (from: dictionary), + (from: str, with: person => name => person(name: name)), + ) +) + +#assert.eq( + e.repr(person(name: "John", age: 50, preference: "soup")), + "person(age: 50, name: \"John\", preference: \"soup\")" +) + +// Manually invoke typechecking and cast +#assert.eq( + types.cast((name: "abc", age: 99), person), + (true, person(name: "abc", age: 99)) +) + +#assert.eq( + types.cast("abc", person), + (true, person(name: "abc")) +) + +// Can then use 'person' as the type of an element's field, for example +``` + +## Source structure + +- `pub/`: Contains public re-exports of each module (so we can keep some things private) +- `data`: Functions and constants related to extracting data from elements and types +- `element`: Functions related to creating and using elements and their rules (set rules, revoke rules and so on) +- `filters`: Functions to create and match element filters +- `types`: Functions and constants related to Elembic's custom type system +- `fields`: Functions related to element and type field parsing + +## License + +Licensed under MIT or Apache-2.0, at your option. diff --git a/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/data.typ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/data.typ new file mode 100644 index 0000000..ee7361e --- /dev/null +++ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/data.typ @@ -0,0 +1,584 @@ +// Functions to extract data from custom elements and types, as well as associated constants. + +// Type constants: + +// Used by typeinfos +#let type-key = "__elembic_type" +// To be used by any custom type instances +#let custom-type-key = "__elembic_custom_type" +// Used by custom types themselves +#let custom-type-data-key = "__elembic_custom_type_data" + +// Versions: +#let element-version = 5 // v1 = alphas 1 and 2, v2 = alpha 3-v1.0.0-rc2, v3 = v1.0.0, v4 = v1.1.0, v5 = v1.1.1+ +#let type-version = 4 // v1 = alphas to v1.0.0-rc2, v2 = v1.0.0, v3 = v1.1.0, v4 = v1.1.1+ +#let custom-type-version = 5 // v1 = alphas 1 and 2, v2 = alpha 3-v1.0.0-rc2, v3 = v1.0.0, v4 = v1.1.0, v5 = v1.1.1+ +#let current-field-version = 4 // v1 = alphas 1 and 2, v2 = alpha 3-v1.0.0-rc2, v3 = v1.0.0-v1.1.0, v4 = v1.1.1+ + +// Potential modes for configuration of styles. +// This defines how we declare a set rule (or similar) +// within a certain scope. +#let style-modes = ( + // Normal mode: we store metadata in a bibliography.title set rule. + // + // Before doing so, we retrieve the original value for bibliography.title, + // allowing us to restore it later. The effect is that the library is + // fully hygienic, that is, the change to bibliography.title is not perceptible. + // + // The downside is that retrieving the original value for bibliography.title costs + // an additional nested context { } call, of which there is a limit of 64. This means + // that, in this mode, you can have up to 32 non-consecutive set rules. + normal: 0, + + // leaky mode: similar to normal mode, but we don't try to preserve the value of bibliography.title + // after applying our changes to the document. This doubles the limit to up to 64 non-consecutive + // set rules since we no longer have an extra step to retrieve the old value, but, as a downside, + // we lose the original value of bibliography.title. While, in a future change, we might be able to + // preserve the FIRST known value, we can't generally preserve its value at later points, so the + // value of bibliography.title is effectively frozen before the first custom set rule. + // + // This mode should be used by package authors which know there won't be a bibliography (or, really, + // any custom user input) at some point to avoid consuming the set rule cost. End users can also use + // this mode if they hit a "max show rule depth exceeded" error. + // + // Note that this mode can only be enabled on individual set rules. + leaky: 1, + + // Stateful mode: this is entirely different from the other modes and should only be set by the end + // user (not by packages). This stores the style chain - and, thus, set rules' updated fields - in + // a 'state()'. This is more likely to be slower and lead to trouble as it triggers at least one + // document relayout. However, **this mode does not have a set rule limit.** Therefore, it can be + // used as a last resort by the end user if they can't fix the "max show rule depth exceeded error". + // + // Enabling this mode is as simple as using `#show: e.stateful.toggle(true)` at the beginning of the + // document. This will trigger a compatibility behavior where existing set rules will push to the + // state, even if they're not in the stateful mode. It will also push existing set rule data into + // the style 'state()'. Therefore, existing set rules are compatible with stateful mode, but this + // only effectively fixes the error if the set rules are individually switched to stateful mode + // with `e.stateful.set_` instead of `e.set_`. + stateful: 2 +) + + +// When on stateful mode, this state holds the sequence of 'data' for each scope. +// The last element on the list is the "current" data. +#let style-state-key = "__elembic_element_state" +#let style-state = state(style-state-key, ()) + +// Element constants: + +// Prefix for the labels added to shown elements. +#let lbl-show-head = "__elembic_element_shown_" + +// Prefix for the labels added to the metadata of each element. +// Used for querying. +#let lbl-meta-head = "__elembic_element_meta_" + +// Prefix for the labels added outside shown elements. +// This is used to be able to effectively apply show-set rules to them. +#let lbl-outer-head = "__elembic_element_outer_" + +// Prefix for counters of elements. +// This is only used if the element isn't 'refable'. +#let lbl-counter-head = "__elembic_element_counter_" + +// Prefix for labels for 'context' which should panic with 'missing prepare(elem)'. +#let lbl-elem-prepare-check-head = "__elembic_elem_prepare_check_" + +// Kind of the special figure used by a labelable element. +#let labelable-elem-figure-kind = "__elembic_element_labelable_figure" + +// Prefix for the figure kind used by 'refable' elements. +// This is not to be confused with figures containing the elements. +// This is the kind for a hidden figure used for ref purposes. +#let lbl-ref-figure-kind-head = "__elembic_element_refable_" + +// Custom label applied to the hidden reference figure when the user specifies their own label. +#let lbl-ref-figure-label-head = "__elembic_element_ref_figure_label_" + +// Label for a context which should panic with 'missing prepare()'... +#let lbl-empty-prepare-check = <__elembic_empty_prepare_check> + +// Label for the hidden figure used for references. +#let lbl-ref-figure = <__elembic_element_ref_figure> + +// Label for context blocks which have access to the virtual stylechain. +#let lbl-get = <__elembic_element_get> + +// Label for metadata indicating an element's initial properties post-construction. +#let lbl-tag = <__elembic_element_tag> + +// Label for metadata indicating a rule's parameters. +#let lbl-rule-tag = <__elembic_element_rule_v2> + +// 'lbl-rule-tag' from older Elembic versions. +#let lbl-old-rule-tag = <__elembic_element_rule> + +// Label for other functions which access or modify the style chain, namely +// 'get', 'select', 'debug-get', 'stateful.toggle'. +// +// This is attached to metadata and the 'special-rule-key' property +// indicates which kind of rule this is. +#let lbl-special-rule-tag = <__elembic_element_special_rule> + +// Label for metadata which stores the global data. +// In practice, this label is never present in the document +// unless one accidentally leaks the 'bibliography.title' +// override from our workaround. +#let lbl-data-metadata = <__elembic_element_global_data_metadata> + +#let lbl-stateful-mode = <__elembic_element_stateful_mode> +#let lbl-normal-mode = <__elembic_element_normal_mode> +#let lbl-leaky-mode = <__elembic_element_leaky_mode> +#let lbl-auto-mode = <__elembic_element_auto_mode> + +// Prefix for labels added by 'select' to matched elements. +// These labels are not specific to eids. +#let lbl-global-select-head = "__elembic_element_global_select_" + +// Special dictionary key to indicate this is a prepared rule. +#let prepared-rule-key = "__elembic-prepared-rule" + +// Special dictionary key to indicate this is a "special rule" +// ('get', 'select', 'debug-get', 'stateful.toggle'). +#let special-rule-key = "__elembic-special-rule" + +// Special dictionary key which stores element context and other data. +#let stored-data-key = "__elembic_stored_element_data" + +// Special dictionary key to indicate this is query metadata for an element. +#let element-meta-key = "__elembic_element_meta" + +#let element-key = "__elembic_element" +#let element-data-key = "__elembic_element_data" +#let global-data-key = "__elembic_element_global_data" +#let filter-key = "__elembic_element_filter" + +#let sequence = [].func() +#let styled = { set text(red); [a] }.func() +#let elem-funcs = (sequence, styled, figure) + +// Special values that can be passed to a type or element's constructor to retrieve some data or show +// some behavior. +#let special-data-values = ( + // Indicate that the constructor should return the type or element's data. + get-data: 0, + // Indicate that the constructor should return an element filter. + get-where: 1, +) + +// Extract data from a type's or element's constructor, as well as convert +// a custom type or element instance into a dictionary with keys such as body (for elements only), +// fields and func, allowing you to access its fields and information when given content (for elements) +// or the type instance (for types). +// +// When given content that is not a custom element, 'body' will be the given value, +// 'fields' will be 'body.fields()' and 'func' will be 'body.func()'. +// +// The returned 'data-kind' indicates which kind of data was retrieved. +#let data(it) = { + if type(it) == function { + it(__elembic_data: special-data-values.get-data) + } else if type(it) == dictionary { + if element-key in it { + (data-kind: "element", ..it) + } else if custom-type-data-key in it { + (data-kind: "custom-type-data", ..it) + } else if custom-type-key in it { + it.at(custom-type-key) + } else if stored-data-key in it { + it.at(stored-data-key) + } else { + (data-kind: "unknown", body: it, fields: (:), func: none, eid: none, fields-known: false, valid: false) + } + } else if type(it) != content { + (data-kind: "unknown", body: it, fields: (:), func: none, eid: none, fields-known: false, valid: false) + } else if ( + it.func() == styled + and it.has("label") + and it.child.func() == sequence + and it.child.children.len() >= 2 + and it.child.children.last().at("label", default: none) == lbl-tag + ) { + // Any labeled elements need to be wrapped in a 'styled' to avoid + // interference when joining with the element in a show rule on the + // label. + it.child.children.last().value + } else if it.func() == sequence and it.children.len() >= 2 { + let last = it.children.last() + if ( + last.at("label", default: none) == lbl-tag + // Workaround for 0.11.0 weirdly placing some 'meta' element sometimes + or sys.version < version(0, 12, 0) and { + last = it.children.at(it.children.len() - 2) + last.at("label", default: none) == lbl-tag + } + // Pre-1.0 elembic versions didn't add a label to metadata + or str(it.at("label", default: "")).starts-with(lbl-show-head) and last.func() == metadata + ) { + // Decomposing a recently-constructed (but not placed) element + last.value + } else { + (data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false) + } + } else if it.func() == figure and it.has("kind") and it.kind == labelable-elem-figure-kind { + let last + if it.body.func() == sequence and it.body.children.len() >= 2 and { + last = it.body.children.last() + ( + last.at("label", default: none) == lbl-tag + // Workaround for 0.11.0 weirdly placing some 'meta' element sometimes + or sys.version < version(0, 12, 0) and { + last = it.children.at(it.children.len() - 2) + last.at("label", default: none) == lbl-tag + } + ) + } { + last.value + } else { + (data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false) + } + } else if ( + it.has("label") + and str(it.label).starts-with(lbl-outer-head) + ) { + (data-kind: "incomplete-element-instance", body: it, fields: (:), func: (:), eid: str(it.label).slice(lbl-outer-head.len()), fields-known: false, valid: false) + } else { + (data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false) + } +} + +// Obtain the fields of a type instance or element instance (custom or not). +// +// SAMPLE USAGE: +// +// #show e.selector(elem): it => { +// let fields = e.fields(it) +// [Hello #fields.name!] +// } +#let fields(it) = { + let info = data(it) + + if type(info) == dictionary and "data-kind" in info { + if info.data-kind in ("content", "element-instance", "type-instance") { + return info.fields + } + } + + (:) +} + +// Obtain context at an element's site. +// +// SAMPLE USAGE: +// +// 1. In show rules: +// +// #show e.selector(elem): it => { +// let (get, ..) = e.ctx(it) +// let other-elem-ctx = get(other-elem) +// [The other element field was set to #other-elem-ctx.field at that point!] +// } +// +// 2. In element declarations: +// +// #e.element.declare( +// ... +// synthesize: it => { +// // Get context for other element +// it.some-field = (e.ctx(it).get)(other-elem).field +// }, +// ... +// ) +#let ctx(it) = { + let info = data(it) + if type(info) == dictionary and "ctx" in info { + info.ctx + } else { + none + } +} + +// Obtain an element's or type's scope (usually a module with important definitions). +// +// SAMPLE USAGE: +// +// #let subelem = e.scope(elem).subelem +#let scope(it) = { + let info = data(it) + if type(info) == dictionary and "scope" in info { + info.scope + } else { + none + } +} + +/// Obtain an element's or custom type's constructor function. +/// For native elements, this will be equivalent to `it.func()`. +/// +/// Useful in custom element show rules, for example. +/// +/// This is equivalent to `e.data(it).func`. +/// +/// SAMPLE USAGE: +/// +/// ```typ +/// #show selector.or(e.selector(elem1), e.selector(elem2)): it => { +/// // Will be either elem1 or elem2 +/// let elem = e.func(it) +/// // 'elem == elem1' works, but comparing 'eid's is recommended +/// if e.eid(elem) == e.eid(elem1) { +/// [This is elem1] +/// } else { +/// [This is elem2] +/// } +/// } +/// ``` +/// +/// - it (any): element/custom type instance (or element/custom type itself) to get the constructor from +/// -> function | none +#let func(it) = { + let info = data(it) + if type(info) == dictionary and "func" in info { + info.func + } else { + none + } +} + +/// Obtain an element's eid. It uniquely distinguishes this element from others, +/// even if they have the same name, by including both its prefix and name. +/// +/// This is equivalent to `e.data(elem).eid`. +/// +/// - elem (any): custom element (or an instance of it) to get 'eid' from +/// -> function | none +#let eid(it) = { + let info = data(it) + if type(info) == dictionary and "eid" in info { + info.eid + } else { + none + } +} + +/// Obtain a custom type's tid. It uniquely distinguishes a custom type from +/// others, even if they have the same name, by including both its prefix and +/// name. Returns `none` on invalid input. +/// +/// This is equivalent to `e.data(typ).tid`. +/// +/// - typ (any): custom type (or an instance of it) to get 'tid' from +/// -> function | none +#let tid(it) = { + let info = data(it) + if type(info) == dictionary and "tid" in info { + info.tid + } else { + none + } +} + +// Obtain an element's counter. +// +// USAGE: +// +// #context { +// [The element counter value is #e.counter(elem).get().first()] +// } +#let counter_(elem) = { + let info = data(elem) + + if type(info) == dictionary and "data-kind" in info and (info.data-kind == "element" or info.data-kind == "element-instance") { + info.counter + } else { + assert(false, message: "elembic: e.counter: this is not an element") + } +} + +/// Get the name of a content's constructor function as a string. +/// +/// Returns 'none' on invalid input. +/// +/// USAGE: +/// +/// ```typ +/// assert.eq(func-name(my-elem()), "my-elem") +/// assert.eq(func-name([= abc]), "heading") +/// ``` +/// +/// - c (content): content to get the constructor function of +/// -> function | none +#let func-name(c) = { + if type(c) == function { + let func-data = data(c) + return if "name" in func-data { + func-data.name + } else { + none + } + } else if type(c) != content { + return none + } + let name = repr(c.func()) + if c.func() in elem-funcs { + let element-data = data(c) + if "eid" in element-data and element-data.eid != none { + name = if "name" in element-data and type(element-data.name) == str { element-data.name } else { "unknown custom element" } + } + } + name +} + +#let _letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" + +/// This is used to obtain a debug representation of custom elements and types. +/// +/// Also supports native types (just calls `repr()` for them). +/// +/// - value (any): value to represent +/// - depth (int): current depth (must start at 0, conservative limit of 10 for now) +/// -> str +#let repr_(value, depth: 0) = { + if depth >= 10 { + return repr(value) + } + let typename = "" + let value-type = type(value) + if value-type == content and value.func() in elem-funcs { + let value-data = data(value) + if "eid" in value-data and value-data.eid != none { + value = value-data.fields + value-type = dictionary + typename = if "name" in value-data and type(value-data.name) == str { + value-data.name + } else { + "unknown-element" + } + } + } + + if value-type == dictionary { + let pairs = if typename != "" { + // Element fields => sort + value.pairs().sorted(key: ((k, _)) => k) + } else if custom-type-key in value { + let type-data = value.at(custom-type-key) + + let id = type-data.id + + typename = if "name" in id { + id.name + } else if id == "custom type" { + return if custom-type-data-key in value { + "custom-type(name: " + repr(value.name) + ", tid: " + repr(value.tid) + ")" + } else { + "custom-type()" + } + } else { + str(id) + } + + type-data.fields.pairs().sorted(key: ((k, _)) => k) + } else { + value.pairs() + } + + typename + "(" + pairs.map(((k, v)) => { + if k.codepoints().all(c => c in _letters) { + k + } else { + repr(k) + } + + ": " + + repr_(v, depth: depth + 1) + }).join(", ") + ")" + } else if value-type == array { + "(" + value.map(repr_.with(depth: depth + 1)).join(", ") + ")" + } else { + repr(value) + } +} + +/// Performs deep equality of values. +/// +/// This is necessary to reliably compare instances of the same element or +/// custom type, as well as data structures containing them such as arrays +/// or dictionary, between different versions of the same element or type, +/// by recursively comparing `eid(a) == eid(b) and fields(a) == fields(b)`. +/// However, this is notably slower than Typst's built-in equality check. +/// +/// - a (any): First value to compare. +/// - b (any): Second value to compare. +/// -> bool +#let eq(a, b) = { + if a == b { + return true + } + if type(a) != type(b) or type(a) not in (content, dictionary, array) { + return false + } + + // Recursively compare until we find a 'false' + let stack = ((a, b),) + let fuel = 3000 + while stack != () { + let (a, b) = stack.pop() + if a == b { + // Good! + continue + } + fuel -= 1 + if fuel == 0 { + return false + } + // Of course, the types must match + let a-type = type(a) + let b-type = type(b) + if a-type != b-type { + return false + } + + if a-type == array { + if a.len() != b.len() { + return false + } + stack += array.zip(a, b) + + // Only have special checks for composed types and custom types and elements + // of same type + } else if (a-type == content or a-type == dictionary) and eid(a) == eid(b) and tid(a) == tid(b) { + if eid(a) != none or tid(a) != none or a-type == content and a.func() == b.func() { + // Same element id, compare their fields + a = fields(a) + b = fields(b) + a-type = type(a) + if a-type != type(b) { + return false + } + } + if a-type != dictionary or a.len() != b.len() { + // Fields were invalid, or content didn't have the same func + return false + } + for (key, a-val) in a { + if key not in b { + return false + } + stack.push((a-val, b.at(key),)) + } + } else { + return false + } + } + + // No checks failed + true +} diff --git a/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/element.typ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/element.typ new file mode 100644 index 0000000..35af18e --- /dev/null +++ b/.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/element.typ @@ -0,0 +1,3693 @@ +#import "data.typ": data, lbl-show-head, lbl-meta-head, lbl-outer-head, lbl-counter-head, lbl-elem-prepare-check-head, lbl-empty-prepare-check, labelable-elem-figure-kind, lbl-ref-figure-kind-head, lbl-ref-figure-label-head, lbl-ref-figure, lbl-get, lbl-tag, lbl-rule-tag, lbl-old-rule-tag, lbl-special-rule-tag, lbl-data-metadata, lbl-stateful-mode, lbl-leaky-mode, lbl-normal-mode, lbl-auto-mode, lbl-global-select-head, prepared-rule-key, stored-data-key, element-key, element-data-key, element-meta-key, global-data-key, filter-key, special-rule-key, special-data-values, custom-type-key, custom-type-data-key, type-key, element-version, style-modes, style-state +#import "fields.typ" as field-internals +#import "filters.typ": verify-filter +#import "types/base.typ" +#import "types/types.typ" + +// Basic elements for our document tree analysis +#let sequence = [].func() +#let space = [ ].func() +#let styled = { set text(red); [a] }.func() +#let state-update-func = state(".").update(1).func() +#let counter-update-func = counter(".").update(1).func() + +// Default library-wide data. +#let default-global-data = ( + (global-data-key): true, + + // Keep track of versions in case we need some backwards-compatibility behavior + // in the future. + version: element-version, + + // If the style state should be read by set rules as the user has + // enabled stateful mode with `#show: e.stateful.enable()`. + stateful: false, + + // First known bib title. + // This is used by leaky mode to attempt to preserve the correct bibliography.title + // property. Evidently, it's not perfect, and leaky mode should be avoided. + first-bib-title: (), + + // Identical to 'global.select-count', this is only here for compatibility + // with older elements. + where-rule-count: 0, + + // Some global settings changeable through set rules. + settings: ( + // Whether non-stateful rules should default to leaky mode. + prefer-leaky: false, + + // Additional elements for which ancestry should be tracked. + // Setting this to 'any' will enable ancestry tracking for all elements + // (POTENTIALLY SLOW!). + track-ancestry: (:), + + // Additional elements which should support ancestry-related filters when + // queried. + store-ancestry: (:), + ), + + // Shared state between elements. + // Differently from settings, this is not meant to be configurable by users. + global: ( + // Version that created the default global data. + version: element-version, + + // Amount of select rules in the style chain so far. + // Used to apply a unique label. + select-count: 0, + + // Current element ancestors, from outermost to innermost. + ancestry-chain: (), + ), + + // Per-element data (set rules and other style chain info). + elements: (:), +) + +// Default per-element data. +#let default-data = ( + (element-data-key): true, + + version: element-version, + + // Chain for foldable fields, that is, fields which have special behavior + // when changed through more than one set rule. By default, specifying the + // same field in two subsequent set rules will have the innermost set rule + // override the value from the previous one, but this can be overridden + // for certain types where it makes sense to combine the two values in + // some way instead. For example, stroke fields have custom folding: if + // you specify 4pt for a stroke field in one set rule and orange in another, + // the final stroke will be 4pt + orange, not orange. + // + // This data structure has an entry for each changed foldable field, laid out as follows: + // ( + // foldable-field-name: ( + // folder: auto or (outer, inner) => combined value // how to combine two values, auto = simple sum, equivalent to (a, b) => a + b + // default: stroke(), // default value for this field to begin folding. This is 'field.default' unless 'required = true'. + // // Then, it is the type's default. + // values: (4pt, orange, ...) // list of all set values for this field (length = amount of times this field was changed) + // // only 'values' is used if possible, for efficiency. E.g.: values.sum(default: stroke()) + // data: ( // list to associate each value with the real style chain index and name. + // (index: 3, name: none, names: (), value: 4pt), // If 'revoke' or 'reset' are used, this list is used instead + // (index: 5, name: none, names: (), value: orange), // so we can know which values were revoked. + // ... + // ) + // ), + // ... + // ) + // + // The final argument passed to the constructor, if any, also has to be folded with the latest folded value, + // or with the field's default value if nothing was changed. However, that step is done separately. So, if + // no set rules change a particular foldable field, it is not present in this dictionary at all. + fold-chain: (:), + + // The current accumulated styles (overridden values for arguments) for the element. + chain: (), + + // Maps each style in the chain to some data. + // This is used to assign names to styles, so they can be revoked later. + data-chain: (), + + // All known names, so we can be aware of invalid revoke rules. + names: (:), + + // List of active revokes, of the form: + // (index: last-chain-index, revoking: name-revoked, name: none / name of the revoke itself) + revoke-chain: (), + + // This is set to true when a rule with '.within(eid)' is used. + track-ancestry: false, + + // Data for filtering. + filters: ( + // Filters applying to this element. Each filter is associated with a rule below. + // While some filters might trivially apply to all instances of an element, + // others might require specific field values to match, for example. + all: (), + + // This is an array of rules to apply for each filter in the same index. + // These rules are applied whenever an element matching the given filter + // is found. + rules: (), + + // Data associated with each filter, such as its name(s). + // Format: + // ((names: ("name1", "name2", ...)), ...) + data: (), + ), + + // Conditional set rules, which only apply to matching instances of an + // element. + cond-sets: ( + // Filters to apply to each set rule. + filters: (), + // For each filter above, the associated set rule args with the changed + // fields. + args: (), + // Data associated with each conditional set rule. + // Format: + // ((names: ("name1", "name2", ...), index: 0), ...) + data: (), + ), + + // Show rules that might apply to this element. + show-rules: ( + // Filters for each show rule. + filters: (), + // For each filter above, the associated show rule function. + callbacks: (), + // Data associated with each show rule. + // Format: + // ((names: ("name1", "name2", ...), index: 0), ...) + data: (), + ), + + // Applied selectors (filters for labels). + selects: ( + filters: (), + labels: (), + data: (), + ) +) + +/// This is meant to be used in a show rule of the form `#show ref: e.ref` to ensure +/// references to custom elements work properly. +/// +/// Please use [`e.prepare`](#eprepare) as it does that automatically, and more if +/// necessary. +/// +/// - args (arguments): ref and extra arguments +/// -> content +#let ref_(..args) = { + assert(args.pos().len() > 0, message: "elembic: element.ref: expected at least one positional argument (reference or label)") + let first-arg = args.pos().first() + + set ref(..args.named()) + + show ref: it => { + let element = if it.has("element") { + it.element + } else { + query(it.target).at(0, default: none) + } + if ( + element == none + or element.has("label") and str(element.label).starts-with(lbl-ref-figure-label-head) + or type(it.target) != label + ) { + // This is known to be a reference to a custom element + // (or the target is not something we can deal with, i.e. not a label) + return it + } + + let info = data(element) + if type(info) == dictionary and "data-kind" in info and info.data-kind == "element-instance" { + if "__future-ref" in info and element-version <= info.__future-ref.max-version { + return (info.__future-ref.call)(target: it.target, supplement: it.at("supplement", default: none), ref-instance: it, __future-version: element-version) + } + + let supplement = if it.has("supplement") and it.supplement != none { + (supplement: it.supplement) + } else { + (:) + } + + // Convert into a reference towards the reference figure + let converted-label = label(lbl-ref-figure-label-head + str(it.target)) + let reference = ref(converted-label, ..supplement) + + if "custom-ref" in info and info.custom-ref != none { + show ref.where(target: converted-label): [#info.custom-ref] + + reference + } else { + reference + } + } else { + it + } + } + + if type(first-arg) == content and first-arg.func() == ref { + first-arg + } else { + ref(..args) + } +} + +// Changes stateful mode settings within a certain scope. +// This function will sync all data between all modes (data from normal mode +// goes to state and data from stateful mode goes to normal mode). +// +// Setting it to 'true' tells all set rules to update the state, and also ensures +// getters retrieve the value from the state, even if not explicitly aware of +// stateful mode. +// +// By default, this function will not trigger any changes if one attempts to +// change the stateful mode to its current value. This behavior can be disabled +// with 'force: true', though that is not expected to make a difference in any way. +#let toggle-stateful-mode(enable, force: false) = doc => { + context { + let previous-bib-title = bibliography.title + [#context { + let (global-data, was-first-bib-title) = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + (bibliography.title.value, false) + } else { + ((..default-global-data, first-bib-title: previous-bib-title), true) + } + + set bibliography(title: previous-bib-title) + + if global-data.stateful != enable or force { + if not enable { + // Enabling stateful mode => use data from the style chain + // + // Disabling stateful mode => need to sync stateful with non-stateful, + // so we use data from the state + let chain = style-state.get() + global-data = if chain == () { + default-global-data + } else { + chain.last() + } + + // Store the first known bib title in the state as well + if global-data.first-bib-title == () and was-first-bib-title { + global-data.first-bib-title = previous-bib-title + } + } + + // Notify both modes about it (non-stateful and stateful) + global-data.stateful = enable + + let (show-normal, show-stateful) = if enable { + // TODO: Have a way to keep track of previous toggles and undo them + (none, it => it.value.body) + } else { + (it => it.value.body, none) + } + + show lbl-auto-mode: none + show lbl-normal-mode: show-normal + show lbl-stateful-mode: show-stateful + + // Sync data with style chain for non-stateful modes + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + + // Sync data with state for stateful mode + // Push at the start of the scope, pop at the end + [#style-state.update(chain => { + chain.push(global-data) + chain + })#doc#style-state.update(chain => { + _ = chain.pop() + chain + })] + } else { + // Nothing to do: it is already toggled to this value + doc + } + }#lbl-get] + } + + [#metadata(((special-rule-key): "toggle-stateful-mode", data-kind: "special-rule", kind: "toggle-stateful-mode", version: element-version, enable: enable, force: force))#lbl-special-rule-tag] +} + +#let request-ancestry-tracking(elements, requests) = { + for (eid, elem-data) in requests { + if eid not in elements { + elements.insert(eid, elem-data.default-data) + } + elements.at(eid).track-ancestry = true + } + + elements +} + +// Apply set and revoke rules to the current per-element data. +#let apply-rules(rules, elements: none, settings: (:), global: (:), extra-output: (:)) = { + for rule in rules { + if "__future" in rule and element-version <= rule.__future.max-version { + let output = (rule.__future.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + let kind = rule.kind + if kind == "settings" { + let (write, transform) = rule + if write != none { + settings += write + } + if transform != none { + settings = transform(settings) + } + } else if kind == "set" { + let (element, args) = rule + let (eid, default-data, fields) = element + + // Forward-compatibility with newer elements + if ( + "__future-rules" in default-data + and "set" in default-data.__future-rules + and element-version <= default-data.__future-rules.set.max-version + ) { + let output = (default-data.__future-rules.set.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid in elements { + elements.at(eid).chain.push(args) + } else { + elements.insert(eid, (..default-data, chain: (args,))) + } + + let names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } + let compat-name = none + if names != () { + let element-data = elements.at(eid) + let index = element-data.chain.len() - 1 + compat-name = names.last() + + // Lazily fill the data chain with 'none' + // Add 'name' for compatibility with older elembic versions + elements.at(eid).data-chain += (none,) * (index - element-data.data-chain.len()) + elements.at(eid).data-chain.push((kind: "set", name: compat-name, names: names)) + + for rule-name in names { + elements.at(eid).names.insert(rule-name, true) + } + } + + if fields.foldable-fields != (:) and args.keys().any(n => n in fields.foldable-fields) { + // A foldable field was specified in this set rule, so we need to record the fold + // data in the corresponding data structures separately for later. + let element-data = elements.at(eid) + let index = element-data.chain.len() - 1 + for (field-name, fold-data) in fields.foldable-fields { + if field-name in args { + let value = args.at(field-name) + let value-data = (index: index, name: compat-name, names: names, value: value) + if field-name in element-data.fold-chain { + elements.at(eid).fold-chain.at(field-name).values.push(value) + elements.at(eid).fold-chain.at(field-name).data.push(value-data) + } else { + elements.at(eid).fold-chain.insert( + field-name, + ( + folder: fold-data.folder, + default: fold-data.default, + values: (value,), + data: (value-data,) + ) + ) + } + } + } + } + } else if kind == "revoke" { + let rule-names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } + let compat-name = if rule-names == () { + none + } else { + rule-names.last() + } + + for (name, element-data) in elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in element-data + and "revoke" in element-data.__future-rules + and element-version <= element-data.__future-rules.revoke.max-version + ) { + let output = (element-data.__future-rules.revoke.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + // Can only revoke what's before us. + // If this element has no rules with this name, there is nothing to revoke; + // we shouldn't revoke names that come after us (inner rules). + // Note that this potentially includes named revokes as well. + if rule.revoking in element-data.names { + elements.at(name).revoke-chain.push((kind: "revoke", name: compat-name, names: rule-names, index: element-data.chain.len(), revoking: rule.revoking)) + + if rule-names != () { + for rule-name in rule-names { + elements.at(name).names.insert(rule-name, true) + } + } + } + } + } else if kind == "reset" { + // Whether the list of elements that this reset applies to is restricted. + let filtering = rule.eids != () + let rule-names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } + let compat-name = if rule-names == () { + none + } else { + rule-names.last() + } + + for (name, element-data) in elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in element-data + and "reset" in element-data.__future-rules + and element-version <= element-data.__future-rules.reset.max-version + ) { + let output = (element-data.__future-rules.reset.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + // Can only revoke what's before us. + // If this element has no rules, no need to add a reset. + if (not filtering or name in rule.eids) and element-data.chain != () { + elements.at(name).revoke-chain.push((kind: "reset", name: compat-name, names: rule-names, index: element-data.chain.len())) + + if rule-names != () { + for rule-name in rule-names { + elements.at(name).names.insert(rule-name, true) + } + } + } + } + } else if kind == "filtered" { + let (filter, rule: inner-rule, names) = rule + if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { + assert(false, message: "elembic: element.filtered: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + let target-elements = filter.elements + if target-elements == none { + assert(false, message: "elembic: element.filtered: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + } + let base-data = (names: names) + + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + + for (eid, all-elem-data) in target-elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in all-elem-data.default-data + and "filtered" in all-elem-data.default-data.__future-rules + and element-version <= all-elem-data.default-data.__future-rules.filtered.max-version + ) { + let output = (all-elem-data.default-data.__future-rules.filtered.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid not in elements { + elements.insert(eid, all-elem-data.default-data) + } + if "filters" not in elements.at(eid) { + // Old version + elements.at(eid).filters = default-data.filters + } + + let index = elements.at(eid).chain.len() + let data = (..base-data, index: index) + + elements.at(eid).filters.all.push(filter) + elements.at(eid).filters.rules.push(inner-rule) + elements.at(eid).filters.data.push(data) + + // Push an entry to the data chain so we have an index to assign to + // this filter rule. This allows us to reset() it later. + elements.at(eid).chain.push((:)) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "filtered", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } + } else if kind == "show" { + let (filter, callback, names) = rule + if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { + assert(false, message: "elembic: element.show_: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + let target-elements = filter.elements + if target-elements == none { + assert(false, message: "elembic: element.show_: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + } + let base-data = (names: names) + + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + + for (eid, all-elem-data) in target-elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in all-elem-data.default-data + and "show" in all-elem-data.default-data.__future-rules + and element-version <= all-elem-data.default-data.__future-rules.show.max-version + ) { + let output = (all-elem-data.default-data.__future-rules.show.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid not in elements { + elements.insert(eid, all-elem-data.default-data) + } + if "show-rules" not in elements.at(eid) { + // Old version + elements.at(eid).show-rules = default-data.show-rules + } + + let index = elements.at(eid).chain.len() + let data = (..base-data, index: index) + + elements.at(eid).show-rules.filters.push(filter) + elements.at(eid).show-rules.callbacks.push(callback) + elements.at(eid).show-rules.data.push(data) + + // Push an entry to the data chain so we have an index to assign to + // this show rule. This allows us to reset() it later. + elements.at(eid).chain.push((:)) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "show", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } + } else if kind == "cond-set" { + let (filter, args, names, element) = rule + if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { + assert(false, message: "elembic: element.cond-set: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + + let (eid,) = element + + // Forward-compatibility with newer elements + if ( + "__future-rules" in element.default-data + and "cond-set" in element.default-data.__future-rules + and element-version <= element.default-data.__future-rules.cond-set.max-version + ) { + let output = (element.default-data.__future-rules.cond-set.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid not in elements { + elements.insert(eid, element.default-data) + } + if "cond-sets" not in elements.at(eid) { + // Old version + elements.at(eid).cond-sets = default-data.cond-sets + } + + let index = elements.at(eid).chain.len() + let data = (names: names, index: index) + elements.at(eid).cond-sets.filters.push(filter) + elements.at(eid).cond-sets.args.push(args) + elements.at(eid).cond-sets.data.push(data) + + // Push an entry to the data chain so we have an index to assign to + // this filter rule. This allows us to reset() it later. + elements.at(eid).chain.push((:)) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "cond-set", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } else if kind == "select" { + let (element-data: target-elements, names) = rule + let base-data = (names: names) + + for (eid, elem-data) in target-elements { + if "filters" not in elem-data or "labels" not in elem-data { + assert(false, message: "elembic: element.select: missing filters or labels for element " + repr(eid)) + } + let (filters, labels) = elem-data + assert(filters.len() == labels.len(), message: "elembic: element.select: differing lengths for filters and labels found (this is an internal error)") + if filters == () { + continue + } + + let sample-filter = filters.first() + assert( + type(sample-filter) == dictionary + and "kind" in sample-filter + and "elements" in sample-filter + and type(sample-filter.elements) == dictionary + and eid in sample-filter.elements, + message: "elembic: element.select: invalid filter found for element " + repr(eid) + ", it must contain the element's data.\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages." + ) + + let all-elem-data = sample-filter.elements.at(eid) + + // Forward-compatibility with newer elements + if ( + "__future-rules" in all-elem-data.default-data + and "select" in all-elem-data.default-data.__future-rules + and element-version <= all-elem-data.default-data.__future-rules.select.max-version + ) { + let output = (all-elem-data.default-data.__future-rules.select.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + for filter in filters { + assert( + type(filter) == dictionary + and "kind" in filter + and "elements" in filter, + message: "elembic: element.select: invalid filter found for element " + repr(eid) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages." + ) + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + } + + if eid not in elements { + elements.insert(eid, all-elem-data.default-data) + } + if "selects" not in elements.at(eid) { + // Old version + elements.at(eid).selects = default-data.selects + } + + let index = elements.at(eid).chain.len() + let data = (..base-data, index: index) + + elements.at(eid).selects.filters += filters + elements.at(eid).selects.labels += labels + elements.at(eid).selects.data += (data,) * filters.len() + + // Push an entry to the data chain so we have an index to assign to + // this filter rule. This allows us to reset() it later. + elements.at(eid).chain += (((:),) * filters.len()) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "select", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } + } else if kind == "apply" { + // Mostly a fallback in case the rule is accidentally passed here... + let output = apply-rules(rule.rules, elements: elements, settings: settings, global: global, extra-output: extra-output) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + } else { + assert(false, message: "elembic: element: invalid rule kind '" + rule.kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.") + } + } + + (..extra-output, elements: elements, settings: settings) +} + +// Prepare rule(s), returning a function `doc => ...` to be used in +// `#show: rule`. The rule is attached as metadata to the returned +// content so it can still be accessed outside of a show rule. +// +// This is where we execute our main machinery to apply rules to the +// document, that is, modifications to the global data of custom +// elements. This is done in different ways depending on the mode: +// +// - In normal mode, we create 'get rule' points by annotating +// context blocks with `#lbl-get`. Any modifications to the global +// data are stored as 'set bibliography(title: metadata with data)' +// scoped to context blocks with that label. Therefore, we can access +// the data by retrieving bibliography.title inside those blocks. +// +// The downside is that the entire document is wrapped in context, +// so 'max show rule depth exceeded' errors can occur. +// +// - In leaky mode, it is similar, but we reset bibliography.title +// to an arbitrary value instead of having two context blocks to +// ensure it remains unchanged. +// +// - In stateful mode, we don't wrap anything around the document, +// removing the 'max show rule depth exceeded' problem. Rather, we +// place a state update at the start and another at the end of the +// scope, respectively updating the global data and then undoing +// the update, ensuring it only applies to that scope. +// +// The downside is that this uses 'state()', which can lead to +// relayouts (slower) and even diverging layout. +#let prepare-rule(rule) = { + let rules = if rule.kind == "apply" { rule.rules } else { (rule,) } + + doc => { + let rule = rule + let rules = rules + let mode = rule.mode + + // If there are two 'show:' in a row, flatten into a single set of rules + // instead of running this function multiple times, reducing the + // probability of accidental nested function limit errors. + // + // Note that all rules replace the document with + // [#context { ... doc .. }[#metadata(doc: doc, rule: rule)#lbl-rule-tag]] + // We get the second child to extract the original rule information. + // If 'doc' has the form above, this means the user wrote + // #show: rule1 + // #show: rule2 + // which we want to unify. So we check children len == 2 and unify if the tag is there. + // + // But we also want to accept some parbreaks before, i.e. + // + // #show: rule1 + // + // #show: rule2 + // + // This generates a doc of the form + // [#parbreak()[#context { ... doc .. }[#metadata(doc: doc, rule: rule)#lbl-rule-tag]]] + // So we also check for children len >= 2 (although == 2 is enough in that case) and + // strip any leading parbreaks / spaces / linebreaks, moving them to the new 'doc' (they + // now receive the rules, which is technically incorrect, but in practice is only a problem + // if you have a show rule on parbreak or space or something, which is odd). + // + // Note also that + // #show: rule1 + // + // // hello world! + // // hello world! + // // hello world! + // + // #show: rule2 + // + // produces + // [#parbreak()#space()#space()#parbreak()[... rule substructure with metadata... ]] + // which makes the need for stripping multiple kinds of whitespace explicit. + // We limit at 100 to prevent unbounded search. + // + // We also need to consider the case with + // #show: rule1 + // #set native(field: value) + // #show: rule2 + // + // in which case the document structure (from rule1's view) is + // + // styled(child: [... rule2 ...], styles: ..) + // + // Worse, there could be parbreaks around the set rule: + // + // #show: rule1 + // + // #set native(field: value) + // + // #show: rule2 + // + // leading to + // + // sequence(parbreak(), styled(child: sequence(parbreak(), [ ... rule2 ... ]), styles: ..)) + // + // so we need to perform a document tree walk to lift rule2 and transform this into + // + // #show: apply( + // rule1 + // rule2 + // ) + // + // #set native(field: value) + // ... + // + // Tree walk is performed as follows: + // + // this rule + // \ + // sequence + // \ space parbreak ... sequence + // \ space parbreak ... styled (styles = S) + // \ sequence + // \ space parbreak ... inner rule! + // \ (rule.doc, rule.rule) + // We store each tree level in 'wrappers' so we can reconstruct this document structure without 'rule!'. + // In the case above, that would correspond to + // wrappers = ((sequence, (space, parbreak, ...)), (sequence, space, parbreak, ...), (styled, S), (sequence, space, parbreak, ...)) + // and 'rule' would become 'potential-doc'. + // + // We would then wrap 'rule.doc' in reverse order, adding after the sequence prefix or + // making it the styled child, producing + // + // this rule + inner rule + // \ + // (sequence, apply(this rule, inner rule)) + // \ space parbreak ... sequence + // \ space parbreak ... styled (styles = S) + // \ sequence + // \ space parbreak ... rule.doc + // + // as desired. That is, we move the inner rule up into this rule in order to only consume 1 from + // the rule limit, which is valid since the rule won't apply to spaces, parbreaks, and styled. + // Of course, there could be show rules towards a different structure, but we assume that the user + // understands that show rules on spacing may cause unexpected behavior. + let potential-doc = [#doc] + let wrappers = () + let max-depth = 150 + // Acceptable content types for set rule lifting. + // These are content types that are leaves and we usually don't expect them to + // be replaced in a show rule by an actual custom element. + // If we find something that isn't here, e.g. a block, we stop searching as we can't lift any further rules. + // We also exclude anything with a label since that indicates there might be a show rule application incoming. + let whitespace-funcs = (parbreak, space, linebreak, h, v, state-update-func, counter-update-func) + // Content types we can peek at. + let recursing-funcs = (styled, sequence) + let loop-prefix = none + let loop-children = () + let loop-last = none + + while max-depth > 0 { + // Child is #{ + // set something(abc: def) + // show something: else + // [some stuff] + // } + if potential-doc.func() == styled { + max-depth -= 1 + wrappers.push((styled, potential-doc.styles)) + + // 'Recursively' check the child + potential-doc = [#potential-doc.child] + } else if ( + // Child is #[ + // (parbreak) + // (space) + // #[ sequence, rule or more styles ] + // ] + potential-doc.func() == sequence + and { loop-children = potential-doc.children; loop-children.len() >= 2 } // something like 'if let Sequence(children) = potential-doc { ... }' + and { loop-last = loop-children.last(); loop-last.func() in recursing-funcs } + and max-depth - loop-children.len() > 0 + and { + loop-prefix = loop-children.slice(0, -1); + loop-prefix.all(t => (t.func() in whitespace-funcs or t == []) and t.at("label", default: none) == none) + } + ) { + max-depth -= loop-children.len() + wrappers.push((sequence, loop-prefix)) + + // 'Recursively' check the last child + potential-doc = loop-last + } else { + break + } + } + + // Merge with the closest rule application below us, "moving" it upwards + // and reducing the rule count by 1 + let last-label = none + while ( + potential-doc.func() == sequence + and potential-doc.children.len() == 2 + and { + last-label = potential-doc.children.last().at("label", default: none) + last-label == lbl-rule-tag or last-label == lbl-old-rule-tag + } + ) { + let last = potential-doc.children.last() + let inner-rule = last.value.rule + + // Process all rules below us together with this one + if inner-rule.kind == "apply" { + // Note: apply should automatically distribute modes across its children, + // so it's okay if we don't inherit its own mode here. + rules += inner-rule.rules + } else { + rules.push(inner-rule) + } + + // We assume 'apply' already checked its own rules. + // Therefore, we only need to fold a single time. + // Don't check all rules every time again. + if ( + inner-rule.mode == style-modes.stateful + or mode != style-modes.stateful and inner-rule.mode == style-modes.leaky + or mode == auto + ) { + // Prioritize more explicit modes: + // stateful > leaky > normal + mode = inner-rule.mode + } + + // Convert this into an 'apply' rule + rule = ((prepared-rule-key): true, version: element-version, kind: "apply", rules: rules, mode: mode, name: none, names: ()) + + // Place what's inside, don't place the context block that would run our code again + doc = last.value.doc + + // Reconstruct the document structure. + // Must be in reverse (innermost wrapper to outermost). + for (func, data) in wrappers.rev() { + if func == styled { + doc = styled(doc, data) + } else { + // (sequence, prefix) + // Re-add stripped whitespace and stuff + doc = data.join() + doc + } + } + + if "__future" in last.value and element-version <= last.value.__future.max-version { + let res = (last.value.__future.call)(rule, doc, __future-version: element-version) + + if "doc" in res { + return res.doc + } + } + + if last-label == lbl-old-rule-tag { + // If we're merging with an older rule version, we may have to merge a + // newer version again + potential-doc = last.value.doc + } else { + break + } + } + + // Stateful mode: no context, just push in a state at the start of the scope + // and pop to previous data at the end. + let stateful = { + style-state.update(chain => { + let global-data = if chain == () { + default-global-data + } else { + chain.last() + } + + assert( + global-data.stateful, + message: "elembic: element rule: cannot use a stateful rule without enabling the global stateful toggle\n hint: if you don't mind the performance hit, write '#show: e.stateful.enable()' somewhere above this rule, or at the top of the document to apply to all" + ) + + if "settings" not in global-data { + global-data.settings = default-global-data.settings + } + + if "global" not in global-data { + global-data.global = default-global-data.global + } + + global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) + + chain.push(global-data) + chain + }) + doc + style-state.update(chain => { + _ = chain.pop() + chain + }) + } + + // Leaky mode: one context resetting bibliography.title. + let leaky = [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + // Bibliography title wasn't overridden, so we can use it + (..default-global-data, first-bib-title: bibliography.title) + } + + if mode == auto and ("settings" not in global-data or "prefer-leaky" not in global-data.settings or not global-data.settings.prefer-leaky) { + // User didn't want leaky. + return none + } + + let first-bib-title = global-data.first-bib-title + if first-bib-title == () { + // Nobody has seen the bibliography title (bug?) + first-bib-title = auto + } + + if global-data.stateful { + if mode == auto { + // User chose something else. + // Don't even place anything. + return none + } else { + // Use state instead! + return { + set bibliography(title: first-bib-title) + stateful + } + } + } + + if "settings" not in global-data { + global-data.settings = default-global-data.settings + } + + if "global" not in global-data { + global-data.global = default-global-data.global + } + + global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) + + set bibliography(title: first-bib-title) + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + doc + }#lbl-get] + + // Normal mode: two nested contexts: one retrieves the current bibliography title, + // and the other retrieves the title with metadata and restores the current title. + let normal = context { + let previous-bib-title = bibliography.title + [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + (..default-global-data, first-bib-title: previous-bib-title) + } + + if mode == auto and "settings" in global-data and "prefer-leaky" in global-data.settings and global-data.settings.prefer-leaky { + // User wants leaky. + return none + } + + if global-data.stateful { + if mode == auto { + // User chose something else. + // Don't even place anything. + return none + } else { + // Use state instead! + return { + set bibliography(title: previous-bib-title) + stateful + } + } + } + + if "settings" not in global-data { + global-data.settings = default-global-data.settings + } + + if "global" not in global-data { + global-data.global = default-global-data.global + } + + global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) + + set bibliography(title: previous-bib-title) + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + doc + }#lbl-get] + } + + let body = if mode == auto { + // Allow user to pick the mode through show rules. + [#metadata((body: stateful))#lbl-stateful-mode] + [#metadata((body: normal))#lbl-normal-mode] + [#leaky] + [#normal#lbl-auto-mode] + } else if mode == style-modes.normal { + normal + } else if mode == style-modes.leaky { + leaky + } else if mode == style-modes.stateful { + [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + default-global-data + } + + if not global-data.stateful { + let only-rule = if rules.len() == 1 { rules.first() } else { (kind: "apply", rules: rules, name: none, names: (), mode: auto) } + let named = if "names" in only-rule and only-rule.names != () { + " named " + only-rule.names.map(repr).join(", ") + } else if "name" in only-rule and only-rule.name != none { + " named " + repr(only-rule.name) + } else { + "" + } + + let extra = if only-rule.kind == "revoke" { + " revoking " + repr(only-rule.revoking) + } else { + "" + } + + assert( + false, + message: ( + "elembic: element rule: cannot use a stateful rule without enabling the global stateful toggle" + + "\n hint: if you don't mind the performance hit, write '#show: e.stateful.enable()' somewhere above this rule, or at the top of the document to apply to all" + + if only-rule.kind != "apply" { "\n help: this was triggered by a " + repr(only-rule.kind) + " rule" + named + extra } + ) + ) + } + }#lbl-get] + + stateful + } else { + panic("element rule: unknown mode: " + repr(mode)) + } + + // Add the rule tag after each rule application. + // This allows extracting information about the rule before it is applied. + // It also allows combining the rule with an outer rule before application, + // as we do earlier. + [#body#metadata((data-kind: "prepared-rule", version: element-version, routines: (prepare-rule: prepare-rule, apply-rules: apply-rules), doc: doc, rule: rule))#lbl-rule-tag] + } +} + +/// Apply a set rule to a custom element. Check out the Styling guide for more information. +/// +/// Note that this function only accepts non-required fields (that have a `default`). +/// Any required fields must always be specified at call site and, as such, are always +/// be prioritized, so it is pointless to have set rules for those. +/// +/// Keep in mind the limitations when using set rules, as well as revoke, reset and +/// apply rules. +/// +/// As such, when applying many set rules at once, please use `e.apply` instead +/// (or specify them consecutively so `elembic` does that automatically). +/// +/// USAGE: +/// +/// ```typ +/// #show: e.set_(superbox, fill: red) +/// #show: e.set_(superbox, optional-pos-arg1, optional-pos-arg2) +/// +/// // This call will be equivalent to: +/// // #superbox(required-arg, optional-pos-arg1, optional-pos-arg2, fill: red) +/// #superbox(required-arg) +/// ``` +/// +/// - elem (function): element to apply the set rule on +/// - fields (arguments): optional fields to set (positionally or named, depending on the field) +/// -> function +#let set_(elem, ..fields) = { + if type(elem) == function { + elem = data(elem) + } + assert(type(elem) == dictionary, message: "elembic: element.set_: please specify the element's constructor or data in the first parameter") + let (res, args) = (elem.parse-args)(fields, include-required: false) + if not res { + assert(false, message: args) + } + + prepare-rule( + ((prepared-rule-key): true, version: element-version, kind: "set", name: none, names: (), mode: auto, element: (eid: elem.eid, default-data: elem.default-data, fields: elem.fields), args: args) + ) +} + +/// Prepare a selector similar to 'element.where(..args)' +/// which can be used in "show sel: set". Receives a filter +/// generated by 'element.with(fields)' or '(element-data.where)(fields)'. +/// +/// This works by checking the filter within all element instances and, +/// if they match, they receive a unique label to be matched +/// by that selector. The label is then provided to the callback function +/// as the selector. +/// +/// Each requested selector is passed as a separate parameter to the callback. +/// You must wrap the remainder of the document that depends on those selectors +/// in this callback. +/// +/// USAGE: +/// +/// ```typ +/// #e.select(superbox.with(fill: red), prefix: "my first select", superbox.with(width: auto), (red-superbox, auto-superbox) => { +/// // Hide superboxes with red fill or auto width +/// show red-superbox: none +/// show auto-superbox: none +/// +/// // This one is hidden +/// #superbox(fill: red) +/// +/// // This one is hidden +/// #superbox(width: auto) +/// +/// // This one is kept +/// #superbox(fill: green, width: 5pt) +/// }) +/// ``` +/// +/// - args (function): filters in the format 'element.with(field-a: a, field-b: b)'. Note that you must write fields' names even if they are positional. +/// - receiver (function): receives one requested selector per filter as separate arguments, must return content. +/// - prefix (str): a unique prefix for selectors generated by this 'selector' to disambiguate from other calls to this function. +/// -> content +#let select(..args, receiver, prefix: 0) = { + assert(type(prefix) == str, message: "elembic: element.select: please pick a unique string 'prefix:' argument for the selectors generated by this call to 'select' to ensure they don't clash with other calls to 'select'.") + assert(args.named() == (:), message: "elembic: element.select: unexpected named arguments") + assert(type(receiver) == function, message: "elembic: element.select: last argument must be a function receiving each prepared selector as a separate argument") + + let filters = args.pos() + + // (eid: ((index, filter), ...)) + // The idea is to apply all filters for a given eid at once + let filters-by-eid = (:) + // (eid: sel) + let labels-by-eid = (:) + // Elements which still require explicit show rules. + let old-elements = (:) + let ordered-eids = () + + let i = 0 + for filter in filters { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + + if type(filter) != dictionary or filter-key not in filter { + if type(filter) == selector { + assert(false, message: "elembic: element.select: Typst-native selectors cannot be specified here, only those of custom elements") + } + assert(false, message: "elembic: element.select: expected a valid filter, such as 'custom-element' or 'custom-element.with(field-name: value, ...)', got " + base.typename(filter)) + } + + if "elements" not in filter { + assert(false, message: "elembic: element.select: invalid filter found while applying rule, as it did not have an 'elements' field: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + + for (eid, elem-data) in filter.elements { + if "sel" not in elem-data { + assert(false, message: "elembic: element.select: filter did not have the element's selector") + } + if elem-data.eid in labels-by-eid and labels-by-eid.at(elem-data.eid) != elem-data.sel { + assert(false, message: "elembic: element.select: filter had a different selector from the others for the same element ID, check if you're not using conflicting library versions (could also be a bug)") + } + + if elem-data.eid not in labels-by-eid { + labels-by-eid.insert(elem-data.eid, elem-data.sel) + } + + if elem-data.eid in filters-by-eid { + filters-by-eid.at(elem-data.eid).push((i, filter)) + } else { + filters-by-eid.insert(elem-data.eid, ((i, filter),)) + ordered-eids.push(elem-data.eid) + } + + if ("version" not in elem-data or elem-data.version <= 1) and ("default-data" not in elem-data or "selects" not in elem-data.default-data) { + old-elements.insert(elem-data.eid, true) + } + } + i += 1 + } + + context { + let previous-bib-title = bibliography.title + [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + (..default-global-data, first-bib-title: previous-bib-title) + } + + if global-data.stateful { + let chain = style-state.get() + global-data = if chain == () { + default-global-data + } else { + chain.last() + } + } + + if "global" not in global-data { + global-data.global = default-global-data.global + } + + // Amount of 'select rules' so far, so we can + // assign a unique number to each query + let rule-counter = global-data.global.at("select-count", default: 0) + + // Generate labels by counting up, and update counter + let matching-labels = range(0, filters.len()).map(i => label(lbl-global-select-head + prefix + str(rule-counter + i))) + rule-counter += matching-labels.len() + global-data.global.select-count = rule-counter + + // Provide labels to the body, one per filter + // These labels only match the shown bodies of + // elements with matching field values + let body = receiver(..matching-labels) + + // Apply show rules to the body to add labels to matching elements + let styled-body = ordered-eids.filter(e => e in old-elements).fold(body, (acc, eid) => { + let filters = filters-by-eid.at(eid) + show labels-by-eid.at(eid): it => { + let data = data(it) + let tag = [#metadata(data)#lbl-tag] + let fields = data.fields + + let labeled-it = it + for (i, filter) in filters { + // Check if all positional and named arguments match + // Note: no ancestry support since newer elements don't run this + // code, they use 'select' rules instead + if verify-filter(fields, eid: eid, filter: filter, ancestry: ()) { + // Add corresponding label and preserve tag so 'data(it)' still works + labeled-it = [#[#labeled-it#tag]#matching-labels.at(i)] + } + } + + labeled-it + } + + acc + }) + + set bibliography(title: previous-bib-title) + + let pairs-by-eid = (:) + for eid in ordered-eids { + if eid in old-elements or filters-by-eid.at(eid, default: ()) == () { + continue + } + let pairs = filters-by-eid.at(eid).map(((i, f)) => (f, matching-labels.at(i))) + let (filters, labels) = array.zip(..pairs) + pairs-by-eid.insert(eid, (filters: filters, labels: labels)) + } + + if pairs-by-eid != (:) { + let select-rule = ( + ((prepared-rule-key): true, + version: element-version, + kind: "select", + name: none, + names: (), + mode: auto, + element-data: pairs-by-eid, + ) + ) + global-data += apply-rules( + (select-rule,), + elements: global-data.elements, + settings: global-data.at("settings", default: default-global-data.settings), + global: global-data.at("global", default: default-global-data.global) + ) + } + + // Increase select rule counter for further select rules + if global-data.stateful { + style-state.update(chain => { + chain.push(global-data) + chain + }) + + styled-body + + style-state.update(chain => { + _ = chain.pop() + chain + }) + } else { + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + styled-body + } + }#lbl-get] + } + + [#metadata( + ( + (special-rule-key): "select", + data-kind: "special-rule", + kind: "select", + version: element-version, + filters: filters, + computed: (filters-by-eid: filters-by-eid, labels-by-eid: labels-by-eid, ordered-eids: ordered-eids), + receiver: receiver, + prefix: prefix + ) + )#lbl-special-rule-tag] +} + +/// Apply filtered rules to a custom element's descendants +/// (but not to itself; for that use `cond-set`). +/// +/// USAGE: +/// +/// ```typ +/// #show: e.filtered( +/// elem, +/// e.set_(elem3, fields: ...) +/// ) +/// ``` +/// +/// When applying many set rules at once, use 'apply' instead of 'set' on the last parameter. +/// +/// - filter (filter): filter specifying which element instances should create this set rule +/// for their children. +/// - rule (rule): which rule to create under matched elements. +/// -> function +#let filtered(filter, rule) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.filtered: invalid filter, please use 'custom-element.with(...)' to generate a filter.") + assert(type(rule) == function, message: "elembic: element.filtered: this is not a valid rule (not a function), please use functions such as 'set_' to create one.") + assert("elements" in filter, message: "elembic: element.filtered: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + assert(filter.elements != (:), message: "elembic: element.filtered: this filter appears to not be restricted to any elements and is thus impossible to match. It must apply to exactly one element (the one receiving the set rule). Consider using a different filter.") + assert(filter.elements != none, message: "elembic: element.filtered: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + + let rule = rule([]).children.last().value.rule + let filtered-rule = ((prepared-rule-key): true, version: element-version, kind: "filtered", filter: filter, rule: rule, name: none, names: (), mode: rule.at("mode", default: auto)) + if rule.kind == "apply" { + // Transpose filtered(filter, apply(a, b, c)) into apply(filtered(filter, a), filtered(filter, b), filtered(filter, c)) + let i = 0 + for inner-rule in rule.rules { + assert(inner-rule.kind in ("show", "set", "revoke", "reset", "cond-set", "filtered"), message: "elembic: element.filtered: can only filter apply, show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + inner-rule.kind + "'") + + rule.rules.at(i) = (..filtered-rule, rule: inner-rule, mode: inner-rule.at("mode", default: auto)) + + i += 1 + } + + // Keep the apply but with everything filtered. + prepare-rule(rule) + } else { + assert(rule.kind in ("show", "set", "revoke", "reset", "cond-set", "filtered"), message: "elembic: element.filtered: can only filter apply, show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + rule.kind + "'") + + prepare-rule(filtered-rule) + } +} + +/// Apply a conditional set rule to a custom element. The set rule is only applied if +/// the given filter matches for that element. +/// +/// Check out the Styling guide for more information. +/// +/// Note that this function only accepts non-required fields (that have a `default`). +/// Any required fields must always be specified at call site and, as such, are always +/// going to be prioritized, so it is pointless to have set rules for those. +/// +/// Keep in mind the limitations when using set rules, as well as revoke, reset and +/// apply rules. +/// +/// As such, when applying many set rules at once, please use `e.apply` instead +/// (or specify them consecutively so `elembic` does that automatically). +/// +/// USAGE: +/// +/// ```typ +/// #show: e.set_(superbox, fill: red) +/// #show: e.cond-set(superbox.with(data: 10), fill: blue) +/// +/// #superbox(data: 5) // this will have red fill +/// #superbox(data: 10) // this will have blue fill +/// ``` +/// +/// - filter (filter): filter specifying which element instances should receive this set rule. +/// - fields (arguments): optional fields to set (positionally or named, depending on the field) +/// -> function +#let cond-set(filter, ..fields) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.cond-set: invalid filter, please pass just 'custom-element' or use 'custom-element.with(...)' to generate a filter.") + assert("elements" in filter, message: "elembic: element.cond-set: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + assert(filter.elements != (:), message: "elembic: element.cond-set: this filter appears to not be restricted to any elements and is thus impossible to match. It must apply to exactly one element (the one receiving the set rule). Consider using a different filter.") + assert(filter.elements != none, message: "elembic: element.cond-set: this filter appears to apply to any element. It must apply to exactly one element (the one receiving the set rule). Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + assert(filter.elements.len() == 1, message: "elembic: element.cond-set: this filter appears to apply to more than one element. It must apply to exactly one element (the one receiving the set rule).") + let (eid, elem) = filter.elements.pairs().first() + + let (res, args) = (elem.parse-args)(fields, include-required: false) + if not res { + assert(false, message: args) + } + + prepare-rule( + ((prepared-rule-key): true, version: element-version, kind: "cond-set", name: none, names: (), mode: auto, filter: filter, element: (eid: elem.eid, default-data: elem.default-data, fields: elem.fields), args: args) + ) +} + +/// Applies a show rule through the elembic stylechain, thus making it +/// revokable and also allowing easy usage of filters. +/// +/// Show rules allow you to transform all occurrences of one or more elements, +/// replacing them with arbitrary document content. +/// +/// For example: +/// +/// ```typ +/// #show: e.show_(elem.with(fill: blue), it => [Hello *#it*!]) +/// +/// #elem(fill: red)[First] +/// #elem(fill: blue)[Second] // displays as "Hello *Second*!" +/// ``` +/// +/// - filter (filter): which element(s) to apply the rule to, with which fields etc. +/// - callback (function | content | str | none): replacement content or transformation function (content -> content) +/// receiving any matched elements and returning what to replace it with. +/// -> function +#let show_(filter, replacement, mode: auto) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.show_: invalid filter, please use 'custom-element.with(...)' to generate a filter.") + assert(replacement == none or type(replacement) in (function, str, content), message: "elembic: element.show_: second parameter is not a valid show rule replacement or callback. Must be either a function 'it => content', or the content to unconditionally replace by (if it does not depend on the matched element). For example, you can write 'show: e.show_(elem, it => [*#it*])' to make an element bold, or 'show: e.show_(elem, [Hi])' to always replace it with the word 'Hi'.") + + let callback = replacement + if type(replacement) != function { + replacement = [#replacement] + callback = _ => replacement + } + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "show", filter: filter, callback: callback, name: none, names: (), mode: mode)) +} + +/// Apply multiple rules (set rules, etc.) at once. +/// +/// These rules do not count towards the "set rule limit" observed in 'Limitations'; +/// `apply` itself will always count as a single rule regardless of the amount of rules +/// inside it (be it 5, 50, or 500). Therefore, +/// **it is recommended to group rules together under `apply` whenever possible.** +/// +/// Note that Elembic will automatically wrap consecutive rules (only whitespace +/// or native set/show rules inbetween) into a single `apply`, bringing the same benefit. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.apply( +/// set_(elem, fields), +/// set_(elem, fields) +/// ) +/// ``` +/// +/// - mode (int): style mode given by the `style-modes` dictionary +/// - args (arguments): rules to apply +/// -> function +#let apply(mode: auto, ..args) = { + assert(args.named() == (:), message: "elembic: element.apply: unexpected named arguments") + assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.apply: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") + + let rules = args.pos().map( + rule => { + assert(type(rule) == function, message: "elembic: element.apply: invalid rule of type " + str(type(rule)) + ", please use 'set_' or some other function from this library to generate it") + + // Call it as if it we were in a show rule. + // It will have some trailing metadata indicating its arguments. + let inner = rule([]) + let rule-data = inner.children.last().value.rule + + if rule-data.kind == "apply" { + // Flatten 'apply' + rule-data.rules + } else { + (rule-data,) + } + } + ).sum(default: ()) + + if mode == auto { + mode = rules.fold(auto, (mode, rule) => { + if ( + rule.mode == style-modes.stateful + or mode != style-modes.stateful and rule.mode == style-modes.leaky + or mode == auto + ) { + // Prioritize more explicit modes: + // stateful > leaky > normal + rule.mode + } else { + mode + } + }) + } + + if mode != auto { + rules = rules.map(r => r + (mode: mode)) + } + + // Set this apply rule's mode as an optimization, but note that we have forcefully altered + // its children's modes above. + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "apply", rules: rules, mode: mode)) +} + +#let settings(..args, mode: auto) = { + assert(args.pos() == (), message: "elembic: element.settings: unexpected positional args") + let args = args.named() + assert(args != (:), message: "elembic: element.settings: please specify some setting, e.g. e.settings(prefer-leaky: true)") + + let write = (:) + let transform = () + for (key, val) in args { + if key not in default-global-data.settings { + assert(false, message: "elembic: element.settings: invalid setting '" + key + "', valid keys are " + default-global-data.settings.keys().map(repr).join(", ")) + } + + let default-setting = default-global-data.settings.at(key) + if key in ("track-ancestry", "store-ancestry") and val != "any" { + if type(val) == array { + let new-elements = (:) + for elem in val { + if type(elem) == function { + elem = data(elem) + } + if type(elem) != dictionary or "eid" not in elem { + assert(false, message: "elembic: element.settings: expected array of elements or literal \"any\" (apply to any element) for setting '" + key + "', got array of '" + str(type(elem)) + "'") + } + + new-elements.insert(elem.eid, elem) + } + + if new-elements != (:) { + transform.push(s => { + let existing = if s != none and key in s { s.at(key) } else { (:) } + if existing == "any" { + // Nothing to change, already applies to all elements + s + } else { + (:..s, (key): (:) + existing + new-elements) + } + }) + } + } else { + assert(false, message: "elembic: element.settings: expected array of elements or literal \"any\" (apply to any element) for setting '" + key + "', got '" + str(type(val)) + "'") + } + } else if key == "prefer-leaky" and type(val) != type(default-setting) { + assert(false, message: "elembic: element.settings: expected type of '" + str(type(default-setting)) + "' for setting '" + key + "', got '" + str(type(val)) + "'") + } else { + write.insert(key, val) + } + } + + let transform = if transform == () { + none + } else if transform.len() == 1 { + transform.first() + } else { + s => transform.fold(s, (acc, fun) => fun(acc)) + } + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "settings", write: write, transform: transform, mode: mode)) +} + +/// Name a certain rule. Use `e.apply` to name a group of rules. +/// This is used to be able to revoke the rule later with `e.revoke`. +/// +/// Please note that, at the moment, each rule can only have +/// one name. This means that applying multiple `named` on +/// the same set of rules will simply replace the previous +/// names. +/// +/// However, more than one rule can have the same name, allowing both to be +/// revoked at once if needed. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.named( +/// "cool set", +/// e.set_(elem, fields) +/// ) +/// ``` +/// +/// - name (str): The name to give to the rule. +/// - rule (function): The rule to apply this name to. +/// -> function +#let named(..names, rule) = { + assert(names.named() == (:), message: "elembic: element.named: unexpected named arguments") + let names = names.pos() + assert(names != (), message: "elembic: element.named: expected at least two arguments (one or more names and a rule)") + assert(type(rule) == function, message: "elembic: element.named: last parameter is not a valid rule (not a function), please use functions such as 'set_' to create one.") + for name in names { + assert(type(name) == str, message: "elembic: element.named: rule name must be a string, not " + str(type(name))) + assert(name != "", message: "elembic: element.named: name must not be empty") + } + + // For backwards compatibility when only one name was possible + let compat-name = names.last() + let rule = rule([]).children.last().value.rule + if rule.kind == "apply" { + let i = 0 + for inner-rule in rule.rules { + assert(inner-rule.kind in ("show", "set", "revoke", "reset", "filtered", "cond-set"), message: "elembic: element.named: can only name show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + inner-rule.kind + "'") + + rule.rules.at(i).name = compat-name + + if "names" in inner-rule { + rule.rules.at(i).names += names + } else { + rule.rules.at(i).names = names + } + + i += 1 + } + } else { + assert(rule.kind in ("show", "set", "revoke", "reset", "filtered", "cond-set"), message: "elembic: element.named: can only name show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + rule.kind + "'") + rule.name = compat-name + + if "names" in rule { + rule.names += names + } else { + rule.names = names + } + } + + // Re-prepare the rule + prepare-rule(rule) +} + +/// Revoke all rules with a certain name. +/// +/// This is intended to be used in a specific scope, +/// and temporary. This means you are supposed to only revoke the rule +/// for a short portion of the document. If you wish to do the opposite, +/// that is, only apply the rule for a short portion for the document +/// (and have it never apply again afterwards), then please just scope +/// the set rule itself instead. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.named("name", set_(element, fields)) +/// ... +/// #[ +/// #show: e.revoke("name") +/// // rule 'name' doesn't apply here +/// ... +/// ] +/// +/// // Applies here again +/// ... +/// ``` +/// +/// - name (str): name of rules to be revoked +/// - mode (int): style mode given by the `style-modes` dictionary +/// -> function +#let revoke(name, mode: auto) = { + assert(type(name) == str, message: "elembic: element.revoke: rule name must be a string, not " + str(type(name))) + assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.revoke: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "revoke", revoking: name, name: none, names: (), mode: mode)) +} + +/// Temporarily revoke all active set rules for certain elements (or even all elements if none are specified). +/// Applies only to the current scope, like other rules. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.set_(element, fill: red) +/// #[ +/// // Revoke all previous set rules on 'element' for this scope +/// #show: e.reset(element) +/// #element[This is using the default fill (not red)] +/// ] +/// +/// // Rules not revoked outside the scope +/// #element[This is using red fill] +/// ``` +/// +/// - args (arguments): elements whose rules should be reset, or none to reset all rules +/// - mode (int): style mode given by the `style-modes` dictionary +/// -> function +#let reset(..args, mode: auto) = { + assert(args.named() == (:), message: "elembic: element.reset: unexpected named arguments") + assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.reset: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") + + let filters = args.pos().map(it => if type(it) == function { data(it) } else { it }) + assert(filters.all(x => type(x) == dictionary and "eid" in x), message: "elembic: element.reset: invalid arguments, please provide a function or element data with at least an 'eid'") + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "reset", eids: filters.map(x => x.eid), name: none, names: (), mode: mode)) +} + +// Stateful variants +#let stateful-set(..args) = { + apply(set_(..args), mode: style-modes.stateful) +} +#let stateful-cond-set(..args) = { + apply(cond-set(..args), mode: style-modes.stateful) +} +#let stateful-settings = settings.with(mode: style-modes.stateful) +#let stateful-apply = apply.with(mode: style-modes.stateful) +#let stateful-show = show_.with(mode: style-modes.stateful) +#let stateful-revoke = revoke.with(mode: style-modes.stateful) +#let stateful-reset = reset.with(mode: style-modes.stateful) + +// Leaky variants +#let leaky-set(..args) = { + apply(set_(..args), mode: style-modes.leaky) +} +#let leaky-cond-set(..args) = { + apply(cond-set(..args), mode: style-modes.leaky) +} +#let leaky-settings = settings.with(mode: style-modes.leaky) +#let leaky-apply = apply.with(mode: style-modes.leaky) +#let leaky-show = show_.with(mode: style-modes.leaky) +#let leaky-revoke = revoke.with(mode: style-modes.leaky) +#let leaky-reset = reset.with(mode: style-modes.leaky) + +#let leaky-toggle(enable) = leaky-settings(prefer-leaky: enable) + +// Apply revokes and other modifications to the chain and generate a final set +// of fields. +#let fold-styles(chain, data-chain, revoke-chain, fold-chain) = { + // Map name -> up to which index (exclusive) it is revoked. + // + // Importantly, a revoke at index B will apply to + // all rules with the revoked name before that index. + // If that revoke rule is, itself, revoked, that either + // completely eliminates the name from being revoked, + // or it simply leads the name to be revoked up to + // an index A < B. That, or it was also being revoked + // by another unrevoked revoke rule at index C > B, + // in which case the name is still revoked up to C. + // In all cases, the name is always revoked from the + // start until some end index. Otherwise, it isn't + // revoked at all (end index 0). + let active-revokes = (:) + + let first-active-index = 0 + + // Revoke revoked revokes by analyzing revokes in reverse + // order: a revoke that came later always takes priority. + for revoke in revoke-chain.rev() { + // This revoke will revoke rules named 'revoking' up to 'index' in the chain, which + // automatically revokes revoke rules before it as well, since they were added when + // the chain length was smaller (or the same), and 'index' is always the chain length + // at the moment the revoke rule was added. + // + // We don't explicitly add revoke rules to the chain as their order in the revoke-chain + // list is enough to know which revoke rules can revoke others, and the index indicates + // which set rules are revoked. + // + // Regarding the first part of the AND, note that, if a name is already revoked up to + // index C from a later revoke (since we're going in reverse, so this one appears earlier + // than the previous ones), then revoking it up to index B <= C for this revoke is + // unnecessary since the index interval [0, B) is already contained in [0, C). + // + // In other words, only the last revoke for a particular name matters, which is the + // first one we find in this loop. + // + // (As you can see, we assume above that, if revoke 1 comes before revoke 2 in the revoke-chain + // (before reversing), with revoke 1 applying up to chain index B and revoke 2 up to index C, + // then B <= C. This is enforced in 'prepare-rules' as we analyze revokes and push their + // information to the chain in order (outer to inner / earlier to later).) + let was-not-revoked = ( + ( + "names" not in revoke or revoke.names.all(n => n not in active-revokes) + ) + and ( + "names" in revoke or "name" not in revoke or revoke.name == none or revoke.name not in active-revokes + ) + ) + + if revoke.kind == "revoke" and revoke.revoking not in active-revokes and was-not-revoked { + active-revokes.insert(revoke.revoking, revoke.index) + } else if revoke.kind == "reset" and was-not-revoked { + // Applying a reset, so we delete everything before this index and stop revoking since + // any revokes before this reset won't count anymore. + first-active-index = revoke.index + + chain = if chain.len() <= first-active-index { + () + } else { + chain.slice(first-active-index) + } + + data-chain = if data-chain.len() <= first-active-index { + () + } else { + data-chain.slice(first-active-index) + } + + for (field-name, fold-data) in fold-chain { + let first-fold-index = fold-data.data.position(d => d.index >= first-active-index) + if first-fold-index == none { + // All folded values removed. + // The caller will be responsible for joining the default value with the + // final arguments (without any chain values inbetween) if that's necessary. + _ = fold-chain.remove(field-name) + } else { + fold-chain.at(field-name).values = fold-data.values.slice(first-fold-index) + fold-chain.at(field-name).data = fold-data.data.slice(first-fold-index) + } + } + + // No need to analyze any further revoke rules since everything was reset. + break + } + } + + if active-revokes != (:) { + let i = first-active-index + for data in data-chain { + if data != none and ( + "names" in data and data.names.any(n => n in active-revokes and i < active-revokes.at(n)) + or "names" not in data and "name" in data and data.name in active-revokes and i < active-revokes.at(data.name) + ) { + // Nullify changes at this stage + chain.at(i) = (:) + } + + i += 1 + } + + for (field-name, fold-data) in fold-chain { + let filtered-data = fold-data.data.filter(d => ( + // Only keep data without a name in the revoked name map, or, if the + // name is there, then data that came after the name was revoked. + ("names" not in d or d.names.all(n => n not in active-revokes or d.index >= active-revokes.at(n))) + and ("names" in d or "name" not in d or (d.name == none or d.name not in active-revokes or d.index >= active-revokes.at(d.name))) + )) + if filtered-data == () { + _ = fold-chain.remove(field-name) + } else { + fold-chain.at(field-name).data = filtered-data + fold-chain.at(field-name).values = filtered-data.map(d => d.value) + } + } + } + + let final-values = chain.sum(default: (:)) + + // Apply folds separately (their fields' values are meaningless in the above dict) + for (field-name, fold-data) in fold-chain { + final-values.at(field-name) = if fold-data.values == () { + fold-data.default + } else if fold-data.folder == auto { + fold-data.default + fold-data.values.sum() + } else { + fold-data.values.fold(fold-data.default, fold-data.folder) + } + } + + (folded: final-values, active-revokes: active-revokes, first-active-index: first-active-index) +} + +// Retrieves the final chain data for an element, after applying all set rules so far. +#let get-styles(element, elements: (:), use-routine: false) = { + if type(element) == function { + element = data(element) + } + let (eid, default-fields) = if type(element) == dictionary and "eid" in element and "default-fields" in element { + (element.eid, element.default-fields) + } else { + assert(false, message: "elembic: element.get: expected element (function / data dictionary), received " + str(type(element))) + } + + if ( + use-routine + and ("version" not in element or element.version != element-version) + and "routines" in element + and "get-styles" in element.routines + and type(element.routines.get-styles) == function + ) { + // Use the element's own "get styles". + return (element.routines.get-styles)(element, elements: elements) + } + + let element-data = elements.at(eid, default: default-data) + let folded-chain = if element-data.revoke-chain == default-data.revoke-chain and element-data.fold-chain == default-data.fold-chain { + element-data.chain.sum(default: (:)) + } else { + fold-styles(element-data.chain, element-data.data-chain, element-data.revoke-chain, element-data.fold-chain).folded + } + + // No need to do extra folding like in constructor: + // if a foldable field hasn't been specified, it is either equal to + // its default, or it is a required field which has no default and + // thus it is not returned here since it can't be set. + default-fields + folded-chain +} + +/// Reads the current values of element fields after applying set rules. +/// Must be in a context block. +/// +/// This is a stateful version, which doesn't require a callback, but only +/// works on stateful mode (less performant). +/// +/// USAGE: +/// ```typ +/// #show: e.set_(elem, fill: green) +/// // ... +/// #context { +/// // OK +/// assert(e.stateful.get(elem).fill == green) +/// } +/// ``` +/// +/// - receiver (function): function ('get' function) -> content +/// -> content +#let stateful-get(element) = { + let chain = style-state.get() + let global-data = if chain == () { + default-global-data + } else { + chain.last() + } + + assert( + global-data.stateful, + message: "elembic: stateful.get: cannot use this function without enabling the global stateful toggle\n hint: if you don't mind the performance hit, write '#show: e.stateful.enable()' somewhere above the 'context {}' in which this call happens, or at the top of the document to apply to all rules as well" + ) + + get-styles(element, elements: global-data.elements, use-routine: true) +} + +/// Used for debugging elembic. Stateful version of `debug-get`. +#let stateful-debug-get() = { + let chain = style-state.get() + let global-data = if chain == () { + default-global-data + } else { + chain.last() + } + + assert( + global-data.stateful, + message: "elembic: stateful.debug-get: cannot use this function without enabling the global stateful toggle\n hint: if you don't mind the performance hit, write '#show: e.stateful.enable()' somewhere above the 'context {}' in which this call happens, or at the top of the document to apply to all rules as well" + ) + + let getter = get-styles.with(elements: global-data.elements, use-routine: true) + (:..global-data, ctx: (get: getter)) +} + +#let _is-get(body) = ( + type(body) == content + and body.func() == sequence + and body.children.len() == 2 // [#context{...}#metadata(get-meta)#lbl-special-rule-tag] + and body.children.last().func() == metadata + and body.at("label", default: none) == none + and body.children.last().at("label", default: none) == lbl-special-rule-tag + and body.children.last().value.kind == "get" +) + +#let _recurse-get(body, elements: none) = { + let get-meta = body.children.last().value + + if "__future" in get-meta and element-version <= get-meta.__future.max-version { + let res = (get-meta.__future.call)(body, __future-version: element-version) + + if "doc" in res { + res.doc + } + } else if "receiver" in get-meta and type(get-meta.receiver) == function { + // Pick up updates from filtered rules + let getter = get-styles.with(elements: elements, use-routine: true) + (get-meta.receiver)(getter) + } +} + +#let prepare-ctx(receiver, include-global: false) = context { + let previous-bib-title = bibliography.title + [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + (..default-global-data, first-bib-title: previous-bib-title) + } + + if global-data.stateful { + let chain = style-state.get() + global-data = if chain == () { + default-global-data + } else { + chain.last() + } + } + + set bibliography(title: previous-bib-title) + + let getter = get-styles.with(elements: global-data.elements, use-routine: true) + let body = if include-global { + receiver((:..global-data, ctx: (get: getter))) + } else { + receiver(getter) + } + + // Optimization: flatten 'get' + while _is-get(body) { body = _recurse-get(body, elements: global-data.elements) } + body + }#lbl-get] +} + +/// Reads the current values of element fields after applying set rules. +/// +/// The callback receives a 'get' function which can be used to read the +/// values for a given element. The content returned by the function, which +/// depends on those values, is then placed into the document. +/// +/// USAGE: +/// ```typ +/// #show: e.set_(elem, fill: green) +/// // ... +/// #e.get(get => { +/// // OK +/// assert(get(elem).fill == green) +/// }) +/// ``` +/// +/// - receiver (function): function ('get' function) -> content +/// -> content +#let prepare-get(receiver) = { + let output = prepare-ctx(include-global: false, receiver) + [#output#metadata(((special-rule-key): "get", data-kind: "special-rule", kind: "get", version: element-version, receiver: receiver))#lbl-special-rule-tag] +} + +/// Used for debugging elembic. Passes the internal style chain information. +#let prepare-debug(receiver) = { + let output = prepare-ctx(include-global: true, receiver) + [#output#metadata(((special-rule-key): "debug-get", data-kind: "special-rule", kind: "debug-get", version: element-version, receiver: receiver))#lbl-special-rule-tag] +} + +// Obtain a Typst selector to use to match this element in show rules or in the outline. +// Specify 'meta: true' to match this element in a query, as that selector is +// generated once regardless of show rules. +#let elem-selector(elem, outline: false, outer: false, meta: false) = { + if outline { + assert(not outer, message: "elembic: element.selector: cannot have 'outline: true' and 'outer: true' at the same time, please pick one selector") + assert(not meta, message: "elembic: element.selector: cannot have 'outline: true' and 'meta: true' at the same time, please pick one selector") + let elem-data = data(elem) + assert("outline-sel" in elem-data, message: "elembic: element.selector: this isn't a valid element") + assert(elem-data.outline-sel != none, message: "elembic: element.selector: this element isn't outlinable\n hint: try asking its author to define it as such with 'outline: auto', 'outline: (caption: [...])' or 'outline: (caption: it => ...)'") + elem-data.outline-sel + } else if outer { + assert(not meta, message: "elembic: element.selector: cannot have 'outer: true' and 'meta: true' at the same time, please pick one selector") + data(elem).outer-sel + } else if meta { + let elem-data = data(elem) + elem-data.at("meta-sel", default: elem-data.sel) + } else { + data(elem).sel + } +} + +#let elem-query(filter, before: none, after: none) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + + if type(filter) != dictionary or filter-key not in filter { + if type(filter) == selector { + assert(false, message: "elembic: element.query: Typst-native selectors cannot be specified here, only those of custom elements") + } + assert(false, message: "elembic: element.query: expected a valid filter, such as 'custom-element' or 'custom-element.with(field-name: value, ...)', got " + base.typename(filter)) + } + + assert("elements" in filter, message: "elembic: element.query: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + assert(filter.elements != none, message: "elembic: element.query: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + + let results = () + for (eid, elem-data) in filter.elements { + if "meta-sel" in elem-data { + let sel = elem-data.meta-sel + if before != none { + sel = selector(sel).before(before) + } + if after != none { + sel = selector(sel).after(after) + } + + results += query(sel).filter( + instance => ( + instance.func() == metadata + and { + let meta = data(instance.value) + + verify-filter( + meta.at("fields", default: (:)), + eid: eid, + filter: filter, + ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry and meta.at("ctx", default: none) != none and "ancestry" in meta.ctx { + meta.ctx.ancestry + } else { + () + } + ) and "rendered" in instance.value + } + ) + ).map( + instance => instance.value.rendered + ) + } else if "sel" in elem-data { + let sel = elem-data.sel + if before != none { + sel = selector(sel).before(before) + } + if after != none { + sel = selector(sel).after(after) + } + // This element is probably too outdated to have ancestry checks anyway, so we don't bother + results += query(sel).filter(instance => verify-filter(data(instance).at("fields", default: (:)), eid: eid, filter: filter, ancestry: ())) + } else { + assert(false, message: "elembic: element.query: filter did not have the element's meta selector") + } + } + + results +} + +#let default-prepare-rules(doc) = { + // Custom references + show ref: ref_ + + // Fix labelable elements + show figure.where(kind: labelable-elem-figure-kind): set align(start) + show figure.where(kind: labelable-elem-figure-kind): fig => { + let fig-label = fig.at("label", default: none) + if fig-label == none { + fig.body + } else { + let fig-data = data(fig) + if "instance-internal-args" in fig-data and "default-constructor" in fig-data { + let named-args = fig-data.instance-internal-args.named() + let given-label = named-args.at("label", default: none) + + if "label" in named-args and (given-label == none or type(given-label) == label and given-label != fig-label) { + // Don't override label specified via argument, unless it's equal to + // the figure's label, then we should avoid a double label + fig.body + } else { + (fig-data.default-constructor)(..fig-data.instance-internal-args, label: fig-label, __elembic_outer_label: true) + } + } else { + fig.body + } + } + } + + // Remove error about missing preparation + show lbl-empty-prepare-check: none + + doc +} + +/// Applies necessary show rules to the entire document so that custom elements behave +/// properly. This is usually only needed for elements which have custom references, +/// since, in that case, the document-wide rule `#show ref: e.ref` is required. +/// In addition, labelable elements (supporting outer labels) also need at least +/// `#show: e.prepare()` to work properly. +/// **It is recommended to always use `e.prepare` when using Elembic.** +/// +/// However, **some custom elements also have their own `prepare` functions.** (Read +/// their documentation to know if that's the case.) Then, you may specify their functions +/// as parameters to this function, and this function will run the `prepare` function of +/// each element. Not specifying any elements will just run the default rules, which may +/// still be important. +/// +/// As an example, an element may use its own `prepare` function to apply some special +/// behavior to its `outline`. +/// +/// USAGE: +/// ```rs +/// // Apply default rules + special rules for these elements (if they need it) +/// #show: e.prepare(elemA, elemB) +/// +/// // Apply default rules only +/// #show: e.prepare() +/// ``` +/// - args (arguments): element functions which need special preparation, or none to just apply default rules +/// -> function +#let prepare( + ..args +) = { + assert(args.named() == (:), message: "elembic: element.prepare: unexpected named arguments") + + if args.pos() == () { + return default-prepare-rules + } + + let elems = args.pos().map(data) + + if elems.len() == 1 and type(args.pos().first()) == content { + assert(false, message: "elembic: element.prepare: expected (optional) element functions as arguments, not the document\n hint: write '#show: e.prepare()', not '#show: e.prepare' - note the parentheses") + } + + assert(elems.all(it => it.data-kind == "element"), message: "elembic: element.prepare: positional arguments must be elements") + let prepares = elems.filter(elem => "prepare" in elem and elem.prepare != none).map(elem => { + if "lbl-elem-prepare-check" in elem { + doc => { + // Remove error about missing element preparation + show elem.lbl-elem-prepare-check: none + + (elem.prepare)(elem.func, doc) + } + } else { + elem.prepare.with(elem.func) + } + }) + + doc => { + show: default-prepare-rules + prepares.fold(doc, (acc, prepare) => prepare(acc)) + } +} + +/// Creates a new element, returning its constructor. Read the "Creating custom elements" +/// chapter for more information. +/// +/// USAGE: +/// +/// ```typ +/// #import "@preview/elembic:X.X.X" as e: field +/// +/// // For references to apply +/// #show: e.prepare() +/// +/// #let elem = e.element.declare( +/// "elem", +/// prefix: "@preview/my-package,v1", +/// display: it => { +/// [== #it.title] +/// block(fill: it.fill)[#it.inner] +/// }, +/// fields: ( +/// field("fill", e.types.option(e.types.paint)), +/// field("inner", content, default: [Hello!]), +/// field("title", content, default: [Hello!]), +/// ), +/// reference: ( +/// supplement: [Elem], +/// numbering: "1" +/// ), +/// outline: (caption: it => it.title), +/// ) +/// +/// #outline(target: e.selector(elem, outline: true)) +/// +/// #elem() +/// #elem(title: [abc], label: ) +/// @abc +/// ``` +/// +/// - name (str): The element's name. +/// - prefix (str): The element's prefix, used to distinguish it from elements with the same name. This is usually your package's name alongside a (major) version. +/// - doc (none | str): The element's documentation, if any. +/// - display (function): Function `fields => content` to display the element. +/// - fields (array): Array with this element's fields. +/// - parse-args (auto | function): Optional override for the built-in argument parser +/// (or `auto` to keep as is). Must be in the form +/// `function(args, include-required: bool) => dictionary`, where `include-required: true` +/// means required fields are enforced (constructor), while `include-required: false` means +/// they are forbidden (set rules). +/// - typecheck (bool): Set to `false` to disable field typechecking. +/// - allow-unknown-fields (bool): Set to `true` to allow users to specify unknown +/// fields to your element. They are not typechecked and are simply forwarded to +/// the element's fields by the argument parser. +/// - template (none | function): Optional function displayed element => content to define overridable default set rules for your elements, such as paragraph settings. Users can override these settings with show-set rules on elements. +/// - prepare (none | function): Optional function (element, document) => content +/// to define show and set rules that should be applied to the whole document for your +/// element to properly function. +/// - construct (none | function): Optional function that overrides the default +/// element constructor, returning arbitrary content. This should be used over +/// manually wrapping the returned constructor as it ensures set rules and data +/// extraction from the constructor still work. +/// - scope (none | dictionary | module): Optional scope with associated data for your +/// element. This could be a module with constructors for associated elements, for +/// instance. This value can be accessed with `e.scope(elem)`, e.g. +/// `#import e.scope(elem): sub-elem`. +/// - count (none | function): Optional function `counter => (content | function fields => content)` +/// which inserts a counter step before the element. Ensures the element's display function has +/// updated context to get the latest counter value (after the step / update) with +/// `e.counter(it).get()`. Defaults to `counter.step` to step the counter once before +/// each element placed. +/// - labelable (auto|bool): Set this to `true` to support outer label syntax: `#elem(...) `. +/// The downsides are that `#show: e.prepare()` becomes required to use the element, the element can no longer be inline, +/// and show rules on the individual labels no longer have access to final fields. +/// Defaults to `auto`, which still allows labeling without those downsides by specifying `#element(label: )`, +/// ensuring show rules on that label work and have access to the element's final fields. +/// In both cases, also allows referring to labeled elements with `@chosen-label` (requires `#show: e.prepare()` to work), +/// but the element may not have its own settable field named `label`. +/// When `false`, the element may have a field named `label` instead, but it won't have any of those effects. +/// - reference (none | (supplement: none | str | content | function fields => str | content, numbering: none | str | function fields => str | function, custom: none | function fields => content)): +/// When not `none`, allows referring to the new element with Typst's built-in +/// `@ref` syntax. Requires the user to execute `#show: e.prepare()` at the top +/// of their document (it is part of the default rules, so `prepare` needs no +/// arguments there). Specify either a `supplement` and `numbering` for references +/// looking like "Name 2", and/or `custom` to show some fully customized content +/// for the reference instead. +/// - outline (none | auto | dictionary): +/// Accepts either `auto` or a dictionary of the form +/// `(caption: str | content | function fields => content)`. +/// When not `none`, allows creating an outline for the element's appearances +/// with `#outline(target: e.selector(elem, outline: true))`. When set to `auto`, +/// the entries will display "Name 2" based on reference information. When a caption +/// is specified, it will display as "Name 2: caption", unless supplement and numbering +/// for reference are both none. +/// - synthesize (none | function): Can be set to a function `fields => fields` to +/// override final values of fields, or create new fields based on final values of +/// fields, before the first show rule. When computing new fields based on other +/// fields, please specify those new fields in the fields array with +/// `synthesized: true`. This forbids the user from specifying them manually, +/// but allows them to filter based on that field. +/// - contextual (bool): When set to `true`, functions `fields => something` for +/// other options, including `display`, will be able to access the current +/// values of set rules with `(e.ctx(fields).get)(other-elem)`. In addition, +/// an additional context block is created, so that you may access the correct +/// values for `native-elem.field` in the context. In practice, this is a bit +/// expensive, and so this option shouldn't be enabled unless you need precisely +/// `bibliography.title`, or you really need to get set rule information from +/// other elements within functions such as `synthesize` or `display`. +/// -> function +#let declare( + name, + display: none, + fields: none, + prefix: none, + doc: none, + parse-args: auto, + typecheck: true, + allow-unknown-fields: false, + template: none, + prepare: none, + construct: none, + scope: none, + count: counter.step, + labelable: auto, + reference: none, + outline: none, + synthesize: none, + contextual: false, +) = { + assert(type(display) == function, message: "elembic: element.declare: please specify a show rule in 'display:' to determine how your element is displayed.") + + let fields-hint = if type(fields) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single field: write 'fields: (field,)', not 'fields: (field)'" } else { "" } + assert(type(fields) == array, message: "elembic: element.declare: please specify an array of fields, creating each field with the 'field' function. It can be empty with '()'." + fields-hint) + assert(doc == none or type(doc) == str, message: "elembic: element.declare: 'doc' must be none or a string (add documentation)") + assert(prefix != none, message: "elembic: element.declare: please specify a 'prefix: ...' for your type, to distinguish it from types with the same name. If you are writing a package or template to be used by others, please do not use an empty prefix.") + assert(type(prefix) == str, message: "elembic: element.declare: the prefix must be a string, not '" + str(type(prefix)) + "'") + assert(parse-args == auto or type(parse-args) == function, message: "elembic: element.declare: 'parse-args' must be either 'auto' (use built-in parser) or a function (default arg parser, fields: dictionary, typecheck: bool) => (user arguments, include-required: true (required fields must be specified - in constructor) / false (required fields must be omitted - in set rules)) => (bool (true on success, false on error), dictionary with parsed fields (or error message string if the bool is false)).") + assert(type(typecheck) == bool, message: "elembic: element.declare: the 'typecheck' argument must be a boolean (true to enable typechecking, false to disable).") + assert(type(allow-unknown-fields) == bool, message: "elembic: element.declare: the 'allow-unknown-fields' argument must be a boolean.") + assert(template == none or type(template) == function, message: "elembic: element.declare: 'template' must be 'none' or a function displayed element => content (usually set rules applied on the displayed element). This is used to add a set of overridable set rules to the element, such as paragraph settings.") + assert(prepare == none or type(prepare) == function, message: "elembic: element.declare: 'prepare' must be 'none' or a function (element, document) => styled document (used to apply show and set rules to the document).") + assert(count == none or type(count) == function, message: "elembic: element.declare: 'count' must be 'none', a function counter => counter step/update element, or a function counter => final fields => counter step/update element.") + assert(synthesize == none or type(synthesize) == function, message: "elembic: element.declare: 'synthesize' must be 'none' or a function element fields => element fields.") + assert(contextual == auto or type(contextual) == bool, message: "elembic: element.declare: 'contextual' must be 'auto' (true if using a contextual feature) or a boolean (true to wrap the output in a 'context { ... }', false to not).") + assert(construct == none or type(construct) == function, message: "elembic: element.declare: 'construct' must be 'none' (use default constructor) or a function receiving the original constructor and returning the new constructor.") + assert(scope == none or type(scope) in (dictionary, module), message: "elembic: element.declare: 'scope' must be either 'none', a dictionary or a module") + assert(labelable == auto or type(labelable) == bool, message: "elembic: element.declare: 'labelable' must be auto (only adds the 'label' constructor argument with 'elem(label: