Files
Report-showcase/.template/lib/components.typ
T
2026-10-09 01:45:17 +00:00

370 lines
12 KiB
Typst
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==========================================
// ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ
// ==========================================
#import "presentation/lists.typ": resolve-list-layout
// Корпоративная таблица с оранжевой шапкой.
// Для простых таблиц первая строка по-прежнему считается шапкой.
// Для сложной шапки используйте `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,
list-layout: auto,
list-indent: auto,
list-body-indent: 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 resolved-list-layout = resolve-list-layout(
list-layout,
kind: "list",
level-indent: list-indent,
body-indent: list-body-indent,
)
let resolved-enum-layout = resolve-list-layout(
list-layout,
kind: "enum",
level-indent: list-indent,
body-indent: list-body-indent,
)
let list-options = (:)
let enum-options = (:)
if resolved-list-layout.level-indent != auto {
list-options.insert("indent", resolved-list-layout.level-indent)
}
if resolved-list-layout.body-indent != auto {
list-options.insert("body-indent", resolved-list-layout.body-indent)
}
if resolved-enum-layout.level-indent != auto {
enum-options.insert("indent", resolved-enum-layout.level-indent)
}
if resolved-enum-layout.body-indent != auto {
enum-options.insert("body-indent", resolved-enum-layout.body-indent)
}
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 }
// Настройка принадлежит таблице и наследуется списками в её ячейках.
// Локальная оболочка bullet-list/numbered-list может переопределить её.
set list(..list-options)
set enum(..enum-options)
// Важно: 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,
)