references.typ 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. // ==========================================
  2. // PRESENTATION: ССЫЛКИ И БИБЛИОГРАФИИ
  3. // ==========================================
  4. #let ensure-label(lbl, scope) = {
  5. if type(lbl) != label {
  6. panic("Scientia / " + scope + ": ожидался label")
  7. }
  8. lbl
  9. }
  10. // В тексте чаще всего нужен предложный падеж, поэтому он используется без
  11. // второго аргумента. Для явного выбора достаточно одной буквы:
  12. // #vref(<figure-label>, "и") или #vref(<figure-label>, "р").
  13. // Прежние сокращения и полные названия сохранены как понятные альтернативы.
  14. #let case-aliases = (
  15. и: "имен",
  16. им: "имен",
  17. имен: "имен",
  18. именительный: "имен",
  19. р: "род",
  20. род: "род",
  21. родительный: "род",
  22. д: "дат",
  23. дат: "дат",
  24. дательный: "дат",
  25. в: "вин",
  26. вин: "вин",
  27. винительный: "вин",
  28. т: "тв",
  29. тв: "тв",
  30. творительный: "тв",
  31. п: "предл",
  32. пр: "предл",
  33. предл: "предл",
  34. предложный: "предл",
  35. )
  36. #let ensure-case(value) = {
  37. let normalized = if type(value) == str {
  38. case-aliases.at(value, default: none)
  39. } else {
  40. none
  41. }
  42. if normalized == none {
  43. panic(
  44. "Scientia / reference: неизвестный падеж " + repr(value)
  45. + "; используйте и, р, д, в, т или п",
  46. )
  47. }
  48. normalized
  49. }
  50. #let resolve-ref-element(lbl) = {
  51. let lbl = ensure-label(lbl, "reference")
  52. let elems = query(selector(lbl))
  53. if elems == () { none } else { elems.first() }
  54. }
  55. #let classify-ref-element(el) = {
  56. if el.func() == figure {
  57. if el.kind == table { "table" } else { "image" }
  58. } else if el.func() == math.equation {
  59. "equation"
  60. } else if el.func() == heading {
  61. "section"
  62. } else {
  63. "other"
  64. }
  65. }
  66. #let noun-forms = (
  67. image: (
  68. singular: (имен: "рисунок", род: "рисунка", дат: "рисунку", вин: "рисунок", тв: "рисунком", предл: "рисунке"),
  69. plural: (имен: "рисунки", род: "рисунков", дат: "рисункам", вин: "рисунки", тв: "рисунками", предл: "рисунках"),
  70. ),
  71. table: (
  72. singular: (имен: "таблица", род: "таблицы", дат: "таблице", вин: "таблицу", тв: "таблицей", предл: "таблице"),
  73. plural: (имен: "таблицы", род: "таблиц", дат: "таблицам", вин: "таблицы", тв: "таблицами", предл: "таблицах"),
  74. ),
  75. equation: (
  76. singular: (имен: "формула", род: "формулы", дат: "формуле", вин: "формулу", тв: "формулой", предл: "формуле"),
  77. plural: (имен: "формулы", род: "формул", дат: "формулам", вин: "формулы", тв: "формулами", предл: "формулах"),
  78. ),
  79. section: (
  80. singular: (имен: "раздел", род: "раздела", дат: "разделу", вин: "раздел", тв: "разделом", предл: "разделе"),
  81. plural: (имен: "разделы", род: "разделов", дат: "разделам", вин: "разделы", тв: "разделами", предл: "разделах"),
  82. ),
  83. other: (
  84. singular: (имен: "элемент", род: "элемента", дат: "элементу", вин: "элемент", тв: "элементом", предл: "элементе"),
  85. plural: (имен: "элементы", род: "элементов", дат: "элементам", вин: "элементы", тв: "элементами", предл: "элементах"),
  86. ),
  87. )
  88. #let noun-for-case(kind, grammatical-case, plural: false, capitalized: false) = {
  89. let grammatical-case = ensure-case(grammatical-case)
  90. let forms = noun-forms.at(kind, default: noun-forms.other)
  91. let number-forms = if plural { forms.plural } else { forms.singular }
  92. let noun = number-forms.at(grammatical-case)
  93. if capitalized {
  94. let characters = noun.clusters()
  95. upper(characters.first()) + characters.slice(1).join()
  96. } else {
  97. noun
  98. }
  99. }
  100. #let render-resolved-ref(lbl, grammatical-case, capitalized: false) = {
  101. let el = resolve-ref-element(lbl)
  102. if el == none {
  103. text(fill: red)[Ссылка не найдена]
  104. } else {
  105. let kind = classify-ref-element(el)
  106. let prefix = noun-for-case(kind, grammatical-case, capitalized: capitalized)
  107. if kind == "equation" {
  108. [#prefix #ref(lbl, supplement: none)]
  109. } else {
  110. ref(lbl, supplement: prefix)
  111. }
  112. }
  113. }
  114. #let join-reference-items(items, conjunction) = {
  115. if items.len() == 1 {
  116. items.first()
  117. } else if items.len() == 2 {
  118. [#items.first() #conjunction #items.last()]
  119. } else {
  120. for (index, item) in items.enumerate() {
  121. if index > 0 {
  122. if index == items.len() - 1 { [ #conjunction ] } else { [, ] }
  123. }
  124. item
  125. }
  126. }
  127. }
  128. #let parse-reference-options(options, scope, allow-conjunction: false) = {
  129. let positional = options.pos()
  130. let named = options.named()
  131. let allowed = if allow-conjunction {
  132. ("grammatical-case", "capitalized", "conjunction")
  133. } else {
  134. ("grammatical-case", "capitalized")
  135. }
  136. for key in named.keys() {
  137. if key not in allowed {
  138. panic("Scientia / " + scope + ": неизвестный параметр " + repr(key))
  139. }
  140. }
  141. if positional.len() > 1 {
  142. panic("Scientia / " + scope + ": после label допустим только один позиционный падеж")
  143. }
  144. if positional.len() == 1 and "grammatical-case" in named {
  145. panic("Scientia / " + scope + ": падеж задан одновременно позиционно и по имени")
  146. }
  147. let grammatical-case = if positional.len() == 1 {
  148. positional.first()
  149. } else {
  150. named.at("grammatical-case", default: "п")
  151. }
  152. let capitalized = named.at("capitalized", default: true)
  153. if type(capitalized) != bool {
  154. panic("Scientia / " + scope + ".capitalized: ожидалось bool")
  155. }
  156. (
  157. grammatical-case: ensure-case(grammatical-case),
  158. capitalized: capitalized,
  159. )
  160. }
  161. #let vref(lbl, ..options) = context {
  162. let parsed = parse-reference-options(options, "vref")
  163. render-resolved-ref(
  164. lbl,
  165. parsed.grammatical-case,
  166. capitalized: parsed.capitalized,
  167. )
  168. }
  169. #let vrefs(labels, ..options) = context {
  170. if type(labels) != array or labels.len() == 0 {
  171. panic("Scientia / vrefs: нужен непустой array labels")
  172. }
  173. let parsed = parse-reference-options(options, "vrefs", allow-conjunction: true)
  174. let grammatical-case = parsed.grammatical-case
  175. let capitalized = parsed.capitalized
  176. let conjunction = options.named().at("conjunction", default: [и])
  177. for item in labels { let _ = ensure-label(item, "vrefs") }
  178. let resolved = labels.map(resolve-ref-element)
  179. let groupable = resolved.all(el => el != none) and resolved.len() > 1
  180. if groupable {
  181. let kind = classify-ref-element(resolved.first())
  182. let same-kind = resolved.all(el => classify-ref-element(el) == kind)
  183. if same-kind {
  184. let prefix = noun-for-case(
  185. kind,
  186. grammatical-case,
  187. plural: true,
  188. capitalized: capitalized,
  189. )
  190. let rendered = labels.map(lbl => ref(lbl, supplement: none))
  191. [#prefix #join-reference-items(rendered, conjunction)]
  192. } else {
  193. let rendered = labels.map(lbl => render-resolved-ref(lbl, grammatical-case, capitalized: capitalized))
  194. join-reference-items(rendered, conjunction)
  195. }
  196. } else {
  197. let rendered = labels.map(lbl => render-resolved-ref(lbl, grammatical-case, capitalized: capitalized))
  198. join-reference-items(rendered, conjunction)
  199. }
  200. }
  201. #let eqref(lbl) = context {
  202. let el = resolve-ref-element(lbl)
  203. if el == none {
  204. text(fill: red)[Ссылка не найдена]
  205. } else if classify-ref-element(el) != "equation" {
  206. text(fill: red)[Метка не является формулой]
  207. } else {
  208. ref(lbl, supplement: none)
  209. }
  210. }
  211. #let render-bibliographies(sections) = {
  212. // Typst 0.15: у каждой bibliography собственный heading. Смещение уровня
  213. // отделяет заголовок библиографии от заголовков отчёта и устраняет конфликт
  214. // нумерации/outline при нескольких независимых списках.
  215. show bibliography: set heading(offset: 1)
  216. for (index, section) in sections.enumerate() {
  217. if index > 0 and section.at("page-break", default: true) {
  218. pagebreak()
  219. }
  220. bibliography(
  221. section.sources,
  222. title: section.title,
  223. full: section.full,
  224. style: section.style,
  225. target: section.target,
  226. group: section.group,
  227. )
  228. }
  229. }