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:
parent
c94f44cefe
commit
c554083f0e
@ -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
65
tags/parseargs.go
Normal 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
34
tags/parseargs_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user