Making Your Own Theme
If you're unhappy with the existing themes, or just want to add your own flair to your notebook, the Notebookinator supports custom themes. We highly recommend you familiarize yourself with the Typst Documentation before trying to implement one yourself.
Themes consist of two different parts, the theme itself, and its corresponding components.
The theme is just a dictionary containing functions. These functions specify how the entries and cover should look, as well as global rules for the whole document. We'll cover the required structure of this variable in a later section.
Components are simply functions stored in a module. These functions contain things like pro/con tables and decision-matrices. Most components are just standalone, but the Notebookinator does supply some utility functions to help with implementing harder components. Implementing components will be covered in this section.
File Structure
The first thing you'll need to do is create a folder for your theme, somewhere in your notebook. As an example, lets create a theme called foo
. The first thing we'll want to do is create a folder called foo/
. Then, inside that folder, we'll want to create a file called foo.typ
inside the foo/
folder. This will be the entry point for your theme, and will contain your theme variable.
Then, we'll want to create a foo/components/components.typ
file. This file will contain all of your components. We recommend having each component inside of its own file. For example, an example-component
might be defined in foo/components/example-component.typ
.
You'll also want to create an entries.typ
file to contain all of your entry functions for your theme variable, and a rules.typ
to store your global rules.
Your final file structure should look like this:
foo/
foo.typ
entries.typ
rules.typ
components/
components.typ
This is just the recommended file structure, as long as you expose a theme variable and components, your theme will work just like all the others. You can also add any additional files as you wish.
The Theme Variable
Now that you've created your files, you can begin writing your theme. The first thing you should do is create a theme variable. Going back to our foo
example, lets create a foo-theme
variable in our foo/foo.typ
file.
// foo/foo.typ
#let foo-theme = (:) // currently a blank dictionary
Currently our theme is blank, and will do nothing. If we try to apply it right now, all functions will fall back onto the default-theme
.
Creating The Entries
Now that we actually have a place to put our theme functions, we can start implementing our entry functions.
Each of these functions has 2 requirements:
- it must return a
page
function as output - it must take a dictionary parameter named
ctx
as input, and a parameter called body.
The ctx
argument provides context about the current entry being created. This dictionary contains the following fields:
title
:str
type
:str
date
:datetime
author
:str
witness
:str
body
contains the content the user has written. It should be passed into the page
function in some shape or form.
We'll write these functions in the foo/entries.typ
file. Below are some minimal starting examples:
Frontmatter
// foo/entries.typ
#let frontmatter-entry(ctx: (:), body) = {
show: page.with( // pass the entire function scope into the `page` function
header: [ = ctx.title ],
footer: context counter(page).display("i")
)
body // display the users's written content
}
Body
// foo/entries.typ
#let body-entry(ctx: (:), body) = {
show: page.with(
header: [ = Body header ],
footer: counter(page).display("1")
)
body
}
Appendix
// foo/entries.typ
#let appendix-entry(ctx: (:), body) = {
show: page.with(
header: [ = Appendix header ],
footer: counter(page).display("i")
)
body
}
With the entry functions written, we can now add them to the theme variable.
// foo/foo.typ
// import the entry functions
#import "./entries.typ": frontmatter-entry, body-entry, appendix-entry
// store the entry functions in the theme variable
#let foo-theme = (
frontmatter-entry: frontmatter-entry,
body-entry: body-entry,
appendix-entry: appendix-entry,
)
Creating a Cover
Then you'll have to implement a cover. The only required parameter here is a context variable, which stores information like team number, game season and year.
Here's an example cover:
// foo/entries.typ
#let cover(ctx: (:)) = [
#set align(center)
*Foo Cover*
]
Then, we'll update the theme variable accordingly:
// foo/foo.typ
// import the cover along with the entry functions
#import "./entries.typ": cover frontmatter-entry, body-entry, appendix-entry
#let foo-theme = (
cover: cover, // store the cover in the theme variable
frontmatter-entry: frontmatter-entry,
body-entry: body-entry,
appendix-entry: appendix-entry,
)
Rules
Next you'll have to define the rules. This function defines all of the global configuration and styling for your entire theme. This function must take a doc parameter, and then return that parameter. The entire document will be passed into this function, and then returned. Here's and example of what this could look like:
// foo/rules.typ
#let rules(doc) = {
set text(fill: red) // Make all of the text red, across the entire document
doc // Return the entire document
}
Then, we'll update the theme variable accordingly:
// foo/foo.typ
#import "./rules.typ": rules // import the rules
#import "./entries.typ": cover frontmatter-entry, body-entry, appendix-entry
#let foo-theme = (
rules: rules, // store the rules in the theme variable
cover: cover,
frontmatter-entry: frontmatter-entry,
body-entry: body-entry,
appendix-entry: appendix-entry,
)
Writing Components
With your base theme done, you may want to create some additional components for you to use while writing your entries. This could be anything, including graphs, tables, Gantt charts, or anything else your heart desires. We recommend including the following components by default:
- Table of contents
toc()
- Decision matrix:
decision-matrix()
- Pros and cons table:
pro-con()
- Glossary:
glossary()
We recommend creating a file for each of these components. After doing so, your file structure should look like this:
foo/components/
components.typ
toc.typ
decision-matrix.typ
pro-con.typ
glossary.typ
Once you make those files, import them all in your components.typ
file:
// foo/components.typ
// make sure to glob import every file
#import "./toc.typ": *
#import "./glossary.typ": *
#import "./pro-con.typ": *
#import "./decision-matrix.typ": *
Then, import your components.typ
into your theme's entry point:
// foo/foo.typ
#import "./components/components.typ" // make sure not to glob import here
Pro / Con Component
Pro / con components tend to be extremely simple. Define a function called pro-con
inside your foo/components/pro-con.typ
file:
#pro-con(pros: [], cons: []) = {
// implement your table here
}
For examples on how to create a pro / con table, check out out how other themes implement them.
TOC Component
The next three components are a bit more complicated, so we'll be spending a little more time explaining how they work. Each of these components requires some information about the document itself (things like the page numbers of entries, etc.). Normally fetching this data can be rather annoying, but fortunately the Notebookinator has created some utility functions to abstract this.
To get started with your table of contents, first define a function called toc
in your foo/components/toc.typ
file.
However, unlike the pro-con
function, this function will depend on the print-toc
utility function. This looks like this:
// foo/components/toc.typ
// Use this import if you're developing in the Notebookinator directly
#import "/utils.typ"
// Use this import if you're using the notebookinator as an external dependency
#import "@local/notebookinator:1.0.1": utils
#let toc() = utils.print-toc((frontmatter, body, appendix) => {
// ...
}
This syntax might look a little weird, so lets break it down. We're defining the function toc
, which is equal to the utils.print-toc
function. The utils.print-toc
function takes 1 argument, and this argument is a function. We're choosing to pass in a lambda function here, since this function only makes sense in the context of the table of contents (we won't need to call it in multiple places).
Inside this lambda we have access to the frontmatter, body, and appendix variables. These variables are all arrays, which dictionaries which all contain the same information as the ctx
variables from this section, with the addition of a page-number
field, which is an integer.
With these variables, we can simply loop over each one, and print out another entry in the table of contents each time.
Here's what that looks like for the frontmatter entries:
// foo/components/toc.typ
#import "/utils.typ"
#let toc() = utils.print-toc((_, body, appendix) => {
// replace frontmatter with _
// to indicate we aren't using it
heading[Contents]
stack(spacing: 0.5em, ..for entry in body {
([
#entry.title
#box(width: 1fr, line(length: 100%, stroke: (dash: "dotted")))
#entry.page-number
],)
})
// TODO: do the same loop for the body entries as well
}
Decision Matrix Component
No data needs to be fetched for the decision matrix, however we do provide a helper function to calculate which choice has the highest score overall, and per category.
// foo/components/decision-matrix.typ
#import "/utils.typ"
#let decision-matrix(properties: (), ..choices) = {
let data = utils.calc-decision-matrix(properties: properties, ..choices)
}
Once you've calculated your results, you can render a table displaying them. Here's a simple example to get you started, copied from the default-theme
:
#let decision-matrix(properties: (), ..choices) = {
let data = utils.calc-decision-matrix(properties: properties, ..choices)
tablex( // table element
// the extra 2 columns account for the names of the choices and the total
columns: for _ in range(properties.len() + 2) {
(1fr,)
},
[], // Blank box
// first we'll display all of the names on the top row
..for property in properties {
([ *#property.name* ],)
},
// then we'll add an extra column for the total
[*Total*],
// then we'll add a row for each of the choices, and their scores
..for choice in data {
// Override the fill if the choice has the highest score
let cell = if choice.values.total.highest { cellx.with(fill: green) } else { cellx }
(cell[*#choice.name*], ..for value in choice.values {
(cell[#value.at(1).value],)
})
},
)
}
Glossary Component
The glossary component is similar to the table of components in that it requires information about the document to function.
To get access to the glossary entries, you can use the print-glossary
function provided by the utils
to fetch all of the glossary terms, in alphabetical order.
The function passed into the print-glossary
function has access to the glossary
variable, which is an array. Each entry in the array is a dictionary containing a word
field and a definition
field. Both are strings.
// foo/components/glossary.typ
// Use this import if you're developing in the Notebookinator directly
#import "/utils.typ"
// Use this import if you're using the notebookinator as an external dependency
#import "@local/notebookinator:1.0.1": utils
#let glossary() = utils.print-glossary(glossary => {
stack(spacing: 0.5em, ..for entry in glossary {
([
= #entry.word
#entry.definition
],)
})
}
Using the Theme
Now that you've written the theme, you can apply it to your notebook.
In the entry point of your notebook (most likely main.typ
), import your theme like this:
// main.typ
#import "foo/foo.typ": foo-theme, components
Once you do that, change the theme to foo-theme
:
// main.typ
#show: notebook.with(
theme: foo-theme
)