Files
test/.template/lib/presentation/references.typ
T
2026-10-09 04:24:38 +00:00

248 lines
9.3 KiB
Typst
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==========================================
// PRESENTATION: ССЫЛКИ И БИБЛИОГРАФИИ
// ==========================================
#let ensure-label(lbl, scope) = {
if type(lbl) != label {
panic("Scientia / " + scope + ": ожидался label")
}
lbl
}
// В тексте чаще всего нужен предложный падеж, поэтому он используется без
// второго аргумента. Для явного выбора достаточно одной буквы:
// #vref(<figure-label>, "и") или #vref(<figure-label>, "р").
// Прежние сокращения и полные названия сохранены как понятные альтернативы.
#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,
)
}
}