// ========================================== // PRESENTATION: ССЫЛКИ И БИБЛИОГРАФИИ // ========================================== #let ensure-label(lbl, scope) = { if type(lbl) != label { panic("Scientia / " + scope + ": ожидался label") } lbl } // В тексте чаще всего нужен предложный падеж, поэтому он используется без // второго аргумента. Для явного выбора достаточно одной буквы: // #vref(, "и") или #vref(, "р"). // Прежние сокращения и полные названия сохранены как понятные альтернативы. #let case-aliases = ( и: "имен", им: "имен", имен: "имен", именительный: "имен", р: "род", род: "род", родительный: "род", д: "дат", дат: "дат", дательный: "дат", в: "вин", вин: "вин", винительный: "вин", т: "тв", тв: "тв", творительный: "тв", п: "предл", пр: "предл", предл: "предл", предложный: "предл", ) #let ensure-case(value) = { let normalized = if type(value) == str { case-aliases.at(value, default: none) } else { none } if normalized == none { panic( "Scientia / reference: неизвестный падеж " + repr(value) + "; используйте и, р, д, в, т или п", ) } normalized } #let resolve-ref-element(lbl) = { let lbl = ensure-label(lbl, "reference") let elems = query(selector(lbl)) if elems == () { none } else { elems.first() } } #let classify-ref-element(el) = { if el.func() == figure { if el.kind == table { "table" } else { "image" } } else if el.func() == math.equation { "equation" } else if el.func() == heading { if el.supplement == [Приложение] { "attachment" } else { "section" } } else { "other" } } #let noun-forms = ( image: ( singular: (имен: "рисунок", род: "рисунка", дат: "рисунку", вин: "рисунок", тв: "рисунком", предл: "рисунке"), plural: (имен: "рисунки", род: "рисунков", дат: "рисункам", вин: "рисунки", тв: "рисунками", предл: "рисунках"), ), table: ( singular: (имен: "таблица", род: "таблицы", дат: "таблице", вин: "таблицу", тв: "таблицей", предл: "таблице"), plural: (имен: "таблицы", род: "таблиц", дат: "таблицам", вин: "таблицы", тв: "таблицами", предл: "таблицах"), ), equation: ( singular: (имен: "формула", род: "формулы", дат: "формуле", вин: "формулу", тв: "формулой", предл: "формуле"), plural: (имен: "формулы", род: "формул", дат: "формулам", вин: "формулы", тв: "формулами", предл: "формулах"), ), section: ( singular: (имен: "раздел", род: "раздела", дат: "разделу", вин: "раздел", тв: "разделом", предл: "разделе"), plural: (имен: "разделы", род: "разделов", дат: "разделам", вин: "разделы", тв: "разделами", предл: "разделах"), ), attachment: ( singular: (имен: "приложение", род: "приложения", дат: "приложению", вин: "приложение", тв: "приложением", предл: "приложении"), plural: (имен: "приложения", род: "приложений", дат: "приложениям", вин: "приложения", тв: "приложениями", предл: "приложениях"), ), other: ( singular: (имен: "элемент", род: "элемента", дат: "элементу", вин: "элемент", тв: "элементом", предл: "элементе"), plural: (имен: "элементы", род: "элементов", дат: "элементам", вин: "элементы", тв: "элементами", предл: "элементах"), ), ) #let noun-for-case(kind, grammatical-case, plural: false, capitalized: false) = { let grammatical-case = ensure-case(grammatical-case) let forms = noun-forms.at(kind, default: noun-forms.other) let number-forms = if plural { forms.plural } else { forms.singular } let noun = number-forms.at(grammatical-case) if capitalized { let characters = noun.clusters() upper(characters.first()) + characters.slice(1).join() } else { noun } } #let render-resolved-ref(lbl, grammatical-case, capitalized: false) = { let el = resolve-ref-element(lbl) if el == none { text(fill: red)[Ссылка не найдена] } else { let kind = classify-ref-element(el) let prefix = noun-for-case(kind, grammatical-case, capitalized: capitalized) if kind == "equation" { [#prefix #ref(lbl, supplement: none)] } else { ref(lbl, supplement: prefix) } } } #let join-reference-items(items, conjunction) = { if items.len() == 1 { items.first() } else if items.len() == 2 { [#items.first() #conjunction #items.last()] } else { for (index, item) in items.enumerate() { if index > 0 { if index == items.len() - 1 { [ #conjunction ] } else { [, ] } } item } } } #let parse-reference-options(options, scope, allow-conjunction: false) = { let positional = options.pos() let named = options.named() let allowed = if allow-conjunction { ("grammatical-case", "capitalized", "conjunction") } else { ("grammatical-case", "capitalized") } for key in named.keys() { if key not in allowed { panic("Scientia / " + scope + ": неизвестный параметр " + repr(key)) } } if positional.len() > 1 { panic("Scientia / " + scope + ": после label допустим только один позиционный падеж") } if positional.len() == 1 and "grammatical-case" in named { panic("Scientia / " + scope + ": падеж задан одновременно позиционно и по имени") } let grammatical-case = if positional.len() == 1 { positional.first() } else { named.at("grammatical-case", default: "п") } let capitalized = named.at("capitalized", default: true) if type(capitalized) != bool { panic("Scientia / " + scope + ".capitalized: ожидалось bool") } ( grammatical-case: ensure-case(grammatical-case), capitalized: capitalized, ) } #let vref(lbl, ..options) = context { let parsed = parse-reference-options(options, "vref") render-resolved-ref( lbl, parsed.grammatical-case, capitalized: parsed.capitalized, ) } #let vrefs(labels, ..options) = context { if type(labels) != array or labels.len() == 0 { panic("Scientia / vrefs: нужен непустой array labels") } let parsed = parse-reference-options(options, "vrefs", allow-conjunction: true) let grammatical-case = parsed.grammatical-case let capitalized = parsed.capitalized let conjunction = options.named().at("conjunction", default: [и]) for item in labels { let _ = ensure-label(item, "vrefs") } let resolved = labels.map(resolve-ref-element) let groupable = resolved.all(el => el != none) and resolved.len() > 1 if groupable { let kind = classify-ref-element(resolved.first()) let same-kind = resolved.all(el => classify-ref-element(el) == kind) if same-kind { let prefix = noun-for-case( kind, grammatical-case, plural: true, capitalized: capitalized, ) let rendered = labels.map(lbl => ref(lbl, supplement: none)) [#prefix #join-reference-items(rendered, conjunction)] } else { let rendered = labels.map(lbl => render-resolved-ref(lbl, grammatical-case, capitalized: capitalized)) join-reference-items(rendered, conjunction) } } else { let rendered = labels.map(lbl => render-resolved-ref(lbl, grammatical-case, capitalized: capitalized)) join-reference-items(rendered, conjunction) } } #let eqref(lbl) = context { let el = resolve-ref-element(lbl) if el == none { text(fill: red)[Ссылка не найдена] } else if classify-ref-element(el) != "equation" { text(fill: red)[Метка не является формулой] } else { ref(lbl, supplement: none) } } #let render-bibliographies(sections) = { // Typst 0.15: у каждой bibliography собственный heading. Смещение уровня // отделяет заголовок библиографии от заголовков отчёта и устраняет конфликт // нумерации/outline при нескольких независимых списках. show bibliography: set heading(offset: 1) for (index, section) in sections.enumerate() { if index > 0 and section.at("page-break", default: true) { pagebreak() } bibliography( section.sources, title: section.title, full: section.full, style: section.style, target: section.target, group: section.group, ) } }