godocx

package module
v0.0.0-...-0037f85 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2025 License: MIT Imports: 14 Imported by: 0

README

Godocx-templates

Template-based docx report creation. (See the blog post).

HEAVILY inspired (aka copy/pasted) from docx-templates 🙏

go get github.com/ctengiz/godocx-template

Why?

  • Write documents naturally using Word, just adding some commands where needed for dynamic contents

Features

  • Insert the data in your document (INS, = or just nothing)
  • Embed images and HTML (IMAGE, HTML). Dynamic images can be great for on-the-fly QR codes, downloading photos straight to your reports, charts… even maps!
  • Add loops with FOR/END-FOR commands, with support for table rows, nested loops
  • Include contents conditionally, IF a certain code expression is truthy (IF/END-IF)
  • Define custom aliases for some commands (ALIAS) — useful for writing table templates!
  • Plenty of examples in this repo
  • Embed hyperlinks (LINK).
Not yet supported

Contributions are welcome!

Table of contents

Installation

$ go get github.com/ctengiz/godocx-template

Usage

Here is a simple example, with report data injected directly as an object:

import (
	"fmt"
	"log/slog"
	"os"
	"reflect"
	"time"

	. "github.com/ctengiz/godocx-template"
)

func main() {
   var data = ReportData{
		"dateOfDay":         time.Now().Local().Format("02/01/2006"),
		"acceptDate":        time.Now().Local().Format("02/01/2006"),
		"company":           "The company",
		"people": []any{
			map[string]any{"name": "John", "lastname": "Doe"},
			map[string]any{"name": "Barn", "lastname": "Simson"},
		},
   }

   options := CreateReportOptions{
        // mandatory
		LiteralXmlDelimiter: "||",
		// optionals
		ProcessLineBreaks: true,
   }

   outBuf, err := CreateReport("mytemplate.docx", &data, options)
	if err != nil {
		panic(err)
	}
	err = os.WriteFile("outdoc.docx", outBuf, 0644)
	if err != nil {
		panic(err)
	}
}

Writing templates

Create a word file, and write your template inside it.

dateOfDay: +++dateOfDay+++  acceptDate: +++acceptDate+++
company: +++company+++

+++FOR person IN people+++
  person:  +++INS $person.firstname+++  +++INS $person.lastname+++
+++END-FOR person+++

Custom command delimiters

You can use different left/right command delimiters by passing an object to CmdDelimiter:

options := CreateReportOptions{
	LiteralXmlDelimiter: "||",
	CmdDelimiter: &Delimiters{
		Open:  "{",
		Close: "}",
	},
}

This allows much cleaner-looking templates!

Then you can add commands in your template like this: {foo}, {project.name}, {FOR ...}.

Supported commands

Currently supported commands are defined below.

Insert data with the INS command ( or using =, or nothing at all)

Inserts the result of a given code snippet as follows.

Using code like this:

import (
	"fmt"
	"log/slog"
	"os"
	"time"

	. "github.com/ctengiz/godocx-template"
)

func main() {
   var data = ReportData{
		"name":    "John",
		"surname": "Appleseed",
   }

   options := CreateReportOptions{
		LiteralXmlDelimiter: "||",
   }

   outBuf, err := CreateReport("mytemplate.docx", &data, options)
	if err != nil {
		panic(err)
	}
	err = os.WriteFile("outdoc.docx", outBuf, 0644)
	if err != nil {
		panic(err)
	}
}

And a template like this:

+++name+++ +++surname+++

Will produce a result docx file that looks like this:

John Appleseed

Alternatively, you can use the more explicit INS (insert) command syntax.

+++INS name+++ +++INS surname+++

You can also use = as shorthand notation instead of INS:

+++= name+++ +++= surname+++

Even shorter (and with custom CmdDelimiter: &Delimiters{Open: "{", Close: "}"}):

{name} {surname}

Includes a hyperlink from a map[string]any with a url and label key, or *LinkPars:

data := ReportData {
	"projectLink": &LinkPars {
		Url: "https://theproject.url",
		Label: "The label"
	}
}
+++LINK projectLink+++

If the label is not specified, the URL is used as a label.

HTML

Takes the HTML resulting from evaluating a code snippet and converts it to Word contents.

Important: This uses altchunk, which is only supported in Microsoft Word, and not in e.g. LibreOffice or Google Docs.

+++HTML `
<meta charset="UTF-8">
<body>
  <h1>${$film.title}</h1>
  <h3>${$film.releaseDate.slice(0, 4)}</h3>
  <p>
    <strong style="color: red;">This paragraph should be red and strong</strong>
  </p>
</body>
`+++
IMAGE

The value should be an ImagePars, containing:

  • width: desired width of the image on the page in cm. Note that the aspect ratio should match that of the input image to avoid stretching.
  • height desired height of the image on the page in cm.
  • data: an ByteArray with the image data
  • extension: one of '.png', '.gif', '.jpg', '.jpeg', '.svg'.
  • thumbnail [optional]: when injecting an SVG image, a fallback non-SVG (png/jpg/gif, etc.) image can be provided. This thumbnail is used when SVG images are not supported (e.g. older versions of Word) or when the document is previewed by e.g. Windows Explorer. See usage example below.
  • alt [optional]: optional alt text.
  • rotation [optional]: optional rotation in degrees, with positive angles moving clockwise.
  • caption [optional]: optional caption displayed below the image

In the .docx template:

+++IMAGE imageKey+++

Note that you can center the image by centering the IMAGE command in the template.

In the ReportData:

data := ReportData {
  "imageKey": &ImagePars{
			Width:     16.88,
			Height:    23.74,
			Data:      imageByteArray,
			Extension: ".png",
		},
}
FOR and END-FOR

Loop over a group of elements (can only iterate over Array).

+++FOR person IN peopleArray+++
+++INS $person.name+++ (since +++INS $person.since+++)
+++END-FOR person+++

Note that inside the loop, the variable relative to the current element being processed must be prefixed with $.

It is possible to get the current element index of the inner-most loop with the variable $idx, starting from 0. For example:

+++FOR company IN companies+++
Company (+++$idx+++): +++INS $company.name+++
Executives:
+++FOR executive IN $company.executives+++
-	+++$idx+++ +++$executive+++
+++END-FOR executive+++
+++END-FOR company+++

FOR loops also work over table rows:

----------------------------------------------------------
| Name                         | Since                   |
----------------------------------------------------------
| +++FOR person IN             |                         |
| project.people+++            |                         |
----------------------------------------------------------
| +++INS $person.name+++       | +++INS $person.since+++ |
----------------------------------------------------------
| +++END-FOR person+++         |                         |
----------------------------------------------------------

And let you dynamically generate columns:

+-------------------------------+--------------------+------------------------+
| +++ FOR row IN rows+++        |                    |                        |
+===============================+====================+========================+
| +++ FOR column IN columns +++ | +++INS $row+++     | +++ END-FOR column +++ |
|                               |                    |                        |
|                               | Some cell content  |                        |
|                               |                    |                        |
|                               | +++INS $column+++  |                        |
+-------------------------------+--------------------+------------------------+
| +++ END-FOR row+++            |                    |                        |
+-------------------------------+--------------------+------------------------+

Finally, you can nest loops (this example assumes a different data set):

+++FOR company IN companies+++
+++INS $company.name+++
+++FOR person IN $company.people+++
* +++INS $person.firstName+++
+++FOR project IN $person.projects+++
    - +++INS $project.name+++
+++END-FOR project+++
+++END-FOR person+++

+++END-FOR company+++
IF and END-IF

Include contents conditionally (support: ==, !=, >=, <=, >, <):

+++IF name == 'John'+++
 Name is John
+++END-IF+++

The IF command is implemented as a FOR command with 1 or 0 iterations, depending on the expression value.

ALIAS (and alias resolution with *)

Define a name for a complete command (especially useful for formatting tables):

+++ALIAS name INS $person.name+++
+++ALIAS since INS $person.since+++

----------------------------------------------------------
| Name                         | Since                   |
----------------------------------------------------------
| +++FOR person IN             |                         |
| project.people+++            |                         |
----------------------------------------------------------
| +++*name+++                  | +++*since+++            |
----------------------------------------------------------
| +++END-FOR person+++         |                         |
----------------------------------------------------------

Inserting literal XML

You can also directly insert Office Open XML markup into the document using the literalXmlDelimiter, which is by default set to ||.

E.g. if you have a template like this:

+++INS text+++
data := ReportData{ "text": "foo||<w:br/>||bar" }

See http://officeopenxml.com/anatomyofOOXML.php for a good reference of the internal XML structure of a docx file.

License (MIT)

This Project is licensed under the MIT License. See LICENSE for more information.

Documentation

Index

Constants

View Source
const (
	T_TAG        = "w:t"
	R_TAG        = "w:r"
	P_TAG        = "w:p"
	RPR_TAG      = "w:rPr"
	TBL_TAG      = "w:tbl"
	TR_TAG       = "w:tr"
	TC_TAG       = "w:tc"
	DOCPR_TAG    = "wp:docPr"
	VSHAPE_TAG   = "v:shape"
	ALTCHUNK_TAG = "w:altChunk"

	DEFAULT_CMD_DELIMITER         = "+++"
	CONTENT_TYPES_PATH            = "[Content_Types].xml"
	TEMPLATE_PATH                 = "word"
	DEFAULT_LITERAL_XML_DELIMITER = "||"
)

Variables

View Source
var (
	IncompleteConditionalStatementError = errors.New("IncompleteConditionalStatementError")
	IgnoreError                         = errors.New("ignore")
	BUILT_IN_COMMANDS                   = []string{
		"CMD_NODE",
		"ALIAS",
		"FOR",
		"END-FOR",
		"IF",
		"END-IF",
		"INS",
		"IMAGE",
		"LINK",
		"HTML",
	}
)
View Source
var BufferKeys []string = []string{P_TAG, TR_TAG, TC_TAG}
View Source
var ImageExtensions []string = []string{
	".png",
	".gif",
	".jpg",
	".jpeg",
	".svg",
}

Functions

func BuildXml

func BuildXml(node Node, options XmlOptions, indent string) []byte

func CreateReport

func CreateReport(templatePath string, data *ReportData, options CreateReportOptions) ([]byte, error)

CreateReport generates a report document based on a given template and data. It parses the template file, processes any commands within the template using provided data, and outputs the final document as a byte slice.

Parameters:

  • templatePath: The file path to the template document.
  • data: A pointer to ReportData containing data to be inserted into the template.

Returns:

  • A byte slice representing the generated document.
  • An error if any occurs during template parsing, processing, or document generation.

func ProcessHtmls

func ProcessHtmls(htmls Htmls, documentComponent string, zip *ZipArchive) error

func ProcessImages

func ProcessImages(images Images, documentComponent string, zip *ZipArchive) error
func ProcessLinks(links Links, documentComponent string, zip *ZipArchive) error

func ZipClone

func ZipClone(reader *zip.ReadCloser, writer *zip.Writer, except []string) error

func ZipGetText

func ZipGetText(z *zip.ReadCloser, filename string) (string, error)

func ZipSet

func ZipSet(z *zip.Writer, filename string, data []byte) error

Types

type BaseNode

type BaseNode struct {
	ParentNode Node
	ChildNodes []Node
	NodeName   string
}

func (*BaseNode) AddChild

func (n *BaseNode) AddChild(node Node)

func (*BaseNode) Children

func (n *BaseNode) Children() []Node

func (*BaseNode) Name

func (n *BaseNode) Name() string

func (*BaseNode) Parent

func (n *BaseNode) Parent() Node

func (*BaseNode) PopChild

func (n *BaseNode) PopChild()

func (*BaseNode) SetChildren

func (n *BaseNode) SetChildren(children []Node)

func (*BaseNode) SetName

func (n *BaseNode) SetName(name string)

func (*BaseNode) SetParent

func (n *BaseNode) SetParent(node Node)

type BufferStatus

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

type CommandProcessor

type CommandProcessor func(data *ReportData, node Node, ctx *Context) (string, error)

type Context

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

func NewContext

func NewContext(options CreateReportOptions, imageAndShapeIdIncrement int) Context

type CreateReportOptions

type CreateReportOptions struct {
	CmdDelimiter        *Delimiters
	LiteralXmlDelimiter string
	ProcessLineBreaks   bool
	//noSandbox          bool
	//runJs              RunJSFunc
	//additionalJsContext Object
	FailFast                   bool
	RejectNullish              bool
	ErrorHandler               ErrorHandler
	FixSmartQuotes             bool
	ProcessLineBreaksAsNewText bool
	MaximumWalkingDepth        int
	Functions                  Functions
}

type Delimiters

type Delimiters struct {
	Open  string
	Close string
}

type ErrorHandler

type ErrorHandler = func(err error, rawCode string) string

type Function

type Function func(args ...any) VarValue

type FunctionNotFoundError

type FunctionNotFoundError struct {
	FunctionName string
}

func (*FunctionNotFoundError) Error

func (e *FunctionNotFoundError) Error() string

type Functions

type Functions map[string]Function

type Htmls

type Htmls map[string]string

type Image

type Image struct {
	Extension string // [".png", ".gif", ".jpg", ".jpeg", ".svg"]
	Data      []byte
}

type ImagePars

type ImagePars struct {
	Extension string // [".png", ".gif", ".jpg", ".jpeg", ".svg"]
	Data      []byte
	Width     float32
	Height    float32
	Thumbnail *Thumbnail // optional
	Alt       string     // optional
	Rotation  int        // optional
	Caption   string     // optional
}

type Images

type Images map[string]*Image

type InvalidCommandError

type InvalidCommandError struct {
	Message string
	Command string
}

func NewInvalidCommandError

func NewInvalidCommandError(message, command string) *InvalidCommandError

func (*InvalidCommandError) Error

func (e *InvalidCommandError) Error() string

Implémenter l'interface error pour InvalidCommandError

type KeyNotFoundError

type KeyNotFoundError struct {
	Key string
}

func (*KeyNotFoundError) Error

func (e *KeyNotFoundError) Error() string
type Link struct {
	// contains filtered or unexported fields
}

type LinkPars

type LinkPars struct {
	Url   string
	Label string
}
type Links map[string]Link

type LoopStatus

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

type Node

type Node interface {
	Parent() Node
	SetParent(Node)
	Children() []Node
	SetChildren([]Node)
	PopChild()
	AddChild(Node)
	Name() string
	SetName(string)
}

func AddChild

func AddChild(parent Node, child Node) Node

func CloneNodeWithoutChildren

func CloneNodeWithoutChildren(node Node) Node

CloneNodeWithoutChildren crée une copie d'un noeud sans ses enfants

func ParseXml

func ParseXml(templateXml string) (Node, error)

func PreprocessTemplate

func PreprocessTemplate(root Node, delimiter Delimiters) (Node, error)

type NonTextNode

type NonTextNode struct {
	BaseNode
	Tag   string
	Attrs map[string]string
}

func NewNonTextNode

func NewNonTextNode(tag string, attrs map[string]string, children []Node) *NonTextNode

type ParseTemplateResult

type ParseTemplateResult struct {
	Root         Node
	MainDocument string
	Zip          *ZipArchive
	ContentTypes *NonTextNode
	Extras       map[string]Node // [path]Node
}

func ParseTemplate

func ParseTemplate(zip *ZipArchive) (*ParseTemplateResult, error)

type ReportData

type ReportData map[string]any

func (ReportData) GetArray

func (rd ReportData) GetArray(key string) ([]any, bool)

func (ReportData) GetImage

func (rd ReportData) GetImage(key string) (*ImagePars, bool)

func (ReportData) GetValue

func (rd ReportData) GetValue(key string) (VarValue, bool)

type ReportOutput

type ReportOutput struct {
	Report Node
	Images Images
	Links  Links
	Htmls  Htmls
}

func ProduceReport

func ProduceReport(data *ReportData, template Node, ctx Context) (*ReportOutput, error)

type TextNode

type TextNode struct {
	BaseNode
	Text string
}

func InsertTextSiblingAfter

func InsertTextSiblingAfter(textNode *TextNode) (*TextNode, error)

InsertTextSiblingAfter crée et insère un nouveau noeud texte après le noeud texte donné Retourne le nouveau noeud texte ou une erreur si les conditions ne sont pas remplies

func NewTextNode

func NewTextNode(text string) *TextNode

type Thumbnail

type Thumbnail struct {
	Image
	Width  int
	Height int
}

type VarValue

type VarValue = any

type XmlOptions

type XmlOptions struct {
	LiteralXmlDelimiter string
}

type ZipArchive

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

func NewZipArchive

func NewZipArchive(name string, w io.Writer) (*ZipArchive, error)

func (*ZipArchive) Close

func (za *ZipArchive) Close() error

func (*ZipArchive) GetFile

func (za *ZipArchive) GetFile(name string) ([]byte, error)

func (*ZipArchive) SetFile

func (za *ZipArchive) SetFile(name string, data []byte)

Jump to

Keyboard shortcuts

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