For a personal project where i’m trying to create a sorted outline by alphabetical order i found myself blocked due to my understanding of state that i consider has a global variable.
The current implementation of this ordered outline is :
#let alphabet-displayed = state("alphabet-displayed", ())
#let ordered-outline() = context {
let current-page = here().page()
let headings = query(selector(heading.where(level : 2)))
let sorted-headings = headings.sorted(
key: heading => {if heading.body.has("text") {heading.body.text} else {""}}
)
sorted-headings.map(entry => {
let first-letter = entry.body.text.first()
// reimplement default outline.entry
if entry.numbering != none {
numbering(entry.numbering, ..counter(heading).at(entry.location()))
}
[ ]
/* Focus Here */
if (first-letter not in alphabet-displayed.get()){
text(first-letter) + linebreak()
alphabet-displayed.update(current => current + (first-letter,))
}
h(4pt) + entry.body
box(width: 1fr, repeat[.])
[#entry.location().page()]
}).join([ \ ])
}
== Aaaa
some song
== Abaaa
some song
== B
some song
== C
some song
== D
some song
#pagebreak()
= Index
#ordered-outline()
I want the letter of each “sub section” of the outline to be shown only one. How can i achieve this result ? In the example give, “A” should only be noted once even thought multiple song begin with A
Thanks in advance for your help.
Edits
EDIT 1 : Reformat code & image for following @Andrew comment.
/* Focus Here */
#context if (first-letter not in alphabet-displayed.get()){
text(first-letter) + linebreak()
alphabet-displayed.update(current => current + (first-letter,))
}
Result
Whole code
#set page(
width:5cm,
height:auto,
margin:10pt,
)
#let alphabet-displayed = state("alphabet-displayed", ())
#let writing = state("writing", true)
#let ordered-outline() = context {
let current-page = here().page()
let headings = query(selector(heading.where(level : 2)))
let sorted-headings = headings.sorted(
key: heading => {if heading.body.has("text") {heading.body.text} else {""}}
)
sorted-headings.map(entry => {
let first-letter = entry.body.text.first()
// reimplement default outline.entry
if entry.numbering != none {
numbering(entry.numbering, ..counter(heading).at(entry.location()))
}
[ ]
/* Focus Here */
context if (first-letter not in alphabet-displayed.get()){
text(first-letter) + linebreak()
alphabet-displayed.update(current => current + (first-letter,))
}
h(4pt) + entry.body
box(width: 1fr, repeat[.])
[#entry.location().page()]
}).join([ \ ])
}
== Aaaa
some song
== Abaaa
some song
== B
some song
== C
some song
== D
== Diary life
== #sym.suit.heart
some song
#pagebreak()
= Index
#ordered-outline()
In a context environment, if you want to use an updated state.value , you have to insert another context by nested or parallel context.
This is explained in the context reference, which provides a very good overview. However, in this case, the fix actually uses a parallel context rather than a nested one, so the official explanation might not be entirely sufficient on its own. That’s why I wanted to add my own example to illustrate both patterns.
Another example
#let mycounter = counter("mycounter")
#context[
#mycounter.step()
Got #mycounter.display() after calling step().
// step() inside a context does not take effect immediately.
#context[
// Previous step() took effect here due to nesting.
Got #mycounter.display() by nesting a context.
// Now insert a counter (i.e., state) update.
#mycounter.step()
Got #mycounter.display(), which has not changed.
// Note: step() does not update the display number in the same context.
#context[
// 1. Nesting context
// Second step() took effect here due to nesting.
Got #mycounter.display() by nesting again.
]
]
#context[
// 2. Parallelizing context
// Second step() took effect here due to parallelizing.
Got #mycounter.display() by using a parallel context.
]
]
If you want to do an outline properly, that mimics the default one, then you need to
use blocks,
the outline heading should not be included in the PDF Document Outline, nor have numbering,
the heading level is 2 for no reason, unless there is a context missing,
and the is link missing to each heading.
Though, the indent thingy kind of goes away, since you make a custom one-time indent (I’m not sure if grid is used in the new outline). I feel the most natural way is to group headings by the first letter and then work on the dictionary and then on individual headings. You also have some stuff that is never used.
#import "@preview/t4t:0.4.3": get
#let ordered-outline(title: "Index") = context {
let alphabet-displayed = state("alphabet-displayed", ())
let headings = query(heading.where(level: 1))
let index = headings.position(it => get.text(it) == get.text(title))
if index != none { _ = headings.remove(index) }
let sorted-headings = headings.sorted(key: get.text)
let buckets = (:)
for heading in sorted-headings {
let letter = get.text(heading).clusters().first()
buckets.insert(letter, buckets.at(letter, default: ()) + (heading,))
}
heading(outlined: false, numbering: none, title)
buckets
.pairs()
.map(((letter, headings)) => {
let fill = box(width: 1fr, repeat[.])
let headings = for entry in headings {
let numbering = if entry.numbering != none {
numbering(entry.numbering, ..counter(heading).at(entry.location()))
h(0.3em)
}
let page = entry.location().page()
let body = pad(left: 0.7em, block[#numbering#entry.body #fill #page])
link(entry.location(), body)
}
block(letter, spacing: 0.65em)
block(spacing: 0pt, layout(size => {
let extend = 0.4em
place(dx: 0.3em, dy: -extend, line(
length: measure(headings).height + extend,
angle: 90deg,
stroke: purple,
))
}))
headings
})
.join()
}
#set heading(numbering: "1.")
#ordered-outline()
= #text(red)[Aaaa]
= Aaaa
some song
= Cheading
some song
= Bheading
#pagebreak()
some song
= Dheading
some song
= Aaaaa
some song