easytemplate

package module
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Mar 28, 2024 License: Apache-2.0 Imports: 14 Imported by: 0

README

easytemplate

made-with-Go Go Report Card GoDoc

easytemplate is Go's text/template with 🦸 super powers 🦸. It is a templating engine that allows you to use Go's text/template syntax, but with the ability to use JavaScript snippets to manipulate data, control templating and run more complex logic while templating.

easytemplate powers Speakeasy's SDK Generation product, which is used by thousands of developers to generate SDKs for their APIs.

The module includes a number of features on top of the standard text/template package, including:

Installation

go get github.com/speakeasy-api/easytemplate

How does it work?

Using goja, easytemplate adds a superset of functionality to Go's text/template package, with minimal dependencies and no bulky external JS runtime.

easytemplate allows you to control templating directly from scripts or other templates which among other things, allows you to:

  • Break templates into smaller, more manageable templates and reuse them, while also including them within one another without the need for your Go code to know about them or control the flow of templating them.
  • Provide templates and scripts at runtime allowing pluggable templating for your application.
  • Separate your templates and scripts from your Go code, allowing you to easily update them without having to recompile your application and keeping concerns separate.

Documentation

See the documentation for more information and instructions on how to use the package.

Basic Usage Example

main.go

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/speakeasy-api/easytemplate"
)

func main() {
    // Create a new easytemplate engine.
    engine := easytemplate.New()
    data := 0
    // Start the engine from a javascript entrypoint.
    err := engine.RunScript("main.js", data)
    if err != nil {
        log.Fatal(err)
    }
}

main.js

// From our main entrypoint, we can render a template file, the last argument is the data to pass to the template.
templateFile("tmpl.stmpl", "out.txt", { name: "John" });

tmpl.stmpl

In the below template we are using the name variable from the data we passed to the template from main.js.

We then also have an embedded JavaScript block that both renders output (the sjs block is replaced in the final output by any rendered text or just removed if nothing is rendered) and sets up additional data available to the template that it then uses to render another template inline.

Hello {{ .Local.name }}!

```sjs
console.log("Hello from JavaScript!"); // Logs message to stdout useful for debugging.

render("This text is rendered from JavaScript!"); 

context.LocalComputed.SomeComputedText = "This text is computed from JavaScript!";
sjs```

{{ templateString "tmpl2.stmpl" .LocalComputed }}

tmpl2.stmpl

And then we are showing some computed text from JavaScript:
{{ .Local.SomeComputedText }}

The rendered file out.txt

Hello John!

This text is rendered from JavaScript!

And then we are showing some computed text from JavaScript:
This text is computed from JavaScript!

Templating

As the templating is based on Go's text/template package, the syntax is exactly the same and can be used mostly as a drop in replacement apart from the differing API to start templating.

Where the engine differs is in the ability to control the flow of templating from within the templates and scripts themselves. This means from a single entry point you can start multiple templates and scripts, and even start templates and scripts from within templates and scripts.

Starting the engine

A number of methods are available to start the engine, including:

  • RunScript(scriptFilePath string, data any) error - Start the engine from a JavaScript file.
    • scriptFilePath (string) - The path to the JavaScript file to start the engine from.
    • data (any) - Context data to provide to templates and scripts. Available as {{.Global}} in templates and context.Global in scripts.
  • RunTemplate(templateFile string, outFile string, data any) error - Start the engine from a template file and write the output to the template to a file.
    • templateFile (string) - The path to the template file to start the engine from.
    • outFile (string) - The path to the output file to write the rendered template to.
    • data (any) - Context data to provide to templates and scripts. Available as {{.Global}} in templates and context.Global in scripts.
  • RunTemplateString(templateFile string, data any) (string, error) - Start the engine from a template file and return the rendered template as a string.
    • templateFile (string) - The path to the template file to start the engine from.
    • data (any) - Context data to provide to templates and scripts. Available as {{.Global}} in templates and context.Global in scripts.
Controlling the flow of templating

The engine allows you to control the flow of templating from within templates and scripts themselves. This means from a single entry point you can start multiple templates and scripts.

This is done by calling the following functions from within templates and scripts:

  • templateFile(templateFile string, outFile string, data any) error - Start a template file and write the output to the template to a file.
    • templateFile (string) - The path to the template file to start the engine from.
    • outFile (string) - The path to the output file to write the rendered template to.
    • data (any) - Context data to provide to templates and scripts. Available as {{.Local}} in templates and context.Local in scripts.
  • templateString(templateFile string, data any) (string, error) - Start a template file and return the rendered template as a string.
    • templateFile (string) - The path to the template file to start the engine from.
    • data (any) - Context data to provide to templates and scripts. Available as {{.Local}} in templates and context.Local in scripts.
  • templateStringInput(templateName string, templateString string, data any) (string, error) - Template the input string and return the rendered template as a string.
    • templateName (string) - The name of the template to render.
    • templateString (string) - An input template string to template.
    • data (any) - Context data to provide to templates and scripts. Available as {{.Local}} in templates and context.Local in scripts.
  • recurse(recursions int) string - Recurse the current template file, recursions is the number of times to recurse the template file.
    • recursions (int) - The number of times to recurse the template file.

This allows for example:

{{ templateFile "tmpl.stmpl" "out.txt" .Local }}{{/* Template another file */}}
{{ templateString "tmpl.stmpl" .Local }}{{/* Template another file and include the rendered output in this templates rendered output */}}
{{ templateStringInput "Hello {{ .Local.name }}" .Local }}{{/* Template a string and include the rendered output in this templates rendered output */}}
Recursive templating

It is possible with the recurse function in a template to render the same template multiple times. This can be useful when data to render parts of the template are only available after you have rendered it at least once.

For example:

{{- recurse 1 -}}
{{"{{.RecursiveComputed.Names}}"}}{{/* Render the names of the customers after we have iterated over them later */}}
{{range .Local.Customers}}
{{- addName .RecursiveComputed.Names (print .FirstName " " .LastName) -}}
{{.FirstName}} {{.LastName}}
{{end}}

Note: The recurse function must be called as the first thing in the template on its own line.

Registering templating functions

The engine allows you to register custom templating functions from Go which can be used within the templates.

engine := easytemplate.New(
  easytemplate.WithTemplateFuncs(map[string]any{
    "hello": func(name string) string {
      return fmt.Sprintf("Hello %s!", name)
    },
  }),
)

Using JavaScript

JavaScript can be used either via inline snippets or by importing scripts from other files.

If using the RunScript method on the engine your entry point will be a JavaScript file where you can setup your environment and start calling template functions.

Alternatively, you can use JavaScript by embedding snippets within your templates using the sjs tag like so:

```sjs
// JS code here
sjs```

The sjs snippet can be used anywhere within your template (including multiple snippets) and will be replaced with any "rendered" output returned when using the render function.

Naive transformation of typescript code is supported through esbuild. This means that you can directly import typescript code and use type annotations in place of any JavaScript. However, be aware:

  • EasyTemplate will not perform type checking itself. Type annotations are transformed into commented out code.
  • Scripts/Snippets are not bundled, but executed as a single module on the global scope. This means no import statements are possible. Instead, the global require function is available to directly execute JS/TS code.
Context data

Context data that is available to the templates is also available to JavaScript. Snippets and Files imported with a template file will have access to the same context data as that template file. For example

```sjs
console.log(context.Global); // The context data provided by the initial call to the templating engine.
console.log(context.Local); // The context data provided to the template file.
sjs```

The context object also contains LocalComputed and GlobalComputed objects that allow you to store computed values that can be later. LocalComputed is only available to the current template file and GlobalComputed is available to all templates and scripts, from the point it was set.

Using the render function

The render function allows the JavaScript snippets to render output into the template file before it is templated allowing for dynamic templating. For example:

{{ .Local.firstName }} ```sjs
if (context.Local.lastName) {
  render("{{ .Local.lastName }}"); // Only add the last name if it is provided.
}

render("Hello from JavaScript!");
sjs```

The above example will render the following output, replacing the sjs block in the template before rendering:

{{ .Local.firstName }} {{ .Local.lastName }}
Hello from JavaScript!

The output from the render function can just be plain text or more templating instructions as shown above.

Registering JS templating functions

In addition from register functions from Go to be used in the templates, you can also register functions from JavaScript. For example:

registerTemplateFunc("hello", function(name) {
  return "Hello " + name + "!";
});

// or
function hello(name) {
  return "Hello " + name + "!";
}

registerTemplateFunc("hello", hello);

The above functions can then be used in the templates like so:

{{ hello "John" }}

Any functions registered will then be available for templates that are templated after the function is registered.

Registering JS functions from Go

You can also register JavaScript functions from Go. For example:

engine := easytemplate.New(
  easytemplate.WithJSFuncs(map[string]func(call easytemplate.CallContext) goja.Value{
    "hello": func(call easytemplate.CallContext) goja.Value {
      name := call.Argument(0).String()
      return call.VM.ToValue("Hello " + name + "!")
    }
  }),
)
Importing JavaScript

JavaScript both inline and in files can import other JavaScript files using the built in require function (Note: this require doesn't work like Node's require and is only used to import other JavaScript files into the global scope):

require("path/to/file.js");

Any functions or variables defined in the imported file will be available in the global scope.

Using Underscore.js

The underscore.js library is included by default and can be used in your JavaScript snippets/code.

_.each([1, 2, 3], console.log);
Importing External Javascript Libraries

Using WithJSFiles you can import external JavaScript libraries into the global scope. For example:

engine := easytemplate.New(
  easytemplate.WithJSFiles("faker.min.js", "<CONTENT OF FILE HERE>"),
)

The imported code will be available in the global scope.

Available Engine functions to JS

The following functions are available to JavaScript from the templating engine:

  • templateFile(templateFilePath, outFilePath, data) - Render a template file to the specified output file path.
    • templateFilePath (string) - The path to the template file to render.
    • outFilePath (string) - The path to the output file to render to.
    • data (object) - Data available to the template as Local context ie {name: "John"} is available as {{ .Local.name }}.
  • templateString(templateString, data) - Render a template and return the rendered output.
    • templateString (string) - The template string to render.
    • data (object) - Data available to the template as Local context ie {name: "John"} is available as {{ .Local.name }}.
  • templateStringInput(templateName, templateString, data) - Render a template and return the rendered output.
    • templateName (string) - The name of the template to render.
    • templateString (string) - The template string to render.
    • data (object) - Data available to the template as Local context ie {name: "John"} is available as {{ .Local.name }}.
  • render(output) - Render the output to the template file, if called multiples times the output will be appended to the previous output as a new line. The cumulative output will replace the current sjs block in the template file.
    • output (string) - The output to render.
  • require(filePath) - Import a JavaScript file into the global scope.
    • filePath (string) - The path to the JavaScript file to import.
  • registerTemplateFunc(name, func) - Register a template function to be used in the template files.
    • name (string) - The name of the function to register.
    • func (function) - The function to register.

Documentation

Overview

Package easytemplate provides a templating engine with super powers, that allows templates to be written in go text/template syntax, with the ability to run javascript snippets in the template, and control further templating from within the javascript or other templates.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrAlreadyInitialized is returned when the engine has already been initialized.
	ErrAlreadyInitialized = errors.New("engine has already been initialized")
	// ErrNotInitialized is returned when the engine has not been initialized.
	ErrNotInitialized = errors.New("engine has not been initialized")
	// ErrReserved is returned when a template or js function is reserved and can't be overridden.
	ErrReserved = errors.New("function is a reserved function and can't be overridden")
	// ErrInvalidArg is returned when an invalid argument is passed to a function.
	ErrInvalidArg = errors.New("invalid argument")
	// ErrTemplateCompilation is returned when a template fails to compile.
	ErrTemplateCompilation = errors.New("template compilation failed")
)

Functions

This section is empty.

Types

type CallContext added in v0.3.0

type CallContext struct {
	goja.FunctionCall
	VM  *vm.VM
	Ctx context.Context //nolint:containedctx // runtime context is necessarily stored in a struct as it jumps from Go to JS.
}

CallContext is the context that is passed to go functions when called from js.

type Engine

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

Engine provides the templating engine.

func New

func New(opts ...Opt) *Engine

New creates a new Engine with the provided options.

func (*Engine) Init added in v0.10.0

func (e *Engine) Init(ctx context.Context, data any) error

Init initializes the engine with global data available to all following methods, and should be called before any other methods are called but only once. When using any of the Run or Template methods after init, they will share the global data, but just be careful they will also share any changes made to the environment by previous runs.

func (*Engine) RunFunction added in v0.10.0

func (e *Engine) RunFunction(ctx context.Context, fnName string, args ...any) (goja.Value, error)

RunFunction will run the named function if it already exists within the environment, for example if it was defined in a script run by RunScript. The provided args will be passed to the function, and the result will be returned.

func (*Engine) RunScript

func (e *Engine) RunScript(ctx context.Context, scriptFile string) error

RunScript runs the provided script file within the environment initialized by Init. This is useful for setting up the environment with global variables and functions, or running code that is not directly related to templating but might setup the environment for templating.

func (*Engine) TemplateFile added in v0.10.0

func (e *Engine) TemplateFile(ctx context.Context, templateFile string, outFile string, data any) error

TemplateFile runs the provided template file, with the provided data and writes the result to the provided outFile.

func (*Engine) TemplateString added in v0.10.0

func (e *Engine) TemplateString(ctx context.Context, templateFilePath string, data any) (string, error)

TemplateString runs the provided template file, with the provided data and returns the rendered result.

func (*Engine) TemplateStringInput added in v0.10.0

func (e *Engine) TemplateStringInput(ctx context.Context, name, template string, data any) (string, error)

TemplateStringInput runs the provided template string, with the provided data and returns the rendered result.

type Opt

type Opt func(*Engine)

Opt is a function that configures the Engine.

func WithDebug added in v0.8.2

func WithDebug() Opt

WithDebug enables debug mode for the engine, which will log additional information when errors occur.

func WithJSFiles added in v0.4.0

func WithJSFiles(files map[string]string) Opt

WithJSFiles allows for providing additional javascript files to be loaded into the engine.

func WithJSFuncs

func WithJSFuncs(funcs map[string]func(call CallContext) goja.Value) Opt

WithJSFuncs allows for providing additional functions available to javascript in the engine.

func WithReadFileSystem

func WithReadFileSystem(fs fs.FS) Opt

WithReadFileSystem sets the file system to use for reading files. This is useful for embedded files or reading from locations other than disk.

func WithSearchLocations added in v0.5.0

func WithSearchLocations(searchLocations []string) Opt

WithSearchLocations allows for providing additional locations to search for templates and scripts.

func WithTemplateFuncs

func WithTemplateFuncs(funcs map[string]any) Opt

WithTemplateFuncs allows for providing additional template functions to the engine, available to all templates.

func WithTracer added in v0.9.0

func WithTracer(t trace.Tracer) Opt

WithTracer attaches an OpenTelemetry tracer to the engine and enables tracing support.

func WithWriteFunc

func WithWriteFunc(writeFunc func(string, []byte) error) Opt

WithWriteFunc sets the write function to use for writing files. This is useful for writing to locations other than disk.

Directories

Path Synopsis
internal
template
Package template contains methods that will template go text/templates files that may contain sjs snippets.
Package template contains methods that will template go text/templates files that may contain sjs snippets.
template/mocks
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.
utils
Package utils contains utility functions.
Package utils contains utility functions.
vm
Package vm provides a wrapper around the goja runtime.
Package vm provides a wrapper around the goja runtime.
pkg
underscore
Package underscore provides the underscore-min.js file.
Package underscore provides the underscore-min.js file.

Jump to

Keyboard shortcuts

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