Initial commit
This commit is contained in:
commit
41d230d524
163 files changed
+11500
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user