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

+349
View File
@@ -0,0 +1,349 @@
// ==========================================
// 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]
]
}