I read it. Makes more sense now. Here are my notes and updated code. I don’t know how much further it is possible to shrink the new code. I guess it 100% depends on your use case. The less variables, the less code is needed.
// Stuff from https://typst.app/universe/package/marginalia
#let fill_config(..config) = {
let config = config.named()
// default margins a4 are 2.5 cm
let inner = config.at("inner", default: (far: 5mm, width: 15mm, sep: 5mm))
let outer = config.at("outer", default: (far: 5mm, width: 15mm, sep: 5mm))
return (
inner: (
far: inner.at("far", default: 5mm),
width: inner.at("width", default: 15mm),
sep: inner.at("sep", default: 5mm),
),
outer: (
far: outer.at("far", default: 5mm),
width: outer.at("width", default: 15mm),
sep: outer.at("sep", default: 5mm),
),
top: config.at("top", default: 2.5cm),
bottom: config.at("bottom", default: 2.5cm),
clearance: config.at("clearance", default: 12pt),
flush-numbers: config.at("flush-numbers", default: false),
)
}
#let _config = state("_config", fill_config())
/// #internal[Mostly internal.]
/// The counter used for the note icons.
///
/// If you use @note-numbering without @note-numbering.repeat, it is reccommended you reset this occasionally, e.g. per heading or per page.
/// #example(scale-preview: 100%, ```typc notecounter.update(1)```)
/// -> counter
#let notecounter = counter("notecounter")
#let page-setup() = {
let config = fill_config()
(
margin: (
left: config.inner.far + config.inner.width + config.inner.sep,
right: config.outer.far + config.outer.width + config.outer.sep,
top: config.top,
bottom: config.bottom,
),
)
}
/// #internal[Mostly internal.]
/// Calculates positions for notes.
///
/// Return type is of the form `(<index/id>: offset)`
/// -> dictionary
#let _calculate-offsets(
/// Of the form
/// ```typc
/// (
/// height: length, // total page height
/// top: length, // top margin
/// bottom: length, // bottom margin
/// )
/// ```
/// -> dictionary
page,
/// Of the form `(<index/id>: item)` where items have the form
/// ```typc
/// (
/// natural: length, // initial vertical position of item, relative to page
/// height: length, // vertical space needed for item
/// clearance: length, // vertical padding required.
/// // may be collapsed at top & bottom of page, and above separators
/// shift: boolean | "ignore" | "avoid", // whether the item may be moved about. `auto` = move only if neccessary
/// keep-order: boolean, // if false, may be reordered. if true, order relative to other `false` items is kept
/// )
/// ```
/// -> dictionary
items,
/// -> length
clearance,
) = {
// sorting
let ignore = ()
let reoderable = ()
let nonreoderable = ()
for (key, item) in items.pairs() {
if item.shift == "ignore" {
ignore.push(key)
} else if item.keep-order == false {
reoderable.push((key, item.natural))
} else {
nonreoderable.push((key, item.natural))
}
}
reoderable = reoderable.sorted(key: ((_, pos)) => pos)
let positions = ()
let index_r = 0
let index_n = 0
while index_r < reoderable.len() and index_n < nonreoderable.len() {
if reoderable.at(index_r).at(1) <= nonreoderable.at(index_n).at(1) {
positions.push(reoderable.at(index_r))
index_r += 1
} else {
positions.push(nonreoderable.at(index_n))
index_n += 1
}
}
while index_n < nonreoderable.len() {
positions.push(nonreoderable.at(index_n))
index_n += 1
}
while index_r < reoderable.len() {
positions.push(reoderable.at(index_r))
index_r += 1
}
// shift down
let cur = page.top
let empty = 0pt
let prev-shift-avoid = false
let positions_d = ()
for (key, position) in positions {
let fault = cur - position
if cur <= position {
positions_d.push((key, position))
if items.at(key).shift == false {
empty = 0pt
} else {
empty += position - cur
}
cur = position + items.at(key).height + clearance
} else if items.at(key).shift == "avoid" {
if fault <= empty {
if prev-shift-avoid {
positions_d.push((key, cur))
cur = cur + items.at(key).height + clearance
} else {
// can stay
positions_d.push((key, position))
empty -= fault // ?
cur = position + items.at(key).height + clearance
}
} else {
positions_d.push((key, position + fault - empty))
cur = position + fault - empty + items.at(key).height + clearance
empty = 0pt
}
} else if items.at(key).shift == false {
// check if we can swap with previous
if (
positions_d.len() > 0
and fault > empty
and items.at(positions_d.last().at(0)).shift != false
and (
(not items.at(key).keep-order)
or (not items.at(positions_d.last().at(0)).keep-order)
)
) {
let (prev, _) = positions_d.pop()
let x = cur
positions_d.push((key, position))
empty = 0pt
cur = calc.max(position + items.at(key).height + clearance, cur)
positions_d.push((prev, cur))
cur = cur + items.at(prev).height + clearance
} else {
positions_d.push((key, position))
empty = 0pt
cur = calc.max(position + items.at(key).height + clearance, cur)
}
} else {
positions_d.push((key, cur))
empty += 0pt
// empty = 0pt
cur = cur + items.at(key).height + clearance
}
prev-shift-avoid = items.at(key).shift == "avoid"
}
let max = page.height - page.bottom
let positions = ()
for (key, position) in positions_d.rev() {
if max > position + items.at(key).height {
positions.push((key, position))
max = position - clearance
} else if items.at(key).shift == false {
positions.push((key, position))
max = calc.min(position - clearance, max)
} else {
positions.push((key, max - items.at(key).height))
max = max - items.at(key).height - clearance
}
}
let result = (:)
for (key, position) in positions.rev() {
result.insert(key, position - items.at(key).natural)
}
for key in ignore {
result.insert(key, 0pt)
}
result
}
#let get-right() = _config.get().outer
#let get-left() = _config.get().inner
#let _note_extends_right = state("_note_extends_right", ("1": ()))
#let note-offset-right(page_num) = {
let page = (
height: page.height,
bottom: _config.get().bottom,
top: _config.get().top,
)
let items = _note_extends_right
.final()
.at(page_num, default: ())
.enumerate()
.map(((key, item)) => (str(key), item))
.to-dict()
_calculate-offsets(page, items, _config.get().clearance)
}
// absolute right
/// #internal()
#let note-right(dy: 0pt, body) = context {
let dy = dy.to-absolute()
let anchor = here().position()
let pagewidth = page.width
let page = here().page()
let width = get-right().width
let notebox = box(width: width, body)
let height = measure(notebox).height
let natural_position = anchor.y + dy
let current = _note_extends_right.get().at(str(page), default: ())
let index = current.len()
_note_extends_right.update(old => {
let oldpage = old.at(str(page), default: ())
oldpage.push((
natural: natural_position,
height: height,
shift: "avoid",
keep-order: false,
))
old.insert(str(page), oldpage)
old
})
let vadjust = (
dy + note-offset-right(str(page)).at(str(index), default: 0pt)
)
let hadjust = pagewidth - anchor.x - get-right().far - get-right().width
place(dx: hadjust, dy: vadjust, notebox)
}
/// Create a marginnote.
#let note(dy: 0pt, body) = context {
let text-style = arguments(0.8em, red, weight: "semibold")
let lineheight = measure(text(..text-style, sym.zws)).height
let dy = dy - lineheight
let body = align(top, block(width: 100%, {
set text(..text-style)
set par(spacing: 1.2em, leading: 0.5em, hanging-indent: 0pt)
body
}))
note-right(dy: dy, body)
}
////////////////////////////////////////////////////////////////////////////////
#set page(..page-setup(), numbering: "1")
#set par(first-line-indent: 1em, justify: true)
#set text(number-type: "old-style", hyphenate: true, costs: (hyphenation: 10%))
#set raw(lang: "typ")
// Convenience variables
#let QGray = luma(175) // for text decorations, table rules
#let QInset = 2em // inset of Quote environment
// https://github.com/typst/typst/issues/3640
// Unable to style table.header and table.footer
#show table.cell.where(y: 0): strong
#set table.hline(stroke: QGray)
// Guidelines (optional, for testing)
#set page(background: context {
// based on marginalia source
let leftm = get-left()
let rightm = get-right()
let thick = 0.5pt
set line(length: 100%)
let rect(width, stroke) = std.rect(width: width, height: 100%, stroke: stroke)
place(
top,
dy: _config.get().top,
line(stroke: thick + luma(80%))
)
place(
bottom,
dy: -_config.get().bottom,
line(stroke: thick + luma(80%))
)
place(
dx: leftm.far,
rect(leftm.width, (x: thick + luma(70%)))
)
place(
dx: leftm.far + leftm.width + leftm.sep,
rect(10pt, (left: thick + luma(85%)))
)
place(
right,
dx: -rightm.far,
rect(rightm.width, (x: thick + luma(70%)))
)
place(
right,
dx: -rightm.far - rightm.width - rightm.sep,
rect(10pt, (right: thick + luma(85%)))
)
place(
// quotation margins
dx: leftm.far + leftm.width + leftm.sep + QInset,
rect(21cm - 5cm - (2 * QInset), thick + luma(90%)),
)
})
//
// QUOTE NUMBERING
//
#let qi = counter("qi")
#let Qm = note.with(dy: 0pt)
#let QI(label) = qi.step() + Qm(context qi.display() + [#metadata("qi")#label])
// borrowing heavily from
// https://forum.typst.app/t/how-can-i-create-referrable-labels-for-custom-multi-level-counters/3511
#show ref: it => {
if it.form == "page" { return it }
let supplement = if it.supplement != auto [#it.supplement ]
let target = query(it.target).first()
if (
type(target) != content or target.func() != metadata or target.value != "qi"
) { return it }
let count = numbering("1.1", ..qi.at(locate(it.target)))
link(it.target)[#supplement#count]
}
//
// QUOTE NUMBER USE
//
// Quote Page (upper-case P, lower-case p)
// https://github.com/typst/typst/issues/2873#issuecomment-2138261314
#let QP(label) = ref(label, form: "page", supplement: "Page")
#let Qp(label) = ref(label, form: "page", supplement: "page")
// Quote Number (upper-case Q, lower-case q)
#let QN(label) = ref(label, supplement: "Quotation")
#let Qn(label) = ref(label, supplement: "quotation")
// Quote on page (Upper and lower)
#let Qo(label) = Qn(label) + " on " + Qp(label)
#let QO(label) = QN(label) + " on " + QP(label)
#show quote: set block(above: 0.5em, below: 0.5em, inset: 0.5em)
// could use #" " instead of #sym.space, and ~ instead of sym.space.nobreak
#let QDecorate(open, close, body, lang) = context {
let QScale = 1.1818 // about 13pt on default 11pt
let llap(Text, Size, Color, Lower) = {
// place(dx: -0.6em, dy: -0.1em, text(size: Size, fill: Color)[#Text])
show: box.with(width: 0pt)
show: align.with(right)
text(baseline: Lower, size: Size, fill: Color)[#Text]
}
let rlap(Text, Size, Color, Lower) = {
show: box.with(width: 0pt)
show: align.with(left)
text(baseline: Lower, size: Size, fill: Color)[#Text]
}
let adjust = (text.size * QScale - text.size) / 4
set text(lang: lang) if lang != none
set par(first-line-indent: 0pt)
llap([#open#sym.space], QScale * text.size, QGray, adjust)
body
rlap([#sym.wj#sym.space.nobreak#close], QScale * text.size, QGray, adjust)
}
#let Qrestate(lang, restatement) = QDecorate("[", "]", restatement, lang)
#let Qtranslate(translation) = QDecorate("(", ")", translation, none)
#let Qattrib(attribution) = QDecorate("–", none, attribution, none)
#let Quote(
label: none,
attribution: none,
restatements: (),
translations: (),
lang: none, // for body and restatements
font: none, // for body
body,
) = {
if type(restatements) != array { restatements = (restatements,) }
restatements = restatements.map(x => if type(x) != array { (lang, x) } else {
x
})
if type(translations) != array { translations = (translations,) }
show: block.with(spacing: 0em, inset: QInset)
if label != none { QI(label) }
set text(lang: lang) if lang != none
{
set text(font: font) if font != none
body
}
for (lang, restatement) in restatements { Qrestate(lang, restatement) }
for translation in translations { Qtranslate(translation) }
if attribution != none { Qattrib(attribution) }
}
= A custom `#quote()` function
I am trying to define a quotation function that expands the default block variant of the `#quote()` function to support, in addition to the existing attribution, the following:
- a referenceable quotation index (counter) that is placed in the right margin
- one or more restatements (transliterations or translations from the original non-default language to a different non-default language)
- one or more translations
- hyphenation according to the respective language of each text
- the text of restatements and translations should be decorated by enclosure in enlarged and spaced brackets---square brackets for the restatements and parentheses for the translation
- the brackets, and the en-dash preceding the attribution, should be hung in their respective margins to allow the text of the quotation, restatements, translations, and attribution to align
With some minor issues, all of this is now working.
== Basic, using `#quote()`
This is the basic quotation using the standard `#quote()` function:
#quote(
block: true,
attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek],
)[ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.]
This is the basic quotation using the standard `#quote()` function together with my `#QI()` function. It fails because the `#QI()` is forcing a paragraph break:
#QI(<NOGOOD>)#quote(
block: true,
attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek],
)[ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.]
== With no restatements
Here is an example with no restatements or translations:
#Quote(
label: <NONE>,
attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek, followed by Burton's English translation in _The Anatomy of Melancholy_ (1628)],
// translations: [It is a greater offence to ſteale dead mens labours, than their clothes],
lang: "el",
"ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.",
)
// #pagebreak()
== With one restatement or translation
With a single restatement and single translation:
#Quote(
label: <ONE>,
attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek, followed by a Latin restatement used by Robert Burton in _The Anatomy of Melancholy_ (1628) and matching the Latin by Thomas Naogeorgus (1559), followed by Burton's English translation],
restatements: (
("la", [Magis impium Mortuorum Lucubrationes quam veſtes furari.]),
),
translations: [It is a greater offence to ſteale dead mens labours, than their clothes],
lang: "el",
)[ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.]
With a single translation and no attribution:
#Quote(
label: <TWO>,
translations: [It is a greater offence to ſteale dead mens labours, than their clothes],
font: "GFS Neohellenic",
lang: "el",
)[ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.]
== With a restatement and two translations
Another favorite, showing another use for restatements:
#Quote(
label: <TIME>,
attribution: [Arthur Schopenhauer in his 1851 _Parerga und Paralipomena_ (said to be from 2:§296a in Chapter 24, but demonstrably not) with translations by Eric F. J. Payne in his 1974 _Parerga and Paralipomena_ (2000 reprint, 2:559), and from the 1980 _Columbia Dictionary of Quotations_ at 102b, which mistates the chapter and does not name a translator],
restatements: (
[Es wäre gut Bücher kaufen, wenn man die Zeit, sie zu lesen, mitkaufen könnte, aber man verwechselt meistens den Ankauf der Bücher mit dem Aneignen ihres Inhalts.]
),
translations: (
[To buy books would be a good thing if we could also buy the time to read them; but the purchase of books is often mistaken for the assimilation and mastering of their contents.],
[Buying books would be a good thing if one could also buy the time to read them in: but as a rule the purchase of books is mistaken for the appropriation of their contents.],
),
lang: "deo",
font: "UnifrakturMaguntia", // good free Fraktur, very close to that used by Schopenhauer's publisher
)[Es wäre gut Bücher kaufen, wenn man die Zeit, ſie zu leſen, mitkaufen könnte, aber man verwechſelt meiſtens den Ankauf der Bücher mit dem Aneignen ihres Inhalts.]
// #pagebreak()
== With three restatements and a translation
And with an array of restatements, all seems to work as expected:
#Quote(
label: <THREE>,
attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek, followed by Latin restatements (1) as used by Robert Burton in _The Anatomy of Melancholy_ (1628) and matching the Latin by Thomas Naogeorgus (1559), (2) published by Claudius Morellus in a 1605 collection of Synesius's letters, and (3) by Rudolf Hercher in his 1873 collection of the letters, followed by Burton's English translation],
restatements: (
(
"xxx",
[hēgoûmai de asebesteron apothanóntōn lógous kléptein ē thoimátia, ho kaleîtai tumbōrukheîn],
),
("la", [Magis impium Mortuorum Lucubrationes quam veſtes furari.]),
(
"la",
[arbitror magis impiú mortuorum ſcript furari, quam veſtes, quod appelatur buſta effodere.],
),
(
"la",
[Magis autem impium esse arbitror mortuorum lucubrationes quam vestes furari, quod sepulcra perfodere dicitur.],
),
),
translations: [It is a greater offence to ſteale dead mens labours, than their clothes],
lang: "el",
font: "New Computer Modern",
)[ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.
]
== Non-block use of my quote index (`#QI()`)
#set par(first-line-indent: 0em, justify: true)
// `#QI()` within the brackets of `#quote[]` pushes the index to far right:
`#QI()` within the brackets of inline `#quote[]` pushes the index too far right:
#lorem(3) #quote[#QI(<xx>)#lorem(25)]
`#QI()` before `#quote[]` looks right:
#lorem(3) #QI(<xx>)#quote[#lorem(25)]
== Cross references
Cross-referencing mechanism tests:
#table(
stroke: none,
columns: 3,
table.hline(stroke: 0.75pt),
table.header()[Entered][Expected][Generated],
table.hline(stroke: 0.5pt),
[See `#Qp(<NONE>)`], [See page 1], [See #Qp(<NONE>)],
// [See `#Qn(<NONE>)`], [See quotation 1], [See #Qn(<NONE>)],
[See `#Qn(<NONE>)`], [See quotation 2], [See #Qn(<NONE>)],
// [`#QP(<ONE>)`], [Page 2], [#QP(<ONE>)],
[`#QP(<ONE>)`], [Page 1], QP(<ONE>),
// [`@ONE`], [3], [@ONE],
[`@TWO`], [4], [@TWO],
[`#QP(<THREE>)`], [Page 2], QP(<THREE>),
[`#Qo(<THREE>)`], [quotation 6 on page 2], Qo(<THREE>),
table.hline(stroke: 0.75pt),
)
== Remaining issues
Based on the examples above, I have the following questions:
+ What is a better way to place text into the margin?
- The `marginalia` package function `note` is being used to place indexes for inline quotations and block quotations. This seems pretty heavy for such a simple task.
- #strike[This works fine for my `#Quote()` and for the standard `#quote()` in non-block use, but the index generates a paragraph break when used with `#quote(block: true)`.]
- There is a `dy` option, but integrating it cleanly will take some additional work.
- It also places notes in the _outside_ margin, not the _right_ margin, which is fine for single-sided docs but fails in my use for recto/verso docs. I do not see an option to force _right_ or _left_.
- It is not a long-term solution because if margin notes are needed with different offsets, there do not appear to be enough controls to select which offset is used -- there are only package-specific left and right page margins.
+ #strike[Why does `#ref(<label>)` generate a leading space while `#link()` does not?]
- See the definition of `#Qn()` and `#Qo()`.
(#ref(<NONE>))
(@NONE)
I also note that Greek text is hyphenated (as expected) using my `#QI()` function, unlike the situation with `#quote()` (https://github.com/typst/typst/issues/5449).
This is currently compiled under Typst #sys.version.
== With no restatements
Here is an example with no restatements or translations:
It has a translation. Text with heading say different things.
Should the Quote
be non-breakable?
- The
marginalia
package function note
is being used to place indexes for inline quotations and block quotations. This seems pretty heavy for such a simple task.
Just fork it and make more specialized.
- This works fine for my
#Quote()
and for the standard #quote()
in non-block use, but the index generates a paragraph break when used with #quote(block: true)
.
The package used box()
a few times inside:
box(width: 0pt, place(dx: hadjust, dy: vadjust, notebox))
h(0pt, weak: true)
box(_note_right(dy: dy, body))
Removing that, fixes the issue.
- There is a
dy
option, but integrating it cleanly will take some additional work.
Integrate how?
- It also places notes in the outside margin, not the right margin, which is fine for single-sided docs but fails in my use for recto/verso docs. I do not see an option to force right or left.
The book
is false
so it doesn’t use outside
at all. I don’t understand.
if config.book {
return (
margin: (
inside: config.inner.far + config.inner.width + config.inner.sep,
outside: config.outer.far + config.outer.width + config.outer.sep,
top: config.top,
bottom: config.bottom,
),
)
} else {
return (
margin: (
left: config.inner.far + config.inner.width + config.inner.sep,
right: config.outer.far + config.outer.width + config.outer.sep,
top: config.top,
bottom: config.bottom,
),
)
}
- It is not a long-term solution because if margin notes are needed with different offsets, there do not appear to be enough controls to select which offset is used – there are only package-specific left and right page margins.
Which “offset”? What margins do you actually need? If it’s used by you, there is no point in generalizing.
- Why does
#ref(<label>)
generate a leading space while #link()
does not?
Because of
link(it.target)[#supplement #count]
Also see https://typst.app/docs/reference/model/ref/#syntax.
#link("https://github.com/typst/typst/issues/5449")
See https://typst.app/docs/reference/model/link/#syntax.