Art credits: a major mangling of the figures ToC?

The desired goal: I have pieces of illustrative or decorative art on various pages of my document. I want to tag some of them with an artist’s name (others are public domain and don’t need this). Then on the contents page I want to have, not just a list of names in order, but something like:

Internal Art: Artist A (p. 7), Artist B (pp. 8, 12), Artist C (p. 10)

If I can get hold of a list of artists and page numbers I can write a transform to produce the output format I want. But how can I get hold of such a list? Should I put them in figures, and set a caption for each figure (with a null show-rule to make it invisible, that’s not a problem as I don’t need figure captions elsewhere), and somehow extract that list from the internals of the system?

Using the caption sounds like a good idea, if you don’t need it for anything else. It should be straightforward to show the caption in the contents listing but not with the main figure.

If in some cases you want a different text in the outline than in the caption below the figure, there is also a recipe for that here: Short figure caption for outline · Issue #1295 · typst/typst · GitHub which lets you define separate texts for the two cases.

Is this what you mean? It’s not clear how art is included.

#show figure.where(kind: "art"): set figure(supplement: none)
#show figure.caption.where(kind: "art"): none

#let art-toc() = context {
  "Internal Art:"
  let items = query(figure.where(kind: "art")).map(fig => (
    artist: fig.caption.body.text,
    page: str(fig.location().page()),
  ))
  let artist-pages = (:)
  for (artist, page) in items {
    let pages = artist-pages.at(artist, default: ())
    artist-pages += ((artist): pages + (page,))
  }

  artist-pages
    .pairs()
    .sorted(key: it => it.first())
    .map(((artist, pgs)) => {
      pgs = if pgs.len() == 1 [p. #pgs.first()] else [pp. #pgs.join(", ")]
      [ #artist (#pgs)]
    })
    .join(",")

  // for (artist, pages) in artist-pages.pairs().sorted(key: it => it.first()) {
  //   let pages = if pages.len() == 1 {
  //     [p. #pages.first()]
  //   } else {
  //     [pp. #pages.join(", ")]
  //   }
  //   ([ #artist (#pages)],)
  // }.join(",")
}

#art-toc()

#range(6).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist A", kind: "art")

#range(1).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist B", kind: "art")

#range(2).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist C", kind: "art")

#range(2).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist B", kind: "art")

image

I also made a PoC, same deal, more or less the same thing. This one allows the artist name to be content, that seems to be the difference. :slightly_smiling_face:

#show figure.where(kind: "artwork"): it => {
  show figure.caption: none
  it
}

#let artwork = figure.with(kind: "artwork", supplement: "Figure")
#let artwork-outline() = context {
  let works = query(figure.where(kind: "artwork"))
  let artists = ()
  let loc = ()
  for work in works {
    let artist = work.caption.body
    if artist != [] and artist != none {
      if artist not in artists {
        artists.push(artist)
        loc.push(())
      }
      let index = artists.position(elt => elt == artist)
      loc.at(index).push(work.location().page())
    }
  }
  [Internal Art: ]
  (for (artist, pages) in artists.zip(loc) {
    let pages = if pages.len() == 1 { [p. #pages.at(0)]} else { [pp. #pages.map(str).join([, ])]}
    ([#artist (#pages)], )
  }).join([, ])
}
#artwork-outline()

#artwork(rect[A], caption: [Mondrian])
#pagebreak()
#artwork(rect[A], caption: [Mondrian])
#pagebreak()
#artwork(rect[B], caption: [Dali])

image

If you wouldn’t want to use captions, there are always other options, like even side tables that link each figure to extra information you keep separately.

It’s not sorted, and you can’t sort content, not directly, hence I used strings and sorted on them. But it’s not said explicitly, so idk.

Well that’s true but in the original instruction it’s not clear which order is required, so I figured document order is the expected one for an outline. The example is both sorted by alphabet and page number, so don’t know.

My actual intention is to sort by first page of occurrence (I very rarely have more than one image on a page in this document). But the query(figure bit is what I was entirely missing. Thanks both!

Then you can technically change sorting to .sorted(key: it => int(it.last().first())), but since all items initially sorted by page, and then transformed to a dictionary, the artist insertion order is preserved, so you don’t need to sort anything, only group by artist.

#show figure.where(kind: "art"): set figure(supplement: none)
#show figure.caption.where(kind: "art"): none

#let art-toc() = context {
  "Internal Art:"
  let items = query(figure.where(kind: "art")).map(fig => (
    artist: fig.caption.body.text,
    page: str(fig.location().page()),
  ))
  let artist-pages = (:)
  for (artist, page) in items {
    let pages = artist-pages.at(artist, default: ())
    artist-pages += ((artist): pages + (page,))
  }
  artist-pages
    .pairs()
    // .sorted(key: it => int(it.last().first()))
    .map(((artist, pgs)) => {
      pgs = if pgs.len() == 1 [p. #pgs.first()] else [pp. #pgs.join(", ")]
      [ #artist (#pgs)]
    })
    .join[,]
}

#art-toc()

#range(6).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist A", kind: "art")

#range(1).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist B", kind: "art")

#range(2).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist C", kind: "art")

#range(2).map(_ => pagebreak()).join()

#figure(rect(), caption: "Artist B", kind: "art")
1 Like