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