350 lines
10 KiB
Typst
350 lines
10 KiB
Typst
// ==========================================
|
||
// 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]
|
||
]
|
||
}
|