Honza Pokorný

A personal blog


Inside-out templates

I have been thinking about literate programming and markup languages for authoring documents. How can we write a document once and easily publish it in different formats? Markdown is user-friendly but bare-bones. XML does what we want but no one wants to write XML. I was reminded of Pollen. It promises to do what I want. Plus it’s a cool Lisp thing. But Lisp isn’t everyone’s thing. Then I had a flash of inspiration: what if we could abuse some Go templates?

Instead of using Go templates as templates, we can use them as an authoring language. Markdown on steroids.

Let’s imagine a document, say chapter.txt:

{{ title "Introduction" }}

Before we can start thinking about recursion we must first think about recursion.

The Wikipedia article on the topic {{ link "says" "https://en.wikipedia.org/wiki/Recursion" }} that...

It’s not as terse as Markdown but way nicer than XML.

How do we process it?

contentsBytes, err := os.ReadFile("chapter.txt")
if err != nil {
    return err
}

contents := string(contentsBytes)

t, err := template.New("chapter").Funcs(funcs).Parse(contents)
if err != nil {
    return err
}

t.Execute(os.Stdout, nil)

Pretty standard Go stuff. Read the file, create a new template, and print the result to standard output. But what is this funcs? You can define custom functions that will be available in your template (our document). This is where we can define our title and link functions. Let’s say we want to publish our chapter as HTML:

funcs := template.FuncMap{
    "title": func(text string) string {
        return fmt.Sprintf("<h1>%s</h1>", text)
    },
    "link": func(label string, url string) string {
        return fmt.Sprintf(`<a href="%s">%s</a>`, url, label)
    },
}

Running it will produce:

<h1>Introduction</h1>

Before we can start thinking about recursion we must first think about recursion.

The Wikipedia article on the topic <a href="https://en.wikipedia.org/wiki/Recursion">says</a> that...

It’s much nicer than using some custom string search and replace because the Go standard library handles all of the replacing for you: they have done the hard work of finding the edge-cases for you and handling them.

OK, onward: what about Markdown? Let’s make this a bit harder and use footnote-style links at the bottom of the page:

links := []string{}
linkCounter := 0
funcs = template.FuncMap{
	"title": func(text string) string {
		return fmt.Sprintf("%s\n%s", text, strings.Repeat("=", len(text)))
	},
	"link": func(label string, url string) string {
		id := linkCounter
		linkCounter++
		links = append(links, fmt.Sprintf("[%d]: %s", id, url))
		return fmt.Sprintf("[%s][%d]", label, id)
	},
}

The links will be collected in a slice and can be appended in a convenient place.

Introduction
============

Before we can start thinking about recursion we must first think about recursion.

The Wikipedia article on the topic [says][0] that...

[0]: https://en.wikipedia.org/wiki/Recursion

You can extend this in a million different ways. Need academic references? Hook this up to bibtex or Zotero. Images? Gnuplot graphs that are generated on the fly but different for webpages and ebooks? Syntax highlighting for code snippets?


This article was first published on November 18, 2024. As you can see, there are no comments. I invite you to email me with your comments, criticisms, and other suggestions. Even better, write your own article as a response. Blogging is awesome.