Initial commit

This commit is contained in:
tkachenko committed 2026-10-09 01:45:17 +00:00
commit 41d230d524
163 files changed
+11500

No files matched your search

+247
View File
@@ -0,0 +1,247 @@
// ==========================================
// 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,
)
}
}