From c9c0577ec863f1d917f50795702c0120bb545fcf Mon Sep 17 00:00:00 2001 From: ilyaoc Date: Wed, 20 May 2026 12:37:09 +0200 Subject: [PATCH] add coursework v2 template --- README.md | 4 +- src/lib.typ | 67 +++++ src/style.typ | 123 +++++++++ src/title-pages/coursework-v2/main.typ | 1 + src/title-pages/coursework-v2/nure.typ | 359 +++++++++++++++++++++++++ src/title-pages/main.typ | 1 + template/default/coursework-v2.typ | 107 ++++++++ 7 files changed, 661 insertions(+), 1 deletion(-) create mode 100644 src/title-pages/coursework-v2/main.typ create mode 100644 src/title-pages/coursework-v2/nure.typ create mode 100644 template/default/coursework-v2.typ diff --git a/README.md b/README.md index d543e04..57f7156 100644 --- a/README.md +++ b/README.md @@ -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". diff --git a/src/lib.typ b/src/lib.typ index 17c8c22..2580860 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -5,6 +5,8 @@ #import "./style.typ" #import "./utils.typ" +#let dstu-table = style.dstu-table + /// Coursework template for NURE /// - university (str): University code, default "ХНУРЕ" /// - subject (str): Subject short name @@ -56,6 +58,71 @@ 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 +/// - 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: (), + 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") + + set document(title: 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)) + } + + 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 diff --git a/src/style.typ b/src/style.typ index 3ea37fa..97f4681 100644 --- a/src/style.typ +++ b/src/style.typ @@ -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") diff --git a/src/title-pages/coursework-v2/main.typ b/src/title-pages/coursework-v2/main.typ new file mode 100644 index 0000000..52e579b --- /dev/null +++ b/src/title-pages/coursework-v2/main.typ @@ -0,0 +1 @@ +#import "nure.typ": * diff --git a/src/title-pages/coursework-v2/nure.typ b/src/title-pages/coursework-v2/nure.typ new file mode 100644 index 0000000..9591e23 --- /dev/null +++ b/src/title-pages/coursework-v2/nure.typ @@ -0,0 +1,359 @@ +#import "../../shared.typ": universities +#import "../../helpers.typ": * +#import "../../style.typ": spacing +#import "../../utils.typ": bold, uline + +#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, 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)[#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, task-list.at("source", default: [])) + #v(0.4em) + #uline(align: left, []) + + #v(0.4em) + #task-num(4) Перелік питань, що потрібно опрацювати в роботі\ + #uline(align: left, 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, + ) + } +} diff --git a/src/title-pages/main.typ b/src/title-pages/main.typ index f28c162..b60052f 100644 --- a/src/title-pages/main.typ +++ b/src/title-pages/main.typ @@ -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 diff --git a/template/default/coursework-v2.typ b/template/default/coursework-v2.typ new file mode 100644 index 0000000..61cf4ae --- /dev/null +++ b/template/default/coursework-v2.typ @@ -0,0 +1,107 @@ +#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 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, + bib-path: bytes(read("bibl.yml")), + appendices: appendices, +) + += Моделювання +#lorem(250) + += Імплементація +#v(-spacing) +== Підготовка +#lorem(200) +== Процес +#lorem(500) + += Тестування +#lorem(300)