From ab859c805992ef3bbb0e627d6b3f295574feda0e Mon Sep 17 00:00:00 2001 From: Darko Luketic Date: Mon, 21 Nov 2022 14:22:40 +0100 Subject: [PATCH] added v6 --- go.mod | 12 +- go.sum | 32 ++--- v6/filters.go | 310 +++++++++++++++++++++++++++++++++++++++++++++ v6/filters_test.go | 97 ++++++++++++++ v6/helpers.go | 15 +++ 5 files changed, 444 insertions(+), 22 deletions(-) create mode 100644 v6/filters.go create mode 100644 v6/filters_test.go create mode 100644 v6/helpers.go diff --git a/go.mod b/go.mod index e86eb76..d225a15 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ -module github.com/idc77/pongo2-addons +module code.icod.de/dalu/pongo2-addons go 1.14 require ( github.com/flosch/go-humanize v0.0.0-20140728123800-3ba51eabe506 - github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915 + github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 github.com/flosch/pongo2/v4 v4.0.2 github.com/flosch/pongo2/v5 v5.0.0 - github.com/gosimple/slug v1.12.0 - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/russross/blackfriday/v2 v2.0.1 - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/flosch/pongo2/v6 v6.0.0 + github.com/gosimple/slug v1.13.1 + github.com/kr/pretty v0.3.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c ) diff --git a/go.sum b/go.sum index 3373163..8c6cd8e 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,31 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/flosch/go-humanize v0.0.0-20140728123800-3ba51eabe506 h1:tN043XK9BV76qc31Z2GACIO5Dsh99q21JtYmR2ltXBg= github.com/flosch/go-humanize v0.0.0-20140728123800-3ba51eabe506/go.mod h1:pSiPkAThBLWmIzJ2fukUGkcxxWR4HoLT7Bp8/krrl5g= -github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915 h1:rNVrewdFbSujcoKZifC6cHJfqCTbCIR7XTLHW5TqUWU= -github.com/flosch/pongo2 v0.0.0-20200529170236-5abacdfa4915/go.mod h1:fB4mx6dzqFinCxIf3a7Mf5yLk+18Bia9mPAnuejcvDA= +github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 h1:fmFk0Wt3bBxxwZnu48jqMdaOR/IZ4vdtJFuaFV8MpIE= +github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3/go.mod h1:bJWSKrZyQvfTnb2OudyUjurSG4/edverV7n82+K3JiM= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/flosch/pongo2/v5 v5.0.0 h1:ZauMp+iPZzh2aI1QM2UwRb0lXD4BoFcvBuWqefkIuq0= github.com/flosch/pongo2/v5 v5.0.0/go.mod h1:6ysKu++8ANFXmc3x6uA6iVaS+PKUoDfdX3yPcv8TIzY= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc= -github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= +github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= +github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= +github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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= diff --git a/v6/filters.go b/v6/filters.go new file mode 100644 index 0000000..cea431e --- /dev/null +++ b/v6/filters.go @@ -0,0 +1,310 @@ +package pongo2addons + +import ( + "bytes" + "errors" + "fmt" + "math/rand" + "regexp" + "strings" + "time" + "unicode/utf8" + + "github.com/flosch/go-humanize" + "github.com/flosch/pongo2/v6" + "github.com/gosimple/slug" + "github.com/russross/blackfriday/v2" +) + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) + + // Regulars + pongo2.RegisterFilter("slugify", filterSlugify) + pongo2.RegisterFilter("filesizeformat", filterFilesizeformat) + pongo2.RegisterFilter("truncatesentences", filterTruncatesentences) + pongo2.RegisterFilter("truncatesentences_html", filterTruncatesentencesHTML) + pongo2.RegisterFilter("random", filterRandom) + + // Markup + pongo2.RegisterFilter("markdown", filterMarkdown) + + // Humanize + pongo2.RegisterFilter("timeuntil", filterTimeuntilTimesince) + pongo2.RegisterFilter("timesince", filterTimeuntilTimesince) + pongo2.RegisterFilter("naturaltime", filterTimeuntilTimesince) + pongo2.RegisterFilter("naturalday", filterNaturalday) + pongo2.RegisterFilter("intcomma", filterIntcomma) + pongo2.RegisterFilter("ordinal", filterOrdinal) +} + +func filterMarkdown(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + return pongo2.AsSafeValue(string(blackfriday.Run([]byte(in.String())))), nil +} + +func filterSlugify(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + return pongo2.AsValue(slug.Make(in.String())), nil +} + +func filterFilesizeformat(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + return pongo2.AsValue(humanize.IBytes(uint64(in.Integer()))), nil +} + +var filterTruncatesentencesRe = regexp.MustCompile(`(?U:.*[\w]{3,}.*([\d][\.!?][\D]|[\D][\.!?][\s]|[\n$]))`) + +func filterTruncatesentences(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + count := param.Integer() + if count <= 0 { + return pongo2.AsValue(""), nil + } + sentencens := filterTruncatesentencesRe.FindAllString(strings.TrimSpace(in.String()), -1) + return pongo2.AsValue(strings.TrimSpace(strings.Join(sentencens[:min(count, len(sentencens))], ""))), nil +} + +// Taken from pongo2/filters_builtin.go +func filterTruncateHTMLHelper(value string, newOutput *bytes.Buffer, cond func() bool, fn func(c rune, s int, idx int) int, finalize func()) { + vLen := len(value) + tagStack := make([]string, 0) + idx := 0 + + for idx < vLen && !cond() { + c, s := utf8.DecodeRuneInString(value[idx:]) + if c == utf8.RuneError { + idx += s + continue + } + + if c == '<' { + newOutput.WriteRune(c) + idx += s // consume "<" + + if idx+1 < vLen { + if value[idx] == '/' { + // Close tag + + newOutput.WriteString("/") + + tag := "" + idx++ // consume "/" + + for idx < vLen { + c2, size2 := utf8.DecodeRuneInString(value[idx:]) + if c2 == utf8.RuneError { + idx += size2 + continue + } + + // End of tag found + if c2 == '>' { + idx++ // consume ">" + break + } + tag += string(c2) + idx += size2 + } + + if len(tagStack) > 0 { + // Ideally, the close tag is TOP of tag stack + // In malformed HTML, it must not be, so iterate through the stack and remove the tag + for i := len(tagStack) - 1; i >= 0; i-- { + if tagStack[i] == tag { + // Found the tag + tagStack[i] = tagStack[len(tagStack)-1] + tagStack = tagStack[:len(tagStack)-1] + break + } + } + } + + newOutput.WriteString(tag) + newOutput.WriteString(">") + } else { + // Open tag + + tag := "" + + params := false + for idx < vLen { + c2, size2 := utf8.DecodeRuneInString(value[idx:]) + if c2 == utf8.RuneError { + idx += size2 + continue + } + + newOutput.WriteRune(c2) + + // End of tag found + if c2 == '>' { + idx++ // consume ">" + break + } + + if !params { + if c2 == ' ' { + params = true + } else { + tag += string(c2) + } + } + + idx += size2 + } + + // Add tag to stack + tagStack = append(tagStack, tag) + } + } + } else { + idx = fn(c, s, idx) + } + } + + finalize() + + for i := len(tagStack) - 1; i >= 0; i-- { + tag := tagStack[i] + // Close everything from the regular tag stack + newOutput.WriteString(fmt.Sprintf("", tag)) + } +} + +func filterTruncatesentencesHTML(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + count := param.Integer() + if count <= 0 { + return pongo2.AsValue(""), nil + } + + value := in.String() + newLen := max(param.Integer(), 0) + + newOutput := bytes.NewBuffer(nil) + + sentencefilter := 0 + + filterTruncateHTMLHelper(value, newOutput, func() bool { + return sentencefilter >= newLen + }, func(_ rune, _ int, idx int) int { + // Get next word + wordFound := false + + for idx < len(value) { + c2, size2 := utf8.DecodeRuneInString(value[idx:]) + if c2 == utf8.RuneError { + idx += size2 + continue + } + + if c2 == '<' { + // HTML tag start, don't consume it + return idx + } + + newOutput.WriteRune(c2) + idx += size2 + + if (c2 == '.' && !(idx+1 < len(value) && value[idx+1] >= '0' && value[idx+1] <= '9')) || + c2 == '!' || c2 == '?' || c2 == '\n' { + // Sentence ends here, stop capturing it now + break + } else { + wordFound = true + } + } + + if wordFound { + sentencefilter++ + } + + return idx + }, func() {}) + + return pongo2.AsSafeValue(newOutput.String()), nil +} + +func filterRandom(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + if !in.CanSlice() { + return nil, &pongo2.Error{ + Sender: "filter:random", + OrigError: errors.New("input is not sliceable"), + } + } + + if in.Len() <= 0 { + return nil, &pongo2.Error{ + Sender: "filter:random", + OrigError: errors.New("input slice is empty"), + } + } + + return in.Index(rand.Intn(in.Len())), nil +} + +func filterTimeuntilTimesince(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + basetime, isTime := in.Interface().(time.Time) + if !isTime { + return nil, &pongo2.Error{ + Sender: "filter:timeuntil/timesince", + OrigError: errors.New("time-value is not a time.Time-instance"), + } + } + var paramtime time.Time + if !param.IsNil() { + paramtime, isTime = param.Interface().(time.Time) + if !isTime { + return nil, &pongo2.Error{ + Sender: "filter:timeuntil/timesince", + OrigError: errors.New("time-parameter is not a time.Time-instance"), + } + } + } else { + paramtime = time.Now() + } + + return pongo2.AsValue(humanize.TimeDuration(basetime.Sub(paramtime))), nil +} + +func filterIntcomma(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + return pongo2.AsValue(humanize.Comma(int64(in.Integer()))), nil +} + +func filterOrdinal(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + return pongo2.AsValue(humanize.Ordinal(in.Integer())), nil +} + +func filterNaturalday(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { + basetime, isTime := in.Interface().(time.Time) + if !isTime { + return nil, &pongo2.Error{ + Sender: "filter:naturalday", + OrigError: errors.New("naturalday-value is not a time.Time-instance"), + } + } + + var referenceTime time.Time + if !param.IsNil() { + referenceTime, isTime = param.Interface().(time.Time) + if !isTime { + return nil, &pongo2.Error{ + Sender: "filter:naturalday", + OrigError: errors.New("naturalday-parameter is not a time.Time-instance"), + } + } + } else { + referenceTime = time.Now() + } + + d := referenceTime.Sub(basetime) / time.Hour + + switch { + case d >= 0 && d < 24: + // Today + return pongo2.AsValue("today"), nil + case d >= 24: + return pongo2.AsValue("yesterday"), nil + case d < 0 && d >= -24: + return pongo2.AsValue("tomorrow"), nil + } + + // Default behaviour + return pongo2.ApplyFilter("naturaltime", in, param) +} diff --git a/v6/filters_test.go b/v6/filters_test.go new file mode 100644 index 0000000..73a621a --- /dev/null +++ b/v6/filters_test.go @@ -0,0 +1,97 @@ +package pongo2addons + +import ( + "testing" + "time" + + "github.com/flosch/pongo2/v6" + . "gopkg.in/check.v1" +) + +// Hook up gocheck into the "go test" runner. +func Test(t *testing.T) { + TestingT(t) +} + +// A wrapprt of pongo2.RenderTemplateString +func getResult(s string, ctx pongo2.Context) string { + result, _ := pongo2.RenderTemplateString(s, ctx) + return result +} + +type TestSuite1 struct{} + +var _ = Suite(&TestSuite1{}) + +func (s *TestSuite1) TestFilters(c *C) { + // Markdown + c.Assert(getResult("{{ \"**test**\"|markdown }}", nil), Equals, "

test

\n") + + // Slugify + c.Assert(getResult("{{ \"this is รค test!\"|slugify }}", nil), Equals, "this-is-a-test") + + // Filesizeformat + c.Assert(getResult("{{ 123456789|filesizeformat }}", nil), Equals, "118MiB") + + // Timesince/timeuntil + baseDate := time.Date(2014, time.February, 1, 8, 30, 00, 00, time.UTC) + futureDate := baseDate.Add(24*7*4*time.Hour + 2*time.Hour) + c.Assert(getResult("{{ future_date|timeuntil:base_date }}", + pongo2.Context{"base_date": baseDate, "future_date": futureDate}), Equals, "4 weeks from now") + + baseDate = time.Date(2014, time.February, 1, 8, 30, 00, 00, time.UTC) + futureDate = baseDate.Add(2 * time.Hour) + c.Assert(getResult("{{ future_date|timeuntil:base_date }}", + pongo2.Context{"base_date": baseDate, "future_date": futureDate}), Equals, "2 hours from now") + + baseDate = time.Date(2014, time.February, 1, 8, 30, 00, 00, time.UTC) + futureDate = baseDate.Add(2 * time.Hour) + c.Assert(getResult("{{ base_date|timesince:future_date }}", + pongo2.Context{"base_date": baseDate, "future_date": futureDate}), Equals, "2 hours ago") + + // Natural time + baseDate = time.Date(2014, time.February, 1, 8, 30, 00, 00, time.UTC) + futureDate = baseDate.Add(4 * time.Second) + c.Assert(getResult("{{ base_date|naturaltime:future_date }}", + pongo2.Context{"base_date": baseDate, "future_date": futureDate}), Equals, "4 seconds ago") + + // Naturalday + today := time.Date(2014, time.February, 1, 8, 30, 00, 00, time.UTC) + yesterday := today.Add(-24 * time.Hour) + tomorrow := today.Add(24 * time.Hour) + todayPlus3 := today.Add(3 * 24 * time.Hour) + c.Assert(getResult("{{ date|naturalday:today }}", + pongo2.Context{"date": today, "today": today}), Equals, "today") + c.Assert(getResult("{{ date|naturalday:today }}", + pongo2.Context{"date": yesterday, "today": today}), Equals, "yesterday") + c.Assert(getResult("{{ date|naturalday:today }}", + pongo2.Context{"date": tomorrow, "today": today}), Equals, "tomorrow") + c.Assert(getResult("{{ date|naturalday:today }}", + pongo2.Context{"date": todayPlus3, "today": today}), Equals, "3 days from now") + + // Intcomma + c.Assert(getResult("{{ 123456789|intcomma }}", nil), Equals, "123,456,789") + + // Ordinal + c.Assert(getResult("{{ 1|ordinal }} {{ 2|ordinal }} {{ 3|ordinal }} {{ 18241|ordinal }}", nil), + Equals, "1st 2nd 3rd 18241st") + + // Truncatesentences + c.Assert(getResult("{{ text|truncatesentences:3|safe }}", pongo2.Context{ + "text": `This is a first sentence with a 4.50 number. The second one is even more fun! Isn't it? Last sentence, okay.`}), + Equals, "This is a first sentence with a 4.50 number. The second one is even more fun! Isn't it?") + + // Truncatesentences_html + c.Assert(getResult("{{ text|truncatesentences_html:2 }}", pongo2.Context{ + "text": `
`}), + Equals, `
`) + c.Assert(getResult("{{ text|truncatesentences_html:3 }}", pongo2.Context{ + "text": `
`}), + Equals, `
`) + + // Random + c.Assert(getResult("{{ array|random }}", + pongo2.Context{"array": []int{42}}), + Equals, "42") + +} diff --git a/v6/helpers.go b/v6/helpers.go new file mode 100644 index 0000000..008052c --- /dev/null +++ b/v6/helpers.go @@ -0,0 +1,15 @@ +package pongo2addons + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +}