components.typ 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // ==========================================
  2. // ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ
  3. // ==========================================
  4. // Корпоративная таблица с оранжевой шапкой.
  5. // Для простых таблиц первая строка по-прежнему считается шапкой.
  6. // Для сложной шапки используйте `header: (...)`.
  7. #let corp_table(
  8. columns: auto,
  9. table-align: center, // center | left | right
  10. row_breakable: true,
  11. page_break: "fit",
  12. text-size: 12pt,
  13. leading: 0.65em,
  14. header-leading: auto,
  15. body-leading: auto,
  16. spacing: auto,
  17. header-spacing: auto,
  18. body-spacing: auto,
  19. justify: auto,
  20. first-line-indent: 0pt,
  21. hyphenate: auto,
  22. header-hyphenate: auto,
  23. body-hyphenate: auto,
  24. inset: 0% + 5pt,
  25. header-inset: auto,
  26. body-inset: auto,
  27. header: auto,
  28. body: auto,
  29. repeat_header: true,
  30. header_fill: rgb("ffd35f"),
  31. continuation: true,
  32. continuation_text: "Продолжение таблицы",
  33. continuation_with_number: true,
  34. continuation_gap: 0.5em,
  35. ..args,
  36. ) = {
  37. let resolve-table-align = value => {
  38. if value == left or value == "left" {
  39. left
  40. } else if value == right or value == "right" {
  41. right
  42. } else {
  43. center
  44. }
  45. }
  46. let block-align = resolve-table-align(table-align)
  47. let cols_count = if type(columns) == int {
  48. columns
  49. } else if type(columns) == array {
  50. columns.len()
  51. } else {
  52. 1
  53. }
  54. let pos_args = args.pos()
  55. let named_args = args.named()
  56. let normalize-cells = value => {
  57. if value == auto or value == none {
  58. ()
  59. } else if type(value) == array {
  60. value
  61. } else {
  62. (value,)
  63. }
  64. }
  65. let span-of = (item, key) => {
  66. if type(item) == content and item.func() == table.cell {
  67. item.at(key, default: 1)
  68. } else {
  69. 1
  70. }
  71. }
  72. let decay-carries = carries => range(carries.len()).map(i => calc.max(carries.at(i) - 1, 0))
  73. let advance-slot(row, col, carries) = {
  74. if col >= cols_count {
  75. advance-slot(row + 1, 0, decay-carries(carries))
  76. } else if carries.at(col) > 0 {
  77. advance-slot(row, col + 1, carries)
  78. } else {
  79. (row, col, carries)
  80. }
  81. }
  82. let update-carries = (carries, col, colspan, rowspan) => {
  83. range(carries.len()).map(i => if i >= col and i < col + colspan { calc.max(carries.at(i), rowspan) } else { carries.at(i) })
  84. }
  85. let update-filled = (filled, row, colspan, rowspan) => {
  86. range(filled.len()).map(i => if i >= row and i < row + rowspan { filled.at(i) + colspan } else { filled.at(i) })
  87. }
  88. let rows-filled(filled, row-count) = {
  89. if row-count <= 0 {
  90. true
  91. } else if filled.at(row-count - 1) == cols_count {
  92. rows-filled(filled, row-count - 1)
  93. } else {
  94. false
  95. }
  96. }
  97. let infer-header-rows(cells, idx: 0, row: 0, col: 0, carries: range(cols_count).map(_ => 0), max-row: -1) = {
  98. if idx >= cells.len() {
  99. calc.max(max-row + 1, 0)
  100. } else {
  101. let item = cells.at(idx)
  102. let slot = advance-slot(row, col, carries)
  103. let place-row = slot.at(0)
  104. let place-col = slot.at(1)
  105. let place-carries = slot.at(2)
  106. let colspan = span-of(item, "colspan")
  107. let rowspan = span-of(item, "rowspan")
  108. let next-carries = update-carries(place-carries, place-col, colspan, rowspan)
  109. infer-header-rows(
  110. cells,
  111. idx: idx + 1,
  112. row: place-row,
  113. col: place-col + colspan,
  114. carries: next-carries,
  115. max-row: calc.max(max-row, place-row + rowspan - 1),
  116. )
  117. }
  118. }
  119. let infer-auto-header-len(
  120. cells,
  121. idx: 0,
  122. row: 0,
  123. col: 0,
  124. carries: range(cols_count).map(_ => 0),
  125. filled: none,
  126. first-row-max: 1,
  127. header-rows: none,
  128. ) = {
  129. let filled = if filled == none { range(calc.max(cells.len(), 1) + 2).map(_ => 0) } else { filled }
  130. if idx >= cells.len() {
  131. cells.len()
  132. } else {
  133. let item = cells.at(idx)
  134. let slot = advance-slot(row, col, carries)
  135. let place-row = slot.at(0)
  136. let place-col = slot.at(1)
  137. let place-carries = slot.at(2)
  138. let colspan = span-of(item, "colspan")
  139. let rowspan = span-of(item, "rowspan")
  140. let next-carries = update-carries(place-carries, place-col, colspan, rowspan)
  141. let next-filled = update-filled(filled, place-row, colspan, rowspan)
  142. let next-first-row-max = if place-row == 0 { calc.max(first-row-max, rowspan) } else { first-row-max }
  143. let next-header-rows = if header-rows == none and next-filled.at(0) == cols_count { next-first-row-max } else { header-rows }
  144. if next-header-rows != none and rows-filled(next-filled, next-header-rows) {
  145. idx + 1
  146. } else {
  147. infer-auto-header-len(
  148. cells,
  149. idx: idx + 1,
  150. row: place-row,
  151. col: place-col + colspan,
  152. carries: next-carries,
  153. filled: next-filled,
  154. first-row-max: next-first-row-max,
  155. header-rows: next-header-rows,
  156. )
  157. }
  158. }
  159. }
  160. let header_cells = if header == auto {
  161. let h_len = infer-auto-header-len(pos_args)
  162. pos_args.slice(0, h_len)
  163. } else {
  164. normalize-cells(header)
  165. }
  166. let body_cells = if body == auto {
  167. if header == auto {
  168. pos_args.slice(header_cells.len())
  169. } else {
  170. pos_args
  171. }
  172. } else {
  173. normalize-cells(body)
  174. }
  175. let header_row_count = if header_cells.len() > 0 { infer-header-rows(header_cells) } else { 0 }
  176. let has_header = header_row_count > 0
  177. if "align" not in named_args {
  178. // Центрирование самой таблицы не должно наследоваться текстом ячеек.
  179. // Шапка по умолчанию центрируется, содержимое читается слева направо.
  180. named_args.insert(
  181. "align",
  182. (x, y) => if has_header and y >= 1 and y <= header_row_count {
  183. center + horizon
  184. } else {
  185. left + top
  186. },
  187. )
  188. }
  189. if type(row_breakable) != bool {
  190. panic("Scientia / corp-table.row-breakable: ожидался bool")
  191. }
  192. let cell_breakable = if type(page_break) == bool {
  193. page_break
  194. } else if page_break == "fit" {
  195. row_breakable
  196. } else if page_break == "cell" {
  197. false
  198. } else {
  199. panic("Scientia / corp-table.page-break: ожидались bool, 'fit' или 'cell'")
  200. }
  201. let resolved-header-leading = if header-leading == auto { leading } else { header-leading }
  202. let resolved-body-leading = if body-leading == auto { leading } else { body-leading }
  203. let resolved-header-spacing = if header-spacing == auto { spacing } else { header-spacing }
  204. let resolved-body-spacing = if body-spacing == auto { spacing } else { body-spacing }
  205. let resolved-header-hyphenate = if header-hyphenate == auto { hyphenate } else { header-hyphenate }
  206. let resolved-body-hyphenate = if body-hyphenate == auto { hyphenate } else { body-hyphenate }
  207. let resolved-header-inset = if header-inset == auto { inset } else { header-inset }
  208. let resolved-body-inset = if body-inset == auto { inset } else { body-inset }
  209. let cont_cell = table.cell(colspan: cols_count, stroke: none, fill: none, align: left, inset: 0pt)[
  210. #context {
  211. if not continuation {
  212. v(0pt)
  213. } else {
  214. let current_page = counter(page).get().first()
  215. let tables_before = query(selector(table).before(here()))
  216. let start_page = if tables_before == () {
  217. current_page
  218. } else {
  219. counter(page).at(tables_before.last().location()).first()
  220. }
  221. if current_page > start_page {
  222. let continuation_label = if type(continuation_text) == str {
  223. text(continuation_text.replace(" ", " "))
  224. } else {
  225. continuation_text
  226. }
  227. let continuation_title = if continuation_with_number {
  228. [#continuation_label #counter(figure.where(kind: table)).display()]
  229. } else {
  230. continuation_label
  231. }
  232. set text(weight: "regular", size: text-size, hyphenate: false)
  233. pad(bottom: continuation_gap)[#box(continuation_title)]
  234. } else {
  235. v(0pt)
  236. }
  237. }
  238. }
  239. ]
  240. set text(size: text-size)
  241. set table.cell(breakable: cell_breakable)
  242. show table.cell: it => {
  243. let is-header = has_header and it.y >= 1 and it.y <= header_row_count
  244. let current-leading = if is-header { resolved-header-leading } else { resolved-body-leading }
  245. let current-spacing = if is-header { resolved-header-spacing } else { resolved-body-spacing }
  246. let current-hyphenate = if is-header { resolved-header-hyphenate } else { resolved-body-hyphenate }
  247. // Важно: block(width:100%, it) ломает align: ... + horizon (вертикальное центрирование).
  248. // Решение: возвращаем `it` напрямую, а все set-правила применяем через вложенные if-else,
  249. // где `it` находится ВНУТРИ той же ветки, что и set — иначе set не распространяется на it.
  250. // spacing применяется снаружи через замыкание, чтобы не дублировать внутренние ветки.
  251. set par(leading: current-leading)
  252. let styled-it = () => {
  253. if is-header {
  254. set par(first-line-indent: 0pt)
  255. set text(weight: "bold")
  256. if justify != auto {
  257. set par(justify: justify)
  258. if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
  259. } else if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
  260. } else if first-line-indent != auto {
  261. set par(first-line-indent: first-line-indent)
  262. if justify != auto {
  263. set par(justify: justify)
  264. if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
  265. } else if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
  266. } else {
  267. if justify != auto {
  268. set par(justify: justify)
  269. if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
  270. } else if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
  271. }
  272. }
  273. if current-spacing != auto {
  274. set par(spacing: current-spacing)
  275. styled-it()
  276. } else {
  277. styled-it()
  278. }
  279. }
  280. align(block-align)[
  281. #table(
  282. columns: columns,
  283. stroke: 0.5pt,
  284. fill: (x, y) => if has_header and y >= 1 and y <= header_row_count { header_fill } else { none },
  285. inset: (x, y) => if has_header and y >= 1 and y <= header_row_count { resolved-header-inset } else { resolved-body-inset },
  286. ..named_args,
  287. ..if has_header {
  288. (
  289. table.header(
  290. repeat: repeat_header,
  291. cont_cell,
  292. ..header_cells,
  293. ),
  294. )
  295. } else {
  296. ()
  297. },
  298. ..body_cells
  299. )
  300. ]
  301. }
  302. // Объект формулы по ГОСТ:
  303. // центрирование формулы и номер справа настраиваются глобально в report.typ.
  304. // Использование: #formula($ ... $) <eq_label>
  305. #let formula(body, ..args) = math.equation(
  306. body,
  307. block: true,
  308. ..args,
  309. )
  310. // Пояснения «где …» после формулы (ГОСТ): без красной строки, с висячим
  311. // отступом, чтобы обозначения величин выстраивались в столбец.
  312. // Использование: #где[где $l_0$ — пролет в свету; …]
  313. #let где(body) = par(first-line-indent: 0pt, hanging-indent: 1.75em, body)