Files
2026-10-09 01:45:17 +00:00

350 lines
10 KiB
Typst
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==========================================
// PRESENTATION: НАСТРАИВАЕМЫЕ СПИСКИ
// ==========================================
// Кириллическая последовательность по ГОСТ: буквы, которые легко спутать
// с цифрами или другими обозначениями, намеренно пропущены.
#let cyrillic-upper = (
"А", "Б", "В", "Г", "Д", "Е", "Ж", "И", "К", "Л", "М", "Н",
"П", "Р", "С", "Т", "У", "Ф", "Х", "Ц", "Ш", "Щ", "Э", "Ю", "Я",
)
#let cyrillic-lower = cyrillic-upper.map(lower)
#let alphabetic-number(value, alphabet) = {
if type(value) != int or value < 1 {
str(value)
} else {
let current = value
let result = ""
while current > 0 {
current -= 1
result = alphabet.at(calc.rem(current, alphabet.len())) + result
current = calc.floor(current / alphabet.len())
}
result
}
}
#let padded-number(value, width) = {
let raw = str(value)
"0" * calc.max(width - raw.len(), 0) + raw
}
// Расширенное описание одного уровня. В простых случаях достаточно строки
// в levels: "1", "I", "i", "A", "a", "01", "А", "а" или маркера.
#let list-level(style, prefix: [], suffix: auto, width: auto) = (
kind: "list-level",
style: style,
prefix: prefix,
suffix: suffix,
width: width,
)
#let list-scheme(
levels: ("1", "а", "1"),
full: false,
separators: [],
suffixes: (".", ")", ")"),
) = {
if type(levels) != array or levels.len() == 0 {
panic("Scientia / list-scheme.levels: нужен непустой array")
}
if type(full) != bool {
panic("Scientia / list-scheme.full: ожидался bool")
}
(
kind: "list-scheme",
levels: levels,
full: full,
separators: separators,
suffixes: suffixes,
)
}
#let list-schemes = (
// 1. -> а) -> 1)
gost: list-scheme(),
// 1. -> 1.1. -> 1.1.1.
decimal: list-scheme(
levels: ("1",),
full: true,
separators: ".",
suffixes: ".",
),
// 1 -> 1.1 -> 1.1.1
decimal-plain: list-scheme(
levels: ("1",),
full: true,
separators: ".",
suffixes: [],
),
// 1. -> а. -> ‣
local-mixed: list-scheme(
levels: ("1", "а", "‣"),
suffixes: (".", ".", []),
),
// 1. -> 1.а. -> 1.а.‣
full-mixed: list-scheme(
levels: ("1", "а", "‣"),
full: true,
separators: ".",
suffixes: (".", ".", []),
),
// A) -> A)1.
legal: list-scheme(
levels: ("A", "1"),
full: true,
separators: ")",
suffixes: (")", "."),
),
bullets: list-scheme(
levels: ("•", "∙", "‣", "⁃", "◦"),
suffixes: [],
),
)
// Короткие варианты геометрии списков. `auto` ничего не переопределяет,
// поэтому список наследует настройки документа или окружающей таблицы.
#let resolve-list-layout(
layout,
kind: "enum",
level-indent: auto,
body-indent: auto,
) = {
let preset = if layout == auto {
(level-indent: auto, body-indent: auto)
} else if layout == "normal" {
if kind == "list" {
(level-indent: 0.7cm, body-indent: 0.5em)
} else {
(level-indent: 0.55cm, body-indent: 0.3em)
}
} else if layout == "compact" {
(level-indent: 0.25cm, body-indent: 0.25em)
} else if layout == "flush" {
(level-indent: 0pt, body-indent: 0.25em)
} else {
panic("Scientia / list-layout: ожидались auto, 'normal', 'compact' или 'flush'")
}
(
level-indent: if level-indent == auto { preset.level-indent } else { level-indent },
body-indent: if body-indent == auto { preset.body-indent } else { body-indent },
)
}
#let sequence-value(values, index, scope) = {
let values = if type(values) == array { values } else { (values,) }
if values.len() == 0 {
panic("Scientia / " + scope + ": последовательность не должна быть пустой")
}
values.at(calc.min(index, values.len() - 1))
}
#let resolve-list-scheme(
scheme,
levels: auto,
full: auto,
separators: auto,
suffixes: auto,
) = {
let base = if type(scheme) == str {
if scheme not in list-schemes {
panic(
"Scientia / numbered-list.scheme: неизвестная схема " + repr(scheme)
+ "; доступны " + list-schemes.keys().map(repr).join(", "),
)
}
list-schemes.at(scheme)
} else if type(scheme) == dictionary and scheme.at("kind", default: none) == "list-scheme" {
scheme
} else {
panic("Scientia / numbered-list.scheme: ожидались имя схемы или list-scheme")
}
list-scheme(
levels: if levels == auto { base.levels } else { levels },
full: if full == auto { base.full } else { full },
separators: if separators == auto { base.separators } else { separators },
suffixes: if suffixes == auto { base.suffixes } else { suffixes },
)
}
#let render-level-core(level, value) = {
let description = if type(level) == dictionary and level.at("kind", default: none) == "list-level" {
level
} else {
list-level(level)
}
let style = description.style
let width = description.width
let core = if type(style) == function {
style(value)
} else if style == "1" {
if width == auto { str(value) } else { padded-number(value, width) }
} else if style == "01" {
padded-number(value, if width == auto { 2 } else { width })
} else if style == "I" or style == "i" or style == "A" or style == "a" {
numbering(style, value)
} else if style == "А" {
alphabetic-number(value, cyrillic-upper)
} else if style == "а" {
alphabetic-number(value, cyrillic-lower)
} else if type(style) == str or type(style) == content {
style
} else {
panic("Scientia / list-level.style: ожидались строка, content или function")
}
[#description.prefix#core]
}
#let level-suffix(config, depth) = {
let level = sequence-value(config.levels, depth, "list-scheme.levels")
if type(level) == dictionary and level.at("kind", default: none) == "list-level" and level.suffix != auto {
level.suffix
} else {
sequence-value(config.suffixes, depth, "list-scheme.suffixes")
}
}
#let list-numbering(
scheme: "gost",
levels: auto,
full: auto,
separators: auto,
suffixes: auto,
) = {
let config = resolve-list-scheme(
scheme,
levels: levels,
full: full,
separators: separators,
suffixes: suffixes,
)
(..numbers) => {
let values = numbers.pos()
let depth = values.len() - 1
let visible = if config.full { range(values.len()) } else { (depth,) }
let parts = ()
for (position, index) in visible.enumerate() {
if position > 0 {
parts.push(sequence-value(config.separators, index - 1, "list-scheme.separators"))
}
let level = sequence-value(config.levels, index, "list-scheme.levels")
parts.push(render-level-core(level, values.at(index)))
}
parts.push(level-suffix(config, depth))
parts.join()
}
}
// Оболочка сохраняет нативный enum: вложенность, переносы страниц,
// многоабзацные пункты и явные начальные номера продолжают работать штатно.
#let numbered-list(
body,
scheme: "gost",
levels: auto,
full: auto,
separators: auto,
suffixes: auto,
layout: auto,
outer-indent: 0pt,
level-indent: auto,
body-indent: auto,
line-leading: 0.55em,
item-spacing: 0.85em,
paragraph-spacing: 0pt,
number-align: end,
) = {
let numbering = list-numbering(
scheme: scheme,
levels: levels,
full: full,
separators: separators,
suffixes: suffixes,
)
let geometry = resolve-list-layout(
layout,
kind: "enum",
level-indent: level-indent,
body-indent: body-indent,
)
let enum-options = (
full: true,
numbering: numbering,
spacing: item-spacing,
number-align: number-align,
)
if geometry.level-indent != auto {
enum-options.insert("indent", geometry.level-indent)
}
if geometry.body-indent != auto {
enum-options.insert("body-indent", geometry.body-indent)
}
block(width: 100%)[
#set enum(..enum-options)
// Важно применять параметры абзаца ко всему enum. Show-правило для
// enum.item повторно контекстуализирует каждый вложенный пункт и в Typst
// 0.15 сбрасывает его локальный счётчик на 1.
#show enum: it => {
set par(
first-line-indent: 0pt,
leading: line-leading,
spacing: paragraph-spacing,
)
it
}
#pad(left: outer-indent)[#body]
]
}
// Симметричная оболочка для маркированных списков. Без параметров она не
// меняет стиль документа; layout и точные отступы действуют только локально.
#let bullet-list(
body,
layout: auto,
marker: auto,
outer-indent: 0pt,
level-indent: auto,
body-indent: auto,
line-leading: auto,
item-spacing: auto,
paragraph-spacing: auto,
) = {
let geometry = resolve-list-layout(
layout,
kind: "list",
level-indent: level-indent,
body-indent: body-indent,
)
let list-options = (:)
if geometry.level-indent != auto {
list-options.insert("indent", geometry.level-indent)
}
if geometry.body-indent != auto {
list-options.insert("body-indent", geometry.body-indent)
}
if marker != auto {
list-options.insert("marker", marker)
}
if item-spacing != auto {
list-options.insert("spacing", item-spacing)
}
let par-options = (first-line-indent: 0pt)
if line-leading != auto {
par-options.insert("leading", line-leading)
}
if paragraph-spacing != auto {
par-options.insert("spacing", paragraph-spacing)
}
block(width: 100%)[
#set list(..list-options)
#show list: it => {
set par(..par-options)
it
}
#pad(left: outer-indent)[#body]
]
}