goxic

package module
v0.13.1 Latest Latest
Warning

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

Go to latest
Published: Dec 27, 2024 License: GPL-3.0 Imports: 17 Imported by: 3

README

goxic – A template engine that only has named placeholders, nothing else.

Test Coverage Go Report Card GoDoc

import "git.fractalqb.de/fractalqb/goxic"


Intro

Template engine with templates that only have named placeholders – but no logic!

Package goxic implements the fractal[qb] toxic template engine concept for the Go programming language. For details on the idea behind the toxic template engine see

https://fractalqb.de/toxic/index.html (TL;DR)

Short version is: A Template is nothing but static parts (text) interspersed with placeholders. Placeholders have a unique name and may appear several times within a template. One cannot generate output from a template only. Before output can be generated – aka "emitted" – all placeholders have to be bound to some Content. The bindings are held by a separate object of type BounT (short for "bound template"). This is important to be able to use a single template with different bindings. Note: A bound template itself is Content. – Be aware of infinite recursion! By the way, a function that writes output can be content too. This has quite some implications.

One can build a template through the Template API of the goxic packed. Though quite easy it is not very convenient to build templates this way. It is considered to be more common to build a template by parsing it from a file. The package provides a quite general Parser that also supports nesting of templates. Finally one can create new templates from bound templates where not every placeholder has been bound to content, i.e. to Fixate a BounT. This way one can build coarse grained templates from smaller ones without loosing efficiency.

The concepts described so far put a lot of control into the hands of the programmer. But there are things that might also be helpful, when controlled by the template writer:

  1. Escaping: This is a common problem with e.g. HTML templates where injection of content can result in security breaches.

  2. Selecting and Formatting Content: This is addressed in goxic with the Bind From Template (BFT) feature.

Both features can be used but don't need to – goxic has a layered design. What goxic still does ban from templates are control structures like conditionals and loops. Such things tend to be better be placed into code of a real programming language.

Escaping Content

This is a common problem with e.g. HTML templates where injection of content can result in security breaches. Here the situation is twofold: It depends on the position of the placeholder in the template whether or how content has to be escaped. This cannot be judged from the programmer's view. Sometimes ToDo…

Bind From Template

ToDo…

Documentation

Overview

Package goxic implements the fractal[qb] toxic template engine concept for the Go programming language. For details on the idea behind the toxic template engine see: https://fractalqb.de/toxic/index.html

Designing good programming languages is hard, even for DSLs. As a developer, I also want to minimise the number of DSLs that I have to master. For this reason alone, it makes sense to control the logic of a template from the host programming language. In addition, the host language usually provides more sophisticated tool support – in the language itself, in the IDE and in the tests.

Example
package main

import (
	"io"
	"os"
)

/* NO ERROR HANDLING FOR BREVITY */

// The template to be parsed – often one parses templates from a files
const template = `<!DOCTYPE html>
<html lang="{lang}">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>goxic Example</title>
  </head>
  <body>
    <h1>Personen</h1>
    <table>
<!-- \ >>> rows <<< \ -->
<!-- >>> table-row >>> -->
      <tr>
        <td>{given-name}</td>
        <td>{familyname}</td>
      </tr>
<!-- \ <<< table-row <<< -->
    </table>
  </body>
</html>`

// Index maps for efficient field access for table-rows
var imapRow struct {
	*Template
	GivenName  Fields `goxic:"given-name"`
	Familyname Fields `goxic:"familyname"`
}

// Some data to be used in the template
type person struct {
	givenName  string
	familyname string
}

var persons = []person{
	{"Liese", "Müller"},
	{"Max", "Mustermann"},
}

func main() {
	// Cerate a simple parser that can handle HTML templates (insecure, no escaping)
	parser := Parser{
		CommentLine: NewBlockComment("<!--", "-->").Line,
		InlineStart: '{', // Repeat runes to put them
		InlineEnd:   '}', // into normal template text.
	}

	// Parse the template; root template gets map key ""
	tmpl, _ := parser.ParseString(template)
	// Initialise index map for table-rows
	MapIndices(&imapRow, tmpl["table-row"], MapAll)

	// Bind root template fields without index map
	rootBind := tmpl[""].NewBindings(nil) // Bindings for root template
	rootBind.BindName("lang", Str("de"))

	rowBind := imapRow.NewBindings(nil) // Bindings for row template (reused)
	// Bind a function that generates all rows
	rootBind.BindName("rows", Func(func(w io.Writer) (n int64, err error) {
		for _, p := range persons {
			imapRow.GivenName.Bind(rowBind, Str(p.givenName))
			imapRow.Familyname.Bind(rowBind, Str(p.familyname))
			m, err := rowBind.WriteTo(w)
			rowBind.Reset()
			n += m
			if err != nil {
				return n, err
			}
		}
		return n, nil
	}))

	// Write template with bindings to stdout
	rootBind.WriteTo(os.Stdout)

}
Output:

<!DOCTYPE html>
<html lang="de">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>goxic Example</title>
  </head>
  <body>
    <h1>Personen</h1>
    <table>
      <tr>
        <td>Liese</td>
        <td>Müller</td>
      </tr>
      <tr>
        <td>Max</td>
        <td>Mustermann</td>
      </tr>
    </table>
  </body>
</html>

Index

Examples

Constants

View Source
const (
	MapAllNamedFields MapMode = (1 << iota)
	MapAllAnonFields
	MapAllIndices

	MapFields   = MapAllNamedFields | MapAllAnonFields
	MapAll      = MapAllNamedFields | MapAllIndices
	MapComplete = MapAll | MapAllAnonFields
)
View Source
const NamePath = Separator(".")

Variables

View Source
var Empty empty

Functions

func BindByName added in v0.13.1

func BindByName(b *Bindings, nmap []Named, content ByNamer)

func Field added in v0.13.0

func Field(name string, esc ...Escaper) appendField

Field is used with Template.Append to build templates.

func Fmt added in v0.13.0

func Fmt(format string, args ...any) io.WriterTo

func MapIndices added in v0.13.0

func MapIndices(m any, t *Template, mode MapMode) error

MapIndices fills the index map m from the Template t. An index map is a struct with an anonymous embedded Template pointer and exported fields of type Fields.

Example
// Explicitly build template, parsing is simpler
var htmlTmpl Template
htmlTmpl.AppendFixString("<html>\n")
htmlTmpl.AppendFixString("  <title>")
htmlTmpl.AppendField("title", nil) // !No escaper
htmlTmpl.AppendFixString("</title>\n  <body>\n    <h1>")
htmlTmpl.AppendField("title", nil) // !No escaper
htmlTmpl.AppendFixString("</h1>\n")
htmlTmpl.AppendField("Body", nil) // !No escaper
htmlTmpl.AppendFixString("\n  </body>\n</html>\n")

// The index map:
var htmlMap struct {
	*Template        // Embed the template pointer
	Title     Fields `goxic:"title"` // Set template field name
	Body      Named  // Template field name is the same: "Body"
	Ignore    Fields `goxic:"-"` // Exclude from index mapping
}
// Fill htmlMap from template
MapIndices(&htmlMap, &htmlTmpl, MapAll)

b := htmlMap.NewBindings(nil)                   // Use the embedded template pointer
htmlMap.Title.Bind(b, Str("Index Map Example")) // Syntactic sugar b.Binds(Str("…"), htmlMap.Title...)
htmlMap.Body.Bind(b, Str(`<script>alert("Use Escapers to avoid injection attacks!")</script>`))
b.WriteTo(os.Stdout)
Output:

<html>
  <title>Index Map Example</title>
  <body>
    <h1>Index Map Example</h1>
<script>alert("Use Escapers to avoid injection attacks!")</script>
  </body>
</html>

func MultiMapIndices added in v0.13.0

func MultiMapIndices(ts map[string]*Template, mode MapMode, idxMaps map[string]any) error

func NamesFields added in v0.13.1

func NamesFields(t *Template, anon bool) map[string]Named

NamesIndices returns a map of all distinct field names along with their respective indices.

Template t must not be changed afterwards.

func NamesIndices added in v0.13.1

func NamesIndices(t *Template, anon bool) map[string]Fields

NamesIndices returns a map of all distinct field names along with their respective indices.

Template t must not be changed afterwards.

func Print

func Print(v any) io.WriterTo

func Seq added in v0.13.0

func Seq[S iter.Seq[E], E any](seq S, b *Bindings, bind func(int, E, *Bindings)) io.WriterTo

func Slice added in v0.13.0

func Slice[S ~[]E, E any](es S, b *Bindings, bind func(int, E, *Bindings)) io.WriterTo

func Time added in v0.13.0

func Time(t time.Time, layout string) io.WriterTo

func ToStr added in v0.13.0

func ToStr(s fmt.Stringer) io.WriterTo

Types

type Bindings added in v0.13.0

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

TODO: It is quite common to bind the same content to more than one field when filling in templates. Therefore, fields can be given names and then content can be bound to all fields with the same name.

Example
var t Template
t.SetFix(0, []byte("FIX"))
t.SetField(0, "foo", nil)
b := t.NewBindings(nil)
b.Bind(0, Str("bar"))
b.Bind(1, Print(4711))
b.WriteTo(os.Stdout)
Output:

barFIX4711

func NewBindings added in v0.13.0

func NewBindings(init []io.WriterTo) *Bindings

func (*Bindings) Bind added in v0.13.0

func (b *Bindings) Bind(field int, content io.WriterTo) error

binding (*Bindings)(nil) is same as binding nil -> Template.NewBindings

func (*Bindings) BindAll added in v0.13.0

func (b *Bindings) BindAll(content io.WriterTo)

func (*Bindings) BindName added in v0.13.0

func (b *Bindings) BindName(name string, content io.WriterTo) error

BindName is a convenience method that collects the field indices of name and then binds all fields to content. When repeatedly binding to the same template field, it is more efficient to use Template.Fields once and then use Fields.Bind or Bindings.Binds repeatedly. MapIndices helps to manage templates and their indices in a structured way with so-called index maps.

func (*Bindings) BindOrName added in v0.13.0

func (b *Bindings) BindOrName(field int, content io.WriterTo) error

BindOrName binds field i to the name of the field if w is nil. Otherwiese it binds w to the field. For an example see Fields.BindOrName.

func (*Bindings) Binding added in v0.13.0

func (bds *Bindings) Binding(field int) io.WriterTo

func (*Bindings) Binds added in v0.13.0

func (b *Bindings) Binds(content io.WriterTo, fields ...int) error

func (*Bindings) Fixate added in v0.13.0

func (bds *Bindings) Fixate(keepAnon bool, name func(path, name string) string) (*Template, error)
Example
t0 := must.Ret(NewTemplate(
	"x0.0", Field("n0.0"), "x0.1", Field("n0.1", embraceString("[", "]")), "x0.2",
))
t1 := must.Ret(NewTemplate(
	"x1.0", Field("n1.0", embraceString("(", ")")), "x1.1", Field("n1.1"), "x1.2",
))

b1 := t1.NewBindings(nil)
must.Do(b1.BindName("n1.1", Str("S1.1")))

b0 := t0.NewBindings(nil)
must.Do(b0.BindName("n0.0", Str("S0.0")))
must.Do(b0.BindName("n0.1", b1))

tf := must.Ret(b0.Fixate(false, nil))
bf := tf.NewBindings(nil)
must.Do(bf.BindName("n1.0", Str("S1.0")))

bf.WriteTo(os.Stdout)
Output:

x0.0S0.0x0.1[x1.0][(S1.0)][x1.1][S1.1][x1.2]x0.2

func (*Bindings) NumField added in v0.13.0

func (b *Bindings) NumField() int

func (*Bindings) Reset added in v0.13.0

func (bds *Bindings) Reset() *Bindings

func (*Bindings) String added in v0.13.0

func (bds *Bindings) String() string

func (*Bindings) Template added in v0.13.0

func (b *Bindings) Template() *Template

func (*Bindings) WriteTo added in v0.13.0

func (bds *Bindings) WriteTo(w io.Writer) (n int64, err error)

type BlockComment added in v0.13.0

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

func NewBlockComment added in v0.13.0

func NewBlockComment(start, end string) BlockComment

func (BlockComment) Line added in v0.13.0

func (lb BlockComment) Line(line []byte) []byte

type ByNameFunc added in v0.13.1

type ByNameFunc func(name string) io.WriterTo

func (ByNameFunc) GoxicByName added in v0.13.1

func (f ByNameFunc) GoxicByName(name string) io.WriterTo

type ByNamer added in v0.13.1

type ByNamer interface{ GoxicByName(name string) io.WriterTo }

type Bytes added in v0.13.0

type Bytes []byte

func (Bytes) WriteTo added in v0.13.0

func (b Bytes) WriteTo(w io.Writer) (int64, error)

type EscapeChain added in v0.13.0

type EscapeChain []Escaper // TODO better encapsulation

func (EscapeChain) Escape added in v0.13.0

func (ec EscapeChain) Escape(w io.Writer) EscapeWriter

type EscapeFunc added in v0.13.0

type EscapeFunc func(io.Writer) EscapeWriter

func (EscapeFunc) Escape added in v0.13.0

func (ef EscapeFunc) Escape(w io.Writer) EscapeWriter

type EscapeWriter added in v0.13.0

type EscapeWriter interface {
	io.Writer
	Close() (int64, error)
}

type Escaper added in v0.13.0

type Escaper interface {
	Escape(io.Writer) EscapeWriter
}

func ChainEscape added in v0.13.0

func ChainEscape(es ...Escaper) Escaper

type Fields added in v0.13.1

type Fields []int

func NameIndices added in v0.13.1

func NameIndices(t *Template, name string) (idxs Fields)

Template t must not be changed afterwards.

func (Fields) Bind added in v0.13.1

func (is Fields) Bind(b *Bindings, content io.WriterTo) error

func (Fields) BindOrName added in v0.13.1

func (is Fields) BindOrName(b *Bindings, content io.WriterTo) error

BindOrName binds fields with their names if content is nil. Otherwise it binds w to the fields. See also Bindings.BindOrName.

Example
const fieldName = "Einfach eine Überschrift"
tmpl := must.Ret(NewTemplate("<h1>", Field(fieldName), "</h1>"))
idxs := NameIndices(tmpl, fieldName)

b := tmpl.NewBindings(nil)
fmt.Println("Empty:", b)
idxs.BindOrName(b, nil)
fmt.Println("Name :", b)
idxs.BindOrName(b, Str("Yet Another Heading"))
fmt.Println("Or   :", b)
Output:

Empty: <h1></h1>
Name : <h1>Einfach eine Überschrift</h1>
Or   : <h1>Yet Another Heading</h1>

type Func added in v0.13.0

type Func func(io.Writer) (int64, error)

func (Func) WriteTo added in v0.13.0

func (vf Func) WriteTo(w io.Writer) (int64, error)

type Int added in v0.13.0

type Int int64

func (Int) WriteTo added in v0.13.0

func (i Int) WriteTo(w io.Writer) (int64, error)

type LineComment added in v0.13.0

type LineComment string

func (LineComment) Line added in v0.13.0

func (le LineComment) Line(line []byte) []byte

type MapMode added in v0.13.0

type MapMode uint

type Named added in v0.13.1

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

func NameFields added in v0.13.1

func NameFields(t *Template, name string) (fs Named)

Template t must not be changed afterwards.

func (Named) Bind added in v0.13.1

func (f Named) Bind(b *Bindings, content io.WriterTo) error

func (Named) BindByName added in v0.13.1

func (f Named) BindByName(b *Bindings, content ByNamer) error
Example
tmpl := must.Ret(NewTemplate(
	"User ID : ", Field("ID"), "\n",
	"Username: ", Field("Name"), "\n",
))
var idxmap struct {
	*Template
	ID   Named
	Name Named
}
must.Do(MapIndices(&idxmap, tmpl, MapAll))

tbn := testByNamer{
	ID:   4711,
	Name: "John Doe",
}

b := idxmap.NewBindings(nil)
idxmap.ID.BindByName(b, &tbn)
idxmap.Name.BindByName(b, &tbn)
b.WriteTo(os.Stdout)
Output:

User ID : 4711
Username: John Doe

func (Named) BindOrName added in v0.13.1

func (f Named) BindOrName(b *Bindings, content io.WriterTo) error

BindOrName binds fields with their names if content is nil. Otherwise it binds w to the fields. See also Bindings.BindOrName.

func (Named) Name added in v0.13.1

func (f Named) Name() string

TODO Panics if Named.Template returns nil?

func (Named) Template added in v0.13.1

func (f Named) Template() *Template

type Parser

type Parser struct {
	CommentLine            func(line []byte) []byte
	InlineStart, InlineEnd rune
	FieldName              func(string) error
	Escape                 map[string]Escaper
	PrepLine               func([]byte) []byte
	Newline                string
	// contains filtered or unexported fields
}

func (*Parser) Parse

func (p *Parser) Parse(r io.Reader) (tmpls map[string]*Template, err error)

func (*Parser) ParseFS added in v0.13.0

func (p *Parser) ParseFS(fs fs.FS, name string) (tmpls map[string]*Template, err error)

func (*Parser) ParseFile

func (p *Parser) ParseFile(name string) (tmpls map[string]*Template, err error)

func (*Parser) ParseString added in v0.9.0

func (p *Parser) ParseString(s string) (tmpls map[string]*Template, err error)

type Separator added in v0.13.0

type Separator string

func (Separator) Join added in v0.13.0

func (sep Separator) Join(path, name string) string

type Str added in v0.13.0

type Str string

func (Str) WriteTo added in v0.13.0

func (s Str) WriteTo(w io.Writer) (int64, error)

type Template

type Template struct {
	ValidName func(string) error
	// contains filtered or unexported fields
}

A template is a sequence of fixed data, preceded, interspersed and followed by fields that can be dynamically filled with content. In the simplest case, fields and fixed data are addressed by indices. Field 0 is before fix data 0 and field 1 follows fix data 0 and is before fix data 1 and so on. The last part is field N for a template with N fixed data parts:

[ Field 0 | Fix 0 | Field 1 | Fix 1 | … | Fix N-1 | Field N ]

It is perfectly possible to define templates using its methods. However, it is far more convenient to read in text templates from a file using the Parser.

To fill in fields, you must first create a Bindings object for the template with Template.NewBindings. This can then be used to bind content to fields. After binding, the complete content is written to an io.Writer using Bindings.WriteTo.

A zero Template is ready for use. It must not be copied or changed after creating the first Bindings for the template.

func NewTemplate

func NewTemplate(ff ...any) (*Template, error)

func (*Template) Append added in v0.13.0

func (t *Template) Append(ff ...any) error

func (*Template) AppendField added in v0.13.0

func (t *Template) AppendField(name string, esc Escaper) error

AppendField sets the next field that follows either another field or a fixed part, whichever is last.

func (*Template) AppendFix added in v0.13.0

func (t *Template) AppendFix(fix []byte) error

AppendFix sets the next fixed part that follows either another fixed part or a field, whichever is last. It may assume ownership of the memory held by fix.

func (*Template) AppendFixString added in v0.13.0

func (t *Template) AppendFixString(fix string) error

func (*Template) Field added in v0.13.0

func (t *Template) Field(field int) (string, Escaper)

func (*Template) Fields added in v0.13.0

func (t *Template) Fields(name string) iter.Seq[int]

Fields returns the field indices of all fields with the given name.

func (*Template) Fix added in v0.13.0

func (t *Template) Fix(i int) []byte

func (*Template) ListNames added in v0.13.0

func (t *Template) ListNames(anon bool) (ls []string)

ListNames returns a sorted list of field names.

func (*Template) MustStatic added in v0.13.0

func (t *Template) MustStatic() Bytes

func (*Template) Names added in v0.13.0

func (t *Template) Names() map[string]int

func (*Template) NewBindings added in v0.13.0

func (t *Template) NewBindings(reuse *Bindings) *Bindings

TODO returns reuse unchangeds if t is nil -> tmpls["no-exists"].NewBindins()

func (*Template) NumFields added in v0.13.0

func (t *Template) NumFields() int

func (*Template) NumFix added in v0.13.0

func (t *Template) NumFix() int

func (*Template) Pack

func (t *Template) Pack()

func (*Template) SetField added in v0.13.0

func (t *Template) SetField(field int, name string, esc Escaper) error

func (*Template) SetFix added in v0.13.0

func (t *Template) SetFix(i int, fix []byte) error

SetFix sets the fix part with index i to fix. The template assumes ownership of the memory held by fix. If the caller wants to keep ownership e.g. use bytes.Clone to pass a copy.

func (*Template) SetFixString added in v0.13.0

func (t *Template) SetFixString(i int, fix string) error

func (*Template) Static

func (t *Template) Static() []byte

type Uint added in v0.13.0

type Uint uint64

func (Uint) WriteTo added in v0.13.0

func (i Uint) WriteTo(w io.Writer) (int64, error)

type UnknownField added in v0.13.0

type UnknownField string

func (UnknownField) Error added in v0.13.0

func (err UnknownField) Error() string

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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