diff --git a/packages/preview/hydra/0.5.2/CHANGELOG.md b/packages/preview/hydra/0.5.2/CHANGELOG.md new file mode 100644 index 0000000000..7797ba6607 --- /dev/null +++ b/packages/preview/hydra/0.5.2/CHANGELOG.md @@ -0,0 +1,73 @@ +# [unreleased](https://github.com/tingerrr/hydra/releases/tags/) +## Added + +## Removed + +## Changed + +## Fixed + +--- + +# [v0.5.2](https://github.com/tingerrr/hydra/releases/tags/v0.5.2) +## Fixed +- Fixed a panic on the development version of typst caused by an outdated version of oxifmt + +--- + +# [v0.5.1](https://github.com/tingerrr/hydra/releases/tags/v0.5.1) +## Fixed +- hydra no longer considers candidates on pages after the current page (https://github.com/tingerrr/hydra/pull/21) + +--- + +# [v0.5.0](https://github.com/tingerrr/hydra/releases/tags/v0.5.0) +## Added +- `use-last` parameter on `hydra` for a more LaTeX style heading look up, thanks @freundTech! + - `hydra` now has a new `use-last` parameter + - `context` now has a new `use-last` field + - **BREAKING CHANGE** `candidates` now has a new `last` field containing a suitable match for the last primary candidate on this page + +--- + +# [v0.4.0](https://github.com/tingerrr/hydra/releases/tags/v0.4.0) +Almost all changes in this release are **BREAKING CHANGES**. + +## Added +- internal util functions and dictionaries for recreating `auto` fallbacks used within the typst + compiler + - `core.get-text-dir` - returns the text direction + - `core.get-binding` - returns the page binding + - `core.get-top-margin` - returns the absolute top margin + - `util.text-direction` - returns the text direction for a given language + - `util.page-binding` - returns the page binding for a given text direction + +## Removed +- various parameters on `hydra` have been removed + - `paper` has been removed in favor of get rule + - `page-size` has been removed in favor of get rule + - `top-margin` has been removed in favor of get rule + - `loc` has been removed in favor of user provided context +- internal util dictionary for page sizes + +## Changed +- hydra now requires a minimum Typst compiler version of `0.11.0` +- `hydra` is now contextual +- most internal functions are now contextual +- the internal context dictionary now holds a `anchor-loc` instead of a `loc` +- `get-anchor-pos` has been renamed to `locate-last-anchor` +- the internal `page-sizes` dictionary was changed to function +- various parameters on `hydra` are now auto by default + - `prev-filter` + - `next-filter` + - `display` + - `dir` + - `binding` + +--- + +# [v0.3.0](https://github.com/tingerrr/hydra/releases/tags/v0.3.0) + +# [v0.2.0](https://github.com/tingerrr/hydra/releases/tags/v0.2.0) + +# [v0.1.0](https://github.com/tingerrr/hydra/releases/tags/v0.1.0) diff --git a/packages/preview/hydra/0.5.2/LICENSE b/packages/preview/hydra/0.5.2/LICENSE new file mode 100644 index 0000000000..b193e61f8d --- /dev/null +++ b/packages/preview/hydra/0.5.2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 - 2025 tinger + +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/packages/preview/hydra/0.5.2/README.md b/packages/preview/hydra/0.5.2/README.md new file mode 100644 index 0000000000..1bb8e0583b --- /dev/null +++ b/packages/preview/hydra/0.5.2/README.md @@ -0,0 +1,44 @@ +# hydra +Hydra is a Typst package allowing you to easily display the heading like elements anywhere in your +document. It's primary focus is to provide the reader with a reminder of where they currently are in +your document only when it is needed. + +## Example +```typst +#import "@preview/hydra:0.5.2": hydra + +#set page(paper: "a7", margin: (y: 4em), numbering: "1", header: context { + if calc.odd(here().page()) { + align(right, emph(hydra(1))) + } else { + align(left, emph(hydra(2))) + } + line(length: 100%) +}) +#set heading(numbering: "1.1") +#show heading.where(level: 1): it => pagebreak(weak: true) + it + += Introduction +#lorem(50) + += Content +== First Section +#lorem(50) +== Second Section +#lorem(100) +``` +![ex] + +## Documentation +For a more in-depth description of hydra's functionality and the reference read its [manual]. + +## Contribution +For contributing, please take a look [CONTRIBUTING][contrib]. + +## Etymology +The package name hydra /ˈhaɪdrə/ is a word play headings and headers, inspired by the monster in +greek and roman mythology resembling a serpent with many heads. + +[ex]: examples/example.png +[manual]: doc/manual.pdf +[contrib]: CONTRIBUTING.md diff --git a/packages/preview/hydra/0.5.2/doc/manual.pdf b/packages/preview/hydra/0.5.2/doc/manual.pdf new file mode 100644 index 0000000000..91b1685635 Binary files /dev/null and b/packages/preview/hydra/0.5.2/doc/manual.pdf differ diff --git a/packages/preview/hydra/0.5.2/examples/example.png b/packages/preview/hydra/0.5.2/examples/example.png new file mode 100644 index 0000000000..aaed9509fc Binary files /dev/null and b/packages/preview/hydra/0.5.2/examples/example.png differ diff --git a/packages/preview/hydra/0.5.2/src/core.typ b/packages/preview/hydra/0.5.2/src/core.typ new file mode 100644 index 0000000000..e1579886b7 --- /dev/null +++ b/packages/preview/hydra/0.5.2/src/core.typ @@ -0,0 +1,254 @@ +#import "/src/util.typ" + +/// Returns the current text direction. +/// +/// This function is contextual. +/// +/// -> direction +#let get-text-dir() = util.auto-or(text.dir, () => util.text-direction(text.lang)) + +/// Returns the current page binding. +/// +/// This function is contextual. +/// +/// -> alignment +#let get-page-binding() = util.auto-or(page.binding, () => util.page-binding(get-text-dir())) + +/// Returns the current top margin. +/// +/// This function is contextual. +/// +/// -> length +#let get-top-margin() = { + let margin = page.margin + if type(margin) == dictionary { + margin = if "top" in margin { + margin.top + } else if "y" in margin { + margin.y + } else { + panic(util.fmt("Margin did not contain `top` or `y` key: `{}`", margin)) + } + } + + let inf = float("inf") * 1mm + let width = util.auto-or(page.width, () => inf) + let height = util.auto-or(page.height, () => inf) + let min = calc.min(width, height) + + // if both were auto, we fallback to a4 margins + if min == inf { + min = 210.0mm + } + + // `+ 0%` forces this to be a relative length + margin = util.auto-or(margin, () => (2.5 / 21) * min) + 0% + margin.length.to-absolute() + (min * margin.ratio) +} + +/// Get the last anchor location. Panics if the last anchor was not on the page of this context. +/// +/// This function is contextual. +/// +/// - ctx (context): The context from which to start. +/// -> location +#let locate-last-anchor(ctx) = { + let starting-locs = query(selector(ctx.anchor).before(here())) + + assert.ne(starting-locs.len(), 0, + message: "No `anchor()` found while searching from outside the page header", + ) + + let anchor = starting-locs.last().location() + + // NOTE: this check ensures that get rules are done within the same page as the queries + // ideally those would be done within the context of the anchor, such that a change in text + // direction between anchor and query does not cause any issues + assert.eq(anchor.page(), here().page(), + message: "`anchor()` must be on every page before the first use of `hydra`" + ) + + anchor +} + +/// Get the element candidates for the given context. +/// +/// This function is contextual. +/// +/// - ctx (context): The context for which to get the candidates. +/// - scope-prev (bool): Whether the search should be scoped by the first ancestor element in this +/// direction. +/// - scope-next (bool): Whether the search should be scoped by the first ancestor element in this +/// direction. +/// -> candidates +#let get-candidates(ctx, scope-prev: true, scope-next: true) = { + let look-prev = selector(ctx.primary.target).before(ctx.anchor-loc) + let look-next = selector(ctx.primary.target).after(ctx.anchor-loc) + let look-last = look-next + + let prev-ancestor = none + let next-ancestor = none + + if ctx.ancestors != none { + let prev-ancestors = query(selector(ctx.ancestors.target).before(ctx.anchor-loc)) + let next-ancestors = query(selector(ctx.ancestors.target).after(ctx.anchor-loc)) + + if ctx.ancestors.filter != none { + prev-ancestors = prev-ancestors.filter(x => (ctx.ancestors.filter)(ctx, x)) + next-ancestors = next-ancestors.filter(x => (ctx.ancestors.filter)(ctx, x)) + } + + if scope-prev and prev-ancestors != () { + prev-ancestor = prev-ancestors.last() + look-prev = look-prev.after(prev-ancestor.location()) + } + + if scope-next and next-ancestors != () { + next-ancestor = next-ancestors.first() + look-next = look-next.before(next-ancestor.location()) + } + } + + let prev-targets = query(look-prev) + let next-targets = query(look-next) + let last-targets = query(look-last) + + if ctx.primary.filter != none { + prev-targets = prev-targets.filter(x => (ctx.primary.filter)(ctx, x)) + next-targets = next-targets.filter(x => (ctx.primary.filter)(ctx, x)) + last-targets = last-targets.filter(x => (ctx.primary.filter)(ctx, x)) + } + next-targets = next-targets.filter(x => x.location().page() == ctx.anchor-loc.page()) + last-targets = last-targets.filter(x => x.location().page() == ctx.anchor-loc.page()) + + let prev = if prev-targets != () { prev-targets.last() } + let next = if next-targets != () { next-targets.first() } + let last = if last-targets != () { last-targets.last() } + + ( + primary: (prev: prev, next: next, last: last), + ancestor: (prev: prev-ancestor, next: next-ancestor), + ) +} + +/// Checks if the current context is on a starting page, i.e. if the next candidates are on top of +/// this context's page. +/// +/// This function is contextual. +/// +/// - ctx (context): The context in which the visibility of the next candidates should be checked. +/// - candidates (candidates): The candidates for this context. +/// -> bool +#let is-on-starting-page(ctx, candidates) = { + let next = if candidates.primary.next != none { candidates.primary.next.location() } + let next-ancestor = if candidates.ancestor.next != none { candidates.ancestor.next.location() } + + let next-starting = if next != none { + next.page() == here().page() and next.position().y <= get-top-margin() + } else { + false + } + + let next-ancestor-starting = if next-ancestor != none { + next-ancestor.page() == here().page() and next-ancestor.position().y <= get-top-margin() + } else { + false + } + + next-starting or next-ancestor-starting +} + +/// Checks if the previous primary candidate is still visible. +/// +/// This function is contextual. +/// +/// - ctx (context): The context in which the visibility of the previous primary candidate should be +/// checked. +/// - candidates (candidates): The candidates for this context. +/// -> bool +#let is-active-visible(ctx, candidates) = { + // depending on the reading direction and binding combination the leading page is either on an odd + // or even number, if it is leading it means the previous page is visible + let cases = ( + left: ( + ltr: calc.odd, + rtl: calc.even, + ), + right: ( + ltr: calc.even, + rtl: calc.odd, + ), + ) + + let is-leading-page = (cases.at(repr(ctx.binding)).at(repr(ctx.dir)))(here().page()) + let active-on-prev-page = candidates.primary.prev.location().page() == here().page() - 1 + + is-leading-page and active-on-prev-page +} + +/// Check if showing the active element would be redudnant in the current context. +/// +/// This function is contextual. +/// +/// - ctx (context): The context in which the redundancy of the previous primary candidate should be +/// checked. +/// - candidates (candidates): The candidates for this context. +/// -> bool +#let is-active-redundant(ctx, candidates) = { + let active-visible = ( + ctx.book and candidates.primary.prev != none and is-active-visible(ctx, candidates) + ) + let starting-page = is-on-starting-page(ctx, candidates) + + active-visible or starting-page +} + +/// Display a heading's numbering and body. +/// +/// - ctx (context): The context in which the element was found. +/// - candidate (content): The heading to display, panics if this is not a heading. +/// -> content +#let display(ctx, candidate) = { + util.assert.element("candidate", candidate, heading, + message: "Use a custom `display` function for elements other than headings", + ) + + if candidate.has("numbering") and candidate.numbering != none { + numbering(candidate.numbering, ..counter(heading).at(candidate.location())) + [ ] + } + + candidate.body +} + +/// Execute the core logic to find and display elements for the current context. +/// +/// This function is contextual. +/// +/// - ctx (context): The context for which to find and display the element. +/// -> content +#let execute(ctx) = { + ctx.anchor-loc = if ctx.anchor != none and here().position().y > get-top-margin() { + locate-last-anchor(ctx) + } else { + here() + } + + let candidates = get-candidates(ctx) + let prev-eligible = candidates.primary.prev != none and (ctx.prev-filter)(ctx, candidates) + let next-eligible = candidates.primary.next != none and (ctx.next-filter)(ctx, candidates) + let last-eligible = candidates.primary.last != none and (ctx.next-filter)(ctx, candidates) + let active-redundant = is-active-redundant(ctx, candidates) + + if active-redundant and ctx.skip-starting { + return + } + + if ctx.use-last and last-eligible { + (ctx.display)(ctx, candidates.primary.last) + } else if prev-eligible and not active-redundant { + (ctx.display)(ctx, candidates.primary.prev) + } else if next-eligible { + (ctx.display)(ctx, candidates.primary.next) + } +} diff --git a/packages/preview/hydra/0.5.2/src/lib.typ b/packages/preview/hydra/0.5.2/src/lib.typ new file mode 100644 index 0000000000..c1b0f688b6 --- /dev/null +++ b/packages/preview/hydra/0.5.2/src/lib.typ @@ -0,0 +1,95 @@ +#import "/src/core.typ" +#import "/src/util.typ" +#import "/src/selectors.typ" + +/// An anchor used to search from. When using `hydra` ouside of the page header, this should be +/// placed inside the pge header to find the correct searching context. `hydra` always searches from +/// the last anchor it finds, if and only if it detects that it is outside of the top-margin. +#let anchor() = [#metadata(()) ] + +/// Query for an element within the bounds of its ancestors. +/// +/// The context passed to various callbacks contains the resolved top-margin, the current location, +/// as well as the binding direction, primary and ancestor element selectors and customized +/// functions. +/// +/// This function is contextual. +/// +/// - ..sel (any): The element to look for, to use other elements than headings, read the +/// documentation on selectors. This can be an element function or selector, or an integer +/// declaring a heading level. +/// - prev-filter (function, auto): A function which receives the `context` and `candidates`, and +/// returns if they are eligible for display. This function is called at most once. The primary +/// next candidate may be none. If this is `auto` no filter is applied. +/// - next-filter (function, auto): A function which receives the `context` and `candidates`, and +/// returns if they are eligible for display. This function is called at most once. The primary +/// prev candidate may be none. If this is `auto` no filter is applied. +/// - display (function, auto): A function which receives the `context` and candidate element to +/// display. If this is `auto`, the default implementaion will be used. +/// - skip-starting (bool): Whether `hydra` should show the current candidate even if it's on top of +/// the current page. +/// - use-last (bool): If hydra should show the name of the first or last candidate on the page. +// Defaults to false. +/// - dir (direction, auto): The reading direction of the document. If this is `auto`, the text +/// direction is used. Be cautious about leaving this option on `auto` if you switch text +/// direction mid-page and use hydra outside of footers or headers. +/// - binding (alignement, auto): The binding of the document. If this is `auto`, the binding is +/// inferred from `dir`, similar to how it is done in page. Be cautious about leaving this on +/// option on `auto` if you switch text direction mid-page and use hydra outside of footers or +/// headers. +/// - book (bool): The binding direction if it should be considered, `none` if not. If the binding +/// direction is set it'll be used to check for redundancy when an element is visible on the last +/// page. Make sure to set `binding` and `dir` if the document is not using left-to-right reading +/// direction. +/// - anchor (label, none): The label to use for the anchor if `hydra` is used outside the header. +/// If this is `none`, the anchor is not searched. +/// -> content +#let hydra( + prev-filter: auto, + next-filter: auto, + display: auto, + skip-starting: true, + use-last: false, + dir: auto, + binding: auto, + book: false, + anchor: , + ..sel, +) = { + util.assert.types("prev-filter", prev-filter, function, auto) + util.assert.types("next-filter", next-filter, function, auto) + util.assert.types("display", display, function, auto) + util.assert.types("skip-starting", skip-starting, bool) + util.assert.types("use-last", use-last, bool) + util.assert.enum("dir", dir, ltr, rtl, auto) + util.assert.enum("binding", binding, left, right, auto) + util.assert.types("book", book, bool) + util.assert.types("anchor", anchor, label, none) + + let (named, pos) = (sel.named(), sel.pos()) + assert.eq(named.len(), 0, message: util.fmt("Unexected named arguments: `{}`", named)) + assert(pos.len() <= 1, message: util.fmt("Unexpected positional arguments: `{}`", pos)) + + let sanitized = selectors.sanitize("sel", pos.at(0, default: heading)) + + let default-filter = (_, _) => true + let dir = util.auto-or(dir, core.get-text-dir) + let binding = util.auto-or(binding, () => page.binding) + let binding = util.auto-or(binding, () => util.page-binding(dir)) + + let ctx = ( + prev-filter: util.auto-or(prev-filter, () => default-filter), + next-filter: util.auto-or(next-filter, () => default-filter), + display: util.auto-or(display, () => core.display), + skip-starting: skip-starting, + use-last: use-last, + dir: dir, + binding: binding, + book: book, + anchor: anchor, + primary: sanitized.primary, + ancestors: sanitized.ancestors, + ) + + core.execute(ctx) +} diff --git a/packages/preview/hydra/0.5.2/src/selectors.typ b/packages/preview/hydra/0.5.2/src/selectors.typ new file mode 100644 index 0000000000..818b245b51 --- /dev/null +++ b/packages/preview/hydra/0.5.2/src/selectors.typ @@ -0,0 +1,147 @@ +#import "/src/util.typ" + +/// Create a custom selector for `hydra`. +/// +/// - element (function, selector): The primary element to search for. +/// - filter (function): The filter to apply to the element. +/// - ancestors (function, selector): The ancestor elements, this should match all of its ancestors. +/// - ancestors-filter (function): The filter applied to the ancestors. +/// -> hydra-selector +#let custom( + element, + filter: none, + ancestors: none, + ancestors-filter: none, +) = { + util.assert.types("element", element, function, selector, label) + util.assert.types("filter", filter, function, none) + util.assert.types("ancestors", ancestors, function, selector, label, none) + util.assert.types("ancestors-filter", ancestors-filter, function, none) + + util.assert.queryable("element", element) + + if ancestors != none { + util.assert.queryable("ancestors", ancestors) + } + + if ancestors == none and ancestors-filter != none { + panic("`ancestor` must be set if `ancestor-filter` is set") + } + + ( + primary: (target: element, filter: filter), + ancestors: if ancestors != none { (target: ancestors, filter: ancestors-filter) }, + ) +} + +/// Create a heading selector for a given range of levels. +/// +/// - ..exact (int, none): The exact level to consider as the primary element +/// - min (int, none): The inclusive minimum level to consider as the primary heading +/// - max (int, none): The inclusive maximum level to consider as the primary heading +/// -> hydra-selector +#let by-level( + min: none, + max: none, + ..exact, +) = { + let (named, pos) = (exact.named(), exact.pos()) + assert.eq(named.len(), 0, message: util.fmt("Unexected named arguments: `{}`", named)) + assert(pos.len() <= 1, message: util.fmt("Unexpected positional arguments: `{}`", pos)) + + exact = pos.at(0, default: none) + + util.assert.types("min", min, int, none) + util.assert.types("max", max, int, none) + util.assert.types("exact", exact, int, none) + + if min == none and max == none and exact == none { + panic("Use `heading` directly if you have no `min`, `max` or `exact` level bound") + } + + if exact != none and (min != none or max != none) { + panic("Can only use `min` and `max`, or `exact` bound, not both") + } + + if exact == none and (min == max) { + exact = min + min = none + max = none + } + + let (primary, primary-filter) = if exact != none { + (heading.where(level: exact), none) + } else if min != none and max != none { + (heading, (ctx, e) => min <= e.level and e.level <= max) + } else if min != none { + (heading, (ctx, e) => min <= e.level) + } else if max != none { + (heading, (ctx, e) => e.level <= max) + } + + let (ancestors, ancestors-filter) = if exact != none { + (heading, (ctx, e) => e.level < exact) + } else if min != none and min > 1 { + (heading, (ctx, e) => e.level < min) + } else { + (none, none) + } + + custom( + primary, + filter: primary-filter, + ancestors: heading, + ancestors-filter: ancestors-filter, + ) +} + +/// Turn a selector or function into a hydra selector. +/// +/// *This function is considered unstable.* +/// +/// - name (str): The name to use in the assertion message. +/// - sel (any): The selector to sanitize. +/// - message (str, auto): The assertion message to use. +/// -> hydra-selector +#let sanitize(name, sel, message: auto) = { + let message = util.or-default(check: auto, message, () => util.fmt( + "`{}` must be a `selector`, a level, or a custom hydra-selector, was {}", name, sel, + )) + + if type(sel) == selector { + let parts = repr(sel).split(".") + + // NOTE: because `repr(math.equation) == equation` we add it to the scope + // NOTE: No, I don't like this either + let func = eval(if parts.len() == 1 { + parts.first() + } else { + parts.slice(0, -1).join(".") + }, scope: (equation: math.equation)) + + if func == heading { + let fields = (:) + if parts.len() > 1 { + let args = parts.remove(-1) + for arg in args.trim("where").trim(regex("\(|\)"), repeat: false).split(",") { + let (name, val) = arg.split(":").map(str.trim) + fields.insert(name, eval(val)) + } + } + + assert.eq(fields.len(), 1, message: message) + assert("level" in fields, message: message) + by-level(fields.level) + } else { + custom(sel) + } + } else if type(sel) == int { + by-level(sel) + } else if type(sel) == function { + custom(sel) + } else if type(sel) == dictionary and "primary" in sel and "ancestors" in sel { + sel + } else { + panic(message) + } +} diff --git a/packages/preview/hydra/0.5.2/src/util.typ b/packages/preview/hydra/0.5.2/src/util.typ new file mode 100644 index 0000000000..a9dc2d3d0e --- /dev/null +++ b/packages/preview/hydra/0.5.2/src/util.typ @@ -0,0 +1,2 @@ +#import "/src/util/assert.typ" +#import "/src/util/core.typ": * diff --git a/packages/preview/hydra/0.5.2/src/util/assert.typ b/packages/preview/hydra/0.5.2/src/util/assert.typ new file mode 100644 index 0000000000..7ca017fcc7 --- /dev/null +++ b/packages/preview/hydra/0.5.2/src/util/assert.typ @@ -0,0 +1,101 @@ +#import "/src/util/core.typ" as _core +#import _core: queryable-functions as _queryable-functions + +/// Assert that `value` is any of the given `expected-values`. +/// +/// - name (str): The name use for the value in the assertion message. +/// - value (any): The value to check for. +/// - message (str, auto): The assertion message to use. +/// - ..expected-values (type): The expected variants of `value`. +#let enum(name, value, ..expected-values, message: auto) = { + expected-values = expected-values.pos() + let message = _core.or-default(check: auto, message, () => if expected-values.len() == 1 { + _core.fmt("`{}` must be `{}`, was `{}`", name, expected-values.first(), value) + } else { + _core.fmt( + "`{}` must be one of {}, was `{}`", + name, + expected-values.map(_core.fmt.with("`{}`")).join(", ", last: " or "), + value, + ) + }) + + assert(value in expected-values, message: message) +} + +/// Assert that `value` is of any of the given `expected-types`. +/// +/// - name (str): The name use for the value in the assertion message. +/// - value (any): The value to check for. +/// - message (str, auto): The assertion message to use. +/// - ..expected-types (type): The expected types of `value`. +#let types(name, value, ..expected-types, message: auto) = { + let given-type = type(value) + expected-types = expected-types.pos().map(t => if t == none { + type(none) + } else if t == auto { + type(auto) + } else { + t + }) + let message = _core.or-default(check: auto, message, () => if expected-types.len() == 1 { + _core.fmt("`{}` must be a `{}`, was `{}`", name, expected-types.first(), given-type) + } else { + _core.fmt( + "`{}` must be one of a {}, was `{}`", + name, + expected-types.map(_core.fmt.with("`{}`")).join(", ", last: " or "), + given-type, + ) + }) + + assert(given-type in expected-types, message: message) +} + +/// Assert that `element` is an element creatd by one of the given `expected-funcs`. +/// +/// - name (str): The name use for the value in the assertion message. +/// - element (any): The value to check for. +/// - message (str, auto): The assertion message to use. +/// - ..expected-funcs (type): The expected element functions of `element`. +#let element(name, element, ..expected-funcs, message: auto) = { + let given-func = element.func() + expected-funcs = expected-funcs.pos() + let message = _core.or-default(check: auto, message, () => if expected-funcs.len() == 1 { + _core.fmt("`{}` must be a `{}`, was `{}`", name, expected-funcs.first(), given-func) + } else { + _core.fmt( + "`{}` must be one of a {}, was `{}`", + name, + expected-funcs.map(_core.fmt.with("`{}`")).join(", ", last: " or a"), + given-func, + ) + }) + + types(name, element, content, message: message) + assert(given-func in expected-funcs, message: message) +} + +/// Assert that `value` can be used in `query`. +/// +/// - name (str): The name use for the value in the assertion message. +/// - value (any): The value to check for. +/// - message (str, auto): The assertion message to use. +#let queryable(name, value, message: auto) = { + let given-type = type(value) + let message = _core.or-default(check: auto, message, () => _core.fmt( + "`{}` must be queryable, such as an element function, selector or label, `{}` is not queryable", + name, + given-type, + )) + + types(name, value, label, function, selector, message: message) + + if type(value) == function { + let message = _core.or-default(check: auto, message, () => { + _core.fmt("`{}` is not an element function, was `{}`", name, value) + }) + assert(value in _queryable-functions, message: message) + } +} + diff --git a/packages/preview/hydra/0.5.2/src/util/core.typ b/packages/preview/hydra/0.5.2/src/util/core.typ new file mode 100644 index 0000000000..ba9c94bfb0 --- /dev/null +++ b/packages/preview/hydra/0.5.2/src/util/core.typ @@ -0,0 +1,47 @@ +#import "@preview/oxifmt:0.2.0": strfmt as fmt + +/// Substitute `value` for the return value of `default()` if it is a sentinel value. +/// +/// - value (any): The value to check. +/// - default (function): The function to produce the default value with. +/// - check (any): The sentinel value to check for. +/// -> any +#let or-default(value, default, check: none) = if value == check { default() } else { value } + +/// An alias for `or-default` with `check: auto`. +#let auto-or = or-default.with(check: auto) + +/// An alias for `or-default` with `check: none`. +#let none-or = or-default.with(check: none) + +/// Returns the text direction for a given language, defaults to `ltr` for unknown languages. +/// +/// Source: #link("https://github.com/typst/typst/blob/9646a132a80d11b37649b82c419833003ac7f455/crates/typst/src/text/lang.rs#L50-57")[`lang.rs#L50-L57`] +/// +/// lang (str): The languge to get the text direction for. +/// -> direction +#let text-direction(lang) = if lang in ( + "ar", "dv", "fa", "he", "ks", "pa", "ps", "sd", "ug", "ur", "yi", +) { rtl } else { ltr } + +/// Returns the page binding for a text direction. +/// +/// Source: #link("https://github.com/typst/typst/blob/9646a132a80d11b37649b82c419833003ac7f455/crates/typst/src/layout/page.rs#L368-L373")[`page.rs#L368-L373`] +/// +/// dir (direction): The direction to get the page binding for. +/// -> alignement +#let page-binding(dir) = (ltr: left, rtl: right).at(repr(dir)) + +/// A list of queryable element functions. +#let queryable-functions = ( + bibliography, + cite, + figure, + footnote, + heading, + locate, + math.equation, + metadata, + ref, +) + diff --git a/packages/preview/hydra/0.5.2/typst.toml b/packages/preview/hydra/0.5.2/typst.toml new file mode 100644 index 0000000000..04bdc09f56 --- /dev/null +++ b/packages/preview/hydra/0.5.2/typst.toml @@ -0,0 +1,12 @@ +[package] +entrypoint = "src/lib.typ" +name = "hydra" +version = "0.5.2" +compiler = "0.11.0" +authors = ["tinger "] +repository = "https://github.com/tingerrr/hydra" +description = "Query and display headings in your documents and templates." +categories = ["components", "scripting"] +keywords = ["context", "chapter", "section", "heading"] +license = "MIT" +exclude = ["examples", "README.md"]