Skip to content

Commit ad69f70

Browse files
committed
initial commit
0 parents  commit ad69f70

File tree

8 files changed

+217
-0
lines changed

8 files changed

+217
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 tinger <me@tinger.dev>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# hydra
2+
Hydra is a [typst] package allowing you to easily display the current section anywhere in your
3+
document. By default, it will assume that it is used in the header of your document and display
4+
the last heading if and only if it is numbered and the next heading is not the first on the current
5+
page.
6+
7+
By default hdyra assumes that you use `a4` page size, see the FAQ if you use different page size or
8+
margins.
9+
10+
## Example
11+
```typst
12+
#import "@preview/hydra:0.0.1": hydra
13+
#set page(header: hydra())
14+
#set heading(numbering: "1.1")
15+
#show heading.where(level: 1): it => pagebreak(weak: true) + it
16+
17+
= Introduction
18+
#lorem(750)
19+
20+
= Content
21+
== First Section
22+
#lorem(500)
23+
== Second Section
24+
#lorem(250)
25+
== Third Section
26+
#lorem(500)
27+
28+
= Annex
29+
#lorem(10)
30+
```
31+
![example1][example1]
32+
![example2][example2]
33+
34+
## Non-default behavior
35+
### Configuring filter and display
36+
By default it will hydra will display `[#numbering #body]` of the heading and this reject unnumbered
37+
ones. This filtering can be configured using `prev-filter` and `next-filter`, if those are changed
38+
to include unnumbered headings `display` be changed too.
39+
```typst
40+
#set page(header: hydra(prev-filter: _ => true, display: h => h.body))
41+
```
42+
43+
Keep in mind that `next-filter` is also responsible for checking that the next heading is on the
44+
current page.
45+
46+
### In the footer
47+
To use the hydra functon in the footer of your doc, pass `is-footer: true` and place a
48+
`#metadata(()) <hydra>` somewhere in your header.
49+
50+
```typst
51+
#set page(header: [#metadata(()) <hydra>], footer: hydra(is-footer: true))
52+
```
53+
54+
Using it outside of footer or header should work as expected.
55+
56+
### Different heading levels or custom heading types
57+
If you use a `figure`-based element for special 0-level chapters or you wish to only consider
58+
specific levels of headings, pass the appropriate selector.
59+
60+
```typst
61+
// only consider level 1
62+
#set page(header: hydra(sel: heading.where(level: 1)))
63+
64+
// only consider level 1 - 3
65+
#set page(header: hydra(sel: (heading, h => h.level <= 3)))
66+
67+
// consider also figures with this kind, must likely override all default functions other than
68+
// resolve, or resolve directly, see source
69+
#set page(header: hydra(sel: figure.where(kind: "chapter").or(heading), display: ...)
70+
```
71+
72+
In short, `sel` can be a selector, or a selector and a filter function. When using anything other
73+
than headings only, consider setting `display` too.
74+
75+
## FAQ
76+
**Q:** Why does hydra display the previous heading if there is a heading at the top of my page?
77+
78+
**A:** If you use non `a4` page margins make sure to pass
79+
`next-filter: default.next-filter.with(top-margin: ...)`. This margin must be known for the default
80+
implementation. If it does but you are using `a4`, then you found a bug.
81+
82+
[example1]: example1.png
83+
[example2]: example2.png
84+
[typst]: https://github.com/typst/typst

example1.png

22.5 KB
Loading

example2.png

33.3 KB
Loading

src/default.typ

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// get the adjacent headings
2+
#let get-adjacent(is-footer: false, sel, loc) = {
3+
import "/src/util.typ": into-sel-filter-pair
4+
let (sel, filter) = into-sel-filter-pair(sel)
5+
6+
if is-footer {
7+
loc = query(selector(<hydra>).before(loc), loc).last().location()
8+
}
9+
10+
let prev = query(sel.before(loc), loc).filter(filter)
11+
let next = query(sel.after(loc), loc).filter(filter)
12+
13+
let prev = if prev != () { prev.last() }
14+
let next = if next != () { next.first() }
15+
16+
(prev, next)
17+
}
18+
19+
// check if the previous heading is numbered
20+
#let prev-filter(element, loc) = {
21+
import "/src/util.typ": assert-element
22+
assert-element(element, heading)
23+
element.numbering != none
24+
}
25+
26+
// check if the next heading is on the curent page
27+
#let next-filter(top-margin: 2.5cm, element, loc) = {
28+
import "/src/util.typ": assert-element
29+
assert-element(element, heading)
30+
(element.numbering != none
31+
and element.location().page() == loc.page()
32+
and element.location().position().y <= top-margin)
33+
}
34+
35+
// display the heading as closely as it occured at the given loc
36+
#let display(element, loc) = {
37+
import "/src/util.typ": assert-element
38+
assert-element(element, heading)
39+
numbering(element.numbering, ..counter(heading).at(element.location()))
40+
[ ]
41+
element.body
42+
}
43+
44+
#let resolve(
45+
sel: heading,
46+
getter: get-adjacent,
47+
prev-filter: prev-filter,
48+
next-filter: next-filter,
49+
display: display,
50+
is-footer: false,
51+
loc,
52+
) = {
53+
let (last, next) = getter(sel, is-footer: is-footer, loc)
54+
55+
let last-eligible = last != none and prev-filter(last, loc)
56+
let next-eligible = next != none and next-filter(next, loc)
57+
58+
if last-eligible and not next-eligible {
59+
display(last, loc)
60+
}
61+
}

src/lib.typ

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#import "/src/default.typ"
2+
3+
#let hydra(
4+
sel: heading,
5+
getter: default.get-adjacent,
6+
prev-filter: default.prev-filter,
7+
next-filter: default.next-filter,
8+
display: default.display,
9+
resolve: default.resolve,
10+
is-footer: false,
11+
) = locate(loc => resolve(
12+
sel: sel,
13+
getter: getter,
14+
prev-filter: prev-filter,
15+
next-filter: next-filter,
16+
display: display,
17+
is-footer: is-footer,
18+
loc
19+
))

src/util.typ

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// assert the element is of the given typ
2+
#let assert-element(element, func) = {
3+
assert.eq(type(element), content, message: "element must be content, was " + repr(type(element)))
4+
assert.eq(element.func(), func, message: "element must be a " + repr(func))
5+
}
6+
7+
// split a selector into a pair of selector and post filter
8+
#let into-sel-filter-pair(sel) = {
9+
if type(sel) in (selector, function) {
10+
(selector(sel), _ => true)
11+
} else if (
12+
type(sel) == array and sel.len() == 2
13+
and type(sel.at(0)) in (selector, function) and type(sel.at(1)) == function
14+
) {
15+
let (sel, func) = sel
16+
(selector(sel), func)
17+
} else {
18+
panic("sel must be a selector or a selector and filter function")
19+
}
20+
}
21+

typst.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
entrypoint = "src/lib.typ"
3+
name = "hydra"
4+
version = "0.0.1"
5+
compiler = "0.8.0"
6+
authors = ["tinger <me@tinger.dev>"]
7+
repository = "https://github.com/tingerrr/hydra"
8+
description = "Query and display headings of the currently active section"
9+
keywords = ["heading", "introspection"]
10+
license = "MIT"
11+
exclude = ["example1.png", "example2.png", "LICENSE", "README.md"]

0 commit comments

Comments
 (0)