This page looks best with JavaScript enabled

My First Git Merge

 ·  ☕ 12 min read

Finally getting to start the year off right. As I’ve kind of alluded to last year, programming is not my core focus: it’s been much more of a pass time for me. As such, it’s not very often I actually work on projects outside of practicing web development or making quick scripts to solve certain problems.

This time, I actually thought of something that might be useful to others.

Ever since I’ve been looking into different text editors, I’ve also been looking at different markup languages as well. I’ve experimented with several: Markdown (of course), LaTeX, ASCIIdoc, and Groff1, and it’s been enlightening to see where each of them excel. The only problem I face though is when I want to use any of these languages to format a specific way.

The problem with a lot of these languages is the amount of fluff and extensions you need to really get what you need done. In this case, I’ve been looking to start an adventure using Pathfinder 2nd Edition, and that game has certain ways of laying out text for things like creatures, hazards, and treasures. Now, there are a few established means to do it in some of these languages. For markdown, you’ve got the community vault for Obsidian with its plugins for rendering and you’ve also got Scribe for a custom markdown syntax similar to Homebrewery for Dungeons and Dragons 5th Edition. While it works fine for preping for adventures, it feels very restrictive when writing, which conflicts really hard with markdown. Likewise, ASCIIdoc has a well maintained plugin, too, asciidoctor-p2e, but getting it sorted out with my unfamiliarity with the Ruby programming language has been a headache.

Me being persnickety like this is what brought me to discover typst, a hybrid language similar to LaTeX, but much easier to write in and (for our case) more manageable to write extensions for. I’ve been curious about language for some time after first hearing about it from watching Sylvan Franklin’s overview video. It was fairly easy to wrap my head around, but it didn’t seem to offer much more that I was looking for; however, when I started to explore the plugins for it, that’s when I saw potential.

A little project was sitting in the back of templates, Owlbear, 5th edition right in Typst. Now I knew what I wanted to make. If someone can set this language up for D&D, how hard can setting it up for Pathfinder be?

Did you know Typst is Turing Complete?

Like I mentioned above, one of things that I’ve been looking for with my markup language of choice in a nice balance of formatting/stylization control and ease to write in. Sure, I could just write it in HTML and style to my hearts content, but Lord knows I don’t want the tedium of having to wrap every bold or italic in <span>. No, what I want is something fairly complete to write in, but allows extension for more specific needs.

Typst delivers on this by having all your standard needs for writing and typesetting along with room to add almost whatever you may need through rust-like syntax. To give an example of what already comes in Typst, we can look at their documentation for something like a box:

1
2
3
4
5
6
7
#let icon = (
    box(
    height: 9pt
    image("docs.svg")
))

Refer to the docs #icon for more information.

A simple line of text reading “Refer to the docs for more information” with a academic paper icon inline between the works “docs” and “for”.

A box element allows you to place things like shapes or math expressions inline with the text of your writing as well allowing you to extend it further by adjusting the stylization of what’s inside the box in question. Plus, once you define a variable like #icon above, you can then refer to that instance throughout the rest of your document. For something like the action icons in PF2, I can just create a variable like #A to serve as a single action; then, any time I need to call upon that symbol, all I have to do is use that variable.

1
2
3
4
5
Single Action = #A
Double Action = #AA
Triple Action = #AAA
Reaction = #R
Free Action = #F

The above text rendered so that each variable is swapped with its respective PF2 icon.

This is just a simple example of what’s already there. The potential really starts to click when you develop your own functions. Typst also offers operational logics that enable you to script almost anything.

Typst is a statically typed language, meaning that all variables are assigned a certain “type” like a string, an integer, or a true or false, just for example. While this can ensure that the system is working with variables correctly, it also means that you might need to convert one into another type before you can successfully work with it. In cases where I need to convert something from “content”, text formatted in Typst, to a string, I use a script I found on the Typst github.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#let to-string(it) = {
  if type(it) == str {
    it
  } else if type(it) != content {
    str(it)
  } else if it.has("text") {
    it.text
  } else if it.has("children") {
    it.children.map(to-string).join()
  } else if it.has("body") {
    to-string(it.body)
  } else if it == [ ] {
    " "
  }
}

to-string checks if the value is already a string and, if not, it goes creates a string value and appends each proceeding word from the content to it. Now, I can take this new string variable and interact with it in ways that I can’t with a content variable.

A perfect example is formatting text dependent on how that string starts. Pathfinder and other games inspired by Dungeons & Dragons have Critical Successes and Critical Failures that can occur by rolling a 20 or a 1 on a 20-sided die, respectfully. PF2 provides special formatting when communicating this in all of its stat block. To make sure that that rule applies, I built another function:

1
2
3
4
5
6
7
8
9
#let roll-result(it) = {
  let output = false
  if to-string(it).starts-with("Critical Success") {output = true}
  else if to-string(it).starts-with("Success") {output = true}
  else if to-string(it).starts-with("Failure") {output = true}
  else if to-string(it).starts-with("Critical Failure") {output = true}
  else if to-string(it).starts-with("Heightened (") {output = true}
  return output
}

Just like the to-string function above, roll-result can be called any time, but instead of returning a string, it returns a boolean value, a true or false. With this check, I can tell a formatting function whether or not to apply something different to that line. For example, in many of the functions, I can use roll-result to tell how to style that paragraph:

1
2
3
4
5
if roll-result(effect) {
    par(hanging-indent: 1em)[#effect]
} else {
    par(hanging-indent: 0pt, first-line-indent: 1em)[#effect]
}}

This hardly expresses the possibilities that can arise from this. You can code a hefty amount in Typst to really deliver whatever it is your looking to express while having a clean way to write and deliver it.

Pathfinder’s Stat Blocks

The first thing to do, and probably the most important, is to lay out creature encounters. PF2 has a unique way of presenting stat blocks with creatures being arguably one of the easier. Here’s an example from the Monster Core 1 book:

A page from the Monster Core 1 depicting the stats for a “Goblin Warrior”

The Goblin Warrior is a simple creature that any adventuring party is likely to run into at least once during their escapades. As you might be able to see above, it has a number of distinct elements for how its formatted to communicate effectively to the reader: the creature name and level being on the same line, traits of the creature being colored block elements listed sequentially under the name, hanging indents on each line element. Tackling each of these things one at time, I needed to make a element with the name and level on the same line, aligned left and right, respectively.

To accomplish that look, I set up an encounter function like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#let encounter(comp) = [
  #v(1em) //applies padding above the stat block
    #set par(spacing: .6em, first-line-indent: 0em) // Sets character spacing 
    #let creature_header(body) = { // The "creature_header" is first defined as a local variable so that I can assign the name and encounter type of it
      box(
        text(weight: "extrabold", size: 1.3em, stretch: 50%)[#upper(comp.name)] // Places the name on the left hand margin
      )
      h(1fr)
      sym.wj // Acts as a dynamic spacing between the "comp.name" and "comp.type" so that the type is on the right hand side
      box(text(weight: "extrabold",size: 1.3em, stretch: 50%)[#upper(comp.type)])
    }
    #creature_header[] // Now that he variable was defined, it's enacted.
    #line(stroke: 1pt, length: 100%) // Just a line
  #pftraits(comp.traits) // This lists and styles all of the traits assigned to the encounter
  #for entry in (comp.details) { // A for loop that iterates over each line in "details" so that they are formatted correctly
  if entry == [---] or entry == [line] {line(stroke: 1pt, length: 100%); continue} // If I write "---", it understands to put an actual line there
  if roll-result(entry) {par(hanging-indent: 1em)[#entry]; continue}
  if comp.type == [Complication] or comp.type == [Opportunities] or comp.type == [] or comp.type == [Obstacle]  {par(hanging-indent: 0em)[#entry]; continue} // A type of encounter with specific formatting
  if comp.type == [Background] {par(hanging-indent: 0em, first-line-indent: 1em)[#entry]; continue} // Another type of encounter
    par(hanging-indent: 1em)[#entry] // The default styling
  }

Now that we have our function defined, we can call it by providing the function with the details it’s looking for.

1
2
3
4
5
6
#encounter((
  name: 
  type:
  traits:
  details: ()
) 

This function takes several parameters and renders them all as a single object on the page. All we have to do is fill it in.

The name is a simple field: just enter the name of what you’re looking to stat. In this case, we would just enter “Goblin Warrior”. For all of these fields, its important to wrap whatever you’re writing in either quotation marks "" or brackets [] so that Typst understands what type they are as mentioned prior.

Next, we have the encounter’s type–I know I keep saying “type” and this can get confusing. All type is is what’s on the right-hand top of the stat block; is it a creature, hazard, town, etc. and what level is it?

Encounters can be with creatures, hazards, or even towns and locations. Each of these contain certain traits like human or dragon, simple or complex, large or small. Pathfinder has these traits towards the top of a stat block to quickly communicate what this thing might be and what sort of aspects might relate to it. For instance, oozes share two similar traits: “ooze” and “mindless”. “Ooze” indicates what creature family its in and what you might expect it to look or act like while “mindless” specifically indicates that this creature can’t be affected by mind altering effects like charm or persuasion.

When you list a creatures traits, they do need to be wrapped in quotation marks. I intend to fix this with next update, but till then it’s important they are entered as strings so they are formatted correctly.

Finally, the details are the actual abilities of the encounter. These are listed per line and wrapped with brackets so that any special styling you apply persists. What’s also cool about the details field is that you can include horizontal lines to break up sections of the stat block.

Now if we about copy what’s in the stat block and format it as its described above, this is what we get:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#encounter((
  name: [Goblin Warrior], 
  type: [Creature -1],
  traits: ([small],[goblin], [humanoid]), //Make sure blocks without traits are written like this; otherwise, you might get a error
  details: (
    [*Perception* +2; darkvision],
    [*Languages* Common. Goblin],
    [*Skills* Acrobatics +5, Athletics +2, Nature +1, Stealth +5],
    [*Str* +0, *Dex* +3, *Con* +1, *Int* +0, *Wis* -1, *Cha* +1],
    [*Items* dogslicer, leather armor, shortbow (10 arrows)],
    [---],
    [*AC* 16, *Fort* +5, *Ref* +7, *Will* +3], 
    [*HP* 6 ],
    [*Goblin Scuttle #R Trigger* A goblin ally ends a move action adjacent to the warrior; *Effect* The goblin warrior Steps.],
    [---],
    [*Speed* 25 feet],
    [*Melee* #A dogslicer +7 [+3/-1] (agile, backstabber, finesse), *Damage* 1d6 slashing],
    [*Ranged* #A shortbow +7 [+2/-3] (deadly d10, range increment 60 feet, reload 0), *Damage* 1d6 piercing]),
)) 

Now with everything filled out, we just need to place the goblin warrior in a .typ file and import my package by placing the following at the top of the file:

1
2
#import "@preview/pf2e-style:0.2.0": *
#show: pf-stylization

And VOILÀ!

A snapshot of what would be rendered using the above set up. It’s a Pathfinder 2nd Edition stat block for the “Goblin Warrior”

This is exactly what I’ve been looking for! Sure, there is extra fluff by having to add the function and other syntax, but it still looks easily more easily readable and renders exactly how I need it to while being optimized from printing. What’s even better is that I can just extend it further!

Along with encounters, I also have things set up for spells, feats, items, chapter headers, comments, etc. No, it’s not nearly as complete as scribe, but I’m not quite looking to be. I wanted a tool I felt good writing in that had room for whatever extension I needed to throw at it. This is only the start. I intend to come back and improve this project as I find more holes or things I need to add. I’m sure there’s also room for general improvement in the syntax or rendering.

If you want to see more of what’s to offer, you can check out either the package page or look into my gitlab. I also intend to write and post some materials using this, but that’s more likely to come with the extra time I’ll be having soon. Either way, it’s felt incredible to actually build something like this and I hope someone else find usage in it, too.


  1. Groff and LaTeX are more typesetting languages. While they can and are typically used for general writing, their main function lines in more particularly in document layout. Still, understanding those languages, especially LaTeX, has been really handy, but the abundance of boilerplating in both of them has been more than I would want to deal with for general writing. ↩︎

Share on

Zachary Burkey
WRITTEN BY
Zachary Burkey
Freelane Web Developer