Initial commit

This commit is contained in:
malysheva committed 2026-10-09 04:24:38 +00:00
commit c534d5ce80
163 files changed
+11500

No files matched your search

+369
View File
@@ -0,0 +1,369 @@
// ==========================================
// ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ
// ==========================================
#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,
)