-
Notifications
You must be signed in to change notification settings - Fork 415
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
796 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 - 2025 tinger <[email protected]> | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
Oops, something went wrong.