Skip to content

Commit

Permalink
Improve interface (bring back tofu), add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
robfig committed Mar 6, 2014
1 parent 94bb3ee commit 7a1dc39
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 83 deletions.
20 changes: 18 additions & 2 deletions bundle.go
Expand Up @@ -13,17 +13,18 @@ import (
"github.com/robfig/soy/data"
"github.com/robfig/soy/parse"
"github.com/robfig/soy/parsepasses"
"github.com/robfig/soy/soyhtml"
"github.com/robfig/soy/template"
)

// Logger is used to print soy compile error messages when using the
// Logger is used to print notifications and compile errors when using the
// "WatchFiles" feature.
var Logger = log.New(os.Stderr, "[soy] ", 0)

type soyFile struct{ name, content string }

// Bundle is a collection of soy content (templates and globals). It acts as
// input for the soy parser.
// input for the soy compiler.
type Bundle struct {
files []soyFile
globals data.Map
Expand Down Expand Up @@ -68,6 +69,7 @@ func (b *Bundle) AddTemplateDir(root string) *Bundle {
}

// AddTemplateFile adds the given soy template file text to this bundle.
// If WatchFiles is on, it will be subsequently watched for updates.
func (b *Bundle) AddTemplateFile(filename string) *Bundle {
content, err := ioutil.ReadFile(filename)
if err != nil {
Expand All @@ -79,11 +81,16 @@ func (b *Bundle) AddTemplateFile(filename string) *Bundle {
return b.AddTemplateString(filename, string(content))
}

// AddTemplateString adds the given template to the bundle. The name is only
// used for error messages - it does not need to be provided nor does it need to
// be a real filename.
func (b *Bundle) AddTemplateString(filename, soyfile string) *Bundle {
b.files = append(b.files, soyFile{filename, soyfile})
return b
}

// AddGlobalsFile opens and parses the given filename for Soy globals, and adds
// the resulting data map to the bundle.
func (b *Bundle) AddGlobalsFile(filename string) *Bundle {
var f, err = os.Open(filename)
if err != nil {
Expand All @@ -108,6 +115,8 @@ func (b *Bundle) AddGlobalsMap(globals data.Map) *Bundle {
return b
}

// Compile parses all of the soy files in this bundle, verifies a number of
// rules about data references, and returns the completed template registry.
func (b *Bundle) Compile() (*template.Registry, error) {
if b.err != nil {
return nil, b.err
Expand Down Expand Up @@ -137,6 +146,13 @@ func (b *Bundle) Compile() (*template.Registry, error) {
return &registry, nil
}

// CompileToTofu returns a soyhtml.Tofu object that allows you to render soy
// templates to HTML.
func (b *Bundle) CompileToTofu() (*soyhtml.Tofu, error) {
var registry, err = b.Compile()
return soyhtml.NewTofu(registry), err
}

func (b *Bundle) recompiler(reg *template.Registry) {
for {
select {
Expand Down
50 changes: 30 additions & 20 deletions doc.go
Expand Up @@ -17,47 +17,57 @@ your pages. For example:
This code snippet will parse a file of globals, all soy templates within
app/views, and provide back a Tofu intance that can be used to render any
declared template. (Error checking is skipped.)
declared template. Additionally, if "mode == dev", it will watch the soy files
for changes and update your compiled templates in the background (or log compile
errors to soy.Logger). Error checking is omitted.
On startup:
registry, _ := soy.NewBundle().
tofu, _ := soy.NewBundle().
WatchFiles(mode == "dev"). // watch soy files, reload on changes (in dev)
AddGlobalsFile("views/globals.txt"). // parse a file of globals
AddTemplateDir("views"). // load *.soy in all sub-directories
Compile()
CompileToTofu()
To render a page:
var obj = data.Map{
var obj = map[string]interface{}{
"user": user,
"account": account,
}
soyhtml.Renderer{
Registry: registry,
Template: "acme.account.overview",
}.Execute(resp, obj)
tofu.Render(resp, "acme.account.overview", obj)
If you prefer to prepare your data in non-soy-specific data structures ahead of
time, you can easily convert it using soy/data.New():
Structs may be used as the data context too, but keep in mind that they are
converted to data maps -- unlike html/template, the context is pure data, and
you can not call methods on it.
.Execute(resp, data.New(obj))
Advanced Usage
var obj = HomepageContext{
User: user,
Account: account,
}
tofu.Render(resp, "acme.account.overview", obj)
The soy package provides a friendly interface to its sub-packages. Advanced
usages like automated template rewriting will be better served by using
e.g. soy/parse directly.
See soyhtml.StructOptions for knobs to control how your structs get converted to
data maps.
Project Status
This project is in beta. The server-side templating functionality is well
tested and pretty complete. However, the API may still change in
backwards-incompatible ways without notice.
The goal is to be fully compatible and at feature parity with the official
Closure Templates project.
The server-side templating functionality is well tested and pretty complete,
except for two notable areas: contextual autoescaping and
internationalization/bidi support and workflow. Contributions welcome.
The Javascript generation is primitive and lacks support for user functions, but
it successfully passes the server-side template test suite. Note that it is
possible to run the official Soy compiler to generate your javascript templates
at build time, even if you use this package for server-side templates.
Please see the TODO file for features that have yet to be implemented.
Please open a Github Issue for any bugs / problems / comments.
Please open a Github Issue for any bugs / problems / comments, or if you find a
template that renders differently than with the official compiler.
*/
package soy
21 changes: 7 additions & 14 deletions features_test.go
Expand Up @@ -11,8 +11,7 @@ import (
"testing"

"github.com/robertkrimen/otto"
"github.com/robfig/soy/data"
"github.com/robfig/soy/soyhtml"

"github.com/robfig/soy/soyjs"
)

Expand Down Expand Up @@ -199,11 +198,11 @@ func BenchmarkExecuteFeatures(b *testing.B) {
features = mustReadFile("testdata/features.soy")
simple = mustReadFile("testdata/simple.soy")
)
var registry, err = NewBundle().
var tofu, err = NewBundle().
AddGlobalsFile("testdata/FeaturesUsage_globals.txt").
AddTemplateString("", features).
AddTemplateString("", simple).
Compile()
CompileToTofu()
if err != nil {
panic(err)
}
Expand All @@ -216,10 +215,7 @@ func BenchmarkExecuteFeatures(b *testing.B) {
// continue
// }
buf.Reset()
err = soyhtml.Renderer{
Registry: registry,
Template: "soy.examples.features." + test.name,
}.Execute(buf, data.New(test.data).(data.Map))
err = tofu.Render(buf, "soy.examples.features."+test.name, test.data)
if err != nil {
b.Error(err)
}
Expand Down Expand Up @@ -306,11 +302,11 @@ func initJs(t *testing.T) *otto.Otto {

func runFeatureTests(t *testing.T, tests []featureTest) {
var features = mustReadFile("testdata/features.soy")
var reg, err = NewBundle().
var tofu, err = NewBundle().
AddGlobalsFile("testdata/FeaturesUsage_globals.txt").
AddTemplateString("", features).
AddTemplateFile("testdata/simple.soy").
Compile()
CompileToTofu()
if err != nil {
t.Error(err)
return
Expand All @@ -319,10 +315,7 @@ func runFeatureTests(t *testing.T, tests []featureTest) {
b := new(bytes.Buffer)
for _, test := range tests {
b.Reset()
err = soyhtml.Renderer{
Registry: reg,
Template: "soy.examples.features." + test.name,
}.Execute(b, data.New(test.data).(data.Map))
err = tofu.Render(b, "soy.examples.features."+test.name, test.data)
if err != nil {
t.Error(err)
continue
Expand Down
8 changes: 3 additions & 5 deletions soyhtml/exec_test.go
Expand Up @@ -709,11 +709,9 @@ func runNsExecTests(t *testing.T, tests []nsExecTest) {
if test.data != nil {
datamap = data.New(test.data).(data.Map)
}
err := Renderer{
Registry: &registry,
Template: test.templateName,
Inject: ij,
}.Execute(b, datamap)
err := NewTofu(&registry).NewRenderer(test.templateName).
Inject(ij).
Execute(b, datamap)
switch {
case !test.ok && err == nil:
t.Errorf("%s: expected error; got none", test.name)
Expand Down
51 changes: 16 additions & 35 deletions soyhtml/renderer.go
Expand Up @@ -7,32 +7,35 @@ import (

"github.com/robfig/soy/ast"
"github.com/robfig/soy/data"
"github.com/robfig/soy/template"
)

var ErrTemplateNotFound = errors.New("template not found")

// Renderer provides parameters to template execution.
// At minimum, Registry and Template are required to render a template..
type Renderer struct {
Registry *template.Registry // a registry of all templates in a bundle
Template string // fully-qualified name of the template to render
Inject data.Map // data for the $ij map
Funcs map[string]Func // augments default funcs.
Directives map[string]PrintDirective // augments default print directives
tofu *Tofu // a registry of all templates in a bundle
name string // fully-qualified name of the template to render
ij data.Map // data for the $ij map
}

// Inject sets the given data map as the $ij injected data.
func (r *Renderer) Inject(ij data.Map) *Renderer {
r.ij = ij
return r
}

// Execute applies a parsed template to the specified data object,
// and writes the output to wr.
func (t Renderer) Execute(wr io.Writer, obj data.Map) (err error) {
if t.Registry == nil {
if t.tofu == nil || t.tofu.registry == nil {
return errors.New("Template Registry required")
}
if t.Template == "" {
if t.name == "" {
return errors.New("Template name required")
}

var tmpl, ok = t.Registry.Template(t.Template)
var tmpl, ok = t.tofu.registry.Template(t.name)
if !ok {
return ErrTemplateNotFound
}
Expand All @@ -42,38 +45,16 @@ func (t Renderer) Execute(wr io.Writer, obj data.Map) (err error) {
autoescapeMode = ast.AutoescapeOn
}

var funcs = DefaultFuncs
if t.Funcs != nil {
funcs = make(map[string]Func)
for k, v := range DefaultFuncs {
funcs[k] = v
}
for k, v := range t.Funcs {
funcs[k] = v
}
}

var directives = DefaultPrintDirectives
if t.Directives != nil {
directives = make(map[string]PrintDirective)
for k, v := range DefaultPrintDirectives {
directives[k] = v
}
for k, v := range t.Directives {
directives[k] = v
}
}

state := &state{
tmpl: tmpl,
registry: *t.Registry,
registry: *t.tofu.registry,
namespace: tmpl.Namespace.Name,
autoescape: autoescapeMode,
wr: wr,
context: scope{obj},
ij: t.Inject,
funcs: funcs,
directives: directives,
ij: t.ij,
funcs: t.tofu.funcs,
directives: t.tofu.directives,
}
defer state.errRecover(&err)
state.walk(tmpl.Node)
Expand Down
77 changes: 77 additions & 0 deletions soyhtml/tofu.go
@@ -0,0 +1,77 @@
package soyhtml

import (
"fmt"
"io"

"github.com/robfig/soy/data"
"github.com/robfig/soy/template"
)

// Tofu is a bundle of compiled soy, ready to render to HTML.
type Tofu struct {
registry *template.Registry
funcs map[string]Func // functions by name
directives map[string]PrintDirective // print directives by name
}

// NewTofu returns a new instance that is ready to provide HTML rendering
// services for the given templates, with the default functions and print
// directives.
func NewTofu(registry *template.Registry) *Tofu {
return &Tofu{registry, DefaultFuncs, DefaultPrintDirectives}
}

// AddFuncs makes funcs available to the template under the given names.
func (tofu *Tofu) AddFuncs(funcs map[string]Func) *Tofu {
var newfuncs = make(map[string]Func)
for k, v := range tofu.funcs {
newfuncs[k] = v
}
for k, v := range funcs {
newfuncs[k] = v
}
tofu.funcs = newfuncs
return tofu
}

// AddDirectives adds print directives
func (tofu *Tofu) AddDirectives(directives map[string]PrintDirective) *Tofu {
var newdirectives = make(map[string]PrintDirective)
for k, v := range tofu.directives {
newdirectives[k] = v
}
for k, v := range directives {
newdirectives[k] = v
}
tofu.directives = newdirectives
return tofu
}

// Render is a convenience function that executes the soy template of the given
// name, using the given object (converted to data.Map) as context, and writes
// the results to the given Writer.
//
// When converting structs to soy's data format, the DefaultStructOptions are
// used. In particular, note that struct properties are converted to lowerCamel
// by default, since that is the Soy naming convention. The caller may update
// those options to change the behavior of this function.
func (tofu Tofu) Render(wr io.Writer, name string, obj interface{}) error {
var m data.Map
if obj != nil {
var ok bool
m, ok = data.New(obj).(data.Map)
if !ok {
return fmt.Errorf("invalid data type. expected map/struct, got %T", obj)
}
}
return tofu.NewRenderer(name).Execute(wr, m)
}

// NewRenderer returns a new instance of a soy html renderer.
func (tofu *Tofu) NewRenderer(name string) *Renderer {
return &Renderer{
tofu: tofu,
name: name,
}
}

0 comments on commit 7a1dc39

Please sign in to comment.