mirror of
https://github.com/danog/liquid.git
synced 2025-01-22 14:02:34 +01:00
Implement include
This commit is contained in:
parent
f313e6f2cd
commit
fab31d9e8b
@ -52,6 +52,7 @@ This library is in its early days. The API may still change.
|
||||
- [x] loop variables
|
||||
- [ ] `tablerow`
|
||||
- [ ] `cycle`
|
||||
- [x] Include
|
||||
- [x] Raw
|
||||
- [x] Variables
|
||||
- [x] Assign
|
||||
@ -72,10 +73,10 @@ Please refer to the [contribution guidelines](./CONTRIBUTING.md).
|
||||
|
||||
## References
|
||||
|
||||
* [Shopify.github.io/liquid](https://shopify.github.io/liquid) is the definitive reference.
|
||||
* [Shopify.github.io/liquid](https://shopify.github.io/liquid)
|
||||
* [Liquid for Designers](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers)
|
||||
* [Liquid for Programmers](https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers)
|
||||
* [Help.shopify.com](https://help.shopify.com/themes/liquid) goes into more detail, but includes features that aren't present in core Liquid as used by Jekyll.
|
||||
* Shopify's [Liquid for Designers](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) is another take.
|
||||
|
||||
|
||||
## Attribution
|
||||
|
||||
|
@ -28,10 +28,6 @@ func (c *blockDef) CanHaveParent(parent BlockSyntax) bool {
|
||||
return c.parent == p
|
||||
}
|
||||
|
||||
func (c *blockDef) requiresParent() bool {
|
||||
return c.isBranchTag || c.isEndTag
|
||||
}
|
||||
|
||||
func (c *blockDef) IsBlock() bool { return true }
|
||||
func (c *blockDef) IsBlockEnd() bool { return c.isEndTag }
|
||||
func (c *blockDef) IsBlockStart() bool { return !c.isBranchTag && !c.isEndTag }
|
||||
@ -55,6 +51,7 @@ func (s Config) findBlockDef(name string) (*blockDef, bool) {
|
||||
return ct, found
|
||||
}
|
||||
|
||||
// BlockSyntax is part of the Grammar interface.
|
||||
func (s Config) BlockSyntax(name string) (BlockSyntax, bool) {
|
||||
ct, found := s.blockDefs[name]
|
||||
return ct, found
|
||||
|
@ -1,9 +1,11 @@
|
||||
package render
|
||||
|
||||
// Grammar supplies the parser with syntax information about blocks.
|
||||
type Grammar interface {
|
||||
BlockSyntax(string) (BlockSyntax, bool)
|
||||
}
|
||||
|
||||
// BlockSyntax supplies the parser with syntax information about blocks.
|
||||
type BlockSyntax interface {
|
||||
IsBlock() bool
|
||||
CanHaveParent(BlockSyntax) bool
|
||||
@ -15,39 +17,5 @@ type BlockSyntax interface {
|
||||
TagName() string
|
||||
}
|
||||
|
||||
// Grammar returns a configuration's grammar.
|
||||
func (c *Config) Grammar() Grammar { return c }
|
||||
|
||||
// func (c *Config) BlockSyntax(tagName string) BlockSyntax {
|
||||
// s, _ := c.findBlockDef(tagName)
|
||||
// return s
|
||||
// }
|
||||
|
||||
// IsBlockTag(string) bool
|
||||
// func (c *Config) IsBranch(tag string) bool {
|
||||
// // RequiresBalancedChildren(string) bool
|
||||
// }
|
||||
|
||||
// // func (c *Config) RequiresBalancedChildren(tag string) bool {
|
||||
// // return tag != "comment" && tag != "raw"
|
||||
// // }
|
||||
|
||||
// func (c *Config) IsBlockTag(tag string) bool {
|
||||
// return c.findBlockDef(c.Name) != nil
|
||||
// }
|
||||
|
||||
// func (c *Config) CanHaveParent(tag, parent string) bool {
|
||||
// cd := c.findBlockDef(c.Name)
|
||||
// p := c.findBlockDef(parent)
|
||||
// return cd.compatibleParent(ccd)
|
||||
// }
|
||||
|
||||
// func (c *Config) IsBlockStart(tag string) bool {
|
||||
// return c.findBlockDef(tag).cd.isStartTag()
|
||||
// }
|
||||
// func (c *Config) IsBlockEnd(tag string) bool {
|
||||
// return c.findBlockDef(tag).cd.isEndTag
|
||||
// }
|
||||
|
||||
// func (c *Config) IsBranch(tag string) bool {
|
||||
// return c.findBlockDef(tag).isBranchTag
|
||||
// }
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/osteele/liquid/expression"
|
||||
)
|
||||
|
||||
// A ParseError is a parse error during the template parsing.
|
||||
type ParseError string
|
||||
|
||||
func (e ParseError) Error() string { return string(e) }
|
||||
@ -76,7 +77,7 @@ func (s Config) parseChunks(chunks []Chunk) (ASTNode, error) { // nolint: gocycl
|
||||
if sd != nil {
|
||||
suffix = "; immediate parent is " + sd.TagName()
|
||||
}
|
||||
return nil, fmt.Errorf("%s not inside %s%s", c.Name, strings.Join(cs.ParentTags(), " or "), suffix)
|
||||
return nil, parseError("%s not inside %s%s", c.Name, strings.Join(cs.ParentTags(), " or "), suffix)
|
||||
case cs.IsBlockStart():
|
||||
push := func() {
|
||||
stack = append(stack, frame{syntax: sd, node: bn, ap: ap})
|
||||
@ -106,12 +107,12 @@ func (s Config) parseChunks(chunks []Chunk) (ASTNode, error) { // nolint: gocycl
|
||||
}
|
||||
*ap = append(*ap, &ASTFunctional{c, f})
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown tag: %s", c.Name)
|
||||
return nil, parseError("unknown tag: %s", c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if bn != nil {
|
||||
return nil, fmt.Errorf("unterminated %s tag at %s", bn.Name, bn.SourceInfo)
|
||||
return nil, parseError("unterminated %s tag at %s", bn.Name, bn.SourceInfo)
|
||||
}
|
||||
if err := s.evaluateBuilders(root); err != nil {
|
||||
return nil, err
|
||||
|
@ -9,6 +9,16 @@ import (
|
||||
"github.com/osteele/liquid/generics"
|
||||
)
|
||||
|
||||
// A RenderError is an evaluation error during template rendering.
|
||||
type renderError string
|
||||
|
||||
func (e renderError) Error() string { return string(e) }
|
||||
|
||||
// Error creates a render error.
|
||||
func Error(format string, a ...interface{}) renderError {
|
||||
return renderError(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// Render renders the AST rooted at node to the writer.
|
||||
func Render(node ASTNode, w io.Writer, b map[string]interface{}, c Config) error {
|
||||
return renderNode(node, w, newNodeContext(b, c))
|
||||
@ -37,21 +47,21 @@ func renderNode(node ASTNode, w io.Writer, ctx nodeContext) error { // nolint: g
|
||||
case *ASTBlock:
|
||||
cd, ok := ctx.config.findBlockDef(n.Name)
|
||||
if !ok || cd.parser == nil {
|
||||
return fmt.Errorf("unknown tag: %s", n.Name)
|
||||
return parseError("unknown tag: %s", n.Name)
|
||||
}
|
||||
renderer := n.renderer
|
||||
if renderer == nil {
|
||||
panic(fmt.Errorf("unset renderer for %v", n))
|
||||
panic(parseError("unset renderer for %v", n))
|
||||
}
|
||||
return renderer(w, renderContext{ctx, nil, n})
|
||||
case *ASTObject:
|
||||
value, err := ctx.Evaluate(n.expr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s in %s", err, n.Source)
|
||||
return parseError("%s in %s", err, n.Source)
|
||||
}
|
||||
return writeObject(value, w)
|
||||
default:
|
||||
panic(fmt.Errorf("unknown node type %T", node))
|
||||
panic(parseError("unknown node type %T", node))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
31
tags/include.go
Normal file
31
tags/include.go
Normal file
@ -0,0 +1,31 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/osteele/liquid/render"
|
||||
)
|
||||
|
||||
func includeTag(source string) (func(io.Writer, render.Context) error, error) {
|
||||
return func(w io.Writer, ctx render.Context) error {
|
||||
// It might be more efficient to add an context interface to render bytes
|
||||
// to a writer. The status quo keeps the interface light at the expense of some overhead
|
||||
// here.
|
||||
value, err := ctx.EvaluateString(ctx.TagArgs())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, ok := value.(string)
|
||||
if !ok {
|
||||
return render.Error("include requires a string argument; got %v", value)
|
||||
}
|
||||
filename := filepath.Join(filepath.Dir(ctx.SourceFile()), rel)
|
||||
s, err := ctx.RenderFile(filename, map[string]interface{}{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte(s))
|
||||
return err
|
||||
}, nil
|
||||
}
|
32
tags/include_test.go
Normal file
32
tags/include_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/osteele/liquid/render"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var includeTestBindings = map[string]interface{}{}
|
||||
|
||||
func TestIncludeTag(t *testing.T) {
|
||||
config := render.NewConfig()
|
||||
config.Filename = "testdata/include_source.html"
|
||||
AddStandardTags(config)
|
||||
|
||||
ast, err := config.Parse(`{% include "include_target.html" %}`)
|
||||
require.NoError(t, err)
|
||||
buf := new(bytes.Buffer)
|
||||
err = render.Render(ast, buf, includeTestBindings, config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "include target", strings.TrimSpace(buf.String()))
|
||||
|
||||
ast, err = config.Parse(`{% include 10 %}`)
|
||||
require.NoError(t, err)
|
||||
err = render.Render(ast, ioutil.Discard, includeTestBindings, config)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "requires a string")
|
||||
}
|
76
tags/loop_test.go
Normal file
76
tags/loop_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package tags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/osteele/liquid/render"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var loopTests = []struct{ in, expected string }{
|
||||
{`{% for a in array %}{{ a }} {% endfor %}`, "first second third "},
|
||||
|
||||
// loop modifiers
|
||||
{`{% for a in array reversed %}{{ a }}.{% endfor %}`, "third.second.first."},
|
||||
{`{% for a in array limit:2 %}{{ a }}.{% endfor %}`, "first.second."},
|
||||
{`{% for a in array offset:1 %}{{ a }}.{% endfor %}`, "second.third."},
|
||||
{`{% for a in array reversed limit:1 %}{{ a }}.{% endfor %}`, "third."},
|
||||
// TODO investigate how these combine; does it depend on the order
|
||||
// {`{% for a in array reversed offset:1 %}{{ a }}.{% endfor %}`, "second.first."},
|
||||
// {`{% for a in array limit:1 offset:1 %}{{ a }}.{% endfor %}`, "second."},
|
||||
// {`{% for a in array reversed limit:1 offset:1 %}{{ a }}.{% endfor %}`, "second."},
|
||||
|
||||
// loop variables
|
||||
{`{% for a in array %}{{ forloop.first }}.{% endfor %}`, "true.false.false."},
|
||||
{`{% for a in array %}{{ forloop.last }}.{% endfor %}`, "false.false.true."},
|
||||
{`{% for a in array %}{{ forloop.index }}.{% endfor %}`, "1.2.3."},
|
||||
{`{% for a in array %}{{ forloop.index0 }}.{% endfor %}`, "0.1.2."},
|
||||
{`{% for a in array %}{{ forloop.rindex }}.{% endfor %}`, "3.2.1."},
|
||||
{`{% for a in array %}{{ forloop.rindex0 }}.{% endfor %}`, "2.1.0."},
|
||||
{`{% for a in array %}{{ forloop.length }}.{% endfor %}`, "3.3.3."},
|
||||
|
||||
{`{% for i in array %}{{ forloop.index }}[{% for j in array %}{{ forloop.index }}{% endfor %}]{{ forloop.index }}{% endfor %}`,
|
||||
"1[123]12[123]23[123]3"},
|
||||
|
||||
{`{% for a in array reversed %}{{ forloop.first }}.{% endfor %}`, "true.false.false."},
|
||||
{`{% for a in array reversed %}{{ forloop.last }}.{% endfor %}`, "false.false.true."},
|
||||
{`{% for a in array reversed %}{{ forloop.index }}.{% endfor %}`, "1.2.3."},
|
||||
{`{% for a in array reversed %}{{ forloop.rindex }}.{% endfor %}`, "3.2.1."},
|
||||
{`{% for a in array reversed %}{{ forloop.length }}.{% endfor %}`, "3.3.3."},
|
||||
|
||||
{`{% for a in array limit:2 %}{{ forloop.index }}.{% endfor %}`, "1.2."},
|
||||
{`{% for a in array limit:2 %}{{ forloop.rindex }}.{% endfor %}`, "2.1."},
|
||||
{`{% for a in array limit:2 %}{{ forloop.first }}.{% endfor %}`, "true.false."},
|
||||
{`{% for a in array limit:2 %}{{ forloop.last }}.{% endfor %}`, "false.true."},
|
||||
{`{% for a in array limit:2 %}{{ forloop.length }}.{% endfor %}`, "2.2."},
|
||||
|
||||
{`{% for a in array offset:1 %}{{ forloop.index }}.{% endfor %}`, "1.2."},
|
||||
{`{% for a in array offset:1 %}{{ forloop.rindex }}.{% endfor %}`, "2.1."},
|
||||
{`{% for a in array offset:1 %}{{ forloop.first }}.{% endfor %}`, "true.false."},
|
||||
{`{% for a in array offset:1 %}{{ forloop.last }}.{% endfor %}`, "false.true."},
|
||||
{`{% for a in array offset:1 %}{{ forloop.length }}.{% endfor %}`, "2.2."},
|
||||
|
||||
{`{% for a in array %}{% if a == 'second' %}{% break %}{% endif %}{{ a }}{% endfor %}`, "first"},
|
||||
{`{% for a in array %}{% if a == 'second' %}{% continue %}{% endif %}{{ a }}.{% endfor %}`, "first.third."},
|
||||
}
|
||||
|
||||
var loopTestBindings = map[string]interface{}{
|
||||
"array": []string{"first", "second", "third"},
|
||||
}
|
||||
|
||||
func TestLoopTag(t *testing.T) {
|
||||
config := render.NewConfig()
|
||||
AddStandardTags(config)
|
||||
for i, test := range loopTests {
|
||||
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
|
||||
ast, err := config.Parse(test.in)
|
||||
require.NoErrorf(t, err, test.in)
|
||||
buf := new(bytes.Buffer)
|
||||
err = render.Render(ast, buf, loopTestBindings, config)
|
||||
require.NoErrorf(t, err, test.in)
|
||||
require.Equalf(t, test.expected, buf.String(), test.in)
|
||||
})
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
// AddStandardTags defines the standard Liquid tags.
|
||||
func AddStandardTags(c render.Config) {
|
||||
c.AddTag("assign", assignTag)
|
||||
c.AddTag("include", includeTag)
|
||||
|
||||
// blocks
|
||||
// The parser only recognize the comment and raw tags if they've been defined,
|
||||
|
@ -54,58 +54,12 @@ var tagTests = []struct{ in, expected string }{
|
||||
{`{%unless true%}0{%elsif true%}1{%else%}2{%endunless%}`, "1"},
|
||||
{`{%unless true%}0{%elsif false%}1{%else%}2{%endunless%}`, "2"},
|
||||
|
||||
// loops
|
||||
{`{%for a in ar%}{{a}} {%endfor%}`, "first second third "},
|
||||
|
||||
// loop modifiers
|
||||
{`{%for a in ar reversed%}{{a}}.{%endfor%}`, "third.second.first."},
|
||||
{`{%for a in ar limit:2%}{{a}}.{%endfor%}`, "first.second."},
|
||||
{`{%for a in ar offset:1%}{{a}}.{%endfor%}`, "second.third."},
|
||||
{`{%for a in ar reversed limit:1%}{{a}}.{%endfor%}`, "third."},
|
||||
// TODO investigate how these combine; does it depend on the order
|
||||
// {`{%for a in ar reversed offset:1%}{{a}}.{%endfor%}`, "second.first."},
|
||||
// {`{%for a in ar limit:1 offset:1%}{{a}}.{%endfor%}`, "second."},
|
||||
// {`{%for a in ar reversed limit:1 offset:1%}{{a}}.{%endfor%}`, "second."},
|
||||
|
||||
// loop variables
|
||||
{`{%for a in ar%}{{forloop.first}}.{%endfor%}`, "true.false.false."},
|
||||
{`{%for a in ar%}{{forloop.last}}.{%endfor%}`, "false.false.true."},
|
||||
{`{%for a in ar%}{{forloop.index}}.{%endfor%}`, "1.2.3."},
|
||||
{`{%for a in ar%}{{forloop.index0}}.{%endfor%}`, "0.1.2."},
|
||||
{`{%for a in ar%}{{forloop.rindex}}.{%endfor%}`, "3.2.1."},
|
||||
{`{%for a in ar%}{{forloop.rindex0}}.{%endfor%}`, "2.1.0."},
|
||||
{`{%for a in ar%}{{forloop.length}}.{%endfor%}`, "3.3.3."},
|
||||
|
||||
{`{%for i in ar%}{{forloop.index}}[{%for j in ar%}{{forloop.index}}{%endfor%}]{{forloop.index}}{%endfor%}`,
|
||||
"1[123]12[123]23[123]3"},
|
||||
|
||||
{`{%for a in ar reversed%}{{forloop.first}}.{%endfor%}`, "true.false.false."},
|
||||
{`{%for a in ar reversed%}{{forloop.last}}.{%endfor%}`, "false.false.true."},
|
||||
{`{%for a in ar reversed%}{{forloop.index}}.{%endfor%}`, "1.2.3."},
|
||||
{`{%for a in ar reversed%}{{forloop.rindex}}.{%endfor%}`, "3.2.1."},
|
||||
{`{%for a in ar reversed%}{{forloop.length}}.{%endfor%}`, "3.3.3."},
|
||||
|
||||
{`{%for a in ar limit:2%}{{forloop.index}}.{%endfor%}`, "1.2."},
|
||||
{`{%for a in ar limit:2%}{{forloop.rindex}}.{%endfor%}`, "2.1."},
|
||||
{`{%for a in ar limit:2%}{{forloop.first}}.{%endfor%}`, "true.false."},
|
||||
{`{%for a in ar limit:2%}{{forloop.last}}.{%endfor%}`, "false.true."},
|
||||
{`{%for a in ar limit:2%}{{forloop.length}}.{%endfor%}`, "2.2."},
|
||||
|
||||
{`{%for a in ar offset:1%}{{forloop.index}}.{%endfor%}`, "1.2."},
|
||||
{`{%for a in ar offset:1%}{{forloop.rindex}}.{%endfor%}`, "2.1."},
|
||||
{`{%for a in ar offset:1%}{{forloop.first}}.{%endfor%}`, "true.false."},
|
||||
{`{%for a in ar offset:1%}{{forloop.last}}.{%endfor%}`, "false.true."},
|
||||
{`{%for a in ar offset:1%}{{forloop.length}}.{%endfor%}`, "2.2."},
|
||||
|
||||
{`{%for a in ar%}{%if a == 'second'%}{%break%}{%endif%}{{a}}{%endfor%}`, "first"},
|
||||
{`{%for a in ar%}{%if a == 'second'%}{%continue%}{%endif%}{{a}}.{%endfor%}`, "first.third."},
|
||||
|
||||
// TODO test whether this requires matching interior tags
|
||||
{`pre{%raw%}{{a}}{%unknown%}{%endraw%}post`, "pre{{a}}{%unknown%}post"},
|
||||
{`pre{%raw%}{%if false%}anyway-{%endraw%}post`, "pre{%if false%}anyway-post"},
|
||||
}
|
||||
|
||||
var bindings = map[string]interface{}{
|
||||
var tagTestBindings = map[string]interface{}{
|
||||
"x": 123,
|
||||
"obj": map[string]interface{}{
|
||||
"a": 1,
|
||||
@ -126,7 +80,6 @@ var bindings = map[string]interface{}{
|
||||
{"weight": 3},
|
||||
{"weight": nil},
|
||||
},
|
||||
"ar": []string{"first", "second", "third"},
|
||||
"page": map[string]interface{}{
|
||||
"title": "Introduction",
|
||||
},
|
||||
@ -144,7 +97,7 @@ func TestParseErrors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestRender(t *testing.T) {
|
||||
func TestTags(t *testing.T) {
|
||||
config := render.NewConfig()
|
||||
AddStandardTags(config)
|
||||
for i, test := range tagTests {
|
||||
@ -152,7 +105,7 @@ func TestRender(t *testing.T) {
|
||||
ast, err := config.Parse(test.in)
|
||||
require.NoErrorf(t, err, test.in)
|
||||
buf := new(bytes.Buffer)
|
||||
err = render.Render(ast, buf, bindings, config)
|
||||
err = render.Render(ast, buf, tagTestBindings, config)
|
||||
require.NoErrorf(t, err, test.in)
|
||||
require.Equalf(t, test.expected, buf.String(), test.in)
|
||||
})
|
||||
|
1
tags/testdata/include_target.html
vendored
Normal file
1
tags/testdata/include_target.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
include target
|
Loading…
x
Reference in New Issue
Block a user