diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml new file mode 100644 index 00000000..3a5610cd --- /dev/null +++ b/.buildkite/pipeline.yml @@ -0,0 +1,10 @@ +--- +env: + DEPENDENCY_NAME: github.com/autopilot3/liquid + VERSION: ${VERSION:-master} + +steps: + - label: ":buildkite: Generate Build Steps" + command: bksg -c golang-module-update -v "$VERSION" + retry: + automatic: true diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index bb8681bc..a6630f10 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ ## Checklist -- [ ] I have searched the [issue list](https://github.com/osteele/liquid/issues) +- [ ] I have searched the [issue list](https://github.com/autopilot3/liquid/issues) - [ ] I have tested my example against Shopify Liquid. (This isn't necessary if the actual behavior is a panic, or an error for which `IsTemplateError` returns false.) ## Expected Behavior diff --git a/.gitignore b/.gitignore index 7abd1c9e..97a203bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +.DS_Store *.output *.out /liquid *.test +.idea \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e0673eed..9ad0ea38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,406 +1,406 @@ ## 1.2.4 (2018-06-05) Contributors: -* @nsf — proper handling of [variadic functions](https://github.com/osteele/liquid/commit/1a2066b87e06ffc189c6cf8671893d4fdba2e607), [implicit conversion to integer types](https://github.com/osteele/liquid/commit/4354d48a0460b6bdebdb6b462abce0ea94f50fa8), and [error handling during expression evaluation](https://github.com/osteele/liquid/commit/c32908a4f3ef1425b3f73530a7de2412e0613c78) +* @nsf — proper handling of [variadic functions](https://github.com/autopilot3/liquid/commit/1a2066b87e06ffc189c6cf8671893d4fdba2e607), [implicit conversion to integer types](https://github.com/autopilot3/liquid/commit/4354d48a0460b6bdebdb6b462abce0ea94f50fa8), and [error handling during expression evaluation](https://github.com/autopilot3/liquid/commit/c32908a4f3ef1425b3f73530a7de2412e0613c78) * @osteele – docs and infrastructure ### Bug Fixes and Compatibility -* Returning proper error type causes less panics during expression eval. ([c32908a](https://github.com/osteele/liquid/commit/c32908a)) -* Properly handle implicit conversion to integer types. ([4354d48](https://github.com/osteele/liquid/commit/4354d48)) -* Properly handle variadic functions. ([1a2066b](https://github.com/osteele/liquid/commit/1a2066b)) -* map[unhashable] returns nil instead of panic ([b6c65ff](https://github.com/osteele/liquid/commit/b6c65ff)) -* join filter: default sep is space; omit nil entries ([cb6efbf](https://github.com/osteele/liquid/commit/cb6efbf)) -* Match Ruby string split semantics ([8874615](https://github.com/osteele/liquid/commit/8874615)) -* Convert MapSlice -> map ([1a12f12](https://github.com/osteele/liquid/commit/1a12f12)) -* list filters operate on MapSlice ([bb24f32](https://github.com/osteele/liquid/commit/bb24f32)) +* Returning proper error type causes less panics during expression eval. ([c32908a](https://github.com/autopilot3/liquid/commit/c32908a)) +* Properly handle implicit conversion to integer types. ([4354d48](https://github.com/autopilot3/liquid/commit/4354d48)) +* Properly handle variadic functions. ([1a2066b](https://github.com/autopilot3/liquid/commit/1a2066b)) +* map[unhashable] returns nil instead of panic ([b6c65ff](https://github.com/autopilot3/liquid/commit/b6c65ff)) +* join filter: default sep is space; omit nil entries ([cb6efbf](https://github.com/autopilot3/liquid/commit/cb6efbf)) +* Match Ruby string split semantics ([8874615](https://github.com/autopilot3/liquid/commit/8874615)) +* Convert MapSlice -> map ([1a12f12](https://github.com/autopilot3/liquid/commit/1a12f12)) +* list filters operate on MapSlice ([bb24f32](https://github.com/autopilot3/liquid/commit/bb24f32)) ### Docs -* Re-organize README ([dbf0f7d](https://github.com/osteele/liquid/commit/dbf0f7d)) -* Add Contributors section; add nsf as contributor; adopt All Contributors and all-contributors-cli ([d2be34e](https://github.com/osteele/liquid/commit/d2be34e)) -* Minor formatting fixes in the README ([aadc886](https://github.com/osteele/liquid/commit/aadc886)) +* Re-organize README ([dbf0f7d](https://github.com/autopilot3/liquid/commit/dbf0f7d)) +* Add Contributors section; add nsf as contributor; adopt All Contributors and all-contributors-cli ([d2be34e](https://github.com/autopilot3/liquid/commit/d2be34e)) +* Minor formatting fixes in the README ([aadc886](https://github.com/autopilot3/liquid/commit/aadc886)) ### Test Coverage -* Add Convert tests ([a50dc10](https://github.com/osteele/liquid/commit/a50dc10)) +* Add Convert tests ([a50dc10](https://github.com/autopilot3/liquid/commit/a50dc10)) ### Build and CI -* Add make pre-commit; lint before testing ([6e1f41e](https://github.com/osteele/liquid/commit/6e1f41e)) -* Add go 1.9 to travis build matrix ([ba2ecf9](https://github.com/osteele/liquid/commit/ba2ecf9)) -* Travis: add go 1.10; drop 1.8 ([e30a0e2](https://github.com/osteele/liquid/commit/e30a0e2)) +* Add make pre-commit; lint before testing ([6e1f41e](https://github.com/autopilot3/liquid/commit/6e1f41e)) +* Add go 1.9 to travis build matrix ([ba2ecf9](https://github.com/autopilot3/liquid/commit/ba2ecf9)) +* Travis: add go 1.10; drop 1.8 ([e30a0e2](https://github.com/autopilot3/liquid/commit/e30a0e2)) ### Code Internals -* Follow go style guide re declaring empty slices ([a02d9e1](https://github.com/osteele/liquid/commit/a02d9e1)) -* variable names ([d27c839](https://github.com/osteele/liquid/commit/d27c839)) -* variable names ([e1c7224](https://github.com/osteele/liquid/commit/e1c7224)) -* Remove errant file ([3811e16](https://github.com/osteele/liquid/commit/3811e16)) +* Follow go style guide re declaring empty slices ([a02d9e1](https://github.com/autopilot3/liquid/commit/a02d9e1)) +* variable names ([d27c839](https://github.com/autopilot3/liquid/commit/d27c839)) +* variable names ([e1c7224](https://github.com/autopilot3/liquid/commit/e1c7224)) +* Remove errant file ([3811e16](https://github.com/autopilot3/liquid/commit/3811e16)) ## 1.2.3 (2017-08-18) -* Default time format is compatible w/ Liquid ([5ebf31a](https://github.com/osteele/liquid/commit/5ebf31a)) -* Define IterationKeyedMap ([4bc4c8a](https://github.com/osteele/liquid/commit/4bc4c8a)) -* Move strftime to a separate repo ([cdb0e44](https://github.com/osteele/liquid/commit/cdb0e44)) -* Nil pointers are equal, even if different types ([fd4d34c](https://github.com/osteele/liquid/commit/fd4d34c)) -* Rearrange tests ([804e3d6](https://github.com/osteele/liquid/commit/804e3d6)) -* Rearrange value methods w/in file ([62f44fa](https://github.com/osteele/liquid/commit/62f44fa)) -* Rename rbstrftime package ([c49d979](https://github.com/osteele/liquid/commit/c49d979)) -* Tests; implement map[nil] ([6b15fbf](https://github.com/osteele/liquid/commit/6b15fbf)) +* Default time format is compatible w/ Liquid ([5ebf31a](https://github.com/autopilot3/liquid/commit/5ebf31a)) +* Define IterationKeyedMap ([4bc4c8a](https://github.com/autopilot3/liquid/commit/4bc4c8a)) +* Move strftime to a separate repo ([cdb0e44](https://github.com/autopilot3/liquid/commit/cdb0e44)) +* Nil pointers are equal, even if different types ([fd4d34c](https://github.com/autopilot3/liquid/commit/fd4d34c)) +* Rearrange tests ([804e3d6](https://github.com/autopilot3/liquid/commit/804e3d6)) +* Rearrange value methods w/in file ([62f44fa](https://github.com/autopilot3/liquid/commit/62f44fa)) +* Rename rbstrftime package ([c49d979](https://github.com/autopilot3/liquid/commit/c49d979)) +* Tests; implement map[nil] ([6b15fbf](https://github.com/autopilot3/liquid/commit/6b15fbf)) ## 1.2.2 (2017-08-08) ### Bug Fixes -* Fix array[nil] ([e39a1fe](https://github.com/osteele/liquid/commit/e39a1fe)) -* Fix file not found tests for Windows ([068afef](https://github.com/osteele/liquid/commit/068afef)) -* Restore m['str'] where m map[interface{}]interface{} ([9852226](https://github.com/osteele/liquid/commit/9852226)) +* Fix array[nil] ([e39a1fe](https://github.com/autopilot3/liquid/commit/e39a1fe)) +* Fix file not found tests for Windows ([068afef](https://github.com/autopilot3/liquid/commit/068afef)) +* Restore m['str'] where m map[interface{}]interface{} ([9852226](https://github.com/autopilot3/liquid/commit/9852226)) ### Docs -* More drop examples ([c50491f](https://github.com/osteele/liquid/commit/c50491f)) -* Package docs ([51d7166](https://github.com/osteele/liquid/commit/51d7166)) +* More drop examples ([c50491f](https://github.com/autopilot3/liquid/commit/c50491f)) +* Package docs ([51d7166](https://github.com/autopilot3/liquid/commit/51d7166)) ### Tests -* Beefy strftime tests ([4a2c4b4](https://github.com/osteele/liquid/commit/4a2c4b4)) +* Beefy strftime tests ([4a2c4b4](https://github.com/autopilot3/liquid/commit/4a2c4b4)) ### Build and CI -* README: add Appveyor badge ([0adf6e7](https://github.com/osteele/liquid/commit/0adf6e7)) -* Appveyor: remove mingw ([1b3e55a](https://github.com/osteele/liquid/commit/1b3e55a)) +* README: add Appveyor badge ([0adf6e7](https://github.com/autopilot3/liquid/commit/0adf6e7)) +* Appveyor: remove mingw ([1b3e55a](https://github.com/autopilot3/liquid/commit/1b3e55a)) ### Code Internals -* Remove (commented-out) Strptime and tests ([8d53a6b](https://github.com/osteele/liquid/commit/8d53a6b)) -* Replace extern "C" strftime by go implementation ([85bd1dd](https://github.com/osteele/liquid/commit/85bd1dd)) +* Remove (commented-out) Strptime and tests ([8d53a6b](https://github.com/autopilot3/liquid/commit/8d53a6b)) +* Replace extern "C" strftime by go implementation ([85bd1dd](https://github.com/autopilot3/liquid/commit/85bd1dd)) ## 1.2.1 (2017-08-03) Contributors: @osteele, @thessem -* "type" filters works on nil ([96307fa](https://github.com/osteele/liquid/commit/96307fa)) -* Actually cache the drop resolution ([83652f5](https://github.com/osteele/liquid/commit/83652f5)) -* Add comments and update tests ([dd4d967](https://github.com/osteele/liquid/commit/dd4d967)) -* Add engine.ParseString ([5151799](https://github.com/osteele/liquid/commit/5151799)) -* Add forwarders from evaluator pkg ([fb70314](https://github.com/osteele/liquid/commit/fb70314)) -* Add setting to customise delimiters ([9dd9191](https://github.com/osteele/liquid/commit/9dd9191)) -* Add some tests ([a07e5fa](https://github.com/osteele/liquid/commit/a07e5fa)) -* Add test ([3d99b41](https://github.com/osteele/liquid/commit/3d99b41)) -* Add top-level test cases for &map, struct ([f670bfc](https://github.com/osteele/liquid/commit/f670bfc)), closes [#23](https://github.com/osteele/liquid/issues/23) -* Allow value to be a pointer ([222559a](https://github.com/osteele/liquid/commit/222559a)) -* Benchmarks ([023fca4](https://github.com/osteele/liquid/commit/023fca4)) -* Change name of repository in README to liquid from goliquid ([08cf333](https://github.com/osteele/liquid/commit/08cf333)) -* Consolidate {expressions,values}/drops.go ([516182a](https://github.com/osteele/liquid/commit/516182a)) -* Document values, includng new struct behavior ([1bc9726](https://github.com/osteele/liquid/commit/1bc9726)) -* Fix struct PropertyValue attempting to use an invalid pointer ([b2f5f1f](https://github.com/osteele/liquid/commit/b2f5f1f)) -* gitgnore *.test ([605d883](https://github.com/osteele/liquid/commit/605d883)) -* Implement #11 contains on hashes ([1b0f0cf](https://github.com/osteele/liquid/commit/1b0f0cf)), closes [#11](https://github.com/osteele/liquid/issues/11) -* make lint includes tests ([dd0fcda](https://github.com/osteele/liquid/commit/dd0fcda)) -* Match Liquid/Ruby array[float] ([fa5de60](https://github.com/osteele/liquid/commit/fa5de60)) -* Move pkg evaluator -> values ([6269836](https://github.com/osteele/liquid/commit/6269836)) -* Move structValue to own file ([bbdb40e](https://github.com/osteele/liquid/commit/bbdb40e)) -* Obey struct field tags ([303027b](https://github.com/osteele/liquid/commit/303027b)) -* Property access to struct pointers ([de5fffa](https://github.com/osteele/liquid/commit/de5fffa)) -* Property access to struct values ([2cdd59d](https://github.com/osteele/liquid/commit/2cdd59d)) -* Pull loop renderer into separate method ([eac67c3](https://github.com/osteele/liquid/commit/eac67c3)) -* Race condition ([9866cbf](https://github.com/osteele/liquid/commit/9866cbf)) -* Race test, benchmarks, for drop resolution ([7f501ce](https://github.com/osteele/liquid/commit/7f501ce)) -* Recognize yaml.MapSlice as a value ([46807c4](https://github.com/osteele/liquid/commit/46807c4)) -* remove fmt.Stringer render case ([474edc1](https://github.com/osteele/liquid/commit/474edc1)) -* Remove generic Index, ObjectProperty ([8040e9e](https://github.com/osteele/liquid/commit/8040e9e)) -* Remove obsolete generic predicates ([cf54755](https://github.com/osteele/liquid/commit/cf54755)) -* Remove obsolete note re Awesome Go ([df3f7b2](https://github.com/osteele/liquid/commit/df3f7b2)) -* Return errors applying filters as Render errors ([8ee8cef](https://github.com/osteele/liquid/commit/8ee8cef)) -* Store original stacktrace in re-thrown errors ([a1c5927](https://github.com/osteele/liquid/commit/a1c5927)) -* Support delimiters of any length ([b7ef67f](https://github.com/osteele/liquid/commit/b7ef67f)) -* Support registering variadic functions as filters ([82a1a6e](https://github.com/osteele/liquid/commit/82a1a6e)) -* teach iteration about MapSlice ([306be63](https://github.com/osteele/liquid/commit/306be63)) -* Test cases for new code ([17def25](https://github.com/osteele/liquid/commit/17def25)) -* test liquid:"-", not liquid:"" (both work, though) ([7634673](https://github.com/osteele/liquid/commit/7634673)) -* Tests ([fd230ed](https://github.com/osteele/liquid/commit/fd230ed)) -* Treat []byte as string, for some purposes ([fd7b1f0](https://github.com/osteele/liquid/commit/fd7b1f0)) -* Value layer recognizes, resolves drops ([560c55e](https://github.com/osteele/liquid/commit/560c55e)) -* Wrap values instead of using generic functions ([85cd6c9](https://github.com/osteele/liquid/commit/85cd6c9)) +* "type" filters works on nil ([96307fa](https://github.com/autopilot3/liquid/commit/96307fa)) +* Actually cache the drop resolution ([83652f5](https://github.com/autopilot3/liquid/commit/83652f5)) +* Add comments and update tests ([dd4d967](https://github.com/autopilot3/liquid/commit/dd4d967)) +* Add engine.ParseString ([5151799](https://github.com/autopilot3/liquid/commit/5151799)) +* Add forwarders from evaluator pkg ([fb70314](https://github.com/autopilot3/liquid/commit/fb70314)) +* Add setting to customise delimiters ([9dd9191](https://github.com/autopilot3/liquid/commit/9dd9191)) +* Add some tests ([a07e5fa](https://github.com/autopilot3/liquid/commit/a07e5fa)) +* Add test ([3d99b41](https://github.com/autopilot3/liquid/commit/3d99b41)) +* Add top-level test cases for &map, struct ([f670bfc](https://github.com/autopilot3/liquid/commit/f670bfc)), closes [#23](https://github.com/autopilot3/liquid/issues/23) +* Allow value to be a pointer ([222559a](https://github.com/autopilot3/liquid/commit/222559a)) +* Benchmarks ([023fca4](https://github.com/autopilot3/liquid/commit/023fca4)) +* Change name of repository in README to liquid from goliquid ([08cf333](https://github.com/autopilot3/liquid/commit/08cf333)) +* Consolidate {expressions,values}/drops.go ([516182a](https://github.com/autopilot3/liquid/commit/516182a)) +* Document values, includng new struct behavior ([1bc9726](https://github.com/autopilot3/liquid/commit/1bc9726)) +* Fix struct PropertyValue attempting to use an invalid pointer ([b2f5f1f](https://github.com/autopilot3/liquid/commit/b2f5f1f)) +* gitgnore *.test ([605d883](https://github.com/autopilot3/liquid/commit/605d883)) +* Implement #11 contains on hashes ([1b0f0cf](https://github.com/autopilot3/liquid/commit/1b0f0cf)), closes [#11](https://github.com/autopilot3/liquid/issues/11) +* make lint includes tests ([dd0fcda](https://github.com/autopilot3/liquid/commit/dd0fcda)) +* Match Liquid/Ruby array[float] ([fa5de60](https://github.com/autopilot3/liquid/commit/fa5de60)) +* Move pkg evaluator -> values ([6269836](https://github.com/autopilot3/liquid/commit/6269836)) +* Move structValue to own file ([bbdb40e](https://github.com/autopilot3/liquid/commit/bbdb40e)) +* Obey struct field tags ([303027b](https://github.com/autopilot3/liquid/commit/303027b)) +* Property access to struct pointers ([de5fffa](https://github.com/autopilot3/liquid/commit/de5fffa)) +* Property access to struct values ([2cdd59d](https://github.com/autopilot3/liquid/commit/2cdd59d)) +* Pull loop renderer into separate method ([eac67c3](https://github.com/autopilot3/liquid/commit/eac67c3)) +* Race condition ([9866cbf](https://github.com/autopilot3/liquid/commit/9866cbf)) +* Race test, benchmarks, for drop resolution ([7f501ce](https://github.com/autopilot3/liquid/commit/7f501ce)) +* Recognize yaml.MapSlice as a value ([46807c4](https://github.com/autopilot3/liquid/commit/46807c4)) +* remove fmt.Stringer render case ([474edc1](https://github.com/autopilot3/liquid/commit/474edc1)) +* Remove generic Index, ObjectProperty ([8040e9e](https://github.com/autopilot3/liquid/commit/8040e9e)) +* Remove obsolete generic predicates ([cf54755](https://github.com/autopilot3/liquid/commit/cf54755)) +* Remove obsolete note re Awesome Go ([df3f7b2](https://github.com/autopilot3/liquid/commit/df3f7b2)) +* Return errors applying filters as Render errors ([8ee8cef](https://github.com/autopilot3/liquid/commit/8ee8cef)) +* Store original stacktrace in re-thrown errors ([a1c5927](https://github.com/autopilot3/liquid/commit/a1c5927)) +* Support delimiters of any length ([b7ef67f](https://github.com/autopilot3/liquid/commit/b7ef67f)) +* Support registering variadic functions as filters ([82a1a6e](https://github.com/autopilot3/liquid/commit/82a1a6e)) +* teach iteration about MapSlice ([306be63](https://github.com/autopilot3/liquid/commit/306be63)) +* Test cases for new code ([17def25](https://github.com/autopilot3/liquid/commit/17def25)) +* test liquid:"-", not liquid:"" (both work, though) ([7634673](https://github.com/autopilot3/liquid/commit/7634673)) +* Tests ([fd230ed](https://github.com/autopilot3/liquid/commit/fd230ed)) +* Treat []byte as string, for some purposes ([fd7b1f0](https://github.com/autopilot3/liquid/commit/fd7b1f0)) +* Value layer recognizes, resolves drops ([560c55e](https://github.com/autopilot3/liquid/commit/560c55e)) +* Wrap values instead of using generic functions ([85cd6c9](https://github.com/autopilot3/liquid/commit/85cd6c9)) ## 1.1.2 (2017-07-20) -* Coverage ([023536f](https://github.com/osteele/liquid/commit/023536f)) -* Coverage ([27580ca](https://github.com/osteele/liquid/commit/27580ca)) -* Coverage ([413b328](https://github.com/osteele/liquid/commit/413b328)) -* Coverage ([a78d95d](https://github.com/osteele/liquid/commit/a78d95d)) -* Lint ([dde3ea7](https://github.com/osteele/liquid/commit/dde3ea7)) -* Lint ([73f0fef](https://github.com/osteele/liquid/commit/73f0fef)) -* make lint enables gofmt ([510b0cb](https://github.com/osteele/liquid/commit/510b0cb)) -* Remove quote from README ([5f79cf1](https://github.com/osteele/liquid/commit/5f79cf1)) -* Rename parse error -> syntax error ([7af399a](https://github.com/osteele/liquid/commit/7af399a)) -* Update expressions.y ParseError -> SyntaxError ([17c5c9c](https://github.com/osteele/liquid/commit/17c5c9c)) +* Coverage ([023536f](https://github.com/autopilot3/liquid/commit/023536f)) +* Coverage ([27580ca](https://github.com/autopilot3/liquid/commit/27580ca)) +* Coverage ([413b328](https://github.com/autopilot3/liquid/commit/413b328)) +* Coverage ([a78d95d](https://github.com/autopilot3/liquid/commit/a78d95d)) +* Lint ([dde3ea7](https://github.com/autopilot3/liquid/commit/dde3ea7)) +* Lint ([73f0fef](https://github.com/autopilot3/liquid/commit/73f0fef)) +* make lint enables gofmt ([510b0cb](https://github.com/autopilot3/liquid/commit/510b0cb)) +* Remove quote from README ([5f79cf1](https://github.com/autopilot3/liquid/commit/5f79cf1)) +* Rename parse error -> syntax error ([7af399a](https://github.com/autopilot3/liquid/commit/7af399a)) +* Update expressions.y ParseError -> SyntaxError ([17c5c9c](https://github.com/autopilot3/liquid/commit/17c5c9c)) ## 1.1.1 (2017-07-17) -* Iterating over hash yields [key, value] pairs ([67cb2e0](https://github.com/osteele/liquid/commit/67cb2e0)) -* Quote tag names in error messages ([2c497e3](https://github.com/osteele/liquid/commit/2c497e3)) +* Iterating over hash yields [key, value] pairs ([67cb2e0](https://github.com/autopilot3/liquid/commit/67cb2e0)) +* Quote tag names in error messages ([2c497e3](https://github.com/autopilot3/liquid/commit/2c497e3)) ## 1.1.0 (2017-07-16) -* CLI script to run shopify liquid for cf. ([534c0e3](https://github.com/osteele/liquid/commit/534c0e3)) -* Disable interfacer linter :frowning: ([6701199](https://github.com/osteele/liquid/commit/6701199)) -* Implement whitespace control ([f9ac12b](https://github.com/osteele/liquid/commit/f9ac12b)) -* Numbers can't start or end with a dot ([f1412b6](https://github.com/osteele/liquid/commit/f1412b6)) -* README ([9fe6a96](https://github.com/osteele/liquid/commit/9fe6a96)) -* README filters and variables ([cfc8a8c](https://github.com/osteele/liquid/commit/cfc8a8c)) -* Report the line only if != 0 ([af93d57](https://github.com/osteele/liquid/commit/af93d57)) -* Scan whitespace control ([bf43fb8](https://github.com/osteele/liquid/commit/bf43fb8)) -* Warn on too many filter args ([de4f81d](https://github.com/osteele/liquid/commit/de4f81d)) -* Whitespace control uses byte.Buffer ([dd49b22](https://github.com/osteele/liquid/commit/dd49b22)) +* CLI script to run shopify liquid for cf. ([534c0e3](https://github.com/autopilot3/liquid/commit/534c0e3)) +* Disable interfacer linter :frowning: ([6701199](https://github.com/autopilot3/liquid/commit/6701199)) +* Implement whitespace control ([f9ac12b](https://github.com/autopilot3/liquid/commit/f9ac12b)) +* Numbers can't start or end with a dot ([f1412b6](https://github.com/autopilot3/liquid/commit/f1412b6)) +* README ([9fe6a96](https://github.com/autopilot3/liquid/commit/9fe6a96)) +* README filters and variables ([cfc8a8c](https://github.com/autopilot3/liquid/commit/cfc8a8c)) +* Report the line only if != 0 ([af93d57](https://github.com/autopilot3/liquid/commit/af93d57)) +* Scan whitespace control ([bf43fb8](https://github.com/autopilot3/liquid/commit/bf43fb8)) +* Warn on too many filter args ([de4f81d](https://github.com/autopilot3/liquid/commit/de4f81d)) +* Whitespace control uses byte.Buffer ([dd49b22](https://github.com/autopilot3/liquid/commit/dd49b22)) ## 1.0.0 (2017-07-16) -* Add appveyor.yml ([06e0833](https://github.com/osteele/liquid/commit/06e0833)) -* Add expression.ParseStatement, statement selector literals ([c864f3c](https://github.com/osteele/liquid/commit/c864f3c)) -* Add FromDrop func ([8efaada](https://github.com/osteele/liquid/commit/8efaada)) -* Add ParseTemplateLocation ([16c3b6e](https://github.com/osteele/liquid/commit/16c3b6e)) -* Allow float index into array ([247c1b1](https://github.com/osteele/liquid/commit/247c1b1)) -* Close #18 loop range ([271f637](https://github.com/osteele/liquid/commit/271f637)), closes [#18](https://github.com/osteele/liquid/issues/18) -* Combine CompilationError -> parser.Error ([816b46a](https://github.com/osteele/liquid/commit/816b46a)) -* Complete #14 and #15 url{en,de}code filters ([2e5cc60](https://github.com/osteele/liquid/commit/2e5cc60)), closes [#14](https://github.com/osteele/liquid/issues/14) [#15](https://github.com/osteele/liquid/issues/15) -* Complete #17 sort_natural filter ([3c242c4](https://github.com/osteele/liquid/commit/3c242c4)), closes [#17](https://github.com/osteele/liquid/issues/17) -* Complete #19 when a or b ([2880ef4](https://github.com/osteele/liquid/commit/2880ef4)), closes [#19](https://github.com/osteele/liquid/issues/19) -* Complete #4 case…else ([26bdd09](https://github.com/osteele/liquid/commit/26bdd09)), closes [#4](https://github.com/osteele/liquid/issues/4) -* Consolidate render.Error -> parser.Error ([198f6bf](https://github.com/osteele/liquid/commit/198f6bf)) -* Coverage ([a2a4a1a](https://github.com/osteele/liquid/commit/a2a4a1a)) -* Coverage ([d6d6929](https://github.com/osteele/liquid/commit/d6d6929)) -* Coverage ([29c902f](https://github.com/osteele/liquid/commit/29c902f)) -* Cycle uses Statement; steps towards cycle groups ([7444118](https://github.com/osteele/liquid/commit/7444118)) -* docs ([4317bfc](https://github.com/osteele/liquid/commit/4317bfc)) -* Error.Filename -> Path ([b95775c](https://github.com/osteele/liquid/commit/b95775c)) -* fun w/ time zones ([4163dfa](https://github.com/osteele/liquid/commit/4163dfa)) -* Implement #15 truncate_words filter ([fdfc5d3](https://github.com/osteele/liquid/commit/fdfc5d3)), closes [#15](https://github.com/osteele/liquid/issues/15) -* Implement tablerow ([cd23447](https://github.com/osteele/liquid/commit/cd23447)) -* Improve strftime error test ([55cf56e](https://github.com/osteele/liquid/commit/55cf56e)) -* Loop uses the statement record ([110fee6](https://github.com/osteele/liquid/commit/110fee6)) -* Make harmless iterating over value ([bad5593](https://github.com/osteele/liquid/commit/bad5593)) -* make install-dev-tools -> setup ([0808c10](https://github.com/osteele/liquid/commit/0808c10)) -* make setup installs dependencies ([68a3e9b](https://github.com/osteele/liquid/commit/68a3e9b)) -* Move control flow tags to separate file ([c3c9de7](https://github.com/osteele/liquid/commit/c3c9de7)) -* Move interpreter ops into evaluator package ([c11cf2a](https://github.com/osteele/liquid/commit/c11cf2a)) -* Move package expression -> expressions ([6ff5721](https://github.com/osteele/liquid/commit/6ff5721)) -* New ParseStatement returns record with different statement parse types ([8964daf](https://github.com/osteele/liquid/commit/8964daf)) -* Parse in local time; switch to stdlib strftime ([f39a2d2](https://github.com/osteele/liquid/commit/f39a2d2)) -* ParseError -> parser.Error; takes Locatable ([8995782](https://github.com/osteele/liquid/commit/8995782)) -* Prep loop for ranges ([22d583f](https://github.com/osteele/liquid/commit/22d583f)) -* Property names can end in ? ([dbba680](https://github.com/osteele/liquid/commit/dbba680)) -* ranges…but need to separated by .. ([497a932](https://github.com/osteele/liquid/commit/497a932)) -* README ([ce7cc8f](https://github.com/osteele/liquid/commit/ce7cc8f)) -* Remove a test that fails on Travis ([55ec347](https://github.com/osteele/liquid/commit/55ec347)) -* Remove dependency on strptime ([da541ab](https://github.com/osteele/liquid/commit/da541ab)) -* Remove IsTemplateError ([724da61](https://github.com/osteele/liquid/commit/724da61)) -* Rename branch -> clause (and remove Governs) ([5547532](https://github.com/osteele/liquid/commit/5547532)) -* Rename Config.Filename -> SourcePath ([df80e8c](https://github.com/osteele/liquid/commit/df80e8c)) -* Rename files -> standard_tags, standard_filters ([8882a7d](https://github.com/osteele/liquid/commit/8882a7d)) -* Rename loop_tag -> iteration_tags ([55eb5b4](https://github.com/osteele/liquid/commit/55eb5b4)) -* rename node.Branch -> Clause too ([5a12245](https://github.com/osteele/liquid/commit/5a12245)) -* Rename xxxTagParser -> xxxTagCompiler ([6b8f76c](https://github.com/osteele/liquid/commit/6b8f76c)) -* Reorganize docs and examples ([bfc7ced](https://github.com/osteele/liquid/commit/bfc7ced)) -* Replace render switch by polymorphism ([1c94b61](https://github.com/osteele/liquid/commit/1c94b61)) -* set travis email notification freq ([9701daa](https://github.com/osteele/liquid/commit/9701daa)) -* Source location is an initialization parameter ([92a4f2d](https://github.com/osteele/liquid/commit/92a4f2d)) -* Start #2 cycle tag ([a637d27](https://github.com/osteele/liquid/commit/a637d27)), closes [#2](https://github.com/osteele/liquid/issues/2) -* Test case for main ([6a3a853](https://github.com/osteele/liquid/commit/6a3a853)) -* TIL io.WriteString ([41e7b29](https://github.com/osteele/liquid/commit/41e7b29)) -* try disabling strptime ([bb0590d](https://github.com/osteele/liquid/commit/bb0590d)) -* Update README to v1 ([f1cddfa](https://github.com/osteele/liquid/commit/f1cddfa)) +* Add appveyor.yml ([06e0833](https://github.com/autopilot3/liquid/commit/06e0833)) +* Add expression.ParseStatement, statement selector literals ([c864f3c](https://github.com/autopilot3/liquid/commit/c864f3c)) +* Add FromDrop func ([8efaada](https://github.com/autopilot3/liquid/commit/8efaada)) +* Add ParseTemplateLocation ([16c3b6e](https://github.com/autopilot3/liquid/commit/16c3b6e)) +* Allow float index into array ([247c1b1](https://github.com/autopilot3/liquid/commit/247c1b1)) +* Close #18 loop range ([271f637](https://github.com/autopilot3/liquid/commit/271f637)), closes [#18](https://github.com/autopilot3/liquid/issues/18) +* Combine CompilationError -> parser.Error ([816b46a](https://github.com/autopilot3/liquid/commit/816b46a)) +* Complete #14 and #15 url{en,de}code filters ([2e5cc60](https://github.com/autopilot3/liquid/commit/2e5cc60)), closes [#14](https://github.com/autopilot3/liquid/issues/14) [#15](https://github.com/autopilot3/liquid/issues/15) +* Complete #17 sort_natural filter ([3c242c4](https://github.com/autopilot3/liquid/commit/3c242c4)), closes [#17](https://github.com/autopilot3/liquid/issues/17) +* Complete #19 when a or b ([2880ef4](https://github.com/autopilot3/liquid/commit/2880ef4)), closes [#19](https://github.com/autopilot3/liquid/issues/19) +* Complete #4 case…else ([26bdd09](https://github.com/autopilot3/liquid/commit/26bdd09)), closes [#4](https://github.com/autopilot3/liquid/issues/4) +* Consolidate render.Error -> parser.Error ([198f6bf](https://github.com/autopilot3/liquid/commit/198f6bf)) +* Coverage ([a2a4a1a](https://github.com/autopilot3/liquid/commit/a2a4a1a)) +* Coverage ([d6d6929](https://github.com/autopilot3/liquid/commit/d6d6929)) +* Coverage ([29c902f](https://github.com/autopilot3/liquid/commit/29c902f)) +* Cycle uses Statement; steps towards cycle groups ([7444118](https://github.com/autopilot3/liquid/commit/7444118)) +* docs ([4317bfc](https://github.com/autopilot3/liquid/commit/4317bfc)) +* Error.Filename -> Path ([b95775c](https://github.com/autopilot3/liquid/commit/b95775c)) +* fun w/ time zones ([4163dfa](https://github.com/autopilot3/liquid/commit/4163dfa)) +* Implement #15 truncate_words filter ([fdfc5d3](https://github.com/autopilot3/liquid/commit/fdfc5d3)), closes [#15](https://github.com/autopilot3/liquid/issues/15) +* Implement tablerow ([cd23447](https://github.com/autopilot3/liquid/commit/cd23447)) +* Improve strftime error test ([55cf56e](https://github.com/autopilot3/liquid/commit/55cf56e)) +* Loop uses the statement record ([110fee6](https://github.com/autopilot3/liquid/commit/110fee6)) +* Make harmless iterating over value ([bad5593](https://github.com/autopilot3/liquid/commit/bad5593)) +* make install-dev-tools -> setup ([0808c10](https://github.com/autopilot3/liquid/commit/0808c10)) +* make setup installs dependencies ([68a3e9b](https://github.com/autopilot3/liquid/commit/68a3e9b)) +* Move control flow tags to separate file ([c3c9de7](https://github.com/autopilot3/liquid/commit/c3c9de7)) +* Move interpreter ops into evaluator package ([c11cf2a](https://github.com/autopilot3/liquid/commit/c11cf2a)) +* Move package expression -> expressions ([6ff5721](https://github.com/autopilot3/liquid/commit/6ff5721)) +* New ParseStatement returns record with different statement parse types ([8964daf](https://github.com/autopilot3/liquid/commit/8964daf)) +* Parse in local time; switch to stdlib strftime ([f39a2d2](https://github.com/autopilot3/liquid/commit/f39a2d2)) +* ParseError -> parser.Error; takes Locatable ([8995782](https://github.com/autopilot3/liquid/commit/8995782)) +* Prep loop for ranges ([22d583f](https://github.com/autopilot3/liquid/commit/22d583f)) +* Property names can end in ? ([dbba680](https://github.com/autopilot3/liquid/commit/dbba680)) +* ranges…but need to separated by .. ([497a932](https://github.com/autopilot3/liquid/commit/497a932)) +* README ([ce7cc8f](https://github.com/autopilot3/liquid/commit/ce7cc8f)) +* Remove a test that fails on Travis ([55ec347](https://github.com/autopilot3/liquid/commit/55ec347)) +* Remove dependency on strptime ([da541ab](https://github.com/autopilot3/liquid/commit/da541ab)) +* Remove IsTemplateError ([724da61](https://github.com/autopilot3/liquid/commit/724da61)) +* Rename branch -> clause (and remove Governs) ([5547532](https://github.com/autopilot3/liquid/commit/5547532)) +* Rename Config.Filename -> SourcePath ([df80e8c](https://github.com/autopilot3/liquid/commit/df80e8c)) +* Rename files -> standard_tags, standard_filters ([8882a7d](https://github.com/autopilot3/liquid/commit/8882a7d)) +* Rename loop_tag -> iteration_tags ([55eb5b4](https://github.com/autopilot3/liquid/commit/55eb5b4)) +* rename node.Branch -> Clause too ([5a12245](https://github.com/autopilot3/liquid/commit/5a12245)) +* Rename xxxTagParser -> xxxTagCompiler ([6b8f76c](https://github.com/autopilot3/liquid/commit/6b8f76c)) +* Reorganize docs and examples ([bfc7ced](https://github.com/autopilot3/liquid/commit/bfc7ced)) +* Replace render switch by polymorphism ([1c94b61](https://github.com/autopilot3/liquid/commit/1c94b61)) +* set travis email notification freq ([9701daa](https://github.com/autopilot3/liquid/commit/9701daa)) +* Source location is an initialization parameter ([92a4f2d](https://github.com/autopilot3/liquid/commit/92a4f2d)) +* Start #2 cycle tag ([a637d27](https://github.com/autopilot3/liquid/commit/a637d27)), closes [#2](https://github.com/autopilot3/liquid/issues/2) +* Test case for main ([6a3a853](https://github.com/autopilot3/liquid/commit/6a3a853)) +* TIL io.WriteString ([41e7b29](https://github.com/autopilot3/liquid/commit/41e7b29)) +* try disabling strptime ([bb0590d](https://github.com/autopilot3/liquid/commit/bb0590d)) +* Update README to v1 ([f1cddfa](https://github.com/autopilot3/liquid/commit/f1cddfa)) ## 0.2.0 (2017-07-10) -* Change Engine, Template from Interface -> struct ([ebb37f8](https://github.com/osteele/liquid/commit/ebb37f8)) -* comments ([328d84f](https://github.com/osteele/liquid/commit/328d84f)) -* docs ([163290b](https://github.com/osteele/liquid/commit/163290b)) -* errors return source location, phase 1 ([342a8b3](https://github.com/osteele/liquid/commit/342a8b3)) -* gopkg; docs ([635383b](https://github.com/osteele/liquid/commit/635383b)) -* Implement hash.size ([c2b7157](https://github.com/osteele/liquid/commit/c2b7157)) -* Parser grammar is distinct from (and embedded in) config ([b269138](https://github.com/osteele/liquid/commit/b269138)) -* README ([c291b2f](https://github.com/osteele/liquid/commit/c291b2f)) -* Rename ParseTime -> ParseDate ([a3a3473](https://github.com/osteele/liquid/commit/a3a3473)) -* Tests ([bfad047](https://github.com/osteele/liquid/commit/bfad047)) -* Update remaining public API to return SourceError ([378c0b2](https://github.com/osteele/liquid/commit/378c0b2)) +* Change Engine, Template from Interface -> struct ([ebb37f8](https://github.com/autopilot3/liquid/commit/ebb37f8)) +* comments ([328d84f](https://github.com/autopilot3/liquid/commit/328d84f)) +* docs ([163290b](https://github.com/autopilot3/liquid/commit/163290b)) +* errors return source location, phase 1 ([342a8b3](https://github.com/autopilot3/liquid/commit/342a8b3)) +* gopkg; docs ([635383b](https://github.com/autopilot3/liquid/commit/635383b)) +* Implement hash.size ([c2b7157](https://github.com/autopilot3/liquid/commit/c2b7157)) +* Parser grammar is distinct from (and embedded in) config ([b269138](https://github.com/autopilot3/liquid/commit/b269138)) +* README ([c291b2f](https://github.com/autopilot3/liquid/commit/c291b2f)) +* Rename ParseTime -> ParseDate ([a3a3473](https://github.com/autopilot3/liquid/commit/a3a3473)) +* Tests ([bfad047](https://github.com/autopilot3/liquid/commit/bfad047)) +* Update remaining public API to return SourceError ([378c0b2](https://github.com/autopilot3/liquid/commit/378c0b2)) ## 0.1.0 (2017-07-09) -* "contains" tests for arrays too ([24d83f1](https://github.com/osteele/liquid/commit/24d83f1)) -* (Some) relationship operators ([d03efed](https://github.com/osteele/liquid/commit/d03efed)) -* a.b syntax ([5dbd972](https://github.com/osteele/liquid/commit/5dbd972)) -* a[b] for invalid a or b ([50d11a6](https://github.com/osteele/liquid/commit/50d11a6)) -* Add a Grammar interface for parsing ([f313e6f](https://github.com/osteele/liquid/commit/f313e6f)) -* Add a Travis file ([8e673ac](https://github.com/osteele/liquid/commit/8e673ac)) -* Add an executable ([f1b2777](https://github.com/osteele/liquid/commit/f1b2777)) -* Add an type filter; inspect is more robust ([3a1506b](https://github.com/osteele/liquid/commit/3a1506b)) -* Add contribution guidelines ([1b7564d](https://github.com/osteele/liquid/commit/1b7564d)) -* Add coverage status ([6297319](https://github.com/osteele/liquid/commit/6297319)) -* Add docs; remove unused UnimplementedError ([983b9f5](https://github.com/osteele/liquid/commit/983b9f5)) -* Add engine.DefineControlTag; currently does nothing ([8f7bcae](https://github.com/osteele/liquid/commit/8f7bcae)) -* Add error line number ([4472b15](https://github.com/osteele/liquid/commit/4472b15)) -* Add goveralls to travis ([c415f89](https://github.com/osteele/liquid/commit/c415f89)) -* Add install-dev-toolsl to travis ([008f1ed](https://github.com/osteele/liquid/commit/008f1ed)) -* Add IsTemplateError ([2161bb6](https://github.com/osteele/liquid/commit/2161bb6)) -* Add Makefile ([29c9ad8](https://github.com/osteele/liquid/commit/29c9ad8)) -* Add more dependencies to credits ([97b36ab](https://github.com/osteele/liquid/commit/97b36ab)) -* Add more parse time formats ([77c5dc9](https://github.com/osteele/liquid/commit/77c5dc9)) -* Add MustConvertItem; convert bool -> int ([80d58dc](https://github.com/osteele/liquid/commit/80d58dc)) -* Add nil; distinguish between identifier and property patterns ([7a2b28c](https://github.com/osteele/liquid/commit/7a2b28c)) -* Add public DefineTag ([e21d2a7](https://github.com/osteele/liquid/commit/e21d2a7)) -* Add references to README ([ac12225](https://github.com/osteele/liquid/commit/ac12225)) -* Add RenderContext.ParseTagArgs ([7c48138](https://github.com/osteele/liquid/commit/7c48138)) -* Add reverse filter; improve generics ([54b9f13](https://github.com/osteele/liquid/commit/54b9f13)) -* Add status badges to the README ([465a681](https://github.com/osteele/liquid/commit/465a681)) -* Add strptime for time parsing ([8ea72e4](https://github.com/osteele/liquid/commit/8ea72e4)) -* Add Template.SetSourcePath ([5425668](https://github.com/osteele/liquid/commit/5425668)) -* Allow - in identifiers ([af8c486](https://github.com/osteele/liquid/commit/af8c486)) -* Allow parens ([607f4f4](https://github.com/osteele/liquid/commit/607f4f4)) -* Catch unimplemented panics ([b1cf056](https://github.com/osteele/liquid/commit/b1cf056)) -* Chunk regex wasn't sufficiently non-greedy ([f8b5503](https://github.com/osteele/liquid/commit/f8b5503)) -* Chunk scanner test cases ([fc6d96e](https://github.com/osteele/liquid/commit/fc6d96e)) -* Closure.Bind copies the original bindings ([4e96c15](https://github.com/osteele/liquid/commit/4e96c15)) -* Compiler copies the syntax tree ([8f63cb7](https://github.com/osteele/liquid/commit/8f63cb7)) -* contains operates on strings not arrays ([9dda87f](https://github.com/osteele/liquid/commit/9dda87f)) -* Control tag parsers can return an error ([61663ab](https://github.com/osteele/liquid/commit/61663ab)) -* Control tags actions are table-driven ([20e4df3](https://github.com/osteele/liquid/commit/20e4df3)) -* Convert -> map[string] ([767f1f4](https://github.com/osteele/liquid/commit/767f1f4)) -* Convert map -> [] ([6075f39](https://github.com/osteele/liquid/commit/6075f39)) -* Convert maps ([2fed70b](https://github.com/osteele/liquid/commit/2fed70b)) -* Convert returns an error; create MustConvert ([4df3f04](https://github.com/osteele/liquid/commit/4df3f04)) -* Coverage ([6f7b67f](https://github.com/osteele/liquid/commit/6f7b67f)) -* Coverage ([36929af](https://github.com/osteele/liquid/commit/36929af)) -* Coverage ([caca7a2](https://github.com/osteele/liquid/commit/caca7a2)) -* Coverage ([78526e7](https://github.com/osteele/liquid/commit/78526e7)) -* Coverage; simplify ([a8afb01](https://github.com/osteele/liquid/commit/a8afb01)) -* Create an Expression interface; add some docs ([2a2f333](https://github.com/osteele/liquid/commit/2a2f333)) -* Create LICENSE ([e3425cc](https://github.com/osteele/liquid/commit/e3425cc)) -* Create top-level interface to liquid package ([514559e](https://github.com/osteele/liquid/commit/514559e)) -* Docs ([f15de87](https://github.com/osteele/liquid/commit/f15de87)) -* Docs ([803fbbc](https://github.com/osteele/liquid/commit/803fbbc)) -* Embed the Chunk in the AST nodes ([089a0c8](https://github.com/osteele/liquid/commit/089a0c8)) -* Expression evaluator tests; fix revealed bugs ([1f805d5](https://github.com/osteele/liquid/commit/1f805d5)) -* Filters are an engine configuration ([2e9903f](https://github.com/osteele/liquid/commit/2e9903f)) -* Filters can have (single) parameters ([70aa70d](https://github.com/osteele/liquid/commit/70aa70d)) -* Filters support multiple argument, including expressions ([a93848a](https://github.com/osteele/liquid/commit/a93848a)) -* Finish generic comparison ([dbdcca4](https://github.com/osteele/liquid/commit/dbdcca4)) -* Fix forloop.last with offset modifier ([394036d](https://github.com/osteele/liquid/commit/394036d)) -* Fix generic equality with nil ([229059c](https://github.com/osteele/liquid/commit/229059c)) -* Fix previous ([87b8198](https://github.com/osteele/liquid/commit/87b8198)) -* Fix the raw tag ([e2bb7c6](https://github.com/osteele/liquid/commit/e2bb7c6)) -* for over a map iterates over its keys ([decd8dd](https://github.com/osteele/liquid/commit/decd8dd)) -* Functional is constructed within parser, not scanner ([c02fbd5](https://github.com/osteele/liquid/commit/c02fbd5)) -* FunctionalNode -> TagNode ([18e2540](https://github.com/osteele/liquid/commit/18e2540)) -* Generic Convert handles conversion to time ([2083747](https://github.com/osteele/liquid/commit/2083747)) -* Generics ([87708a0](https://github.com/osteele/liquid/commit/87708a0)) -* GitHub templates ([7bd8a8d](https://github.com/osteele/liquid/commit/7bd8a8d)) -* gometalinter doesn't have a config in this repo ([28db298](https://github.com/osteele/liquid/commit/28db298)) -* identifiers can include '-' ([606870e](https://github.com/osteele/liquid/commit/606870e)) -* If tag parses during parse stage ([621992c](https://github.com/osteele/liquid/commit/621992c)) -* Implement {% else %}, {% elsif %} ([cab7845](https://github.com/osteele/liquid/commit/cab7845)) -* Implement {% if %} ([60d2f78](https://github.com/osteele/liquid/commit/60d2f78)) -* Implement {% unless %} ([6a06665](https://github.com/osteele/liquid/commit/6a06665)) -* Implement <=, >=, contains ([6c56efd](https://github.com/osteele/liquid/commit/6c56efd)) -* Implement a big chunk of filters ([1630af7](https://github.com/osteele/liquid/commit/1630af7)) -* Implement a[n] ([555991c](https://github.com/osteele/liquid/commit/555991c)) -* Implement and, or, != ([a5a3ad2](https://github.com/osteele/liquid/commit/a5a3ad2)) -* Implement ar.first, ar.list ([c648a70](https://github.com/osteele/liquid/commit/c648a70)) -* Implement booleans ([6af4fca](https://github.com/osteele/liquid/commit/6af4fca)) -* Implement break, continue ([a1784cd](https://github.com/osteele/liquid/commit/a1784cd)) -* Implement capture tag ([055e789](https://github.com/osteele/liquid/commit/055e789)) -* Implement case (w/out else) ([c5e7e6c](https://github.com/osteele/liquid/commit/c5e7e6c)) -* Implement comment tag ([eb7a18e](https://github.com/osteele/liquid/commit/eb7a18e)) -* Implement date formats ([61b651c](https://github.com/osteele/liquid/commit/61b651c)) -* Implement drops ([ba874de](https://github.com/osteele/liquid/commit/ba874de)) -* Implement expression scanner ([57be549](https://github.com/osteele/liquid/commit/57be549)) -* Implement filters: default; date (w/out format) ([d849e74](https://github.com/osteele/liquid/commit/d849e74)) -* Implement forloop variables ([e9c35a3](https://github.com/osteele/liquid/commit/e9c35a3)) -* Implement include ([fab31d9](https://github.com/osteele/liquid/commit/fab31d9)) -* Implement loop modifiers ([53a41f3](https://github.com/osteele/liquid/commit/53a41f3)) -* Implement loop reversed ([383db45](https://github.com/osteele/liquid/commit/383db45)) -* Implement loop tag ([babfc3e](https://github.com/osteele/liquid/commit/babfc3e)) -* Implement obj['name'] ([63e2c5c](https://github.com/osteele/liquid/commit/63e2c5c)) -* Implement raw tag ([c09652b](https://github.com/osteele/liquid/commit/c09652b)) -* Implement remaining numeric filters ([5ec1f66](https://github.com/osteele/liquid/commit/5ec1f66)) -* Implement some filters ([30211ac](https://github.com/osteele/liquid/commit/30211ac)) -* Implement sort: key ([612f456](https://github.com/osteele/liquid/commit/612f456)) -* Implement string literals (without escapes) ([ed150c5](https://github.com/osteele/liquid/commit/ed150c5)) -* Implement uniq filter ([585cc5d](https://github.com/osteele/liquid/commit/585cc5d)) -* Implement variable assignment ([cd15950](https://github.com/osteele/liquid/commit/cd15950)) -* Improve docs ([a077502](https://github.com/osteele/liquid/commit/a077502)) -* Improve some internal names ([1da9d40](https://github.com/osteele/liquid/commit/1da9d40)) -* Initial ([58395a8](https://github.com/osteele/liquid/commit/58395a8)) -* lint ([a824673](https://github.com/osteele/liquid/commit/a824673)) -* Lint ([e71bc95](https://github.com/osteele/liquid/commit/e71bc95)) -* Lint ([09d3650](https://github.com/osteele/liquid/commit/09d3650)) -* Lint ([c4bd99b](https://github.com/osteele/liquid/commit/c4bd99b)) -* Lint; remove dead code ([fb26bb3](https://github.com/osteele/liquid/commit/fb26bb3)) -* make install-dev-tools doesn't update packages ([9714544](https://github.com/osteele/liquid/commit/9714544)) -* Makefile default target is ci ([3dba4ee](https://github.com/osteele/liquid/commit/3dba4ee)) -* Match print object to observed ([d924e0b](https://github.com/osteele/liquid/commit/d924e0b)) -* Match split filter to observed ([6a8127a](https://github.com/osteele/liquid/commit/6a8127a)) -* More filters ([910d4b2](https://github.com/osteele/liquid/commit/910d4b2)) -* More filters ([c433c08](https://github.com/osteele/liquid/commit/c433c08)) -* More generic.Less; tests ([43bedef](https://github.com/osteele/liquid/commit/43bedef)) -* More time formats ([2f0f6ba](https://github.com/osteele/liquid/commit/2f0f6ba)) -* Move assign tag -> tags package ([d31fe04](https://github.com/osteele/liquid/commit/d31fe04)) -* Move chunk marshalling to separate file ([b367592](https://github.com/osteele/liquid/commit/b367592)) -* Move chunks -> render ([6161e6d](https://github.com/osteele/liquid/commit/6161e6d)) -* Move chunks to sub-package ([2e61304](https://github.com/osteele/liquid/commit/2e61304)) -* Move expression parser to sub-package ([373b2fb](https://github.com/osteele/liquid/commit/373b2fb)) -* Move expressions -> expression ([9691dc2](https://github.com/osteele/liquid/commit/9691dc2)) -* Move filters to own package ([4189f03](https://github.com/osteele/liquid/commit/4189f03)) -* Move generics -> evaluator ([a434a75](https://github.com/osteele/liquid/commit/a434a75)) -* Move generics to own package ([f52d00f](https://github.com/osteele/liquid/commit/f52d00f)) -* Move tag compilation to compiler stage ([54e840c](https://github.com/osteele/liquid/commit/54e840c)) -* Move tags to own package ([83503a1](https://github.com/osteele/liquid/commit/83503a1)) -* Negative integer indexes from end of list ([c1fd00c](https://github.com/osteele/liquid/commit/c1fd00c)) -* New top-level Context wrapper ([d6bc456](https://github.com/osteele/liquid/commit/d6bc456)) -* Optional filter arguments declared as functions ([8397c5e](https://github.com/osteele/liquid/commit/8397c5e)) -* Parse control tag forms at parse time ([5dddabe](https://github.com/osteele/liquid/commit/5dddabe)) -* Parse object expressions during parse stage; report error source ([d4c895d](https://github.com/osteele/liquid/commit/d4c895d)) -* Rationalize some filenames ([c4ff3d2](https://github.com/osteele/liquid/commit/c4ff3d2)) -* README ([d29e4b2](https://github.com/osteele/liquid/commit/d29e4b2)) -* README ([c67d027](https://github.com/osteele/liquid/commit/c67d027)) -* README links to godoc ([a4b1835](https://github.com/osteele/liquid/commit/a4b1835)) -* Record source line number ([08fcc4e](https://github.com/osteele/liquid/commit/08fcc4e)) -* remove a debug print ([e332e53](https://github.com/osteele/liquid/commit/e332e53)) -* Remove else/elsif from unless ([12045b5](https://github.com/osteele/liquid/commit/12045b5)) -* Remove gratuitous Context wrapper ([cb8911a](https://github.com/osteele/liquid/commit/cb8911a)) -* Rename ([594ec99](https://github.com/osteele/liquid/commit/594ec99)) -* Rename chunk -> token ([69d26a2](https://github.com/osteele/liquid/commit/69d26a2)) -* Rename render.(Context,RenderContext) -> (NodeContext,Context) ([411a2f0](https://github.com/osteele/liquid/commit/411a2f0)) -* Rename renderError -> render.Error ([315af1a](https://github.com/osteele/liquid/commit/315af1a)) -* Rename Settings -> Config ([405c5bf](https://github.com/osteele/liquid/commit/405c5bf)) -* Rename some files ([bcef4dc](https://github.com/osteele/liquid/commit/bcef4dc)) -* Rename to match Liquid terminology ([2e8f51a](https://github.com/osteele/liquid/commit/2e8f51a)) -* Render tree is distinct type from parse AST ([803471c](https://github.com/osteele/liquid/commit/803471c)) -* Render uses a switch instead of polymorphism ([0559730](https://github.com/osteele/liquid/commit/0559730)) -* Renderers return a string, rather than taking an io.writer ([8d9df82](https://github.com/osteele/liquid/commit/8d9df82)) -* Replace GetVariableMap -> UpdateBindings, RenderFile ([a7cbb9b](https://github.com/osteele/liquid/commit/a7cbb9b)) -* Restore tag tests ([db5a3af](https://github.com/osteele/liquid/commit/db5a3af)) -* Separate interface.go from engine.go ([ebc29dc](https://github.com/osteele/liquid/commit/ebc29dc)) -* simplify ([af95c44](https://github.com/osteele/liquid/commit/af95c44)) -* simplify ([846987d](https://github.com/osteele/liquid/commit/846987d)) -* simplify ([c599761](https://github.com/osteele/liquid/commit/c599761)) -* Simplify external tag interface ([f6c4299](https://github.com/osteele/liquid/commit/f6c4299)) -* slice, truncate use runes not bytes ([a3c646c](https://github.com/osteele/liquid/commit/a3c646c)) -* SortByProperty can sort nil first or last ([e2fd3bb](https://github.com/osteele/liquid/commit/e2fd3bb)) -* Split package render->parser ([903acb8](https://github.com/osteele/liquid/commit/903acb8)) -* Start to separate parser and compiler ([c7d9af2](https://github.com/osteele/liquid/commit/c7d9af2)) -* Tags are an engine configuration ([e6f8eac](https://github.com/osteele/liquid/commit/e6f8eac)) -* Tags are called within a RenderContext ([41da3f9](https://github.com/osteele/liquid/commit/41da3f9)) -* tavis uses makefile lint ([8f148dc](https://github.com/osteele/liquid/commit/8f148dc)) -* tests ([d435cf5](https://github.com/osteele/liquid/commit/d435cf5)) -* Uh-oh – strftime gets the day of week wrong! ([25e97ed](https://github.com/osteele/liquid/commit/25e97ed)) -* Un-export ControlTagDefinition; create builder ([0c7a8d2](https://github.com/osteele/liquid/commit/0c7a8d2)) -* Unconfuse unless/endunless ([9b8da4f](https://github.com/osteele/liquid/commit/9b8da4f)) -* Undefined tags, filters are errors not panics ([9a807d0](https://github.com/osteele/liquid/commit/9a807d0)) -* Update Contributing to point to the project boards ([dd41a36](https://github.com/osteele/liquid/commit/dd41a36)) -* Update guidelines to refer to issues board ([aad76bd](https://github.com/osteele/liquid/commit/aad76bd)) -* Use C strptime to format dates ([247bec3](https://github.com/osteele/liquid/commit/247bec3)) -* Work around missing %-H in strftime ([fc227aa](https://github.com/osteele/liquid/commit/fc227aa)) -* Yacc expression parsing ([9c64c5a](https://github.com/osteele/liquid/commit/9c64c5a)) -* Yacc, ragel source match package moves ([a7a1ee5](https://github.com/osteele/liquid/commit/a7a1ee5)) +* "contains" tests for arrays too ([24d83f1](https://github.com/autopilot3/liquid/commit/24d83f1)) +* (Some) relationship operators ([d03efed](https://github.com/autopilot3/liquid/commit/d03efed)) +* a.b syntax ([5dbd972](https://github.com/autopilot3/liquid/commit/5dbd972)) +* a[b] for invalid a or b ([50d11a6](https://github.com/autopilot3/liquid/commit/50d11a6)) +* Add a Grammar interface for parsing ([f313e6f](https://github.com/autopilot3/liquid/commit/f313e6f)) +* Add a Travis file ([8e673ac](https://github.com/autopilot3/liquid/commit/8e673ac)) +* Add an executable ([f1b2777](https://github.com/autopilot3/liquid/commit/f1b2777)) +* Add an type filter; inspect is more robust ([3a1506b](https://github.com/autopilot3/liquid/commit/3a1506b)) +* Add contribution guidelines ([1b7564d](https://github.com/autopilot3/liquid/commit/1b7564d)) +* Add coverage status ([6297319](https://github.com/autopilot3/liquid/commit/6297319)) +* Add docs; remove unused UnimplementedError ([983b9f5](https://github.com/autopilot3/liquid/commit/983b9f5)) +* Add engine.DefineControlTag; currently does nothing ([8f7bcae](https://github.com/autopilot3/liquid/commit/8f7bcae)) +* Add error line number ([4472b15](https://github.com/autopilot3/liquid/commit/4472b15)) +* Add goveralls to travis ([c415f89](https://github.com/autopilot3/liquid/commit/c415f89)) +* Add install-dev-toolsl to travis ([008f1ed](https://github.com/autopilot3/liquid/commit/008f1ed)) +* Add IsTemplateError ([2161bb6](https://github.com/autopilot3/liquid/commit/2161bb6)) +* Add Makefile ([29c9ad8](https://github.com/autopilot3/liquid/commit/29c9ad8)) +* Add more dependencies to credits ([97b36ab](https://github.com/autopilot3/liquid/commit/97b36ab)) +* Add more parse time formats ([77c5dc9](https://github.com/autopilot3/liquid/commit/77c5dc9)) +* Add MustConvertItem; convert bool -> int ([80d58dc](https://github.com/autopilot3/liquid/commit/80d58dc)) +* Add nil; distinguish between identifier and property patterns ([7a2b28c](https://github.com/autopilot3/liquid/commit/7a2b28c)) +* Add public DefineTag ([e21d2a7](https://github.com/autopilot3/liquid/commit/e21d2a7)) +* Add references to README ([ac12225](https://github.com/autopilot3/liquid/commit/ac12225)) +* Add RenderContext.ParseTagArgs ([7c48138](https://github.com/autopilot3/liquid/commit/7c48138)) +* Add reverse filter; improve generics ([54b9f13](https://github.com/autopilot3/liquid/commit/54b9f13)) +* Add status badges to the README ([465a681](https://github.com/autopilot3/liquid/commit/465a681)) +* Add strptime for time parsing ([8ea72e4](https://github.com/autopilot3/liquid/commit/8ea72e4)) +* Add Template.SetSourcePath ([5425668](https://github.com/autopilot3/liquid/commit/5425668)) +* Allow - in identifiers ([af8c486](https://github.com/autopilot3/liquid/commit/af8c486)) +* Allow parens ([607f4f4](https://github.com/autopilot3/liquid/commit/607f4f4)) +* Catch unimplemented panics ([b1cf056](https://github.com/autopilot3/liquid/commit/b1cf056)) +* Chunk regex wasn't sufficiently non-greedy ([f8b5503](https://github.com/autopilot3/liquid/commit/f8b5503)) +* Chunk scanner test cases ([fc6d96e](https://github.com/autopilot3/liquid/commit/fc6d96e)) +* Closure.Bind copies the original bindings ([4e96c15](https://github.com/autopilot3/liquid/commit/4e96c15)) +* Compiler copies the syntax tree ([8f63cb7](https://github.com/autopilot3/liquid/commit/8f63cb7)) +* contains operates on strings not arrays ([9dda87f](https://github.com/autopilot3/liquid/commit/9dda87f)) +* Control tag parsers can return an error ([61663ab](https://github.com/autopilot3/liquid/commit/61663ab)) +* Control tags actions are table-driven ([20e4df3](https://github.com/autopilot3/liquid/commit/20e4df3)) +* Convert -> map[string] ([767f1f4](https://github.com/autopilot3/liquid/commit/767f1f4)) +* Convert map -> [] ([6075f39](https://github.com/autopilot3/liquid/commit/6075f39)) +* Convert maps ([2fed70b](https://github.com/autopilot3/liquid/commit/2fed70b)) +* Convert returns an error; create MustConvert ([4df3f04](https://github.com/autopilot3/liquid/commit/4df3f04)) +* Coverage ([6f7b67f](https://github.com/autopilot3/liquid/commit/6f7b67f)) +* Coverage ([36929af](https://github.com/autopilot3/liquid/commit/36929af)) +* Coverage ([caca7a2](https://github.com/autopilot3/liquid/commit/caca7a2)) +* Coverage ([78526e7](https://github.com/autopilot3/liquid/commit/78526e7)) +* Coverage; simplify ([a8afb01](https://github.com/autopilot3/liquid/commit/a8afb01)) +* Create an Expression interface; add some docs ([2a2f333](https://github.com/autopilot3/liquid/commit/2a2f333)) +* Create LICENSE ([e3425cc](https://github.com/autopilot3/liquid/commit/e3425cc)) +* Create top-level interface to liquid package ([514559e](https://github.com/autopilot3/liquid/commit/514559e)) +* Docs ([f15de87](https://github.com/autopilot3/liquid/commit/f15de87)) +* Docs ([803fbbc](https://github.com/autopilot3/liquid/commit/803fbbc)) +* Embed the Chunk in the AST nodes ([089a0c8](https://github.com/autopilot3/liquid/commit/089a0c8)) +* Expression evaluator tests; fix revealed bugs ([1f805d5](https://github.com/autopilot3/liquid/commit/1f805d5)) +* Filters are an engine configuration ([2e9903f](https://github.com/autopilot3/liquid/commit/2e9903f)) +* Filters can have (single) parameters ([70aa70d](https://github.com/autopilot3/liquid/commit/70aa70d)) +* Filters support multiple argument, including expressions ([a93848a](https://github.com/autopilot3/liquid/commit/a93848a)) +* Finish generic comparison ([dbdcca4](https://github.com/autopilot3/liquid/commit/dbdcca4)) +* Fix forloop.last with offset modifier ([394036d](https://github.com/autopilot3/liquid/commit/394036d)) +* Fix generic equality with nil ([229059c](https://github.com/autopilot3/liquid/commit/229059c)) +* Fix previous ([87b8198](https://github.com/autopilot3/liquid/commit/87b8198)) +* Fix the raw tag ([e2bb7c6](https://github.com/autopilot3/liquid/commit/e2bb7c6)) +* for over a map iterates over its keys ([decd8dd](https://github.com/autopilot3/liquid/commit/decd8dd)) +* Functional is constructed within parser, not scanner ([c02fbd5](https://github.com/autopilot3/liquid/commit/c02fbd5)) +* FunctionalNode -> TagNode ([18e2540](https://github.com/autopilot3/liquid/commit/18e2540)) +* Generic Convert handles conversion to time ([2083747](https://github.com/autopilot3/liquid/commit/2083747)) +* Generics ([87708a0](https://github.com/autopilot3/liquid/commit/87708a0)) +* GitHub templates ([7bd8a8d](https://github.com/autopilot3/liquid/commit/7bd8a8d)) +* gometalinter doesn't have a config in this repo ([28db298](https://github.com/autopilot3/liquid/commit/28db298)) +* identifiers can include '-' ([606870e](https://github.com/autopilot3/liquid/commit/606870e)) +* If tag parses during parse stage ([621992c](https://github.com/autopilot3/liquid/commit/621992c)) +* Implement {% else %}, {% elsif %} ([cab7845](https://github.com/autopilot3/liquid/commit/cab7845)) +* Implement {% if %} ([60d2f78](https://github.com/autopilot3/liquid/commit/60d2f78)) +* Implement {% unless %} ([6a06665](https://github.com/autopilot3/liquid/commit/6a06665)) +* Implement <=, >=, contains ([6c56efd](https://github.com/autopilot3/liquid/commit/6c56efd)) +* Implement a big chunk of filters ([1630af7](https://github.com/autopilot3/liquid/commit/1630af7)) +* Implement a[n] ([555991c](https://github.com/autopilot3/liquid/commit/555991c)) +* Implement and, or, != ([a5a3ad2](https://github.com/autopilot3/liquid/commit/a5a3ad2)) +* Implement ar.first, ar.list ([c648a70](https://github.com/autopilot3/liquid/commit/c648a70)) +* Implement booleans ([6af4fca](https://github.com/autopilot3/liquid/commit/6af4fca)) +* Implement break, continue ([a1784cd](https://github.com/autopilot3/liquid/commit/a1784cd)) +* Implement capture tag ([055e789](https://github.com/autopilot3/liquid/commit/055e789)) +* Implement case (w/out else) ([c5e7e6c](https://github.com/autopilot3/liquid/commit/c5e7e6c)) +* Implement comment tag ([eb7a18e](https://github.com/autopilot3/liquid/commit/eb7a18e)) +* Implement date formats ([61b651c](https://github.com/autopilot3/liquid/commit/61b651c)) +* Implement drops ([ba874de](https://github.com/autopilot3/liquid/commit/ba874de)) +* Implement expression scanner ([57be549](https://github.com/autopilot3/liquid/commit/57be549)) +* Implement filters: default; date (w/out format) ([d849e74](https://github.com/autopilot3/liquid/commit/d849e74)) +* Implement forloop variables ([e9c35a3](https://github.com/autopilot3/liquid/commit/e9c35a3)) +* Implement include ([fab31d9](https://github.com/autopilot3/liquid/commit/fab31d9)) +* Implement loop modifiers ([53a41f3](https://github.com/autopilot3/liquid/commit/53a41f3)) +* Implement loop reversed ([383db45](https://github.com/autopilot3/liquid/commit/383db45)) +* Implement loop tag ([babfc3e](https://github.com/autopilot3/liquid/commit/babfc3e)) +* Implement obj['name'] ([63e2c5c](https://github.com/autopilot3/liquid/commit/63e2c5c)) +* Implement raw tag ([c09652b](https://github.com/autopilot3/liquid/commit/c09652b)) +* Implement remaining numeric filters ([5ec1f66](https://github.com/autopilot3/liquid/commit/5ec1f66)) +* Implement some filters ([30211ac](https://github.com/autopilot3/liquid/commit/30211ac)) +* Implement sort: key ([612f456](https://github.com/autopilot3/liquid/commit/612f456)) +* Implement string literals (without escapes) ([ed150c5](https://github.com/autopilot3/liquid/commit/ed150c5)) +* Implement uniq filter ([585cc5d](https://github.com/autopilot3/liquid/commit/585cc5d)) +* Implement variable assignment ([cd15950](https://github.com/autopilot3/liquid/commit/cd15950)) +* Improve docs ([a077502](https://github.com/autopilot3/liquid/commit/a077502)) +* Improve some internal names ([1da9d40](https://github.com/autopilot3/liquid/commit/1da9d40)) +* Initial ([58395a8](https://github.com/autopilot3/liquid/commit/58395a8)) +* lint ([a824673](https://github.com/autopilot3/liquid/commit/a824673)) +* Lint ([e71bc95](https://github.com/autopilot3/liquid/commit/e71bc95)) +* Lint ([09d3650](https://github.com/autopilot3/liquid/commit/09d3650)) +* Lint ([c4bd99b](https://github.com/autopilot3/liquid/commit/c4bd99b)) +* Lint; remove dead code ([fb26bb3](https://github.com/autopilot3/liquid/commit/fb26bb3)) +* make install-dev-tools doesn't update packages ([9714544](https://github.com/autopilot3/liquid/commit/9714544)) +* Makefile default target is ci ([3dba4ee](https://github.com/autopilot3/liquid/commit/3dba4ee)) +* Match print object to observed ([d924e0b](https://github.com/autopilot3/liquid/commit/d924e0b)) +* Match split filter to observed ([6a8127a](https://github.com/autopilot3/liquid/commit/6a8127a)) +* More filters ([910d4b2](https://github.com/autopilot3/liquid/commit/910d4b2)) +* More filters ([c433c08](https://github.com/autopilot3/liquid/commit/c433c08)) +* More generic.Less; tests ([43bedef](https://github.com/autopilot3/liquid/commit/43bedef)) +* More time formats ([2f0f6ba](https://github.com/autopilot3/liquid/commit/2f0f6ba)) +* Move assign tag -> tags package ([d31fe04](https://github.com/autopilot3/liquid/commit/d31fe04)) +* Move chunk marshalling to separate file ([b367592](https://github.com/autopilot3/liquid/commit/b367592)) +* Move chunks -> render ([6161e6d](https://github.com/autopilot3/liquid/commit/6161e6d)) +* Move chunks to sub-package ([2e61304](https://github.com/autopilot3/liquid/commit/2e61304)) +* Move expression parser to sub-package ([373b2fb](https://github.com/autopilot3/liquid/commit/373b2fb)) +* Move expressions -> expression ([9691dc2](https://github.com/autopilot3/liquid/commit/9691dc2)) +* Move filters to own package ([4189f03](https://github.com/autopilot3/liquid/commit/4189f03)) +* Move generics -> evaluator ([a434a75](https://github.com/autopilot3/liquid/commit/a434a75)) +* Move generics to own package ([f52d00f](https://github.com/autopilot3/liquid/commit/f52d00f)) +* Move tag compilation to compiler stage ([54e840c](https://github.com/autopilot3/liquid/commit/54e840c)) +* Move tags to own package ([83503a1](https://github.com/autopilot3/liquid/commit/83503a1)) +* Negative integer indexes from end of list ([c1fd00c](https://github.com/autopilot3/liquid/commit/c1fd00c)) +* New top-level Context wrapper ([d6bc456](https://github.com/autopilot3/liquid/commit/d6bc456)) +* Optional filter arguments declared as functions ([8397c5e](https://github.com/autopilot3/liquid/commit/8397c5e)) +* Parse control tag forms at parse time ([5dddabe](https://github.com/autopilot3/liquid/commit/5dddabe)) +* Parse object expressions during parse stage; report error source ([d4c895d](https://github.com/autopilot3/liquid/commit/d4c895d)) +* Rationalize some filenames ([c4ff3d2](https://github.com/autopilot3/liquid/commit/c4ff3d2)) +* README ([d29e4b2](https://github.com/autopilot3/liquid/commit/d29e4b2)) +* README ([c67d027](https://github.com/autopilot3/liquid/commit/c67d027)) +* README links to godoc ([a4b1835](https://github.com/autopilot3/liquid/commit/a4b1835)) +* Record source line number ([08fcc4e](https://github.com/autopilot3/liquid/commit/08fcc4e)) +* remove a debug print ([e332e53](https://github.com/autopilot3/liquid/commit/e332e53)) +* Remove else/elsif from unless ([12045b5](https://github.com/autopilot3/liquid/commit/12045b5)) +* Remove gratuitous Context wrapper ([cb8911a](https://github.com/autopilot3/liquid/commit/cb8911a)) +* Rename ([594ec99](https://github.com/autopilot3/liquid/commit/594ec99)) +* Rename chunk -> token ([69d26a2](https://github.com/autopilot3/liquid/commit/69d26a2)) +* Rename render.(Context,RenderContext) -> (NodeContext,Context) ([411a2f0](https://github.com/autopilot3/liquid/commit/411a2f0)) +* Rename renderError -> render.Error ([315af1a](https://github.com/autopilot3/liquid/commit/315af1a)) +* Rename Settings -> Config ([405c5bf](https://github.com/autopilot3/liquid/commit/405c5bf)) +* Rename some files ([bcef4dc](https://github.com/autopilot3/liquid/commit/bcef4dc)) +* Rename to match Liquid terminology ([2e8f51a](https://github.com/autopilot3/liquid/commit/2e8f51a)) +* Render tree is distinct type from parse AST ([803471c](https://github.com/autopilot3/liquid/commit/803471c)) +* Render uses a switch instead of polymorphism ([0559730](https://github.com/autopilot3/liquid/commit/0559730)) +* Renderers return a string, rather than taking an io.writer ([8d9df82](https://github.com/autopilot3/liquid/commit/8d9df82)) +* Replace GetVariableMap -> UpdateBindings, RenderFile ([a7cbb9b](https://github.com/autopilot3/liquid/commit/a7cbb9b)) +* Restore tag tests ([db5a3af](https://github.com/autopilot3/liquid/commit/db5a3af)) +* Separate interface.go from engine.go ([ebc29dc](https://github.com/autopilot3/liquid/commit/ebc29dc)) +* simplify ([af95c44](https://github.com/autopilot3/liquid/commit/af95c44)) +* simplify ([846987d](https://github.com/autopilot3/liquid/commit/846987d)) +* simplify ([c599761](https://github.com/autopilot3/liquid/commit/c599761)) +* Simplify external tag interface ([f6c4299](https://github.com/autopilot3/liquid/commit/f6c4299)) +* slice, truncate use runes not bytes ([a3c646c](https://github.com/autopilot3/liquid/commit/a3c646c)) +* SortByProperty can sort nil first or last ([e2fd3bb](https://github.com/autopilot3/liquid/commit/e2fd3bb)) +* Split package render->parser ([903acb8](https://github.com/autopilot3/liquid/commit/903acb8)) +* Start to separate parser and compiler ([c7d9af2](https://github.com/autopilot3/liquid/commit/c7d9af2)) +* Tags are an engine configuration ([e6f8eac](https://github.com/autopilot3/liquid/commit/e6f8eac)) +* Tags are called within a RenderContext ([41da3f9](https://github.com/autopilot3/liquid/commit/41da3f9)) +* tavis uses makefile lint ([8f148dc](https://github.com/autopilot3/liquid/commit/8f148dc)) +* tests ([d435cf5](https://github.com/autopilot3/liquid/commit/d435cf5)) +* Uh-oh – strftime gets the day of week wrong! ([25e97ed](https://github.com/autopilot3/liquid/commit/25e97ed)) +* Un-export ControlTagDefinition; create builder ([0c7a8d2](https://github.com/autopilot3/liquid/commit/0c7a8d2)) +* Unconfuse unless/endunless ([9b8da4f](https://github.com/autopilot3/liquid/commit/9b8da4f)) +* Undefined tags, filters are errors not panics ([9a807d0](https://github.com/autopilot3/liquid/commit/9a807d0)) +* Update Contributing to point to the project boards ([dd41a36](https://github.com/autopilot3/liquid/commit/dd41a36)) +* Update guidelines to refer to issues board ([aad76bd](https://github.com/autopilot3/liquid/commit/aad76bd)) +* Use C strptime to format dates ([247bec3](https://github.com/autopilot3/liquid/commit/247bec3)) +* Work around missing %-H in strftime ([fc227aa](https://github.com/autopilot3/liquid/commit/fc227aa)) +* Yacc expression parsing ([9c64c5a](https://github.com/autopilot3/liquid/commit/9c64c5a)) +* Yacc, ragel source match package moves ([a7a1ee5](https://github.com/autopilot3/liquid/commit/a7a1ee5)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ea25622..5384efb0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,11 @@ Here's some ways to help: -* Select an item from the [issues list](https://github.com/osteele/liquid/issues) +* Select an item from the [issues list](https://github.com/autopilot3/liquid/issues) * Search the sources for FIXME and TODO comments. * Improve the [code coverage](https://coveralls.io/github/osteele/liquid?branch=master). -Review the [pull request template](https://github.com/osteele/liquid/blob/master/.github/PULL_REQUEST_TEMPLATE.md) before you get too far along on coding. +Review the [pull request template](https://github.com/autopilot3/liquid/blob/master/.github/PULL_REQUEST_TEMPLATE.md) before you get too far along on coding. A note on lint: `nolint: gocyclo` has been used to disable cyclomatic complexity checks on generated functions, hand-written parsers, and some of the generic interpreter functions. IMO this check isn't appropriate for those classes of functions. This isn't a license to disable cyclomatic complexity checks or lint in general. @@ -40,7 +40,7 @@ make lint ```bash godoc -http=:6060 -open http://localhost:6060/pkg/github.com/osteele/liquid/ +open http://localhost:6060/pkg/github.com/autopilot3/liquid/ ``` ### Work on the Expression Parser and Lexer diff --git a/Makefile b/Makefile index d4cf0dba..7700f67f 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ SOURCEDIR=. SOURCES := $(shell find $(SOURCEDIR) -name '*.go') LIB = liquid -PACKAGE = github.com/osteele/liquid +PACKAGE = github.com/autopilot3/liquid LDFLAGS= .DEFAULT_GOAL: ci @@ -24,17 +24,15 @@ imports: ## list imports @go list -f '{{join .Imports "\n"}}' ./... | grep -v `go list -f '{{.ImportPath}}'` | grep '\.' | sort | uniq lint: ## lint the package - gometalinter ./... --tests --deadline=5m --include=gofmt --exclude expressions/scanner.go --exclude y.go --exclude '.*_string.go' --disable=gotype --disable=interfacer + golangci-lint run @echo lint passed pre-commit: lint test ## lint and test the package setup: ## install dependencies and development tools - go get golang.org/x/tools/cmd/stringer + go install golang.org/x/tools/cmd/stringer go install golang.org/x/tools/cmd/goyacc go get -t ./... - go get github.com/alecthomas/gometalinter - gometalinter --install test: ## test the package go test ./... diff --git a/README.md b/README.md index 2e734a06..997a20e1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ It was developed for use in the [Gojekyll](https://github.com/osteele/gojekyll) `go get gopkg.in/osteele/liquid.v1` # latest snapshot -`go get -u github.com/osteele/liquid` # development version +`go get -u github.com/autopilot3/liquid` # development version ## Usage @@ -83,7 +83,7 @@ Drops have a different design from the Shopify (Ruby) implementation. A Ruby drop sets `liquid_attributes` to a list of attributes that are exposed to Liquid. A Go drop implements `ToLiquid() interface{}`, that returns a proxy object. Conventionally, the proxy is a `map` or `struct` that defines the exposed properties. -See for additional information. +See for additional information. ### Value Types @@ -141,7 +141,7 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds -| [
Oliver Steele](https://osteele.com/)
[💻](https://github.com/osteele/liquid/commits?author=osteele "Code") [📖](https://github.com/osteele/liquid/commits?author=osteele "Documentation") [🤔](#ideas-osteele "Ideas, Planning, & Feedback") [🚇](#infra-osteele "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-osteele "Reviewed Pull Requests") [⚠️](https://github.com/osteele/liquid/commits?author=osteele "Tests") | [
James Littlejohn](https://github.com/thessem)
[💻](https://github.com/osteele/liquid/commits?author=thessem "Code") [📖](https://github.com/osteele/liquid/commits?author=thessem "Documentation") [⚠️](https://github.com/osteele/liquid/commits?author=thessem "Tests") | [
nsf](http://nosmileface.ru)
[💻](https://github.com/osteele/liquid/commits?author=nsf "Code") [⚠️](https://github.com/osteele/liquid/commits?author=nsf "Tests") | +| [
Oliver Steele](https://osteele.com/)
[💻](https://github.com/autopilot3/liquid/commits?author=osteele "Code") [📖](https://github.com/autopilot3/liquid/commits?author=osteele "Documentation") [🤔](#ideas-osteele "Ideas, Planning, & Feedback") [🚇](#infra-osteele "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-osteele "Reviewed Pull Requests") [⚠️](https://github.com/autopilot3/liquid/commits?author=osteele "Tests") | [
James Littlejohn](https://github.com/thessem)
[💻](https://github.com/autopilot3/liquid/commits?author=thessem "Code") [📖](https://github.com/autopilot3/liquid/commits?author=thessem "Documentation") [⚠️](https://github.com/autopilot3/liquid/commits?author=thessem "Tests") | [
nsf](http://nosmileface.ru)
[💻](https://github.com/autopilot3/liquid/commits?author=nsf "Code") [⚠️](https://github.com/autopilot3/liquid/commits?author=nsf "Tests") | | :---: | :---: | :---: | @@ -177,14 +177,14 @@ MIT License [coveralls-url]: https://coveralls.io/r/osteele/liquid?branch=master [coveralls-svg]: https://img.shields.io/coveralls/osteele/liquid.svg?branch=master -[godoc-url]: https://godoc.org/github.com/osteele/liquid -[godoc-svg]: https://godoc.org/github.com/osteele/liquid?status.svg +[godoc-url]: https://godoc.org/github.com/autopilot3/liquid +[godoc-svg]: https://godoc.org/github.com/autopilot3/liquid?status.svg -[license-url]: https://github.com/osteele/liquid/blob/master/LICENSE +[license-url]: https://github.com/autopilot3/liquid/blob/master/LICENSE [license-svg]: https://img.shields.io/badge/license-MIT-blue.svg -[go-report-card-url]: https://goreportcard.com/report/github.com/osteele/liquid -[go-report-card-svg]: https://goreportcard.com/badge/github.com/osteele/liquid +[go-report-card-url]: https://goreportcard.com/report/github.com/autopilot3/liquid +[go-report-card-svg]: https://goreportcard.com/badge/github.com/autopilot3/liquid [travis-url]: https://travis-ci.org/osteele/liquid [travis-svg]: https://img.shields.io/travis/osteele/liquid.svg?branch=master diff --git a/cmd/liquid/main.go b/cmd/liquid/main.go index 2b59d00c..f96ea078 100644 --- a/cmd/liquid/main.go +++ b/cmd/liquid/main.go @@ -4,19 +4,18 @@ // // Examples: // -// echo '{{ "Hello " | append: "World" }}' | liquid -// liquid source.tpl +// echo '{{ "Hello " | append: "World" }}' | liquid +// liquid source.tpl package main import ( "bytes" "fmt" "io" - "io/ioutil" "os" "strings" - "github.com/osteele/liquid" + "github.com/autopilot3/liquid" ) // for testing @@ -49,7 +48,7 @@ func run(args []string) error { usage() exit(1) case len(args) == 1: - s, err := ioutil.ReadFile(args[0]) + s, err := os.ReadFile(args[0]) if err != nil { return err } diff --git a/cmd/liquid/main_test.go b/cmd/liquid/main_test.go index 15caf6a3..67680b6e 100644 --- a/cmd/liquid/main_test.go +++ b/cmd/liquid/main_test.go @@ -3,7 +3,10 @@ package main import ( "bytes" "testing" + "time" + "github.com/autopilot3/ap3-types-go/types/date" + "github.com/autopilot3/liquid" "github.com/stretchr/testify/require" ) @@ -46,3 +49,110 @@ func TestMain(t *testing.T) { require.NoError(t, run([]string{"file1", "file2"})) require.Equal(t, 1, exitCode) } + +func TestRenderAllowedTags(t *testing.T) { + + bindings := map[string]interface{}{ + "people": map[string]interface{}{ + "name": "bob", + }, + } + tests := []struct { + name string + allowTagsWithDefault bool + src string + expected string + }{ + { + "Allow name only", + false, + "Hello {{ people.name | default: 'there' }}, your email is {{ people.email }}! {% if people.random == '123' %} you can't see me {% endif %}", + "Hello bob, your email is {{ people.email }}! {% if people.random == '123' %} you can't see me {% endif %}", + }, + { + "Allow name only, others have default", + false, + "Hello {{ people.name | default: 'there' }}, your email is {{ people.email | default: 'unknown' }}!", + "Hello bob, your email is {{ people.email | default: 'unknown' }}!", + }, + { + "Allow name and default", + true, + "Hello {{ people.name | default: 'there' }}, your email is {{ people.email | default: 'unknown' }}!", + "Hello bob, your email is unknown!", + }, + { + "Allow name and default", + true, + "Hello {{ people.name | default: 'there' }}, your email is {{ people.email | default: 'unknown' }}!{% if people.random == '123' %} you can't see me.{% endif %}", + "Hello bob, your email is unknown!{% if people.random == '123' %} you can't see me.{% endif %}", + }, + } + for _, tt := range tests { + engine := liquid.NewEngine() + engine.SetAllowedTags(map[string]struct{}{ + "people.name": {}, + }) + if tt.allowTagsWithDefault { + engine.AllowedTagsWithDefault() + } + tmpl, err := engine.ParseString(tt.src) + if err != nil { + t.Fatal(err) + } + t.Run(tt.name, func(t *testing.T) { + out, err := tmpl.RenderString(bindings) + if err != nil { + t.Fatal(err) + } + if out != tt.expected { + t.Errorf("TestRenderAllowedTags() = %v, want %v", out, tt.expected) + } + }) + } +} + +func TestDateFormat(t *testing.T) { + + bDate, _ := date.New(1994, 4, 28, "UTC") + bindings := map[string]interface{}{ + "people": map[string]interface{}{ + "birthday": bDate, + }, + } + tests := []struct { + name string + v interface{} + src string + expected string + }{ + { + "date", + bDate, + "{{ people.birthday | dateFormatOrDefault: 'dmy' | default: '0001-01-01' }}", + "28/04/1994", + }, + { + "time", + time.Date(1994, time.April, 28, 0, 0, 0, 0, time.UTC), + "{{ people.birthday | dateFormatOrDefault: 'dmy' | default: '0001-01-01' }}", + "28/04/1994", + }, + } + for _, tt := range tests { + engine := liquid.NewEngine() + tmpl, err := engine.ParseString(tt.src) + if err != nil { + t.Fatal(err) + } + t.Run(tt.name, func(t *testing.T) { + out, err := tmpl.RenderString(bindings) + if err != nil { + t.Fatal(err) + } + if out != tt.expected { + t.Errorf("TestRenderAllowedTags() = %v, want %v", out, tt.expected) + } + }) + } +} diff --git a/customtags/tags.go b/customtags/tags.go new file mode 100644 index 00000000..70509d64 --- /dev/null +++ b/customtags/tags.go @@ -0,0 +1,79 @@ +package customtags + +import ( + "fmt" + + crmtypes "github.com/autopilot3/ap3-crm-api-go/services/types" +) + +const ( + TagDisplayTypeDate string = "date" + TagDisplayTypeTime string = "time" + TagDisplayTypeBool string = "bool" + TagDisplayTypeCurrency string = "currency" + TagDisplayTypeDecimal string = "decimal" + TagDisplayTypeAggregate string = "aggregate" + TagDisplayTypePhone string = "phone" +) + +type TagType string + +const ( + TagTypeMergeTag TagType = "merge" + TagTypeMergeText TagType = "text" +) + +type Tag struct { + ID string `json:"id" bson:"id"` + Type TagType `json:"type" bson:"type"` + Icon string `json:"icon" bson:"icon"` + Title string `json:"title" bson:"title"` + DisplayType string `json:"display_type" bson:"display_type"` + LiquidName string `json:"liquid_name" bson:"liquid_name"` + DefaultValue string `json:"default_value" bson:"default_value"` + FormatOption string `json:"format_option" bson:"format_option"` + FieldID crmtypes.FieldID `json:"field_id" bson:"field_id"` + ActivityID crmtypes.FieldID `json:"activity_id" bson:"activity_id"` +} + +func (t *Tag) GetPreviewString() (string, error) { + if t.Type == TagTypeMergeText { + return t.Title, nil + } + if t.Type == TagTypeMergeTag { + return fmt.Sprintf("[%s]", t.Title), nil + } + return "", fmt.Errorf("unsupported subject merge tag %+v type: %s", t, t.Type) +} + +func (t *Tag) GetLiquidString() (string, error) { + if t.Type == TagTypeMergeText { + return t.Title, nil + } + if t.Type == TagTypeMergeTag { + switch t.DisplayType { + case TagDisplayTypeDate: + return fmt.Sprintf(`{{ %s | dateFormatOrDefault: "%s", "%s" }}`, t.LiquidName, t.FormatOption, t.DefaultValue), nil + case TagDisplayTypeTime: + return fmt.Sprintf(`{{ %s | dateTimeFormatOrDefault: "%s", "%s" }}`, t.LiquidName, t.FormatOption, t.DefaultValue), nil + case TagDisplayTypeBool: + return fmt.Sprintf(`{{ %s | booleanFormat: "%s" }}`, t.LiquidName, t.FormatOption), nil + case TagDisplayTypeCurrency, TagDisplayTypeDecimal, TagDisplayTypeAggregate: + return fmt.Sprintf(`{{ %s | decimal: "%s", "%s" }}`, t.LiquidName, t.FormatOption, t.DefaultValue), nil + case TagDisplayTypePhone: + willHide := false + if t.FormatOption == "hide" { + willHide = true + } + return fmt.Sprintf(`{{ %s | hideCountryCodeAndDefault: %t, "%s" }}`, t.LiquidName, willHide, t.DefaultValue), nil + default: + result := `{{ ` + t.LiquidName + if t.DefaultValue != "" { + result += ` | default: "` + t.DefaultValue + `"` + } + result += ` }}` + return result, nil + } + } + return "", fmt.Errorf("unsupported subject merge tag %+v type: %s", t, t.Type) +} diff --git a/engine.go b/engine.go index d1433648..b281a6c9 100644 --- a/engine.go +++ b/engine.go @@ -1,11 +1,25 @@ package liquid import ( + "context" + "fmt" + "html" "io" + "reflect" + "strconv" + "strings" + "time" - "github.com/osteele/liquid/filters" - "github.com/osteele/liquid/render" - "github.com/osteele/liquid/tags" + "github.com/autopilot3/ap3-helpers-go/logger" + "github.com/autopilot3/ap3-types-go/types/date" + "github.com/autopilot3/ap3-types-go/types/phone" + "github.com/autopilot3/liquid/filters" + "github.com/autopilot3/liquid/render" + "github.com/autopilot3/liquid/tags" + + "github.com/bojanz/currency" + "golang.org/x/text/language" + "golang.org/x/text/message" ) // An Engine parses template source into renderable text. @@ -13,12 +27,359 @@ import ( // An engine can be configured with additional filters and tags. type Engine struct{ cfg render.Config } -// NewEngine returns a new Engine. +func (e *Engine) SetAllowedTags(allowedTags map[string]struct{}) *Engine { + e.cfg.AllowedTags = allowedTags + return e +} +func (e *Engine) AllowedTagsWithDefault() *Engine { + e.cfg.AllowTagsWithDefault = true + return e +} + func NewEngine() *Engine { - e := Engine{render.NewConfig()} - filters.AddStandardFilters(&e.cfg) - tags.AddStandardTags(e.cfg) - return &e + return NewEngineWithContext(context.Background()) +} + +// NewEngine returns a new Engine. +func NewEngineWithContext(ctx context.Context) *Engine { + engine := &Engine{render.NewConfigWitchContext(ctx)} + filters.AddStandardFilters(&engine.cfg) + tags.AddStandardTags(engine.cfg) + engine.RegisterFilter("hideCountryCodeAndDefault", func(v interface{}, hide bool, defaultValue string) string { + s, ok := v.(phone.International) + if !ok { + return defaultValue + } + if s.Number.IsZero() && s.CountryCode.IsZero() { + return defaultValue + } + if hide { + return s.Number.String() + } + return s.String() + }) + + engine.RegisterFilter("timeInTimezone", func(s time.Time, timezone string, format string) string { + tz, err := time.LoadLocation(timezone) + if err != nil { + return "" + } + switch format { + case "mdy12": + return s.In(tz).Format("Jan 02 2006 3:04 PM") + case "mdy24": + return s.In(tz).Format("Jan 02 2006 15:04") + case "dmy12": + return s.In(tz).Format("02 Jan 2006 3:04 PM") + case "dmy24": + return s.In(tz).Format("02 Jan 2006 15:04") + case "ymd12": + return s.Format("2006 Jan 02 3:04 PM") + case "ymd24": + return s.Format("2006 Jan 02 15:04") + case "ydm12": + return s.Format("2006 02 Jan 3:04 PM") + case "ydm24": + return s.Format("2006 02 Jan 15:04") + default: + return s.String() + } + }) + + engine.RegisterFilter("rawPhone", func(s phone.International) string { + return s.CountryCode.String() + s.Number.String() + }) + + engine.RegisterFilter("dateTimeFormatOrDefault", func(s time.Time, format string, defaultValue string) string { + if s.IsZero() { + return defaultValue + } + + switch format { + case "mdy12": + return s.Format("Jan 02 2006 3:04 PM") + case "mdy24": + return s.Format("Jan 02 2006 15:04") + case "dmy12": + return s.Format("02 Jan 2006 3:04 PM") + case "dmy24": + return s.Format("02 Jan 2006 15:04") + case "ymd12": + return s.Format("2006 Jan 02 3:04 PM") + case "ymd24": + return s.Format("2006 Jan 02 15:04") + case "ydm12": + return s.Format("2006 02 Jan 3:04 PM") + case "ydm24": + return s.Format("2006 02 Jan 15:04") + default: + return s.String() + } + }) + + engine.RegisterFilter("dateFormatOrDefault", func(s interface{}, format string, defaultValue string) string { + var ( + d date.Date + err error + ) + switch s := s.(type) { + case date.Date: + d = s + case time.Time: + d, err = date.NewFromTime(s) + if err != nil { + return defaultValue + } + } + if d.IsZero() { + return defaultValue + } + + switch format { + case "mdy": + return fmt.Sprintf("%02d/%02d/%d", d.Month(), d.Day(), d.Year()) + case "dmy": + return fmt.Sprintf("%02d/%02d/%d", d.Day(), d.Month(), d.Year()) + case "ymd": + return fmt.Sprintf("%d/%02d/%02d", d.Year(), d.Month(), d.Day()) + case "ydm": + return fmt.Sprintf("%d/%02d/%02d", d.Year(), d.Day(), d.Month()) + default: + return d.String() + } + }) + + engine.RegisterFilter("decimal", func(s string, format string, currency string) string { + if s == "" { + return s + } + num, err := strconv.ParseFloat(s, 64) + if err != nil { + logger.Warnw(engine.cfg.Context(), fmt.Sprintf("failed to parse field value %s to decimal: %s", s, err.Error()), "lqiuid", "filter") + return s + } + var formatTemplate string + switch format { + case "whole": + formatTemplate = "%.0f" + case "one": + formatTemplate = "%.1f" + case "two": + formatTemplate = "%.2f" + default: + formatTemplate = "%.2f" + } + + p := message.NewPrinter(language.English) + value := p.Sprintf(formatTemplate, float64(num)/1000) + if currency != "" { + return currency + value + } + + return value + }) + + engine.RegisterFilter("decimalWithDelimiter", func(s string, format string, currencyCode string, loc string) string { + if s == "" { + return s + } + num, err := strconv.ParseFloat(s, 64) + if err != nil { + logger.Warnw(engine.cfg.Context(), fmt.Sprintf("failed to parse field value %s to decimal: %s", s, err.Error()), "lqiuid", "filter") + return s + } + + if len(currencyCode) == 3 { // iso code + amount, err := currency.NewAmount(fmt.Sprintf("%.3f", num/1000), currencyCode) + if err == nil { + locale := currency.NewLocale(loc) + formatter := currency.NewFormatter(locale) + + switch format { + case "whole": + formatter.MaxDigits = 0 + case "one": + formatter.MaxDigits = 1 + case "two": + formatter.MaxDigits = 2 + default: + formatter.MaxDigits = 2 + } + return formatter.Format(amount) + } else { + // log and fallback to the previous logic + logger.Warnw(engine.cfg.Context(), fmt.Sprintf("failed to parse field value %s with currency code %s to decimal: %s", s, currencyCode, err.Error()), "lqiuid", "filter") + } + } + var formatTemplate string + switch format { + case "whole": + formatTemplate = "%.0f" + case "one": + formatTemplate = "%.1f" + case "two": + formatTemplate = "%.2f" + default: + formatTemplate = "%.2f" + } + + tag, err := language.Parse(loc) + if err != nil { + tag = language.English + } + p := message.NewPrinter(tag) + val := p.Sprintf(formatTemplate, num/1000) + if currencyCode != "" { + return currencyCode + val + } + return val + }) + + engine.RegisterFilter("numberWithDelimiter", func(s string, loc string, format string) string { + if s == "" { + return s + } + num, err := strconv.ParseFloat(s, 64) + if err != nil { + logger.Warnw(engine.cfg.Context(), fmt.Sprintf("failed to parse field value %s to decimal: %s", s, err.Error()), "lqiuid", "filter") + return s + } + var formatTemplate string + switch format { + case "whole": + formatTemplate = "%.0f" + case "one": + formatTemplate = "%.1f" + case "two": + formatTemplate = "%.2f" + default: + formatTemplate = "%.2f" + } + + tag, err := language.Parse(loc) + if err != nil { + tag = language.English + } + p := message.NewPrinter(tag) + value := p.Sprintf(formatTemplate, float64(num)) + + return value + }) + + engine.RegisterFilter("booleanFormat", func(s string, format string) string { + if s == "" { + return "" + } + var b bool + if s == "true" { + b = true + } + if format == "yesNo" { + if b { + return "Yes" + } + return "No" + } + if format == "onOff" { + if b { + return "On" + } + return "Off" + } + if b { + return "True" + } + return "False" + }) + + // a set [a,b,c] contains at least one of matches, [a,d] will return true in this case + engine.RegisterFilter("setContains", func(s interface{}, matches ...interface{}) bool { + if s == nil { + return false + } + switch k := reflect.TypeOf(s).Kind(); k { + case reflect.String: + str := s.(string) + splits := strings.Split(str, ",") + for _, match := range matches { + for _, s := range splits { + if s == match { + return true + } + } + } + return false + case reflect.Slice: + val := reflect.ValueOf(s) + for i := 0; i < val.Len(); i++ { + elem := val.Index(i).Interface() + for _, match := range matches { + if reflect.DeepEqual(elem, match) { + return true + } + } + } + return false + } + return false + }) + + // a set [a,b,c] contains all matches, [a,d] will return false in this case, [a,c] will return true + engine.RegisterFilter("setContainsAll", func(s interface{}, matches ...interface{}) bool { + if s == nil { + return false + } + switch k := reflect.TypeOf(s).Kind(); k { + case reflect.String: + str := s.(string) + splits := strings.Split(str, ",") + for _, match := range matches { + containMatch := false + for _, s := range splits { + if s == match { + containMatch = true + break + } + } + if !containMatch { + return false + } + } + return true + case reflect.Slice: + val := reflect.ValueOf(s) + for i := 0; i < val.Len(); i++ { + elem := val.Index(i).Interface() + containMatch := false + for _, match := range matches { + if reflect.DeepEqual(elem, match) { + containMatch = true + break + } + } + if !containMatch { + return false + } + } + return true + } + return false + }) + + // trackURL here is a dummy filter, it is used to avoid error when parsing liquid template. Services support this filter will replace it with real filter + engine.RegisterFilter("trackURL", func(s string) string { + return "$$TRACK_ME:" + html.UnescapeString(s) + "$$" + }) + + engine.RegisterFilter("startsWith", func(s string, prefix string) bool { + return strings.HasPrefix(s, prefix) + }) + + engine.RegisterFilter("endsWith", func(s string, suffix string) bool { + return strings.HasSuffix(s, suffix) + }) + + return engine } // RegisterBlock defines a block e.g. {% tag %}…{% endtag %}. @@ -40,10 +401,9 @@ func (e *Engine) RegisterBlock(name string, td Renderer) { // // Examples: // -// * https://github.com/osteele/liquid/blob/master/filters/filters.go +// * https://github.com/autopilot3/liquid/blob/master/filters/filters.go // // * https://github.com/osteele/gojekyll/blob/master/filters/filters.go -// func (e *Engine) RegisterFilter(name string, fn interface{}) { e.cfg.AddFilter(name, fn) } diff --git a/engine_examples_test.go b/engine_examples_test.go index 81607129..1fd8b683 100644 --- a/engine_examples_test.go +++ b/engine_examples_test.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/render" ) func Example() { diff --git a/engine_test.go b/engine_test.go index 4a56258d..f51d52e3 100644 --- a/engine_test.go +++ b/engine_test.go @@ -2,9 +2,11 @@ package liquid import ( "bytes" + "encoding/json" "fmt" "io" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -17,14 +19,20 @@ var liquidTests = []struct{ in, expected string }{ {`{{ page.title }}`, "Introduction"}, {`{% if x %}true{% endif %}`, "true"}, {`{{ "upper" | upcase }}`, "UPPER"}, + {`{{ page.ar | first }}`, "first"}, + {`{% if page.nil-number < x %}true{% endif %}`, "true"}, } var testBindings = map[string]interface{}{ "x": 123, "ar": []string{"first", "second", "third"}, "page": map[string]interface{}{ + "ar": []interface{}{"first", "second", "third"}, "title": "Introduction", }, + "set": map[string]interface{}{ + "chars": []string{"a", "b", "c"}, + }, } func TestEngine_ParseAndRenderString(t *testing.T) { @@ -92,3 +100,177 @@ func BenchmarkEngine_Parse(b *testing.B) { engine.ParseTemplate(s) } } + +func TestCustomFilter(t *testing.T) { + // GMT Sun Dec 11 2022 14:02:03 GMT+0000 + // Your Time Zone Mon Dec 12 2022 01:02:03 GMT+1100 (Australian Eastern Daylight Time) + params := map[string]interface{}{ + "message": &map[string]interface{}{ + "created_at": time.Unix(1670767323, 0), + }, + } + engine := NewEngine() + template := "{{ message.created_at | timeInTimezone: 'Australia/Sydney', 'mdy12' }}" + str, err := engine.ParseAndRenderString(template, params) + require.NoError(t, err) + require.Equal(t, "Dec 12 2022 1:02 AM", str) + template = "{{ message.created_at | timeInTimezone: 'Asia/Shanghai', 'mdy12' }}" + str, err = engine.ParseAndRenderString(template, params) + require.NoError(t, err) + require.Equal(t, "Dec 11 2022 10:02 PM", str) +} + +func TestDateFilter(t *testing.T) { + engine := NewEngine() + template := `{% assign vardays = 30 | times: 24 | times: 60 | times: 60 %}{{ 'now' | date: "%s" | plus: vardays | date: "%d/%m/%Y" }}` + str, err := engine.ParseAndRenderString(template, nil) + require.NoError(t, err) + t.Log(str) + if len(str) == 0 { + t.Error("date filter error") + } +} + +func TestDecimalFilter(t *testing.T) { + engine := NewEngine() + template := `{{ 12345 | decimal: 'one', '$' }}` + str, err := engine.ParseAndRenderString(template, nil) + require.NoError(t, err) + t.Log(str) + if str != "$12.3" { + t.Error("decimal filter error") + } +} + +func TestDecimalWithDelimiterFilter(t *testing.T) { + engine := NewEngine() + tests := []struct { + name string + liquid string + expectedValue string + }{ + { + name: "currency symbol", + liquid: `{{ 12345 | decimalWithDelimiter: 'one', '€', 'de' }}`, + expectedValue: "€12,3", + }, + { + name: "norwegian Krone", + liquid: `{{ 12345 | decimalWithDelimiter: 'one', 'NOK', 'no-NO' }}`, + expectedValue: "12,3 kr", + }, + { + name: "us dollar in english", + liquid: `{{ 12345 | decimalWithDelimiter: 'one', 'USD', 'en' }}`, + expectedValue: "$12.3", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + str, err := engine.ParseAndRenderString(test.liquid, nil) + require.NoError(t, err) + if str != test.expectedValue { + t.Errorf("For %s, expected %s, got %s", test.name, test.expectedValue, str) + } + }) + } +} + +func TestFindVariables(t *testing.T) { + engine := NewEngine() + + tests := []struct { + name string + liquid string + expectedVars string + }{ + { + name: "2 levels loop", + liquid: `{% for company in people.companies %} + {% for instance in people.instances %} + {{ company.name }} + {{ instance.name }} + {% endfor %} + {% endfor %}`, + expectedVars: `{"people.companies":{"Loop":true,"Attributes":{"name":{"Loop":false,"Attributes":null}}},"people.instances":{"Loop":true,"Attributes":{"name":{"Loop":false,"Attributes":null}}}}`, + }, + { + name: "2 levels loop which uses var of top loop", + liquid: `{% for company in people.companies %} + {% for instance in company.instances %} + {{ company.name }} + {{ instance.name }} + {% endfor %} + {% endfor %}`, + expectedVars: `{"people.companies":{"Loop":true,"Attributes":{"instances":{"Loop":true,"Attributes":{"name":{"Loop":false,"Attributes":null}}},"name":{"Loop":false,"Attributes":null}}}}`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tmpl, err := engine.ParseString(test.liquid) + if err != nil { + t.Fatalf("Expected no err: but got %s", err) + } + vars, err := tmpl.FindVariables() + if err != nil { + t.Fatalf("Expected no FindVariables err: but got %s", err) + } + varsJSON, jerr := json.Marshal(vars) + if jerr != nil { + t.Fatalf("Expected no Marshal err: but got %s", jerr) + } + + if string(varsJSON) != test.expectedVars { + t.Errorf("Expected:\n%s\nbut got:\n%s", test.expectedVars, string(varsJSON)) + } + }) + } +} + +func TestStartsWith(t *testing.T) { + engine := NewEngine() + template := `{{ 'hello' | startsWith: 'he' }}` + str, err := engine.ParseAndRenderString(template, nil) + require.NoError(t, err) + t.Log(str) + if str != "true" { + t.Error("startsWith filter error") + } +} + +func TestEndsWith(t *testing.T) { + engine := NewEngine() + template := `{{ 'hello' | endsWith: 'lo' }}` + str, err := engine.ParseAndRenderString(template, nil) + require.NoError(t, err) + t.Log(str) + if str != "true" { + t.Error("endsWith filter error") + } +} + +func TestSetContains(t *testing.T) { + engine := NewEngine() + template := `{{ set.chars | setContains: 'a' }}` + str, err := engine.ParseAndRenderString(template, testBindings) + require.NoError(t, err) + t.Log(str) + if str != "true" { + t.Error("set contains filter error") + } + template = `{{ set.chars | setContains: 'd' }}` + str, err = engine.ParseAndRenderString(template, testBindings) + require.NoError(t, err) + t.Log(str) + if str != "false" { + t.Error("set contains filter error") + } + template = `{{ set.chars | setContains: 'a', 'b' }}` + str, err = engine.ParseAndRenderString(template, testBindings) + require.NoError(t, err) + t.Log(str) + if str != "true" { + t.Error("set contains filter error") + } +} diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 497254d4..3836b56f 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -5,7 +5,7 @@ import ( "reflect" "time" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" ) // Convert should be replaced by values.Convert. diff --git a/expressions/builders.go b/expressions/builders.go index 3f47d70f..b6b5d5da 100644 --- a/expressions/builders.go +++ b/expressions/builders.go @@ -1,7 +1,7 @@ package expressions import ( - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" ) func makeRangeExpr(startFn, endFn func(Context) values.Value) func(Context) values.Value { @@ -40,6 +40,10 @@ func makeIndexExpr(sequenceFn, indexFn func(Context) values.Value) func(Context) func makeObjectPropertyExpr(objFn func(Context) values.Value, name string) func(Context) values.Value { index := values.ValueOf(name) return func(ctx Context) values.Value { + k, ok := ctx.(*varsContext) + if ok { + k.BuildVar(name) + } return objFn(ctx).PropertyValue(index) } } diff --git a/expressions/config.go b/expressions/config.go index 6f8bc9c8..e78d46a1 100644 --- a/expressions/config.go +++ b/expressions/config.go @@ -1,11 +1,18 @@ package expressions +import gocontext "context" + // Config holds configuration information for expression interpretation. type Config struct { filters map[string]interface{} + ctx gocontext.Context +} + +func (c *Config) Context() gocontext.Context { + return c.ctx } // NewConfig creates a new Config. -func NewConfig() Config { - return Config{} +func NewConfig(ctx gocontext.Context) Config { + return Config{ctx: ctx} } diff --git a/expressions/context.go b/expressions/context.go index 4d6392ba..08958d62 100644 --- a/expressions/context.go +++ b/expressions/context.go @@ -1,6 +1,13 @@ package expressions -import "github.com/osteele/liquid/values" +import ( + "fmt" + "reflect" + "strings" + + "github.com/autopilot3/ap3-helpers-go/logger" + "github.com/autopilot3/liquid/values" +) // Context is the expression evaluation context. It maps variables names to values. type Context interface { @@ -39,3 +46,176 @@ func (c *context) Get(name string) interface{} { func (c *context) Set(name string, value interface{}) { c.bindings[name] = value } + +type varsContext struct { + Config + variables map[string]interface{} + currentVars []string +} + +// NewContext makes a new expression evaluation context. +func NewVariablesContext(vars map[string]interface{}, cfg Config) Context { + return &varsContext{ + Config: cfg, + variables: vars, + } +} + +func (c *varsContext) BuildVar(name string) { + c.currentVars = append(c.currentVars, name) +} + +func (c *varsContext) Clone() Context { + return c +} + +const LatestVarNameKey = "$LATEST$" +const LoopVarsKey = "$LOOP_VARS$" +const AssignedVarsKey = "$ASSIGNED_VARS$" + +type VariableBind struct { + Loop bool + Attributes map[string]*VariableBind +} + +type LoopVar struct { + Name string + Source string +} + +type LoopVarsStack struct { + LastKey int + Vars map[int]LoopVar +} + +func (l *LoopVarsStack) Set(name string, source string) int { + key := l.LastKey + l.Vars[key] = LoopVar{ + Name: name + ".", + Source: source, + } + l.LastKey++ + return key +} + +func (l *LoopVarsStack) Remove(key int) { + delete(l.Vars, key) +} + +func NewLoopVars() *LoopVarsStack { + return &LoopVarsStack{ + Vars: make(map[int]LoopVar), + } +} + +// Get looks up a variable value in the expression context. +func (c *varsContext) Get(name string) interface{} { + if len(c.currentVars) > 0 { + for idx := len(c.currentVars) - 1; idx >= 0; idx-- { + name += "." + c.currentVars[idx] + } + c.currentVars = c.currentVars[:0] + } + if loopVars, ok := c.variables[LoopVarsKey]; ok { + loopVars := loopVars.(*LoopVarsStack) + for _, loopVar := range loopVars.Vars { + if strings.HasPrefix(name, loopVar.Name) { + var bind *VariableBind + attributeName := strings.TrimPrefix(name, loopVar.Name) + // only 2 levels of array is allowed + objArrays := strings.SplitN(loopVar.Source, "[]", 2) + if len(objArrays) > 1 { + if val, ok := c.variables[objArrays[0]]; ok { + bind, ok = val.(*VariableBind) + if !ok { + if _, ok := val.(string); ok { // assigned variable + bind = &VariableBind{} + c.variables[objArrays[0]] = bind + } else { + logger.Errorw(c.ctx, fmt.Sprintf("Error variable %s is not of VariableBind type %T: variables: %+v, current varibale %+v, config filter %+v,", objArrays[0], val, c.variables, c.currentVars, c.Config.filters), "Get", "context") + continue + } + } + attr, ok := bind.Attributes[strings.TrimPrefix(objArrays[1], ".")] + if ok { + if attr.Attributes == nil { + attr.Attributes = make(map[string]*VariableBind) + } + attr.Loop = true + attr.Attributes[attributeName] = &VariableBind{} + } + } + } else { + if val, ok := c.variables[loopVar.Source]; !ok || val == nil { + bind = &VariableBind{ + Loop: true, + Attributes: make(map[string]*VariableBind), + } + c.variables[loopVar.Source] = bind + } else { + bind, ok = val.(*VariableBind) + if !ok { + if _, ok := val.(string); ok { // assigned variable + bind = &VariableBind{} + c.variables[loopVar.Source] = bind + } else { + // it could be binded in assign before loop (% assign varoffers = activity.custom.comparison-offers.offers | default: '' %}) + logger.Errorw(c.ctx, fmt.Sprintf("Error variables %s is not of VariableBind type %T: variables: %+v, current varibale %+v, config filter %+v,", loopVar.Source, val, c.variables, c.currentVars, c.Config.filters), "Get", "context") + continue + } + } + bind.Loop = true + if bind.Attributes == nil { + bind.Attributes = make(map[string]*VariableBind) + } + } + bind.Attributes[attributeName] = &VariableBind{} + } + c.variables[LatestVarNameKey] = loopVar.Source + "[]." + attributeName + return values.ValueOf(nil) + } + } + } + if _, ok := c.variables[name]; !ok { + c.variables[name] = &VariableBind{} + } + c.variables[LatestVarNameKey] = name + return values.ValueOf(nil) +} + +// Set sets a variable value in the expression context. +func (c *varsContext) Set(name string, value interface{}) { +} + +func (ctx *varsContext) ApplyFilter(name string, receiver valueFn, params []valueFn) (interface{}, error) { + filter, ok := ctx.filters[name] + if !ok { + panic(UndefinedFilter(name)) + } + fr := reflect.ValueOf(filter) + args := []interface{}{receiver(ctx).Interface()} + for i, param := range params { + if i+1 < fr.Type().NumIn() && isClosureInterfaceType(fr.Type().In(i+1)) { + expr, err := Parse(param(ctx).Interface().(string)) + if err != nil { + panic(err) + } + args = append(args, closure{expr, ctx}) + } else { + args = append(args, param(ctx).Interface()) + } + } + out, err := values.Call(fr, args) + if err != nil { + if e, ok := err.(*values.CallParityError); ok { + err = &values.CallParityError{NumArgs: e.NumArgs - 1, NumParams: e.NumParams - 1} + } + return nil, err + } + switch out := out.(type) { + case []byte: + return string(out), nil + default: + return out, nil + } +} diff --git a/expressions/expressions.go b/expressions/expressions.go index 61d98b66..8f6fd12a 100644 --- a/expressions/expressions.go +++ b/expressions/expressions.go @@ -7,7 +7,7 @@ import ( "fmt" "runtime/debug" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" ) // TODO Expression and Closure are confusing names. diff --git a/expressions/expressions.y b/expressions/expressions.y index da46b7ba..24d63b8d 100644 --- a/expressions/expressions.y +++ b/expressions/expressions.y @@ -3,7 +3,7 @@ package expressions import ( "fmt" "math" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" ) func init() { @@ -210,13 +210,23 @@ cond: | cond AND rel { fa, fb := $1, $3 $$ = func(ctx Context) values.Value { - return values.ValueOf(fa(ctx).Test() && fb(ctx).Test()) + if _, ok := ctx.(*varsContext); ok { + a := fa(ctx).Test() + b := fb(ctx).Test() + return values.ValueOf(a && b) + } + return values.ValueOf(fa(ctx).Test() && fb(ctx).Test()) } } | cond OR rel { fa, fb := $1, $3 $$ = func(ctx Context) values.Value { - return values.ValueOf(fa(ctx).Test() || fb(ctx).Test()) + if _, ok := ctx.(*varsContext); ok { + a := fa(ctx).Test() + b := fb(ctx).Test() + return values.ValueOf(a || b) + } + return values.ValueOf(fa(ctx).Test() || fb(ctx).Test()) } } ; diff --git a/expressions/expressions_test.go b/expressions/expressions_test.go index f381c525..a236fb6e 100644 --- a/expressions/expressions_test.go +++ b/expressions/expressions_test.go @@ -1,6 +1,7 @@ package expressions import ( + gocontext "context" "errors" "fmt" "strings" @@ -23,6 +24,8 @@ var evaluatorTests = []struct { // Variables {`n`, 123}, + {`array`, []string{"first", "second", "third"}}, + {`array.first`, "first"}, // Attributes {`hash.a`, "first"}, @@ -43,9 +46,11 @@ var evaluatorTests = []struct { {`array[100]`, nil}, {`hash[1]`, nil}, {`hash.c[0]`, "r"}, + {`hash.c`, []string{"r", "g", "b"}}, // Expressions {`(n)`, 123}, + {`(array)`, []string{"first", "second", "third"}}, // Operators {`1 == 1`, true}, @@ -106,6 +111,7 @@ var evaluatorTests = []struct { var evaluatorTestBindings = (map[string]interface{}{ "n": 123, + "str_array": "first, second, third", "array": []string{"first", "second", "third"}, "interface_array": []interface{}{"first", "second", "third"}, "empty_list": []interface{}{}, @@ -119,7 +125,7 @@ var evaluatorTestBindings = (map[string]interface{}{ }) func TestEvaluateString(t *testing.T) { - cfg := NewConfig() + cfg := NewConfig(gocontext.Background()) cfg.AddFilter("length", strings.Count) ctx := NewContext(evaluatorTestBindings, cfg) for i, test := range evaluatorTests { @@ -142,7 +148,7 @@ func TestEvaluateString(t *testing.T) { } func TestClosure(t *testing.T) { - cfg := NewConfig() + cfg := NewConfig(gocontext.Background()) ctx := NewContext(map[string]interface{}{"x": 1}, cfg) expr, err := Parse("x") require.NoError(t, err) diff --git a/expressions/filters.go b/expressions/filters.go index f80ee6d5..eb79daf7 100644 --- a/expressions/filters.go +++ b/expressions/filters.go @@ -4,7 +4,7 @@ import ( "fmt" "reflect" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" ) // An InterpreterError is an error during expression interpretation. diff --git a/expressions/filters_test.go b/expressions/filters_test.go index 2a938de8..9f1f1513 100644 --- a/expressions/filters_test.go +++ b/expressions/filters_test.go @@ -1,26 +1,27 @@ package expressions import ( + gocontext "context" "fmt" "testing" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" "github.com/stretchr/testify/require" ) func TestContext_AddFilter(t *testing.T) { - cfg := NewConfig() + cfg := NewConfig(gocontext.Background()) require.NotPanics(t, func() { cfg.AddFilter("f", func(int) int { return 0 }) }) require.NotPanics(t, func() { cfg.AddFilter("f", func(int) (a int, e error) { return }) }) require.Panics(t, func() { cfg.AddFilter("f", func() int { return 0 }) }) require.Panics(t, func() { cfg.AddFilter("f", func(int) {}) }) // require.Panics(t, func() { cfg.AddFilter("f", func(int) (a int, b int) { return }) }) - require.Panics(t, func() { cfg.AddFilter("f", func(int) (a int, e error, b int) { return }) }) + require.Panics(t, func() { cfg.AddFilter("f", func(int) (a int, b int, e error) { return }) }) require.Panics(t, func() { cfg.AddFilter("f", 10) }) } func TestContext_runFilter(t *testing.T) { - cfg := NewConfig() + cfg := NewConfig(gocontext.Background()) constant := func(value interface{}) valueFn { return func(Context) values.Value { return values.ValueOf(value) } } diff --git a/expressions/functional_test.go b/expressions/functional_test.go index fe3c7232..c7c1cfe0 100644 --- a/expressions/functional_test.go +++ b/expressions/functional_test.go @@ -1,20 +1,21 @@ package expressions import ( + gocontext "context" "testing" "github.com/stretchr/testify/require" ) func TestConstant(t *testing.T) { - ctx := NewContext(map[string]interface{}{}, NewConfig()) + ctx := NewContext(map[string]interface{}{}, NewConfig(gocontext.Background())) k := Constant(10) v, err := k.Evaluate(ctx) require.NoError(t, err) require.Equal(t, 10, v) } func TestNot(t *testing.T) { - ctx := NewContext(map[string]interface{}{}, NewConfig()) + ctx := NewContext(map[string]interface{}{}, NewConfig(gocontext.Background())) k := Constant(10) v, err := Not(k).Evaluate(ctx) require.NoError(t, err) diff --git a/expressions/parser.go b/expressions/parser.go index 88c2c824..09408ccf 100644 --- a/expressions/parser.go +++ b/expressions/parser.go @@ -7,7 +7,7 @@ package expressions import ( "fmt" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" ) type parseValue struct { diff --git a/expressions/parser_test.go b/expressions/parser_test.go index 8517b272..6524b1a0 100644 --- a/expressions/parser_test.go +++ b/expressions/parser_test.go @@ -1,6 +1,7 @@ package expressions import ( + gocontext "context" "fmt" "testing" @@ -35,7 +36,7 @@ var parseErrorTests = []struct{ in, expected string }{ // Since the parser returns funcs, there's no easy way to test them except evaluation func TestParse(t *testing.T) { - cfg := NewConfig() + cfg := NewConfig(gocontext.Background()) cfg.AddFilter("add", func(a, b int) int { return a + b }) ctx := NewContext(map[string]interface{}{ "a": 1, diff --git a/expressions/scanner.go b/expressions/scanner.go index 774a1682..e1e906db 100644 --- a/expressions/scanner.go +++ b/expressions/scanner.go @@ -277,11 +277,7 @@ func (lex *lexer) Lex(out *yySymType) int { _lower := int(_keys) var _mid int _upper := int(_keys + _klen - 1) - for { - if _upper < _lower { - break - } - + for _upper >= _lower { _mid = _lower + ((_upper - _lower) >> 1) switch { case lex.data[(lex.p)] < _expression_trans_keys[_mid]: @@ -302,11 +298,7 @@ func (lex *lexer) Lex(out *yySymType) int { _lower := int(_keys) var _mid int _upper := int(_keys + (_klen << 1) - 2) - for { - if _upper < _lower { - break - } - + for _upper >= _lower { _mid = _lower + (((_upper - _lower) >> 1) & ^1) switch { case lex.data[(lex.p)] < _expression_trans_keys[_mid]: diff --git a/expressions/y.go b/expressions/y.go index dd627031..d8302e03 100644 --- a/expressions/y.go +++ b/expressions/y.go @@ -1,3 +1,5 @@ +// Code generated by goyacc expressions.y. DO NOT EDIT. + //line expressions.y:2 package expressions @@ -6,7 +8,7 @@ import __yyfmt__ "fmt" //line expressions.y:2 import ( "fmt" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" "math" ) @@ -84,6 +86,7 @@ var yyToknames = [...]string{ "'['", "']'", } + var yyStatenames = [...]string{} const yyEofCode = 1 @@ -91,7 +94,7 @@ const yyErrCode = 2 const yyInitialStackSize = 16 //line yacctab:1 -var yyExca = [...]int{ +var yyExca = [...]int8{ -1, 1, 1, -1, -2, 0, @@ -107,8 +110,7 @@ const yyPrivate = 57344 const yyLast = 104 -var yyAct = [...]int{ - +var yyAct = [...]int8{ 9, 74, 46, 41, 8, 87, 78, 23, 14, 15, 18, 10, 11, 25, 42, 3, 4, 5, 6, 25, 37, 58, 10, 11, 40, 42, 45, 50, 51, 52, @@ -121,8 +123,8 @@ var yyAct = [...]int{ 85, 86, 83, 19, 34, 2, 1, 73, 20, 39, 17, 22, 67, 63, } -var yyPact = [...]int{ +var yyPact = [...]int16{ 7, -1000, 60, 76, 89, 71, 18, -1000, 19, 49, -1000, -1000, 18, -1000, 18, 18, -6, 14, -3, -1000, 10, 38, 1, 39, 83, -1000, 18, 18, 18, 18, @@ -133,29 +135,29 @@ var yyPact = [...]int{ -1000, -1000, -1000, 69, 20, -1000, -1000, -1000, 18, -1000, 88, 86, 6, -1000, -25, -1000, -1000, -1000, } -var yyPgo = [...]int{ +var yyPgo = [...]int8{ 0, 0, 71, 4, 94, 1, 103, 102, 101, 2, 100, 99, 3, 98, 97, 10, 96, } -var yyR1 = [...]int{ +var yyR1 = [...]int8{ 0, 16, 16, 16, 16, 16, 10, 11, 11, 12, 12, 8, 9, 9, 15, 13, 6, 6, 5, 5, 14, 14, 14, 1, 1, 1, 1, 1, 3, 3, 3, 7, 7, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, } -var yyR2 = [...]int{ +var yyR2 = [...]int8{ 0, 2, 5, 3, 3, 3, 2, 3, 1, 0, 3, 2, 0, 3, 1, 4, 5, 1, 1, 1, 0, 2, 3, 1, 1, 2, 4, 3, 1, 3, 4, 1, 3, 1, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, } -var yyChk = [...]int{ +var yyChk = [...]int16{ -1000, -16, -4, 8, 9, 10, 11, -2, -3, -1, 4, 5, 29, 25, 17, 18, 5, -10, -15, 4, -13, 5, -8, -1, 22, 7, 31, 12, 13, 24, @@ -166,8 +168,8 @@ var yyChk = [...]int{ 25, -12, -12, -14, -5, 4, 5, -9, 28, 5, 6, 20, -1, 4, -5, 4, 5, 30, } -var yyDef = [...]int{ +var yyDef = [...]int8{ 0, -2, 0, 0, 0, 0, 0, 41, 33, 28, 23, 24, 0, 1, 0, 0, 0, 0, 9, 14, 0, 0, 0, 12, 0, 25, 0, 0, 0, 0, @@ -178,8 +180,8 @@ var yyDef = [...]int{ 2, 7, 10, 15, 0, -2, -2, 13, 0, 21, 0, 0, 32, 22, 0, 18, 19, 16, } -var yyTok1 = [...]int{ +var yyTok1 = [...]int8{ 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, @@ -194,12 +196,13 @@ var yyTok1 = [...]int{ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 22, } -var yyTok2 = [...]int{ +var yyTok2 = [...]int8{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, } -var yyTok3 = [...]int{ + +var yyTok3 = [...]int8{ 0, } @@ -281,9 +284,9 @@ func yyErrorMessage(state, lookAhead int) string { expected := make([]int, 0, 4) // Look for shiftable tokens. - base := yyPact[state] + base := int(yyPact[state]) for tok := TOKSTART; tok-1 < len(yyToknames); tok++ { - if n := base + tok; n >= 0 && n < yyLast && yyChk[yyAct[n]] == tok { + if n := base + tok; n >= 0 && n < yyLast && int(yyChk[int(yyAct[n])]) == tok { if len(expected) == cap(expected) { return res } @@ -293,13 +296,13 @@ func yyErrorMessage(state, lookAhead int) string { if yyDef[state] == -2 { i := 0 - for yyExca[i] != -1 || yyExca[i+1] != state { + for yyExca[i] != -1 || int(yyExca[i+1]) != state { i += 2 } // Look for tokens that we accept or reduce. for i += 2; yyExca[i] >= 0; i += 2 { - tok := yyExca[i] + tok := int(yyExca[i]) if tok < TOKSTART || yyExca[i+1] == 0 { continue } @@ -330,30 +333,30 @@ func yylex1(lex yyLexer, lval *yySymType) (char, token int) { token = 0 char = lex.Lex(lval) if char <= 0 { - token = yyTok1[0] + token = int(yyTok1[0]) goto out } if char < len(yyTok1) { - token = yyTok1[char] + token = int(yyTok1[char]) goto out } if char >= yyPrivate { if char < yyPrivate+len(yyTok2) { - token = yyTok2[char-yyPrivate] + token = int(yyTok2[char-yyPrivate]) goto out } } for i := 0; i < len(yyTok3); i += 2 { - token = yyTok3[i+0] + token = int(yyTok3[i+0]) if token == char { - token = yyTok3[i+1] + token = int(yyTok3[i+1]) goto out } } out: if token == 0 { - token = yyTok2[1] /* unknown char */ + token = int(yyTok2[1]) /* unknown char */ } if yyDebug >= 3 { __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char)) @@ -408,7 +411,7 @@ yystack: yyS[yyp].yys = yystate yynewstate: - yyn = yyPact[yystate] + yyn = int(yyPact[yystate]) if yyn <= yyFlag { goto yydefault /* simple state */ } @@ -419,8 +422,8 @@ yynewstate: if yyn < 0 || yyn >= yyLast { goto yydefault } - yyn = yyAct[yyn] - if yyChk[yyn] == yytoken { /* valid shift */ + yyn = int(yyAct[yyn]) + if int(yyChk[yyn]) == yytoken { /* valid shift */ yyrcvr.char = -1 yytoken = -1 yyVAL = yyrcvr.lval @@ -433,7 +436,7 @@ yynewstate: yydefault: /* default state action */ - yyn = yyDef[yystate] + yyn = int(yyDef[yystate]) if yyn == -2 { if yyrcvr.char < 0 { yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) @@ -442,18 +445,18 @@ yydefault: /* look through exception table */ xi := 0 for { - if yyExca[xi+0] == -1 && yyExca[xi+1] == yystate { + if yyExca[xi+0] == -1 && int(yyExca[xi+1]) == yystate { break } xi += 2 } for xi += 2; ; xi += 2 { - yyn = yyExca[xi+0] + yyn = int(yyExca[xi+0]) if yyn < 0 || yyn == yytoken { break } } - yyn = yyExca[xi+1] + yyn = int(yyExca[xi+1]) if yyn < 0 { goto ret0 } @@ -475,10 +478,10 @@ yydefault: /* find a state where "error" is a legal shift action */ for yyp >= 0 { - yyn = yyPact[yyS[yyp].yys] + yyErrCode + yyn = int(yyPact[yyS[yyp].yys]) + yyErrCode if yyn >= 0 && yyn < yyLast { - yystate = yyAct[yyn] /* simulate a shift of "error" */ - if yyChk[yystate] == yyErrCode { + yystate = int(yyAct[yyn]) /* simulate a shift of "error" */ + if int(yyChk[yystate]) == yyErrCode { goto yystack } } @@ -514,7 +517,7 @@ yydefault: yypt := yyp _ = yypt // guard against "declared and not used" - yyp -= yyR2[yyn] + yyp -= int(yyR2[yyn]) // yyp is now the index of $0. Perform the default action. Iff the // reduced production is ε, $1 is possibly out of range. if yyp+1 >= len(yyS) { @@ -525,16 +528,16 @@ yydefault: yyVAL = yyS[yyp+1] /* consult goto table to find next state */ - yyn = yyR1[yyn] - yyg := yyPgo[yyn] + yyn = int(yyR1[yyn]) + yyg := int(yyPgo[yyn]) yyj := yyg + yyS[yyp].yys + 1 if yyj >= yyLast { - yystate = yyAct[yyg] + yystate = int(yyAct[yyg]) } else { - yystate = yyAct[yyj] - if yyChk[yystate] != -yyn { - yystate = yyAct[yyg] + yystate = int(yyAct[yyj]) + if int(yyChk[yystate]) != -yyn { + yystate = int(yyAct[yyg]) } } // dummy call; replaced with literal code @@ -542,87 +545,87 @@ yydefault: case 1: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:46 +//line expressions.y:46 { yylex.(*lexer).val = yyDollar[1].f } case 2: yyDollar = yyS[yypt-5 : yypt+1] - //line expressions.y:47 +//line expressions.y:47 { yylex.(*lexer).Assignment = Assignment{yyDollar[2].name, &expression{yyDollar[4].f}} } case 3: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:50 +//line expressions.y:50 { yylex.(*lexer).Cycle = yyDollar[2].cycle } case 4: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:51 +//line expressions.y:51 { yylex.(*lexer).Loop = yyDollar[2].loop } case 5: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:52 +//line expressions.y:52 { yylex.(*lexer).When = When{yyDollar[2].exprs} } case 6: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:55 +//line expressions.y:55 { yyVAL.cycle = yyDollar[2].cyclefn(yyDollar[1].s) } case 7: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:58 +//line expressions.y:58 { h, t := yyDollar[2].s, yyDollar[3].ss yyVAL.cyclefn = func(g string) Cycle { return Cycle{g, append([]string{h}, t...)} } } case 8: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:62 +//line expressions.y:62 { vals := yyDollar[1].ss yyVAL.cyclefn = func(h string) Cycle { return Cycle{Values: append([]string{h}, vals...)} } } case 9: yyDollar = yyS[yypt-0 : yypt+1] - //line expressions.y:69 +//line expressions.y:69 { yyVAL.ss = []string{} } case 10: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:70 +//line expressions.y:70 { yyVAL.ss = append([]string{yyDollar[2].s}, yyDollar[3].ss...) } case 11: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:73 +//line expressions.y:73 { yyVAL.exprs = append([]Expression{&expression{yyDollar[1].f}}, yyDollar[2].exprs...) } case 12: yyDollar = yyS[yypt-0 : yypt+1] - //line expressions.y:75 +//line expressions.y:75 { yyVAL.exprs = []Expression{} } case 13: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:76 +//line expressions.y:76 { yyVAL.exprs = append([]Expression{&expression{yyDollar[2].f}}, yyDollar[3].exprs...) } case 14: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:79 +//line expressions.y:79 { s, ok := yyDollar[1].val.(string) if !ok { @@ -632,40 +635,40 @@ yydefault: } case 15: yyDollar = yyS[yypt-4 : yypt+1] - //line expressions.y:87 +//line expressions.y:87 { name, expr, mods := yyDollar[1].name, yyDollar[3].f, yyDollar[4].loopmods yyVAL.loop = Loop{name, &expression{expr}, mods} } case 16: yyDollar = yyS[yypt-5 : yypt+1] - //line expressions.y:93 +//line expressions.y:93 { yyVAL.f = makeRangeExpr(yyDollar[2].f, yyDollar[4].f) } case 18: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:101 +//line expressions.y:101 { val := yyDollar[1].val yyVAL.f = func(Context) values.Value { return values.ValueOf(val) } } case 19: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:102 +//line expressions.y:102 { name := yyDollar[1].name yyVAL.f = func(ctx Context) values.Value { return values.ValueOf(ctx.Get(name)) } } case 20: yyDollar = yyS[yypt-0 : yypt+1] - //line expressions.y:105 +//line expressions.y:105 { yyVAL.loopmods = loopModifiers{Cols: math.MaxUint32} } case 21: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:106 +//line expressions.y:106 { switch yyDollar[2].name { case "reversed": @@ -677,7 +680,7 @@ yydefault: } case 22: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:115 +//line expressions.y:115 { // TODO can this be a variable? switch yyDollar[2].name { case "cols": @@ -705,63 +708,63 @@ yydefault: } case 23: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:143 +//line expressions.y:143 { val := yyDollar[1].val yyVAL.f = func(Context) values.Value { return values.ValueOf(val) } } case 24: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:144 +//line expressions.y:144 { name := yyDollar[1].name yyVAL.f = func(ctx Context) values.Value { return values.ValueOf(ctx.Get(name)) } } case 25: yyDollar = yyS[yypt-2 : yypt+1] - //line expressions.y:145 +//line expressions.y:145 { yyVAL.f = makeObjectPropertyExpr(yyDollar[1].f, yyDollar[2].name) } case 26: yyDollar = yyS[yypt-4 : yypt+1] - //line expressions.y:146 +//line expressions.y:146 { yyVAL.f = makeIndexExpr(yyDollar[1].f, yyDollar[3].f) } case 27: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:147 +//line expressions.y:147 { yyVAL.f = yyDollar[2].f } case 29: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:152 +//line expressions.y:152 { yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name, nil) } case 30: yyDollar = yyS[yypt-4 : yypt+1] - //line expressions.y:153 +//line expressions.y:153 { yyVAL.f = makeFilter(yyDollar[1].f, yyDollar[3].name, yyDollar[4].filter_params) } case 31: yyDollar = yyS[yypt-1 : yypt+1] - //line expressions.y:157 +//line expressions.y:157 { yyVAL.filter_params = []valueFn{yyDollar[1].f} } case 32: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:159 +//line expressions.y:159 { yyVAL.filter_params = append(yyDollar[1].filter_params, yyDollar[3].f) } case 34: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:163 +//line expressions.y:163 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -771,7 +774,7 @@ yydefault: } case 35: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:170 +//line expressions.y:170 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -781,7 +784,7 @@ yydefault: } case 36: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:177 +//line expressions.y:177 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -791,7 +794,7 @@ yydefault: } case 37: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:184 +//line expressions.y:184 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -801,7 +804,7 @@ yydefault: } case 38: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:191 +//line expressions.y:191 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -811,7 +814,7 @@ yydefault: } case 39: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:198 +//line expressions.y:198 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { @@ -821,25 +824,35 @@ yydefault: } case 40: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:205 +//line expressions.y:205 { yyVAL.f = makeContainsExpr(yyDollar[1].f, yyDollar[3].f) } case 42: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:210 +//line expressions.y:210 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { + if _, ok := ctx.(*varsContext); ok { + a := fa(ctx).Test() + b := fb(ctx).Test() + return values.ValueOf(a && b) + } return values.ValueOf(fa(ctx).Test() && fb(ctx).Test()) } } case 43: yyDollar = yyS[yypt-3 : yypt+1] - //line expressions.y:216 +//line expressions.y:221 { fa, fb := yyDollar[1].f, yyDollar[3].f yyVAL.f = func(ctx Context) values.Value { + if _, ok := ctx.(*varsContext); ok { + a := fa(ctx).Test() + b := fb(ctx).Test() + return values.ValueOf(a || b) + } return values.ValueOf(fa(ctx).Test() || fb(ctx).Test()) } } diff --git a/filters/sort_filters.go b/filters/sort_filters.go index 7ef2fb03..741d5e66 100644 --- a/filters/sort_filters.go +++ b/filters/sort_filters.go @@ -6,7 +6,7 @@ import ( "sort" "strings" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/values" ) func sortFilter(array []interface{}, key interface{}) []interface{} { diff --git a/filters/standard_filters.go b/filters/standard_filters.go index 1e0b25c6..9dd19a40 100644 --- a/filters/standard_filters.go +++ b/filters/standard_filters.go @@ -2,20 +2,27 @@ package filters import ( + "crypto/hmac" + "crypto/md5" // #nosec G501 + "crypto/sha1" // #nosec G505 + "crypto/sha256" "encoding/json" "fmt" + "hash" "html" "math" "net/url" "reflect" "regexp" + "strconv" "strings" "time" "unicode" - "unicode/utf8" - "github.com/osteele/liquid/values" "github.com/osteele/tuesday" + + "github.com/autopilot3/ap3-types-go/types/date" + "github.com/autopilot3/liquid/values" ) // A FilterDictionary holds filters. @@ -68,12 +75,66 @@ func AddStandardFilters(fd FilterDictionary) { // nolint: gocyclo fd.AddFilter("uniq", uniqFilter) // date filters - fd.AddFilter("date", func(t time.Time, format func(string) string) (string, error) { + fd.AddFilter("date", func(t interface{}, format func(string) string) (string, error) { f := format("%a, %b %d, %y") - return tuesday.Strftime(f, t) + switch tp := t.(type) { + case date.Date: + d := t.(date.Date) + tme, err := d.Time() + if err != nil { + return "", err + } + return tuesday.Strftime(f, tme) + case string: + tme, err := values.ParseDate(t.(string)) + if err != nil { + return "", err + } + return tuesday.Strftime(f, tme) + case time.Time: + tme := t.(time.Time) + return tuesday.Strftime(f, tme) + case int64: + unixTime := t.(int64) + tme := time.Unix(unixTime, 0) + return tuesday.Strftime(f, tme) + case float64: + unixTime := t.(float64) + tme := time.Unix(int64(unixTime), 0) + return tuesday.Strftime(f, tme) + case nil: + return "", nil + default: + return "", fmt.Errorf("date filter: unsupported type %T", tp) + } }) // number filters + fd.AddFilter("to_number", func(value interface{}) float64 { + switch v := value.(type) { + case float64: + return v + case int: + return float64(v) + case int8: + return float64(v) + case int16: + return float64(v) + case int32: + return float64(v) + case int64: + return float64(v) + case float32: + return float64(v) + case string: + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + return 0 + default: + return 0 + } + }) fd.AddFilter("abs", math.Abs) fd.AddFilter("ceil", math.Ceil) fd.AddFilter("floor", math.Floor) @@ -90,9 +151,16 @@ func AddStandardFilters(fd FilterDictionary) { // nolint: gocyclo fd.AddFilter("divided_by", func(a float64, b interface{}) interface{} { switch q := b.(type) { case int, int16, int32, int64: + if q.(int) == 0 { + return nil + } return int(a) / q.(int) case float32, float64: - return a / b.(float64) + // check divider + if q.(float64) == 0 { + return nil + } + return a / q.(float64) default: return nil } @@ -103,6 +171,9 @@ func AddStandardFilters(fd FilterDictionary) { // nolint: gocyclo return math.Floor(n*exp+0.5) / exp }) + fd.AddFilter("at_least", floatFilter(math.Max)) + fd.AddFilter("at_most", floatFilter(math.Min)) + // sequence filters fd.AddFilter("size", values.Length) @@ -114,7 +185,9 @@ func AddStandardFilters(fd FilterDictionary) { // nolint: gocyclo if len(s) == 0 { return s } - return strings.ToUpper(s[:1]) + s[1:] + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) }) fd.AddFilter("downcase", func(s, suffix string) string { return strings.ToLower(s) @@ -124,19 +197,19 @@ func AddStandardFilters(fd FilterDictionary) { // nolint: gocyclo return html.EscapeString(html.UnescapeString(s)) }) fd.AddFilter("newline_to_br", func(s string) string { - return strings.Replace(s, "\n", "
", -1) + return strings.ReplaceAll(s, "\n", "
") }) fd.AddFilter("prepend", func(s, prefix string) string { return prefix + s }) fd.AddFilter("remove", func(s, old string) string { - return strings.Replace(s, old, "", -1) + return strings.ReplaceAll(s, old, "") }) fd.AddFilter("remove_first", func(s, old string) string { return strings.Replace(s, old, "", 1) }) fd.AddFilter("replace", func(s, old, new string) string { - return strings.Replace(s, old, new, -1) + return strings.ReplaceAll(s, old, new) }) fd.AddFilter("replace_first", func(s, old, new string) string { return strings.Replace(s, old, new, 1) @@ -145,19 +218,30 @@ func AddStandardFilters(fd FilterDictionary) { // nolint: gocyclo fd.AddFilter("slice", func(s string, start int, length func(int) int) string { // runes aren't bytes; don't use slice n := length(1) + runes := []rune(s) if start < 0 { - start = utf8.RuneCountInString(s) + start + start = len(runes) + start } - p := regexp.MustCompile(fmt.Sprintf(`^.{%d}(.{0,%d}).*$`, start, n)) - return p.ReplaceAllString(s, "$1") + if start < 0 { + return s + } + if start >= len(runes) { + return "" + } + end := start + n + if end > len(runes) { + end = len(runes) + } + return string(runes[start:end]) }) + fd.AddFilter("split", splitFilter) fd.AddFilter("strip_html", func(s string) string { // TODO this probably isn't sufficient return regexp.MustCompile(`<.*?>`).ReplaceAllString(s, "") }) fd.AddFilter("strip_newlines", func(s string) string { - return strings.Replace(s, "\n", "", -1) + return strings.ReplaceAll(s, "\n", "") }) fd.AddFilter("strip", strings.TrimSpace) fd.AddFilter("lstrip", func(s string) string { @@ -201,6 +285,102 @@ func AddStandardFilters(fd FilterDictionary) { // nolint: gocyclo fd.AddFilter("type", func(value interface{}) string { return fmt.Sprintf("%T", value) }) + + // Hash filters + + // #nosec G401 + fd.AddFilter("md5", hashFilter(md5.New)) + fd.AddFilter("sha1", hashFilter(sha1.New)) + fd.AddFilter("sha256", hashFilter(sha256.New)) + // #nosec G401 + fd.AddFilter("hmac", hmacFilter(md5.New)) + fd.AddFilter("hmac_sha1", hmacFilter(sha1.New)) + fd.AddFilter("hmac_sha256", hmacFilter(sha256.New)) +} + +func hashFilter(hashFn func() hash.Hash) func(value interface{}) string { + return func(value interface{}) string { + valueBytes := toBytes(value) + if len(valueBytes) == 0 { + return "" + } + h := hashFn() + if _, err := h.Write(valueBytes); err == nil { + return fmt.Sprintf("%x", h.Sum(nil)) + } + return "" + } +} + +func hmacFilter(hashFn func() hash.Hash) func(value, key interface{}) string { + + return func(value, key interface{}) string { + valueBytes := toBytes(value) + if len(valueBytes) == 0 { + return "" + } + keyBytes := toBytes(key) + if len(keyBytes) == 0 { + return "" + } + hm := hmac.New(hashFn, keyBytes) + if _, err := hm.Write(valueBytes); err == nil { + return fmt.Sprintf("%x", hm.Sum(nil)) + } + return "" + } +} + +func floatFilter(fn func(v1, v2 float64) float64) func(lhs, rhs interface{}) interface{} { + return func(lhs, rhs interface{}) interface{} { + lhsValue, ok := parseAsFloat64(lhs) + if !ok { + return "" + } + rhsValue, ok := parseAsFloat64(rhs) + if !ok { + return "" + } + return fn(lhsValue, rhsValue) + } +} + +func parseAsFloat64(value interface{}) (float64, bool) { + switch v := value.(type) { + case string: + parsed, err := strconv.ParseFloat(v, 64) + if err != nil { + return 0, false + } + return parsed, true + case int: + return float64(v), true + case int8: + return float64(v), true + case int16: + return float64(v), true + case int32: + return float64(v), true + case int64: + return float64(v), true + case float32: + return float64(v), true + case float64: + return v, true + default: + return 0, false + } +} + +func toBytes(value interface{}) []byte { + switch v := value.(type) { + case string: + return []byte(v) + case int, int8, int16, int32, int64, float32, float64: + return []byte(fmt.Sprint(v)) + default: + return nil + } } func joinFilter(a []interface{}, sep func(string) string) interface{} { diff --git a/filters/standard_filters_test.go b/filters/standard_filters_test.go index 76a116e2..5917e781 100644 --- a/filters/standard_filters_test.go +++ b/filters/standard_filters_test.go @@ -1,6 +1,7 @@ package filters import ( + gocontext "context" "fmt" "os" "testing" @@ -8,8 +9,10 @@ import ( yaml "gopkg.in/yaml.v2" - "github.com/osteele/liquid/expressions" "github.com/stretchr/testify/require" + + "github.com/autopilot3/ap3-types-go/types/date" + "github.com/autopilot3/liquid/expressions" ) var filterTests = []struct { @@ -74,6 +77,8 @@ var filterTests = []struct { {`"2017-07-09" | date: "%d/%m"`, "09/07"}, {`"2017-07-09" | date: "%e/%m"`, " 9/07"}, {`"2017-07-09" | date: "%-d/%-m"`, "9/7"}, + {`ortto.example_date | date`, "Fri, Jul 17, 15"}, + {`ortto.not_existing_date | date`, ""}, // sequence (array or string) filters {`"Ground control to Major Tom." | size`, 28}, @@ -85,6 +90,7 @@ var filterTests = []struct { {`"/my/fancy/url" | append: ".html"`, "/my/fancy/url.html"}, {`"website.com" | append: "/index.html"`, "website.com/index.html"}, {`"title" | capitalize`, "Title"}, + {`"Élio Silva" | capitalize`, "Élio Silva"}, {`"my great title" | capitalize`, "My great title"}, {`"" | capitalize`, ""}, {`"Parker Moore" | downcase`, "parker moore"}, @@ -100,6 +106,7 @@ var filterTests = []struct { {`"Liquid" | slice: 2`, "q"}, {`"Liquid" | slice: 2, 5`, "quid"}, {`"Liquid" | slice: -3, 2`, "ui"}, + {`"Привет" | slice: -3, 2`, "ве"}, {`"a/b/c" | split: '/' | join: '-'`, "a-b-c"}, {`"a/b/" | split: '/' | join: '-'`, "a-b"}, @@ -143,6 +150,7 @@ var filterTests = []struct { {`"Tetsuro Takara" | url_encode`, "Tetsuro+Takara"}, // number filters + {`"45" | to_number`, 45}, {`-17 | abs`, 17}, {`4 | abs`, 4}, {`"-19.86" | abs`, 19.86}, @@ -176,6 +184,7 @@ var filterTests = []struct { {`20 | divided_by: 7`, 2}, {`20 | divided_by: 7.0`, 2.857142857142857}, {`20 | divided_by: 's'`, nil}, + {`20 | divided_by: 0`, nil}, {`1.2 | round`, 1}, {`2.7 | round`, 3}, @@ -186,6 +195,153 @@ var filterTests = []struct { {`map | inspect`, `{"a":1}`}, {`1 | type`, `int`}, {`"1" | type`, `string`}, + + // Hash filters + + {`"Take my protein pills and put my helmet on" | md5`, "505a1a407670a93d9ef2cf34960002f9"}, + {`100 | md5`, "f899139df5e1059396431415e770c6dd"}, + {`100.01 | md5`, "e74f9831767648ecdd211c3f8cd85b86"}, + + {`"Take my protein pills and put my helmet on" | sha1`, "07f3b4973325af9109399ead74f2180bcaefa4c0"}, + {`"" | sha1`, ""}, + {`100 | sha1`, "310b86e0b62b828562fc91c7be5380a992b2786a"}, + {`100.01 | sha1`, "2cf9b40e62dd0bff2c57d179bfc99674d25f3c33"}, + + {`"Take my protein pills and put my helmet on" | sha256`, "b19c3d04c1b80ae9acd15227c0dde0cb6f5755995afa3c846a3473ac42de6f63"}, + {`"" | sha256`, ""}, + {`100 | sha256`, "ad57366865126e55649ecb23ae1d48887544976efea46a48eb5d85a6eeb4d306"}, + {`100.01 | sha256`, "4b46711a09b65af6dcbbc4caab38ab58e06d08eb75fbeb8e367fdd1ccc289fba"}, + + {`"Take my protein pills and put my helmet on" | hmac: "key"`, "5b74077685d98d1e1d03cd289e2c2bfc"}, + {`"Take my protein pills and put my helmet on" | hmac: ""`, ""}, + {`"" | hmac: "key"`, ""}, + {`"" | hmac: 100`, ""}, + {`"" | hmac: 100.01`, ""}, + {`"Take my protein pills and put my helmet on" | hmac: 100`, "3494f6a7895d9e8084343e1020984ba6"}, + {`"Take my protein pills and put my helmet on" | hmac: 100.01`, "c1ef31ab6b3630ffb2e6842a600bf572"}, + {`"Only numeric and string keys are supported" | hmac: true`, ""}, + {`100 | hmac: "key"`, "f69388563202c10d4e0dc44646a3b937"}, + {`100 | hmac: 100`, "e459c4d00f32981388e5d0e797c8ac68"}, + {`100 | hmac: 100.01`, "f88e6d1df733b884b9748bbab83b3e68"}, + {`100.01 | hmac: "key"`, "41e66d9c6ca6e0b7b0470d9c03fef001"}, + {`100.01 | hmac: 100`, "7ac1da15168b6bf50c2975fa3198e84e"}, + {`100.01 | hmac: 100.01`, "bcd8551b5dbc26ed858752b9046dc654"}, + + {`"Take my protein pills and put my helmet on" | hmac_sha1: "key"`, "fca4135e0bc4d4bcdccfd0bd98edc30d3d7ac629"}, + {`"Take my protein pills and put my helmet on" | hmac_sha1: ""`, ""}, + {`"" | hmac_sha1: "key"`, ""}, + {`"" | hmac_sha1: 100`, ""}, + {`"" | hmac_sha1: 100.01`, ""}, + {`"Take my protein pills and put my helmet on" | hmac_sha1: 100`, "595095014fab1b061a47cc1b7856b78bd78ad998"}, + {`"Take my protein pills and put my helmet on" | hmac_sha1: 100.01`, "3922875669b50f66373f1a21d91fd113f456b66c"}, + {`"Only numeric and string keys are supported" | hmac_sha1: true`, ""}, + {`100 | hmac_sha1: "key"`, "30385a0b6d754aee6a69093edd9d16accd57e26d"}, + {`100 | hmac_sha1: 100`, "56ba1ffa433eef7d9ebe9ef9fc464bdf2d68d7ed"}, + {`100 | hmac_sha1: 100.01`, "f962759dc0683e9aed4728d10cad6ade3c0f03ac"}, + {`100.01 | hmac_sha1: "key"`, "a3812ff53e8080fd42193b75d2245fe7ecb08df5"}, + {`100.01 | hmac_sha1: 100`, "877bfb3895f60525f123edec278d7dd915c6b2a6"}, + {`100.01 | hmac_sha1: 100.01`, "0efc1381dd2a001a0ba3db56f6e9456f3f4d73a8"}, + + {`"Take my protein pills and put my helmet on" | hmac_sha256: "key"`, "111fce4b586c1c54804196bbc014e45005958fcaf5462fa206ad5856811686f5"}, + {`"Take my protein pills and put my helmet on" | hmac_sha256: ""`, ""}, + {`"" | hmac_sha256: "key"`, ""}, + {`"" | hmac_sha256: 100`, ""}, + {`"" | hmac_sha256: 100.01`, ""}, + {`"Take my protein pills and put my helmet on" | hmac_sha256: 100`, "c23af083390e2408faed6cf7d23f914425e9cab268050d5dc674f023bc8a8d6a"}, + {`"Take my protein pills and put my helmet on" | hmac_sha256: 100.01`, "9a19b23c1e55a2f570aad746844cb36f928d20ff4c837dca8fef0c2ef453cf63"}, + {`"Only numeric and string keys are supported" | hmac_sha256: true`, ""}, + {`100 | hmac_sha256: "key"`, "71d0fcbb40b55250039eb1f8bf363e280431f868af075355e6c9e44574f915d8"}, + {`100 | hmac_sha256: 100`, "f74a692209268d93c5a6ec227edfe17f7a70b28e049648f80238695798ffd407"}, + {`100 | hmac_sha256: 100.01`, "571751c3df688bc29af6e730c0c0d02ed4f1261fdfc9de2bf51a274106a5c6d4"}, + {`100.01 | hmac_sha256: "key"`, "b6c9391539ba7d250c9cbea6fb8aaaf278a5f858ad9206ae7ba6063ae17f2eb6"}, + {`100.01 | hmac_sha256: 100`, "7a48e1789185ab575a94579302ff9c4b57e58c70e40609f7a2a76469c9381d01"}, + {`100.01 | hmac_sha256: 100.01`, "bad95722cd8088216306962a575751a3a7251234f61504b33be224f9a9c2971c"}, + + // at_least + {`"10" | at_least: "20"`, 20}, + {`"10.5" | at_least: "20"`, 20}, + {`"10.5" | at_least: "20.5"`, 20.5}, + {`10 | at_least: 20`, 20}, + {`10.5 | at_least: 20`, 20}, + {`10.5 | at_least: 20.5`, 20.5}, + {`10 | at_least: "20"`, 20}, + {`10.5 | at_least: "20"`, 20}, + {`10.5 | at_least: "20.5"`, 20.5}, + {`"10" | at_least: 20`, 20}, + {`"10.5" | at_least: 20`, 20}, + {`"10.5" | at_least: 20.5`, 20.5}, + + {`"20" | at_least: "10"`, 20}, + {`"20.5" | at_least: "10"`, 20.5}, + {`"20.5" | at_least: "10.5"`, 20.5}, + {`20 | at_least: 10`, 20}, + {`20.5 | at_least: 10`, 20.5}, + {`20.5 | at_least: 10.5`, 20.5}, + {`20 | at_least: "10"`, 20}, + {`20.5 | at_least: "10"`, 20.5}, + {`20.5 | at_least: "10.5"`, 20.5}, + {`"20" | at_least: 10`, 20}, + {`"20.5" | at_least: 10`, 20.5}, + {`"20.5" | at_least: 10.5`, 20.5}, + + {`"0" | at_least: "0"`, 0}, + {`0 | at_least: "0"`, 0}, + {`"0" | at_least: 0`, 0}, + {`"0.0" | at_least: "0.0"`, 0}, + {`0.0 | at_least: "0.0"`, 0}, + {`"0.0" | at_least: 0.0`, 0}, + + {`"" | at_least: 20`, ""}, + {`"" | at_least: "20"`, ""}, + {`"" | at_least: 20.5`, ""}, + {`"" | at_least: "20.5"`, ""}, + {`10 | at_least: ""`, ""}, + {`"10" | at_least: ""`, ""}, + {`"10.2" | at_least: ""`, ""}, + {`"10.2" | at_least: ""`, ""}, + + // at_most + {`"10" | at_most: "20"`, 10}, + {`"10.5" | at_most: "20"`, 10.5}, + {`"10.5" | at_most: "20.5"`, 10.5}, + {`10 | at_most: 20`, 10}, + {`10.5 | at_most: 20`, 10.5}, + {`10.5 | at_most: 20.5`, 10.5}, + {`10 | at_most: "20"`, 10}, + {`10.5 | at_most: "20"`, 10.5}, + {`10.5 | at_most: "20.5"`, 10.5}, + {`"10" | at_most: 20`, 10}, + {`"10.5" | at_most: 20`, 10.5}, + {`"10.5" | at_most: 20.5`, 10.5}, + + {`"20" | at_most: "10"`, 10}, + {`"20.5" | at_most: "10"`, 10}, + {`"20.5" | at_most: "10.5"`, 10.5}, + {`20 | at_most: 10`, 10}, + {`20.5 | at_most: 10`, 10}, + {`20.5 | at_most: 10.5`, 10.5}, + {`20 | at_most: "10"`, 10}, + {`20.5 | at_most: "10"`, 10}, + {`20.5 | at_most: "10.5"`, 10.5}, + {`"20" | at_most: 10`, 10}, + {`"20.5" | at_most: 10`, 10}, + {`"20.5" | at_most: 10.5`, 10.5}, + + {`"0" | at_most: "0"`, 0}, + {`0 | at_most: "0"`, 0}, + {`"0" | at_most: 0`, 0}, + {`"0.0" | at_most: "0.0"`, 0}, + {`0.0 | at_most: "0.0"`, 0}, + {`"0.0" | at_most: 0.0`, 0}, + + {`"" | at_most: 20`, ""}, + {`"" | at_most: "20"`, ""}, + {`"" | at_most: 20.5`, ""}, + {`"" | at_most: "20.5"`, ""}, + {`10 | at_most: ""`, ""}, + {`"10" | at_most: ""`, ""}, + {`"10.2" | at_most: ""`, ""}, + {`"10.2" | at_most: ""`, ""}, } var filterTestBindings = map[string]interface{}{ @@ -224,6 +380,9 @@ var filterTestBindings = map[string]interface{}{ "article": map[string]interface{}{ "published_at": timeMustParse("2015-07-17T15:04:05Z"), }, + "ortto": map[string]interface{}{ + "example_date": date.MustNewFromTime(timeMustParse("2015-07-17T15:04:05Z")), + }, "page": map[string]interface{}{ "title": "Introduction", }, @@ -248,7 +407,7 @@ func TestFilters(t *testing.T) { ) filterTestBindings["dup_maps"] = []interface{}{m1, m2, m1, m3} - cfg := expressions.NewConfig() + cfg := expressions.NewConfig(gocontext.Background()) AddStandardFilters(&cfg) context := expressions.NewContext(filterTestBindings, cfg) diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..257e5e14 --- /dev/null +++ b/go.mod @@ -0,0 +1,72 @@ +module github.com/autopilot3/liquid + +go 1.24 + +toolchain go1.24.1 + +require ( + github.com/autopilot3/ap3-crm-api-go v0.0.0-20230721090944-c8612d9147a5 + github.com/autopilot3/ap3-helpers-go v0.0.0-20250506013120-83414d6baf31 + github.com/autopilot3/ap3-types-go v0.0.0-20250409014854-d8c500a2e4d0 + github.com/bojanz/currency v1.4.0 + github.com/osteele/tuesday v1.0.3 + github.com/stretchr/testify v1.10.0 + golang.org/x/text v0.25.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/alexsergivan/transliterator v1.0.0 // indirect + github.com/autopilot3/ap3-auth-go v1.0.13-0.20250415000245-a563d6fec65b // indirect + github.com/autopilot3/ap3-billing-api-go v0.0.0-20230717054957-3d5d1e327a8c // indirect + github.com/autopilot3/ap3-index-go v0.0.0-20230614154429-8e96f0923dea // indirect + github.com/autopilot3/ap3-ring-api-go v0.0.0-20230711140618-2033889bea7f // indirect + github.com/autopilot3/ap3-tasks-api-go v0.0.0-20230712063215-13b009ae11f2 // indirect + github.com/autopilot3/uasurfer v0.0.0-20220503043020-b0a5a9c79fb5 // indirect + github.com/aws/aws-sdk-go v1.44.278 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gocql/gocql v1.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/nats-io/nats.go v1.31.0 // indirect + github.com/nats-io/nkeys v0.4.7 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/nyaruka/phonenumbers v1.6.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.15.1 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/recurly/recurly-client-go/v3 v3.17.0 // indirect + github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.1+incompatible // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.mongodb.org/mongo-driver v1.17.3 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/grpc v1.67.2 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..a5ddf229 --- /dev/null +++ b/go.sum @@ -0,0 +1,213 @@ +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/alexsergivan/transliterator v1.0.0 h1:SAA+fkGZKLnak47h8Dr6829IE2kpSZR2Y3yTd69cIwY= +github.com/alexsergivan/transliterator v1.0.0/go.mod h1:0IrumukulURJ4PD0z6UcdJKP2job1DYDhnHAP5y+5pE= +github.com/autopilot3/ap3-auth-go v1.0.13-0.20250415000245-a563d6fec65b h1:GffxQnGzRSP+lier71a2u5CBIqga0cQwYmi8ekQSjM8= +github.com/autopilot3/ap3-auth-go v1.0.13-0.20250415000245-a563d6fec65b/go.mod h1:igk6G3+5encRxS+88+Be7qP6QCIcswmSEtZt06mRPVA= +github.com/autopilot3/ap3-billing-api-go v0.0.0-20230717054957-3d5d1e327a8c h1:m8FLXveG84nlTh8DNKteH5clIaEv578g+jDYtBg5xxw= +github.com/autopilot3/ap3-billing-api-go v0.0.0-20230717054957-3d5d1e327a8c/go.mod h1:bgKV0qCvMk+1A1XScaoB5QfGbchGl5YEJCqMcIcN4UI= +github.com/autopilot3/ap3-crm-api-go v0.0.0-20230721090944-c8612d9147a5 h1:JS2A7z9JlEknMUNQJmeY3SX5s0I8D5ga0H8JSngPLHw= +github.com/autopilot3/ap3-crm-api-go v0.0.0-20230721090944-c8612d9147a5/go.mod h1:SJs66Oti32Zro6TrN3+5+mgsuI7CPkgGNxXsJIZG1Zg= +github.com/autopilot3/ap3-helpers-go v0.0.0-20250506013120-83414d6baf31 h1:MphTkfMC1Te1yiLntpSm+vmSPsJlps1bgD5R1R2NDRo= +github.com/autopilot3/ap3-helpers-go v0.0.0-20250506013120-83414d6baf31/go.mod h1:kWYICeX9C9WiVFkyDvWGu0Ks8oBioQ5IMqHzeznow+Y= +github.com/autopilot3/ap3-index-go v0.0.0-20230614154429-8e96f0923dea h1:styrioXYutY+Iw+cayBdyJo17uksbxfqkcnOnPzf6S0= +github.com/autopilot3/ap3-index-go v0.0.0-20230614154429-8e96f0923dea/go.mod h1:i7rtUVGqaVgEK+MRY9OgpDaKEoSRkj4JAsDWiTZC9wg= +github.com/autopilot3/ap3-ring-api-go v0.0.0-20230711140618-2033889bea7f h1:xzzx7z1F2XK9DDS3MR5a5plWX6T0YpVzAWcabMnf/SQ= +github.com/autopilot3/ap3-ring-api-go v0.0.0-20230711140618-2033889bea7f/go.mod h1:p+zOzV+4mfoRpKUZrVnf3g9ENKRPAJkwyFEQxU12iMY= +github.com/autopilot3/ap3-tasks-api-go v0.0.0-20230712063215-13b009ae11f2 h1:yPjtxirjuvzhbuuMAhdIZFBzYFM6rKn/EXst068RDJY= +github.com/autopilot3/ap3-tasks-api-go v0.0.0-20230712063215-13b009ae11f2/go.mod h1:grZ3E1woGyvBHDdzOZz5vC7Ysr3Kqtgn0MX7fphM5Ao= +github.com/autopilot3/ap3-types-go v0.0.0-20250409014854-d8c500a2e4d0 h1:Q9H0R/EmaWV7De4n29widFfWDegRGfDm8sPMTk6Mhu0= +github.com/autopilot3/ap3-types-go v0.0.0-20250409014854-d8c500a2e4d0/go.mod h1:a1Re4maX1gsCxvFRO43Reho5CGUjGuohmznJhUpgEwU= +github.com/autopilot3/uasurfer v0.0.0-20220503043020-b0a5a9c79fb5 h1:WO0/Itf2u8Aa+v8zcZN27mDmo2j6d0Gy296eacLwvcU= +github.com/autopilot3/uasurfer v0.0.0-20220503043020-b0a5a9c79fb5/go.mod h1:WPlm07a+dgY9dsmM0ai6/tIC+dm9PUxu+UlTMTKS28s= +github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1/go.mod h1:noBAuukeYOXa0aXGqxr24tADqkwDO2KRD15FsuaZ5a8= +github.com/aws/aws-sdk-go v1.44.278 h1:jJFDO/unYFI48WQk7UGSyO3rBA/gnmRpNYNuAw/fPgE= +github.com/aws/aws-sdk-go v1.44.278/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bojanz/currency v1.4.0 h1:sPLts/Qx3u0+FfYx+31NLYYOiOdPiH8mBWdJCAZHj3g= +github.com/bojanz/currency v1.4.0/go.mod h1:hv7AAJ5jNRvE/SXaJe74uBPg6syV21imy4yKP7fi9GM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gocql/gocql v1.4.0 h1:NIlXAJXsjzjGvVn36njh9OLYWzS3D7FdvsifLj4eDEY= +github.com/gocql/gocql v1.4.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= +github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nyaruka/phonenumbers v1.6.0 h1:r9ax45fFg+YLUs2X4bNXm5RAxWl00hYjFgNlv32vtHk= +github.com/nyaruka/phonenumbers v1.6.0/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU= +github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/recurly/recurly-client-go/v3 v3.17.0 h1:wJ9prRj6zZQWTvj2wiRETZ95SerzgceU3cPKRpumqiw= +github.com/recurly/recurly-client-go/v3 v3.17.0/go.mod h1:4qKAuNK6JbnLwhd7M3ZcD6Jbniq9M1ESBwSxbLaS9eQ= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200617212913-87be026d3888/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.67.2 h1:Lq11HW1nr5m4OYV+ZVy2BjOK78/zqnTx24vyDBP1JcQ= +google.golang.org/grpc v1.67.2/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/liquid.go b/liquid.go index 341e11ba..80877835 100644 --- a/liquid.go +++ b/liquid.go @@ -1,16 +1,15 @@ /* Package liquid is a pure Go implementation of Shopify Liquid templates, developed for use in https://github.com/osteele/gojekyll. -See the project README https://github.com/osteele/liquid for additional information and implementation status. - +See the project README https://github.com/autopilot3/liquid for additional information and implementation status. The liquid package itself is versioned in gopkg.in. Subpackages have no compatibility guarantees. Except where specifically documented, the “public” entities of subpackages are intended only for use by the liquid package and its subpackages. */ package liquid import ( - "github.com/osteele/liquid/render" - "github.com/osteele/liquid/tags" + "github.com/autopilot3/liquid/render" + "github.com/autopilot3/liquid/tags" ) // Bindings is a map of variable names to values. diff --git a/parser/ast.go b/parser/ast.go index 32568175..4009c18c 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -1,7 +1,7 @@ package parser import ( - "github.com/osteele/liquid/expressions" + "github.com/autopilot3/liquid/expressions" ) // ASTNode is a node of an AST. diff --git a/parser/config.go b/parser/config.go index 162f1179..5fb7d65c 100644 --- a/parser/config.go +++ b/parser/config.go @@ -1,6 +1,10 @@ package parser -import "github.com/osteele/liquid/expressions" +import ( + "context" + + "github.com/autopilot3/liquid/expressions" +) // A Config holds configuration information for parsing and rendering. type Config struct { @@ -10,6 +14,9 @@ type Config struct { } // NewConfig creates a parser Config. -func NewConfig(g Grammar) Config { - return Config{Grammar: g} +func NewConfig(g Grammar, ctx context.Context) Config { + return Config{ + Grammar: g, + Config: expressions.NewConfig(ctx), + } } diff --git a/parser/error.go b/parser/error.go index b50e78ca..ff6e7833 100644 --- a/parser/error.go +++ b/parser/error.go @@ -1,6 +1,8 @@ package parser -import "fmt" +import ( + "fmt" +) // An Error is a syntax error during template parsing. type Error interface { diff --git a/parser/parser.go b/parser/parser.go index 9211a15d..4adf70b2 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/osteele/liquid/expressions" + "github.com/autopilot3/liquid/expressions" ) // Parse parses a source template. It returns an AST root, that can be compiled and evaluated. diff --git a/render/blocks.go b/render/blocks.go index ce80c00f..3c790290 100644 --- a/render/blocks.go +++ b/render/blocks.go @@ -4,7 +4,7 @@ import ( "io" "sort" - "github.com/osteele/liquid/parser" + "github.com/autopilot3/liquid/parser" ) // BlockCompiler builds a renderer for the tag instance. diff --git a/render/compiler.go b/render/compiler.go index 745504cb..b0e19d1f 100644 --- a/render/compiler.go +++ b/render/compiler.go @@ -3,7 +3,7 @@ package render import ( "fmt" - "github.com/osteele/liquid/parser" + "github.com/autopilot3/liquid/parser" ) // Compile parses a source template. It returns an AST root, that can be evaluated. diff --git a/render/compiler_test.go b/render/compiler_test.go index 2c00023b..56f22743 100644 --- a/render/compiler_test.go +++ b/render/compiler_test.go @@ -5,7 +5,7 @@ import ( "io" "testing" - "github.com/osteele/liquid/parser" + "github.com/autopilot3/liquid/parser" "github.com/stretchr/testify/require" ) diff --git a/render/config.go b/render/config.go index ab66bf27..80867b54 100644 --- a/render/config.go +++ b/render/config.go @@ -1,13 +1,17 @@ package render import ( - "github.com/osteele/liquid/parser" + "context" + + "github.com/autopilot3/liquid/parser" ) // Config holds configuration information for parsing and rendering. type Config struct { parser.Config grammar + AllowedTags map[string]struct{} + AllowTagsWithDefault bool } type grammar struct { @@ -15,11 +19,20 @@ type grammar struct { blockDefs map[string]*blockSyntax } -// NewConfig creates a new Settings. func NewConfig() Config { + return NewConfigWitchContext(context.Background()) +} + +// NewConfig creates a new Settings. +func NewConfigWitchContext(ctx context.Context) Config { g := grammar{ tags: map[string]TagCompiler{}, blockDefs: map[string]*blockSyntax{}, } - return Config{parser.NewConfig(g), g} + return Config{ + parser.NewConfig(g, ctx), + g, + nil, + false, + } } diff --git a/render/context.go b/render/context.go index 0931924d..a0fc96b2 100644 --- a/render/context.go +++ b/render/context.go @@ -2,11 +2,11 @@ package render import ( "bytes" + "errors" "io" - "io/ioutil" "strings" - "github.com/osteele/liquid/expressions" + "github.com/autopilot3/liquid/expressions" ) // Context provides the rendering context for a tag renderer. @@ -49,6 +49,14 @@ type Context interface { TagName() string // WrapError creates a new error that records the source location from the current context. WrapError(err error) Error + + GetConfig() *Config + + IsFindVars() bool + + SetLoopVar(name string, sourceName string) int + RemoveLoopVar(key int) + SetVar(name string) } type rendererContext struct { @@ -57,6 +65,35 @@ type rendererContext struct { cn *BlockNode } +func (c rendererContext) IsFindVars() bool { + return c.ctx.findVariablesOnly +} + +func (c rendererContext) SetLoopVar(name string, sourceName string) int { + loopVars, ok := c.ctx.bindings[expressions.LoopVarsKey] + if !ok { + loopVars = expressions.NewLoopVars() + c.ctx.bindings[expressions.LoopVarsKey] = loopVars + } + return loopVars.(*expressions.LoopVarsStack).Set(name, sourceName) +} + +func (c rendererContext) SetVar(name string) { + assignedVars, ok := c.ctx.bindings[expressions.AssignedVarsKey] + if !ok { + assignedVars = make(map[string]struct{}) + c.ctx.bindings[expressions.AssignedVarsKey] = assignedVars + } + assignedVars.(map[string]struct{})[name] = struct{}{} +} + +func (c rendererContext) RemoveLoopVar(key int) { + loopVars, ok := c.ctx.bindings[expressions.LoopVarsKey] + if ok { + loopVars.(*expressions.LoopVarsStack).Remove(key) + } +} + func (c rendererContext) Errorf(format string, a ...interface{}) Error { return renderErrorf(c.node, format, a...) } @@ -109,27 +146,10 @@ func (c rendererContext) RenderChildren(w io.Writer) Error { return c.ctx.RenderSequence(w, c.cn.Body) } +// Deprecated: RenderFile parses and renders a template. It's used in the implementation of the {% include %} tag. +// We removed `include` tag to prevent reading files from the filesystem. func (c rendererContext) RenderFile(filename string, b map[string]interface{}) (string, error) { - source, err := ioutil.ReadFile(filename) - if err != nil { - return "", err - } - root, err := c.ctx.config.Compile(string(source), c.node.SourceLoc) - if err != nil { - return "", err - } - bindings := map[string]interface{}{} - for k, v := range c.ctx.bindings { - bindings[k] = v - } - for k, v := range b { - bindings[k] = v - } - buf := new(bytes.Buffer) - if err := Render(root, buf, bindings, c.ctx.config); err != nil { - return "", err - } - return buf.String(), nil + return "", errors.New("RenderFile is deprecated and removed") } // InnerString renders the children to a string. @@ -171,3 +191,7 @@ func (c rendererContext) TagName() string { return "" } } + +func (c rendererContext) GetConfig() *Config { + return &c.ctx.config +} diff --git a/render/context_test.go b/render/context_test.go index e5559258..8b4ea4e1 100644 --- a/render/context_test.go +++ b/render/context_test.go @@ -4,11 +4,9 @@ import ( "bytes" "fmt" "io" - "io/ioutil" - "os" "testing" - "github.com/osteele/liquid/parser" + "github.com/autopilot3/liquid/parser" "github.com/stretchr/testify/require" ) @@ -46,16 +44,6 @@ func addContextTestTags(s Config) { return err }, nil }) - s.AddTag("test_render_file", func(filename string) (func(w io.Writer, c Context) error, error) { - return func(w io.Writer, c Context) error { - s, err := c.RenderFile(filename, map[string]interface{}{"shadowed": 2}) - if err != nil { - return err - } - _, err = io.WriteString(w, s) - return err - }, nil - }) } var contextTests = []struct{ in, out string }{ @@ -64,16 +52,12 @@ var contextTests = []struct{ in, out string }{ {`{% test_expand_tag_arg x %}`, "x"}, {`{% test_expand_tag_arg {{x}} %}`, "123"}, {`{% test_tag_name %}`, "test_tag_name"}, - {`{% test_render_file testdata/render_file.txt %}; unshadowed={{ shadowed }}`, - "rendered shadowed=2; unshadowed=1"}, } var contextErrorTests = []struct{ in, expect string }{ {`{% test_evaluate_string syntax error %}`, "syntax error"}, {`{% test_expand_tag_arg {{ syntax error }} %}`, "syntax error"}, {`{% test_expand_tag_arg {{ x | undefined_filter }} %}`, "undefined filter"}, - {`{% test_render_file testdata/render_file_syntax_error.txt %}`, "syntax error"}, - {`{% test_render_file testdata/render_file_runtime_error.txt %}`, "undefined tag"}, } var contextTestBindings = map[string]interface{}{ @@ -103,23 +87,9 @@ func TestContext_errors(t *testing.T) { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { root, err := cfg.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) - err = Render(root, ioutil.Discard, contextTestBindings, cfg) + err = Render(root, io.Discard, contextTestBindings, cfg) require.Errorf(t, err, test.in) require.Containsf(t, err.Error(), test.expect, test.in) }) } } - -func TestContext_file_not_found_error(t *testing.T) { - // Test the cause instead of looking for a string, since the error message is different - // between Darwin and Linux ("no such file") and Windows ("The system cannot find the file specified"), at least. - // - // Also see TestIncludeTag_file_not_found_error. - cfg := NewConfig() - addContextTestTags(cfg) - root, err := cfg.Compile(`{% test_render_file testdata/missing_file %}`, parser.SourceLoc{}) - require.NoError(t, err) - err = Render(root, ioutil.Discard, contextTestBindings, cfg) - require.Error(t, err) - require.True(t, os.IsNotExist(err.Cause())) -} diff --git a/render/error.go b/render/error.go index 649d79d6..7255be38 100644 --- a/render/error.go +++ b/render/error.go @@ -1,7 +1,7 @@ package render import ( - "github.com/osteele/liquid/parser" + "github.com/autopilot3/liquid/parser" ) // An Error is an error during template rendering. diff --git a/render/node_context.go b/render/node_context.go index b71b90b9..652031ae 100644 --- a/render/node_context.go +++ b/render/node_context.go @@ -1,7 +1,7 @@ package render import ( - "github.com/osteele/liquid/expressions" + "github.com/autopilot3/liquid/expressions" ) // nodeContext provides the evaluation context for rendering the AST. @@ -9,8 +9,9 @@ import ( // This type has a clumsy name so that render.Context, in the public API, can // have a clean name that doesn't stutter. type nodeContext struct { - bindings map[string]interface{} - config Config + bindings map[string]interface{} + config Config + findVariablesOnly bool } // newNodeContext creates a new evaluation context. @@ -21,10 +22,24 @@ func newNodeContext(scope map[string]interface{}, c Config) nodeContext { for k, v := range scope { vars[k] = v } - return nodeContext{vars, c} + return nodeContext{ + bindings: vars, + config: c, + } +} + +func newFindVariablesNodeContext(c Config) nodeContext { + return nodeContext{ + bindings: make(map[string]interface{}), + config: c, + findVariablesOnly: true, + } } // Evaluate evaluates an expression within the template context. func (c nodeContext) Evaluate(expr expressions.Expression) (out interface{}, err error) { + if c.findVariablesOnly { + return expr.Evaluate(expressions.NewVariablesContext(c.bindings, c.config.Config.Config)) + } return expr.Evaluate(expressions.NewContext(c.bindings, c.config.Config.Config)) } diff --git a/render/nodes.go b/render/nodes.go index 6695d2fa..5282cc7d 100644 --- a/render/nodes.go +++ b/render/nodes.go @@ -3,8 +3,8 @@ package render import ( "io" - "github.com/osteele/liquid/expressions" - "github.com/osteele/liquid/parser" + "github.com/autopilot3/liquid/expressions" + "github.com/autopilot3/liquid/parser" ) // Node is a node of the render tree. diff --git a/render/render.go b/render/render.go index 3e8b8129..e3b455e2 100644 --- a/render/render.go +++ b/render/render.go @@ -5,9 +5,11 @@ import ( "fmt" "io" "reflect" + "strings" "time" - "github.com/osteele/liquid/values" + "github.com/autopilot3/liquid/expressions" + "github.com/autopilot3/liquid/values" ) // Render renders the render tree. @@ -22,6 +24,28 @@ func Render(node Node, w io.Writer, vars map[string]interface{}, c Config) Error return nil } +func FindVariables(node Node, c Config) (map[string]interface{}, Error) { + tw := trimWriter{w: io.Discard} + ctx := newFindVariablesNodeContext(c) + if err := node.render(&tw, ctx); err != nil { + return nil, err + } + for k, v := range ctx.bindings { + if v == nil { + delete(ctx.bindings, k) + } + } + delete(ctx.bindings, expressions.LatestVarNameKey) + delete(ctx.bindings, expressions.LoopVarsKey) + if assignedVars, ok := ctx.bindings[expressions.AssignedVarsKey]; ok { + for name := range assignedVars.(map[string]struct{}) { + delete(ctx.bindings, name) + } + delete(ctx.bindings, expressions.AssignedVarsKey) + } + return ctx.bindings, nil +} + // RenderASTSequence renders a sequence of nodes. func (c nodeContext) RenderSequence(w io.Writer, seq []Node) Error { tw := trimWriter{w: w} @@ -47,6 +71,13 @@ func (n *BlockNode) render(w *trimWriter, ctx nodeContext) Error { panic(fmt.Errorf("unset renderer for %v", n)) } err := renderer(w, rendererContext{ctx, nil, n}) + + if len(ctx.config.AllowedTags) > 0 { + end, ok := ctx.config.findBlockDef("end" + n.Name) + if ok { + w.Write([]byte("{% " + end.TagName() + " %}")) + } + } return wrapRenderError(err, n) } @@ -61,15 +92,49 @@ func (n *RawNode) render(w *trimWriter, ctx nodeContext) Error { } func (n *ObjectNode) render(w *trimWriter, ctx nodeContext) Error { - w.TrimLeft(n.TrimLeft) - value, err := ctx.Evaluate(n.expr) - if err != nil { - return wrapRenderError(err, n) - } - if err := wrapRenderError(writeObject(w, value), n); err != nil { - return err + var ( + value interface{} + err error + ) + if len(ctx.config.AllowedTags) != 0 { + allowed := false + if ctx.config.AllowTagsWithDefault && strings.Contains(n.Source, "default:") { + allowed = true + } + if !allowed { + for tag := range ctx.config.AllowedTags { + if strings.Contains(n.Source, tag) { + allowed = true + break + } + } + } + if allowed { + w.TrimLeft(n.TrimLeft) + value, err = ctx.Evaluate(n.expr) + if err != nil { + return wrapRenderError(err, n) + } + if err := wrapRenderError(writeObject(w, value), n); err != nil { + return err + } + w.TrimRight(n.TrimRight) + } else { + w.TrimLeft(n.TrimLeft) + writeObject(w, n.Source) + w.TrimRight(n.TrimRight) + } + } else { + w.TrimLeft(n.TrimLeft) + value, err = ctx.Evaluate(n.expr) + if err != nil { + return wrapRenderError(err, n) + } + if err := wrapRenderError(writeObject(w, value), n); err != nil { + return err + } + w.TrimRight(n.TrimRight) } - w.TrimRight(n.TrimRight) return nil } @@ -116,7 +181,7 @@ func writeObject(w io.Writer, value interface{}) error { for i := 0; i < rt.Len(); i++ { item := rt.Index(i) if item.IsValid() { - if err := writeObject(w, item.Interface()); err != nil { + if err := writeArray(w, item.Interface(), i); err != nil { return err } } @@ -129,3 +194,60 @@ func writeObject(w io.Writer, value interface{}) error { return err } } + +func writeArray(w io.Writer, value interface{}, idx int) error { + if value == nil { + return nil + } + value = values.ToLiquid(value) + if value == nil { + return nil + } + writeJoin := func() error { + if idx > 0 { + _, err := io.WriteString(w, ", ") + if err != nil { + return err + } + } + return nil + } + + switch value := value.(type) { + case time.Time: + if err := writeJoin(); err != nil { + return err + } + _, err := io.WriteString(w, value.Format("2006-01-02 15:04:05 -0700")) + return err + case []byte: + if err := writeJoin(); err != nil { + return err + } + _, err := w.Write(value) + return err + // there used be a case on fmt.Stringer here, but fmt.Sprint produces better results than obj.Write + // for instances of error and *string + } + rt := reflect.ValueOf(value) + switch rt.Kind() { + case reflect.Array, reflect.Slice: + for i := 0; i < rt.Len(); i++ { + item := rt.Index(i) + if item.IsValid() { + if err := writeArray(w, item.Interface(), i); err != nil { + return err + } + } + } + return nil + case reflect.Ptr: + return writeObject(w, reflect.ValueOf(value).Elem()) + default: + if err := writeJoin(); err != nil { + return err + } + _, err := io.WriteString(w, fmt.Sprint(value)) + return err + } +} diff --git a/render/render_test.go b/render/render_test.go index db8d334a..31655277 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -4,11 +4,10 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "testing" "time" - "github.com/osteele/liquid/parser" + "github.com/autopilot3/liquid/parser" "github.com/stretchr/testify/require" ) @@ -21,12 +20,14 @@ var renderTests = []struct{ in, out string }{ {`{{ 12.3 }}`, "12.3"}, {`{{ date }}`, "2015-07-17 15:04:05 +0000"}, {`{{ "string" }}`, "string"}, - {`{{ array }}`, "firstsecondthird"}, + {`{{ array }}`, "first, second, third"}, + {`{{ array_bool }}`, "true, false, true"}, // variables and properties {`{{ int }}`, "123"}, {`{{ page.title }}`, "Introduction"}, {`{{ array[1] }}`, "second"}, + {`{{ array_bool[1] }}`, "false"}, // whitespace control {` {{ 1 }} `, " 1 "}, @@ -57,9 +58,10 @@ var renderErrorTests = []struct{ in, out string }{ } var renderTestBindings = map[string]interface{}{ - "array": []string{"first", "second", "third"}, - "date": time.Date(2015, 7, 17, 15, 4, 5, 123456789, time.UTC), - "int": 123, + "array": []string{"first", "second", "third"}, + "array_bool": []bool{true, false, true}, + "date": time.Date(2015, 7, 17, 15, 4, 5, 123456789, time.UTC), + "int": 123, "sort_prop": []map[string]interface{}{ {"weight": 1}, {"weight": 5}, @@ -104,7 +106,7 @@ func TestRenderErrors(t *testing.T) { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { root, err := cfg.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) - err = Render(root, ioutil.Discard, renderTestBindings, cfg) + err = Render(root, io.Discard, renderTestBindings, cfg) require.Errorf(t, err, test.in) require.Containsf(t, err.Error(), test.out, test.in) }) diff --git a/tags/control_flow_tags.go b/tags/control_flow_tags.go index 6639a393..246890c9 100644 --- a/tags/control_flow_tags.go +++ b/tags/control_flow_tags.go @@ -3,9 +3,9 @@ package tags import ( "io" - e "github.com/osteele/liquid/expressions" - "github.com/osteele/liquid/render" - "github.com/osteele/liquid/values" + e "github.com/autopilot3/liquid/expressions" + "github.com/autopilot3/liquid/render" + "github.com/autopilot3/liquid/values" ) type caseInterpreter interface { @@ -62,12 +62,22 @@ func caseTagCompiler(node render.BlockNode) (func(io.Writer, render.Context) err if err != nil { return err } + + cfg := ctx.GetConfig() + if len(cfg.AllowedTags) > 0 && len(cases) > 0 { + return ctx.RenderBlock(w, cases[0].body()) + } + for _, clause := range cases { b, err := clause.test(sel, ctx) if err != nil { return err } - if b { + if ctx.IsFindVars() { + if err := ctx.RenderBlock(w, clause.body()); err != nil { + return err + } + } else if b { return ctx.RenderBlock(w, clause.body()) } } @@ -106,12 +116,24 @@ func ifTagCompiler(polarity bool) func(render.BlockNode) (func(io.Writer, render branches = append(branches, branchRec{test, c}) } return func(w io.Writer, ctx render.Context) error { + cfg := ctx.GetConfig() + if len(cfg.AllowedTags) > 0 && len(branches) > 0 { + for _, b := range branches { + w.Write([]byte(b.body.SourceText())) + ctx.RenderBlock(w, b.body) + } + return nil + } for _, b := range branches { value, err := ctx.Evaluate(b.test) if err != nil { return err } - if value != nil && value != false { + if ctx.IsFindVars() { + if err := ctx.RenderBlock(w, b.body); err != nil { + return err + } + } else if value != nil && value != false { return ctx.RenderBlock(w, b.body) } } diff --git a/tags/control_flow_tags_test.go b/tags/control_flow_tags_test.go index 8e357167..949feaf1 100644 --- a/tags/control_flow_tags_test.go +++ b/tags/control_flow_tags_test.go @@ -4,11 +4,10 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "testing" - "github.com/osteele/liquid/parser" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/parser" + "github.com/autopilot3/liquid/render" "github.com/stretchr/testify/require" ) @@ -93,7 +92,7 @@ func TestControlFlowTags_errors(t *testing.T) { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { root, err := cfg.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) - err = render.Render(root, ioutil.Discard, tagTestBindings, cfg) + err = render.Render(root, io.Discard, tagTestBindings, cfg) require.Errorf(t, err, test.in) require.Contains(t, err.Error(), test.expected, test.in) }) diff --git a/tags/include_tag.go b/tags/include_tag.go deleted file mode 100644 index 6442a248..00000000 --- a/tags/include_tag.go +++ /dev/null @@ -1,31 +0,0 @@ -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 a 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 ctx.Errorf("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 = io.WriteString(w, s) - return err - }, nil -} diff --git a/tags/include_tag_test.go b/tags/include_tag_test.go deleted file mode 100644 index 52647ab1..00000000 --- a/tags/include_tag_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package tags - -import ( - "bytes" - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/osteele/liquid/parser" - "github.com/osteele/liquid/render" - "github.com/stretchr/testify/require" -) - -var includeTestBindings = map[string]interface{}{ - "test": true, - "var": "value", -} - -func TestIncludeTag(t *testing.T) { - config := render.NewConfig() - loc := parser.SourceLoc{Pathname: "testdata/include_source.html", LineNo: 1} - AddStandardTags(config) - - // basic functionality - root, err := config.Compile(`{% include "include_target.html" %}`, loc) - require.NoError(t, err) - buf := new(bytes.Buffer) - err = render.Render(root, buf, includeTestBindings, config) - require.NoError(t, err) - require.Equal(t, "include target", strings.TrimSpace(buf.String())) - - // tag and variable - root, err = config.Compile(`{% include "include_target_2.html" %}`, loc) - require.NoError(t, err) - buf = new(bytes.Buffer) - err = render.Render(root, buf, includeTestBindings, config) - require.NoError(t, err) - require.Equal(t, "value", strings.TrimSpace(buf.String())) - - // errors - root, err = config.Compile(`{% include 10 %}`, loc) - require.NoError(t, err) - err = render.Render(root, ioutil.Discard, includeTestBindings, config) - require.Error(t, err) - require.Contains(t, err.Error(), "requires a string") -} - -func TestIncludeTag_file_not_found_error(t *testing.T) { - config := render.NewConfig() - loc := parser.SourceLoc{Pathname: "testdata/include_source.html", LineNo: 1} - AddStandardTags(config) - - // See the comment in TestIncludeTag_file_not_found_error. - root, err := config.Compile(`{% include "missing_file.html" %}`, loc) - require.NoError(t, err) - err = render.Render(root, ioutil.Discard, includeTestBindings, config) - require.Error(t, err) - require.True(t, os.IsNotExist(err.Cause())) -} diff --git a/tags/iteration_tags.go b/tags/iteration_tags.go index 581ff851..81a9b0ea 100644 --- a/tags/iteration_tags.go +++ b/tags/iteration_tags.go @@ -8,8 +8,8 @@ import ( yaml "gopkg.in/yaml.v2" - "github.com/osteele/liquid/expressions" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/expressions" + "github.com/autopilot3/liquid/render" ) // An IterationKeyedMap is a map that yields its keys, instead of (key, value) pairs, when iterated. @@ -81,15 +81,25 @@ func (loop loopRenderer) render(w io.Writer, ctx render.Context) error { if err != nil { return err } + var loopVarIdx int + if ctx.IsFindVars() { + loopSourceVarName := ctx.Get(expressions.LatestVarNameKey).(string) + val = []map[string]interface{}{ + {}, // empty object + } + loopVarIdx = ctx.SetLoopVar(loop.Variable, loopSourceVarName) + } iter := makeIterator(val) if iter == nil { return nil } iter = applyLoopModifiers(loop.Loop, iter) + // shallow-bind the loop variables; restore on exit defer func(index, forloop interface{}) { ctx.Set(forloopVarName, index) ctx.Set(loop.Variable, forloop) + ctx.RemoveLoopVar(loopVarIdx) }(ctx.Get(forloopVarName), ctx.Get(loop.Variable)) cycleMap := map[string]int{} loop: diff --git a/tags/iteration_tags_test.go b/tags/iteration_tags_test.go index d3acfecc..89b970b5 100644 --- a/tags/iteration_tags_test.go +++ b/tags/iteration_tags_test.go @@ -3,15 +3,15 @@ package tags import ( "bytes" "fmt" - "io/ioutil" + "io" "regexp" "strings" "testing" yaml "gopkg.in/yaml.v2" - "github.com/osteele/liquid/parser" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/parser" + "github.com/autopilot3/liquid/render" "github.com/stretchr/testify/require" ) @@ -157,7 +157,7 @@ func TestIterationTags_errors(t *testing.T) { t.Run(fmt.Sprintf("%02d", i+1+len(iterationSyntaxErrorTests)), func(t *testing.T) { root, err := cfg.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) - err = render.Render(root, ioutil.Discard, iterationTestBindings, cfg) + err = render.Render(root, io.Discard, iterationTestBindings, cfg) require.Errorf(t, err, test.in) require.Containsf(t, err.Error(), test.expected, test.in) }) diff --git a/tags/standard_tags.go b/tags/standard_tags.go index 38b3e5ab..06295621 100644 --- a/tags/standard_tags.go +++ b/tags/standard_tags.go @@ -4,14 +4,13 @@ package tags import ( "io" - "github.com/osteele/liquid/expressions" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/expressions" + "github.com/autopilot3/liquid/render" ) // 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, @@ -39,7 +38,9 @@ func assignTag(source string) (func(io.Writer, render.Context) error, error) { if err != nil { return err } - _ = value + if ctx.IsFindVars() { + ctx.SetVar(stmt.Assignment.Variable) + } ctx.Set(stmt.Assignment.Variable, value) return nil }, nil diff --git a/tags/standard_tags_test.go b/tags/standard_tags_test.go index a0cbf6c6..04041bab 100644 --- a/tags/standard_tags_test.go +++ b/tags/standard_tags_test.go @@ -3,11 +3,11 @@ package tags import ( "bytes" "fmt" - "io/ioutil" + "io" "testing" - "github.com/osteele/liquid/parser" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/parser" + "github.com/autopilot3/liquid/render" "github.com/stretchr/testify/require" ) @@ -100,7 +100,7 @@ func TestStandardTags_render_errors(t *testing.T) { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { root, err := config.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) - err = render.Render(root, ioutil.Discard, tagTestBindings, config) + err = render.Render(root, io.Discard, tagTestBindings, config) require.Errorf(t, err, test.in) require.Containsf(t, err.Error(), test.expected, test.in) }) diff --git a/template.go b/template.go index 0628272a..27bbc597 100644 --- a/template.go +++ b/template.go @@ -3,8 +3,8 @@ package liquid import ( "bytes" - "github.com/osteele/liquid/parser" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/parser" + "github.com/autopilot3/liquid/render" ) // A Template is a compiled Liquid template. It knows how to evaluate itself within a variable binding environment, to create a rendered byte slice. @@ -24,6 +24,12 @@ func newTemplate(cfg *render.Config, source []byte, path string, line int) (*Tem return &Template{root, cfg}, nil } +// GetRoot returns the root node of the abstract syntax tree (AST) representing +// the parsed template. +func (t *Template) GetRoot() render.Node { + return t.root +} + // Render executes the template with the specified variable bindings. func (t *Template) Render(vars Bindings) ([]byte, SourceError) { buf := new(bytes.Buffer) @@ -42,3 +48,7 @@ func (t *Template) RenderString(b Bindings) (string, SourceError) { } return string(bs), nil } + +func (t *Template) FindVariables() (map[string]interface{}, SourceError) { + return render.FindVariables(t.root, *t.cfg) +} diff --git a/template_test.go b/template_test.go index 1f164496..5dc5cfd9 100644 --- a/template_test.go +++ b/template_test.go @@ -5,7 +5,7 @@ import ( "sync" "testing" - "github.com/osteele/liquid/render" + "github.com/autopilot3/liquid/render" "github.com/stretchr/testify/require" ) diff --git a/values/compare.go b/values/compare.go index 4edbf440..09124974 100644 --- a/values/compare.go +++ b/values/compare.go @@ -2,6 +2,10 @@ package values import ( "reflect" + "time" + + "github.com/autopilot3/ap3-types-go/types/date" + "github.com/autopilot3/ap3-types-go/types/phone" ) var ( @@ -16,6 +20,29 @@ func Equal(a, b interface{}) bool { // nolint: gocyclo return a == b } ra, rb := reflect.ValueOf(a), reflect.ValueOf(b) + // time comparison + if ra.Kind() == reflect.Struct && ra.Type() == reflect.TypeOf(time.Time{}) { + // we have a time comparison, try to convert b to time.Time + // there should be only two cases: b is a user input string or a time.Time which is our variabeles from crm + if rb.Kind() == reflect.String { + db, err := ParseDate(rb.String()) + if err == nil { + return ra.Interface().(time.Time).Equal(db) + } else { + return false + } + } else if rb.Kind() == reflect.Struct && rb.Type() == reflect.TypeOf(time.Time{}) { + return ra.Interface().(time.Time).Equal(rb.Interface().(time.Time)) + } + } + // phone comparison + if ra.Kind() == reflect.Struct && ra.Type() == reflect.TypeOf(phone.International{}) { + if rb.Kind() == reflect.String { + return ra.Interface().(phone.International).String() == rb.String() + } else if rb.Kind() == reflect.Struct && rb.Type() == reflect.TypeOf(phone.International{}) { + return ra.Interface().(phone.International).Equal(rb.Interface().(phone.International)) + } + } switch joinKind(ra.Kind(), rb.Kind()) { case reflect.Array, reflect.Slice: if ra.Len() != rb.Len() { @@ -48,10 +75,80 @@ func Equal(a, b interface{}) bool { // nolint: gocyclo // Less returns a bool indicating whether a < b. func Less(a, b interface{}) bool { a, b = ToLiquid(a), ToLiquid(b) - if a == nil || b == nil { + if a == nil && b == nil { + return false + } else if a == nil { + if reflect.ValueOf(b).Kind() == reflect.String { + return "" < b.(string) + } else if reflect.ValueOf(b).Kind() == reflect.Int || reflect.ValueOf(b).Kind() == reflect.Int8 || reflect.ValueOf(b).Kind() == reflect.Int16 || reflect.ValueOf(b).Kind() == reflect.Int32 || reflect.ValueOf(b).Kind() == reflect.Int64 { + rb := reflect.ValueOf(b).Convert(int64Type).Int() + return 0 < rb + } else if reflect.ValueOf(b).Kind() == reflect.Float32 || reflect.ValueOf(b).Kind() == reflect.Float64 { + rb := reflect.ValueOf(b).Convert(float64Type).Float() + return 0 < rb + } + return false + } else if b == nil { + if reflect.ValueOf(a).Kind() == reflect.String { + return a.(string) < "" + } else if reflect.ValueOf(a).Kind() == reflect.Int || reflect.ValueOf(a).Kind() == reflect.Int8 || reflect.ValueOf(a).Kind() == reflect.Int16 || reflect.ValueOf(a).Kind() == reflect.Int32 || reflect.ValueOf(a).Kind() == reflect.Int64 { + ra := reflect.ValueOf(a).Convert(int64Type).Int() + return ra < 0 + } else if reflect.ValueOf(a).Kind() == reflect.Float32 || reflect.ValueOf(a).Kind() == reflect.Float64 { + ra := reflect.ValueOf(a).Convert(float64Type).Float() + return ra < 0 + } return false } ra, rb := reflect.ValueOf(a), reflect.ValueOf(b) + // time comparison + if ra.Kind() == reflect.Struct { + if ra.Type() == reflect.TypeOf(time.Time{}) { + // we have a time comparison, try to convert b to time.time + // there should be only two cases: b is a user input string or a time.Time which is our variabeles from crm + if rb.Kind() == reflect.String { + db, err := ParseDate(rb.String()) + if err == nil { + return ra.Interface().(time.Time).Before(db) + } + } else if rb.Kind() == reflect.Struct && rb.Type() == reflect.TypeOf(time.Time{}) { + return ra.Interface().(time.Time).Before(rb.Interface().(time.Time)) + } + } + } else if rb.Kind() == reflect.Struct { + if rb.Type() == reflect.TypeOf(time.Time{}) { + // we have a time comparison, try to convert a to time.time + // there should be only two cases: a is a user input string or a time.Time which is our variabeles from crm + if ra.Kind() == reflect.String { + da, err := ParseDate(ra.String()) + if err == nil { + return da.Before(rb.Interface().(time.Time)) + } + } else if ra.Kind() == reflect.Struct && ra.Type() == reflect.TypeOf(time.Time{}) { + return ra.Interface().(time.Time).Before(rb.Interface().(time.Time)) + } + } + } + // date comparison only for date.Date vs string case, since date.Date is of kind int so naturally two date.Date can be compared + dVar := date.Date(1) + if reflect.TypeOf(a) == reflect.TypeOf(dVar) { + if rb.Kind() == reflect.String { + db, err := ParseDate(rb.String()) + if err == nil { + d := date.NewFromUTCTime(db) + return a.(date.Date) < d + } + } + } else if reflect.TypeOf(b) == reflect.TypeOf(dVar) { + if ra.Kind() == reflect.String { + da, err := ParseDate(ra.String()) + if err == nil { + d := date.NewFromUTCTime(da) + return d < b.(date.Date) + } + } + } + switch joinKind(ra.Kind(), rb.Kind()) { case reflect.Bool: return !ra.Bool() && rb.Bool() diff --git a/values/compare_test.go b/values/compare_test.go index b199d462..03fcdf87 100644 --- a/values/compare_test.go +++ b/values/compare_test.go @@ -3,6 +3,7 @@ package values import ( "fmt" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -37,6 +38,9 @@ var eqTests = []struct { {[]string{"a", "b"}, []string{"a", "c"}, false}, {[]interface{}{1.0, 2}, []interface{}{1, 2.0}, true}, {eqTestObj, eqTestObj, true}, + {time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), true}, + {time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), "2023-01-01", false}, + {time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), "2021-01-01 00:00:00 UTC", true}, } func TestEqual(t *testing.T) { diff --git a/values/convert.go b/values/convert.go index 4962c22b..2e6be0c2 100644 --- a/values/convert.go +++ b/values/convert.go @@ -80,7 +80,7 @@ func Convert(value interface{}, typ reflect.Type) (interface{}, error) { // noli // } switch typ.Kind() { case reflect.Bool: - return !(value == nil || value == false), nil + return value != nil && value != false, nil case reflect.Uint: v, err := convertValueToInt(value, typ) return uint(v), err diff --git a/values/drop_test.go b/values/drop_test.go index c55a2381..64981c38 100644 --- a/values/drop_test.go +++ b/values/drop_test.go @@ -56,7 +56,7 @@ func BenchmarkDrop_Resolve_3(b *testing.B) { values <- d.Int() } for i := cap(values); i > 0; i-- { - _ = <-values + <-values } } } diff --git a/values/evaluator_test.go b/values/evaluator_test.go index d45d7291..5d3a262c 100644 --- a/values/evaluator_test.go +++ b/values/evaluator_test.go @@ -25,6 +25,18 @@ var lessTests = []struct { {"a", "b", true}, {"b", "a", false}, {[]string{"a"}, []string{"a"}, false}, + // comparisons involving 1 nil and the other non-nil, nil will be default to 0 for number/string comparison. Otherwise all false + {nil, 0, false}, + {nil, 1, true}, + {nil, -1, false}, + {nil, 1.1, true}, + {nil, -1.1, false}, + {1.1, nil, false}, + {0, nil, false}, + {-1, nil, true}, + {nil, "", false}, + {nil, "a", true}, + {nil, struct{}{}, false}, } func TestLess(t *testing.T) { diff --git a/values/value.go b/values/value.go index 6bee0be0..a3f938fe 100644 --- a/values/value.go +++ b/values/value.go @@ -6,6 +6,8 @@ import ( "strings" yaml "gopkg.in/yaml.v2" + + "github.com/autopilot3/ap3-helpers-go/language" ) // A Value is a Liquid runtime value. @@ -107,6 +109,7 @@ func (v wrapperValue) Int() int { } // interned values +var NilValue = wrapperValue{nil} var nilValue = wrapperValue{nil} var falseValue = wrapperValue{false} var trueValue = wrapperValue{true} @@ -213,7 +216,12 @@ func (sv stringValue) Contains(substr Value) bool { if !ok { s = fmt.Sprint(substr.Interface()) } - return strings.Contains(sv.value.(string), s) + switch sv.value.(type) { + case language.LanguageCode: + return strings.Contains(string(sv.value.(language.LanguageCode)), s) + default: + return strings.Contains(sv.value.(string), s) + } } func (sv stringValue) PropertyValue(iv Value) Value {