diff --git a/README.md b/README.md index e953a17..36b992b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # FET -FET is a go template engineer that can translate code to `html/template` code. +FET is a go template engineer that can translate code to `html/template` code. ## Why FET + FET means Friendly, Easily for Templating.`html/template` has a basic support for templating, but it's not easy to use, so you need FET. + - Expression support - Use `incldue` with defined variables scopes - Use `extends` inherit base template with defined variables scopes - Extends support for `for` loop, e.g ## Document - -[Document](https://github.com/fefit/fet/wiki/Wiki) + +[Document](https://github.com/fefit/fet/wiki/Wiki) [中文文档](https://github.com/fefit/fet/wiki/%E4%B8%AD%E6%96%87%E6%96%87%E6%A1%A3) @@ -26,7 +28,7 @@ it's more like the php template engineer smarty. ``` - blocks for inherit - + ```php {%block "header"%}
some code here
@@ -40,13 +42,13 @@ it's more like the php template engineer smarty. ``` - loop, do not support keyword `break` `continue` - + ```php // for Gofet mode {%for item,key in list%} // output {%/for%} - + {%for i = 0, j = 10; i < j; i++%} // output {%/for%} @@ -61,19 +63,19 @@ it's more like the php template engineer smarty. ``` - if condition - + ```php {%if $num > 100%} - + {%elseif $num < 50%} - + {%else%} - + {%/if%} ``` - output - + ```php {%$item.url%} ``` @@ -85,20 +87,20 @@ it's more like the php template engineer smarty. ``` - variable define - + ```php {%$title = "this is a title"%} ``` -- capture - +- capture + ```php - {%capture "hello"%} - {%/capture%} + {%capture "hello"%} + {%/capture%} {%$fet.capture.hello%} ``` -- static variables +- static variables ```php {%$fet.capture.xxx%} @@ -107,80 +109,84 @@ it's more like the php template engineer smarty. {%$fet.config.templateDir%} {%$fet.config.compileDir%} {%$fet.now%} + {%$fet.debug%} // will output all the variables that assigned in the template to the js devtools's Console panel. ``` -- special variables +- special variables ```php {%$ROOT%} // will output $ {%$fet%} // will output . ``` ### Expression - -1. operators - `+ - * / % ! ** == >= <= != && || & ^` + +1. operators + `+ - * / % ! ** == >= <= != && || & ^` 2. keyword operators - `and` && `or` || `not` ! `eq` == `ne` != `gt` > `ge` >= `lt` < `le` <= - `bitor` for "|" + `and` && `or` || `not` ! `eq` == `ne` != `gt` > `ge` >= `lt` < `le` <= + `bitor` for "|" -3. pipe - `|` pipeline funcs - `:` set arguments for pipeline funcs +3. pipe + `|` pipeline funcs + `:` set arguments for pipeline funcs -4. numbers - hex: `0xffff` - octal: `0o777` - binary: `0b1000` - scientific notation `1e10` +4. numbers + hex: `0xffff` + octal: `0o777` + binary: `0b1000` + scientific notation `1e10` -Be careful of the `and` and `or` operators, they don't have short circuit with conditions. +Be careful of the `and` and `or` operators, they don't have short circuit with conditions. -### Characters concat - ```php - {% $sayHello = "world" %} - {% "hello `$sayHello`"%} // output "hello world" - ``` - use ` `` ` for variable or expression in strings. do not use `+`. - - -### Func Maps - - Math - `min` `max` `floor` `ceil` - - - Helpers - `number_format` `truncate` - - - Assert - `empty` - - - Length - `count` - - - Output - `safe` - - [view more in funcs.go](./lib/funcs/funcs.go) - -### Config types.Mode - - types.Smarty - the variable and field must begin with `$`, use `foreach` tag for loops. - - - - types.Gofet - the variable and field mustn't begin with `$`, use `for` tag for loops. +### Characters concat + +```php +{% $sayHello = "world" %} +{% "hello `$sayHello`"%} // output "hello world" +``` + +use ` `` ` for variable or expression in strings. do not use `+`. + +### Func Maps + +- Math + `min` `max` `floor` `ceil` + +- Helpers + `number_format` `truncate` + +- Assert + `empty` + +- Length + `count` + +- Output + `safe` +- [view more in funcs.go](./lib/funcs/funcs.go) + +### Config types.Mode + +- types.Smarty + the variable and field must begin with `$`, use `foreach` tag for loops. + +* types.Gofet + the variable and field mustn't begin with `$`, use `for` tag for loops. ### In development - ```bash - # install command line tool `fetc` - go get -v github.com/fefit/fetc - # then init the config, will make a config file `fet.config.json` - fetc init - # then watch the file change, compile your fet template file immediately - fetc watch - ``` +```bash +# install command line tool `fetc` +go get -v github.com/fefit/fetc +# then init the config, will make a config file `fet.config.json` +fetc init +# then watch the file change, compile your fet template file immediately +fetc watch +``` ### Demo code + ```go package main @@ -198,7 +204,7 @@ func main(){ CompileDir: "views", // default "templates_c", Ignores: []string{"inc/*"}, // ignore compile,paths and files that will be included and extended. use filepath.Match() method. UcaseField: false, // default false, if true will auto uppercase field name to uppercase. - CompileOnline: false, // default false, you should compile your template files offline + CompileOnline: false, // default false, you should compile your template files offline Glob: false, // default false, if true, will add {{define "xxx"}}{{end}} to wrap the compiled content,"xxx" is the relative pathname base on your templateDir, without the file extname. AutoRoot: false, // default false,if true, if the variable is not assign in the scope, will treat it as the root field of template data, otherwise you need use '$ROOT' to index the data field. Mode: types.Smarty, // default types.Smarty, also can be "types.Gofet" @@ -215,49 +221,47 @@ func main(){ } ``` -### API + +### API #### static methods -- `fet.LoadConf(configFile string) (*types.FetConfig, error)` - - if you use the command line `fetc` build a config file `fet.config.json`, then you can use `fet.LoadConf` to get the config. +- `fet.LoadConf(configFile string) (*types.FetConfig, error)` + + if you use the command line `fetc` build a config file `fet.config.json`, then you can use `fet.LoadConf` to get the config. + +- `fet.New(config *types.FetConfig) (instance *Fet, error)` -- `fet.New(config *types.FetConfig) (instance *Fet, error)` - get a fet instance. #### instance methods -- `instance.Compile(tpl string, createFile bool) (result string, err error) ` - - compile a template file, if `createFile` is true, will create the compiled file. +- `instance.Compile(tpl string, createFile bool) (result string, err error)` -- `instance.CompileAll() error` - - compile all files need to compile. + compile a template file, if `createFile` is true, will create the compiled file. +- `instance.CompileAll() error` -- `instance.Display(tpl string, data interface{}, output io.Wirter) error` + compile all files need to compile. - render the parsed html code into `output`. +* `instance.Display(tpl string, data interface{}, output io.Wirter) error` -- `instance.Fetch(tpl string, data interface{}) (result string, err error)` + render the parsed html code into `output`. - just get the parsed `string` code, it always use `CompileOnline` mode. +* `instance.Fetch(tpl string, data interface{}) (result string, err error)` + just get the parsed `string` code, it always use `CompileOnline` mode. ## Use in project -1. `compile mode` - +1. `compile mode` + just use fet compile your template files offline, and add the FuncMap `lib/funcs/funcs.go` to your project. -2. `install mode` +2. `install mode` install `fet`,and use `fet.Display(tpl, data, io.Writer)` to render the template file. - ## License [MIT License](./LICENSE). diff --git a/fet.go b/fet.go index 4adcec2..2e4ac5d 100644 --- a/fet.go +++ b/fet.go @@ -379,6 +379,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { addVarPrefix := "$" isSmartyMode := conf.Mode == types.Smarty compiledText := "" + noDelimit := false if isSmartyMode { addVarPrefix = "" } @@ -424,15 +425,19 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if contains(currentScopes, name) { symbol = " = " } - if compiledText, err = gen.Build(ast, genOptions, parseOptions); err != nil { + if compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions); err != nil { return "", node.halt("compile error:%s", err.Error()) } result = delimit(addVarPrefix + name + localNS + symbol + compiledText) } else { - if compiledText, err = gen.Build(ast, genOptions, parseOptions); err != nil { + if compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions); err != nil { return "", node.halt("compile error:%s", err.Error()) } - result = delimit(compiledText) + if noDelimit { + result = compiledText + } else { + result = delimit(compiledText) + } } case SingleType: isInclude := name == "include" @@ -474,7 +479,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if key != defField { value := prop.Raw if ast, expErr := exp.Parse(value); expErr == nil { - if compiledText, err = gen.Build(ast, genOptions, parseOptions); err != nil { + if compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions); err != nil { return "", node.halt("compile error:%s", err.Error()) } incLocalScopes = append(incLocalScopes, "$"+key) @@ -503,7 +508,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if expErr != nil { return "", toError(expErr) } - compiledText, err = gen.Build(ast, genOptions, parseOptions) + compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions) if err != nil { return "", node.halt("parse 'foreach' error:%s", err.Error()) } @@ -526,7 +531,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if expErr != nil { return "", toError(expErr) } - compiledText, err = gen.Build(ast, genOptions, parseOptions) + compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions) if err != nil { return "", node.halt("parse 'for' error:%s", err.Error()) } @@ -544,7 +549,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if expErr != nil { return "", toError(expErr) } - compiledText, err = gen.Build(ast, genOptions, parseOptions) + compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions) if err != nil { return "", node.halt("parse 'for' statement error:%s", err.Error()) } @@ -561,7 +566,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if expErr != nil { return "", toError(expErr) } - compiledText, err = gen.Build(ast, genOptions, parseOptions) + compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions) if err != nil { return "", node.halt("parse 'if' statement error:%s", err.Error()) } @@ -590,7 +595,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if expErr != nil { return "", toError(expErr) } - compiledText, err = gen.Build(ast, genOptions, parseOptions) + compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions) if err != nil { return "", node.halt("parse 'if' statement error:%s", err.Error()) } @@ -617,7 +622,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { if expErr != nil { return "", toError(expErr) } - compiledText, err = gen.Build(ast, genOptions, parseOptions) + compiledText, noDelimit, err = gen.Build(ast, genOptions, parseOptions) if err != nil { return "", node.halt("parse 'for' loop error:%s", err.Error()) } @@ -626,7 +631,7 @@ func (node *Node) Compile(options *CompileOptions) (result string, err error) { return "", toError(expErr) } var code string - if code, err = gen.Build(ast, genOptions, parseOptions); err != nil { + if code, noDelimit, err = gen.Build(ast, genOptions, parseOptions); err != nil { return "", node.halt("parse 'for' loops error:%s", err.Error()) } compiledText += " = " + code diff --git a/lib/funcs/funcs.go b/lib/funcs/funcs.go index 49e8976..06dfce4 100644 --- a/lib/funcs/funcs.go +++ b/lib/funcs/funcs.go @@ -14,15 +14,28 @@ import ( "github.com/fefit/dateutil" ) +// OperatorNumberFn func for operate numbers type OperatorNumberFn func(interface{}, interface{}) interface{} + +// OperatorIntFn func for int types type OperatorIntFn func(int64, int64) int64 + +// ResultNumberFn wrap the OperatorNumberFn type ResultNumberFn func(args ...interface{}) interface{} + +// ResultIntFn wrap the OperatorIntFn type ResultIntFn func(args ...interface{}) int64 + +// JSON alias a json type type JSON map[string]interface{} + +// LoopChan used for "for" blocks type LoopChan struct { Chan chan int Loop int } + +// CaptureData used for Capture type CaptureData struct { Variables map[string]interface{} Data interface{} @@ -33,11 +46,15 @@ func (lc *LoopChan) init() { lc.Loop = -1 _, _ = lc.Next() } + +// Close close the loop chan func (lc *LoopChan) Close() (string, error) { lc.Loop = -1 close(lc.Chan) return "", nil } + +// Next goto the next step func (lc *LoopChan) Next() (string, error) { lc.Loop++ lc.Chan <- lc.Loop @@ -61,51 +78,51 @@ func Inject() template.FuncMap { if a, b, err := toIntNumbers(a, b); err == nil { return a + b } - if a, b, err := toFloatNumbers(a, b); err == nil { - return a + b - } else { + if a, b, err := toFloatNumbers(a, b); err != nil { panic(makeHaltInfo("plus(+)", err)) + } else { + return a + b } }, true) injects["INJECT_MINUS"] = generateNumberFunc(func(a, b interface{}) interface{} { if a, b, err := toIntNumbers(a, b); err == nil { return a - b } - if a, b, err := toFloatNumbers(a, b); err == nil { - return a - b - } else { + if a, b, err := toFloatNumbers(a, b); err != nil { panic(makeHaltInfo("minus(-)", err)) + } else { + return a - b } }, true) injects["INJECT_MULTIPLE"] = generateNumberFunc(func(a, b interface{}) interface{} { if a, b, err := toIntNumbers(a, b); err == nil { return a * b } - if a, b, err := toFloatNumbers(a, b); err == nil { - return a * b - } else { + if a, b, err := toFloatNumbers(a, b); err != nil { panic(makeHaltInfo("multiple(*)", err)) + } else { + return a * b } }, true) injects["INJECT_DIVIDE"] = generateNumberFunc(func(a, b interface{}) interface{} { - if a, b, err := toFloatNumbers(a, b); err == nil { - return a / b - } else { + if a, b, err := toFloatNumbers(a, b); err != nil { panic(makeHaltInfo("divide(/)", err)) + } else { + return a / b } }, false) injects["INJECT_MOD"] = generateNumberFunc(func(a, b interface{}) interface{} { - if a, b, err := toFloatNumbers(a, b); err == nil { - return math.Mod(a, b) - } else { + if a, b, err := toFloatNumbers(a, b); err != nil { panic(makeHaltInfo("mod(%)", err)) + } else { + return math.Mod(a, b) } }, false) injects["INJECT_POWER"] = generateNumberFunc(func(a, b interface{}) interface{} { - if a, b, err := toFloatNumbers(a, b); err == nil { - return math.Pow(a, b) - } else { + if a, b, err := toFloatNumbers(a, b); err != nil { panic(makeHaltInfo("power(**)", err)) + } else { + return math.Pow(a, b) } }, false) injects["INJECT_BITAND"] = generateIntFunc(func(a, b int64) int64 { @@ -144,13 +161,13 @@ func Helpers() template.FuncMap { } return b } - if a, b, err := toFloatNumbers(a, b); err == nil { + if a, b, err := toFloatNumbers(a, b); err != nil { + panic(makeHaltInfo("min", err)) + } else { if a < b { return a } return b - } else { - panic(makeHaltInfo("min", err)) } }, true) helpers["max"] = generateNumberFunc(func(a, b interface{}) interface{} { @@ -160,13 +177,13 @@ func Helpers() template.FuncMap { } return b } - if a, b, err := toFloatNumbers(a, b); err == nil { + if a, b, err := toFloatNumbers(a, b); err != nil { + panic(makeHaltInfo("max", err)) + } else { if a > b { return a } return b - } else { - panic(makeHaltInfo("max", err)) } }, true) // format @@ -188,6 +205,7 @@ func Helpers() template.FuncMap { helpers["count"] = count helpers["mrange"] = makeRange helpers["json_encode"] = jsonEncode + helpers["json_decode"] = jsonDecode // slice, don't add this line since go1.13 helpers["slice"] = slice return helpers @@ -567,7 +585,7 @@ func stringify(target interface{}) template.HTML { return template.HTML(result) } -func jsonEncode(str string, args ...interface{}) JSON { +func jsonDecode(str string, args ...interface{}) JSON { if len(args) == 1 { fns := template.FuncMap{ "stringify": stringify, @@ -590,6 +608,14 @@ func jsonEncode(str string, args ...interface{}) JSON { return result } +func jsonEncode(data interface{}, args ...interface{}) string { + b, err := json.Marshal(data) + if err != nil { + panic(err) + } + return string(b[:]) +} + func concat(str string, args ...interface{}) string { var builder strings.Builder builder.WriteString(str) diff --git a/lib/funcs/funcs_test.go b/lib/funcs/funcs_test.go index f73be24..beabe38 100644 --- a/lib/funcs/funcs_test.go +++ b/lib/funcs/funcs_test.go @@ -140,3 +140,14 @@ func TestTrim(t *testing.T) { assert.Equal(t, "bc", trim(baseChar, "a")) assert.Equal(t, "c", trim(baseChar, "ab")) } + +func TestJsonEncode(t *testing.T) { + m := map[string]interface{}{ + "hello": "world", + } + a := []int{1, 2, 3} + s := "it's a string" + assert.Equal(t, `{"hello":"world"}`, jsonEncode(m)) + assert.Equal(t, `[1,2,3]`, jsonEncode(a)) + assert.Equal(t, `"`+s+`"`, jsonEncode(s)) +} diff --git a/lib/generator/generator.go b/lib/generator/generator.go index 8171907..e2b7069 100644 --- a/lib/generator/generator.go +++ b/lib/generator/generator.go @@ -94,14 +94,14 @@ var ( ) // Build for code -func (gen *Generator) Build(node *Node, options *GenOptions, parseOptions *ParseOptions) (string, error) { +func (gen *Generator) Build(node *Node, options *GenOptions, parseOptions *ParseOptions) (result string, noDelimit bool, err error) { // conf := gen.Conf var str strings.Builder options.Str = &str - if err := gen.parseRecursive(node, options, parseOptions); err != nil { - return "", err + if noDelimit, err = gen.parseRecursive(node, options, parseOptions); err != nil { + return "", noDelimit, err } - return str.String(), nil + return str.String(), noDelimit, nil } func (gen *Generator) wrapToFloat(node *Node, options *GenOptions, parseOptions *ParseOptions, op string) error { @@ -119,7 +119,7 @@ func (gen *Generator) wrapToFloat(node *Node, options *GenOptions, parseOptions str.WriteString(fn) str.WriteString(SPACE) } - err := gen.parseRecursive(node, options, parseOptions) + _, err := gen.parseRecursive(node, options, parseOptions) if isNative { str.WriteString(")") } @@ -191,7 +191,7 @@ func (gen *Generator) parseIdentifier(options *GenOptions, parseOptions *ParseOp return nil } -func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptions *ParseOptions) (err error) { +func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptions *ParseOptions) (noDelimit bool, err error) { str, exp := options.Str, options.Exp noObjectIndex, parseConf, captures := parseOptions.NoObjectIndex, parseOptions.Conf, parseOptions.Captures curType := node.Type @@ -220,8 +220,8 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio express := string(runes[pos.StartIndex+1 : pos.EndIndex-1]) ast, _ := exp.Parse(express) var inner string - if inner, err = gen.Build(ast, options, parseOptions); err != nil { - return err + if inner, noDelimit, err = gen.Build(ast, options, parseOptions); err != nil { + return noDelimit, err } str.WriteString(inner) i = pos.EndIndex + 1 @@ -245,7 +245,7 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio if name == "$fet" { str.WriteString(".") } else if err = gen.parseIdentifier(options, parseOptions, name, ExpName); err != nil { - return err + return noDelimit, err } } } else if curType == "object" { @@ -287,6 +287,11 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio switch names[0] { case "now": str.WriteString("now") + case "debug": + noDelimit = true + if parseConf.Debug { + str.WriteString(``) + } default: panic("unsupport static variable $fet." + names[0]) } @@ -322,7 +327,7 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio } else { addIndexFn() if err = gen.parseIdentifier(options, parseOptions, string(t.Stat.Values), ObjectRoot); err != nil { - return err + return noDelimit, err } isParsed = true } @@ -330,8 +335,8 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio } if !isParsed { addIndexFn() - if err = gen.parseRecursive(root, options, parseOptions); err != nil { - return err + if noDelimit, err = gen.parseRecursive(root, options, parseOptions); err != nil { + return noDelimit, err } } if !isStatic { @@ -363,17 +368,17 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio str.WriteString("\"") } else { if err = gen.parseIdentifier(options, parseOptions, ident, ObjectField); err != nil { - return err + return noDelimit, err } } } else { - if err = gen.parseRecursive(cur, options, parseOptions); err != nil { - return err + if noDelimit, err = gen.parseRecursive(cur, options, parseOptions); err != nil { + return noDelimit, err } } } else { - if err = gen.parseRecursive(cur, options, parseOptions); err != nil { - return err + if noDelimit, err = gen.parseRecursive(cur, options, parseOptions); err != nil { + return noDelimit, err } } } @@ -392,7 +397,7 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio if t, ok := root.Token.(*e.IdentifierToken); ok { name := string(t.Stat.Values) if err = gen.parseIdentifier(options, parseOptions, name, FuncName); err != nil { - return err + return noDelimit, err } if _, ok := NoNeedIndexFuncs[name]; ok { parseOptions.NoObjectIndex = true @@ -401,8 +406,8 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio } } if !isParsed { - if err = gen.parseRecursive(root, options, parseOptions); err != nil { - return err + if noDelimit, err = gen.parseRecursive(root, options, parseOptions); err != nil { + return noDelimit, err } } str.WriteString(SPACE) @@ -410,8 +415,8 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio if i > 0 { str.WriteString(SPACE) } - if err = gen.parseRecursive(args[i], options, parseOptions); err != nil { - return err + if noDelimit, err = gen.parseRecursive(args[i], options, parseOptions); err != nil { + return noDelimit, err } } str.WriteString(")") @@ -423,18 +428,18 @@ func (gen *Generator) parseRecursive(node *Node, options *GenOptions, parseOptio str.WriteString(SPACE) } if err = gen.wrapToFloat(node.Left, options, parseOptions, op); err != nil { - return err + return noDelimit, err } str.WriteString(SPACE) if err = gen.wrapToFloat(node.Right, options, parseOptions, op); err != nil { - return err + return noDelimit, err } str.WriteString(")") } if isNot { str.WriteString(")") } - return nil + return noDelimit, nil } // New for Generator