From 9c0e59a604724d86d5b19f367654b8e150f66b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20J=C3=B6rg?= <14005567+mrtz-j@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:32:14 +0200 Subject: [PATCH] Headers (#5) * Add header content on odd and even pages * Add Table of abbreviations * Add page numbering footer on chapter start --------- Co-authored-by: Ole Tytlandsvik --- .gitignore | 1 + README.md | 4 +- flake.nix | 9 +- lib.typ | 363 ++++++---- modules/abbreviations.typ | 27 + modules/abstract.typ | 3 +- modules/acknowledgements.typ | 18 +- modules/epigraph.typ | 2 - modules/glossarium.typ | 881 +++++++++++++++++++++++ template/chapters/global.typ | 3 +- template/chapters/glossary.typ | 30 - template/chapters/introduction.typ | 35 +- template/chapters/tables_and_figures.typ | 36 +- template/thesis.typ | 27 +- typst.toml | 4 +- 15 files changed, 1231 insertions(+), 212 deletions(-) create mode 100644 modules/abbreviations.typ create mode 100644 modules/glossarium.typ delete mode 100644 template/chapters/glossary.typ diff --git a/.gitignore b/.gitignore index b102556..66ec37a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .direnv/* .pre-commit-config.yaml *.pdf +result diff --git a/README.md b/README.md index d85f959..11f7e8d 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ - [ ] List of Definitions - [x] List of Abbreviations - [ ] Styling - - [ ] Headings (chapter on odd, subsection on even) + - [x] Headings (chapter on odd, subsection on even) - [ ] Font features - [ ] Figures (captions etc) - [x] Code blocks (syntax highlights) - - [ ] Tables + - [x] Tables - [x] Footnotes (?) - [ ] Good examples - [ ] Use of figures, tables, code blocks diff --git a/flake.nix b/flake.nix index 72253bf..584f4da 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,8 @@ let pkgs = import nixpkgs { inherit system; }; in - with pkgs; { + with pkgs; + { checks = { pre-commit-check = pre-commit-hooks.lib.${system}.run { src = ./.; @@ -54,8 +55,12 @@ open-sans jetbrains-mono ]; - extraCompileFlags = [ "--root" "./" ]; + extraCompileFlags = [ + "--root" + "./" + ]; mainFile = "template/thesis.typ"; + outputFile = "thesis.pdf"; typstPackages = { preview = "${typst-packages}/packages/preview"; }; diff --git a/lib.typ b/lib.typ index b43c1fa..ec9c37b 100644 --- a/lib.typ +++ b/lib.typ @@ -1,5 +1,9 @@ #import "@preview/subpar:0.1.1" #import "@preview/physica:0.9.3": * +#import "@preview/outrageous:0.1.0" +// TODO: Update when 0.4.2 is published +// #import "@preview/glossarium:0.4.2": make-glossary, print-glossary, gls, glspl +#import "modules/glossarium.typ": make-glossary, print-glossary, gls, glspl #import "@preview/codly:1.0.0": * #import "modules/frontpage.typ": frontpage @@ -7,6 +11,7 @@ #import "modules/acknowledgements.typ": acknowledgements_page #import "modules/abstract.typ": abstract_page #import "modules/epigraph.typ": epigraph_page +#import "modules/abbreviations.typ": abbreviations-page #import "modules/metadata.typ": * #import "utils/print_pagebreak.typ": * @@ -23,14 +28,19 @@ // Colors used across the template. #let stroke-color = luma(200) #let fill-color = luma(250) +#let uit-teal-color = rgb("#0095b6") +#let uit-gray-color = rgb("#48545e") +// Helper to display two pieces of content with space between #let fill-line(left-text, right-text) = [#left-text #h(1fr) #right-text] -#let isappendix = state("isappendix", false) + +#let in-appendix = state("in-appendix", false) // The `in-outline` mechanism is for showing a short caption in the list of figures // See https://sitandr.github.io/typst-examples-book/book/snippets/chapters/outlines.html#long-and-short-captions-for-the-outline #let in-outline = state("in-outline", false) +// TODO: Remove if remains unused #let flex-caption(long, short) = ( context { if in-outline.get() { @@ -43,6 +53,7 @@ // --- +// Common styles for front matter #let front_matter(body) = { set page(numbering: "i") counter(page).update(1) @@ -54,8 +65,19 @@ body } +// Common styles for main matter #let main_matter(body) = { - set page(numbering: "1") + set page(numbering: "1", + // Only show numbering in footer when no header is peresent + footer: context { + let chapters = heading.where(level: 1) + if query(chapters).any(it => it.location().page() == here().page()) { + align(center, counter(page).display()) + } else { + none + } + } + ) counter(page).update(1) counter(heading).update(0) set heading(numbering: "1.1") @@ -66,23 +88,24 @@ body } +// Common styles for back matter #let back_matter(body) = { - set heading(numbering: "A", supplement: [Appendix]) + // TODO: Should not outline bibliography, but maybe appendix? + set heading(numbering: "A", supplement: [Appendix], outlined: false) // Without this, the header says "Chapter F" counter(heading.where(level: 1)).update(0) - // Without this, the table of contents line says "Chapter F" counter(heading).update(0) body } #let subfigure = { subpar.grid.with( - numbering: n => if isappendix.get() { + numbering: n => if in-appendix.get() { numbering("A.1", counter(heading).get().first(), n) } else { numbering("1.1", counter(heading).get().first(), n) }, - numbering-sub-ref: (m, n) => if isappendix.get() { + numbering-sub-ref: (m, n) => if in-appendix.get() { numbering("A.1a", counter(heading).get().first(), m, n) } else { numbering("a", m, n) @@ -178,6 +201,9 @@ title: "Listings", ), + // List of abbreviations + abbreviations: none, + // The content of your work. body, ) = { @@ -192,19 +218,29 @@ }, ) - // Optimise numbers with superscript - // espcecially for nice bibliography entries + // Required init for packages + show: make-glossary + show: codly-init + + // Optimize numbers with superscript + // especially nice for bibliography entries show regex("\d?\dth"): w => { - // 26th, 1st, ... + // 26th, ... let b = w.text.split(regex("th")).join() [#b#super([th])] } + show regex("\d?\dst"): w => { + // 1st + let b = w.text.split(regex("st")).join() + [#b#super([st])] + } show regex("\d?\d[nr]d"): w => { - // 2dn, 3rd + // 2nd, 3rd, ... let s = w.text.split(regex("\d")).last() let b = w.text.split(regex("[nr]d")).join() [#b#super(s)] } + // if we find in bibentries some ISBN, we add link to it show "https://doi.org/": w => { // handle DOIs @@ -221,9 +257,7 @@ show footnote.entry: set par(hanging-indent: 1.5em) // Set the body font. - // Default is Linux Libertine at 11pt - // set text(font: ("Libertinus Serif", "Linux Libertine"), size: 11pt) - // set text(font: ("Utopia LaTeX"), size: 11pt) + // Default is Charter at 11pt set text(font: ("Charter"), size: 11pt) // Set raw text font. @@ -240,7 +274,7 @@ outside: 37mm, ), numbering: "1", - number-align: right, + number-align: center, ) // Configure paragraph properties. @@ -250,17 +284,24 @@ // Default spacing is 1.2em. show par: set block(spacing: 1.35em) + // Add some vertical spacing for all headings show heading: it => { + let body = if it.level > 1 { + box(width: 35pt, counter(heading).display()) + it.body + } else { + it + } v(2.5em, weak: true) - it + body v(1.5em, weak: true) } - // Style chapter headings. + // Style chapter headings show heading.where(level: 1): it => { - set text(font: "Noto Sans", weight: "bold", size: 24pt) + set text(font: "Open Sans", weight: "bold", size: 24pt) - // Has no effect, still shows "Section" + // FIXME: Has no effect, still shows "Section" set heading(supplement: [Chapter]) let heading_number = if heading.numbering == none { @@ -279,8 +320,8 @@ move( dy: 32pt, polygon( - fill: rgb("#0095b6"), - stroke: rgb("#0095b6"), + fill: uit-teal-color, + stroke: uit-teal-color, (0pt, 0pt), (5pt, 0pt), (25pt, -60pt), @@ -302,105 +343,102 @@ // Do not hyphenate headings. show heading: set text( - font: "Noto Sans", + font: "Open Sans", + weight: "bold", features: ("sc", "si", "scit"), hyphenate: false, ) - // Set page header set page( + // Set page header header-ascent: 30%, header: context { - // Get current page number. + // Get current page number let page-number = here().page() - // [ #repr(query().map(el => el.location().page()).slice(0, 5)) ] // If the current page is the start of a chapter, don't show a header - let target = heading.where(level: 1) - if query(target).any(it => it.location().page() == page-number) { - // return [New chapter! page #here().page()] + let chapters = heading.where(level: 1) + if query(chapters).any(it => it.location().page() == page-number) { return [] } - // Find the chapter of the section we are currently in. - let before = query(target.before(here())) - if before.len() > 0 { - let current = before.last() + // Find the chapter of the section we are currently in + let chapters-before = query(chapters.before(here())) + if chapters-before.len() > 0 { + let current-chapter = chapters-before.last() + + + // If a new subsecion starts on this page, select that subsection. + // Otherwise, select the last subsection + let current-subsection = { + let subsections = heading.where(level: 2) + let subsections-after = query(subsections.after(here())) + + if subsections-after.len() > 0 { + let next-subsection = subsections-after.first() + + if next-subsection.location().page() == page-number { + (next-subsection) + } else { + let subsections-before = query(subsections.before(here())) + + if subsections-before.len() > 0 { + (subsections-before.last()) + } else { + // No subsections in this chapter + (none) + } + } + } + } - let chapter-title = upper(current.body) - let chapter-number = counter(heading.where(level: 1)).display() - // let chapter-number-text = [#current.supplement Chapter #chapter-number] - let chapter-number-text = [#chapter-number] + let colored-slash = text(fill: uit-teal-color, "/") + let spacing = h(3pt) - // FIXME: Breaks when show: x_matter in lib - // Get next subsection name and number for header - // let subsection = query(heading.where(level: 2)).first() - // let next-subsection = subsection. + // Content to display subsection count and heading + let subsection-text = if current-subsection != none { + let subsection-numbering = current-subsection.numbering + let location = current-subsection.location() + let subsection-count = numbering(subsection-numbering,..counter(heading).at(location)) - let colored-slash = text(fill: rgb("#0095b6"), "/") + [#subsection-count #spacing #colored-slash #spacing #current-subsection.body] + } else { + // No subsections in chapter, display nothing + [] + } - if current.numbering != none { + // Content to display chapter count and heading + let chapter-text = { + let chapter-title = current-chapter.body + let chapter-number = counter(heading.where(level: 1)).display() + + [CHAPTER #chapter-number #spacing #colored-slash #spacing #chapter-title] + } + + if current-chapter.numbering != none { + // Show current chapter on odd pages, current subsection on even let (left-text, right-text) = if calc.odd(page-number) { - ([#counter(page).display()], chapter-title) + (counter(page).display(), chapter-text) } else { - let spacing = h(7pt) ( - stack( - dir: ltr, - "CHAPTER", - spacing, - chapter-number, - spacing, - colored-slash, - spacing, - chapter-title, - ), - [#counter(page).display()], + subsection-text, + counter(page).display(), ) } text( weight: "thin", - font: "Noto Sans", - size: 9pt, - tracking: 0.1em, - fill: rgb("#2B3333"), - features: ("sc", "si", "scit"), - fill-line(left-text, right-text), + font: "Open Sans", + size: 8pt, + fill: uit-gray-color, + // FIXME: Seems to have no effect + // features: ("sc", "si", "scit"), + fill-line(upper(left-text), upper(right-text)), ) - // v(-1em) - // line(length: 100%, stroke: 0.5pt) } } }, ) - // The `in-outline` is for showing a short caption in the list of figures - // See https://sitandr.github.io/typst-examples-book/book/snippets/chapters/outlines.html#long-and-short-captions-for-the-outline - show outline: it => { - in-outline.update(true) - // Show table of contents, list of figures, list of tables, etc. in the table of contents - set heading(outlined: true) - it - in-outline.update(false) - } - - // Indent nested entries in the outline. - set outline(indent: auto, fill: repeat([#h(2.5pt) . #h(2.5pt)])) - - show outline.entry: it => { - // Only apply styling if we're in the table of contents (not list of figures or list of tables, etc.) - if it.element.func() == heading { - if it.level == 1 { - v(1.5em, weak: true) - strong(it) - } else { - it - } - } else { - it - } - } - // Configure equation numbering. set math.equation(numbering: n => { let h1 = counter(heading).get().first() @@ -416,29 +454,28 @@ // FIXME: Has no effect? // set place(clearance: 2em) - set figure( - numbering: n => { - let h1 = counter(heading).get().first() - numbering("1.1", h1, n) - }, - gap: 1.5em, - ) + // Set figure numbering to follow chapter + set figure(numbering: n => { + let h1 = counter(heading).get().first() + numbering("1.1", h1, n) + }) set figure.caption(separator: [ -- ]) - show figure.caption: it => { - if it.kind == table { - align(center, it) - } else { - align(left, it) - } - } + // Place table captions above table show figure.where(kind: table): it => { set figure.caption(position: top) // Break large tables across pages. set block(breakable: true) it } - set table(stroke: none) + + // Use lighter gray color for table stroke + set table( + inset: 7pt, + stroke: (0.5pt + stroke-color), + ) + // Show table header in small caps + show table.cell.where(y: 0): smallcaps // Display inline code in a small box that retains the correct baseline. show raw.where(block: false): box.with( @@ -476,6 +513,7 @@ submission-date: submission-date, ) + // Use front matter stylings show: front_matter // List of Supervisors @@ -492,32 +530,111 @@ // Acknowledgements acknowledgements_page()[#acknowledgements] - // Display indices of figures, tables, and listings. - let fig-t(kind) = figure.where(kind: kind) - if figure-index.enabled or table-index.enabled or listing-index.enabled { - context { - pagebreak() - let imgs = figure-index.enabled - let tbls = table-index.enabled - let lsts = listing-index.enabled - - outline(title: "Contents") - if imgs { - outline(title: "List of Figures", target: fig-t(image)) - } - if tbls { - outline(title: "List of Tables", target: fig-t(table)) - } - if lsts { - outline(title: "List of Listings", target: fig-t(raw)) + // Display outlines (table of content, table of figures, etc...) + context { + // Helper to target figure kinds + let fig-t(kind) = figure.where(kind: kind) + + // The `in-outline` is for showing a short caption in the list of figures + // See https://sitandr.github.io/typst-examples-book/book/snippets/chapters/outlines.html#long-and-short-captions-for-the-outline + show outline: it => { + in-outline.update(true) + // Show table of contents, list of figures, list of tables, etc. in the table of contents + set heading(outlined: true) + it + in-outline.update(false) + } + + // Increase distance between dots on all outline fill + set outline(fill: box(width: 1fr, repeat([#h(2.5pt) . #h(2.5pt)]))) + + pagebreak() + + // Common padding to use on outline entries + let entry-padding = 0.5em + + // Styling for ToC + // NOTE: When/if setting fill dependent on the depth becomes easily possible + // in typst itself, we can remove the 'outrageous' package + [ + #show outline.entry: outrageous.show-entry.with( + ..outrageous.presets.typst, + font-weight: ("bold", auto), + fill: (none, auto), + font: ("Carter", "Carter"), + vspace: (1.5em, 0.5em), + // FIXME: This should work... + // fill-right-pad: .4cm, + // fill-align: true, + + // Manually add indent and spacing + body-transform: (lvl, body) => { + let indent = (lvl - 1) * 1.5em + // Entries with (number, text, page) should have more spacing between number and text + let spaced-entry = if "children" in body.fields() { + let (number, space, ..text) = body.children + [#number #h(entry-padding) #text.join()] + } else { + body + } + [#h(indent) #spaced-entry] + }, + // Manually add spacing between fill and page number + page-transform: (lvl, page) => { + if lvl > 1 { + [#h(entry-padding) #page] + } else { + page + } + } + ) + #outline(title: "Contents") + ] + + // Styling for remaining outlines + show outline.entry: outrageous + .show-entry + .with( + ..outrageous.presets.typst, + // Don't display 'figure' or 'table' before each entry + body-transform: (_lvl, body) => { + if "children" in body.fields() { + let (fig-type, space, number, colon, ..text) = body.children + [#number #h(entry-padding) #text.join()] + } else { + body + } + }, + // Manually add spacing between fill and page number + page-transform: (_lvl, page) => { + [#h(entry-padding) #page] } + ) + + // ToF, ToT and ToL are optional + if figure-index.enabled { + outline(title: "List of Figures", target: fig-t(image)) + } + if table-index.enabled { + outline(title: "List of Tables", target: fig-t(table)) + } + if listing-index.enabled { + outline(title: "List of Listings", target: fig-t(raw)) } } - // Thesis content + // List of Abbreviations + if abbreviations != none { + abbreviations-page(abbreviations) + } + + // Use main matter stylings show: main_matter + + // Thesis content body + // Use back matter stylings show: back_matter //Style bibliography diff --git a/modules/abbreviations.typ b/modules/abbreviations.typ new file mode 100644 index 0000000..27f47d7 --- /dev/null +++ b/modules/abbreviations.typ @@ -0,0 +1,27 @@ +// TODO: Update when 0.4.2 is published +// #import "@preview/glossarium:0.4.2": make-glossary, print-glossary, gls, glspl +#import "glossarium.typ": print-glossary + +// Only print short and long, disregard rest +#let custom-print-title(entry) = { + let short = entry.at("short") + let long = entry.at("long", default: "") + [#strong(smallcaps(short)) #h(0.5em) #long] +} + +#let abbreviations-page(abbreviations) = { + // --- List of Abbreviations --- + pagebreak(weak: true, to: "even") + align(left)[ + = List of Abbreviations + #v(1em) + #print-glossary( + abbreviations, + user-print-title: custom-print-title, + // Show all terms even if they are not referenced + show-all: true, + // Disable back references in abbreviations list + disable-back-references: true + ) + ] +} diff --git a/modules/abstract.typ b/modules/abstract.typ index 634d62d..073b6f0 100644 --- a/modules/abstract.typ +++ b/modules/abstract.typ @@ -1,9 +1,8 @@ #let abstract_page(body) = { pagebreak(weak: true, to: "even") - set page(header: none, footer: none, numbering: none) // --- Abstract --- align(left)[ - = *Abstract* + = Abstract #v(1em) #body ] diff --git a/modules/acknowledgements.typ b/modules/acknowledgements.typ index cdb6c6a..fb5e1e9 100644 --- a/modules/acknowledgements.typ +++ b/modules/acknowledgements.typ @@ -2,15 +2,9 @@ pagebreak(weak: true, to: "even") // --- Acknowledgements --- - align( - left, - heading( - numbering: none, - outlined: true, - bookmarked: true, - level: 1, - )[Acknowledgements], - ) - v(2em) - body -} \ No newline at end of file + align(left)[ + = Acknowledgements + #v(1em) + #body + ] +} diff --git a/modules/epigraph.typ b/modules/epigraph.typ index 6943b28..d7a6a41 100644 --- a/modules/epigraph.typ +++ b/modules/epigraph.typ @@ -1,8 +1,6 @@ #let epigraph_page(body) = { // --- Epigraphs --- page( - header: none, - footer: none, numbering: none, align(right + bottom)[ #body diff --git a/modules/glossarium.typ b/modules/glossarium.typ new file mode 100644 index 0000000..e947ffb --- /dev/null +++ b/modules/glossarium.typ @@ -0,0 +1,881 @@ +// MIT License + +// Copyright (c) [2023] [Enib Community] + +// 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. + +// glossarium figure kind +#let __glossarium_figure = "glossarium_entry" + +// prefix of label for references query +#let __glossary_label_prefix = "__gls:" + +// global state containing the glossary entry and their location +// A glossary entry is a `dictionary`. +// See `__normalize_entry_list`. +#let __glossary_entries = state("__glossary_entries", (:)) + +// glossarium version +#let glossarium_version = "0.4.0" + +// error prefix +#let __glossarium_error_prefix = "glossarium@" + glossarium_version + " error : " + +// Errors types +#let __key_not_found = "key_not_found" +#let __attribute_is_empty = "attribute_is_empty" +#let __unknown_error = "unknown_error" + +// __error_message(key, kind, ..kwargs) -> str +// Generate an error message +// +// # Arguments +// key (str): the key of the term +// kind (str): the kind of the error +// kwargs (arguments): additional arguments +// +// # Returns +// The error message +#let __error_message(key, kind, ..kwargs) = { + let msg = none + let kwargs = kwargs.named() // convert arguments sink to dictionary + + // Generate the error message + if kind == __key_not_found { + msg = "key '" + key + "' not found" + } else if kind == __attribute_is_empty { + attr = kwargs.get("attr") + " " + msg = "requested attribute " + attr + "is empty for key '" + key + "'" + } else { + msg = "unknown error" + } + + // return the error message + return __glossarium_error_prefix + msg +} + +// __query_labels_with_key(loc, key, before: false) -> array