From 8a49c0b3b8b5a374a64b639f46806192cd663fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 30 Apr 2017 21:52:56 +0200 Subject: [PATCH] tpl/collections: Make it a package that stands on its own See #3042 --- deps/deps.go | 13 +++-- hugolib/alias.go | 4 +- hugolib/hugo_sites_build.go | 2 +- hugolib/page.go | 2 +- hugolib/shortcode.go | 2 +- hugolib/site.go | 2 +- tpl/collections/apply.go | 9 +++- tpl/collections/apply_test.go | 80 ++++++++++-------------------- tpl/collections/collections.go | 13 ----- tpl/collections/init.go | 72 +++++++++++++++++++++++++++ tpl/template.go | 4 ++ tpl/tplimpl/template.go | 5 ++ tpl/tplimpl/templateFuncster.go | 39 +++++++-------- tpl/tplimpl/template_funcs.go | 34 +++---------- tpl/tplimpl/template_funcs_test.go | 16 ------ 15 files changed, 156 insertions(+), 141 deletions(-) create mode 100644 tpl/collections/init.go diff --git a/deps/deps.go b/deps/deps.go index 99763c115..5f016c4d7 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -20,8 +20,8 @@ type Deps struct { // The logger to use. Log *jww.Notepad `json:"-"` - // The templates to use. - Tmpl tpl.TemplateHandler `json:"-"` + // The templates to use. This will usually implement the full tpl.TemplateHandler. + Tmpl tpl.TemplateFinder `json:"-"` // The file systems to use. Fs *hugofs.Fs `json:"-"` @@ -55,6 +55,10 @@ type ResourceProvider interface { Clone(deps *Deps) error } +func (d *Deps) TemplateHandler() tpl.TemplateHandler { + return d.Tmpl.(tpl.TemplateHandler) +} + func (d *Deps) LoadResources() error { // Note that the translations need to be loaded before the templates. if err := d.translationProvider.Update(d); err != nil { @@ -64,7 +68,10 @@ func (d *Deps) LoadResources() error { if err := d.templateProvider.Update(d); err != nil { return err } - d.Tmpl.PrintErrors() + + if th, ok := d.Tmpl.(tpl.TemplateHandler); ok { + th.PrintErrors() + } return nil } diff --git a/hugolib/alias.go b/hugolib/alias.go index d1a1b5534..20be7c732 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -44,12 +44,12 @@ func init() { } type aliasHandler struct { - t tpl.TemplateHandler + t tpl.TemplateFinder log *jww.Notepad allowRoot bool } -func newAliasHandler(t tpl.TemplateHandler, l *jww.Notepad, allowRoot bool) aliasHandler { +func newAliasHandler(t tpl.TemplateFinder, l *jww.Notepad, allowRoot bool) aliasHandler { return aliasHandler{t, l, allowRoot} } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index e694ab52f..58088fd7c 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -111,7 +111,7 @@ func (h *HugoSites) initRebuild(config *BuildCfg) error { // This is for the non-renderable content pages (rarely used, I guess). // We could maybe detect if this is really needed, but it should be // pretty fast. - h.Tmpl.RebuildClone() + h.TemplateHandler().RebuildClone() } for _, s := range h.Sites { diff --git a/hugolib/page.go b/hugolib/page.go index 1e9c06af7..bed2e254e 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1389,7 +1389,7 @@ func (p *Page) prepareLayouts() error { if p.Kind == KindPage { if !p.IsRenderable() { self := "__" + p.UniqueID() - err := p.s.Tmpl.AddLateTemplate(self, string(p.Content)) + err := p.s.TemplateHandler().AddLateTemplate(self, string(p.Content)) if err != nil { return err } diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index d72a96faa..01beffe2b 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -565,7 +565,7 @@ func replaceShortcodeTokens(source []byte, prefix string, replacements map[strin return source, nil } -func getShortcodeTemplate(name string, t tpl.TemplateHandler) *tpl.TemplateAdapter { +func getShortcodeTemplate(name string, t tpl.TemplateFinder) *tpl.TemplateAdapter { isInnerShortcodeCache.RLock() defer isInnerShortcodeCache.RUnlock() diff --git a/hugolib/site.go b/hugolib/site.go index 394549c41..4f7c2c5fb 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -676,7 +676,7 @@ func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { s.Log.ERROR.Println(err) } - s.Tmpl.PrintErrors() + s.TemplateHandler().PrintErrors() for i := 1; i < len(sites); i++ { site := sites[i] diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go index cb4dfa64e..562d70828 100644 --- a/tpl/collections/apply.go +++ b/tpl/collections/apply.go @@ -18,6 +18,8 @@ import ( "fmt" "reflect" "strings" + + "github.com/spf13/hugo/tpl" ) // Apply takes a map, array, or slice and returns a new slice with the function fname applied over it. @@ -104,7 +106,12 @@ func applyFnToThis(fn, this reflect.Value, args ...interface{}) (reflect.Value, func (ns *Namespace) lookupFunc(fname string) (reflect.Value, bool) { if !strings.ContainsRune(fname, '.') { - fn, found := ns.funcMap[fname] + templ, ok := ns.deps.Tmpl.(tpl.TemplateFuncsGetter) + if !ok { + panic("Needs a tpl.TemplateFuncsGetter") + } + fm := templ.GetFuncs() + fn, found := fm[fname] if !found { return reflect.Value{}, false } diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go index 9718570fd..ee570993e 100644 --- a/tpl/collections/apply_test.go +++ b/tpl/collections/apply_test.go @@ -14,79 +14,51 @@ package collections import ( - "fmt" - "html/template" "testing" + "fmt" + "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/tpl/strings" - "github.com/stretchr/testify/assert" + "github.com/spf13/hugo/tpl" + "github.com/stretchr/testify/require" ) +type templateFinder int + +func (templateFinder) Lookup(name string) *tpl.TemplateAdapter { + return nil +} + +func (templateFinder) GetFuncs() map[string]interface{} { + return map[string]interface{}{ + "print": fmt.Sprint, + } +} + func TestApply(t *testing.T) { t.Parallel() - hstrings := strings.New(&deps.Deps{}) - - ns := New(&deps.Deps{}) - ns.Funcs(template.FuncMap{ - "apply": ns.Apply, - "chomp": hstrings.Chomp, - "strings": hstrings, - "print": fmt.Sprint, - }) + ns := New(&deps.Deps{Tmpl: new(templateFinder)}) strings := []interface{}{"a\n", "b\n"} - noStringers := []interface{}{tstNoStringer{}, tstNoStringer{}} - result, _ := ns.Apply(strings, "chomp", ".") - assert.Equal(t, []interface{}{template.HTML("a"), template.HTML("b")}, result) + result, err := ns.Apply(strings, "print", "a", "b", "c") + require.NoError(t, err) + require.Equal(t, []interface{}{"abc", "abc"}, result, "testing variadic") - result, _ = ns.Apply(strings, "chomp", "c\n") - assert.Equal(t, []interface{}{template.HTML("c"), template.HTML("c")}, result) - - result, _ = ns.Apply(strings, "strings.Chomp", "c\n") - assert.Equal(t, []interface{}{template.HTML("c"), template.HTML("c")}, result) - - result, _ = ns.Apply(strings, "print", "a", "b", "c") - assert.Equal(t, []interface{}{"abc", "abc"}, result, "testing variadic") - - result, _ = ns.Apply(nil, "chomp", ".") - assert.Equal(t, []interface{}{}, result) - - _, err := ns.Apply(strings, "apply", ".") - if err == nil { - t.Errorf("apply with apply should fail") - } + _, err = ns.Apply(strings, "apply", ".") + require.Error(t, err) var nilErr *error _, err = ns.Apply(nilErr, "chomp", ".") - if err == nil { - t.Errorf("apply with nil in seq should fail") - } + require.Error(t, err) _, err = ns.Apply(strings, "dobedobedo", ".") + require.Error(t, err) + + _, err = ns.Apply(strings, "foo.Chomp", "c\n") if err == nil { t.Errorf("apply with unknown func should fail") } - _, err = ns.Apply(noStringers, "chomp", ".") - if err == nil { - t.Errorf("apply when func fails should fail") - } - - _, err = ns.Apply(tstNoStringer{}, "chomp", ".") - if err == nil { - t.Errorf("apply with non-sequence should fail") - } - - _, err = ns.Apply(strings, "foo.Chomp", "c\n") - if err == nil { - t.Errorf("apply with unknown namespace should fail") - } - - _, err = ns.Apply(strings, "strings.Foo", "c\n") - if err == nil { - t.Errorf("apply with unknown namespace method should fail") - } } diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index 95d1df642..86674f423 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -21,7 +21,6 @@ import ( "net/url" "reflect" "strings" - "sync" "time" "github.com/spf13/cast" @@ -37,24 +36,12 @@ func New(deps *deps.Deps) *Namespace { // Namespace provides template functions for the "collections" namespace. type Namespace struct { - sync.Mutex - funcMap template.FuncMap - deps *deps.Deps } // Namespace returns a pointer to the current namespace instance. func (ns *Namespace) Namespace() *Namespace { return ns } -// Funcs sets the internal funcMap for the collections namespace. -func (ns *Namespace) Funcs(fm template.FuncMap) *Namespace { - ns.Lock() - ns.funcMap = fm - ns.Unlock() - - return ns -} - // After returns all the items after the first N in a rangeable list. func (ns *Namespace) After(index interface{}, seq interface{}) (interface{}, error) { if index == nil || seq == nil { diff --git a/tpl/collections/init.go b/tpl/collections/init.go new file mode 100644 index 000000000..ded7b803c --- /dev/null +++ b/tpl/collections/init.go @@ -0,0 +1,72 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import ( + "github.com/spf13/hugo/deps" + "github.com/spf13/hugo/tpl/internal" +) + +const name = "collections" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx := New(d) + + examples := [][2]string{ + {`delimit: {{ delimit (slice "A" "B" "C") ", " " and " }}`, `delimit: A, B and C`}, + {`echoParam: {{ echoParam .Params "langCode" }}`, `echoParam: en`}, + {`in: {{ if in "this string contains a substring" "substring" }}Substring found!{{ end }}`, `in: Substring found!`}, + { + `querify 1: {{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }}`, + `querify 1: bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose`}, + { + `querify 2: Search`, + `querify 2: Search`}, + {`sort: {{ slice "B" "C" "A" | sort }}`, `sort: [A B C]`}, + {`seq: {{ seq 3 }}`, `seq: [1 2 3]`}, + {`union: {{ union (slice 1 2 3) (slice 3 4 5) }}`, `union: [1 2 3 4 5]`}, + } + + return &internal.TemplateFuncsNamespace{ + Name: name, + Context: func() interface{} { return ctx }, + Aliases: map[string]interface{}{ + "after": ctx.After, + "apply": ctx.Apply, + "delimit": ctx.Delimit, + "dict": ctx.Dictionary, + "echoParam": ctx.EchoParam, + "first": ctx.First, + "in": ctx.In, + "index": ctx.Index, + "intersect": ctx.Intersect, + "isSet": ctx.IsSet, + "isset": ctx.IsSet, + "last": ctx.Last, + "querify": ctx.Querify, + "shuffle": ctx.Shuffle, + "slice": ctx.Slice, + "sort": ctx.Sort, + "union": ctx.Union, + "where": ctx.Where, + "seq": ctx.Seq, + }, + Examples: examples, + } + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/template.go b/tpl/template.go index 356d66f1e..9fbf6b7b8 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -96,6 +96,10 @@ func (t *TemplateAdapter) Tree() string { return s } +type TemplateFuncsGetter interface { + GetFuncs() map[string]interface{} +} + // TemplateTestMocker adds a way to override some template funcs during tests. // The interface is named so it's not used in regular application code. type TemplateTestMocker interface { diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index c86f4cf1e..77826e0b0 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -39,6 +39,7 @@ const ( var ( _ tpl.TemplateHandler = (*templateHandler)(nil) + _ tpl.TemplateFuncsGetter = (*templateHandler)(nil) _ tpl.TemplateTestMocker = (*templateHandler)(nil) _ tpl.TemplateFinder = (*htmlTemplates)(nil) _ tpl.TemplateFinder = (*textTemplates)(nil) @@ -262,6 +263,10 @@ func (t *templateHandler) SetFuncs(funcMap map[string]interface{}) { t.setFuncs(funcMap) } +func (t *templateHandler) GetFuncs() map[string]interface{} { + return t.html.funcster.funcMap +} + func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) { t.t.Funcs(funcMap) } diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go index a3ad97a29..c5134f740 100644 --- a/tpl/tplimpl/templateFuncster.go +++ b/tpl/tplimpl/templateFuncster.go @@ -21,7 +21,6 @@ import ( bp "github.com/spf13/hugo/bufferpool" "github.com/spf13/hugo/deps" - "github.com/spf13/hugo/tpl/collections" "github.com/spf13/hugo/tpl/crypto" "github.com/spf13/hugo/tpl/encoding" "github.com/spf13/hugo/tpl/images" @@ -39,16 +38,15 @@ type templateFuncster struct { cachedPartials partialCache // Namespaces - collections *collections.Namespace - crypto *crypto.Namespace - encoding *encoding.Namespace - images *images.Namespace - inflect *inflect.Namespace - os *os.Namespace - safe *safe.Namespace - time *time.Namespace - transform *transform.Namespace - urls *urls.Namespace + crypto *crypto.Namespace + encoding *encoding.Namespace + images *images.Namespace + inflect *inflect.Namespace + os *os.Namespace + safe *safe.Namespace + time *time.Namespace + transform *transform.Namespace + urls *urls.Namespace *deps.Deps } @@ -59,16 +57,15 @@ func newTemplateFuncster(deps *deps.Deps) *templateFuncster { cachedPartials: partialCache{p: make(map[string]interface{})}, // Namespaces - collections: collections.New(deps), - crypto: crypto.New(), - encoding: encoding.New(), - images: images.New(deps), - inflect: inflect.New(), - os: os.New(deps), - safe: safe.New(), - time: time.New(), - transform: transform.New(deps), - urls: urls.New(deps), + crypto: crypto.New(), + encoding: encoding.New(), + images: images.New(deps), + inflect: inflect.New(), + os: os.New(deps), + safe: safe.New(), + time: time.New(), + transform: transform.New(deps), + urls: urls.New(deps), } } diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index d56cc661b..eb266bc0c 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/hugo/tpl/internal" // Init the namespaces + _ "github.com/spf13/hugo/tpl/collections" _ "github.com/spf13/hugo/tpl/compare" _ "github.com/spf13/hugo/tpl/data" _ "github.com/spf13/hugo/tpl/lang" @@ -82,43 +83,30 @@ func (t *templateFuncster) partialCached(name string, context interface{}, varia func (t *templateFuncster) initFuncMap() { funcMap := template.FuncMap{ // Namespaces - "collections": t.collections.Namespace, - "crypto": t.crypto.Namespace, - "encoding": t.encoding.Namespace, - "images": t.images.Namespace, - "inflect": t.inflect.Namespace, - "os": t.os.Namespace, - "safe": t.safe.Namespace, + "crypto": t.crypto.Namespace, + "encoding": t.encoding.Namespace, + "images": t.images.Namespace, + "inflect": t.inflect.Namespace, + "os": t.os.Namespace, + "safe": t.safe.Namespace, //"time": t.time.Namespace, "transform": t.transform.Namespace, "urls": t.urls.Namespace, "absURL": t.urls.AbsURL, "absLangURL": t.urls.AbsLangURL, - "after": t.collections.After, - "apply": t.collections.Apply, "base64Decode": t.encoding.Base64Decode, "base64Encode": t.encoding.Base64Encode, "dateFormat": t.time.Format, - "delimit": t.collections.Delimit, - "dict": t.collections.Dictionary, - "echoParam": t.collections.EchoParam, "emojify": t.transform.Emojify, - "first": t.collections.First, "getenv": t.os.Getenv, "highlight": t.transform.Highlight, "htmlEscape": t.transform.HTMLEscape, "htmlUnescape": t.transform.HTMLUnescape, "humanize": t.inflect.Humanize, "imageConfig": t.images.Config, - "in": t.collections.In, - "index": t.collections.Index, "int": func(v interface{}) (int, error) { return cast.ToIntE(v) }, - "intersect": t.collections.Intersect, - "isSet": t.collections.IsSet, - "isset": t.collections.IsSet, "jsonify": t.encoding.Jsonify, - "last": t.collections.Last, "markdownify": t.transform.Markdownify, "md5": t.crypto.MD5, "now": t.time.Now, @@ -129,7 +117,6 @@ func (t *templateFuncster) initFuncMap() { "print": fmt.Sprint, "printf": fmt.Sprintf, "println": fmt.Sprintln, - "querify": t.collections.Querify, "readDir": t.os.ReadDir, "readFile": t.os.ReadFile, "ref": t.urls.Ref, @@ -144,18 +131,12 @@ func (t *templateFuncster) initFuncMap() { "safeURL": t.safe.URL, "sanitizeURL": t.safe.SanitizeURL, "sanitizeurl": t.safe.SanitizeURL, - "seq": t.collections.Seq, "sha1": t.crypto.SHA1, "sha256": t.crypto.SHA256, - "shuffle": t.collections.Shuffle, "singularize": t.inflect.Singularize, - "slice": t.collections.Slice, - "sort": t.collections.Sort, "string": func(v interface{}) (string, error) { return cast.ToStringE(v) }, "time": t.time.AsTime, - "union": t.collections.Union, "urlize": t.PathSpec.URLize, - "where": t.collections.Where, } // Merge the namespace funcs @@ -172,5 +153,4 @@ func (t *templateFuncster) initFuncMap() { t.funcMap = funcMap t.Tmpl.(*templateHandler).setFuncs(funcMap) - t.collections.Funcs(funcMap) } diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index 49a7363d9..b7212cfc2 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -129,8 +129,6 @@ base64Decode 2: {{ 42 | base64Encode | base64Decode }} base64Encode: {{ "Hello world" | base64Encode }} crypto.MD5: {{ crypto.MD5 "Hello world, gophers!" }} dateFormat: {{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }} -delimit: {{ delimit (slice "A" "B" "C") ", " " and " }} -echoParam: {{ echoParam .Params "langCode" }} emojify: {{ "I :heart: Hugo" | emojify }} htmlEscape 1: {{ htmlEscape "Cathal Garvey & The Sunshine Band " | safeHTML}} htmlEscape 2: {{ htmlEscape "Cathal Garvey & The Sunshine Band "}} @@ -143,7 +141,6 @@ humanize 1: {{ humanize "my-first-post" }} humanize 2: {{ humanize "myCamelPost" }} humanize 3: {{ humanize "52" }} humanize 4: {{ humanize 103 }} -in: {{ if in "this string contains a substring" "substring" }}Substring found!{{ end }} jsonify: {{ (slice "A" "B" "C") | jsonify }} markdownify: {{ .Title | markdownify}} md5: {{ md5 "Hello world, gophers!" }} @@ -152,8 +149,6 @@ printf: {{ printf "%s!" "works" }} println: {{ println "works!" -}} plainify: {{ plainify "Hello world, gophers!" }} pluralize: {{ "cat" | pluralize }} -querify 1: {{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }} -querify 2: Search readDir: {{ range (readDir ".") }}{{ .Name }}{{ end }} readFile: {{ readFile "README.txt" }} relLangURL: {{ "index.html" | relLangURL }} @@ -165,14 +160,11 @@ safeHTML: {{ "Bat&Man" | safeHTML | safeHTML }} safeHTML: {{ "Bat&Man" | safeHTML }} safeJS: {{ "(1*2)" | safeJS | safeJS }} safeURL: {{ "http://gohugo.io" | safeURL | safeURL }} -seq: {{ seq 3 }} sha1: {{ sha1 "Hello world, gophers!" }} sha256: {{ sha256 "Hello world, gophers!" }} singularize: {{ "cats" | singularize }} -sort: {{ slice "B" "C" "A" | sort }} strings.TrimPrefix: {{ strings.TrimPrefix "Goodbye,, world!" "Goodbye," }} time: {{ (time "2015-01-21").Year }} -union: {{ union (slice 1 2 3) (slice 3 4 5) }} urlize: {{ "Bat Man" | urlize }} ` @@ -185,8 +177,6 @@ base64Decode 2: 42 base64Encode: SGVsbG8gd29ybGQ= crypto.MD5: b3029f756f98f79e7f1b7f1d1f0dd53b dateFormat: Wednesday, Jan 21, 2015 -delimit: A, B and C -echoParam: en emojify: I ❤️ Hugo htmlEscape 1: Cathal Garvey & The Sunshine Band <cathal@foo.bar> htmlEscape 2: Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt; @@ -199,7 +189,6 @@ humanize 1: My first post humanize 2: My camel post humanize 3: 52nd humanize 4: 103rd -in: Substring found! jsonify: ["A","B","C"] markdownify: BatMan md5: b3029f756f98f79e7f1b7f1d1f0dd53b @@ -208,8 +197,6 @@ printf: works! println: works! plainify: Hello world, gophers! pluralize: cats -querify 1: bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose -querify 2: Search readDir: README.txt readFile: Hugo Rocks! relLangURL: /hugo/en/index.html @@ -221,14 +208,11 @@ safeHTML: Bat&Man safeHTML: Bat&Man safeJS: (1*2) safeURL: http://gohugo.io -seq: [1 2 3] sha1: c8b5b0e33d408246e30f53e32b8f7627a7a649d4 sha256: 6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46 singularize: cat -sort: [A B C] strings.TrimPrefix: , world! time: 2015 -union: [1 2 3 4 5] urlize: bat-man `