How can I parse this two-level array, or is there a better alternative

I have created an enhanced #quote function to support, in addition to an attribution, restatements and translations. Translation is used to present a quotation that is not in the language of the article in the language of the article; restatement is used to present a quotation in a different form (antiqua instead of an original fraktur type face, or phonetically, or translated into a third language.) There may be multiple restatements and translations. My current code handles the multiples well, but I cannot seem to get a second level of array working to support specification of the language of a restatement so that it is properly hyphenated.

Restatements are almost always in the language of the original quotation, but not always (consider phonetic transcriptions, or instances as shown here where Latin is used to restate a Greek original). So, I would prefer not to explicitly state a language, but only state it for the exceptional cases. I would expect an array like;

 restatement: (
   [restatement_in_language_of_quotation],
   ( [other_language_code], [restatement_in_other_language]),
   ( [null_language_code], [phonetic transcription])
 )

should work, but I cannot get that working.

The code so far:

#let    QGray = luma(175)
#let   QInset = 2em
#let   QScale = 1.1818em // roughly 13pt for 11pt text

#let   QAOpen = [#text(size: QScale, fill: QGray)[--]]
#let QAIndent = -4mm  // Indent for attribution decoration

#let   QROpen = [#text(size: QScale, fill: QGray)[\[]]
#let  QRClose = [#text(size: QScale, fill: QGray)[#sym.wj\]]]
#let QRIndent = -2.5mm  // Indent for attribution decoration

#let   QTOpen = [#text(size: QScale, fill: QGray)[\(]]
#let  QTClose = [#text(size: QScale, fill: QGray)[#sym.wj\)]]
#let QTIndent = -2.5mm  // Indent for attribution decoration

#let Quote(
  label: none,
  attribution: none,
  restatement: (),
  translation: (),
  lang: none,
  body,
) = {
  if restatement != none [
    #if type(restatement) != array {
      restatement = (restatement,)
    }
  ]
  if translation != none [
    #if type(translation) != array {
      translation = (translation,)
    }
  ]
  block(
    above: 0em, //-0.5em,
    below: 0em, //-0.5em,
    inset: QInset,
    [
      #if lang != none [
        #set text(
          lang: lang
        )
      ]
      #body
      #if restatement != none [
        #for restate in restatement [
          #par(
            first-line-indent: QRIndent
          )[
            #QROpen
            #restate
            #QRClose
          ]
        ]
      ]
      #if translation != none [
        #for trans in translation [
          #par(
            first-line-indent: QTIndent
          )[
            #QTOpen
            #trans
            #QTClose
          ]
        ]
      ]
      #if attribution != none [
        #par(
            first-line-indent: QAIndent
        )[
          #QAOpen
          #attribution
        ]
      ]
    ]
  )
}
#Quote(
  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],
  restatement: (
    [Magis impium Mortuorum Lucubrationes quam veſtes furari.],
    [arbitror magis impiú mortuorum ſcript furari, quam veſtes, quod appelatur buſta effodere.],
    [Magis autem impium esse arbitror mortuorum lucubrationes quam vestes furari, quod sepulcra perfodere dicitur.]
  ),
  translation: [It is a greater offence to ſteale dead mens labours, than their clothes],
  lang: "el"
)[ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.
]

Can somebody suggest how to process the two-level array?

Hello! If I understand correctly, you want the correct language parameter for your restatement in case you specify one.

I can only suggest to use a dictionary so it’s more explicit what’s going on, although an array with a string, then text is also fine. From your example, it would look like this:

#Quote(
  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],
  restatement: (
    (
      [Magis impium Mortuorum Lucubrationes quam veſtes furari.],
      [arbitror magis impiú mortuorum ſcript furari, quam veſtes, quod appelatur buſta effodere.],
      [Magis autem impium esse arbitror mortuorum lucubrationes quam vestes furari, quod sepulcra perfodere dicitur.],
    ),
    (
      [mɑdʒis impium mɔɾtuɔɾum lukubɾɑtsiɔnɛs kwɑm vɛſtɛs fuɾɑɾi.],
      [ɑɾbitɾɔɾ mɑdʒis impiu mɔɾtuɔɾum ſkɾipt fuɾɑɾi, kwɑm vɛſtɛs, kwɔd ɑppɛlɑtuɾ buſtɑ ɛffɔdɛɾɛ.],
      [mɑdʒis ɑutɛm impium ɛssɛ ɑɾbitɾɔɾ mɔɾtuɔɾum lukubɾɑtsiɔnɛs kwɑm vɛstɛs fuɾɑɾi, kwɔd sɛpulkɾɑ pɛɾfɔdɛɾɛ ditʃituɾ.],
    ),
    (
      lang: "fr",
      text: [Je mène une voie plus irrespectueuse pour voler ou profaner les paroles des morts, ce qu'on appelle le pillage de tombes.],
    )
  ),
  lang: "el",
  translation: [It is a greater offence to ſteale dead mens labours, than their clothes],
  [ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.],
)

restatement (should probably be restatements) is a nested array as you say, so you need to process every restatement, then every line in the text.

In which case, the restatement processing should look like this. I deliberately made it very verbose, but I’m sure it could be written more succinctly.

...
        #for restate in restatement [ // per statement
          #let restate = (
            lang: if type(restate) == dictionary { restate.lang } else {
              ""
            }, // extract the lang if provided
            text: if type(restate) == dictionary {
              restate.text
            } else {
              restate // ([Lorem],), [Lorem]
            },
          )
          #if type(restate.text) != array { // if text is not an array
            restate.text = (restate.text,) // (.., text: [Lorem])
          }
          #let txt = if restate.lang != "" {
            text.with(lang: restate.lang) // specify the lang!
          } else {
            text
          }
          #for line in restate.text { // iterate over (text: ([Lorem],))
            par(first-line-indent: QRIndent)[
              #QROpen
              #txt(line)
              #QRClose
            ]
          }
        ]

Then you get something like this

Please try avoiding using markup square brackets in scripting if you continue to use scripting inside. It can add spaces in some cases. Can you share the reason you are using it? I don’t think documentation has this.

Like @quachpas said, maybe restatement really should be restatements, even though you technically can pass only one. I guess this is the same problem as document.author.

To parse array with different data structures, use type(). With array.map you can check the type and change the element’s structure.

The conditional set rule doesn’t work this way, see this.

There is also block.spacing.

#let    QGray = luma(175)
#let   QInset = 2em
#let   QScale = 1.1818em // roughly 13pt for 11pt text

#let   QAOpen = text(size: QScale, fill: QGray)[--]
#let QAIndent = -4mm  // Indent for attribution decoration

#let   QROpen = text(size: QScale, fill: QGray)[\[]
#let  QRClose = text(size: QScale, fill: QGray)[#sym.wj\]]
#let QRIndent = -2.5mm  // Indent for attribution decoration

#let   QTOpen = text(size: QScale, fill: QGray)[\(]
#let  QTClose = text(size: QScale, fill: QGray)[#sym.wj\)]
#let QTIndent = -2.5mm  // Indent for attribution decoration

#let Quote(
  label: none,
  attribution: none,
  restatement: (),
  translation: (),
  lang: none,
  body,
) = {
  if type(restatement) != array { restatement = (restatement,) }
  if type(translation) != array { translation = (translation,) }
  restatement = restatement.map(x => if type(x) != array { (lang, x) } else { x })
  block(spacing: 0em, inset: QInset, {
    set text(lang: lang) if lang != none
    body
    for (lang, restate) in restatement {
        set text(lang: lang)
      par(first-line-indent: QRIndent)[#QROpen (#lang) #restate #QRClose]
    }
    for trans in translation {
      par(first-line-indent: QTIndent)[#QTOpen #trans #QTClose]
    }
    if attribution != none {
      par(first-line-indent: QAIndent)[#QAOpen #attribution]
    }
  })
}

#Quote(
  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],
  restatement: (
     [restatement_in_language_of_quotation],
     ("us", ["restatement_in_other_language"]),
     ("ru", ["phonetic" transcription]),
  ),
  translation: [It is a greater offence to ſteale dead mens labours, than their clothes],
  lang: "el"
)[ἡγοῦμαι δὲ ἀσεβέστερον ἀποθανόντων λόγους κλέπτειν ἢ θοἰμάτια, ὃ καλεῖται τυμβωρυχεῖν.
]

With show rule wrapping it can be slightly simplified:

#let Quote(
  label: none,
  attribution: none,
  restatement: (),
  translation: (),
  lang: none,
  body,
) = {
  if type(restatement) != array { restatement = (restatement,) }
  if type(translation) != array { translation = (translation,) }
  restatement = restatement.map(x => if type(x) != array { (lang, x) } else { x })
  show: block.with(spacing: 0em, inset: QInset)
  set text(lang: lang) if lang != none
  body
  for (lang, restate) in restatement {
    set text(lang: lang)
    par(first-line-indent: QRIndent)[#QROpen (#lang) #restate #QRClose]
  }
  for trans in translation {
    par(first-line-indent: QTIndent)[#QTOpen #trans #QTClose]
  }
  if attribution != none {
    par(first-line-indent: QAIndent)[#QAOpen #attribution]
  }
}
1 Like

To the question about where I saw the use of scripting inside square brackets and why I have used it: I can only say that I saw it in some example, somewhere, as I was getting a feel for the basic syntax and it worked, so I continued to use it. Likely it was not in formal documentation but in this forum. Sorry I cannot be more specific, but if I notice it I will investigate further.

1 Like

Seems like Scripting – Typst Documentation under the heading Conditionals shows that it can be done, it shows both options of course, (and markup blocks is the cool novelty, IMO, could attract anyone) but no guidance at that point which one to use.

1 Like

Great. Include explanation between `[]` and `{}` conditional/loop blocks · Issue #6403 · typst/typst · GitHub

1 Like

Also under the heading Embedding set and show rules at Making a Template – Typst Documentation.