Endnotes are typically encountered in one of two forms. One use is to move citations out of the main flow and off the text pages. This prevents ugly pages with many footnotes in heavily documented texts, and in the past greatly simplified the typesetter’s job when in-production changes were made. The second use is to provide a place for incidental material to be set apart from the main flow, preserving the primary narrative flow.
In the first use, endnotes are typically one or two lines in length, and sometimes serve in the place of a formal bibliography. When a book is referenced more than once in the text, a new endnote provides the same information as the first associated endnote, or perhaps a shortened form, depending on house style. There is little need under this model to reference endnotes from any place other than the initial location.
In the second use, endnotes can be quite lengthy, sometimes exceeding a page. They are used to pursue topics related to that of the main flow, but that do not advance that flow. They may also contain information relevant to different parts of the main text flow, and the ability to easily reference them from multiple locations is desirable.
This second use is the target of this implementation, although it serves the first use as well, and in practice both often occur in the same document.
When there are many endnotes, it is useful to have headings on the endnote pages to help locate specific endnotes. These headings can reference either the endnote numbers found on the page (Notes 180 – 187) or the pages for which the notes apply (Notes for pages 123 – 125).
This test harness displays my endnotes implementation. It is usable, but lacks some functions that should be provided in any reasonable endnote implementation. There are also some features that may be considered desirable that I have not tried to implement. For these reasons, this should be seen as a stopgap until Typst provides a first-class endnote mechanism that includes the substantive features provided here and those identified as useful but missing.
Features that I have implemented
These bullets reflect features from the bluss implementation, although some are managed differently in this code.
- Links work between the endnote flag and the endnote index.
- By default, endnote numbering is consecutive from the first to the last.
- Numbering can use any Typst numbering regime, defaulting to “1” (ENNUMBERING).
- Endnote numbers can be reset at each level-1 heading via ENRESET in my code or by leaving the default
encnt(0)in the level-1 heading definition in the bluss code. - By default, endnotes are displayed in sections with the chapter named displayed before each chapter’s notes. This is via a level-2 heading in my code, or a bold text line in the bluss code. It can be suppressed via ENCHAPTER in my code, or removing one line in the bluss code.
- There are no restrictions on what can appear in an endnote (tested with figures, tables, endnotes, footnotes, and columns), except that, while endnotes can have endnotes, those second-level endnotes cannot themselves have endnotes.
These bullets reflect features not in the bluss implementation.
-
Endnotes can be given a tag (a Typst label) which displays the endnote number and links to the endnote.
-
This implementation allows one endnote to be added before the first chapter, for example, in an acknowledgements note on a title page.
-
By default, endnote pages have headings of the form Notes for page 99 and Notes for pages 120–123. This can be changed to Note 3 and Notes 7 – 12, or a combination of both, or suppressed to allow default document headings to be used (ENHEADERS).
-
Different numbering regimes can be specified for the flag in the text and the number preceding the note in the notes list. To have arabic numerals in the text and lower-case roman numerals in the notes, use:
ENNUMBERING = ("1", "i")
Missing features
The first of these is also present in the bluss implementation. The second is not applicable there.
-
Endnotes should not be attached to headings or captions if a ToC or list of figures outline is produced. When they are so attached, they appear in both the outline and the text flow. This is also a problem with footnotes, but there is a workaround for footnotes that does not work for endnotes.
-
The endnote reference tag must be created as an optional argument to the endnote macro, which is different from the way that labels are attached to footnotes. Compare:
#endnote(enref: "tag")[Note content.]
#footnote[Note content.]<tag>
Unattempted or untested features
- Chapter endnotes
- Chapter endnotes should both reset the endnote counters and remove the stored endnotes after placing them. Neither bluss’s implementation nor mine supports this.
- Chapter endnotes are incompatible with endnotes before the first chapter.
- Multilevel endnote numbering, as note 3.1 for the first endnote in chapter 3. I don’t think that this actually makes sense – how would you handle unnumbered chapters (Preface, …)? If you can restrict this to endnotes only in numbered chapters, perhaps a custom numbering function can be used for the display number.
- This is not currently a first-class reference implementation as is provided for so-called referenceable objects, but serves much the same purpose in a limited manner. I have not attempted to create a custom
kindat this point, but this is needed.
Credits
I have built this on snippets gathered from elsewhere in the Typst community.
- The basic outline for the endnotes using metadata, and not states or an array, is taken from work by bluss together with some ideas from Survari and sijo and from sijo.
- The ability to make notes referenceable was a major step forward, and is based on a note from nleanba.
- Changes from bluss:
- Added page header mechanism
- Add reference label ability
- Restrict link target to endnote number, not complete endnote
- Allow different numbering regime in text and notes (because it was easy, not because there is a real need)
There are other contributors as well to many of the layout features shown here. See the bibliography for links.
Recommendations
If you need a working endnote system and do not need page headings as I have implement them, or endnotes before the first chapter, start with what bluss wrote. If you need the page headings or references, start with my work. If you need an endnote before the first chapter, start with my work or adapt bluss’s.
Adding reference tags to bluss’s implementation, for example, requires changes to two lines of code and the addition of one more.
And now the code
The underlying code is from bluss with my additions for referencing and headings. I have renamed some of the variables to be more attuned to my practices and tried to separate out constants that might be language dependent.
I welcome suggestions for improvement.
////////////////////////////////////////////////////////////////////
// Test harness for endnote development
// based on work by bluss and nleanda, with help from many others
//
// There are no external dependencies, although the code is
// easily adaptable to the margialia and hydra packages.
////////////////////////////////////////////////////////////////////
//
// Endnote options, the last of each group is the default
//
// RESET at each level-1 heading
#let ENRESET = true // reset the displayed note numbers
#let ENRESET = false // default false, true with caution
//
// HEADERS in notes chapter
#let ENHEADERS = false // no special headers
#let ENHEADERS = "note"// Note 23 or Notes 36--52
#let ENHEADERS = "page"// Notes for page 23 or Notes for pages 36--52
#let ENHEADERS = "both"// Notes for page 23 Notes 36--52
//
// NUMBERING, any Typst #numbering regime, default "1"
// if array, first applies to text flag, last to notes display
#let ENNUMBERING = ("1","①") // example
#let ENNUMBERING = ("1","i") // example
#let ENNUMBERING = "1" // default
//
// CHAPTERS show chapter breaks as level 2 heads in notes
#let ENCHAPTERS = false // no chapter breaks as sections
#let ENCHAPTERS = true // display chapter breaks as sections
//
// Message defaults
// if you have notes before the first chapter, perhaps set this
#let ENHEAD = none
#let ENHEAD = [Endnotes, by note number]
//
// English literals, change as you like
#let ENPANICPAGE = "ERROR: note page numbers, no page numbering"
#let ENINTRO = [All endnotes for the document appear here.]
#let ENCHAPTERTITLE = [Notes]
#let ENSECTIONTITLE = [#smallcaps[Notes for]]
#let ENNOTE = [Note]
#let ENNOTES = [Notes]
#let ENNOTESFORPAGE = [Notes for page]
#let ENNOTESFORPAGES = [Notes for pages]
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
// utility functions
// llap protrudes its body into the left margin
#let llap(body) = {
show: box.with(width: 0pt)
show: align.with(right)
body
}
// endnotes are numbered, so use * † ‡ sequence for footnotes
// use std.footnote, footnote may be redefined for outline issues
#set std.footnote(numbering: "*")
// key contribution from nleanba, enables referencability
#show ref: it => {
if it.element != none and it.element.func() == metadata {
link(it.element.location())[#it.element.value]
} else {
it
}
}
#let encnt = counter("endnote")
#let encnt-serial = counter("endnote-serial")
#let endnote(body, enref: none) = {
encnt.step()
encnt-serial.step()
context {
let enpagenum = str(here().page()) // from beginning of doc
let enpagefmt = counter(page).display()
let enserial = str(encnt-serial.get().first())
let endflag = std.numbering(ENNUMBERING.first(), ..encnt.get())
let endisplay = std.numbering(ENNUMBERING.last(), ..encnt.get())
let labelname = "_endnote:serial:" + enserial
let link = x => x
if query(label(labelname)).len() > 0 {
link = std.link.with(label(labelname))
}
// style endnote flag here
link[#super(endflag)]
[#metadata((endisplay: endisplay, // displayed endnote number
enserial: enserial, // sequential number
enpagefmt: enpagefmt, // origin page formatted
enpagenum: enpagenum, // origin page integer
enref: enref, // optional reference string
labelname: labelname, // unique label string
content: body, // text of the endnote
))<_endnote>]
}
}
#let split-array-by(arr, func) = {
let chunks = ()
let chunk = ()
for elt in arr {
if func(elt) {
if chunk != () { chunks.push(chunk) }
chunk = ()
}
chunk.push(elt)
}
if chunk != () { chunks.push(chunk) }
chunks
}
#let showendnote(note) = {
let link = link.with(note.location())
let item = note.value
let data = (item.enserial,item.endisplay,
item.enpagenum,item.enpagefmt)
if item.enref != none {
[#metadata([#item.endisplay])#label(item.enref)]
}
[#metadata((data))<en-start>]
// style endnote number here
llap[#link[#str(item.endisplay)#sym.space.en]#label(item.labelname)]
h(0pt, weak: true) // allows newline after [
item.content
[#metadata((data))<en-end>]
}
#let show-heading(head) = {
let num = none
if head.numbering != none {
num = " " + std.numbering(head.numbering,
..counter(heading).at(head.location())) + ":"
}
heading(level: 2)[#ENSECTIONTITLE#num #head.body]
//heading(level: 2)[#smallcaps[Notes for]#num #head.body]
}
//===============================================
#let make-notes(
enintro: ENINTRO,
// use enhead to introduce notes before the first chapter
enhead: ENHEAD,
) = {
//remove this test if you have numbering where notes are
// generated but not for the notes display
context [#if page.numbering == none and (
ENHEADERS == "page" or ENHEADERS == "both") {
panic(ENPANICPAGE)
return none
}]
let placenoteheads(hda, hdb) = {
if calc.odd(here().page()) {
emph(hdb);h(1fr);emph(hda)
} else {
emph(hda);h(1fr);emph(hdb)
}
}
let format-header(a, b) = {
// a and b are (enserial, endisplay, enpagefmt, enpagenum)
// a and b are arrays of strings with integer values
// serial numbers for compare, formatted for display
let fn = a.at(0); let ffn = a.at(1) // first note
let fp = a.at(2); let ffp = a.at(3) // first page
let ln = b.at(0); let fln = b.at(1) // last note
let lp = b.at(2); let flp = b.at(3) // last page
if ENHEADERS == "page" {
if fp == lp {
placenoteheads([#ENNOTESFORPAGE #ffp], [])
} else {
placenoteheads([#ENNOTESFORPAGES #ffp -- #flp], [])
}
} else {
if ENHEADERS == "note" {
if fn == ln {
placenoteheads([#ENNOTE #ffn], [])
} else {
placenoteheads([#ENNOTES #ffn -- #fln], [])
}
} else {
if ENHEADERS == "both" {
if fp == lp {
if fn == ln {
placenoteheads([#ENNOTESFORPAGE #ffp],
[#ENNOTE #ffn])
} else {
placenoteheads([#ENNOTESFORPAGE #ffp],
[#ENNOTES #ffn -- #fln])
}
} else {
if fn == ln {
placenoteheads([#ENNOTESFORPAGES #ffp -- #flp],
[#ENNOTE #ffn])
} else {
placenoteheads([#ENNOTESFORPAGES #ffp -- #flp],
[#ENNOTES #ffn -- #fln])
}
}
}
}
}
}
set page(
header: context {
let is-start-chapter() = query(
heading.where(level: 1).after(here()))
.map(h => h.location().page())
.at(0, default: 0) == here().page()
if is-start-chapter() {
return
}
let notenums = query(selector.or(<en-start>, <en-end>))
.filter(x => x.location().page() == here().page())
.map(x => x.value)
.dedup()
if notenums.len() > 0 {
return format-header(notenums.at(0),
notenums.at(-1, default: none))
}
// No paragraph starting or ending on this page
// -> check if a paragraph starts before and ends after this page
let prevs = query(selector(<en-start>).before(here()))
let nexts = query(selector(<en-end>).after(here()))
if prevs.len() == 0 or nexts.len() == 0 {
return none
}
let prev = prevs.last().value
let next = nexts.first().value
if prev != next { // should not happen perhaps assert here
return none
}
return format-header(prev, next)
}
) if ENHEADERS != false//
//===============================================
//
heading(level: 1)[#ENCHAPTERTITLE]
if enintro != none {
enintro;parbreak()
}
context {
if enhead != none {
heading(level: 2, [#enhead])
}
let headings-and-endnotes = query(
selector.or(heading.where(level: 1), <_endnote>))
// next test allows one endnote before the first chapter
if headings-and-endnotes.at(0).func() == metadata {
showendnote(headings-and-endnotes.at(0))
let _ = headings-and-endnotes.slice(1)
}
let chunks = split-array-by(headings-and-endnotes,
elt => elt.func() == heading)
for chunk in chunks {
if chunk.len() <= 1 { continue }
if ENCHAPTERS { show-heading(chunk.at(0)) }
let notes = chunk.slice(1)
set par(first-line-indent: 0em, spacing: 1em)
notes.map(note => {showendnote(note)})
.join(parbreak())
}
}
}
//
// End endnote setup
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
// Begin layout preamble
//
#show heading.where(level: 1): it => {
if ENRESET { encnt.update(0) }
it
}
#set page(numbering: "1")
#set heading(numbering: "1.1")
This precedes chapter 1 and has an endnote.#endnote[Before chapter 1.]
= A chapter
#lorem(10)#endnote[
Endnotes can be have columnar material:
#linebreak()
#columns(2, gutter: 8pt)[#lorem(20)#colbreak()#lorem(25)
]
]
#lorem(10)#endnote(enref: "en:123")[Here is a endnote with a figure:
#figure(
table(
columns: 4,
[t], [1], [2], [3],
[y], [0.3s], [0.4s], [0.8s],
),
caption: [Timing results],
)
]
#lorem(10)#endnote[And an endnote with an endnote:#endnote[But why?]]
// #lorem(10)#endnote[And an endnote with an endnote with an endnote:#endnote[But why?#endnote[Fails]]]
#lorem(10)#endnote[
And an endnote with an footnote:#footnote[Buy what?]]
#lorem(10)#endnote[
As you can see, endnotes can be pretty long.
#lorem(500)
#lorem(600)
]
#lorem(200)
= Second chapter
This chapter has no endnotes.
= Third chapter
#lorem(200)#endnote[Also see the discussion at note
@en:123.] #lorem(10)
#lorem(10)
#lorem(200)#endnote[And another #lorem(200)]
#lorem(10)#endnote[And another #lorem(200)]
#lorem(10)#endnote[And another #lorem(100)]
#lorem(10)#endnote[And another #lorem(100)]
#lorem(200)#endnote[And another #lorem(100)]
#lorem(10)#endnote[And another #lorem(100)]
#lorem(10)#endnote[And another #lorem(100)]
#lorem(10)#endnote[And another #lorem(100)]
#lorem(10)#endnote[And another #lorem(100)]
#lorem(10)#endnote[And another #lorem(30)]
#make-notes()