20 Commits

Author SHA1 Message Date
unexplrd 1d5ffd045c fix: typo and other 2026-01-29 18:54:32 +02:00
unexplrd a77366239b feat: add custom CSL for bibliography 2026-01-29 18:02:12 +02:00
unexplrd be23837699 1769603703 2026-01-28 14:35:09 +02:00
unexplrd f2c10c10bd 1769259968 2026-01-24 15:06:08 +02:00
unexplrd 173acfc35a 1769259936 2026-01-24 15:05:36 +02:00
unexplrd efd9d2c70c add img to utils.typ 2026-01-24 15:03:23 +02:00
unexplrd f897c6dca0 update readme 2026-01-24 15:01:44 +02:00
unexplrd 8160fa8f10 1769258864 2026-01-24 14:47:44 +02:00
unexplrd c4e4bbf093 helpers, fixes 2026-01-24 14:45:17 +02:00
unexplrd 3e55bb39a2 1769194824 2026-01-23 21:00:24 +02:00
unexplrd 8b86abb244 1769194548 2026-01-23 20:55:48 +02:00
unexplrd 8ead3f8ebb fixes 2026-01-22 18:07:16 +02:00
unexplrd ae6bce387c split up again, with some cool stuff 2026-01-22 17:39:47 +02:00
unexplrd e643c313ba merge back into lib.typ 2026-01-18 01:21:27 +02:00
unexplrd 3865c00ad9 move to dictionary.at(layout) 2026-01-17 18:22:47 +02:00
unexplrd fa76185b7f add complex page 2026-01-17 17:54:11 +02:00
unexplrd 891d33b236 move lib.typ to src/ 2026-01-17 17:24:32 +02:00
unexplrd f3adc98f86 rename 2026-01-17 16:41:05 +02:00
unexplrd 7bb0925662 i think we need edu_program for authorless jobs 2026-01-17 16:25:44 +02:00
unexplrd 3cde131ed6 WIP!: break up lib.typ 2026-01-17 15:45:36 +02:00
30 changed files with 390 additions and 2131 deletions
+14 -67
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,94 +19,43 @@ 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
git clone -b 0.1.0 https://gitea.linerds.us/pencelheimer/typst_nure_template.git ~/.local/share/typst/packages/local/nure/0.1.0
```
2. Init your project with Typst:
```bash
typst init @local/nure:0.1.1 project-name
typst init @local/nure:0.1.0 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,
@@ -144,7 +91,7 @@ Some text
And a TOML file would look like this:
```toml
# university = "ХНУРЕ" # "ХНУРЕ" is the default
# edu-program = "ПЗПІ" # can be null, sourced from authors.first() by default
# edu_program = "ПЗПІ" # can be null, sourced from authors.first() by default
subject = "СМП"
doctype = "ЛБ"
@@ -163,19 +110,19 @@ gender = "m"
[[authors]]
name = "Косач Л. П."
edu-program = "ПЗПІ"
edu_program = "ПЗПІ"
group = "23-2"
gender = "f"
variant = 8
# For coursework
full-name-gen = "Косач Лариси Петрівни"
full_name_gen = "Косач Лариси Петрівни"
course = 2
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.0` 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.0/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.
+21 -45
View File
@@ -1,90 +1,67 @@
ХНУРЕ:
name: Харківський національний університет радіоелектроніки
name-en: Kharkiv National University of Radioelectronics
edu-programs:
name_en: Kharkiv National University of Radioelectronics
edu_programs:
ПЗПІ:
name-long: Інженерія програмного забезпечення
department-gen: Програмної інженерії
name_long: Інженерія програмного забезпечення
department_gen: Програмної інженерії
code: 121 # TODO: change to F2?
КУІБ:
name-long: Управління інформаційною безпекою
department-gen: Інфокомунікаційної інженерії ім. В. В. Поповського
name_long: Управління інформаційною безпекою
department_gen: Інфокомунікаційної інженерії ім. В. В. Поповського
code: 125 # TODO: change to F5?
description: Кібербезпека та захист інформації
КНТ:
name-en: CST # computer sciences and technologies
name-long: Комп'ютерні науки та технології
department-gen: Системотехніки
department-en: ST
name_en: CST # computer sciences and technologies
name_long: Комп'ютерні науки та технології
department_gen: Системотехніки
department_en: ST
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?
ТКП: Технології комп'ютерного проєктування
ТКП: Технології комп`ютерного проєктування
УФМ: Українське фахове мовлення
ФВС: Фізичне виховання та спорт
ФІЗ: Фізика
ФІЛ: Філософія
ФВС: Фізичне виховання та спорт
ХТ: Хмарні технології
+3 -2
View File
@@ -9,13 +9,14 @@
<info>
<title>ДСТУ 3008:2015 (DSTU 3008:2015)</title>
<title-short>ДСТУ 3008:2015</title-short>
<id>dstu-3008-2015</id>
<id>http://www.zotero.org/styles/dstu-3008-2015</id>
<link href="http://www.zotero.org/styles/dstu-3008-2015" rel="self" />
<link
href="https://uk.wikipedia.org/wiki/ДСТУ_3008:2015"
rel="documentation"
/>
<author>
<name>Linerds</name>
<name>Automated</name>
</author>
<category citation-format="numeric" />
<category field="generic-base" />
-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>
+8 -17
View File
@@ -14,38 +14,29 @@
"грудня",
).at(month - 1)
#let is-cyr(c) = regex("^\p{Cyrillic}") in c
#let is-cyr(c) = regex("[\p{Cyrillic}]") in c
#let is-empty(val) = val == none or str(val).len() == 0 or val == []
#let in-keys(key, dict) = str(key) in dict.keys()
/// type-safe emptiness check
#let is-empty(val) = {
if val == none { return true }
if type(val) == str { val.len() == 0 } else if type(val) == array { val == [] } else { false }
}
#let degree-get(m) = if in-keys("degree", m) and not is-empty(m.degree) { [#m.degree\ ] }
#let degree-get(m) = if "degree" in m and not is-empty(m.degree) { [#m.degree\ ] }
/// returns verb form based on gender ("m", "f", or "p" for plural)
#let gender-verb(verb, gender: "p") = {
(
"author": ("m": "Виконав", "f": "Виконала", "p": "Виконали"),
"mentor": ("m": "Перевірив", "f": "Перевірила", "p": "Перевірили"),
)
.at(verb)
.at(if gender == "m" or gender == "f" { gender } else { "p" })
}
/// returns verb form for dictionary containing gender field
#let gender-form(verb, dict: none) = {
let g = if type(dict) == dictionary and "gender" in dict { dict.gender } else { "p" }
gender-verb(verb, gender: g)
.at(if gender == "m" or gender == "f" { gender } else { "p" }, default: "p")
}
#let gender-get(dict) = if type(dict) == dictionary and in-keys("gender", dict) { dict.gender }
#let gender-form(verb, dict: none) = gender-verb(verb, gender: gender-get(dict))
#let pz-lb-title(type, number: none) = {
let type-title = (
"ЛБ": [Звіт \ з лабораторної роботи],
"ПЗ": [Звіт \ з практичної роботи],
"КР": [Контрольна робота],
"РФ": [Реферат],
"РФ": [Реферат], // зрада
"ІДЗ": [Індивідуальне домашнє завдання],
).at(type, default: type)
if not is-empty(number) { [#type-title #number] } else { [#type-title] }
+55 -135
View File
@@ -5,20 +5,20 @@
#import "./style.typ"
#import "./utils.typ"
#let dstu-table = style.dstu-table
#let hfill = utils.hfill
// Coursework template {{{1
/// Coursework template for NURE
/// - university (str): University code, default "ХНУРЕ"
/// - subject (str): Subject short name
/// - 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 and approval date
/// - abstract (dict): Keywords and abstract text
/// - bib-path (str): Path to bibliography file
/// - appendices (content): Appendix content
/// DSTU 3008:2015 Template for NURE
/// -> content
/// - doc (content): Content to apply the template to.
/// - title (str): Title of the document.
/// - subject (str): Subject short name.
/// - authors ((name: str, full_name_gen: str, variant: int, course: int, semester: int, group: str, gender: str),): List of authors.
/// - mentors ((name: str, degree: str),): List of mentors.
/// - task_list (done_date: datetime, initial_date: datetime, source: (content | str), content: (content | str), graphics: (content | str)): Task list object.
/// - calendar_plan ( plan_table: (content | str), approval_date: datetime): Calendar plan object.
/// - abstract (keywords: (str, ), text: (content | str)): Abstract object.
/// - bib_path path: Path to the bibliography yaml file.
/// - appendices (content): Content with appendices.
#let coursework(
doc,
university: "ХНУРЕ",
@@ -26,130 +26,55 @@
title: none,
authors: (),
mentors: (),
task-list: (),
calendar-plan: (),
task_list: (),
calendar_plan: (),
abstract: (),
bib-path: none,
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)
tp.cw.nure(
tp.cw.coursework(
university,
subject,
type,
title,
authors,
mentors,
task-list,
calendar-plan,
task_list,
calendar_plan,
abstract,
)
doc
// Bibliography with DSTU formatting
// bibliography {{{2
{
// shall CSL descend to hell for it's a horrid standard
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)
bibliography(bib_path, title: [Перелік джерел посилання], style: "csl/dstu-3008-2015.csl", full: true)
}
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")
// Practice and Laboratory works template {{{1
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
/// - edu-program (str): Education program code
/// - subject (str): Subject code
/// - type (str): Work type (ЛБ, ПЗ, КР, РФ, ІДЗ)
/// - number (int): Work number
/// - title (str): Work title
/// - authors (array): List of authors
/// - mentors (array): List of mentors
/// DSTU 3008:2015 Template for NURE
/// -> content
/// - doc (content): Content to apply the template to.
/// - layout: ("default" | "simple"): Title page layout variant.
/// - university: "ХНУРЕ": University metadata. Optional.
/// - edu-program: (str or none): Education program shortcode. Optional.
/// - subject: str: Subject shortcode.
/// - type: ("ЛБ" | "ПЗ" | "КР" | "РФ" | str): Work type.
/// - number: int or none: Work number. Optional.
/// - title: str or none: Work title. Optional.
/// - authors ((name: str, full_name_gen: str or none, edu-program: str, group: str, gender: str, variant: int or none),): List of authors.
/// - mentors ((name: str, degree: str, gender: ("m" | "f" | "p" | none)),): List of mentors. Optional.
#let pz-lb(
doc,
layout: "default",
@@ -161,42 +86,37 @@
title: none,
authors: (),
mentors: (),
skip-heading: false,
) = {
assert(authors.len() > 0, message: "At least one author required")
// TODO: add actually relevant asserts
let edu-program = if edu-program != none { edu-program } else { authors.first().edu-program }
let edu-program = if edu-program != none { edu-program } else { authors.first().edu_program }
let uni = universities.at(university)
set document(title: title, author: authors.map(c => c.name))
show: style.dstu.with(skip: 1)
// 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),
)
// page 1 {{{2
(layouts.at(university, default: layouts.default))()
(
"complex": tp.pz-lb.complex(uni, edu-program, subject, type, number, title, authors, mentors),
"minimal": tp.pz-lb.minimal(uni, edu-program, subject, type, number, title, authors, mentors),
"default": tp.pz-lb.minimal(uni, edu-program, subject, type, number, title, authors, mentors),
).at(layout)
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"))
// TODO(unexplrd): wrap my head around the old way
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
}
// vim:sts=2:sw=2:fdm=marker:cms=/*%s*/
+95 -264
View File
@@ -1,154 +1,22 @@
#import "utils.typ": bold
/// Constants for consistent styling
#let spacing = 0.95em
#let indent-size = 1.25cm
#let double-spacing = spacing * 2
#let double-half-spacing = spacing * 2.5
#import "./utils.typ": bold
/// Ukrainian alphabet for DSTU 3008:2015 numbering
#let ukr-enum = "абвгдежиклмнпрстуфхцшщюя".clusters()
#let spacing = 0.95em // spacing between lines
#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)
block(width: 100%, spacing: 0em)[
#h(indent-size)
#counter(heading).display(num)
#it.body
]
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)
}
}
/// symbols used for numbering according to DSTU 3008:2015
#let ukr-enum = "абвгдежиклмнпрстуфхцшщюя".split("") // 0 = "", 1 = "а"
/// DSTU 3008:2015 Style
/// -> content
/// - it (content): Content to apply the style to.
/// - skip (int): Do not show page number for this number of pages.
/// - offset (int): Adjust all page numbers by this amount.
#let dstu(
it,
skip: 0,
offset: 0,
) = {
// Page setup
// General Styling {{{1
set page(
paper: "a4",
number-align: top + right,
@@ -156,176 +24,139 @@
numbering: (i, ..) => if i > skip { numbering("1", i + offset) },
)
// 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,
amount: 1.25cm,
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) + ")")
// Enums & Lists {{{1
// First level
set enum(indent: 1.25cm, body-indent: 0.5cm, numbering: i => { ukr-enum.at(i) + ")" })
// Second level and further nesting
show enum: it => {
set enum(indent: 0em, numbering: "1)")
it
}
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)
}
// Lists are not intended for multiple levels, use `enum`
set list(indent: 1.35cm, body-indent: 0.5cm, marker: [--])
// Figures {{{1
show figure: it => {
v(double-spacing, weak: true)
v(spacing * 2, weak: true)
it
v(double-spacing, weak: true)
v(spacing * 2, weak: true)
}
set figure.caption(separator: [ -- ])
show figure.where(kind: table): set figure.caption(position: top)
show figure.caption.where(kind: table): set align(left)
show figure.where(kind: raw): set figure.caption(position: top)
show figure.where(kind: raw): set align(left)
// Numbering reset on level 1 headings
// Numbering {{{1
show heading.where(level: 1): it => {
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)
it
}
set figure(numbering: i => context numbering("1.1", counter(heading).get().at(0), i))
set math.equation(numbering: i => context numbering("(1.1)", counter(heading).get().at(0), i))
set figure(numbering: i => numbering("1.1", counter(heading).get().at(0), i))
set math.equation(numbering: i => numbering("(1.1)", counter(heading).get().at(0), i))
// Headings
// Headings {{{1
set heading(numbering: "1.1")
show heading: it => {
set text(size: 14pt)
if it.level == 1 {
set align(center)
set text(weight: "semibold")
pagebreak(weak: true)
upper(it)
v(double-spacing, weak: true)
} else {
set text(weight: "regular")
heading-block(it, num: if it.level == 3 { it.numbering } else { auto })
}
show heading: it => if it.level == 1 {
set align(center)
set text(size: 14pt, weight: "semibold")
pagebreak(weak: true)
upper(it)
v(spacing * 2, weak: true)
} else {
set text(size: 14pt, weight: "regular")
v(spacing * 2, weak: true)
block(width: 100%, spacing: 0em)[
#h(1.25cm)
#counter(heading).display(auto)
#it.body
]
v(spacing * 2, weak: true)
}
// Code listings
show heading.where(level: 3): it => {
set text(size: 14pt, weight: "regular")
v(spacing * 2, weak: true)
block(width: 100%, spacing: 0em)[
#h(1.25cm)
#counter(heading).display(it.numbering)
#it.body
]
v(spacing * 2, weak: true)
}
// listings {{{3
show raw.where(block: true): it => {
let code-spacing = 0.5em
set block(spacing: code-spacing)
set par(spacing: code-spacing, leading: code-spacing)
let new_spacing = 0.5em
set block(spacing: new_spacing)
set par(spacing: new_spacing, leading: new_spacing)
set text(size: 11pt, weight: "semibold", font: ("Courier New", "Liberation Mono"))
v(double-half-spacing, weak: true)
pad(it, left: indent-size)
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"]
v(spacing * 2.5, weak: true)
pad(it, left: 1.25cm)
v(spacing * 2.5, weak: true)
}
it
// }}}
}
/// DSTU 3008:2015 Appendices Style
#let appendices(it) = {
/// -> content
/// - it (content): Content to apply the style to.
#let appendices(it) = /* {{{ */ {
// Numbering
counter(heading).update(0)
set heading(numbering: (i, ..n) => upper(ukr-enum.at(i)) + numbering(".1.1", ..n))
set figure(numbering: i => upper(ukr-enum.at(counter(heading).get().at(0))).i)
set math.equation(numbering: i => upper(ukr-enum.at(counter(heading).get().at(0))).i)
set heading(numbering: (i, ..n) => upper(ukr-enum.at(i - 1)) + numbering(".1.1", ..n))
// Heading supplement (Heading name shown when citing with @ref)
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))])
// Headings
show heading: it => if it.level == 1 {
set align(center)
set text(size: 14pt, weight: "regular")
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))
pagebreak(weak: true)
bold([ДОДАТОК #counter(heading).display(auto)])
linebreak()
it.body
v(spacing * 2, weak: true)
} else {
set text(size: 14pt, weight: "regular")
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)
}
v(spacing * 2, weak: true)
block(width: 100%, spacing: 0em)[
#h(1.25cm)
#counter(heading).display(auto)
#it.body
]
v(spacing * 2, weak: true)
}
it
}
// vim:sts=2:sw=2:fdm=marker:cms=/*%s*/
-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,
)
}
}
@@ -3,14 +3,15 @@
#import "../../style.typ": *
#import "../../utils.typ": bold, hfill, uline
#let nure(
#let coursework(
university,
subject,
type,
title,
authors,
mentors,
task-list,
calendar-plan,
task_list,
calendar_plan,
abstract,
) = {
let bib-count = state("citation-counter", ())
@@ -20,10 +21,10 @@
}
let author = authors.first()
let head-mentor = mentors.first()
let head_mentor = mentors.first()
let uni = universities.at(university)
let edu-prog = uni.edu-programs.at(author.edu-program)
let edu_prog = uni.edu_programs.at(author.edu_program)
// page 1 {{{2
[
@@ -33,7 +34,7 @@
\
Кафедра #edu-prog.department-gen
Кафедра #edu_prog.department_gen
\
@@ -48,10 +49,10 @@
#set align(left)
#set par(first-line-indent: 0pt)
#gender-form("author", dict: author) ст. гр. #author.edu-program\-#author.group
#gender-form("author", dict: author) ст. гр. #author.edu_program\-#author.group
\
Керівник:\ #head-mentor.degree
Керівник:\ #head_mentor.degree
\
Робота захищена на оцінку
@@ -66,7 +67,7 @@
#author.name
\ \
#head-mentor.name
#head_mentor.name
\
#underline(" " * 35)
@@ -77,7 +78,7 @@
#v(1fr)
Харків -- #task-list.done-date.display("[year]")
Харків -- #task_list.done_date.display("[year]")
#pagebreak()
]
@@ -97,18 +98,18 @@
Спеціальність
],
{
uline(align: left, edu-prog.department-gen)
uline(align: left, edu_prog.department_gen)
linebreak()
uline(align: left, uni.subjects.at(subject, default: subject))
linebreak()
uline(align: left, [#edu-prog.code #edu-prog.name-long])
uline(align: left, [#edu_prog.code #edu_prog.name_long])
},
)
grid(
columns: (1fr, 1fr, 1fr),
gutter: 0.3fr,
[#bold[Курс] #uline(author.course)],
[#bold[Група] #uline([#author.edu-program\-#author.group])],
[#bold[Група] #uline([#author.edu_program\-#author.group])],
[#bold[Семестр] #uline(author.semester)],
)
@@ -120,7 +121,7 @@
linebreak()
uline(align: left)[_#author.full-name-gen _]
uline(align: left)[_#author.full_name_gen _]
linebreak()
linebreak()
@@ -132,30 +133,30 @@
{
bold[\2. Строк здачі закінченої роботи:]
uline(task-list.done-date.display("[day].[month].[year]"))
uline(task_list.done_date.display("[day].[month].[year]"))
hfill(10fr)
}
linebreak()
bold[\3. Вихідні дані для роботи:]
uline(task-list.source)
uline(task_list.source)
linebreak()
bold[\4. Зміст розрахунково-пояснювальної записки:]
uline(task-list.content)
uline(task_list.content)
linebreak()
bold[\5. Перелік графічного матеріалу:]
uline(task-list.graphics)
uline(task_list.graphics)
linebreak()
{
bold[\6. Дата видачі завдання:]
uline(task-list.initial-date.display("[day].[month].[year]"))
uline(task_list.initial_date.display("[day].[month].[year]"))
hfill(10fr)
}
@@ -169,7 +170,7 @@
linebreak()
calendar-plan.plan-table
calendar_plan.plan_table
linebreak()
@@ -181,22 +182,22 @@
[
Студент \
Керівник \
#align(center)["#underline[#calendar-plan.approval-date.day()]"]
#align(center)["#underline[#calendar_plan.approval_date.day()]"]
],
[
#uline(align: center, []) \
#uline(align: center, []) \
#uline(align: center, month-gen(calendar-plan.approval-date.month()))
#uline(align: center, month-gen(calendar_plan.approval_date.month()))
],
[
\ \
#underline[#calendar-plan.approval-date.year()] р.
#underline[#calendar_plan.approval_date.year()] р.
],
),
[
#author.name, \
#head-mentor.degree
#head-mentor.name.
#head_mentor.degree
#head_mentor.name.
],
)
@@ -210,8 +211,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 -1
View File
@@ -1 +1 @@
#import "nure.typ": *
#import "coursework.typ": *
-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
+9 -8
View File
@@ -1,5 +1,5 @@
#import "../../helpers.typ": *
#let complex(uni, edu-program, subject, type, number, title, authors, mentors) = {
#let complex(uni, edu_program, subject, type, number, title, authors, mentors) = {
align(center)[
Міністерство освіти і науки України \
#uni.name
@@ -7,10 +7,10 @@
\
#set par(first-line-indent: 0pt)
#align(left)[
#let edu = uni.edu-programs.at(edu-program)
Кафедра #underline(edu.department-gen) \
#let edu = uni.edu_programs.at(edu_program)
Кафедра #underline(edu.department_gen) \
Спеціальність #underline([#edu.code #edu.description]) \
Освітня програма #underline(edu.name-long)
Освітня програма #underline(edu.name_long)
]
\ \
@@ -26,12 +26,12 @@
#align(right)[
#if authors.len() == 1 {
let a = authors.first()
[#gender-form("author", dict: a):\
студент групи #a.edu-program\-#a.group\ #a.name\ ]
[#gender-verb("author", gender: gender-get(a)):\
студент групи #a.edu_program\-#a.group\ #a.name\ ]
text(size: 8pt, [(прізвище та ініціали)\ ])
} else if authors.len() > 1 [
#gender-verb("author"):\
#for a in authors [студент групи #a.edu-program\-#a.group\ #a.name\ ]
#for a in authors [студент групи #a.edu_program\-#a.group\ #a.name\ ]
#text(size: 8pt, [(прізвище та ініціали)\ ])
]
@@ -39,7 +39,7 @@
#if mentors.len() == 1 {
let m = mentors.first()
[#gender-form("mentor", dict: m):\ ]
[#gender-verb("mentor", gender: gender-get(m)):\ ]
degree-get(m)
[#m.name\ ]
text(size: 8pt, [(прізвище та ініціали)\ ])
@@ -58,3 +58,4 @@
#datetime.today().display("[year]")
]
}
+1 -1
View File
@@ -1,2 +1,2 @@
#import "complex.typ": *
#import "nure.typ": *
#import "minimal.typ": *
@@ -1,10 +1,10 @@
#import "../../helpers.typ": *
#let nure(uni, edu-program, subject, type, number, title, authors, mentors) = {
#let minimal(uni, edu-program, subject, type, number, title, authors, mentors) = {
align(center)[
#upper([Міністерство освіти і науки України\ #uni.name])
\ \
Кафедра #uni.edu-programs.at(edu-program).department-gen
Кафедра #uni.edu_programs.at(edu-program).department_gen
\ \ \
#pz-lb-title(type, number: number)
@@ -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()
@@ -53,3 +50,4 @@
Харків -- #datetime.today().display("[year]")
]
}
+45 -47
View File
@@ -1,63 +1,61 @@
// Template formatting functions {{{1
/// bold text
#let bold(content) = text(weight: "bold")[#content]
/// numberless heading
#let nheading(title) = heading(depth: 1, numbering: none, title)
/// fill horizontal space with a filled box
#let hfill(width) = box(width: width, repeat("")) // HAIR SPACE (U+200A)
/// fill horizontal space with a box and not an empty space
#let hfill(width) = box(
width: width,
repeat(""),
) // NOTE: This is a HAIR SPACE (U+200A), not a regular space
/// 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
/// make underlined cell with filled value
#let uline(align: center, content) = underline[
#if align != left { hfill(1fr) }
#content
#if align != right { hfill(1fr) }
]
/// Extract filename stem without extension
#let stem(path) = path.split("/").last().split(".").first()
// Helper functions {{{1
/// Extract parent directory name
#let parent-dir(path) = path.split("/").at(-2, default: "")
/// Generate label from image path:
/// - "image.png" → "image"
/// - "img/foo/bar.png" → "foo_bar"
#let img-label(path) = {
let name = stem(path)
let parent = parent-dir(path)
// If parent exists and name doesn't start with parent name, combine them
let base = if parent != "" and not name.starts-with(parent) {
parent + "_" + name
} else {
name
}
label(base.replace(" ", "_"))
}
/// Format image caption based on optional source
#let img-caption(base-caption, source) = {
if source == none {
base-caption + " (рисунок виконано самостійно)"
} else if source == () or source == "" {
base-caption
} else {
base-caption + " (за даними " + source + ")"
}
}
/// captioned image with auto-generated label from path
/// Usage: img("path/to/image.png", "Caption")(optional: "source")
/// captioned image with label derived from path:
/// - "image.png" = @image
/// - "img/image.png" = @image
/// - "img/foo/image.png" = @foo_image
/// - "img/foo/foo_image.png" = @foo_image
/// the caption will be modified based on a conditional positional value:
/// - `none`: no change
/// - some value: "`caption` (за даними `value`)"
/// - no value: "`caption` (рисунок виконано самостійно)"
/// additional named arguments will be passed to original `image` function
#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) ]
let parts = path.split(".").first().split("/")
let label_string = if (
parts.len() <= 2 or parts.at(-1).starts-with(parts.at(-2))
) {
// ("image",), (_, "image") and (.., "img", "img_image")
parts.last()
} else {
// (.., "img", "image") = "img_image"
parts.at(-2) + "_" + parts.at(-1)
}.replace(" ", "_")
let caption = if sink.pos().len() == 0 {
caption
} else if sink.pos().first() == none {
caption + " (рисунок виконано самостійно)"
} else {
[#caption (за даними #sink.pos().first())]
}
[#figure(
image(path, ..sink.named()),
caption: caption,
) #label(label_string)]
}
@@ -1,4 +1,4 @@
#import "@local/nure:0.1.1": *
#import "@local/nure:0.1.0": *
#import style: spacing
#import "utils.typ": img
@@ -6,8 +6,8 @@
#let authors = (
(
name: "Ситник Є. С.",
full-name-gen: "Ситника Єгора Сергійовича",
edu-program: "ПЗПІ",
full_name_gen: "Ситника Єгора Сергійовича",
edu_program: "ПЗПІ",
group: "23-2",
gender: "m",
course: 2,
@@ -22,16 +22,16 @@
(name: "Широкопетлєва М. С.", degree: "Ст. викл. каф. ПІ"),
)
#let task-list = (
done-date: datetime(year: 2024, month: 12, day: 27),
initial-date: datetime(year: 2024, month: 9, day: 15),
#let task_list = (
done_date: datetime(year: 2024, month: 12, day: 27),
initial_date: datetime(year: 2024, month: 9, day: 15),
source: "методичні вказівки до виконання курсової роботи, вимоги до інформаційної системи, предметна область, що пов’язана з управлінням класом та класним керівництвом.",
content: "вступ, аналіз предметної області; постановка задачі; проектування бази даних; опис програми; висновки; перелік джерел посилання.",
graphics: "загальна діаграма класів, ER-діаграма, UML-діаграми, DFD-діаграма, схема БД в 1НФ, 2НФ, 3НФ, копії екранів (“скриншоти”) прикладної програми, приклади звітів прикладної програми.",
)
#let calendar-plan = (
plan-table: table(
#let calendar_plan = (
plan_table: table(
columns: 4,
align: (center, left, center, center),
[Номер], [Назва етапів курсової роботи], [Строк виконання етапів роботи], [Примітки],
@@ -54,7 +54,7 @@
[12], [Третя контрольна точка з курсової роботи], [27.12.24], [Виконано],
),
approval-date: datetime(year: 2024, month: 12, day: 27),
approval_date: datetime(year: 2024, month: 12, day: 27),
)
#let abstract = (
@@ -113,10 +113,10 @@
subject: "БД",
authors: authors,
mentors: mentors,
task-list: task-list,
calendar-plan: calendar-plan,
task_list: task_list,
calendar_plan: calendar_plan,
abstract: abstract,
bib-path: bytes(read("bibl.yml")), // NOTE: use `bytes("bibl.yml")` as typst looks in template dir when using just filename
bib_path: bytes(read("bibl.yml")), // NOTE: use `bytes("bibl.yml")` as typst looks in template dir when using just filename
appendices: appendices,
)
-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)
-7
View File
@@ -1,7 +0,0 @@
#import "@local/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) ]
}
@@ -1,4 +1,4 @@
#import "@local/nure:0.1.1": *
#import "@local/nure:0.1.0": *
#import "utils.typ": img
#import style: spacing
@@ -16,8 +16,8 @@
authors: (
(
name: "Косач Л. П.",
full-name-gen: "Косач Лариси Петрівни",
edu-program: "КУІБ",
full_name_gen: "Косач Лариси Петрівни",
edu_program: "КУІБ",
group: "23-2",
gender: "f",
course: 2,
@@ -68,7 +68,7 @@
#v(-spacing)
== Частина 1
#lorem(100)
== Частина 2
== Частина2
#lorem(200)
= Приклад звіту 2
-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
}
+36
View File
@@ -0,0 +1,36 @@
/// captioned image with label derived from path:
/// - "image.png" = @image
/// - "img/image.png" = @image
/// - "img/foo/image.png" = @foo_image
/// - "img/foo/foo_image.png" = @foo_image
/// the caption will be modified based on a conditional positional value:
/// - `none`: no change
/// - some value: "`caption` (за даними `value`)"
/// - no value: "`caption` (рисунок виконано самостійно)"
/// additional named arguments will be passed to original `image` function
#let img(path, caption, ..sink) = {
let parts = path.split(".").first().split("/")
let label_string = if (
parts.len() <= 2 or parts.at(-1).starts-with(parts.at(-2))
) {
// ("image",), (_, "image") and (.., "img", "img_image")
parts.last()
} else {
// (.., "img", "image") = "img_image"
parts.at(-2) + "_" + parts.at(-1)
}.replace(" ", "_")
let caption = if sink.pos().len() == 0 {
caption
} else if sink.pos().first() == none {
caption + " (рисунок виконано самостійно)"
} else {
[#caption (за даними #sink.pos().first())]
}
[#figure(
image(path, ..sink.named()),
caption: caption,
) #label(label_string)]
}
+51
View File
@@ -0,0 +1,51 @@
#import "@local/test-multifile:0.1.0" as nure
#import nure.utils: *
#import nure.style
// #set document(title: "Тест лейаутів", author: "Іванов І.І.")
#let authors = (
(
name: "Іванов І.І.",
full_name_gen: "Іванова Івана Івановича",
edu_program: "КУІБ",
group: "23-1",
gender: "m",
variant: 5,
),
)
#let mentors = (
(name: "Петров П.П.", degree: "доцент кафедри ІКІ", gender: "m"),
)
// Тест default лейауту
#show: nure.pz-lb.with(
layout: "complex",
subject: "БД",
edu-program: "КУІБ",
type: "ЛБ",
number: 2,
title: "SQL запити",
authors: authors,
mentors: mentors,
)
#bold(lorem(10))
#pagebreak()
// Тест simple лейауту
// #show: pz-lb.with(
// layout: "minimal",
// subject: "БД",
// type: "ЛБ",
// number: 2,
// title: "SQL запити",
// authors: authors,
// mentors: mentors,
// )
#show: style.appendices
= #lorem(5)
+2 -2
View File
@@ -1,11 +1,11 @@
[package]
name = "nure"
version = "0.1.1"
version = "0.1.0"
entrypoint = "src/lib.typ"
authors = ["linerds"]
license = "GPL-3.0"
description = "Typst NURE package"
[template]
path = "template/default"
path = "template"
entrypoint = "lab.typ"