15 Commits
11 changed files with 348 additions and 167 deletions
+2
View File
@@ -1 +1,3 @@
template/*.pdf
tests/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dominykas Svetikas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9
View File
@@ -1,3 +1,12 @@
# ktu-paper
This repository contains a Typst template for academic papers at the Kaunas University of Technology, Faculty of Informatics. A final Bachelor's thesis written with this template has successfully passed the formal review.
## Initiating the template
Install the downloaded template under the following directory:
- LINUX: `~/.local/share/typst/packages/local/ktu-paper/0.2.1/`
And then initiate a new project:
```sh
typst init @local/ktu-paper:0.2.1
```
+13
View File
@@ -1,3 +1,16 @@
/// Whether to number figures per section or not.
/// If true, figures will be numbered like "1.1", "1.2", "2.1", etc.
/// If false, figures will be numbered like "1", "2", "3", etc.
///
/// TODO: figure out how to deal with numbering on sections such as
/// Appendix and other non-standard sections.
#let __FIG_PER_SECTION = state("svetikas.lt/ktu-paper/figure-numbering-per-section", false)
/// Whether to uppercase section titles or not.
/// If true, section titles will be uppercased.
/// If false, section titles will be displayed as-is.
#let __UPPERCASE_SECTION_TITLES = state("svetikas.lt/ktu-paper/uppercase-section-titles", false)
#let __DEBUG_AUTHOR_TABLE = state("$ktu-template-DEBUG_AUTHOR_TABLE", false)
#let debug-author-table() = {
__DEBUG_AUTHOR_TABLE.update(true)
+137
View File
@@ -0,0 +1,137 @@
// This file includes logic for displaying figures in the document, including numbering, captions, and references. It also includes logic for handling figure numbering per section or globally, based on the configuration set in `config.typ`.
#import "config.typ": __FIG_PER_SECTION
/// Localizations for various recognized referencable figure types.
/// This is used to provide the correct supplement text for each type of figure
/// when generating references, captions and outlines.
#let __figureNames = (
table: (
supplement: "lentelė",
),
image: (
supplement: "pav",
),
raw: (
supplement: "kodo frag",
),
equation: (
supplement: "lygtis",
),
)
/// Helper function to get the supplement text for a given figure type.
#let __fig-supplement(func) = {
return __figureNames.at(repr(func)).supplement
}
#let __fig-func-to-name(func) = {
if func == table {
return "lentelė"
}
if func == image {
return "pav."
}
if func == grid {
return "pav."
}
return func
}
/// Helper function to resolve figure numbers (either global "1" or section-based "1.1").
/// It takes a figure item and referenced location and returns the appropriate number as a string.
#let __get-figure-number(fig-item, loc) = {
if type(fig-item) != content {
panic("can only get figure number for content elements")
}
if fig-item.func() != figure {
panic("can only get figure number for figure elements")
}
let perSection = __FIG_PER_SECTION.at(loc)
if perSection {
let chapter = counter(heading).at(loc).at(0, default: 0)
let figNum = counter(figure.where(kind: fig-item.kind)).at(loc).at(0, default: 1)
str(chapter) + "." + str(figNum)
} else {
let caption = fig-item.caption
if caption != none and caption.numbering != none {
str(caption.counter.at(loc).at(0))
} else {
str(counter(figure.where(kind: fig-item.kind)).at(loc).at(0))
}
}
}
/// Helper function to get the number of a locatable element (figure, table, etc.)
/// based on its location in the document. It returns the number as a string, formatted according to the current numbering scheme.
#let __get-locatable-element-number(element) = context {
if type(element) != content {
panic("can only reference elements of type content")
}
let loc = element.location()
let elem = query(loc)
let fig = elem.at(0)
if fig.func() != figure {
panic("referenced element does not have a figure function")
}
__get-figure-number(fig, loc)
}
/// Formats a reference to a figure, table, or other numbered element.
#let __format-ref(it) = {
let el = it.element
if el == none {
return it
}
// Override equation references
if el.func() == math.equation {
link(
el.location(),
numbering(el.numbering, ..counter(math.equation).at(el.location()))
)
// Override references to figures
} else if el.func() == figure {
let fig = el.body
let figType = fig.func()
let numStr = __get-figure-number(el, el.location())
link(el.location(), [
(žr. #numStr #__fig-func-to-name(figType))
])
} else {
it
}
}
/// Formats a figure with a caption and number.
#let __format-figure(it) = {
let separator = [.]
let caption = it.caption
// If caption is not provided, just show the figure body
if caption == none {
return it.body
}
let counter = caption.counter
let supplement = caption.supplement
if it.body.func() == math.equation {
supplement = __fig-supplement("equation")
} else {
supplement = __fig-supplement(it.kind)
}
set text(size: 11pt)
let fig-num = context __get-figure-number(it, here())
// Tables have captions at the top
if it.kind == table {
align(left)[*#fig-num #supplement#separator* #caption.body]
it.body
} else {
it.body
align(center)[*#fig-num #supplement#separator* #caption.body]
}
}
+109 -157
View File
@@ -1,9 +1,18 @@
#import "@preview/codly:1.3.0": codly, codly-init
#import "config.typ": debug-author-table, enable-heading-experiment, __HEADING_EXPERIMENT
#import "config.typ": debug-author-table, enable-heading-experiment, __HEADING_EXPERIMENT, __FIG_PER_SECTION, __UPPERCASE_SECTION_TITLES
#import "fragments.typ": (
ktu-table,
ktu-heading-page-centered, ktu-heading-page-normal, ktu-academic-honestly-declaration-page
)
#import "figures.typ": (
__fig-supplement,
__get-figure-number,
__get-locatable-element-number,
__format-ref,
__format-figure,
)
/**
* Paruošta pagal "Rašto ..."
@@ -26,26 +35,6 @@
[#text]
}
#let figureDefinitions = (
table: (
outlineFormat: (number) => [*#number lentelė.*],
captionFormat: (number) => [*#number lentelė.*],
),
)
#let custom-figure(
kind,
caption: none,
body
) = {
figure(
body,
kind: kind,
caption: caption,
supplement: "CUSTOM-FIGURE"
)
}
#let appendix-item(body) = {
heading(
metadata("appendix-heading") + "priedas. " + body,
@@ -54,14 +43,6 @@
)
}
// Stores localized names for various figure types
#let figureNames = (
table: ("lentelė"),
image: ("pav"),
code: ("kodo frag"),
equation: ("lygtis"),
)
// Utility function to round numbers with padded zeros
#let round-and-pad(number, decimals) = {
let rounded = calc.round(number, digits: decimals)
@@ -89,17 +70,20 @@
integer-part + padded-decimal
}
/// Returns a number for the given reference target.
/// - target (label): Label to get the reference number for.
#let ref-no(target) = context {
if type(target) != label {
panic("ref-no expects a label as input")
}
let loc = locate(target)
let elem = query(target)
let item = elem.at(0)
// If it's a figure, return the figure number
if item.func() == figure {
link(loc, [#counter(figure.where(kind: item.kind)).at(loc).at(0)])
link(loc, [#__get-figure-number(item, loc)])
} else if item.func() == heading {
let fullValue = counter(heading).at(loc)
@@ -116,20 +100,6 @@
}
}
#let __ref-no-element(element) = context {
// Ensure that we pass an element
if type(element) != content {
panic("can only reference elements of type content")
}
let loc = element.location()
let elem = query(loc)
let fig = elem.at(0)
if fig.func() != figure {
panic("referencing unknown type: " + fig.func())
}
fig.caption.counter.at(loc).at(0)
}
// Constructs a reference from multiple targets
#let custom-ref(..targets) = context {
let collected = (:)
@@ -187,7 +157,7 @@
}
collected.at(elementName).push(
(loc, fig.caption.counter.at(loc).at(0))
(loc, __get-figure-number(fig, loc))
)
}
@@ -222,6 +192,9 @@
show outline: o => context {
// If it's a heading, show the default outline with bold text
if (o.target == selector(heading)) {
// Determine whether to uppercase section titles based on the global setting
let isUpper = __UPPERCASE_SECTION_TITLES.get()
// Define known target widths based on KTU template
let targets = (0.64cm, 0.96cm, 1.28cm, 1.6cm, 1.92cm)
@@ -237,9 +210,25 @@
target = 0.64cm
}
let newGap = target - measurement.width
// Uppercase body if need be
let bodyContent = it.element.body
if isUpper and (it.level == 1 or it.element.numbering == none) {
bodyContent = upper(bodyContent)
}
// Reconstruct the it.inner content with the new body
let formattedInner = {
bodyContent
[ ] // whitespace is required for backwards compat
box(width: 1fr, it.fill)
[ ] // whitespace is required for backwards compat
it.page()
}
link(
it.element.location(),
it.indented(prefixContent, it.inner(), gap: newGap),
it.indented(prefixContent, formattedInner, gap: newGap),
)
}
@@ -252,7 +241,10 @@
} else if (o.target == figure.where(kind: image)) {
show outline.entry: it => link(
it.element.location(),
it.indented([*#__ref-no-element(it.element) #figureNames.image.*], [ #it.inner()], gap: 0pt),
it.indented(
[*#__get-locatable-element-number(it.element) #__fig-supplement(image).*],
[ #it.inner()], gap: 0pt
),
)
o
@@ -260,7 +252,10 @@
} else if (o.target == figure.where(kind: table)) {
show outline.entry: it => link(
it.element.location(),
it.indented([*#__ref-no-element(it.element) #figureNames.table.*], [ #it.inner()], gap: 0pt),
it.indented(
[*#__get-locatable-element-number(it.element) #__fig-supplement(table).*],
[ #it.inner()], gap: 0pt
),
)
o
@@ -268,7 +263,10 @@
} else if (o.target == figure.where(kind: raw)) {
show outline.entry: it => link(
it.element.location(),
it.indented([*#__ref-no-element(it.element) #figureNames.code.*], [ #it.inner()], gap: 0pt),
it.indented(
[*#__get-locatable-element-number(it.element) #__fig-supplement(raw).*],
[ #it.inner()], gap: 0pt
),
)
o
@@ -286,7 +284,10 @@
show outline.entry: it => link(
it.element.location(),
it.indented([*#__ref-no-element(it.element) unknown.*], it.inner(), gap: 0pt),
it.indented(
[*#__get-locatable-element-number(it.element) unknown.*],
it.inner(), gap: 0pt
),
)
o
}
@@ -294,19 +295,6 @@
outline(depth: depth, indent: indent, target: target, title: title)
}
#let func-to-name(func) = {
if func == table {
return "lentelė"
}
if func == image {
return "pav."
}
if func == grid {
return "pav."
}
return func
}
// Doesn't work in Typst 0.12.0 due to relative paths
#let bibliography-list(path) = {
bibliography(path, title: "Literatūros sąrašas", full: true)
@@ -361,7 +349,7 @@
body
}
#let __page_rules(body) = {
#let __page_rules(font: "Times New Roman", body) = {
// Page size and margins
set page(
paper: "a4",
@@ -390,7 +378,7 @@
// Set text font
set text(
font: "Times New Roman",
font: font,
size: 12pt,
lang: "lt",
// Change where the bounding box is drawn for the text
@@ -411,8 +399,13 @@
body
}
#let setup-page(body) = {
show: __page_rules.with()
#let setup-page(
font: "Times New Roman",
uppercaseSectionTitles: false,
unnumberedHeadingAlignment: center,
body
) = {
show: __page_rules.with(font: font)
// Pagal formaliuosius rašto darbų reikalavimus
@@ -424,6 +417,14 @@
set par(justify: true)
set linebreak(justify: true)
let maybe-uppercase(body) = {
if uppercaseSectionTitles {
upper(body)
} else {
body
}
}
// Antraštės
// Force them into blocks so they don't count as paragraphs
show heading: it => {
@@ -441,11 +442,11 @@
// Antraštė be nr.
if it.numbering == none {
// Centruota lygiuotė
set align(center)
// Centruota lygiuotė by default, but can be overridden by the user
set align(unnumberedHeadingAlignment)
// atstumas prieš ir po antraštės - 10 pt
block[#it.body]
block[#maybe-uppercase(it.body)]
if useExperiment {
v(10pt)
}
@@ -456,10 +457,16 @@
if it.numbering != none and it.level == 1 {
// abipusė lygiuotė
set par(justify: true)
//set align
// Reset figure counters if numbering per section is enabled
if __FIG_PER_SECTION.get() {
counter(figure.where(kind: image)).update(0)
counter(figure.where(kind: table)).update(0)
counter(figure.where(kind: raw)).update(0)
}
// po antraštės - 10 pt
block[#counter(heading).display() #it.body]
block[#counter(heading).display() #maybe-uppercase(it.body)]
if useExperiment {
v(10pt)
}
@@ -487,94 +494,39 @@
}
}
// Override references for equations
show ref: it => {
let el = it.element
// Reference style and numbering
show ref: it => __format-ref(it)
if el == none {
return it
}
// Figure captions and numbering
show figure: it => __format-figure(it)
// Override equation references
if el.func() == math.equation {
link(
el.location(),
numbering(el.numbering, ..counter(math.equation).at(el.location()))
)
// Override references to figures
} else if el.func() == figure {
let fig = el.body
let figType = fig.func()
link(el.location(), [
(žr. #numbering(el.numbering, ..counter(figure.where(kind: figType)).at(el.location())) #func-to-name(figType))
])
} else {
it
}
}
// Change captions for figures
show figure: it => {
let separator = [.]
let caption = it.caption
// If caption is not provided, just show the figure body
if caption == none {
it.body
return
}
let counter = caption.counter
let supplement = caption.supplement
if it.body.func() == math.equation {
supplement = figureNames.equation
} else if it.kind == image {
supplement = figureNames.image
} else if it.kind == table {
supplement = figureNames.table
} else if it.kind == raw {
supplement = figureNames.code
}
// Tables have captions at the top
set text(size: 11pt)
if it.kind == table {
{set align(left)
[*#context counter.display(caption.numbering) #supplement#separator* #caption.body]}
it.body
} else {
it.body
[*#context counter.display(caption.numbering) #supplement#separator* #caption.body]
}
}
// Lentelės pirma eilutė
/*
show table.cell.where(y: 0): it => context {
set align(left)
set par(justify: false)
set text(size: TableHeadSize.get()) // Šrifto dydis
v(3pt)
// Paryškintas
[*#it*]
v(3pt)
}
show table.cell: it => context {
set align(left)
set par(justify: false)
set text(size: TableCellSize.get())
v(3pt)
[#it]
v(3pt)
}*/
// Set bibliography and citing style
// Bibliography citing style
set bibliography(style: "assets/iso690-numeric-lt.csl")
// Return the body
body
}
/// Configure the template for a KTU paper.
/// - font (str): Text font to use for the paper.
/// - figureNumberingPerSection (bool): Whether to number figures per section rather than globally.
/// - uppercaseSectionTitles (bool): Whether to uppercase section titles.
/// - unnumberedHeadingAlignment (alignment): Alignment for unnumbered headings.
#let ktu-paper(
font: "Times New Roman",
figureNumberingPerSection: false,
// Not sure about formal conformity of deviating
// from the defaults of the next options,
// but some modules may require them
uppercaseSectionTitles: false,
unnumberedHeadingAlignment: center,
body
) = context {
show: setup-page.with(
font: font,
uppercaseSectionTitles: uppercaseSectionTitles,
unnumberedHeadingAlignment: unnumberedHeadingAlignment,
)
__FIG_PER_SECTION.update(figureNumberingPerSection)
__UPPERCASE_SECTION_TITLES.update(uppercaseSectionTitles)
body
}
+5 -3
View File
@@ -1,12 +1,14 @@
#import "@local/ktu-paper:0.1.0": (
setup-page, setup-code,
#import "@local/ktu-paper:0.3.0": (
ktu-paper, setup-code,
ktu-table-of-contents, ktu-picture-list, ktu-table-list,
ktu-heading-page-normal,
ktu-table,
custom-ref, ref-no
)
#show: setup-page.with()
#show: ktu-paper.with(
font: "Times New Roman",
)
#show: setup-code.with()
#show math.equation: it => {
+5 -3
View File
@@ -1,5 +1,5 @@
#import "@local/ktu-paper:0.1.0": (
setup-page, setup-code,
#import "@local/ktu-paper:0.3.0": (
ktu-paper, setup-code,
ktu-table-of-contents, ktu-picture-list, ktu-table-list,
ktu-heading-page-normal,
ktu-table,
@@ -7,7 +7,9 @@
custom-ref, ref-no
)
#show: setup-page.with()
#show: ktu-paper.with(
font: "Times New Roman",
)
#show: setup-code.with()
#show figure: it => {
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Clean up previous test results
if [ -d "tests/original-res" ]; then
rm -r tests/original-res
fi
if [ -d "tests/modified-res" ]; then
rm -r tests/modified-res
fi
if [ -d "tests/modified" ]; then
rm -r tests/modified
fi
if [ -d "tests/original/src" ]; then
rm -r tests/original/src
fi
mkdir -p tests/original-res
mkdir -p tests/modified-res
# Copy current template
cp -r src/ tests/original/src
cp -r tests/original tests/modified
# replace the "#import "@local/ktu-paper:0.1.0""
# with "#import "./src/lib.typ"" in the modified files
find tests/modified -type f -name "*.typ" -exec \
sed -i 's/#import "@local\/ktu-paper:0.1.0"/#import ".\/src\/lib.typ"/g' {} \;
find tests/modified -type f -name "ataskaita.typ" -exec \
sed -i 's/setup-page/ktu-paper/g' {} \;
# Compile the original document
typst compile --root tests/original tests/original/ataskaita.typ "tests/original-res/{0p}.png"
# Compile the modified document
typst compile --root tests/modified tests/modified/ataskaita.typ "tests/modified-res/{0p}.png"
typst compile --root tests/modified tests/modified/ataskaita.typ "tests/modified.pdf"
# Compare the results
diff -r tests/original-res tests/modified-res
if [ $? -eq 0 ]; then
echo "No differences found between original and modified results."
else
echo "Differences found between original and modified results."
fi
+3 -3
View File
@@ -1,9 +1,9 @@
[package]
name = "ktu-paper"
version = "0.1.0"
compiler = "0.12.0"
version = "0.3.0"
compiler = "0.15.0"
entrypoint = "src/lib.typ"
repository = "https://git.svetikas.lt/JustAnyone/dotfiles"
repository = "https://github.com/svetikas/ktu-paper"
authors = ["Dominykas Svetikas <https://svetikas.lt>"]
license = "MIT"
description = "KTU report template to use for writing lab reports or final thesis."