HTML Templates
Written by Isaac Van Doren
Notes
- This chapter has no code dependencies, but it would be good to place it somewhere after the HTML parsing chapter so that parsing without combinators can be introduced earlier.
- The generated HTML will contain some whitespace weirdness due to the presence of the directives in the templates. This does not impact the way the HTML is rendered (unless using
pre
). Fully handling the whitespace correctly would complicate the parser more, so I am going to leave it out of the chapter.
- There is more code that can be cut to decrease the scope of the chapter if necessary, but I am leaving it in for the time being.
Outline
Intro
- What is an HTML template language?
- Why use one instead of writing functions to generate HTML?
- Good for enhancing existing content.
- The template looks much more like the output.
- Many template languages have runtime errors; we want compile time errors.
- We will use code generation to accomplish this.
- Reflection is not an option here.
- Avoiding these features can give us compile time errors and faster runtime execution.
- Having a fast compiler is very relevant here.
Design
- Write a template like
helloWorld.rtl
.
- Run
rtl
to parse the template file and generate a Pages.roc
file containing a function called helloWorld
.
- Call the function to generate HTML.
- Doing this means the template function becomes a normal part of our codebase so we get compile time errors and LSP support.
Parsing
- First we need to parse the template into a data structure we can work with.
- A parser is a function that takes unstructured input like a list of bytes, and produces a useful data structure.
- In our case the data structure will look something like this: (show small version of Node type)
- A parser may not always succeed, so we need to model the failure with
[Match, NoMatch a]
.
- We need to know how much the parser consumed, so we need to return the remaining input in addition to the value:
[Match, NoMatch {val: a, input: List U8}]
.
- Composing these parsers becomes tedious, so we can use parser combinators.
- The
Parser a
type alias is useful to help us think about composing parsers.
- We don’t need to have failure states in our parser; if a parser doesn’t match, just fall back on text.
- We could implement error messages for incomplete directives if desired.
- Supporting these items will give us a very expressive language:
- Interpolation
- Conditionals
- Lists
- When is
- Raw interpolation
- Useful for dynamically displaying HTML
Code Generation
- Once we’ve parsed the input we need to generate the output.
- First we can walk the data structure from the parser and condense it into a smaller group of nodes.
- We can then take each node and generate the Roc code that will generate the output string.
- This process is recursive because some nodes contain a list of other nodes.
- For each list of nodes, we will generate a Roc list and pass it into
Str.joinWith _ ""
.
- This has the advantage of being easy and readable, although it would probably be faster with buffer passing style.
- We need to take care to indent each level properly.
Conclusion
- With a relatively small amount of work, we already have a very nice template language with compile time errors.
- Using Roc functions directly in the template is great because we don’t have to learn/build another language.
- We can do this nicely because Roc uses pure functions which allows us to pipe together single line expressions.
- This doesn’t work as well in languages where the standard library uses mutation more commonly.
- We can write arbitrary patterns in list and when expressions because we are just generating normal Roc code.
- We can approach the feeling of a dynamic template language because we have
- A fast compiler
- structural records
- type inference
- We have the opportunity to make the runtime execution very fast because we don’t have to do reflection.
- Errors in the template reference Pages.roc which is not ideal.
- This could be resolved by building out more tooling.