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

+449
View File
@@ -0,0 +1,449 @@
// ==========================================
// ОСНОВНАЯ ФУНКЦИЯ ШАБЛОНА ОТЧЕТА
// ==========================================
#import "presentation/lists.typ": list-numbering
#let report(
// ВАЖНО: позиционный аргумент (body) всегда должен идти ПЕРВЫМ
body,
company_info: "ООО «Скиентия» ИНН 6686148633\nул. Шейнкмана, стр. 9, офис 65\nг. Екатеринбург, 620014, Россия\n+7 (922) 203-24-60 ☏\ninfo@scientia.ru 🖂",
udk: "622.023; 622.271",
director_company: "Директор ООО «Скиентия»",
director_name: "Мусихин А.С.",
director_date: "«23» августа 2025 г.",
sign_image: none,
stamp_image: none,
logo_image: path("../companies/logo.svg"),
is_research: true,
is_intermediate: false,
stage_num: none,
vol_num: none,
title: "МЕСТО ДЛЯ ВВОДА ТЕКСТА",
theme: none,
contract_num: "МЕСТО ДЛЯ ВВОДА ТЕКСТА",
contract_date: "«22» августа 2025",
city: "Екатеринбург",
year: datetime.today().year(),
executors: (),
figure-before: 0.75em,
figure-after: 0.75em,
table-before: 0.75em,
table-after: 0.75em,
figure-caption-before: 0pt,
figure-caption-after: 0pt,
table-caption-before: 0pt,
table-caption-after: 0pt,
show_title_page: true,
show_executors: true,
show_outline: true,
report_mode: "default", // default | draft
watermark: none,
) = {
let resolve-asset-path = resource => {
if type(resource) == str and resource.starts-with("companies/") {
path("../" + resource)
} else {
resource
}
}
let mode = if report_mode == "draft" { "draft" } else { "default" }
let with-signs-and-stamps = mode == "default"
let has-watermark = watermark != none
let watermark-mark = (dx, dy) => place(center + horizon, dx: dx, dy: dy)[
#rotate(35deg)[
#text(
font: "Arial",
size: 40pt,
weight: "bold",
fill: rgb("e8c9c9").transparentize(65%),
)[#watermark]
]
]
let page-foreground = if has-watermark {
[
#watermark-mark(-4.8cm, -8.5cm)
#watermark-mark(0cm, 0cm)
#watermark-mark(4.8cm, 8.5cm)
]
} else {
none
}
let service-title = title => {
v(1.1em, weak: true)
block(sticky: true, width: 100%, {
set par(first-line-indent: 0pt, leading: 0.65em)
set text(weight: "bold", size: 12pt, hyphenate: false)
align(center)[#upper(title)]
})
}
let heading-pre-gap = 1.5em
let heading-to-text-gap = 1.2em
// --- БАЗОВЫЕ НАСТРОЙКИ ---
set document(title: title, author: director_company)
set text(font: "Times New Roman", size: 12pt, lang: "ru")
set page(paper: "a4", margin: (left: 3cm, right: 1.5cm, top: 2cm, bottom: 2cm), foreground: page-foreground)
set par(
justify: true,
first-line-indent: (amount: 1.25cm, all: true),
leading: 0.85em,
// Typst размещает соседние абзацы по отдельному параметру spacing.
// Для этого шрифта 0.85em сохраняет тот же шаг около 18 pt.
spacing: 0.85em,
)
// Табличный текст никогда не наследует красную строку основного текста.
// Правило распространяется и на обычный table, и на corp-table.
show table.cell: set par(first-line-indent: 0pt)
// --- НАСТРОЙКИ СПИСКОВ И ОТСТУПОВ ---
// Слабые отступы схлопываются на границе страницы и между соседними
// структурными блоками. Поэтому здесь нет состояния, которое могло бы
// протечь через таблицу, библиографию или принудительный перенос страницы.
show figure: it => {
let block-before = if it.kind == table { table-before } else { figure-before }
let block-after = if it.kind == table { table-after } else { figure-after }
v(block-before, weak: true)
it
v(block-after, weak: true)
}
show math.equation.where(block: true): it => {
it
}
// Базовые отступы для списков
// Внутри многострочного пункта строки компактнее обычного абзаца, а
// соседние пункты отделены друг от друга заметным интервалом.
set enum(indent: 0.55cm, body-indent: 0.3em, spacing: 0.85em)
set list(indent: 0.7cm, body-indent: 0.5em, spacing: 0.85em, marker: ([--], [--], [--]))
show enum: it => block(above: 0.85em, below: 0.85em, {
set par(leading: 0.55em, spacing: 0pt)
it
})
show list: it => block(above: 0.85em, below: 0.85em, {
set par(leading: 0.55em, spacing: 0pt)
it
})
// ГОСТ-формат по умолчанию использует тот же движок, что и numbered-list.
set enum(full: true, numbering: list-numbering(scheme: "gost"))
// --- НАСТРОЙКИ ЗАГОЛОВКОВ ---
set heading(numbering: "1.1")
show heading: it => {
v(heading-pre-gap, weak: true)
let w = if it.level <= 3 { "bold" } else { "regular" }
set text(weight: w, size: 12pt, hyphenate: false)
// Блок для того, чтобы локальные настройки абзаца применялись ТОЛЬКО к заголовку
[#block(sticky: true, width: 100%, {
set par(first-line-indent: 0pt, leading: 0.65em)
let alignment = if it.level <= 2 { center } else { left }
align(alignment)[
#if it.level == 1 {
if it.numbering != none {
[#counter(heading).display() #upper(it.body)]
} else {
[#upper(it.body)]
}
} else {
if it.numbering != none {
[#counter(heading).display() #it.body ]
} else {
[#it.body]
}
}
]
})#v(heading-to-text-gap, weak: true)]
}
// Сброс счетчиков рисунков/таблиц/формул при новой главе
show heading.where(level: 1): it => {
counter(figure.where(kind: image)).update(0)
counter(figure.where(kind: table)).update(0)
counter(math.equation).update(0)
it
}
// --- НАСТРОЙКИ РИСУНКОВ И ТАБЛИЦ ---
set figure(
numbering: (..nums) => {
let chap = counter(heading).get().first()
if chap > 0 { str(chap) + "." + str(nums.pos().first()) } else { str(nums.pos().first()) }
},
// Задаем полные слова для стандартных ссылок (вместо Рис. / Табл.)
supplement: body => {
if type(body) == content and body.func() == table [Таблица] else [Рисунок]
},
)
// Формат подписей рисунков/таблиц (через тире, как в ГОСТ)
show figure.where(kind: image): set figure.caption(position: bottom)
show figure.where(kind: image): set block(breakable: false)
show figure.where(kind: image): set align(center)
show figure.caption: it => block(
sticky: true,
width: if it.kind == table { 100% } else { auto },
)[
#set text(hyphenate: false)
#set par(
leading: 0.65em,
// Подписи рисунков и таблиц являются отдельными служебными строками,
// поэтому красная строка в них не применяется.
first-line-indent: 0pt,
)
#let cap-before = if it.kind == table { table-caption-before } else { figure-caption-before }
#let cap-after = if it.kind == table { table-caption-after } else { figure-caption-after }
#let prefix = if it.kind == table [Таблица] else [Рисунок]
#let num = it.counter.display(it.numbering)
#v(cap-before)
#align(if it.kind == table { left } else { center })[
#prefix #num -- #it.body
]
#v(cap-after)
]
show figure.where(kind: table): set figure.caption(position: top)
show figure.where(kind: table): set block(breakable: true)
// Формулы: центр по строке, номер справа в формате (глава.номер)
set math.equation(
numbering: (..nums) => {
let chap = counter(heading).get().first()
let num = nums.pos().first()
if chap > 0 { "(" + str(chap) + "." + str(num) + ")" } else { "(" + str(num) + ")" }
},
number-align: end + horizon,
supplement: none,
)
if show_title_page {
// ==========================================
// 1. ТИТУЛЬНЫЙ ЛИСТ
// ==========================================
set page(numbering: none, footer: none, foreground: page-foreground)
// Верхний оранжевый блок с логотипом (прижат к левому краю бумаги)
place(top + left, dx: -2cm, dy: -2cm)[
#rect(fill: rgb("fbb20d"), width: 6.5cm, height: 5.5cm, radius: (bottom: 15pt))[
#place(center + horizon)[
#if logo_image != none {
image(resolve-asset-path(logo_image), width: 4cm)
}
]
]
]
// Нижний оранжевый блок
place(bottom + left, dx: -2cm, dy: 2cm)[
#rect(fill: rgb("fbb20d"), width: 6.5cm, height: 2.5cm, radius: (top: 15pt))
]
// Реквизиты сверху справа
align(right)[
#text(font: "Arial", size: 8pt, fill: luma(100), align(right)[#company_info])
]
v(3cm)
// Блок УДК и Утверждаю
grid(
columns: (1fr, 1fr),
align(left)[
#if udk != none [ УДК: #udk ]
],
align(right)[
#box(width: 8cm)[
#set align(right)
УТВЕРЖДАЮ: \
#director_company \
#v(0.2cm)
#grid(
columns: (4.2cm, 1fr),
column-gutter: 0.2cm,
align: (center + horizon, right + horizon),
box(width: 4.2cm, height: 3.5cm)[
#if with-signs-and-stamps and stamp_image != none {
place(center, dx: -0.4cm)[
#rotate(8deg)[
#image(
resolve-asset-path(stamp_image),
width: 3.5cm,
height: 3.5cm,
fit: "contain",
)
]
]
}
#if with-signs-and-stamps and sign_image != none {
place(center, dx: 0.8cm)[
#rotate(-4deg)[
#image(
resolve-asset-path(sign_image),
width: 3cm,
height: 2.5cm,
fit: "contain",
)
]
]
}
],
align(right)[
#director_name \
#director_date
],
)
]
],
)
v(1.7cm)
// Название
align(center)[
ОТЧЕТ \
#if is_research [
О НАУЧНО-ИССЛЕДОВАТЕЛЬСКОЙ РАБОТЕ
] else [
О РАБОТЕ
]
#v(0cm)
#text(weight: "bold", size: 14pt)[«#upper(title)»] \
#v(0cm)
#if theme != none {
[по теме: \ #theme \ ]
}
#if is_intermediate {
[(промежуточный)]
}
#if stage_num != none or vol_num != none {
v(0.001cm)
}
#if stage_num != none {
[Этап #stage_num \ ]
}
#if vol_num != none {
[Том #vol_num \ ]
}
#if mode == "draft" and has-watermark {
[#text(fill: rgb("cc0000"), weight: "bold")[DRAFT] \ ]
}
]
v(1fr) // Пружина, толкает текст вниз
// Реквизиты договора не должны оставлять служебные подписи при пустых данных.
let has-contract-number = contract_num != none and (
type(contract_num) != str or contract_num.trim() != ""
)
let has-contract-date = contract_date != none and (
type(contract_date) != str or contract_date.trim() != ""
)
if has-contract-number or has-contract-date {
align(right)[
#if has-contract-number [Договор № #contract_num]
#if has-contract-number and has-contract-date { linebreak() }
#if has-contract-date [От #contract_date г.]
]
}
v(1cm)
align(center)[#city #year]
v(1cm)
pagebreak()
}
// ==========================================
// 2. СПИСОК ИСПОЛНИТЕЛЕЙ
// ==========================================
// Встроенное page(numbering: ...) может наследовать контекст выравнивания
// на промежуточном фрагменте разрываемой строки таблицы. Независимый footer
// гарантирует одно и то же положение номера на каждой странице.
set page(
numbering: none,
footer: context {
set text(font: "Times New Roman", size: 12pt)
block(width: 100%)[#align(center)[#counter(page).display("1")]]
},
footer-descent: 30%,
foreground: page-foreground,
)
// Титульный лист не входит в видимую нумерацию. Если титула нет,
// основной текст или первая выбранная служебная страница получает номер 1.
counter(page).update(1)
if show_executors and executors.len() > 0 {
service-title([СПИСОК ИСПОЛНИТЕЛЕЙ])
v(1cm)
table(
columns: (1fr, 3.4cm, auto),
stroke: none,
row-gutter: 2.3em,
align: (left + bottom, center + bottom, left + bottom),
..executors
.map(e => (
e.at(0),
[
#if e.len() > 2 and e.at(2) != none {
let offset = if e.len() > 3 { e.at(3) } else { -0.2cm }
// Картинка располагается относительно линии (bottom ячейки)
if with-signs-and-stamps{place(bottom + center, dy: offset)[
#image(resolve-asset-path(e.at(2)), width: 90%)
]}
}
// Линия является единственным элементом, занимающим место,
// поэтому она всегда строго на уровне нижней границы текста других колонок!
#line(length: 100%, stroke: 0.5pt)
// Текст "Подпись" подвешивается снизу без влияния на высоту строки
#place(top + center, dy: 0.4cm)[
#text(size: 8pt)[Подпись]
]
],
e.at(1),
))
.flatten()
)
pagebreak()
}
// ==========================================
// 3. СОДЕРЖАНИЕ
// ==========================================
if show_outline {
service-title([СОДЕРЖАНИЕ])
// Настройка внешнего вида содержания под стандарт
show outline.entry.where(level: 1): it => {
v(12pt, weak: true)
text(weight: "regular")[#it]
}
// Переопределяем отображение элементов содержания для Приложений (где есть supplement "Приложение")
show outline.entry.where(level: 1): it => {
if it.element.supplement == [Приложение] {
// Поскольку нумерация - функция, мы передаем ее в платформенную функцию numbering
let app_num = numbering(it.element.numbering, ..counter(heading).at(it.element.location()))
// Отменяем глобальный абзацный отступ, чтобы Приложение Б, В не уезжали вправо, и делаем кликабельным
set par(first-line-indent: 0pt)
link(it.element.location())[
#text(weight: "regular")[Приложение #app_num. #it.element.body]
#box(width: 1fr, repeat[.])
#it.page()
]
} else {
text(weight: "regular")[#it]
}
}
outline(title: none, depth: 3, indent: 2em)
pagebreak()
}
// --- ВСТАВКА ОСНОВНОГО ТЕКСТА ---
// В Typst leading — дополнительный промежуток между строками, а не
// коэффициент высоты строки. Для Times New Roman 12 pt значение 0.85em
// даёт шаг базовых линий около 18 pt, то есть полуторный интервал.
set par(leading: 0.85em, spacing: 0.85em)
body
}