// ========================================== // ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ // ========================================== // Корпоративная таблица с оранжевой шапкой. // Для простых таблиц первая строка по-прежнему считается шапкой. // Для сложной шапки используйте `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($ ... $) #let formula(body, ..args) = math.equation( body, block: true, ..args, ) // Пояснения «где …» после формулы (ГОСТ): без красной строки, с висячим // отступом, чтобы обозначения величин выстраивались в столбец. // Использование: #где[где $l_0$ — пролет в свету; …] #let где(body) = par(first-line-indent: 0pt, hanging-indent: 1.75em, body)