| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- // ==========================================
- // ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ
- // ==========================================
- // Корпоративная таблица с оранжевой шапкой.
- // Для простых таблиц первая строка по-прежнему считается шапкой.
- // Для сложной шапки используйте `header: (...)`.
- #let corp_table(
- columns: auto,
- table-align: center, // center | left | right
- row_breakable: true,
- page_break: "fit",
- text-size: 12pt,
- leading: 0.65em,
- header-leading: auto,
- body-leading: auto,
- spacing: auto,
- header-spacing: auto,
- body-spacing: auto,
- justify: auto,
- first-line-indent: 0pt,
- hyphenate: auto,
- header-hyphenate: auto,
- body-hyphenate: auto,
- inset: 0% + 5pt,
- header-inset: auto,
- body-inset: auto,
- header: auto,
- body: auto,
- repeat_header: true,
- header_fill: rgb("ffd35f"),
- continuation: true,
- continuation_text: "Продолжение таблицы",
- continuation_with_number: true,
- continuation_gap: 0.5em,
- ..args,
- ) = {
- let resolve-table-align = value => {
- if value == left or value == "left" {
- left
- } else if value == right or value == "right" {
- right
- } else {
- center
- }
- }
- let block-align = resolve-table-align(table-align)
- let cols_count = if type(columns) == int {
- columns
- } else if type(columns) == array {
- columns.len()
- } else {
- 1
- }
- let pos_args = args.pos()
- let named_args = args.named()
- let normalize-cells = value => {
- if value == auto or value == none {
- ()
- } else if type(value) == array {
- value
- } else {
- (value,)
- }
- }
- let span-of = (item, key) => {
- if type(item) == content and item.func() == table.cell {
- item.at(key, default: 1)
- } else {
- 1
- }
- }
- let decay-carries = carries => range(carries.len()).map(i => calc.max(carries.at(i) - 1, 0))
- let advance-slot(row, col, carries) = {
- if col >= cols_count {
- advance-slot(row + 1, 0, decay-carries(carries))
- } else if carries.at(col) > 0 {
- advance-slot(row, col + 1, carries)
- } else {
- (row, col, carries)
- }
- }
- let update-carries = (carries, col, colspan, rowspan) => {
- range(carries.len()).map(i => if i >= col and i < col + colspan { calc.max(carries.at(i), rowspan) } else { carries.at(i) })
- }
- let update-filled = (filled, row, colspan, rowspan) => {
- range(filled.len()).map(i => if i >= row and i < row + rowspan { filled.at(i) + colspan } else { filled.at(i) })
- }
- let rows-filled(filled, row-count) = {
- if row-count <= 0 {
- true
- } else if filled.at(row-count - 1) == cols_count {
- rows-filled(filled, row-count - 1)
- } else {
- false
- }
- }
- let infer-header-rows(cells, idx: 0, row: 0, col: 0, carries: range(cols_count).map(_ => 0), max-row: -1) = {
- if idx >= cells.len() {
- calc.max(max-row + 1, 0)
- } else {
- let item = cells.at(idx)
- let slot = advance-slot(row, col, carries)
- let place-row = slot.at(0)
- let place-col = slot.at(1)
- let place-carries = slot.at(2)
- let colspan = span-of(item, "colspan")
- let rowspan = span-of(item, "rowspan")
- let next-carries = update-carries(place-carries, place-col, colspan, rowspan)
- infer-header-rows(
- cells,
- idx: idx + 1,
- row: place-row,
- col: place-col + colspan,
- carries: next-carries,
- max-row: calc.max(max-row, place-row + rowspan - 1),
- )
- }
- }
- let infer-auto-header-len(
- cells,
- idx: 0,
- row: 0,
- col: 0,
- carries: range(cols_count).map(_ => 0),
- filled: none,
- first-row-max: 1,
- header-rows: none,
- ) = {
- let filled = if filled == none { range(calc.max(cells.len(), 1) + 2).map(_ => 0) } else { filled }
- if idx >= cells.len() {
- cells.len()
- } else {
- let item = cells.at(idx)
- let slot = advance-slot(row, col, carries)
- let place-row = slot.at(0)
- let place-col = slot.at(1)
- let place-carries = slot.at(2)
- let colspan = span-of(item, "colspan")
- let rowspan = span-of(item, "rowspan")
- let next-carries = update-carries(place-carries, place-col, colspan, rowspan)
- let next-filled = update-filled(filled, place-row, colspan, rowspan)
- let next-first-row-max = if place-row == 0 { calc.max(first-row-max, rowspan) } else { first-row-max }
- let next-header-rows = if header-rows == none and next-filled.at(0) == cols_count { next-first-row-max } else { header-rows }
- if next-header-rows != none and rows-filled(next-filled, next-header-rows) {
- idx + 1
- } else {
- infer-auto-header-len(
- cells,
- idx: idx + 1,
- row: place-row,
- col: place-col + colspan,
- carries: next-carries,
- filled: next-filled,
- first-row-max: next-first-row-max,
- header-rows: next-header-rows,
- )
- }
- }
- }
- let header_cells = if header == auto {
- let h_len = infer-auto-header-len(pos_args)
- pos_args.slice(0, h_len)
- } else {
- normalize-cells(header)
- }
- let body_cells = if body == auto {
- if header == auto {
- pos_args.slice(header_cells.len())
- } else {
- pos_args
- }
- } else {
- normalize-cells(body)
- }
- let header_row_count = if header_cells.len() > 0 { infer-header-rows(header_cells) } else { 0 }
- let has_header = header_row_count > 0
- if "align" not in named_args {
- // Центрирование самой таблицы не должно наследоваться текстом ячеек.
- // Шапка по умолчанию центрируется, содержимое читается слева направо.
- named_args.insert(
- "align",
- (x, y) => if has_header and y >= 1 and y <= header_row_count {
- center + horizon
- } else {
- left + top
- },
- )
- }
- if type(row_breakable) != bool {
- panic("Scientia / corp-table.row-breakable: ожидался bool")
- }
- let cell_breakable = if type(page_break) == bool {
- page_break
- } else if page_break == "fit" {
- row_breakable
- } else if page_break == "cell" {
- false
- } else {
- panic("Scientia / corp-table.page-break: ожидались bool, 'fit' или 'cell'")
- }
- let resolved-header-leading = if header-leading == auto { leading } else { header-leading }
- let resolved-body-leading = if body-leading == auto { leading } else { body-leading }
- let resolved-header-spacing = if header-spacing == auto { spacing } else { header-spacing }
- let resolved-body-spacing = if body-spacing == auto { spacing } else { body-spacing }
- let resolved-header-hyphenate = if header-hyphenate == auto { hyphenate } else { header-hyphenate }
- let resolved-body-hyphenate = if body-hyphenate == auto { hyphenate } else { body-hyphenate }
- let resolved-header-inset = if header-inset == auto { inset } else { header-inset }
- let resolved-body-inset = if body-inset == auto { inset } else { body-inset }
- let cont_cell = table.cell(colspan: cols_count, stroke: none, fill: none, align: left, inset: 0pt)[
- #context {
- if not continuation {
- v(0pt)
- } else {
- let current_page = counter(page).get().first()
- let tables_before = query(selector(table).before(here()))
- let start_page = if tables_before == () {
- current_page
- } else {
- counter(page).at(tables_before.last().location()).first()
- }
- if current_page > start_page {
- let continuation_label = if type(continuation_text) == str {
- text(continuation_text.replace(" ", " "))
- } else {
- continuation_text
- }
- let continuation_title = if continuation_with_number {
- [#continuation_label #counter(figure.where(kind: table)).display()]
- } else {
- continuation_label
- }
- set text(weight: "regular", size: text-size, hyphenate: false)
- pad(bottom: continuation_gap)[#box(continuation_title)]
- } else {
- v(0pt)
- }
- }
- }
- ]
- set text(size: text-size)
- set table.cell(breakable: cell_breakable)
- show table.cell: it => {
- let is-header = has_header and it.y >= 1 and it.y <= header_row_count
- let current-leading = if is-header { resolved-header-leading } else { resolved-body-leading }
- let current-spacing = if is-header { resolved-header-spacing } else { resolved-body-spacing }
- let current-hyphenate = if is-header { resolved-header-hyphenate } else { resolved-body-hyphenate }
- // Важно: block(width:100%, it) ломает align: ... + horizon (вертикальное центрирование).
- // Решение: возвращаем `it` напрямую, а все set-правила применяем через вложенные if-else,
- // где `it` находится ВНУТРИ той же ветки, что и set — иначе set не распространяется на it.
- // spacing применяется снаружи через замыкание, чтобы не дублировать внутренние ветки.
- set par(leading: current-leading)
- let styled-it = () => {
- if is-header {
- set par(first-line-indent: 0pt)
- set text(weight: "bold")
- if justify != auto {
- set par(justify: justify)
- if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
- } else if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
- } else if first-line-indent != auto {
- set par(first-line-indent: first-line-indent)
- if justify != auto {
- set par(justify: justify)
- if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
- } else if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
- } else {
- if justify != auto {
- set par(justify: justify)
- if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
- } else if current-hyphenate != auto { set text(hyphenate: current-hyphenate); it } else { it }
- }
- }
- if current-spacing != auto {
- set par(spacing: current-spacing)
- styled-it()
- } else {
- styled-it()
- }
- }
- align(block-align)[
- #table(
- columns: columns,
- stroke: 0.5pt,
- fill: (x, y) => if has_header and y >= 1 and y <= header_row_count { header_fill } else { none },
- inset: (x, y) => if has_header and y >= 1 and y <= header_row_count { resolved-header-inset } else { resolved-body-inset },
- ..named_args,
- ..if has_header {
- (
- table.header(
- repeat: repeat_header,
- cont_cell,
- ..header_cells,
- ),
- )
- } else {
- ()
- },
- ..body_cells
- )
- ]
- }
- // Объект формулы по ГОСТ:
- // центрирование формулы и номер справа настраиваются глобально в report.typ.
- // Использование: #formula($ ... $) <eq_label>
- #let formula(body, ..args) = math.equation(
- body,
- block: true,
- ..args,
- )
- // Пояснения «где …» после формулы (ГОСТ): без красной строки, с висячим
- // отступом, чтобы обозначения величин выстраивались в столбец.
- // Использование: #где[где $l_0$ — пролет в свету; …]
- #let где(body) = par(first-line-indent: 0pt, hanging-indent: 1.75em, body)
|