How does one place content relative to the page margin?

I am using a counter as a label and index for block and inline quotations. The counter should appear in the right margin of the page. As currently coded, the placement of the counter depends on the right edge of the quotation. The relevant code:

#let qi = counter("qi")
#let QI(label) = {
  qi.step()
  context{
    place(right,
      dx: 15mm,// How can this be made relative to the page?
      text(
        fill: red,
        size: 0.8em,
        weight: "semibold",
        qi.display()
      )
    )
    [
      #metadata("qi")#label
    ]

  }
}

#QI(<RB2>)Synesius wrote (as translated by Robert Burton): #quote(block: false)[It is a greater offence to ſteale dead mens labours, than their clothes.]

#quote(block: true,
  attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek],
)[#QI(<NOGOOD>)ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.]

Which results in:

How can the offset be specified to always place the index in the same horizontal location relative to the page margin (as with quotation 1 above)?

(This is one question of a few that this formatting has engendered. More to follow.)

Hello. This seems kind of complicated without use case context (for labels). Sounds like "Real" absolute option for `place` (or alternative function) · Issue #5233 · typst/typst · GitHub.

To get consistent positioning, you need the global scope, which can only be achieved with a wrapper. Because of https://typst.app/docs/reference/layout/place/#effect-on-other-elements, I had to save the start Y position first and then place the marker there. It probably won’t work when the bottom of the quote goes to the new page.

#set page(margin: 2.5cm)
#place(rect(width: 100%, height: 100%))

#let QI(label, ..args) = {
  let qi = counter("qi")
  qi.step()
  context [#metadata(here())<pos>]
  [#metadata("qi")#label]
  quote(..args)
  context {
    let count-marker = text(0.8em, red, weight: "semibold", qi.display())
    let pos = query(selector(<pos>).before(here())).last().value.position()
    place(right + top, dx: 15mm, dy: -page.margin + pos.y, count-marker)
  }
}

Synesius wrote (as translated by Robert Burton): #QI(<RB2>)[It is a greater offence to ſteale dead mens labours, than their clothes.]

#QI(<NOGOOD>, block: true, attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek])[
  ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.
]

I guess you can move out this completely to a different place, that is not bounded by the margins, however, since you need it to be relative to the margin, then you need to use it anyway:

#let qi = counter("qi")
#let QI(label, ..args) = {
  qi.step()
  context [#metadata(here())<pos>]
  quote(..args)
  [#metadata("qi")#label]
}

#set page(margin: 2.5cm, background: context {
  let positions = query(<pos>)
    .map(x => x.value)
    .filter(x => x.page() == here().page())
  for pos in positions {
    let num = qi.at(pos).first()
    let body = text(0.8em, red, weight: "semibold")[#num]
    place(right + top, dx: -page.margin + 15mm, dy: pos.position().y, body)
  }
})

#place(rect(width: 100%, height: 100%))

Synesius wrote (as translated by Robert Burton): #QI(<RB2>)[It is a greater offence to ſteale dead mens labours, than their clothes.]

#QI(<NOGOOD>, block: true, attribution: [Synesius of Cyrene (_circa_ 370--_circa_ 413) in modernized Greek])[
  ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.
]

There is a slight Y shift that I don’t know how to easily fix.

At this point I have worked around the problem of doing this directly by loading the marginalia package, which appears to give me sufficient control of the placement.

I would think that a more direct solution is possible without loading an external package.

You can edit and delete posts.

Can you share your solution, if the package worked for you?

As I said, I have a work-around that achieves my goals. There are a number of issues that I still need to resolve before I can consider it a stable (to the degree that Typst, not yet at v1, should be considered stable) solution.
The complete test harness is below. A number of issues are listed at the end.
I do use a non-standard (for Typst) free font, UnifrakturMaguntia, available from Google and elsewhere.

Pardon the style, and please suggest improvements. I am Typst tyro.

 // Test harness

#import "@preview/marginalia:0.1.4" as marginalia: note//, wideblock
#let config = ()
#marginalia.configure(..config)
#set page(
  ..marginalia.page-setup(..config),
  paper: "a4",
  numbering: "1",
)
#set par( first-line-indent: 1em, justify: true, )
#set text( lang: "en", number-type: "old-style", hyphenate: true,
  overhang: true,
  costs: (hyphenation: 10%),
)

// Convenience variables

#let  QGray = luma(175)// for text decorations, table rules
#let QInset = 2em      // inset of Quote environment

// Guidelines (optional, for testing)
#set page(
    background: context {// based on marginalia source
    let leftm = marginalia.get-left()
    let rightm = marginalia.get-right()
    place( top,
      dy: marginalia._config.get().top,
      line(length: 100%, stroke: 0.5pt + luma(80%)), )
    place( bottom,
      dy: -marginalia._config.get().bottom,
      line(length: 100%, stroke: 0.5pt + luma(80%)), )
    place( dx: leftm.far,
      rect( width: leftm.width, height: 100%, stroke: (x: 0.5pt + luma(70%))), )
    place( dx: leftm.far + leftm.width + leftm.sep,
      rect(width: 10pt, height: 100%, stroke: (left: 0.5pt + luma(85%))), )
    place( right, dx: -rightm.far,
      rect(width: rightm.width, height: 100%, stroke: (x: 0.5pt + luma(70%))), )
    place( right, dx: -rightm.far - rightm.width - rightm.sep,
      rect(width: 10pt, height: 100%, stroke: (right: 0.5pt + luma(85%))), )
    place( // quotation margins
      dx: leftm.far + leftm.width + leftm.sep + QInset,
      rect(width:21cm-5cm-(2*QInset), height: 100%, stroke: luma(90%) + 0.5pt) )
  },
)

//
// QUOTE NUMBERING
//
#let qi = counter("qi")
#let Qm = marginalia.note.with(
  text-style: (fill: red, size: 0.8em, weight: "semibold"),
  numbered: false,
)
#let QI(label) = {
  qi.step()
  Qm[#context{ qi.display();[#metadata("qi")#label]}]
}

// borrowing heavily from
//   httpsforum.typst.app/t/how-can-i-create-referrable-labels-for-custom-multi-level-counters/3511
#show ref: it => {
  let 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) = context {
  let loc = locate(label)
  let nums = counter(page).at(loc)
  // explicit space needed here
  "Page ";link( loc, numbering( loc.page-numbering(), ..nums ) )
}
#let Qp(label) = context {
  let loc = locate(label)
  let nums = counter(page).at(loc)
  // explicit space needed here
  "page ";link( loc, numbering( loc.page-numbering(), ..nums ) )
}

//   Quote Number (upper-case Q, lower-case q)
#let QN(label) = context { // no space required here
  "Quotation";ref(label)
}
#let Qn(label) = context { // no space required here
  "quotation";ref(label)
}

//   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, Text, lang) = {
  let QScale = 1.1818// about 13pt on default 11pt
  let llap(Text, Size, Color, Lower) = {
    box(width: 0pt,)[
      #align(right)[
        #text(baseline:Lower,size:Size,fill:Color)[#Text]]]
  }
  let rlap(Text, Size, Color, Lower) = {
    box(width: 0pt,)[
      #align(left)[
        #text(baseline:Lower,size:Size,fill:Color)[#Text]]]
  }
  context{
    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)
    Text
    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",
"ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.")

== 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.]

== 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:

#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.header(
    table.hline(stroke: 0.75pt + QGray),
    [*Entered*],
        [*Expected*],
        [*Generated*],
        table.hline(stroke: 0.5pt + QGray)),
  [See `#Qp`(\<NONE>)], [See page 1], [See #Qp(<NONE>)],
  [See `#Qn`(\<NONE>)], [See quotation 1], [See #Qn(<NONE>)],
  [`#QP`(\<ONE>)], [Page 2], [#QP(<ONE>)],
  [\@ONE], [3], [@ONE],
  [Page (\<THREE>)], [Page 2], [#QP(<THREE>)],
  [`#Qo`(\<THREE>)], [quotation 6 on page 2], [#Qo(<THREE>)],
 table.hline(stroke: 0.75pt + QGray),
)

== 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.
   - 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.
 + Why does `#ref(<label>)` generate a leading space while `#link...` does not?
   - See the definition of `#Qn` and `#Qo`.

 I also note that Greek text is hyphenated (as expected) using my `#QI` function, unlike the situation with `#quote` (#link("https://github.com/typst/typst/issues/5449")).

 This is currently compiled under Typst #sys.version.

Did a first pass. It’s all pretty good, but the sheer volume is overwhelming, to be honest. Why do you use uppercase where there is no reason to use uppercase in names?

#import "@preview/marginalia:0.1.4" as marginalia: note //, wideblock

#let config = ()
#marginalia.configure(..config)

#set page(..marginalia.page-setup(..config), 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 = marginalia.get-left()
  let rightm = marginalia.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: marginalia._config.get().top,
    line(stroke: thick + luma(80%))
  )
  place(
    bottom,
    dy: -marginalia._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 = marginalia.note.with(
  // text-style: (fill: red, size: 0.8em, weight: "semibold"),
  text-style: arguments(0.8em, red, weight: "semibold"),
  numbered: false,
)
#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",
  "ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.",
)

== 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.]

== 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:

#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>)],
  // [`#QP(<ONE>)`], [Page 2], [#QP(<ONE>)],
  [`#QP(<ONE>)`], [Page 1], QP(<ONE>),
  [`@ONE`], [3], [@ONE],
  [`#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.
  - 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.
+ Why does `#ref(<label>)` generate a leading space while `#link...` does not?
  - See the definition of `#Qn` and `#Qo`.

I also note that Greek text is hyphenated (as expected) using my `#QI` function, unlike the situation with `#quote` (#link("https://github.com/typst/typst/issues/5449")).

This is currently compiled under Typst #sys.version.

Thank you for that rewrite—I am studying and learning.

As to the upper-casing of variables: In an earlier iteration I used a the name text and found that other things were failing. Renaming it to Text resolved the issue. I am sorry that I do not remember just where I did it—I think it was in a function-call argument name. I note that all Typst terms appear to be lowercase and this makes it less likely that I will redefine one accidentally.

The volume comes about because I use this to test as many variants as I can, changing line lengths to force hyphenation, put page breaks in different places, and expand the capability of the underlying #Quote function.

In formats other than Typst I have options to adjust font, language, spacing, alignment, ligature selection, margins, and more in typesetting to match the original appearance of quotations; so far with Typst I have settings for font and language. So, the file will grow considerably, but likely be broken into smaller files to work on specific features.

See Add entry for `std` variable in documentation · Issue #6395 · typst/typst · GitHub. Just prefix stuff with std. if you override names. Uppercase takes more effort to type.

Which margins, ligature selection, and alignment?

Separately, I’ve spotted this thing:

    box(width: 0pt,)[
      #align(right)[
        #text(baseline:Lower,size:Size,fill:Color)[#Text]]]

And here you add 2 spaces at the start, which may or may not affect the output. In the other topic, I still didn’t get the answer to my question:

Thank you for pointing out the problem with the construction I used for \llap. I have a better understanding now of how such things work and appreciate the use of show rules to handle such formatting.

To the need for control of margins, ligatures, spacing, and alignment—in one book-in-process I am quoting older works where accurately replicating the appearance is critical. Consider this from Robert Burton’s 1628 3rd edition of The Anatomy of Melancholy, which demonstrates one step in the development of our modern footnoting, and from which the Synesius work may have entered English:

and this from Thomas Bowdler’s 1807 The Family Shakspeare [his spelling], which helps to illustrate the development of the use of quotation marks in English typesetting.

I have previously replicated these with ConTeXt with quite reasonable fidelity and want to see if Typst can do the same for less effort.

More challenging in Typst will be replicating such things as scribal abbreviations used in early typeset works, such as the qz in Dicitqz (Dicitque) in the Burton above. I assume that it can be done, but I haven’t looked at it yet. The z is actually Latin small et (Unicode A76B, ꝫ), shrunk, lowered, and kerned into the q.

Separately, two questions and a comment on the rewrite you graciously provided.

  1. How does the arguments function work (in the setup for marginalia notes)? Must one know the expected order of arguments, or does Typst analyze them and guess that “0.8em” is a font size and “red” is a color but need a hint for the weight?

  2. You move the invocation of context from near the end of the decoration function, where it is needed in calculating the height adjustment, to the beginning. Is there a reason to move it from where it is needed to the beginning, or is it simply a preferred style?

  3. The alternative line you provided in your rewrite to place the opening decoration, place(dx: -0.6em, dy: -0.1em, text(size: Size, fill: Color)[#Text])
    has a few issues.

    • It allows a page break before the text that it decorates, as you can see in the preview.
    • It is not precise with the measurements given (although they can obviously be tweaked to improve the appearance).
    • It does not adapt to changes in either the base font size or the chosen decoration scale.

The arguments function simply preserves the arguments as they are passed in, so that they can be passed on further to another function. The specific details you are asking about here are all about how the text function works, which is where the text-style arguments ultimately end up.

I’m not sure why, maybe for convenience or for historical reasons, but the text function is especially adaptive, or “DWIM”-y (Do what I mean), about its arguments. A function can receive both positional and named arguments, so it’s open for them to analyze the positional arguments and “guess” how they should be interpreted, but it’s not really the norm in typst to do so excessively.

You can’t do this excessively, because only text can do this. This is the preferred way, because inline it’s much more readable to do #text(12pt)[word] or #text(red)[word] or both, rather than adding size: and fill: .

See arguments, https://typst.app/docs/reference/text/text/#example, and How does one place content relative to the page margin? - #11 by bluss.

Look at indentations:

#let QDecorate(open, close, body, lang) = context {
  let QScale = 1.1818
  let llap(Text, Size, Color, Lower) = {
    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 QDecorate(Open, Close, Text, lang) = {
  let QScale = 1.1818
  let llap(Text, Size, Color, Lower) = {
    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]
  }
  context{
    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)
    Text
    rlap([#sym.wj#sym.space.nobreak#Close],QScale*text.size,QGray,Adjust)
  }
}

Since output is content either way, and it’s not a global show rule, and no other state is changed/fetched above, so to simplify the code flow I moved it. Indentations are bad, when they accumulate. Maybe you’ve heard about Linux source code and that 3 indentations is really bad. I try to follow this when possible. It makes the code easier to read and maintain.

I just tried something, and it didn’t work, at least for the closing symbol. I think that there should be something better than box(width: 0pt), as it’s (currently) behaving strangely.

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.

Thank you, Andrew, and bluss, for the tutorial on arguments. The arguments page of the reference was not helpful.

I think it has to be a zero-width box for the closing decoration. If the container has any width, it will prevent the surrounded text from going flush to the margin. For the opening decoration you can measure the decoration width and hang the paragraph by that amount. I like the symmetry of having both set in zero-width boxes, hence the names llap and rlap, which come from TeX where they set text in a zero-width box in a similar manner.

Indeed, that has been a problem with programming languages since comments were first allowed! Fixed, but likely to recur.

No. I have at least one quote (where Coleridge misquotes Spinoza) that goes well over a page.

Generally above my current skill level, but I will see what I can do with what you provided.

Hmm, are those all zero-width boxes? If so, where is the space coming from. I guess this may be a reason to inform marginalia authors, and possibly a reason to fork the bits I need.

I was thinking that I could detect it being used in a block #quote and apply an appropriate offset for those instances only.

When I add book: true to the marginalia config, all indices (marginalia notes) appear in the outer margin – right margin on recto pages and left margin on verso pages. There is no option to say to always use the right (or left) margin. While my test rig is currently single-sided, it will eventually be double-sided. (book: true for marginalia).

Some quoted material has marginal notations. These need to be placed a proper distance from the margin of the quotation text. For example, in the Burton quotation shown above the proto-footnotes are in a close-up margin note on the outside of text block. When they are reproduced, the note should be shown as it originally appeared even if it ends up on the other side. Think of it as a figure that should be reproduced in its original format.
Furthermore, different extracts (from different writers) may require placement at different distances from the text block margin.

Ahh! Thank you. As I noted, I took the code from elsewhere and didn’t sufficiently understand it to properly modify it. But you already fixed it in the earlier rewrite, so it is no longer an issue.

How come? It’s literally this, but simpler.

I mean, if it’s good enough, then it’s good enough. The only other functions I can think of are pad and move, but with pad you kinda have to set the width to the width of a glyph, which requires measure, which is somewhat expensive. Well, if all symbols are kind of same width, then it might not matter, though as it turns out you also need to make it inline-level width box:

#box(pad(left: -0.6em)[(])#lorem(20)

But it will jump to next line if there is not enough space, so I guess it’s no good.

And where is the code? If I use my own, you’d have to fix it yet again.

Not all. The spacing is coming from box, that is inline-level container in the root scope, so it’s creating a paragraph that has context() element that has different stuff plus place(). To lay out the next block, you must start from a new line.

Feel free.

You can simply have 2 different functions and use them for block/non-block quotes. Or do funky stuff with state/context, etc.

So basically a dx?

I don’t know what this means.

If marginalia has an example of text in the page side margins, then it should be possible to put text in the margins. But with more control comes more code.


So, what’s next?

It was unhelpful in understanding the use with text.

Fixed in my working version by correcting the introductory text (comment).

Certainly. I shall open an issue in their repository referencing this discussion.

More study. Following open issues have an impact on this (and thank you for opening some of them). Reconsidering the use of Typst for (what I consider) fine typography, at least until it matures a bit more, but playing with it more to feel comfortable with smaller projects that do not meet that bar.

The goal for many, I suppose, is a single-source publishing tool that can produce html, epub, and conformant, accessible pdf documents. I certainly would like to see that, and I am also interested in beautifully typeset printed books.

What would be your suggestion for the docs in this regard? Specifically.

I mean, that list of comments is fixable. I’m pretty sure all the comments will be struck out, assuming there is a precise spec for how everything supposed to work. To hopefully reduce the amount of code copied from the package. You would put it in a separate file anyway, so after that you will have a clean document where you can use your custom functions.

Like, what is the state of this topic?

You can ask for marginal notes in Typst, if it’s not too niche.

Oh, it’s already on the roadmap:

  • End notes, maybe margin notes

This is probably it: More Note Functions · Issue #1337 · typst/typst · GitHub.


Well, I guess so. Apparently EPUB is coming too.

Perhaps a note that different functions marshal arguments in different ways. The problem isn’t really with the reference page for arguments, which is a nice reference guide but not a usage guide – it is with the reference page for text, which shows all of the arguments as key: value pairs. (To be fair, the example for size shows raw use, but viewing the example is optional.)

I would specifically suggest that each reference page should have an explicit section on related syntactic sugar such as this.

Indeed. I am quite aware of those plans, at least to the degree described on those public pages.

Margin notes and end notes are basic requirements. Without robust end notes Typst will be a non-starter for almost any serious non-fiction book. By robust I mean that they should support all of the features of the non-note text except perhaps, for foot- and end-notes, and should be positionable at the end of a chapter, part or work.

I think EPUB is ugly, but publishers want to be able to distribute in it and are reluctant to accept material that cannot easily be cast as such.

What does this mean? Are you talking about optional size: and fill: ?

Yes, hidden examples are not super great, but if they are all visible, then it might also not be great. The https://typst.app/docs/reference/text/text/#example does show both text and fill, as I said in How does one place content relative to the page margin? - #12 by Andrew. Is this not it?

Well, it’s shown semi-implicitly, it’s just not said that you can drop the named part in text. Should it?

https://typst.app/docs/reference/model/ref/#syntax and Link Function – Typst Documentation do have an explicit section for this, because there is actually a special syntax. With text it’s not that prominent.

I have tried to answer the issues you created on GitHub (Possible spurious spaces and breaks generated by marginalia · Issue #7 · nleanba/typst-marginalia · GitHub and Always place note in right margin · Issue #8 · nleanba/typst-marginalia · GitHub) so I wont repeat myself here.

However, one of your remaning issues you list is

I’m assuming you want a way to horizontally shift some of the margin-notes, e.g. such that it has a different horizontal alignemt. I think you should be able to do this by passing appropriate outset and inset to the block-style of these notes.

Example:

#lorem(40)
#note(block-style: (fill: yellow))[#lorem(15)]
#lorem(30)
#note(block-style: (outset: (left: 2cm), inset: (left: -2cm), fill: yellow))[#lorem(15)]
#lorem(40)

(the yellow fill is there only for visualization)