1
0
mirror of https://github.com/danog/gojekyll.git synced 2025-01-23 00:21:15 +01:00

Factored argument parsing for include tag

This commit is contained in:
Oliver Steele 2017-07-01 09:37:48 -04:00
parent c94f44cefe
commit c554083f0e
3 changed files with 107 additions and 55 deletions

View File

@ -4,73 +4,26 @@ import (
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"github.com/osteele/liquid/chunks"
)
// TODO string escapes
var includeLinePattern = regexp.MustCompile(`^\S+(?:\s+\S+=("[^"]+"|'[^']'|[^'"\s]+))*$`)
var includeParamPattern = regexp.MustCompile(`\b(\S+)=("[^"]+"|'[^']'|[^'"\s]+)(?:\s|$)`)
type includeArgSpec struct {
value string
eval bool
}
type includeSpec struct {
filename string
args map[string]includeArgSpec
}
func parseIncludeArgs(line string) (*includeSpec, error) {
if !includeLinePattern.MatchString(line) {
return nil, fmt.Errorf("parse error in include tag parameters")
}
spec := includeSpec{
strings.Fields(line)[0],
map[string]includeArgSpec{},
}
for _, m := range includeParamPattern.FindAllStringSubmatch(line, -1) {
k, v, eval := m[1], m[2], true
if strings.HasPrefix(v, `'`) || strings.HasPrefix(v, `"`) {
v, eval = v[1:len(v)-1], false
}
spec.args[k] = includeArgSpec{v, eval}
}
return &spec, nil
}
func (spec *includeSpec) eval(ctx chunks.RenderContext) (map[string]interface{}, error) {
include := map[string]interface{}{}
for k, v := range spec.args {
if v.eval {
value, err := ctx.EvaluateString(v.value)
if err != nil {
return nil, err
}
include[k] = value
} else {
include[k] = v.value
}
}
return include, nil
}
func (tc tagContext) includeTag(line string) (func(io.Writer, chunks.RenderContext) error, error) {
spec, err := parseIncludeArgs(line)
func (tc tagContext) includeTag(argsline string) (func(io.Writer, chunks.RenderContext) error, error) {
args, err := ParseArgs(argsline)
if err != nil {
return nil, err
}
if len(args.Args) != 1 {
return nil, fmt.Errorf("parse error")
}
return func(w io.Writer, ctx chunks.RenderContext) error {
params, err := spec.eval(ctx)
include, err := args.EvalOptions(ctx)
if err != nil {
return err
}
filename := filepath.Join(tc.config.Source, tc.config.IncludesDir, spec.filename)
filename := filepath.Join(tc.config.Source, tc.config.IncludesDir, args.Args[0])
ctx2 := ctx.Clone()
ctx2.UpdateBindings(map[string]interface{}{"include": params})
ctx2.UpdateBindings(map[string]interface{}{"include": include})
return ctx2.RenderFile(w, filename)
}, nil

65
tags/parseargs.go Normal file
View File

@ -0,0 +1,65 @@
package tags
import (
"fmt"
"regexp"
"github.com/osteele/liquid/chunks"
)
// TODO string escapes
var argPattern = regexp.MustCompile(`^([^=\s]+)(?:\s+|$)`)
var optionPattern = regexp.MustCompile(`^(\w+)=("[^"]*"|'[^']*'|[^'"\s]*)(?:\s+|$)`)
type parsedArgs struct {
Args []string
Options map[string]optionRecord
}
type optionRecord struct {
value string
quoted bool
}
// ParseArgs parses a tag argument line {% include arg1 arg2 opt=a opt2='b' %}
func ParseArgs(argsline string) (*parsedArgs, error) {
args := parsedArgs{
[]string{},
map[string]optionRecord{},
}
// Ranging over FindAllStringSubmatch would be better golf but got out of hand
// maintenance-wise.
for r, i := argsline, 0; len(r) > 0; r = r[i:] {
am := argPattern.FindStringSubmatch(r)
om := optionPattern.FindStringSubmatch(r)
switch {
case am != nil:
args.Args = append(args.Args, am[1])
i = len(am[0])
case om != nil:
k, v, quoted := om[1], om[2], false
args.Options[k] = optionRecord{v, quoted}
i = len(om[0])
default:
return nil, fmt.Errorf("parse error in tag parameters %q", argsline)
}
}
return &args, nil
}
// EvalOptions evaluates unquoted options.
func (r *parsedArgs) EvalOptions(ctx chunks.RenderContext) (map[string]interface{}, error) {
options := map[string]interface{}{}
for k, v := range r.Options {
if v.quoted {
options[k] = v.value
} else {
value, err := ctx.EvaluateString(v.value)
if err != nil {
return nil, err
}
options[k] = value
}
}
return options, nil
}

34
tags/parseargs_test.go Normal file
View File

@ -0,0 +1,34 @@
package tags
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
var argTests = []struct {
in string
optionCount int
positional []string
}{
{`filename`, 0, []string{"filename"}},
{`filename a=1`, 1, []string{"filename"}},
{`filename a=1 b=2`, 2, []string{"filename"}},
{`filename a='1' b=2`, 2, []string{"filename"}},
{`filename a='1 b=' c`, 2, []string{"filename"}},
{`a=1 b=2`, 2, []string{}},
{`a='1' b=2`, 2, []string{}},
{`arg1 arg2`, 0, []string{"arg1", "arg2"}},
}
func TestFilters(t *testing.T) {
for i, test := range argTests {
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
actual, err := ParseArgs(test.in)
require.NoError(t, err)
require.Equal(t, test.optionCount, len(actual.Options), "options in %q", test.in)
require.Equal(t, test.positional, actual.Args, "args in %q", test.in)
})
}
}