Introduction

Welcome to the documentation for the notebookinator, a Typst package meant to simplify the notebooking process for the Vex Robotics Competition. Its theming capabilities handle all of the styling for you, letting you jump right into writing documentation.

While it was designed with VRC in mind, it could just as easily be used for other competitor systems such as the First Robotics Competition and the First Tech Challenge.

If you're new here, we recommend you read the installation guide and the basic usage guide.

If you already know what you're doing, and just some quick information, check out the API reference.

If you're an advanced user, and want to extend / contribute to the Notebookinator, check out our developer documentation.

Installation

Warning

This installation process is temporary, as we wait for Typst to overhaul its process for packaging templates and packages.

Devcontainer / Codespaces

The easiest way to use the Notebookinator is to use our devcontainer/codespace. This will automatically install all of the needed dependencies inside of a Docker container. We recommend using our quick start template for the best experience.

Warning

While this approach is easier, you may encounter some performance issues as your notebook increases in size.

The only thing you'll need in preparation is a GitHub account (only if using Codespaces) and VSCode installed. Once you do that, just follow the instructions in the README.

Local Installation

This installation process is a little harder, and requires more software to be manually installed on your computer.

Make sure you have the following software installed:

Once you've installed everything, run the following commands:

Info

If you're running this on Windows, you'll need to run these commands in a sh shell, like git-bash or the shell packaged with Cygwin or GitHub Desktop.

git clone --depth 1 --branch 1.0.1 https://github.com/The-Notebookinator/notebookinator
./notebookinator/scripts/package @local
rm -rf notebookinator

Once you do that you should be good to go!

Basic Usage

Now that you have the Notebookinator installed, you can start notebooking.

Setup

You can use our template either by creating a GitHub repository based on it with GitHub's official template feature, or just by downloading it. You can download the template simply by cloning it.

git clone https://github.com/The-Notebookinator/quick-start-template.git
# alternatively if you made your own repository you can clone it like this:
git clone <your-url-here>

Once you've done that, open the newly downloaded folder inside of VSCode or your editor of choice.

Editing Your Notebook

Adding New Entries

The Notebookinator allows for three different types of entries, frontmatter, body, and appendix. Each will be rendered as its own section, and has its own page count.

Frontmatter

Frontmatter entries, as their name implies, are shown at the beginning of the notebook. Entries of this type typically contain things like introductions, and the table of contents.

The template stores all of the frontmatter entries into the frontmatter.typ file by default. To add more frontmatter entries, simply call the create-frontmatter-entry function inside of the file like so:

#create-frontmatter-entry(title: "About")[
    Here's some info about this amazing notebook!
]

Frontmatter entries are rendered in the order they are created.

Body

The most common type of entry is the body entry. These entries store all of your notebook's main content.

The template puts all of the body entries inside of the entries/ folder. To make a new entry, make a new file in that folder. Then, #include that file in the entries/entries.typ file. For example, if you created a file called entries/my-entry.typ, then you'd add this line to your entries/entries.typ file:

#include "./my-entry.typ"

Body entries will be displayed in the order they are included in the entries/entries.typ file.

Once you've done that, you'll need to create a new entry inside of that file. This can be done with the create-body-entry function. If the file only contains a single entry, we recommend using a show rule to wrap the function as well, which will pass all of the content in the file into the create-body-entry function.

You can create a new body entry like so:

// not all themes require every one of these options
#show: create-body-entry.with(
  title: "My Awesome Entry",
  type: "identify", // The type of the entry depends on which theme you're using
  date: datetime(year: 2024, month: 1, day: 1),
)

Appendix

Appendix entries go at the end of the notebook, and are stored in the appendix.typ file.

You can create a new appendix entry like this:

#create-appendix-entry(title: "Programming")[
    Here's information about how we programmed the robot.

    #lorem(500)
]

Changing the Theme

In order to change the theme you'll need to edit two files, packages.typ and main.typ.

The first thing you'll need to do is edit which theme is being imported in packages.typ. For example, if you wanted to switch to the linear theme from the radial theme, you'd change packages.typ to look like this:

// packages.typ

// this file allows us to only specify package versions once
#import "@local/notebookinator:1.0.1": *
#import themes.linear: linear-theme, components // components is imported here so we don't have to specify which theme's components we're using.

Once you do that, you'll want to edit your main.typ to use the linear-theme instead of the radial-theme.

// main.typ

#show: notebook.with(
  // ...
  theme: linear-theme,
)

Note

Not all themes implement the same components, so you may encounter some issues when changing themes with a more developed notebook.

Using Components

Components are reusable elements created by themes. These are just functions stored inside a components module. Each theme should expose its own separate components module.

packages.typ should already export this module, so you can access it just by importing packages.typ

#import "/packages.typ": *

Now you can use any of the components in the theme by just calling them like you would a normal function. Here's how you would create a simple pro-con table.

#components.pro-con(
    pros: [
    Here are the pros.
    ],
    cons: [
    Here are the cons.
    ]
)

You can see what components a theme implements by reading the API reference.

Compiling / Viewing Your Notebook

Once you're happy with your notebook, you'll want to render it into a PDF.

You can do that with either of the following commands:

typst compile main.typ
# or if you want live updates
typst watch main.typ

You can then open main.pdf in any PDF viewer to see your rendered output.

API Reference

Check out the API reference here.

Developer Documentation

Welcome to the Notebookinator's developer documentation. This section is for advanced users who want to contribute to the Notebookinator's existing codebase, or want to use the existing tools at a more advanced level.

The section covers:

  • project architecture
  • creating your own theme
  • contributing themes to the Notebookinator

Project Architecture

The Notebookinator is split into two sections, the base template, and the themes. The base template functions as the backend of the project. It handles all of the information processing, keeps track of global state, makes sure page numbers are correct, and so on. It exposes the main API that the user interacts for creating entries and creating glossary entries.

The themes act as the frontend to the project, and are what the user actually sees. The themes expose an API for components that need to be called directly inside of entries. This could include things like admonitions, charts, and decision matrices.

File Structure

  • lib.typ: The entrypoint for the whole template.
  • internals.typ: All of the internal function calls that should not be used by theme authors or users.
  • entries.typ: Contains the user facing API for entries, as well as the internal template functions for printing out the entries and cover.
  • glossary.typ: Contains the user facing API for the glossary.
  • globals.typ: Contains all of the global variables for the entire project.
  • utils.typ: Utility functions intended to help implement themes.
  • themes/: The folder containing all of the themes.
    • themes.typ: An index of all the themes are contained in the template
  • docs.typ: The entry point for the project documentation.
  • docs-template.typ: The template for the project documentation.

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

Info

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
)

Contributing a Theme

Before you get started, we ask that you read our contributing guide.

Upon cloning your fork of the Notebookinator, you can find all of the existing themes in the themes/ folder.

Each theme in the themes/ folder follows the same file structure detailed in the previous guide. If you have a theme on hand, just copy it into the themes/ folder. For example, the foo theme's entry point should now be at themes/foo/foo.typ

If you refer to the Notebookinator as a local package anywhere in your theme, make sure to replace those imports with references to the files directly.

For example, if a file imports utils like this:

#import "@local/notebookinator:1.0.1": utils

Replace it with this:

#import "/utils.typ"

If you don't currently have a theme written, follow the previous guide from the beginning, but place the theme folder in the themes/ folder.