lists.typ 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // ==========================================
  2. // PRESENTATION: НАСТРАИВАЕМЫЕ СПИСКИ
  3. // ==========================================
  4. // Кириллическая последовательность по ГОСТ: буквы, которые легко спутать
  5. // с цифрами или другими обозначениями, намеренно пропущены.
  6. #let cyrillic-upper = (
  7. "А", "Б", "В", "Г", "Д", "Е", "Ж", "И", "К", "Л", "М", "Н",
  8. "П", "Р", "С", "Т", "У", "Ф", "Х", "Ц", "Ш", "Щ", "Э", "Ю", "Я",
  9. )
  10. #let cyrillic-lower = cyrillic-upper.map(lower)
  11. #let alphabetic-number(value, alphabet) = {
  12. if type(value) != int or value < 1 {
  13. str(value)
  14. } else {
  15. let current = value
  16. let result = ""
  17. while current > 0 {
  18. current -= 1
  19. result = alphabet.at(calc.rem(current, alphabet.len())) + result
  20. current = calc.floor(current / alphabet.len())
  21. }
  22. result
  23. }
  24. }
  25. #let padded-number(value, width) = {
  26. let raw = str(value)
  27. "0" * calc.max(width - raw.len(), 0) + raw
  28. }
  29. // Расширенное описание одного уровня. В простых случаях достаточно строки
  30. // в levels: "1", "I", "i", "A", "a", "01", "А", "а" или маркера.
  31. #let list-level(style, prefix: [], suffix: auto, width: auto) = (
  32. kind: "list-level",
  33. style: style,
  34. prefix: prefix,
  35. suffix: suffix,
  36. width: width,
  37. )
  38. #let list-scheme(
  39. levels: ("1", "а", "1"),
  40. full: false,
  41. separators: [],
  42. suffixes: (".", ")", ")"),
  43. ) = {
  44. if type(levels) != array or levels.len() == 0 {
  45. panic("Scientia / list-scheme.levels: нужен непустой array")
  46. }
  47. if type(full) != bool {
  48. panic("Scientia / list-scheme.full: ожидался bool")
  49. }
  50. (
  51. kind: "list-scheme",
  52. levels: levels,
  53. full: full,
  54. separators: separators,
  55. suffixes: suffixes,
  56. )
  57. }
  58. #let list-schemes = (
  59. // 1. -> а) -> 1)
  60. gost: list-scheme(),
  61. // 1. -> 1.1. -> 1.1.1.
  62. decimal: list-scheme(
  63. levels: ("1",),
  64. full: true,
  65. separators: ".",
  66. suffixes: ".",
  67. ),
  68. // 1 -> 1.1 -> 1.1.1
  69. decimal-plain: list-scheme(
  70. levels: ("1",),
  71. full: true,
  72. separators: ".",
  73. suffixes: [],
  74. ),
  75. // 1. -> а. -> ‣
  76. local-mixed: list-scheme(
  77. levels: ("1", "а", "‣"),
  78. suffixes: (".", ".", []),
  79. ),
  80. // 1. -> 1.а. -> 1.а.‣
  81. full-mixed: list-scheme(
  82. levels: ("1", "а", "‣"),
  83. full: true,
  84. separators: ".",
  85. suffixes: (".", ".", []),
  86. ),
  87. // A) -> A)1.
  88. legal: list-scheme(
  89. levels: ("A", "1"),
  90. full: true,
  91. separators: ")",
  92. suffixes: (")", "."),
  93. ),
  94. bullets: list-scheme(
  95. levels: ("•", "∙", "‣", "⁃", "◦"),
  96. suffixes: [],
  97. ),
  98. )
  99. #let sequence-value(values, index, scope) = {
  100. let values = if type(values) == array { values } else { (values,) }
  101. if values.len() == 0 {
  102. panic("Scientia / " + scope + ": последовательность не должна быть пустой")
  103. }
  104. values.at(calc.min(index, values.len() - 1))
  105. }
  106. #let resolve-list-scheme(
  107. scheme,
  108. levels: auto,
  109. full: auto,
  110. separators: auto,
  111. suffixes: auto,
  112. ) = {
  113. let base = if type(scheme) == str {
  114. if scheme not in list-schemes {
  115. panic(
  116. "Scientia / numbered-list.scheme: неизвестная схема " + repr(scheme)
  117. + "; доступны " + list-schemes.keys().map(repr).join(", "),
  118. )
  119. }
  120. list-schemes.at(scheme)
  121. } else if type(scheme) == dictionary and scheme.at("kind", default: none) == "list-scheme" {
  122. scheme
  123. } else {
  124. panic("Scientia / numbered-list.scheme: ожидались имя схемы или list-scheme")
  125. }
  126. list-scheme(
  127. levels: if levels == auto { base.levels } else { levels },
  128. full: if full == auto { base.full } else { full },
  129. separators: if separators == auto { base.separators } else { separators },
  130. suffixes: if suffixes == auto { base.suffixes } else { suffixes },
  131. )
  132. }
  133. #let render-level-core(level, value) = {
  134. let description = if type(level) == dictionary and level.at("kind", default: none) == "list-level" {
  135. level
  136. } else {
  137. list-level(level)
  138. }
  139. let style = description.style
  140. let width = description.width
  141. let core = if type(style) == function {
  142. style(value)
  143. } else if style == "1" {
  144. if width == auto { str(value) } else { padded-number(value, width) }
  145. } else if style == "01" {
  146. padded-number(value, if width == auto { 2 } else { width })
  147. } else if style == "I" or style == "i" or style == "A" or style == "a" {
  148. numbering(style, value)
  149. } else if style == "А" {
  150. alphabetic-number(value, cyrillic-upper)
  151. } else if style == "а" {
  152. alphabetic-number(value, cyrillic-lower)
  153. } else if type(style) == str or type(style) == content {
  154. style
  155. } else {
  156. panic("Scientia / list-level.style: ожидались строка, content или function")
  157. }
  158. [#description.prefix#core]
  159. }
  160. #let level-suffix(config, depth) = {
  161. let level = sequence-value(config.levels, depth, "list-scheme.levels")
  162. if type(level) == dictionary and level.at("kind", default: none) == "list-level" and level.suffix != auto {
  163. level.suffix
  164. } else {
  165. sequence-value(config.suffixes, depth, "list-scheme.suffixes")
  166. }
  167. }
  168. #let list-numbering(
  169. scheme: "gost",
  170. levels: auto,
  171. full: auto,
  172. separators: auto,
  173. suffixes: auto,
  174. ) = {
  175. let config = resolve-list-scheme(
  176. scheme,
  177. levels: levels,
  178. full: full,
  179. separators: separators,
  180. suffixes: suffixes,
  181. )
  182. (..numbers) => {
  183. let values = numbers.pos()
  184. let depth = values.len() - 1
  185. let visible = if config.full { range(values.len()) } else { (depth,) }
  186. let parts = ()
  187. for (position, index) in visible.enumerate() {
  188. if position > 0 {
  189. parts.push(sequence-value(config.separators, index - 1, "list-scheme.separators"))
  190. }
  191. let level = sequence-value(config.levels, index, "list-scheme.levels")
  192. parts.push(render-level-core(level, values.at(index)))
  193. }
  194. parts.push(level-suffix(config, depth))
  195. parts.join()
  196. }
  197. }
  198. // Оболочка сохраняет нативный enum: вложенность, переносы страниц,
  199. // многоабзацные пункты и явные начальные номера продолжают работать штатно.
  200. #let numbered-list(
  201. body,
  202. scheme: "gost",
  203. levels: auto,
  204. full: auto,
  205. separators: auto,
  206. suffixes: auto,
  207. outer-indent: 0pt,
  208. level-indent: 0.55cm,
  209. body-indent: 0.3em,
  210. line-leading: 0.55em,
  211. item-spacing: 0.85em,
  212. paragraph-spacing: 0pt,
  213. number-align: end,
  214. ) = {
  215. let numbering = list-numbering(
  216. scheme: scheme,
  217. levels: levels,
  218. full: full,
  219. separators: separators,
  220. suffixes: suffixes,
  221. )
  222. block(width: 100%)[
  223. #set enum(
  224. full: true,
  225. numbering: numbering,
  226. indent: level-indent,
  227. body-indent: body-indent,
  228. spacing: item-spacing,
  229. number-align: number-align,
  230. )
  231. // Важно применять параметры абзаца ко всему enum. Show-правило для
  232. // enum.item повторно контекстуализирует каждый вложенный пункт и в Typst
  233. // 0.15 сбрасывает его локальный счётчик на 1.
  234. #show enum: it => {
  235. set par(
  236. first-line-indent: 0pt,
  237. leading: line-leading,
  238. spacing: paragraph-spacing,
  239. )
  240. it
  241. }
  242. #pad(left: outer-indent)[#body]
  243. ]
  244. }