// ========================================== // 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] ] }