Compare commits

...

15 Commits

Author SHA1 Message Date
Sven Vogel 255cdfeae1 Merge branch 'feature/typstyle'
Gitea Action for checking typst compilation / run-ci-linux (push) Successful in 2m48s Details
2024-08-27 21:16:23 +02:00
Sven Vogel 1548bf8c5b changed release pipeline to make use of nix
Gitea Action for checking typst compilation / run-ci-linux (push) Successful in 1m43s Details
2024-08-27 20:58:01 +02:00
Sven Vogel dabe0a2ef5 fixed failing tests
Gitea Action for checking typst compilation / run-ci-linux (push) Successful in 1m41s Details
2024-08-27 20:54:33 +02:00
Sven Vogel 6c8f94e4b7 fixed test local import 2024-08-27 20:36:21 +02:00
Sven Vogel ae729230bb made generate-theme executable 2024-08-27 20:28:50 +02:00
Sven Vogel 9c3ae5c9e7 switched to unstable nix channel 2024-08-27 20:27:41 +02:00
Sven Vogel 093ed09293 fixed format of template for (#13)
Gitea Action for checking typst compilation / run-ci-linux (push) Failing after 1m47s Details
2024-08-27 19:52:07 +02:00
Sven Vogel 946a16fce6 add typstyle check to CI (#13) 2024-08-27 19:50:31 +02:00
Sven Vogel 7ad138dc00 fix CI failing (#18) 2024-08-27 19:50:09 +02:00
Sven Vogel 201e991ac4 added sudo to action runner
Gitea Action for checking typst compilation / run-ci-linux (push) Successful in 1m52s Details
2024-08-27 19:36:24 +02:00
Sven Vogel 2daf7bb6c9 updated nix action to v27
Gitea Action for checking typst compilation / run-ci-linux (push) Failing after 11s Details
2024-08-27 19:34:46 +02:00
Sven Vogel 9ba1fea4d7 changed: ci action syntax
Gitea Action for checking typst compilation / run-ci-linux (push) Failing after 10s Details
2024-07-08 22:58:13 +02:00
Sven Vogel 4a1fcd0a35 changed: CI runs on nix shell
Gitea Action for checking typst compilation / run-ci-linux (push) Failing after 0s Details
2024-07-08 22:56:56 +02:00
Sven Vogel d648d5121c changed: formatted source files 2024-07-08 21:57:23 +02:00
Sven Vogel 4fd088ce3f added: format script 2024-07-08 21:50:13 +02:00
16 changed files with 316 additions and 145 deletions

View File

@ -5,15 +5,16 @@ on: [push]
jobs: jobs:
run-ci-linux: run-ci-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
TYPST_INSTALL: /usr/local
TYPST_VERSION: 0.11.1
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Typst CLI - name: Install dependencies for Nix setup action
run: curl -fsSL https://typst.community/typst-install/install.sh | sh -s "$TYPST_VERSION" run: |
- name: Set up Python 3 apt update -y
run: apt update -y && apt install python3-pip -y apt install sudo -y
- name: Setup Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Run CI - name: Run CI
run: bash -c ./run-ci.sh run: nix-shell --run ./run-ci.sh

View File

@ -3,21 +3,24 @@ name: release
on: on:
push: push:
tags: tags:
- '*' - 'v*.*.*'
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: Check out repository code
uses: actions/checkout@v3
- name: Install dependencies for Nix setup action
run: |
apt update -y
apt install sudo -y
- name: Setup Nix
uses: cachix/install-nix-action@v27
with: with:
fetch-depth: 0 nix_path: nixpkgs=channel:nixos-unstable
- name: Install Typst CLI
run: curl -fsSL https://typst.community/typst-install/install.sh | sh -s "$TYPST_VERSION"
- name: Set up Python 3
run: apt update -y && apt install python3-pip -y
- name: Run CI - name: Run CI
run: bash -c ./run-ci.sh run: nix-shell --run ./run-ci.sh
- name: Setup go - name: Setup go
uses: https://github.com/actions/setup-go@v4 uses: https://github.com/actions/setup-go@v4
with: with:

0
generate-theme.sh Normal file → Executable file
View File

View File

@ -25,19 +25,29 @@ function enter-section() {
printf "\\n" printf "\\n"
eval "$2" eval "$2"
if [ $? -neq $3 ]; then exit_status=$?
abort "command: $2 failed in section: $1"
if [ "$3" == "should fail" ]; then
log "INFO" "expected to fail..."
if [ $exit_status -eq 0 ]; then
abort "command: $2 failed in section: $1 with: $exit_status"
fi
else
log "INFO" "expected to pass..."
if [ ! $exit_status -eq 0 ]; then
abort "command: $2 failed in section: $1 with: $exit_status"
fi
fi fi
printf "\\n" printf "\\n"
log "INFO" "section $1 completed successfully" log "INFO" "section $1 completed successfully"
} }
enter-section "BUILD: ABB code theme" "./generate-theme.sh" 0 enter-section "Typstyle checking" "./run-fmt.sh --check src/lib.typ" 0
enter-section "Compiling template..." "typst compile template/main.typ --root . example.pdf" 0 enter-section "Compiling template..." "typst compile template/main.typ --root . example.pdf"
enter-section "TEST: local template import" "typst compile tests/local-import/main.typ --root ." 0 enter-section "TEST: local template import" "typst compile tests/local-import/main.typ --root ."
enter-section "TEST: invalid config case 1" "typst compile tests/invalid-config/test-case-1.typ --root ." 1 enter-section "TEST: invalid config case 1" "typst compile tests/invalid-config/test-case-1.typ --root ." "should fail"
enter-section "TEST: invalid config case 2" "compile tests/invalid-config/test-case-2.typ" 0 enter-section "TEST: invalid config case 2" "typst compile tests/invalid-config/test-case-2.typ --root ."
enter-section "TEST: invalid config case 3" "typst compile tests/invalid-config/test-case-3.typ --root ." 1 enter-section "TEST: invalid config case 3" "typst compile tests/invalid-config/test-case-3.typ --root ." "should fail"
log "INFO" "CI completed successfully" log "INFO" "CI completed successfully"

42
run-fmt.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/bash
function format() {
# format file
# typstyle --format $1
if [ -z "$1" ]; then
return
fi
local wd=$(dirname $(realpath "$1"))
echo "processing file $1..."
typstyle "$2" "$1" > /dev/null
if [ $? -eq 1 ]; then
echo "failed format validation: $1"
exit 1
fi
local imports=$(rg "#import \"([a-z0-9/\-]+\.typ)\"" -Nor '$1' "$1")
# format all included files
while IFS= read -r line; do
if [ -z "$line" ]; then
continue
fi
format "$wd/$line" "$2"
done <<< "$imports"
}
case $1 in
"--format")
format "$2" "--inplace"
;;
"--check")
format "$2" "--check"
;;
*)
echo "unknown option: $1"
exit 1
;;
esac

13
shell.nix Normal file
View File

@ -0,0 +1,13 @@
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-unstable";
pkgs = import nixpkgs { config = {}; overlays = []; };
in
pkgs.mkShellNoCC {
packages = with pkgs; [
python312
python312Packages.pyyaml
typst
typstyle
];
}

View File

@ -30,7 +30,10 @@ SOFTWARE.*/
#let __query_labels_with_key(loc, key, before: false) = { #let __query_labels_with_key(loc, key, before: false) = {
if before { if before {
query( query(
selector(label(__glossary_label_prefix + key)).before(loc, inclusive: false), selector(label(__glossary_label_prefix + key)).before(
loc,
inclusive: false,
),
loc, loc,
) )
} else { } else {
@ -56,13 +59,18 @@ SOFTWARE.*/
let entlong = entry.at("long", default: "") let entlong = entry.at("long", default: "")
let textLink = if display != none { let textLink = if display != none {
[#display] [#display]
} else if (is_first or long == true) and entlong != [] and entlong != "" and long != false { } else if (
is_first or long == true
) and entlong != [] and entlong != "" and long != false {
[#entlong (#entry.short#suffix)] [#entlong (#entry.short#suffix)]
} else { } else {
[#entry.short#suffix] [#entry.short#suffix]
} }
[#link(label(entry.key), textLink)#label(__glossary_label_prefix + entry.key)] [#link(
label(entry.key),
textLink,
)#label(__glossary_label_prefix + entry.key)]
} else { } else {
panic(__not-found-panic-error-msg(key)) panic(__not-found-panic-error-msg(key))
} }
@ -100,13 +108,18 @@ SOFTWARE.*/
[#entplural] [#entplural]
} }
let textLink = if (is_first or long == true) and entlong != [] and entlong != "" and long != false { let textLink = if (
is_first or long == true
) and entlong != [] and entlong != "" and long != false {
[#entlong (#short)] [#entlong (#short)]
} else { } else {
[#short] [#short]
} }
[#link(label(entry.key), textLink)#label(__glossary_label_prefix + entry.key)] [#link(
label(entry.key),
textLink,
)#label(__glossary_label_prefix + entry.key)]
} else { } else {
panic(__not-found-panic-error-msg(key)) panic(__not-found-panic-error-msg(key))
} }
@ -116,7 +129,9 @@ SOFTWARE.*/
// show rule to make the references for glossarium // show rule to make the references for glossarium
#let make-glossary(body) = { #let make-glossary(body) = {
show ref: r => { show ref: r => {
if r.element != none and r.element.func() == figure and r.element.kind == __glossarium_figure { if r.element != none and r.element.func() == figure and r
.element
.kind == __glossarium_figure {
// call to the general citing function // call to the general citing function
gls(str(r.target), suffix: r.citation.supplement) gls(str(r.target), suffix: r.citation.supplement)
} else { } else {
@ -200,7 +215,9 @@ SOFTWARE.*/
.sorted(key: x => x.page()) .sorted(key: x => x.page())
.fold( .fold(
(values: (), pages: ()), (values: (), pages: ()),
((values, pages), x) => if pages.contains(x.page()) { ((values, pages), x) => if pages.contains(
x.page(),
) {
(values: values, pages: pages) (values: values, pages: pages)
} else { } else {
values.push(x) values.push(x)
@ -214,7 +231,10 @@ SOFTWARE.*/
if page-numbering == none { if page-numbering == none {
page-numbering = "1" page-numbering = "1"
} }
link(x)[#numbering(page-numbering, ..counter(page).at(x))] link(x)[#numbering(
page-numbering,
..counter(page).at(x),
)]
} }
) )
.join(", ") .join(", ")

View File

@ -75,8 +75,14 @@
// create dedicated entries for // create dedicated entries for
// acronym and glossary // acronym and glossary
if "long" in v and "desc" in v { if "long" in v and "desc" in v {
processed_glossary.insert(k, (short: v.short, long: v.long, group: acronym_group)) processed_glossary.insert(
processed_glossary.insert(k + "__glossary_entry", (short: v.short, desc: v.desc, long: v.long, group: glossary_group)) k,
(short: v.short, long: v.long, group: acronym_group),
)
processed_glossary.insert(
k + "__glossary_entry",
(short: v.short, desc: v.desc, long: v.long, group: glossary_group),
)
} else { } else {
processed_glossary.insert(k, v) processed_glossary.insert(k, v)
processed_glossary.at(k).group = group processed_glossary.at(k).group = group

View File

@ -31,7 +31,13 @@
inset: (left: 2pt, right: 2pt), inset: (left: 2pt, right: 2pt),
outset: (top: 4pt, bottom: 4pt), outset: (top: 4pt, bottom: 4pt),
fill: ABB-GRAY-06, fill: ABB-GRAY-06,
[#box(fill: rgb(color), radius: 2pt, inset: 0pt, width: 0.75em, height: 0.75em) #text( [#box(
fill: rgb(color),
radius: 2pt,
inset: 0pt,
width: 0.75em,
height: 0.75em,
) #text(
font: default-config.style.code.font, font: default-config.style.code.font,
size: default-config.style.code.size, size: default-config.style.code.size,
content, content,
@ -40,13 +46,17 @@
} }
#let url(label, content) = { #let url(label, content) = {
link(label)[#underline(offset: 2pt, stroke: 0.5pt + blue, text(fill: blue)[ link(label)[#underline(
#content offset: 2pt,
#let domain = label.find(regex("\w+\.\w+(?:\.\w+)*")) stroke: 0.5pt + blue,
#if domain.len() > 0 [ text(fill: blue)[
(#domain) #content
] #let domain = label.find(regex("\w+\.\w+(?:\.\w+)*"))
])] #if domain.len() > 0 [
(#domain)
]
],
)]
} }
// start of template pages and styles // start of template pages and styles

View File

@ -43,11 +43,11 @@
set par(justify: true) set par(justify: true)
if text.lang == "de" [ if text.lang == "de" [
darf weder als Ganzes noch in Auszügen Personen außerhalb des Prüfungsprozesses und des Evaluationsverfahrens zugänglich gemacht werden, sofern keine anderslautende Genehmigung der Ausbildungsstätte vorliegt. darf weder als Ganzes noch in Auszügen Personen außerhalb des Prüfungsprozesses und des Evaluationsverfahrens zugänglich gemacht werden, sofern keine anderslautende Genehmigung der Ausbildungsstätte vorliegt.
] else if text.lang == "en" [ ] else if text.lang == "en" [
may not be made accessible to persons outside the examination process and the evaluation procedure, either as a whole or in excerpts, unless otherwise authorized by the training institution. may not be made accessible to persons outside the examination process and the evaluation procedure, either as a whole or in excerpts, unless otherwise authorized by the training institution.
] ]
set align(horizon) set align(horizon)

View File

@ -64,7 +64,9 @@
stroke: none, stroke: none,
[*Verfasser:*], author.name, [*Verfasser:*], author.name,
[*Bearbeitungszeitraum:*], thesis.timeframe, [*Bearbeitungszeitraum:*], thesis.timeframe,
[*Matrikelnummer, Kurs:*], str(author.matriculation-number) + ", " + author.course, [*Matrikelnummer, Kurs:*],
str(author.matriculation-number) + ", " + author.course,
[*Ausbildungsbetrieb:*], author.company, [*Ausbildungsbetrieb:*], author.company,
[*Betrieblicher Betreuer:*], author.supervisor, [*Betrieblicher Betreuer:*], author.supervisor,
[*Abgabedatum:*], thesis.submission-date, [*Abgabedatum:*], thesis.submission-date,
@ -77,7 +79,9 @@
stroke: none, stroke: none,
[*Author:*], author.name, [*Author:*], author.name,
[*Editing period:*], thesis.timeframe, [*Editing period:*], thesis.timeframe,
[*Matriculation number, course:*], str(author.matriculation-number) + ", " + author.course, [*Matriculation number, course:*],
str(author.matriculation-number) + ", " + author.course,
[*Training company:*], author.company, [*Training company:*], author.company,
[*Company supervisor:*], author.supervisor, [*Company supervisor:*], author.supervisor,
[*Submission date:*], thesis.submission-date, [*Submission date:*], thesis.submission-date,

View File

@ -12,7 +12,13 @@
#let watermark-color = luma(50%).transparentize(70%) #let watermark-color = luma(50%).transparentize(70%)
#let watermark-pattern = pattern(size: (5pt, 5pt))[ #let watermark-pattern = pattern(size: (5pt, 5pt))[
#place(line(start: (50%, 0%), end: (50%, 100%), stroke: (paint: watermark-color, thickness: 3pt))) #place(
line(
start: (50%, 0%),
end: (50%, 100%),
stroke: (paint: watermark-color, thickness: 3pt),
),
)
] ]
#let watermark(config) = if config.draft { #let watermark(config) = if config.draft {
@ -29,7 +35,10 @@
#linebreak() #linebreak()
document version. document version.
#linebreak() #linebreak()
#text(size: 0.75em, "Further usage without the authors consent is not permitted.")]]] #text(
size: 0.75em,
"Further usage without the authors consent is not permitted.",
)]]]
} }
#let numberingH(c) = { #let numberingH(c) = {
@ -117,37 +126,51 @@
e => { e => {
let (i, l) = e let (i, l) = e
let n = i + 1 let n = i + 1
let n_str = if (calc.rem(n, 1) == 0) or (true and i == 0) { text(font: style.code.font, size: style.code.size, fill: ABB-BLACK, str(n)) } else { none } let n_str = if (calc.rem(n, 1) == 0) or (true and i == 0) {
(n_str + h(0.5em), raw(block: true, lang: lang, l)) text(
}) font: style.code.font,
} size: style.code.size,
else { fill: ABB-BLACK,
( ( 1fr, ), str(n),
( left, ),
e => {
let (i, l) = e
raw(block: true, lang: lang, l)
}
) )
} } else {
none
}
(n_str + h(0.5em), raw(block: true, lang: lang, l))
},
)
} else {
(
(1fr,),
(left,),
e => {
let (i, l) = e
raw(block: true, lang: lang, l)
},
)
} }
grid( }
stroke: none, grid(
columns: columns, stroke: none,
rows: (auto,), columns: columns,
gutter: 0pt, rows: (auto,),
inset: 0.25em, gutter: 0pt,
align: (col, _) => align.at(col), inset: 0.25em,
fill: ABB-GRAY-06, align: (col, _) => align.at(col),
..content fill: ABB-GRAY-06,
.text ..content
.split("\n") .text
.enumerate() .split("\n")
.map(make_row) .enumerate()
.flatten() .map(make_row)
.map(c => if c.has("text") and c.text == "" { v(1em) } else { c }) .flatten()
) .map(c => if c.has("text") and c.text == "" {
} v(1em)
} else {
c
})
)
},
) )
#v(-1em) #v(-1em)
#align(center + top, it.caption) #align(center + top, it.caption)
@ -184,8 +207,20 @@
stroke: (x, y) => ( stroke: (x, y) => (
left: none, left: none,
right: none, right: none,
top: if y == 0 { 1.5pt } else if y < 2 { 1pt } else { 0.5pt }, top: if y == 0 {
bottom: if y == 0 { 1pt } else { 1.5pt } )) 1.5pt
} else if y < 2 {
1pt
} else {
0.5pt
},
bottom: if y == 0 {
1pt
} else {
1.5pt
},
),
)
// make table header bold // make table header bold
show table.cell.where(y: 0): set text(weight: "bold") show table.cell.where(y: 0): set text(weight: "bold")
@ -194,7 +229,8 @@
set par( set par(
justify: true, justify: true,
first-line-indent: 1em, first-line-indent: 1em,
leading: 1em) leading: 1em,
)
// give links a color // give links a color
show link: set text(fill: style.link.color) show link: set text(fill: style.link.color)
@ -207,17 +243,26 @@
header-ascent: style.header.content-padding, header-ascent: style.header.content-padding,
footer-descent: style.header.content-padding, footer-descent: style.header.content-padding,
margin: ( margin: (
top: style.page.margin.top + style.header.underline-top-padding + style.header.content-padding, top: style.page.margin.top + style.header.underline-top-padding + style
.header
.content-padding,
bottom: style.page.margin.bottom + style.footer.content-padding, bottom: style.page.margin.bottom + style.footer.content-padding,
left: style.page.margin.left, left: style.page.margin.left,
right: style.page.margin.right), right: style.page.margin.right,
),
numbering: (..nums) => { numbering: (..nums) => {
let current-page = here().page() let current-page = here().page()
if current-page == 1{ if current-page == 1 {
[] []
} else if query(<end-of-prelude>).first().location().page() > current-page { } else if query(<end-of-prelude>)
.first()
.location()
.page() > current-page {
numbering("I", nums.pos().first()) numbering("I", nums.pos().first())
} else if query(<end-of-content>).first().location().page() >= current-page { } else if query(<end-of-content>)
.first()
.location()
.page() >= current-page {
numbering("1", nums.pos().first()) numbering("1", nums.pos().first())
} else { } else {
numbering("a", nums.pos().first()) numbering("a", nums.pos().first())
@ -234,7 +279,11 @@
set align(center) set align(center)
numbering("I", page-counter) numbering("I", page-counter)
} else if query(<end-of-content>).first().location().page() >= page-number { } else if query(<end-of-content>).first().location().page() >= page-number {
numbering("1 / 1", page-counter, counter(page).at(<end-of-content>).last()) numbering(
"1 / 1",
page-counter,
counter(page).at(<end-of-content>).last(),
)
} else { } else {
numbering("a", page-counter) numbering("a", page-counter)
} }
@ -254,7 +303,13 @@
// right align logo of DHBW // right align logo of DHBW
align(right, image("res/DHBW.svg", height: style.header.logo-height))) align(right, image("res/DHBW.svg", height: style.header.logo-height)))
} else if query(<end-of-content>).first().location().page() >= current-page and query(<end-of-prelude>).first().location().page() < current-page + 1 { } else if query(<end-of-content>)
.first()
.location()
.page() >= current-page and query(<end-of-prelude>)
.first()
.location()
.page() < current-page + 1 {
let heading = currentH() let heading = currentH()
heading.at(0) heading.at(0)
@ -267,7 +322,8 @@
v(style.header.underline-top-padding - 1em) v(style.header.underline-top-padding - 1em)
line(length: 100%) line(length: 100%)
} }
}) },
)
body body
} }

View File

@ -1,8 +1,7 @@
#import "../../src/lib.typ": dhbw-template #import "../../src/lib.typ": dhbw-template
#show: dhbw-template.with( #show: dhbw-template.with((
config: (
lang: none, lang: none,
region: "en", region: "en",
author: ( author: (

View File

@ -1,30 +1,34 @@
#import "../../src/lib.typ": dhbw-template #import "../../src/lib.typ": dhbw-template
#show: dhbw-template.with( #show: dhbw-template.with((
config: ( lang: "en",
lang: "en", this-key-is-not-in-config: "Ha Ha",
this-key-is-not-in-config: "Ha Ha", region: "en",
region: "en", author: (
author: ( name: "Sven Vogel",
name: "Sven Vogel", semester: 4,
semester: 4, program: "Informationtechnology",
program: "Informationtechnology", course: "TINF19IT1",
course: "TINF19IT1", faculty: "Technik",
faculty: "Technik", university: "DHBW Mannheim",
university: "DHBW Mannheim", company: "ABB AG",
company: "ABB AG", supervisor: "Benny Goodman",
supervisor: "Benny Goodman", matriculation-number: 123456789,
matriculation-number: 123456789), ),
thesis: ( thesis: (
title: "Unofficial ABB/DHBW Typst template", title: "Unofficial ABB/DHBW Typst template",
subtitle: "for reports and thesises", subtitle: "for reports and thesises",
submission-date: "23rd march 2020", submission-date: "23rd march 2020",
timeframe: "1st january 2020 - 20th march 2020", timeframe: "1st january 2020 - 20th march 2020",
kind: "T2000", kind: "T2000",
summary: none, summary: none,
abstract: none, abstract: none,
keywords: ( "IT", "other stuff" ), keywords: ("IT", "other stuff"),
bibliography: none, bibliography: none,
glossary: none, glossary: none,
appendices: none))) appendices: none,
),
))
= Heading

View File

@ -1,24 +1,26 @@
#import "../../src/lib.typ": dhbw-template #import "../../src/lib.typ": dhbw-template
#show: dhbw-template.with( #show: dhbw-template.with((
config: ( lang: "en",
lang: "en", region: "en",
region: "en", author: (
author: ( university: "DHBW Mannheim",
university: "DHBW Mannheim", company: "ABB AG",
company: "ABB AG", supervisor: none,
supervisor: none, matriculation-number: 123456789,
matriculation-number: 123456789), ),
thesis: ( thesis: (
title: "Unofficial ABB/DHBW Typst template", title: "Unofficial ABB/DHBW Typst template",
subtitle: "for reports and thesises", subtitle: "for reports and thesises",
submission-date: "23rd march 2020", submission-date: "23rd march 2020",
timeframe: "1st january 2020 - 20th march 2020", timeframe: "1st january 2020 - 20th march 2020",
kind: "T2000", kind: "T2000",
summary: none, summary: none,
abstract: none, abstract: none,
keywords: ( "IT", "other stuff" ), keywords: ("IT", "other stuff"),
bibliography: none, bibliography: none,
glossary: none, glossary: none,
appendices: none))) appendices: none,
),
))

View File

@ -1,8 +1,7 @@
#import "../../src/lib.typ": dhbw-template #import "../../src/lib.typ": dhbw-template
#show: dhbw-template.with( #show: dhbw-template.with((
config: (
lang: "en", lang: "en",
region: "en", region: "en",
author: ( author: (
@ -27,3 +26,5 @@
bibliography: none, bibliography: none,
glossary: none, glossary: none,
appendices: none))) appendices: none)))
= Heading