Merge pull request #1 from ilyaoc/add-coursework-v2-template

feat: add coursework v2 template
This commit is contained in:
2026-05-20 15:44:39 +02:00
committed by GitHub
8 changed files with 716 additions and 2 deletions
+3 -1
View File
@@ -3,7 +3,7 @@
## General Info
This project contains two template functions and some utilities for writing NURE works. All functions include documentation comments inside them, so you can explore all possibilities using LSP.
This project contains template functions and some utilities for writing NURE works. All functions include documentation comments inside them, so you can explore all possibilities using LSP.
### Templates
@@ -19,6 +19,8 @@ This template:
- Typesets the bibliography according to ДСТУ 3008:2015 using custom CSL style;
- Typesets the outline and appendices according to standard requirements.
#### `coursework-v2` - New Coursework Variant
This template keeps the legacy coursework template intact while offering the newer title/task-page layout. See `template/default/coursework-v2.typ` for an example entrypoint.
### Utilities
- `nheading` - For unnumbered headings, such as "Introduction" and "Conclusion".
+79 -1
View File
@@ -5,6 +5,9 @@
#import "./style.typ"
#import "./utils.typ"
#let dstu-table = style.dstu-table
#let hfill = utils.hfill
/// Coursework template for NURE
/// - university (str): University code, default "ХНУРЕ"
/// - subject (str): Subject short name
@@ -29,7 +32,8 @@
bib-path: none,
appendices: (),
) = {
set document(title: title, author: authors.map(c => c.name))
let doc-title = if type(title) == array { title.join(" ") } else { title }
set document(title: doc-title, author: authors.map(c => c.name))
show: style.dstu.with(skip: 1)
@@ -56,6 +60,80 @@
style.appendices(appendices)
}
/// Alternative coursework template for NURE.
/// - university (str): University code, default "ХНУРЕ"
/// - title (str): Work title
/// - authors (array): List of author dictionaries
/// - mentors (array): List of mentor dictionaries
/// - task-list (dict): Task metadata
/// - calendar-plan (dict): Calendar plan table
/// - abstract (dict): Keywords and abstract text
/// - abstract-en (dict): Optional English keywords and abstract text
/// - bib-path (str): Path to bibliography file
/// - appendices (content): Appendix content
#let coursework-v2(
doc,
university: "ХНУРЕ",
title: none,
authors: (),
mentors: (),
task-list: (),
calendar-plan: (),
abstract: (),
abstract-en: none,
bib-path: none,
appendices: (),
faculty: "комп’ютерних наук",
education-level: "перший (бакалаврський)",
program-type: "освітньо-професійна",
program-name: none,
) = {
assert(authors.len() > 0, message: "At least one author required")
assert(mentors.len() > 0, message: "At least one mentor required")
let doc-title = if type(title) == array { title.join(" ") } else { title }
set document(title: doc-title, author: authors.map(c => c.name))
show: style.dstu.with(skip: 1)
let bib-count = state("citation-counter", ())
show cite: it => {
it
bib-count.update(((..c)) => (..c, it.key))
}
let abstract = if abstract-en != none {
abstract + (en: abstract-en)
} else {
abstract
}
tp.cw-v2.nure(
university,
title,
authors,
mentors,
task-list,
calendar-plan,
abstract,
bib-count,
faculty: faculty,
education-level: education-level,
program-type: program-type,
program-name: program-name,
)
doc
{
show regex("^\\d+\\."): it => [#it#h(0.5cm)]
show block: it => [#it.body#parbreak()]
bibliography(bib-path, title: [Перелік джерел посилання], style: "csl/dstu-3008-2015.csl", full: true)
}
style.appendices(appendices)
}
/// Practice and Laboratory works template
/// - layout (str): "default", "minimal", or "complex"
/// - university (str): University code
+123
View File
@@ -8,6 +8,10 @@
/// Ukrainian alphabet for DSTU 3008:2015 numbering
#let ukr-enum = "абвгдежиклмнпрстуфхцшщюя".clusters()
#let dstu-table-counter = counter("dstu-table")
#let dstu-table-appendix = state("dstu-table-appendix", none)
#let dstu-table-caption-gap = 0.65em
/// Helper for level 2/3 heading blocks
#let heading-block(it, num: auto) = {
v(double-spacing, weak: true)
@@ -19,6 +23,123 @@
v(double-spacing, weak: true)
}
#let _col-count(columns) = {
if type(columns) == int {
columns
} else if type(columns) == array {
columns.len()
} else {
panic("dstu-table: columns must be an int or array, e.g. 2 or (1fr, 3fr)")
}
}
#let _required(name, value) = {
if value == none {
panic("dstu-table: " + name + " is required")
}
value
}
#let dstu-table-label(it) = {
set par(first-line-indent: 0pt)
align(left)[#it]
}
#let dstu-table(
caption: none,
columns: none,
header: none,
..args,
) = {
let caption = _required("caption", caption)
let columns = _required("columns", columns)
let header = _required("header", header)
if type(header) != array {
panic("dstu-table: header must be an array, e.g. ([A], [B])")
}
dstu-table-counter.step()
let named = args.named()
let body = args.pos()
context {
let h = counter(heading).get()
let section = if h.len() > 0 { h.at(0) } else { 0 }
let n = dstu-table-counter.get().first()
let appendix = dstu-table-appendix.get()
let num = if appendix == none {
numbering("1.1", section, n)
} else {
upper(ukr-enum.at(appendix - 1)) + "." + str(n)
}
let id = "dstu-table-" + str(section) + "-" + str(n)
let start-marker = "start-" + id
let end-marker = "end-" + id
let cols = _col-count(columns)
v(double-spacing, weak: true)
{
set block(spacing: dstu-table-caption-gap)
block(sticky: true)[
#dstu-table-label[Таблиця #num -- #caption]
]
table(
columns: columns,
..named,
table.header(
repeat: true,
table.cell(
colspan: cols,
stroke: none,
inset: 0pt,
)[
#metadata(start-marker)
#context {
let starts = query(metadata.where(value: start-marker))
let ends = query(metadata.where(value: end-marker))
if starts.len() > 0 and ends.len() > 0 {
let start-page = starts.first().location().page()
let end-page = ends.first().location().page()
let current-page = here().page()
if current-page != start-page {
let label = if current-page == end-page {
[Кінець таблиці #num]
} else {
[Продовження таблиці #num]
}
pad(top: dstu-table-caption-gap, bottom: dstu-table-caption-gap)[
#dstu-table-label[#label]
]
}
}
}
],
..header,
),
..body,
)
}
metadata(end-marker)
v(double-spacing, weak: true)
}
}
/// DSTU 3008:2015 Style
#let dstu(
it,
@@ -118,6 +239,8 @@
counter(figure.where(kind: raw)).update(0)
counter(figure.where(kind: image)).update(0)
counter(figure.where(kind: table)).update(0)
dstu-table-counter.update(0)
dstu-table-appendix.update(counter(heading).get().at(0))
set align(center)
set text(weight: "regular")
+1
View File
@@ -0,0 +1 @@
#import "nure.typ": *
+358
View File
@@ -0,0 +1,358 @@
#import "../../shared.typ": universities
#import "../../helpers.typ": *
#import "../../style.typ": spacing
#import "../../utils.typ": bold, uline, filled-lines
#let note(content) = block(width: 100%, above: 5pt, below: 0pt)[
#set text(size: 10pt)
#set par(first-line-indent: 0pt, spacing: 0pt)
#align(center)[#content]
]
#let form-field(alignment: center, content) = box(
width: 100%,
stroke: (bottom: 0.5pt),
inset: (bottom: 1.5pt),
)[
#align(alignment)[#content]
]
#let label-line(label, value, caption: none, label-width: auto) = {
set par(first-line-indent: 0pt)
if label-width == auto {
[#label #form-field(alignment: center, value)]
} else {
grid(
columns: (label-width, 1fr),
gutter: 0pt,
align: horizon,
label, form-field(alignment: center, value),
)
}
if caption != none {
note(caption)
}
}
#let inline-field(value) = form-field(alignment: center, value)
#let inline-label-line(label, value) = {
set par(first-line-indent: 0pt)
block(width: 100%, below: 0pt)[
#label #uline(align: center, value)
]
}
#let task-head-fields(fields) = {
set par(first-line-indent: 0pt)
let cells = ()
for (label, value) in fields {
if type(value) == array {
for (i, line) in value.enumerate() {
cells.push(if i == 0 { label } else { [] })
cells.push(inline-field(line))
}
} else {
cells.push(label)
cells.push(inline-field(value))
}
}
grid(
columns: (auto, 1fr),
gutter: 0pt,
row-gutter: 0.65em,
align: top,
..cells,
)
}
#let task-num(n) = box(str(n) + ".")
#let title-field(value) = {
uline(align: center, filled-lines(value))
uline(align: center, [])
note[(тема)]
}
#let nure(
university,
title,
authors,
mentors,
task-list,
calendar-plan,
abstract,
bib-count,
faculty: "комп’ютерних наук",
education-level: "перший (бакалаврський)",
program-type: "освітньо-професійна",
program-name: none,
) = {
let author = authors.first()
let head-mentor = mentors.first()
let uni = universities.at(university)
let edu-prog = uni.edu-programs.at(author.edu-program)
let program-name = if program-name == none {
edu-prog.at("program-name", default: edu-prog.name-long)
} else {
program-name
}
let group-name = if str(author.group).starts-with(author.edu-program) {
str(author.group)
} else {
author.edu-program + "-" + str(author.group)
}
let executor-label = if author.gender == "f" or author.gender == "female" or author.gender == "ж" {
"Виконала:"
} else {
"Виконав:"
}
let author-display-name = author.at("display-name", default: author.name)
let author-full-name-dat = author.at("full-name-dat", default: author.full-name-gen)
let mentor-display-name = head-mentor.at("display-name", default: head-mentor.name)
let mentor-degree = head-mentor.at("degree", default: "")
[
#set par(first-line-indent: 0pt, justify: false, leading: 0.45em)
#set text(size: 14pt)
#set align(center)
МІНІСТЕРСТВО ОСВІТИ І НАУКИ УКРАЇНИ\
#uni.name
#v(0.7em)
#set align(left)
#inline-label-line(
[Факультет],
faculty,
)
#note([(повна назва)])
#v(0.8em)
#inline-label-line([Кафедра], edu-prog.department-gen)
#note([(повна назва)])
#v(1.8em)
#set align(center)
#text(size: 20pt, weight: "bold")[КОМПЛЕКСНИЙ КУРСОВИЙ ПРОЄКТ]\
#text(size: 20pt, weight: "bold")[Пояснювальна записка]
#v(0.9em)
#set align(left)
#label-line([рівень вищої освіти], education-level, label-width: 120pt)
#v(1.0em)
#title-field(title)
#v(3em)
#grid(
columns: (0.36fr, 0.64fr),
[],
[
#executor-label\
здобувач #underline([#author.course]) курсу, групи #underline(group-name)\
#uline(align: center, author-display-name)
#note[(Власне ім’я, ПРІЗВИЩЕ)]
#v(0.2em)
#inline-label-line([Спеціальність], [#edu-prog.code -- #edu-prog.name-long])
#note[(код і повна назва спеціальності)]
#inline-label-line([Тип програми], program-type)
#inline-label-line([Освітня програма], program-name)
#note[(повна назва освітньої програми)]
#v(0.3em)
#inline-label-line([Керівник], [#mentor-degree #mentor-display-name])
#note[(посада, Власне ім’я, ПРІЗВИЩЕ)]
#v(0.3em)
#pad(left: 75pt)[
#set par(first-line-indent: 0pt)
Члени комісії (#text(size: 10pt)[Власне ім’я, ПРІЗВИЩЕ, підпис])
#v(0.55em)
#line(length: 100%, stroke: 0.5pt)
#v(0.55em)
#line(length: 100%, stroke: 0.5pt)
#v(0.55em)
#line(length: 100%, stroke: 0.5pt)
]
],
)
#v(1fr)
#set align(center)
#task-list.done-date.display("[year]") р.
#pagebreak()
]
[
#set par(first-line-indent: 0pt, justify: false)
#align(center)[#uni.name]
#v(1.1em)
#task-head-fields((
(
[Факультет],
(
faculty,
),
),
([Кафедра], edu-prog.department-gen),
([Рівень вищої освіти], education-level),
([Спеціальність], [#edu-prog.code -- #edu-prog.name-long]),
([Тип програми], program-type),
([Освітня програма], program-name),
))
#note[(шифр і назва)]
#v(1.7em)
#grid(
columns: (1.1fr, 1.1fr, 1.1fr, 1.7fr, 1.3fr, 1.1fr),
gutter: 0pt,
align: center + horizon,
[Курс], uline(author.course), [Група], uline(group-name), [Семестр], uline(author.semester),
)
#v(2.6em)
#align(center)[
#bold[ЗАВДАННЯ]\
#text(style: "italic", weight: "bold")[на курсовий проєкт (роботу) студента]
]
#v(1.0em)
#label-line([здобувачеві], author-full-name-dat, caption: [(прізвище, ім’я, по батькові)], label-width: 95pt)
#v(1.0em)
#task-num(1) Тема роботи #uline(align: left, filled-lines(title))
#v(0.4em)
#task-num(2) Термін здачі студентом закінченої роботи
“#underline(task-list.done-date.display("[day]"))” #underline(month-gen(task-list.done-date.month())) #task-list.done-date.display("[year]")р.
#v(0.4em)
#task-num(3) Вихідні дані до проєкту #uline(align: left, filled-lines(task-list.at("source", default: [])))
#v(0.4em)
#uline(align: left, [])
#v(0.4em)
#task-num(4) Перелік питань, що потрібно опрацювати в роботі\
#uline(align: left, filled-lines(task-list.at("content", default: [])))
#v(0.4em)
#uline(align: left, [])
#pagebreak()
]
[
#align(center, bold[КАЛЕНДАРНИЙ ПЛАН])
#set par(first-line-indent: 0pt)
#v(1.4em)
#calendar-plan.plan-table
#v(5.0em)
Дата видачі завдання “#underline(task-list.initial-date.display("[day]"))” #underline(month-gen(task-list.initial-date.month())) #task-list.initial-date.display("[year]")р.
#v(1.4em)
Здобувач #uline(align: center, [])
#note[(підпис)]
#v(1.4em)
Керівник роботи #uline(align: center, []) #h(1cm) #underline[#mentor-degree #mentor-display-name]
#note[(підпис) #h(4.5cm) (посада, Власне ім’я, ПРІЗВИЩЕ)]
#pagebreak()
]
[
#let header = if abstract.at("en", default: none) != none {
bold[РЕФЕРАТ / ABSTRACT]
} else {
bold[РЕФЕРАТ]
}
#align(center, header) \
#context [
#let pages = counter(page).final().at(0)
#let images = query(figure.where(kind: image)).len()
#let tables = query(figure.where(kind: table)).len()
#let bibs = bib-count.final().dedup().len()
#let counters = ()
#if pages != 0 { counters.push[#pages с.] }
#if tables != 0 { counters.push[#tables табл.] }
#if images != 0 { counters.push[#images рис.] }
#if bibs != 0 { counters.push[#bibs джерел] }
Пояснювальна записка містить: #counters.join(", ").
]
\
#(
abstract
.keywords
.map(upper)
.sorted(by: (a, b) => {
if is-cyr(a) != is-cyr(b) { is-cyr(a) } else { a < b }
})
.join(", ")
)
\
#abstract.text
#if abstract.at("en", default: none) != none [
\
#(abstract.en.keywords.map(upper).join(", "))
\
#abstract.en.text
]
]
{
show outline.entry: it => {
let el = it.element
if el.func() == heading and el.supplement == [Додаток] {
if el.level > 1 {
none
} else {
block(width: 100%)[
#link(el.location())[
ДОДАТОК #it.prefix()#h(0.5em)#it.inner()
]
]
}
} else {
it
}
}
outline(
title: [
ЗМІСТ
#v(spacing * 2, weak: true)
],
depth: 2,
indent: auto,
)
}
}
+1
View File
@@ -1,2 +1,3 @@
#import "pz-lb/main.typ" as pz-lb
#import "coursework/main.typ" as cw
#import "coursework-v2/main.typ" as cw-v2
+7
View File
@@ -7,6 +7,13 @@
/// fill horizontal space with a filled box
#let hfill(width) = box(width: width, repeat("")) // HAIR SPACE (U+200A)
/// convert an array of lines into filled lines for underlined task fields
#let filled-lines(content) = if type(content) == array {
content.map(line => [#line #hfill(1fr)]).join(linebreak())
} else {
content
}
/// underlined cell with centered content by default
#let uline(align: center, content) = underline[
#if align != left { hfill(1fr) }
+144
View File
@@ -0,0 +1,144 @@
#import "@local/nure:0.1.1": *
#import style: spacing
#let authors = (
(
name: "Сокорчук І. П.",
display-name: "Ігор СОКОРЧУК",
full-name-gen: "Сокорчука Ігоря Петровича",
full-name-dat: "Сокорчуку Ігорю Петровичу",
edu-program: "ПЗПІ",
group: "23-3",
gender: "m",
course: 3,
semester: 6,
variant: 13,
),
)
#let mentors = (
(name: "Сокорчук І. П.", display-name: "Ігор СОКОРЧУК", degree: "ст.викл. кафедри ПІ"),
)
#let task-list = (
done-date: datetime(year: 2026, month: 12, day: 27),
initial-date: datetime(year: 2026, month: 9, day: 15),
source: (
[Узагальнити модель предметної області,],
[визначити основні сутності та зв'язки між ними.],
[Підготувати демонстраційний набір технологій для шаблону.],
),
content: (
[Опис предметної галузі, формування вимог,],
[архітектурне проєктування, тестування та],
[перевірка верстки пояснювальної записки.],
),
)
#let calendar-plan = (
plan-table: table(
columns: (0.7fr, 5.8fr, 2.5fr, 1.6fr),
align: (center, left, center, center),
[], [Назва етапів роботи], [Термін виконання етапів роботи], [Примітка],
[1], [Аналіз предметної галузі], [], [виконано],
[2], [Розробка постановки задачі], [], [виконано],
[3], [Проєктування ПЗ], [], [виконано],
[4], [Програмна реалізація], [], [виконано],
[5], [Аналіз результатів], [], [виконано],
[6], [Підготовка пояснювальної записки.], [], [виконано],
[7], [Перевірка на наявність ознак академічного плагіату], [], [виконано],
[8], [Захист роботи], [], [виконано],
),
)
#let abstract = (
keywords: (
"веб-застосунок",
"інформаційна система",
"курсова робота",
"програмна інженерія",
"тестовий приклад",
),
text: [
Мета даної роботи -- продемонструвати оформлення пояснювальної записки
для комплексного курсового проєкту з використанням нового варіанта шаблону.
Приклад містить узагальнену тему, типові сторінки завдання, календарного плану,
реферату, змісту, переліку джерел посилання та додатків.
У роботі наведено умовну структуру програмної системи, що може бути
адаптована під конкретну предметну область. Основний акцент зроблено на
перевірці полів титульної сторінки, сторінки завдання, службових підписів,
нумерації розділів і коректної роботи додатків.
Результатом є демонстраційний документ, який показує очікуване використання
шаблону без прив'язки до реального студента, викладача або завершеної роботи.
],
)
#let abstract_en = (
keywords: (
"WEB APPLICATION",
"INFORMATION SYSTEM",
"COURSEWORK",
"SOFTWARE ENGINEERING",
"DEMO SAMPLE",
),
text: [
The purpose of this work is to demonstrate the formatting of a coursework
explanatory note using the new template variant.
The sample contains a generalized topic, typical pages for the assignment,
calendar plan, abstract, contents, bibliography, and appendices.
The work presents an illustrative structure of a software system that can be
adapted to a specific subject area. The main focus is on checking title-page
fields, assignment-page fields, signature blocks, section numbering, and
appendix handling.
The result is a demonstration document that shows the expected template usage
without being tied to a real student, supervisor, or completed project.
],
)
#let appendices = [
= Приклад звіту 1
#v(-spacing)
== Частина 1
#lorem(100)
== Частина 2
#lorem(200)
= Приклад звіту 2
#lorem(200)
= Приклад звіту 3
#lorem(200)
]
#show: coursework-v2.with(
title: (
"Демонстраційна інформаційна система для комплексного ",
"курсового проєкту",
),
authors: authors,
mentors: mentors,
task-list: task-list,
calendar-plan: calendar-plan,
abstract: abstract,
abstract-en: abstract_en,
bib-path: bytes(read("bibl.yml")),
appendices: appendices,
)
= Моделювання
#lorem(250)
= Імплементація
#v(-spacing)
== Підготовка
#lorem(200)
== Процес
#lorem(500)
= Тестування
#lorem(300)