| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- // ==========================================
- // 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: [],
- ),
- )
- #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,
- outer-indent: 0pt,
- level-indent: 0.55cm,
- body-indent: 0.3em,
- 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,
- )
- block(width: 100%)[
- #set enum(
- full: true,
- numbering: numbering,
- indent: level-indent,
- body-indent: body-indent,
- spacing: item-spacing,
- number-align: number-align,
- )
- // Важно применять параметры абзаца ко всему 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]
- ]
- }
|