Решили издать каталог подразделений, проектов и людей достаточно крупной организации — и встал вопрос, в чем именно готовить макет: InDesign, TeX или typst. При выборе инструмента хотелось учесть:
1) удобство работы каждого участника,
2) удобство совместной работы,
3) удобство внесения правок в последний момент.
Третий пункт даже был самым важным, посколько было очевидно, что первоначальные данные весьма грязные, будут правки не только орфографические, но и в масштабе плюс/минус подраздел.

InDesign — старый добрый друг, в котором есть работа со стилями, скрипты типа DoTextOK, генерация оглавлений и прочее. Но я пока не освоил систему совместной работы в InDesign, плюс планировалась верстка с выводом данных сеткой — а это означало бы, что при добавлении или удалении одного элемента немалую часть работы пришлось бы делать заново (опасения о таких подковырках оправдались). Пугала еще одна рутинная процедура — вставка вручную QR-кодов, которых в итоге оказалось 170 штук. Напутать их было бы проще простого.

TeX для меня — тоже старый добрый друг, о сопоставлении его с InDesign я уже писал на Хабре. Совместную работу можно было организовать в git, устойчивость к внесению правок в последний момент вполне надежная. QR-коды умеет генерировать сам.

Но на горизонте появился typst, который работает по принципу TeX'а (гибрид текста и команд + картинки --> pdf), но в несколько раз быстрее (особенно если TeX надо прогонять несколько раз для выставления перекрестных ссылок). Еще больше порадовал typst тем, что можно прямо в режиме реального времени видеть результат всех вносимых изменений (Visual Studio Code + плагины для typst), а также прыгать из превьюшки на нужное место кода, а из кода — на нужное место превьюшки.

И окончательное решение в сторону typst было принято, когда стало понятно, что в нем можно удобно представить исходные данные по типу JSON, конкретно у меня — как список словарей

#let names = ((name: "Иван", year: "2000"), 
  (name: "Петр", year: "2010"))

а далее сортировать, фильтровать, заниматься арифметическими вычислениями. Ну и он умеет делать QR-коды.

Итак, был выбран typst, а для изящной типографики (и опять же, для избавления от прогонки скриптами) вот такая постепенно сформировалась преамбула.

Размеры страницы, поля, нижний колонтитул с выравниванием по внешнему краю. У нас не было картинок с вылетом за обрез, так что типография приняла макет в обрезном формате, но в typst можно задать и обрезной отступ параметром outset.

#set page(
  width: 170mm, 
  height: 225mm, 
  margin: (inside: 21mm, outside: 14mm, top: 19mm, bottom: 28mm),
  numbering: "1",
  footer: context{
    set text(size: 16pt, 
      number-type: "old-style", 
      number-width: "proportional")
    let (n,) = counter(page).get()
	  set align(if calc.even(n) { left } else { right })
	  counter(page).display("1")
  }
  )

Основной шрифт задан был так:

#set text(font: "Ladoga",
          lang: "ru",
          size: 12pt,          
          fill: cmyk(0%,0%,0%,100%))

Последняя строка появилась уже при проверке макета в Adobe Acrobat Pro: оказалось, что основной текст идет не чистым черным, а составным из 4 красок — на экране это, конечно, не заметишь, а вот при офсетной печати в типографии на такой подход ругались бы знатно.

С русским языком typst работает хорошо, неадекватных переносов замечено не было (в отличие от InDesign, в который правильно ставить модуль переносов Батова).

Абзацы равняем по ширине, выставляем интерлиньяж (spacing) и абзацный отступ (я сторонник убирать «красную строку» после заголовков, так что all: false).

#set par(justify: true, 
  spacing: 0.65em,
  first-line-indent: (amount: 1em, all: false),
  )

Дальше идет ряд директив, указывающих, как обращаться со знаками процента, номера, однобуквенными словами (считаю, что лепить их к следующему слову надо только в случае заглавной буквы). Тире не должно отрываться от предыдущего слова, а отступы вокруг него хочется сжать до 30%. Сокращения в адресах не должны отрываться от имен населенных пунктов и от чисел.

// знак процента
#let percents = regex("(\d+)\s*\%")
#show percents: it => {
  let (d, ) = it.text.match(percents).captures
  [#box([#d\u{2009}%])]
}
// знак номера
#let numero = regex("№\s*(\d)")
#show numero: it => {
  let (d, ) = it.text.match(numero).captures
  [№\u{2009}#d]
}
// однобуквенными слова в начале предложения
#let aviko = regex("(\b[АВИКОСУЯ])\s+")
#show aviko: it => {
  let (d, ) = it.text.match(aviko).captures
  [#d~]
}
// инициалы и фамилия
#let fio = regex("(\b[А-ЯЁ]\.)\s*([А-ЯЁ]\.)\s*([А-ЯЁ][а-яё])")
#show fio: it => {
  let (i, o, f) = it.text.match(fio).captures
  [#i\u{202F}#o\u{202F}#f]
}
// тире
#let tire = regex("\s+(—)\s+")
#show tire: it => {
  let (d, ) = it.text.match(tire).captures
  [#text(spacing: 30%)[~#d ]]
}
// сокращения в адресах
#let gorod_selo = regex("\b((г|с|пос|дер|д|ул|пер|п|кв)\.)\s+")
#show gorod_selo: it => {
  let d = it.text.match(gorod_selo).captures.first()
  [#d~]
}

Отдельная заморочка была с телефонами. Убираем все лишние символы, определяем типичные 5-циферные коды регионов, выводим красиво и единообразно, без скобок, с пробелами и маленькими центральными точками, отделяющими последние две пары цифр, типа +7 910 123⋅45⋅67. Местные номера — по типу +7 48532 1⋅23⋅45. Если эстетическое чувство захочет изменить подачу, меняем всё несколькими нажатиями клавиш.

#let format_phone(p) = {
  let psplit = p.split(" доб. ")
  if psplit.len() > 1 {
    return format_phone(psplit.at(0)) + " доб." + sym.space.nobreak.narrow + psplit.at(1)
  }
  let digits = p.replace(regex("[^0-9]"), "")
  if digits.len() != 11 {return p}
  let formatted = "+7"
  let space = " "
  let space_nobreak = sym.space.nobreak
  // let space = sym.space.narrow
  // let space_nobreak = sym.space.nobreak.narrow
  let open_bracket = ""
  let close_bracket = ""
  // let open_bracket = "("
  // let close_bracket = ")"
  // let separator = "-"
  let separator = sym.space.hair + sym.dot.op + sym.space.hair
  if digits.slice(1,3) == "48" {
    formatted += space_nobreak + open_bracket + digits.slice(1,6) + close_bracket + space + digits.at(6) + separator + digits.slice(7,9) + separator + digits.slice(9,11)}
  else {
    formatted += space_nobreak + open_bracket + digits.slice(1,4) + close_bracket + space + digits.slice(4,7) + separator + digits.slice(7,9) + separator + digits.slice(9,11)}
  return formatted
}

С форматированием электронных адресов всё проще, просто убираем заглавные:

#let format_email(e) = {lower(e)}

Счастье, которое было у меня от генерации QR-кодов командами qrcode("habr.com", 2cm), почти рухнуло на этапе препресса �� Adobe Acrobat выдал ошибку, что все QR-коды не чисто чёрные, а 4-цветные. Прописывание cmyk-цвета в качестве параметра вызова не приносило нужного результата. Связано это было с тем, что генерация QR-кода происходит через промежуточный этап в SVG, а формат SVG хранит информацию о цвете только RGB, но не CMYK. Тыканье мэтров на форумах не принесло результатов. Пришлось самому написать код, который разбирает текст SVG и отрисовывает. Оказалось не слишком сложно. Там QR-код записан как набор прямоугольников, заданных командами типа "M1 2h3v4h-3Z": сдвинься в точку (1,2), рисуй направо 3 клетки, вверх 4 клетки, налево 3 клетки, замкни контур. Парсим, получаем массив координат и размеров, рисуем. Правда, это решение было придумано на следующий день после того, как макет ушел в печать — быстрее оказалось прощелкать все 170 QR кодов в Acrobat'е и преобразовать их в чистый CMYK-черный.

#let black_cmyk_qrcode(text, width: 1.5cm, color: cmyk(0%, 0%, 0%, 100%)) = {
  let svg-bytes = qrcode(text).source
  let qr_path = str(svg-bytes).match(regex("<path d=\"([^\"]*)")).captures.at(0) // get the path part of code from SVG
  let qr_rectangles_text = qr_path.matches(regex("M(\d+) (\d+)h(\d+)v(\d+)h-\d+Z"))
  let qr_rectangles_coord = ()
  for qr in qr_rectangles_text {
    qr_rectangles_coord.push(("x", "y", "width", "height").zip(qr.captures.map(x => int(x))).to-dict())
  }
  let tiles = calc.max(..qr_rectangles_coord.map(x => (x.x + x.width)))
  let tile_size = width/tiles
  rect(width:width, height: width, inset: 0mm, stroke: 0pt,
    {
    for r in qr_rectangles_coord {
        place(dx: r.x*tile_size, dy: r.y*tile_size, rect(height: r.height*tile_size, width: r.width*tile_size, fill: color))
    }}
  )
}

Итоговый макет на 240 страниц содержал 480 картинок, 170 QR-кодов и весил 2.2Gb. С таким размером он уже переставал показывать превью в Visual Studio Code.

Сжатый вариант макета весом 6Mb можно посмотреть/скачать тут.

Главный вывод, который я сделал для себя — typst удобен, быстр и перспективен, буду дальше пользовать его и для книг, и для рутины с договорами, и для разминки мозгов.