Template-based docx report creation. (See the blog post).
HEAVILY inspired (aka copy/pasted) from docx-templates 🙏
go get github.com/ArFnds/godocx-template- Write documents naturally using Word, just adding some commands where needed for dynamic contents
- 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-FORcommands, 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).
Contributions are welcome!
$ go get github.com/ArFnds/godocx-template
Here is a simple example, with report data injected directly as an object:
import (
"fmt"
"log/slog"
"os"
"reflect"
"time"
. "github.com/ArFnds/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)
}
}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+++
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 ...}.
Currently supported commands are defined below.
Inserts the result of a given code snippet as follows.
Using code like this:
import (
"fmt"
"log/slog"
"os"
"time"
. "github.com/ArFnds/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.
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>
`+++
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.heightdesired height of the image on the page in cm.data: an ByteArray with the image dataextension: 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",
},
}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+++
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.
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+++ | |
----------------------------------------------------------
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.
This Project is licensed under the MIT License. See LICENSE for more information.