1 Commits

Author SHA1 Message Date
unexplrd 92a52fb7a1 chore!: bump to 0.1.1
refactor: break up into multiple files
feat: csl style
refactor!: rename variables
Update template, readme, and more
2026-02-06 01:46:07 +02:00
24 changed files with 67 additions and 1926 deletions
+9 -62
View File
@@ -3,7 +3,7 @@
## General Info
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.
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.
### Templates
@@ -19,25 +19,20 @@ 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".
- `hfill` - Fills horizontal space with a filled box instead of just empty space; useful for creating underlines.
- `uline` - Creates underlined fields that need to be filled, such as the name field on the task list.
- `bold` - Inserts bold text inside functional environments.
- `img` - Inserts images with a caption, automatically deriving the label from the image file name (use via `#import "@local/nure:0.1.1": utils` and call `utils.img`).
- `img` - Inserts images with a caption, automatically deriving the label from the image file name.
**Note:** `img()` is provided in `template/utils.typ` so you can copy it into your project root for compatibility, until [path() type](https://github.com/typst/typst/pull/7555) is released.
**Note:** `img()` is provided in `utils.typ` in project's root directory for compatibility, until [path() type](https://github.com/typst/typst/pull/7555) is released.
## Usage
### As a local typst package
You can install the template as a local Typst package. Choose the instructions for your platform below.
#### Unix (Linux / macOS)
1. Clone this repository into your local Typst packages folder:
1. Clone this repository into ~/.local/share/typst/packages/:
```bash
git clone -b 0.1.1 https://gitea.linerds.us/pencelheimer/typst_nure_template.git ~/.local/share/typst/packages/local/nure/0.1.1
```
@@ -46,67 +41,21 @@ git clone -b 0.1.1 https://gitea.linerds.us/pencelheimer/typst_nure_template.git
typst init @local/nure:0.1.1 project-name
```
#### Windows (symlink method)
This method creates a symbolic link between the cloned repository and Typst's local packages folder in `%APPDATA%`. The benefit: when the template is updated on GitHub you only need to `git pull` in the cloned folder — VS Code (and Typst) will use the updated files automatically.
Step 1 — clone the template (for example, into your Documents folder):
```powershell
cd $HOME\Documents
git clone https://github.com/linerds/typst_nure_template.git
```
Step 2 — create the symlink (PowerShell must be run as Administrator):
1. Open Start, find PowerShell, right-click and choose "Run as Administrator".
2. Run the following commands as a single block:
```powershell
# Create local packages folder (if it doesn't exist)
New-Item -ItemType Directory -Force -Path "$env:APPDATA\typst\packages\local\nure"
# Point to the folder you cloned
$gitFolder = "$HOME\Documents\typst_nure_template"
# Create a symbolic link for the package version (match the version in this repo)
New-Item -ItemType SymbolicLink -Path "$env:APPDATA\typst\packages\local\nure\0.1.1" -Target $gitFolder
```
Step 3 — use in VS Code
Open your project folder (for example, your coursework or lab folder) in VS Code with the Tinymist Typst extension installed. Create `main.typ` and import the package using the official `@local` import:
```typst
#import "@local/nure:0.1.1": nure-report, style
// If you need specific style variables from the package:
#import "@local/nure:0.1.1/src/style.typ": spacing
#show: nure-report.with(
title: "Report for Lab Work",
type: "Lab Work #1",
discipline: "Object-Oriented Programming",
author: "Surname I.I.",
supervisor: "Assoc. Prof. Kuznetsov O.V.",
)
= Work progress
Your content...
```
The Windows steps above are intentionally minimal and focused on the symlink workflow. If you prefer not to use symlinks, you can copy `src/` to your project root and import `lib.typ` directly (see the "As a standalone file" section).
### As a standalone file
Copy `src/` to your project's root directory, optionally renaming `src/` to `lib/` (then import `src/lib.typ` or `lib/lib.typ` accordingly).
Copy `src/` to your project's root directory, optionally renaming `src/` to `lib/`.
### In your project
```typst
// Import the template either from a local package...
#import "@local/nure:0.1.1": *
#import "@local/nure:0.1.0": *
// ...or by importing a lib.typ directly
// #import "/lib/lib.typ": *
// NOTE: all template arguments use kebab-case.
// 1. Setup the document
// by setting values directly...
#show: pz-lb.with(
title: "Some title",
// etc: "and so on",
// ...
)
// ...or using a yaml/toml file
@@ -124,7 +73,7 @@ Some text
#include "chapters/chapter1.typ"
#include "chapters/chapter2.typ"
// NOTE: if you want to use variables or utils provided by the package,
// you have to import the package or a lib.typ inside a module (e.g. #import "@local/nure:0.1.1": utils).
// you have to import the package or a lib.typ inside a module.
// If you ever need appendices in pz-lb template use the show rule
@@ -132,8 +81,6 @@ Some text
// so it can put bibliography before appendices
#show: style.appendices
// For coursework appendices, pass them via `appendices:` argument instead.
= Quote
#link("https://youtu.be/bJQj1uKtnus")[
The art isn't the art, the art is never the art,
@@ -175,7 +122,7 @@ semester = 4
### Notes:
1. Use `#v(-spacing)` to remove vertical spacing between titles (this cannot be automatically handled by the template). Variable `spacing` used here is imported from the template.
2. When importing `@local/nure:0.1.1` and specifying file paths in functions handled by the package, the path will be relative to the package root, e.g. setting `#show: coursework.with(bib-path: "bibl.yml")` will evaluate to `~/.local/share/typst/packages/local/nure/0.1.1/bibl.yml`. The same applies to `utils.img` unless you copy `template/utils.typ` into your project root and import from there.
2. When importing `@local/nure:0.1.1` and specifying file paths in functions handled by the package, the path will relative to package's root directory, e.g. setting `#show: coursework.with(bib-path: "bibl.yml")` will evaluate to `~/.local/share/typst/packages/local/nure/0.1.1/bibl.yml`, the same is for `#img` function, which makes it quite annoying and forces one to import `lib.typ` file. Please open an issue or contact us in any other way if you have any advice.
### Bibliography Format
The template uses a custom CSL (Citation Style Language) file located at `src/csl/dstu-3008-2015.csl` to format bibliography entries.
+11 -35
View File
@@ -19,72 +19,49 @@
code: 122
subjects:
DMT: Decision making theory
NoSQL: NoSQL-системи
ODS: Основи Dаtа Sсіеnсе # NOTE: Eng O here
ІМ: Іноземна мова
ІТР: Information Technologies of Reengineering
ІТРОІ: Інтернет-технології Розподіленої Обробки Інформації
АВпЗ: Аналіз вимог до програмного забезпечення
АДан: Аналітика даних
АКС: Архітектура комп'ютерних систем
АКтаК: Архітектура комп'ютера та комп'ютерних мереж
АПЗ: Архітектура програмного забезпечення
АтаРК: Аналіз та рефакторинг коду
АТСД: Алгоритми та структури даних
АтаРК: Аналіз та рефакторинг коду
БД: Бази даних
БЖД: Безпека життєдіяльності
ВSAP: Введення до SAP-технологій
ВДІТБ: Введення до ІТ-бізнесу # NOTE: all in UA
ВМ: Вища математика
ВМПтФ: Високорівневі мови програмування та фреймворки
ГТГ: Гіпертекст та гіпермедіа
ДМ: Дискретна математика
ДПК: Динаміка Проектних Команд
ЕРВ: Електрорадіовимірювання
ІКС: Інформаційно-комунікаційні системи
ІМ: Іноземна мова
ІМпк: Іноземна мова для професійної комунікації
ІНТ: Історія науки і техніки
ІТР: Information Technologies of Reengineering
ІТРОІ: Інтернет-технології Розподіленої Обробки Інформації
КДМА: Комп'ютерна дискретна математика
КЗВШ: Креативність з використанням штучного інтелекту
КМ: Комп'ютерні мережі
КМ: Комп`ютерні мережі
ЛМВ: Людино-машинна взаємодія
ЛМтБ: Локальні мережі та їх безпека
Лог: Логіка
МБ: Мережна безпека
МКр: Мережна криміналістика
ЛМтБ: Локальні мережі та їх безпека # бидло не знає що українською "їхня"
МОКр: Математичні основи криптології
МОТДО: Методи оптимізаціі та дослідження операцій
МОТІ: Методи оптимізації та теорія ігор
МППЗ: Менеджмент проектів програмного забезпечення
МППС: Methodologies of designing software systems
МС: Моделювання систем
ОІДт: Обробка ігрових даних та звітів
ОІМ: Основи IP-мереж
ОКЗІ: Основи криптографічного захисту інформації
ООАПС: Об'єктно-орієнтований аналіз в проектуванні систем
ООП: Об'єктно-орієнтоване програмування
ОП: Основи права
ОПІ: Основи програмноі інженеріі
ОПНJ: Основи програмування на Java
ОП: Основи права
ОПр: Основи програмування
ОРвІТ: Оцінка Ризиків в IT-проектах
ОС: Операційні системи
ОТК: Основи теорії кіл
ПPyt: Програмування мовою Python
ПарП: Параллельне програмування
ПБІП: Проектування та балансування ігрового процесу
ПВJS: Поглиблене вивчення JavaScript
ПВJ: Поглиблене вивчення Java
ПвІТ: Підприємництво в ІТ
ПЕСЕ: Психологія екстремальних стосунків та ефективної адаптації
ПНП: Програмування на платформі .NЕТ
ПП: Проектний практикум
Про1: Програмування (ч. 1)
Про2: Програмування (ч. 2)
Про3: Програмування (ч. 3)
Прог: Програмування # NOTE: was "ПРОГ" before
РNet: Робота з даними на платформі .Net
РХЗ: Розробка хмарних застосувань в AZURE
ПРОГ: Програмування
ПарП: Параллельне програмування
СА: Системний аналіз
СМП: Скриптові мови програмування
СОАПЗ: Сервіс-Орієнтована Архітектура Програмного Забезпечення
@@ -92,11 +69,10 @@
СхТ: Схемотехніка
ТВО: Технології Високопродуктивних Обчислень
ТЗІ: Технології захисту інформації
ТІК: Теорія інформації та кодування
ТЙтаМ: Теорія ймовірностей та математична # TODO: what?
ТКП: Технології комп'ютерного проєктування
ТКП: Технології комп`ютерного проєктування
УФМ: Українське фахове мовлення
ФВС: Фізичне виховання та спорт
ФІЗ: Фізика
ФІЛ: Філософія
ФВС: Фізичне виховання та спорт
ХТ: Хмарні технології
-420
View File
@@ -1,420 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<style xmlns="http://purl.org/net/xbiblio/csl"
class="in-text"
version="1.0"
default-locale="uk-UA"
demote-non-dropping-particle="never">
<info>
<title>ДСТУ 8302:2015 — бібліографічні посилання</title>
<title-short>ДСТУ 8302:2015</title-short>
<id>dstu-8302-2015-typst</id>
<link href="https://online.budstandart.com/ua/catalog/doc-page.html?id_doc=64411" rel="documentation"/>
<author>
<name>Custom Typst CSL</name>
</author>
<category citation-format="numeric"/>
<category field="generic-base"/>
<summary>CSL для оформлення переліку джерел за логікою ДСТУ 8302:2015 у Typst/Pandoc: числові посилання, українські скорочення, DOI/URL, дата звернення, коректні вебджерела, статті та нормативні акти.</summary>
<updated>2026-05-25T00:00:00+00:00</updated>
<rights license="http://creativecommons.org/licenses/by-sa/4.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 License</rights>
</info>
<locale xml:lang="uk-UA">
<terms>
<term name="accessed">дата звернення</term>
<term name="page" form="short">С.</term>
<term name="page" form="symbol">с.</term>
<term name="volume" form="short">Т.</term>
<term name="issue" form="short">№</term>
<term name="edition" form="short">вид.</term>
<term name="editor" form="short">ред.</term>
<term name="translator" form="short">пер.</term>
<term name="et-al">та ін.</term>
<term name="and">,</term>
</terms>
</locale>
<!-- Ініціали після прізвища: Vergara L. M., Park M. -->
<macro name="contributors">
<names variable="author">
<name name-as-sort-order="all"
sort-separator=" "
initialize-with=". "
delimiter=", "
delimiter-precedes-last="never"/>
<substitute>
<names variable="editor">
<name name-as-sort-order="all"
sort-separator=" "
initialize-with=". "
delimiter=", "
delimiter-precedes-last="never"/>
<label form="short" prefix=" (" suffix=")"/>
</names>
<names variable="translator">
<name name-as-sort-order="all"
sort-separator=" "
initialize-with=". "
delimiter=", "
delimiter-precedes-last="never"/>
<label form="short" prefix=" (" suffix=")"/>
</names>
</substitute>
</names>
</macro>
<macro name="title">
<text variable="title"/>
</macro>
<macro name="container">
<text variable="container-title"/>
</macro>
<macro name="site-or-publisher">
<choose>
<if variable="container-title">
<text variable="container-title"/>
</if>
<else>
<text variable="publisher"/>
</else>
</choose>
</macro>
<macro name="issued-year">
<date variable="issued">
<date-part name="year"/>
</date>
</macro>
<macro name="issued-date">
<date variable="issued">
<date-part name="day" form="numeric-leading-zeros" suffix="."/>
<date-part name="month" form="numeric-leading-zeros" suffix="."/>
<date-part name="year"/>
</date>
</macro>
<macro name="accessed-date">
<date variable="accessed">
<date-part name="day" form="numeric-leading-zeros" suffix="."/>
<date-part name="month" form="numeric-leading-zeros" suffix="."/>
<date-part name="year"/>
</date>
</macro>
<macro name="publisher-place">
<group delimiter=" : ">
<text variable="publisher-place"/>
<text variable="publisher"/>
</group>
</macro>
<macro name="edition">
<group delimiter=" ">
<text variable="edition"/>
<label variable="edition" form="short"/>
</group>
</macro>
<macro name="book-pages">
<choose>
<if variable="number-of-pages">
<group delimiter=" ">
<text variable="number-of-pages"/>
<text term="page" form="symbol"/>
</group>
</if>
<else-if variable="page">
<group delimiter=" ">
<text variable="page"/>
<text term="page" form="symbol"/>
</group>
</else-if>
</choose>
</macro>
<macro name="pages">
<choose>
<if variable="page">
<group delimiter=" ">
<text term="page" form="short"/>
<text variable="page"/>
</group>
</if>
</choose>
</macro>
<macro name="volume-issue">
<group delimiter=", ">
<choose>
<if variable="volume">
<group delimiter=" ">
<text value="Т."/>
<text variable="volume"/>
</group>
</if>
</choose>
<choose>
<if variable="issue">
<group delimiter=" ">
<text value="№"/>
<text variable="issue"/>
</group>
</if>
</choose>
</group>
</macro>
<macro name="url-access">
<choose>
<if variable="URL">
<group delimiter=" ">
<text variable="URL" prefix="URL: "/>
<choose>
<if variable="accessed">
<group delimiter=": " prefix="(" suffix=")">
<text term="accessed"/>
<text macro="accessed-date"/>
</group>
</if>
</choose>
</group>
</if>
</choose>
</macro>
<macro name="doi-or-url">
<choose>
<if variable="DOI">
<text variable="DOI" prefix="DOI: "/>
</if>
<else>
<text macro="url-access"/>
</else>
</choose>
</macro>
<macro name="legal-details">
<choose>
<if variable="genre">
<group delimiter=" : ">
<text macro="title"/>
<group delimiter=" ">
<text variable="genre"/>
<choose>
<if variable="issued">
<group delimiter=" ">
<text value="від"/>
<text macro="issued-date"/>
</group>
</if>
</choose>
<choose>
<if variable="number">
<group delimiter=" ">
<text value="№"/>
<text variable="number"/>
</group>
</if>
</choose>
</group>
</group>
</if>
<else>
<group delimiter=". ">
<text macro="title"/>
<group delimiter=" ">
<choose>
<if variable="issued">
<group delimiter=" ">
<text value="від"/>
<text macro="issued-date"/>
</group>
</if>
</choose>
<choose>
<if variable="number">
<group delimiter=" ">
<text value="№"/>
<text variable="number"/>
</group>
</if>
</choose>
</group>
</group>
</else>
</choose>
</macro>
<citation collapse="citation-number">
<sort>
<key variable="citation-number"/>
</sort>
<layout delimiter=", " prefix="[" suffix="]">
<text variable="citation-number"/>
</layout>
</citation>
<bibliography hanging-indent="false" entry-spacing="0" line-spacing="1">
<sort>
<key variable="citation-number"/>
</sort>
<layout>
<group display="block">
<text display="left-margin" variable="citation-number" suffix=". "/>
<choose>
<!-- Нормативні акти: Цивільний кодекс України : Закон України від 16.01.2003 № 435-IV. URL: ... -->
<if type="legislation bill legal_case treaty" match="any">
<group delimiter=". " suffix=".">
<text macro="legal-details"/>
<text macro="url-access"/>
</group>
</if>
<!-- Наукова стаття -->
<else-if type="article-journal">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<text macro="container"/>
<text macro="issued-year"/>
<text macro="volume-issue"/>
<text macro="pages"/>
<text macro="doi-or-url"/>
</group>
</else-if>
<!-- Газетна / журнальна стаття без DOI -->
<else-if type="article-newspaper article-magazine" match="any">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<text variable="container-title" font-style="italic"/>
<text macro="issued-date"/>
<text macro="pages"/>
<text macro="url-access"/>
</group>
</else-if>
<!-- Вебсторінки: без дублювання назви замість автора -->
<else-if type="webpage post post-weblog" match="any">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<text macro="site-or-publisher"/>
<text macro="issued-date"/>
<text macro="url-access"/>
</group>
</else-if>
<!-- Книга -->
<else-if type="book">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<text macro="edition"/>
<group delimiter=", ">
<text macro="publisher-place"/>
<text macro="issued-year"/>
</group>
<text macro="book-pages"/>
<text macro="doi-or-url"/>
</group>
</else-if>
<!-- Розділ у книзі -->
<else-if type="chapter">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<group delimiter=" ">
<text value="In:"/>
<text variable="container-title" font-style="italic"/>
</group>
<names variable="editor">
<name name-as-sort-order="all"
sort-separator=" "
initialize-with=". "
delimiter=", "
delimiter-precedes-last="never"/>
<label form="short" prefix=" (" suffix=")"/>
</names>
<group delimiter=", ">
<text macro="publisher-place"/>
<text macro="issued-year"/>
</group>
<text macro="pages"/>
<text macro="doi-or-url"/>
</group>
</else-if>
<!-- Матеріали конференції -->
<else-if type="paper-conference">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<group delimiter=" ">
<text value="In:"/>
<text variable="container-title"/>
</group>
<group delimiter=", ">
<text macro="publisher-place"/>
<text macro="issued-year"/>
</group>
<text macro="pages"/>
<text macro="doi-or-url"/>
</group>
</else-if>
<!-- Дисертації, звіти -->
<else-if type="thesis report" match="any">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<text variable="genre"/>
<group delimiter=", ">
<text macro="publisher-place"/>
<text macro="issued-year"/>
</group>
<text macro="book-pages"/>
<text macro="doi-or-url"/>
</group>
</else-if>
<!-- Патенти -->
<else-if type="patent">
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<group delimiter=" ">
<text value="№"/>
<text variable="number"/>
</group>
<text macro="issued-date"/>
<text macro="url-access"/>
</group>
</else-if>
<!-- Універсальний fallback -->
<else>
<group delimiter=". " suffix=".">
<text macro="contributors"/>
<text macro="title"/>
<text variable="container-title" font-style="italic"/>
<group delimiter=", ">
<text macro="publisher-place"/>
<text macro="issued-year"/>
</group>
<text macro="pages"/>
<text macro="doi-or-url"/>
</group>
</else>
</choose>
</group>
</layout>
</bibliography>
</style>
+1 -1
View File
@@ -14,7 +14,7 @@
"грудня",
).at(month - 1)
#let is-cyr(c) = regex("^\p{Cyrillic}") in c
#let is-cyr(c) = regex("[\p{Cyrillic}]") in c
/// type-safe emptiness check
#let is-empty(val) = {
+15 -103
View File
@@ -5,9 +5,6 @@
#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
@@ -32,8 +29,7 @@
bib-path: none,
appendices: (),
) = {
let doc-title = if type(title) == array { title.join(" ") } else { title }
set document(title: doc-title, author: authors.map(c => c.name))
set document(title: title, author: authors.map(c => c.name))
show: style.dstu.with(skip: 1)
@@ -60,86 +56,6 @@
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
/// - committee-members (array): Optional list of commission member dictionaries for the title page
/// - 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: (),
committee-members: none,
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
}
let committee_members = committee-members
tp.cw-v2.nure(
university,
title,
authors,
mentors,
committee_members,
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-8302-2015.csl", full: true)
}
style.appendices(appendices)
}
/// Practice and Laboratory works template
/// - layout (str): "default", "minimal", or "complex"
/// - university (str): University code
@@ -161,7 +77,6 @@
title: none,
authors: (),
mentors: (),
skip-heading: false,
) = {
assert(authors.len() > 0, message: "At least one author required")
@@ -174,28 +89,25 @@
// Select layout variant
let layouts = (
"complex": () => tp.pz-lb.complex(uni, edu-program, subject, type, number, title, authors, mentors),
"ХНУРЕ": () => tp.pz-lb.nure(uni, edu-program, subject, type, number, title, authors, mentors),
"default": () => tp.pz-lb.nure(uni, edu-program, subject, type, number, title, authors, mentors),
"complex": tp.pz-lb.complex(uni, edu-program, subject, type, number, title, authors, mentors),
"ХНУРЕ": tp.pz-lb.nure(uni, edu-program, subject, type, number, title, authors, mentors),
"default": tp.pz-lb.nure(uni, edu-program, subject, type, number, title, authors, mentors),
)
(layouts.at(university, default: layouts.default))()
layouts.at(university, default: layouts.default)
if not skip-heading {
pagebreak(weak: true)
pagebreak(weak: true)
// Set heading counter based on title/number
if title == none {
if number == none { context counter(heading).update(1) } else {
context counter(heading).update(number)
}
} else {
if number != none {
context counter(heading).update(number - 1)
}
heading(eval(title, mode: "markup"))
// Set heading counter based on title/number
if title == none {
if number == none { context counter(heading).update(1) } else {
context counter(heading).update(number)
}
} else {
if number != none {
context counter(heading).update(number - 1)
}
heading(eval(title, mode: "markup"))
}
doc
+25 -224
View File
@@ -6,11 +6,7 @@
#let double-half-spacing = spacing * 2.5
/// 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
#let ukr-enum = "абвгдежиклмнпрстуфхцшщюя".split("")
/// Helper for level 2/3 heading blocks
#let heading-block(it, num: auto) = {
@@ -23,125 +19,6 @@
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,
tag: 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)
[#metadata((kind: "dstu-table", number: num)) #if tag != none { label(tag) }]
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,
@@ -158,15 +35,12 @@
// Text and paragraph
set text(lang: "uk", size: 14pt, hyphenate: false, font: ("Times New Roman", "Liberation Serif"))
set par(justify: true, spacing: spacing, leading: spacing, first-line-indent: (
amount: indent-size,
all: true,
))
set par(justify: true, spacing: spacing, leading: spacing, first-line-indent: (amount: indent-size, all: true))
set block(spacing: spacing)
set underline(evade: false)
// Lists
set enum(indent: indent-size, body-indent: 0.5cm, numbering: i => ukr-enum.at(i - 1) + ")")
set enum(indent: indent-size, body-indent: 0.5cm, numbering: i => ukr-enum.at(i) + ")")
show enum: it => {
set enum(indent: 0em, numbering: "1)")
it
@@ -174,63 +48,6 @@
set list(indent: indent-size + 0.1cm, body-indent: 0.5cm, marker: [--])
// Figures
show ref: it => {
let target = it.element
if target == none {
return it
}
// dstu-table refs carry a number already resolved at the table location.
if (
target.func() == metadata
and type(target.value) == dictionary
and target.value.at("kind", default: none) == "dstu-table"
) {
return link(target.location())[#target.value.at("number")]
}
if target.func() != figure and target.func() != math.equation {
return it
}
// A figure or equation number must be read at the element itself, not at the citation site.
// The default ref display re-runs the numbering function here, where its inner `counter(heading)`
// context resolves to the citing chapter and prints the wrong section or appendix letter.
// We sample every counter at the target location instead.
let loc = target.location()
let appendix = dstu-table-appendix.at(loc)
let section = if appendix == none {
str(counter(heading).at(loc).first())
} else {
upper(ukr-enum.at(appendix - 1))
}
let number = if target.func() == figure {
let idx = counter(figure.where(kind: target.kind)).at(loc).first()
section + "." + str(idx)
} else {
let idx = counter(math.equation).at(loc).first()
"(" + section + "." + str(idx) + ")"
}
let supplement = if it.supplement == auto {
target.supplement
} else {
it.supplement
}
let body = if supplement != auto and supplement != none {
[#supplement #number]
} else {
[#number]
}
link(loc, body)
}
show figure: it => {
v(double-spacing, weak: true)
it
@@ -248,7 +65,6 @@
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)
it
}
set figure(numbering: i => context numbering("1.1", counter(heading).get().at(0), i))
@@ -281,15 +97,6 @@
v(double-half-spacing, weak: true)
}
// blocks `like this` aren't welcome, so ` is replaced with "
show raw.where(block: false): it => text(
lang: "uk",
size: 14pt,
hyphenate: false,
weight: "regular",
font: ("Times New Roman", "Liberation Serif"),
)["#it.text"]
it
}
@@ -297,35 +104,29 @@
#let appendices(it) = {
counter(heading).update(0)
set heading(numbering: (i, ..n) => upper(ukr-enum.at(i - 1)) + numbering(".1.1", ..n))
set heading(supplement: [Додаток])
context {
let app-letter = upper(ukr-enum.at(counter(heading).get().at(0)))
set heading(numbering: (i, ..n) => upper(ukr-enum.at(i)) + numbering(".1.1", ..n))
set figure(numbering: i => app-letter + "." + str(i))
set math.equation(numbering: i => app-letter + "." + str(i))
set heading(supplement: [Додаток])
let app-letter = context upper(ukr-enum.at(counter(heading).get().at(0) - 1))
set figure(numbering: i => app-letter + "." + str(i))
set math.equation(numbering: i => [(#app-letter.#str(i))])
show heading: h => {
set text(size: 14pt)
if h.level == 1 {
counter(math.equation).update(0)
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")
pagebreak(weak: true)
bold([ДОДАТОК #counter(heading).display(auto)])
linebreak()
h.body
v(double-spacing, weak: true)
} else {
set text(weight: "regular")
heading-block(h)
show heading: h => {
set text(size: 14pt)
if h.level == 1 {
set align(center)
set text(weight: "regular")
pagebreak(weak: true)
bold([ДОДАТОК #counter(heading).display(auto)])
linebreak()
h.body
v(double-spacing, weak: true)
} else {
set text(weight: "regular")
heading-block(h)
}
}
}
it
it
}
}
-1
View File
@@ -1 +0,0 @@
#import "nure.typ": *
-394
View File
@@ -1,394 +0,0 @@
#import "../../shared.typ": universities
#import "../../helpers.typ": *
#import "../../style.typ": spacing
#import "../../utils.typ": bold, uline, filled-lines, hfill
#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.join())
uline(align: center, [])
note[(тема)]
}
#let commission-lines(members) = {
if members.len() > 0 {
for (i, member) in members.enumerate() {
if i > 0 {
linebreak()
}
let member-display-name = member.at("display-name", default: member.name)
let member-degree = member.at("degree", default: "")
uline(align: left, [#member-degree #member-display-name])
}
} else {
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)
}
}
#let nure(
university,
title,
authors,
mentors,
committee_members,
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 commission-members = if committee_members == none {
mentors.slice(1)
} else {
committee_members
}
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(
[Кафедра],
lower(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.15em)
#commission-lines(commission-members)
]
],
)
#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,
),
),
([Кафедра], lower(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)
Здобувач #underline([#hfill(6cm)])
#note[(підпис) #h(8cm)]
#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 dstu-tables = query(metadata).filter(it => type(it.value) == dictionary and it.value.at("kind", default: none) == "dstu-table").len()
#let tables = query(figure.where(kind: table)).len() + dstu-tables
#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(", ").
]
\
#let keyword-pairs = if abstract.keywords.len() > 0 and type(abstract.keywords.first()) == array {
abstract.keywords.map(pair => (
uk: pair.at(0),
en: pair.at(1),
))
} else if abstract.at("en", default: none) != none and abstract.en.at("keywords", default: none) != none {
abstract.keywords.enumerate().map(((i, uk)) => (
uk: uk,
en: abstract.en.keywords.at(i),
))
} else {
abstract.keywords.map(uk => (uk: uk))
}
#let sorted-keyword-pairs = keyword-pairs.sorted(by: (a, b) => {
if is-cyr(a.uk) != is-cyr(b.uk) { is-cyr(a.uk) } else { a.uk < b.uk }
})
#(sorted-keyword-pairs.map(pair => upper(pair.uk)).join(", "))
\
#abstract.text
#if abstract.at("en", default: none) != none or (keyword-pairs.len() > 0 and keyword-pairs.first().at("en", default: none) != none) [
\
#(sorted-keyword-pairs.map(pair => upper(pair.en)).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 -2
View File
@@ -210,8 +210,7 @@
#context [
#let pages = counter(page).final().at(0)
#let images = query(figure.where(kind: image)).len()
#let dstu-tables = query(metadata).filter(it => type(it.value) == dictionary and it.value.at("kind", default: none) == "dstu-table").len()
#let tables = query(figure.where(kind: table)).len() + dstu-tables
#let tables = query(figure.where(kind: table)).len()
#let bibs = bib-count.final().dedup().len()
/* TODO: why this stopped working?
#let tables = counter(figure.where(kind: table)).final().at(0)
-1
View File
@@ -1,3 +1,2 @@
#import "pz-lb/main.typ" as pz-lb
#import "coursework/main.typ" as cw
#import "coursework-v2/main.typ" as cw-v2
+3 -6
View File
@@ -20,15 +20,12 @@
#if authors.len() == 1 {
let a = authors.first()
[#gender-form("author", dict: a):\ ]
if ("edu-program" in a) and ("group" in a) [ст. гр. #a.edu-program\-#a.group\ ]
[ст. гр. #a.edu-program\-#a.group\ ]
[#a.name\ ]
if ("variant" in a) and (not is-empty(a.variant)) [Варіант: #a.variant]
if not is-empty(a.variant) [Варіант: #a.variant]
} else if authors.len() > 1 [
#gender-form("author"):\
#for a in authors [
#if ("edu-program" in a) and ("group" in a) [ ст. гр. #a.edu-program\-#a.group\ ]
#a.name\
]
#for a in authors [ст. гр. #a.edu-program\-#a.group\ #a.name\ ]
]
#colbreak()
-7
View File
@@ -7,13 +7,6 @@
/// 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) }
@@ -1,4 +1,4 @@
#import "@local/nure:0.1.1": *
#import "@local/nure:0.1.0": *
#import style: spacing
#import "utils.typ": img
-143
View File
@@ -1,143 +0,0 @@
#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 committee_members = (
(name: "Груздо І.В.", degree: "Доц."),
(name: "Зибіна К.В.", degree: "Ст. викл."),
(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: (
("веб-застосунок", "WEB APPLICATION"),
("інформаційна система", "INFORMATION SYSTEM"),
("курсова робота", "COURSEWORK"),
("програмна інженерія", "SOFTWARE ENGINEERING"),
("тестовий приклад", "DEMO SAMPLE"),
),
text: [
Мета даної роботи -- продемонструвати оформлення пояснювальної записки
для комплексного курсового проєкту з використанням нового варіанта шаблону.
Приклад містить узагальнену тему, типові сторінки завдання, календарного плану,
реферату, змісту, переліку джерел посилання та додатків.
У роботі наведено умовну структуру програмної системи, що може бути
адаптована під конкретну предметну область. Основний акцент зроблено на
перевірці полів титульної сторінки, сторінки завдання, службових підписів,
нумерації розділів і коректної роботи додатків.
Результатом є демонстраційний документ, який показує очікуване використання
шаблону без прив'язки до реального студента, викладача або завершеної роботи.
],
)
#let abstract_en = (
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,
committee-members: committee_members,
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)
-66
View File
@@ -1,66 +0,0 @@
[settings]
quiet = true
env_shell_expand = true
lockfile = true
[tools]
typst = "latest"
typstyle = "latest"
tinymist = "latest"
yq = "latest"
[vars]
vendor_dir = "{{config_root}}/vendor"
package_dir = "{{vars.vendor_dir}}/typst-packages"
work_doc_config = "src/doc.toml"
work_input_file = "src/main.typ"
work_output_file = "{{config_root}}/main.pdf"
nure_package_repo = "https://gitea.linerds.us/pencelheimer/typst_nure_template.git"
nure_package_ref = "0.1.1"
nure_package_rev = "d6037872ddb6391afec147a679fa05f08cba6bb0"
nure_package_name = "vendor/nure"
nure_package_ver = "{{vars.nure_package_ref}}"
nure_package_path = "{{vars.package_dir}}/{{vars.nure_package_name}}/{{vars.nure_package_ver}}"
[env]
TYPST_PACKAGE_DIR = "{{vars.package_dir}}"
TYPST_PACKAGE_PATH = "$TYPST_PACKAGE_DIR"
[tasks]
compile.depends = ["fetch-nure-package"]
watch.depends = ["fetch-nure-package"]
compile.run = "mise exec -- typst compile {{vars.work_input_file}} {{vars.work_output_file}}"
watch.run = "mise exec -- typst watch {{vars.work_input_file}} {{vars.work_output_file}}"
format.run = "mise exec -- typstyle -l 120 -i $MISE_PROJECT_ROOT/src"
clean.run = "rm -rvif *.pdf"
update-package-rev.run = "mise config set vars.nure_package_rev $(git ls-remote {{vars.nure_package_repo}} refs/heads/{{vars.nure_package_ref}} | cut -f1)"
[tasks.fetch-nure-package]
silent = "stdout"
run = """
#!/usr/bin/env bash
if [ ! -d {{vars.nure_package_path}}/.git ]; then
git clone --depth 1 --revision {{vars.nure_package_rev}} {{vars.nure_package_repo}} {{vars.nure_package_path}}
else
git -C {{vars.nure_package_path}} fetch --depth 1 origin {{vars.nure_package_rev}}
git -C {{vars.nure_package_path}} checkout {{vars.nure_package_rev}}
fi
"""
[tasks.rename]
depends = ["compile"]
run = """
#!/usr/bin/env bash
# Generates the following name from doc.toml: ПЗ1_Прізвище_ПЗПІ-23-2_ПарП.pdf
# Assumes authors.at(0).name to be in the following format: "Прізвище І. Б."
final_name=$(mise exec -- yq '.type + (.number | tostring) + "_" + (.authors[0].name | split(" "))[0] + "_" + (.authors[0].edu-program + "-" + .authors[0].group) + "_" + .subject + ".pdf"' "{{vars.work_doc_config}}");
cp -v "{{vars.work_output_file}}" "$final_name"
"""
-35
View File
@@ -1,35 +0,0 @@
[[tools.tinymist]]
version = "0.14.14"
backend = "aqua:Myriad-Dreamin/tinymist"
"platforms.linux-arm64" = { checksum = "sha256:39eb99d290fd1aa0e4508e5d5bbbf5a49648cf26da8e7bc3ff49f75c5fd0dc6c", url = "https://github.com/Myriad-Dreamin/tinymist/releases/download/v0.14.14/tinymist-linux-arm64"}
"platforms.linux-x64" = { checksum = "sha256:cda2ba76a455f3577cf2a1bd6340b75491f0c673770f631cf4d3232663860776", url = "https://github.com/Myriad-Dreamin/tinymist/releases/download/v0.14.14/tinymist-linux-x64"}
"platforms.macos-arm64" = { checksum = "sha256:ea7a22af0d54a71fbd1ee6c8c9f8434b521549926ae0f53a82391adeea741aba", url = "https://github.com/Myriad-Dreamin/tinymist/releases/download/v0.14.14/tinymist-darwin-arm64"}
"platforms.macos-x64" = { checksum = "sha256:f640889414d5030de1f03b966ccb10a84941bc1176980ff956d66714508ccd85", url = "https://github.com/Myriad-Dreamin/tinymist/releases/download/v0.14.14/tinymist-darwin-x64"}
"platforms.windows-x64" = { checksum = "sha256:3d39c7b417287d8689c69159b65fbc62427e141b9489500ad81039ee783f2549", url = "https://github.com/Myriad-Dreamin/tinymist/releases/download/v0.14.14/tinymist-win32-x64.exe"}
[[tools.typst]]
version = "0.14.2"
backend = "aqua:typst/typst"
"platforms.linux-arm64" = { checksum = "sha256:491b101aa40a3a7ea82a3f8a6232cabb4e6a7e233810082e5ac812d43fdcd47a", url = "https://github.com/typst/typst/releases/download/v0.14.2/typst-aarch64-unknown-linux-musl.tar.xz"}
"platforms.linux-x64" = { checksum = "sha256:a6044cbad2a954deb921167e257e120ac0a16b20339ec01121194ff9d394996d", url = "https://github.com/typst/typst/releases/download/v0.14.2/typst-x86_64-unknown-linux-musl.tar.xz"}
"platforms.macos-arm64" = { checksum = "sha256:470aa49a2298d20b65c119a10e4ff8808550453e0cb4d85625b89caf0cedf048", url = "https://github.com/typst/typst/releases/download/v0.14.2/typst-aarch64-apple-darwin.tar.xz"}
"platforms.macos-x64" = { checksum = "sha256:4e91d8e1e33ab164f949c5762e01ee3faa585c8615a2a6bd5e3677fa8506b249", url = "https://github.com/typst/typst/releases/download/v0.14.2/typst-x86_64-apple-darwin.tar.xz"}
"platforms.windows-x64" = { checksum = "sha256:51353994ac83218c3497052e89b2c432c53b9d4439cdc1b361e2ea4798ebfc13", url = "https://github.com/typst/typst/releases/download/v0.14.2/typst-x86_64-pc-windows-msvc.zip"}
[[tools.typstyle]]
version = "0.14.4"
backend = "aqua:Enter-tainer/typstyle"
"platforms.linux-arm64" = { checksum = "sha256:b8c220c0d940d7690fb15ef10415de35fdc115465a7534d8102459c68aeec74d", url = "https://github.com/typstyle-rs/typstyle/releases/download/v0.14.4/typstyle-aarch64-unknown-linux-gnu"}
"platforms.linux-x64" = { checksum = "sha256:48d9a9a3885855f1b4f6c2b8ea9739623bc458b99c015b77c4f50a1f342ea091", url = "https://github.com/typstyle-rs/typstyle/releases/download/v0.14.4/typstyle-x86_64-unknown-linux-musl"}
"platforms.macos-arm64" = { checksum = "sha256:dda3a162f2457a40570c3552cc9c01004138b33f2f921112b117d72f48bd6d56", url = "https://github.com/typstyle-rs/typstyle/releases/download/v0.14.4/typstyle-aarch64-apple-darwin"}
"platforms.macos-x64" = { checksum = "sha256:2343f7a4801d9bbda5d7441e8fea95f98b590d6aa3083da5c4e4ca3de885350f", url = "https://github.com/typstyle-rs/typstyle/releases/download/v0.14.4/typstyle-x86_64-apple-darwin"}
"platforms.windows-x64" = { checksum = "sha256:b16aebaed0aa296ef3e055d611f1e917f5849d3eb0f953bbcaa6f09f0503fb5b", url = "https://github.com/typstyle-rs/typstyle/releases/download/v0.14.4/typstyle-x86_64-pc-windows-msvc.exe"}
[[tools.yq]]
version = "4.52.2"
backend = "aqua:mikefarah/yq"
"platforms.linux-arm64" = { checksum = "sha256:c82856ac30da522f50dcdd4f53065487b5a2927e9b87ff637956900986f1f7c2", url = "https://github.com/mikefarah/yq/releases/download/v4.52.2/yq_linux_arm64"}
"platforms.linux-x64" = { checksum = "sha256:a74bd266990339e0c48a2103534aef692abf99f19390d12c2b0ce6830385c459", url = "https://github.com/mikefarah/yq/releases/download/v4.52.2/yq_linux_amd64"}
"platforms.macos-arm64" = { checksum = "sha256:34613ea97c4c77e1894a8978dbf72588d187a69a6292c10dab396c767a1ecde7", url = "https://github.com/mikefarah/yq/releases/download/v4.52.2/yq_darwin_arm64"}
"platforms.macos-x64" = { checksum = "sha256:54a63555210e73abed09108097072e28bf82a6bb20439a72b55509c4dd42378d", url = "https://github.com/mikefarah/yq/releases/download/v4.52.2/yq_darwin_amd64"}
"platforms.windows-x64" = { checksum = "sha256:2b6cd8974004fa0511f6b6b359d2698214fadeb4599f0b00e8d85ae62b3922d4", url = "https://github.com/mikefarah/yq/releases/download/v4.52.2/yq_windows_amd64.exe"}
-265
View File
@@ -1,265 +0,0 @@
# unexplrd's example setup with mise
This example has the following structure:
```
.
├── .mise/
│ ├── config.toml
│ └── mise.lock
├── vendor/
│ └── typst-packages/
│ └── ...
└── src/
├── assets/
│ ├── foo.csv
│ ├── bar.c
│ └── ...
├── figures/
│ ├── clojure-logo.png
│ ├── error-log.jpg
│ └── ...
├── doc.toml
├── main.typ
└── utils.typ
```
## Advantages
- Minimal
- All you need is [`mise`](https://mise.jdx.dev/) and `git`
- Declarative
- Sets up the environment and dependencies automatically
- Every work config is separate to avoid breaking changes in Typst and `nure` package
- Customizable
- Add your own mise tasks, change existing tasks, it's just a `.toml` file
## Project structure
All work contents are stored in `src/` to avoid clutter.
### Files
- `src/doc.toml`: work-specific settings, could be auto-filled, useful for scripts, see (TODO: add a demo nushell script)
- `src/utils.typ`: utilities and functions used across the project
- `src/main.typ`: main entry file
### Directories
- `src/chapters/`: for breaking up the project into multiple files, if necessary
- `src/figures/`: for images and pictures
- `src/assets/`: other non-image, non-typst files
## Mise
### Tasks
- `compile`: Execute `typst compile` on `src/main.typ`
- `watch`: Execute `typst watch` on `src/main.typ`
- `rename`: Copy output `.pdf` file with an auto-generated name from metadata in `doc.toml`
- `fetch-nure-package`: clone package git repository to a directory specified in `[vars]`
- `update-package-rev`: updates `vars.nure_package_rev` with the last commit id from the specified branch (`vars.nure_package_ref`)
## Examples
### Templating
I have a following per-semester directory structure:
```
/home/user/Documents/nure/
└── .config/
├── defaults.yaml
├── subjects.yaml
├── mise/
│ ├── config.toml
│ └── mise.lock
└── template/
├── .mise
│ ├── config.toml
│ └── mise.lock
└── src
├── appendices.typ
├── assets/
├── figures/
├── main.typ
└── utils.typ
```
<details><summary>Contents of <code>.config/default.yaml</code></summary><p>
```yaml
university: ХНУРЕ
default_author:
name: Косач Л. П.
edu-program: ПЗПІ
group: 23-2
gender: f
variant: 8
full-name-gen: Косач Лариси Петрівни
course: 2
semester: 4
work_types:
lb: ЛБ
pz: ПЗ
kr: КР
```
</p></details>
<details><summary>Contents of <code>.config/subjects.yaml</code></summary><p>
```yaml
smp:
name: СМП
full_name: Скриптові мови програмування
mentors:
- name: Шевченко Т. Г.
degree: Доцент кафедри ПІ
gender: m
- name: Франко І. Я.
degree: Асистент кафедри ПІ
gender: m
iks:
name: ІКС
full_name: Інформаційно-комунікаційні системи
mentors:
- name: Мартинчук О. О.
degree: Доцент кафедри ІКІ
gender: m
# etc...
```
</p></details>
<details><summary>Contents of <code>.config/mise/config.toml</code></summary><p>
```toml
[settings]
quiet = true
env_shell_expand = true
lockfile = true
[tools]
"aqua:nushell/nushell" = "latest"
[tasks.work]
usage = '''
arg "<subject>" help="Target environment"
arg "<type>" help="Target environment"
arg "<number>" help="Target environment"
'''
run = '''
#!/usr/bin/env nu
let template_dir = ".config/template"
let config_dir = ".config"
# let out_dir = $"(pwd)"
let out_dir = $"(pwd)/works"
def not_main [subject: string, shortcode: string, number?: int] {
let subject_lower = ($subject | str downcase)
let shortcode_lower = ($shortcode | str downcase)
validate-input $subject $shortcode $number
let temp_dir = $"/tmp/nure-work-(random chars -l 8)"
let final_dir = $"($out_dir)/($subject_lower)/($shortcode_lower)($number)"
let subject_dir = $"($out_dir)/($subject_lower)"
# TODO?: replace with git clone
cp -r ($config_dir)/template $temp_dir
generate-doc-toml $shortcode_lower $subject_lower $temp_dir $number
check-mkdir $out_dir
if ($final_dir | path exists) {
error make -u {msg: $"Directory ($final_dir) already exists"}
}
check-mkdir $subject_dir; cp -r $temp_dir $final_dir; rm -rf $temp_dir
print $"=> Created new work: ($final_dir)"
}
def check-mkdir [path: string] { if not ($path | path exists) { mkdir $path } }
def validate-input [subject: string, shortcode: string, number?: int] {
if not ($config_dir | path exists) {
error make -u {msg: $"($config_dir) not found"}
}
let defaults_yaml = $"($config_dir)/defaults.yaml"
let subjects_yaml = $"($config_dir)/subjects.yaml"
if not ($defaults_yaml | path exists) {
error make -u {msg: $"defaults.yaml not found in ($config_dir)"}
}
if not ($subjects_yaml | path exists) {
error make -u {msg: $"subjects.yaml not found in ($config_dir)"}
}
if ($number != nothing and $number < 1 ) {
error make -u {msg: "Work number should be greater than 0"}
}
let subject_lower = ($subject | str downcase)
let subjects = (open $subjects_yaml | columns)
if $subject_lower not-in $subjects {
error make -u {
msg: $"Subject '($subject)' not found in configuration"
help: $"Available subjects: ($subjects | str join ', ')"
}
}
}
def generate-doc-toml [shortcode: string, subject: string, target_dir: string, number?: int] {
let defaults = (open $"($config_dir)/defaults.yaml")
let subjects = (open $"($config_dir)/subjects.yaml")
let subject_data = ($subjects | get $subject)
let work_type = ($defaults | get work_types | try { get $shortcode } catch { $shortcode })
{
university: ($defaults | get university)
subject: ($subject_data | get name), type: $work_type, number: $number
mentors: ($subject_data | get mentors), authors: [($defaults | get default_author)]
} | compact --empty | to toml | save $"($target_dir)/src/doc.toml" --force
}
not_main {{usage.subject}} {{usage.type}} {{usage.number}}
'''
```
</p></details>
`template/` is this empty template without `doc.toml`
Nushell script does two things:
- Creates a new work from a template: `works/smp/lb1`
- Auto-fills `doc.toml` with data from `defaults.yaml` and mentor data from `subjects.yaml`
### Patching the package
A patch to add new subjects:
```diff
diff --git a/src/config/universities.yaml b/src/config/universities.yaml
index 8855a07..1aefc96 100644
--- a/src/config/universities.yaml
+++ b/src/config/universities.yaml
@@ -60,7 +60,6 @@
ПЕСЕ: Психологія екстремальних стосунків та ефективної адаптації
ПНП: Програмування на платформі .NЕТ
ПП: Проектний практикум
- ПРОГ: Програмування
ПарП: Параллельне програмування
СА: Системний аналіз
СМП: Скриптові мови програмування
@@ -76,3 +75,10 @@
ФІЛ: Філософія
ФВС: Фізичне виховання та спорт
ХТ: Хмарні технології
+ АКС: Архітектура комп'ютерних систем
+ ІКС: Інформаційно-комунікаційні системи
+ МБ: Мережна безпека
+ МКр: Мережна криміналістика
+ ОКЗІ: Основи криптографічного захисту інформації
+ Прог: Програмування
+ ТІК: Теорія інформації та кодування
```
To apply it, add the following command to the end of `fetch_nure_package` task:
```sh
git -C {{vars.nure_package_path}} apply --check --apply --quiet $MISE_PROJECT_ROOT/custom-subjects.patch || exit 0
```
-19
View File
@@ -1,19 +0,0 @@
university = "ХНУРЕ"
subject = "АКС"
type = "ЛБ"
number = 1
[[mentors]]
name = ""
degree = ""
gender = ""
[[authors]]
name = ""
edu-program = "КУІБ"
group = "24-1"
gender = "m"
variant = 6
full-name-gen = ""
course = 2
semester = 4
-28
View File
@@ -1,28 +0,0 @@
#import "@vendor/nure:0.1.1": *
#import "utils.typ": *
#import style: spacing
// Apply custom rule from utils.typ
// #show: correctly-indent-list-and-enum-items
// #show: style.dstu
#show: pz-lb.with(..toml("doc.toml"), title: "") // set title to none if empty
/// Useful snippets
/// Import a .csv table
// #figure(
// caption: [],
// table(
// columns: 4,
// table.header([], [], [], []),
// ..csv("assets/table.csv").flatten(),
// ),
// )
/// Appendices
// #style.appendices(include "chapters/appendices.typ")
// or
// #show: style.appendices
// = ...
-112
View File
@@ -1,112 +0,0 @@
#import "@vendor/nure:0.1.1": utils
/// captioned image with auto-generated label from path
/// Usage: img("path/to/image.png", "Caption")(optional: "source")
#let img(path, caption, ..sink) = {
let source = sink.pos().at(0, default: ())
[ #figure(image(path, ..sink.named()), caption: utils.img-caption(caption, source)) #utils.img-label(path) ]
}
/// takes in a string of code, e.g. #code(read("foo.c"))
#let code(content) = raw(block: true, theme: none, content)
/// read path as bytes
#let b(path) = bytes(read(path, encoding: none))
/// include chapters by file names from /chapters
#let chapters(ch) = (
array(ch).map(chapter => include str(chapter) + ".typ").join()
)
/// https://forum.typst.app/t/how-to-make-bullet-list-item-bodies-flow-like-paragraphs/3756/3?u=andrew
/// Spacing doesn't work the same way as native solution if par leading and
/// spacing are different.
#let correctly-indent-list-and-enum-items(doc) = {
let first-line-indent() = if type(par.first-line-indent) == dictionary {
par.first-line-indent.amount
} else {
par.first-line-indent
}
show list: li => {
for (i, it) in li.children.enumerate() {
let nesting = state("list-nesting", 0)
let indent = context h((nesting.get() + 1) * li.indent)
let get-nesting() = calc.div-euclid(nesting.get(), 10)
let marker = context {
let n = get-nesting()
if type(li.marker) == array {
li.marker.at(calc.rem-euclid(n, li.marker.len()))
} else if type(li.marker) == content {
li.marker
} else {
li.marker(n)
}
}
let parents = state("enum-parents", ()) // Support enum nesting.
let body = {
parents.update(arr => arr + (-1,))
nesting.update(x => x + 10)
it.body + parbreak()
nesting.update(x => x - 10)
parents.update(arr => arr.slice(0, -1))
}
let content = {
marker
h(li.body-indent)
body
}
context pad(left: int(nesting.get() != 0) * li.indent, content)
}
}
show enum: en => {
let start = if en.start == auto {
if en.children.first().has("number") {
if en.reversed { en.children.first().number } else { 1 }
} else {
if en.reversed { en.children.len() } else { 1 }
}
} else {
en.start
}
let number = start
for (i, it) in en.children.enumerate() {
number = if it.number != auto { it.number } else { number }
if en.reversed { number = start - i }
let parents = state("enum-parents", ())
let get-parents() = parents.get().filter(x => x >= 0)
let indent = context h((get-parents().len() + 1) * en.indent)
let num = if en.full {
context numbering(en.numbering, ..get-parents(), number)
} else {
numbering(en.numbering, number)
}
let max-num = if en.full {
context numbering(en.numbering, ..get-parents(), en.children.len())
} else {
numbering(en.numbering, en.children.len())
}
num = context box(
width: measure(max-num).width,
align(right, text(overhang: false, num)),
)
let list-nesting = state("list-nesting", 0) // Support list nesting.
let body = {
parents.update(arr => arr + (number,))
list-nesting.update(x => x + 1)
it.body + parbreak()
list-nesting.update(x => x - 1)
parents.update(arr => arr.slice(0, -1))
}
if not en.reversed { number += 1 }
let content = {
num
h(en.body-indent)
body
}
context pad(left: int(parents.get().len() != 0) * en.indent, content)
}
}
doc
}
+1 -1
View File
@@ -7,5 +7,5 @@ license = "GPL-3.0"
description = "Typst NURE package"
[template]
path = "template/default"
path = "template"
entrypoint = "lab.typ"