370 lines
12 KiB
Typst
370 lines
12 KiB
Typst
// ==========================================
|
||
// ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ
|
||
// ==========================================
|
||
#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,
|
||
)
|