godtpl

package module
v0.0.0-...-0107538 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2026 License: MIT Imports: 11 Imported by: 0

README

godtpl

A Go package for generating OpenDocument Text (.odt) files using Go's standard text/template engine.

English | 繁體中文

[!WARNING] This project was developed with AI assistance (Vibe Coding). The code has not been fully reviewed by a human and may contain bugs, security vulnerabilities, or unexpected behavior. Do not use in production environments. Use at your own risk. Issues and PRs to improve the project are welcome.

Overview

godtpl lets you create .odt documents by writing them as Go templates in LibreOffice Writer (or any ODT-compatible editor), then rendering them with data at runtime. It handles the XML patching needed to work around how editors fragment template tags across multiple XML elements.

Features

  • Use standard Go text/template syntax inside ODT files
  • Loop over table rows, paragraphs, and inline spans
  • Rich text formatting (bold, italic, color, font size, etc.)
  • Multi-line text with newlines, tabs, and paragraph breaks
  • Embed inline images
  • Embed sub-documents (merge multiple ODT files)
  • No external dependencies — standard library only

Installation

go get github.com/scbmark/godtpl

Quick Start

  1. Create a template in LibreOffice Writer with placeholders like {{ .name }}, save it as template.odt.
  2. Render it from Go:
package main

import (
    "github.com/scbmark/godtpl"
    "log"
)

func main() {
    tpl, err := godtpl.New("template.odt")
    if err != nil {
        log.Fatal(err)
    }

    err = tpl.Render(map[string]any{
        "company": "ACME Corp",
        "year":    2024,
    })
    if err != nil {
        log.Fatal(err)
    }

    err = tpl.SaveTo("output.odt")
    if err != nil {
        log.Fatal(err)
    }
}

Template Syntax

Templates use standard Go text/template syntax. Write directly inside your ODT document.

Variable substitution
{{ .name }}
{{ .address }}
{{ .order.total }}        {{/* nested field / map key access */}}
Template variables

Declare local variables with :=:

{{ $label := .firstName }}
Hello, {{ $label }}!

{{ $count := len .items }}
Total: {{ $count }}
Conditionals
{{ if .isPremium }}Premium customer{{ end }}

{{ if .isPremium }}
  Premium
{{ else }}
  Standard
{{ end }}

{{ if gt .score 90 }}Excellent{{ else if gt .score 60 }}Pass{{ else }}Fail{{ end }}
Loops
Loop over a slice
{{ range .items }}
  {{ .name }}: {{ .price }}
{{ end }}

Access the loop index with $i:

{{ range $i, $item := .items }}
  {{ $i }}: {{ $item.name }}
{{ end }}
Loop over a map (map[string]string / map[string]any)

Pass a map value in your context:

err = tpl.Render(map[string]any{
    "attrs": map[string]string{
        "color": "red",
        "size":  "large",
    },
})

Then iterate over it in the template:

{{ range $key, $value := .attrs }}
  {{ $key }}: {{ $value }}
{{ end }}

Keys are iterated in alphabetical order (Go's standard map range behaviour).

Loop over ODT table rows / paragraphs / spans

For looping over table rows, paragraphs, or inline spans, use shorthand prefixes to strip the surrounding ODT XML element:

Prefix Strips element
{%tr <table:table-row>
{%tc <table:table-cell>
{%p <text:p>
{%s <text:span>

In your ODT table, put the loop control in the first cell of a row:

{%tr range .items %}  {%tr end %}

The row element itself is stripped; only the template statement remains.

Combining with map range:

{%tr range $key, $value := .attrs %}
  (first cell) {{ $key }}   (second cell) {{ $value }}
{%tr end %}
Escaping literal {{ and }}

If you need literal braces in output, use the escape sequences:

Write in template Output
{_{ {{
}_} }}
Built-in template functions
Function Description
xml_escape Escapes a string for safe insertion into XML
xml_safe Pass-through for values already valid as XML
{{ xml_escape .userInput }}

API Reference

Loading a template
// From a file path
tpl, err := godtpl.New("template.odt")

// From an io.Reader
tpl, err := godtpl.NewFromReader(r)
Rendering and saving
// Render with a context map
err = tpl.Render(map[string]any{ ... })

// Save to a file
err = tpl.SaveTo("output.odt")

// Save to an io.Writer
err = tpl.Save(w)

The same Template instance can be rendered multiple times with different contexts.

Multi-line text — Listing

Listing handles newlines and special characters inside a single ODT text node.

listing := godtpl.NewListing("Line one\nLine two\nLine three")
Escape ODT output
\n <text:line-break/>
\t <text:tab/>
\a New paragraph (same style)
\f Soft page break + new paragraph

Use in template:

{{ .description }}

where ctx["description"] = godtpl.NewListing("...").

Rich text — RichText / R

RichText (alias R) builds inline-formatted text programmatically.

rt := godtpl.NewRichText(tpl)
rt.Add("Hello ", godtpl.TextProps{Bold: true})
rt.Add("world", godtpl.TextProps{Color: "#FF0000", Size: 14})
rt.AddStyled("Important", "Emphasis")  // use a named style from the ODT

TextProps fields:

Field Type Description
Bold bool Bold
Italic bool Italic
Underline string "", "solid", "dotted", etc.
Strike bool Strikethrough
Color string Hex color, e.g. "#FF0000"
Size float64 Font size in points
Font string Font family name
Superscript bool Superscript
Subscript bool Subscript
ctx["title"] = rt
Rich text paragraphs — RichTextParagraph / RP

RichTextParagraph (alias RP) builds block-level paragraphs with optional paragraph styles.

rp := godtpl.NewRichTextParagraph(tpl)
rp.Add(rt, "Heading_20_2")  // second arg is an optional paragraph style name
rp.Add(rt2, "")             // no paragraph style
Inline images — InlineImage
// From a file
img, err := godtpl.NewInlineImage(tpl, "logo.png")

// From an io.Reader
img, err := godtpl.NewInlineImageFromReader(tpl, r, "png")

img.Width  = "4cm"
img.Height = "2cm"
{{ .logo }}

Supported formats: PNG, JPG, GIF, BMP, SVG, TIFF, WEBP.

Sub-documents — Subdoc

Embed a separate ODT file inside the output document.

sd, err := tpl.NewSubdoc("chapter.odt")
ctx["chapter"] = sd
{{p .chapter }}

Styles from sub-documents are automatically prefixed to avoid name collisions.

Requirements

  • Go 1.21+
  • No external dependencies

License

MIT

Documentation

Overview

Package godtpl renders OpenDocument Text (.odt) files as Go templates.

An ODT file is a ZIP archive whose main content lives in content.xml. This package treats that XML as a Go text/template, renders it with a user-supplied context map, and writes the result back into a new ZIP.

Basic usage

tpl, err := godtpl.New("template.odt")
if err != nil { ... }
err = tpl.Render(map[string]any{
    "company": "ACME",
    "items":   []map[string]any{...},
})
if err != nil { ... }
err = tpl.SaveTo("output.odt")

Template syntax

Templates use Go's text/template syntax. Variables in the context map are accessed with a leading dot:

{{ .company }}          simple substitution
{{ range .items }}      loop
  {{ .name }}
{{ end }}
{{ if .show }} … {{ end }}   conditional

To escape a literal {{ write {_{ and }_} for }}:

{_{ and }_}  →  {{ and }}  in the output

Shorthand prefixes

To control an entire ODF element (e.g. a table row) from a template tag, add a shorthand prefix to strip the surrounding element:

{%tr range .items %}   or   {{tr range .items }}   →  {{ range .items }}  (strips <table:table-row>)
{%tr end %}            or   {{tr end }}             →  {{ end }}
{%tc STMT %}           or   {{tc STMT }}             →  controls <table:table-cell>
{%p  STMT %}           or   {{p  STMT }}             →  controls <text:p>
{%s  STMT %}           or   {{s  STMT }}             →  controls <text:span>

Special context types

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type InlineImage

type InlineImage struct {
	Width  string // ODF length string, e.g. "5cm", "2in", "96pt"
	Height string
	Anchor string // text:anchor-type; defaults to "as-char" (inline)
	// contains filtered or unexported fields
}

InlineImage embeds a picture inline inside an ODF paragraph.

In ODF, images are stored in the Pictures/ directory inside the ZIP archive and referenced from content.xml via <draw:frame>/<draw:image> elements.

Usage:

img, err := godtpl.NewInlineImage(tpl, "logo.png")
img.Width = "4cm"
img.Height = "2cm"
context["logo"] = img

In the .odt template:

{{ .logo }}

func NewInlineImage

func NewInlineImage(tpl *Template, path string) (*InlineImage, error)

NewInlineImage creates an InlineImage by reading the file at path.

func NewInlineImageFromReader

func NewInlineImageFromReader(tpl *Template, r io.Reader, ext string) (*InlineImage, error)

NewInlineImageFromReader creates an InlineImage from an io.Reader. ext is the file extension without the leading dot (e.g. "png").

func (*InlineImage) String

func (img *InlineImage) String() string

String implements fmt.Stringer. Called by text/template during rendering.

type Listing

type Listing struct {
	// contains filtered or unexported fields
}

Listing wraps multi-line / tabulated text for use in an ODF template.

After template rendering, special characters in the text are converted to proper ODF inline elements by the template engine:

  • \n → <text:line-break/> (soft line break inside the same paragraph)
  • \t → <text:tab/>
  • \a → closes and reopens <text:p> (new paragraph, same style)
  • \f → soft page break + new <text:p>

Usage in Go code:

context["body"] = godtpl.NewListing("Line one\nLine two")

Usage in the .odt template:

{{ .body }}

func NewListing

func NewListing(text string) *Listing

NewListing creates a Listing from a plain text string. HTML special characters (<, >, &) are escaped so they are safe in XML. The control characters \n, \t, \a, \f are preserved for later conversion.

func (*Listing) String

func (l *Listing) String() string

String implements fmt.Stringer for use with text/template.

type R

type R = RichText

R is a convenience alias for RichText (mirrors the Python docxtpl naming).

type RP

type RP = RichTextParagraph

RP is a convenience alias for RichTextParagraph.

type RichText

type RichText struct {
	// contains filtered or unexported fields
}

RichText builds an inline rich-text fragment for use inside an existing ODF paragraph. It implements fmt.Stringer so that text/template inserts the generated XML verbatim.

Usage:

rt := godtpl.NewRichText(tpl)
rt.Add("Hello ", TextProps{Bold: true})
rt.Add("world", TextProps{Color: "#FF0000"})
context["greeting"] = rt

In the .odt template:

{{ .greeting }}

func NewRichText

func NewRichText(tpl *Template) *RichText

NewRichText creates a new empty RichText associated with the given template.

func (*RichText) Add

func (rt *RichText) Add(text string, props TextProps) *RichText

Add appends a text run with the given formatting properties. If props is zero-valued, the text is inserted without any style wrapper.

func (*RichText) AddPlain

func (rt *RichText) AddPlain(text string) *RichText

AddPlain appends unstyled text.

func (*RichText) AddStyled

func (rt *RichText) AddStyled(text, style string) *RichText

AddStyled appends text wrapped in a named character style from the ODT template.

func (*RichText) Merge

func (rt *RichText) Merge(other *RichText) *RichText

Merge appends all fragments from another RichText into rt.

func (*RichText) String

func (rt *RichText) String() string

String implements fmt.Stringer. Called by text/template during rendering.

type RichTextParagraph

type RichTextParagraph struct {
	// contains filtered or unexported fields
}

RichTextParagraph builds one or more complete <text:p> paragraphs for block-level insertion. Use with the {{p ...}} shorthand in the template.

Usage:

rp := godtpl.NewRichTextParagraph(tpl)
rt := godtpl.NewRichText(tpl)
rt.Add("Bold heading", godtpl.TextProps{Bold: true})
rp.Add(rt, "Heading_20_2")
context["header"] = rp

In the .odt template (block-level, strips surrounding <text:p>):

{{p .header }}

func NewRichTextParagraph

func NewRichTextParagraph(tpl *Template) *RichTextParagraph

NewRichTextParagraph creates a new empty RichTextParagraph.

func (*RichTextParagraph) Add

func (rp *RichTextParagraph) Add(content fmt.Stringer, paraStyle string)

Add appends a paragraph. content may be a *RichText or any fmt.Stringer. paraStyle is an optional named paragraph style from the template document.

func (*RichTextParagraph) String

func (rp *RichTextParagraph) String() string

String implements fmt.Stringer.

type Subdoc

type Subdoc struct {
	// contains filtered or unexported fields
}

Subdoc wraps an existing .odt file so it can be embedded into a Template.

Create instances via Template.NewSubdoc():

sd, err := tpl.NewSubdoc("chapter.odt")
tpl.Render(map[string]any{"chapter": sd})

In the master template (block-level insertion, strips surrounding <text:p>):

{{p .chapter }}

Automatic styles from the sub-document are renamed with a unique prefix and injected into the master document to avoid collisions. Images inside the sub-document are copied into the output archive.

func (*Subdoc) String

func (sd *Subdoc) String() string

String implements fmt.Stringer. text/template calls this when it evaluates {{ .subdocVar }}. The body XML is inserted verbatim into the output.

type Template

type Template struct {
	IsRendered bool
	// contains filtered or unexported fields
}

Template is the central object. It holds the original ODT bytes and accumulates rendering state. A single Template may be rendered multiple times with different contexts.

func New

func New(path string) (*Template, error)

New loads an ODT template from a file.

func NewFromReader

func NewFromReader(r io.Reader) (*Template, error)

NewFromReader loads an ODT template from an io.Reader.

func (*Template) ContentXML

func (t *Template) ContentXML() (string, error)

ContentXML returns the raw content.xml from the template (before patching).

func (*Template) NewSubdoc

func (t *Template) NewSubdoc(path string) (*Subdoc, error)

NewSubdoc creates a new Subdoc associated with tpl. If path is non-empty, the ODT file at that path is loaded.

func (*Template) Render

func (t *Template) Render(ctx map[string]any) error

Render renders the template with the supplied context map and stores the result internally. Call SaveTo or Save afterwards to write the output.

func (*Template) Save

func (t *Template) Save(w io.Writer) error

Save writes the rendered ODT to w.

func (*Template) SaveTo

func (t *Template) SaveTo(path string) error

SaveTo writes the rendered ODT to path.

func (*Template) StylesXML

func (t *Template) StylesXML() (string, error)

StylesXML returns the raw styles.xml from the template (before patching).

type TextProps

type TextProps struct {
	Bold        bool
	Italic      bool
	Underline   string // "" = none, "solid", "dotted", etc.
	Strike      bool
	Color       string  // hex color with or without leading "#"
	Size        float64 // font size in points, 0 = unset
	Font        string  // font family, "" = unset
	Superscript bool
	Subscript   bool
}

TextProps holds character formatting properties for a RichText run.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL