...

Source file src/github.com/noirbizarre/gonja/ext/django/filters.go

Documentation: github.com/noirbizarre/gonja/ext/django

     1  package django
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"math/rand"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  	"unicode/utf8"
    12  
    13  	"github.com/pkg/errors"
    14  
    15  	"github.com/noirbizarre/gonja/exec"
    16  	u "github.com/noirbizarre/gonja/utils"
    17  )
    18  
    19  func init() {
    20  	rand.Seed(time.Now().Unix())
    21  }
    22  
    23  var Filters = exec.FilterSet{
    24  	"escapejs":           filterEscapejs,
    25  	"add":                filterAdd,
    26  	"addslashes":         filterAddslashes,
    27  	"capfirst":           filterCapfirst,
    28  	"cut":                filterCut,
    29  	"date":               filterDate,
    30  	"default_if_none":    filterDefaultIfNone,
    31  	"floatformat":        filterFloatformat,
    32  	"get_digit":          filterGetdigit,
    33  	"iriencode":          filterIriencode,
    34  	"length_is":          filterLengthis,
    35  	"linebreaks":         filterLinebreaks,
    36  	"linebreaksbr":       filterLinebreaksbr,
    37  	"linenumbers":        filterLinenumbers,
    38  	"ljust":              filterLjust,
    39  	"make_list":          filterMakelist,
    40  	"phone2numeric":      filterPhone2numeric,
    41  	"pluralize":          filterPluralize,
    42  	"removetags":         filterRemovetags,
    43  	"rjust":              filterRjust,
    44  	"split":              filterSplit,
    45  	"stringformat":       filterStringformat,
    46  	"time":               filterDate, // time uses filterDate (same golang-format,
    47  	"truncatechars":      filterTruncatechars,
    48  	"truncatechars_html": filterTruncatecharsHTML,
    49  	"truncatewords":      filterTruncatewords,
    50  	"truncatewords_html": filterTruncatewordsHTML,
    51  	"yesno":              filterYesno,
    52  }
    53  
    54  func filterTruncatecharsHelper(s string, newLen int) string {
    55  	runes := []rune(s)
    56  	if newLen < len(runes) {
    57  		if newLen >= 3 {
    58  			return fmt.Sprintf("%s...", string(runes[:newLen-3]))
    59  		}
    60  		// Not enough space for the ellipsis
    61  		return string(runes[:newLen])
    62  	}
    63  	return string(runes)
    64  }
    65  
    66  func filterTruncateHTMLHelper(value string, newOutput *bytes.Buffer, cond func() bool, fn func(c rune, s int, idx int) int, finalize func()) {
    67  	vLen := len(value)
    68  	var tagStack []string
    69  	idx := 0
    70  
    71  	for idx < vLen && !cond() {
    72  		c, s := utf8.DecodeRuneInString(value[idx:])
    73  		if c == utf8.RuneError {
    74  			idx += s
    75  			continue
    76  		}
    77  
    78  		if c == '<' {
    79  			newOutput.WriteRune(c)
    80  			idx += s // consume "<"
    81  
    82  			if idx+1 < vLen {
    83  				if value[idx] == '/' {
    84  					// Close tag
    85  
    86  					newOutput.WriteString("/")
    87  
    88  					tag := ""
    89  					idx++ // consume "/"
    90  
    91  					for idx < vLen {
    92  						c2, size2 := utf8.DecodeRuneInString(value[idx:])
    93  						if c2 == utf8.RuneError {
    94  							idx += size2
    95  							continue
    96  						}
    97  
    98  						// End of tag found
    99  						if c2 == '>' {
   100  							idx++ // consume ">"
   101  							break
   102  						}
   103  						tag += string(c2)
   104  						idx += size2
   105  					}
   106  
   107  					if len(tagStack) > 0 {
   108  						// Ideally, the close tag is TOP of tag stack
   109  						// In malformed HTML, it must not be, so iterate through the stack and remove the tag
   110  						for i := len(tagStack) - 1; i >= 0; i-- {
   111  							if tagStack[i] == tag {
   112  								// Found the tag
   113  								tagStack[i] = tagStack[len(tagStack)-1]
   114  								tagStack = tagStack[:len(tagStack)-1]
   115  								break
   116  							}
   117  						}
   118  					}
   119  
   120  					newOutput.WriteString(tag)
   121  					newOutput.WriteString(">")
   122  				} else {
   123  					// Open tag
   124  
   125  					tag := ""
   126  
   127  					params := false
   128  					for idx < vLen {
   129  						c2, size2 := utf8.DecodeRuneInString(value[idx:])
   130  						if c2 == utf8.RuneError {
   131  							idx += size2
   132  							continue
   133  						}
   134  
   135  						newOutput.WriteRune(c2)
   136  
   137  						// End of tag found
   138  						if c2 == '>' {
   139  							idx++ // consume ">"
   140  							break
   141  						}
   142  
   143  						if !params {
   144  							if c2 == ' ' {
   145  								params = true
   146  							} else {
   147  								tag += string(c2)
   148  							}
   149  						}
   150  
   151  						idx += size2
   152  					}
   153  
   154  					// Add tag to stack
   155  					tagStack = append(tagStack, tag)
   156  				}
   157  			}
   158  		} else {
   159  			idx = fn(c, s, idx)
   160  		}
   161  	}
   162  
   163  	finalize()
   164  
   165  	for i := len(tagStack) - 1; i >= 0; i-- {
   166  		tag := tagStack[i]
   167  		// Close everything from the regular tag stack
   168  		newOutput.WriteString(fmt.Sprintf("</%s>", tag))
   169  	}
   170  }
   171  
   172  func filterTruncatechars(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   173  	s := in.String()
   174  	newLen := params.Args[0].Integer()
   175  	return exec.AsValue(filterTruncatecharsHelper(s, newLen))
   176  }
   177  
   178  func filterTruncatecharsHTML(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   179  	value := in.String()
   180  	newLen := u.Max(params.Args[0].Integer()-3, 0)
   181  
   182  	newOutput := bytes.NewBuffer(nil)
   183  
   184  	textcounter := 0
   185  
   186  	filterTruncateHTMLHelper(value, newOutput, func() bool {
   187  		return textcounter >= newLen
   188  	}, func(c rune, s int, idx int) int {
   189  		textcounter++
   190  		newOutput.WriteRune(c)
   191  
   192  		return idx + s
   193  	}, func() {
   194  		if textcounter >= newLen && textcounter < len(value) {
   195  			newOutput.WriteString("...")
   196  		}
   197  	})
   198  
   199  	return exec.AsSafeValue(newOutput.String())
   200  }
   201  
   202  func filterTruncatewords(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   203  	words := strings.Fields(in.String())
   204  	n := params.Args[0].Integer()
   205  	if n <= 0 {
   206  		return exec.AsValue("")
   207  	}
   208  	nlen := u.Min(len(words), n)
   209  	out := make([]string, 0, nlen)
   210  	for i := 0; i < nlen; i++ {
   211  		out = append(out, words[i])
   212  	}
   213  
   214  	if n < len(words) {
   215  		out = append(out, "...")
   216  	}
   217  
   218  	return exec.AsValue(strings.Join(out, " "))
   219  }
   220  
   221  func filterTruncatewordsHTML(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   222  	value := in.String()
   223  	newLen := u.Max(params.Args[0].Integer(), 0)
   224  
   225  	newOutput := bytes.NewBuffer(nil)
   226  
   227  	wordcounter := 0
   228  
   229  	filterTruncateHTMLHelper(value, newOutput, func() bool {
   230  		return wordcounter >= newLen
   231  	}, func(_ rune, _ int, idx int) int {
   232  		// Get next word
   233  		wordFound := false
   234  
   235  		for idx < len(value) {
   236  			c2, size2 := utf8.DecodeRuneInString(value[idx:])
   237  			if c2 == utf8.RuneError {
   238  				idx += size2
   239  				continue
   240  			}
   241  
   242  			if c2 == '<' {
   243  				// HTML tag start, don't consume it
   244  				return idx
   245  			}
   246  
   247  			newOutput.WriteRune(c2)
   248  			idx += size2
   249  
   250  			if c2 == ' ' || c2 == '.' || c2 == ',' || c2 == ';' {
   251  				// Word ends here, stop capturing it now
   252  				break
   253  			} else {
   254  				wordFound = true
   255  			}
   256  		}
   257  
   258  		if wordFound {
   259  			wordcounter++
   260  		}
   261  
   262  		return idx
   263  	}, func() {
   264  		if wordcounter >= newLen {
   265  			newOutput.WriteString("...")
   266  		}
   267  	})
   268  
   269  	return exec.AsSafeValue(newOutput.String())
   270  }
   271  
   272  func filterEscapejs(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   273  	sin := in.String()
   274  
   275  	var b bytes.Buffer
   276  
   277  	idx := 0
   278  	for idx < len(sin) {
   279  		c, size := utf8.DecodeRuneInString(sin[idx:])
   280  		if c == utf8.RuneError {
   281  			idx += size
   282  			continue
   283  		}
   284  
   285  		if c == '\\' {
   286  			// Escape seq?
   287  			if idx+1 < len(sin) {
   288  				switch sin[idx+1] {
   289  				case 'r':
   290  					b.WriteString(fmt.Sprintf(`\u%04X`, '\r'))
   291  					idx += 2
   292  					continue
   293  				case 'n':
   294  					b.WriteString(fmt.Sprintf(`\u%04X`, '\n'))
   295  					idx += 2
   296  					continue
   297  					/*case '\'':
   298  						b.WriteString(fmt.Sprintf(`\u%04X`, '\''))
   299  						idx += 2
   300  						continue
   301  					case '"':
   302  						b.WriteString(fmt.Sprintf(`\u%04X`, '"'))
   303  						idx += 2
   304  						continue*/
   305  				}
   306  			}
   307  		}
   308  
   309  		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == ' ' || c == '/' {
   310  			b.WriteRune(c)
   311  		} else {
   312  			b.WriteString(fmt.Sprintf(`\u%04X`, c))
   313  		}
   314  
   315  		idx += size
   316  	}
   317  
   318  	return exec.AsValue(b.String())
   319  }
   320  
   321  func filterAdd(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   322  	param := params.Args[0]
   323  	if in.IsNumber() && param.IsNumber() {
   324  		if in.IsFloat() || param.IsFloat() {
   325  			return exec.AsValue(in.Float() + param.Float())
   326  		}
   327  		return exec.AsValue(in.Integer() + param.Integer())
   328  	}
   329  	// If in/param is not a number, we're relying on the
   330  	// Value's String() conversion and just add them both together
   331  	return exec.AsValue(in.String() + param.String())
   332  }
   333  
   334  func filterAddslashes(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   335  	output := strings.Replace(in.String(), "\\", "\\\\", -1)
   336  	output = strings.Replace(output, "\"", "\\\"", -1)
   337  	output = strings.Replace(output, "'", "\\'", -1)
   338  	return exec.AsValue(output)
   339  }
   340  
   341  func filterCut(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   342  	return exec.AsValue(strings.Replace(in.String(), params.Args[0].String(), "", -1))
   343  }
   344  
   345  func filterLengthis(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   346  	return exec.AsValue(in.Len() == params.Args[0].Integer())
   347  }
   348  
   349  func filterDefaultIfNone(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   350  	if in.IsError() || in.IsNil() {
   351  		return params.Args[0]
   352  	}
   353  	return in
   354  }
   355  
   356  func filterFloatformat(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   357  	val := in.Float()
   358  	param := params.First()
   359  
   360  	decimals := -1
   361  	if !param.IsNil() {
   362  		// Any argument provided?
   363  		decimals = param.Integer()
   364  	}
   365  
   366  	// if the argument is not a number (e. g. empty), the default
   367  	// behaviour is trim the result
   368  	trim := !param.IsNumber()
   369  
   370  	if decimals <= 0 {
   371  		// argument is negative or zero, so we
   372  		// want the output being trimmed
   373  		decimals = -decimals
   374  		trim = true
   375  	}
   376  
   377  	if trim {
   378  		// Remove zeroes
   379  		if float64(int(val)) == val {
   380  			return exec.AsValue(in.Integer())
   381  		}
   382  	}
   383  
   384  	return exec.AsValue(strconv.FormatFloat(val, 'f', decimals, 64))
   385  }
   386  
   387  func filterGetdigit(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   388  	if len(params.Args) > 1 {
   389  		return exec.AsValue(errors.New("'getdigit' filter expect one and only one argument"))
   390  		// return nil, &Error{
   391  		// 	Sender:    "filter:getdigit",
   392  		// 	OrigError: errors.New("'getdigit' filter expect one and only one argument"),
   393  		// }
   394  	}
   395  	param := params.First()
   396  	i := param.Integer()
   397  	l := len(in.String()) // do NOT use in.Len() here!
   398  	if i <= 0 || i > l {
   399  		return in
   400  	}
   401  	return exec.AsValue(in.String()[l-i] - 48)
   402  }
   403  
   404  func filterIriencode(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   405  	return exec.AsValue(u.IRIEncode(in.String()))
   406  }
   407  
   408  func filterMakelist(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   409  	s := in.String()
   410  	result := make([]string, 0, len(s))
   411  	for _, c := range s {
   412  		result = append(result, string(c))
   413  	}
   414  	return exec.AsValue(result)
   415  }
   416  
   417  func filterCapfirst(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   418  	if in.Len() <= 0 {
   419  		return exec.AsValue("")
   420  	}
   421  	t := in.String()
   422  	r, size := utf8.DecodeRuneInString(t)
   423  	return exec.AsValue(strings.ToUpper(string(r)) + t[size:])
   424  }
   425  
   426  func filterDate(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   427  	t, isTime := in.Interface().(time.Time)
   428  	if !isTime {
   429  		return exec.AsValue(errors.New("filter input argument must be of type 'time.Time'"))
   430  		// return nil, &Error{
   431  		// 	Sender:    "filter:date",
   432  		// 	OrigError: errors.New("filter input argument must be of type 'time.Time'"),
   433  		// }
   434  	}
   435  	return exec.AsValue(t.Format(params.Args[0].String()))
   436  }
   437  
   438  func filterLinebreaks(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   439  	if in.Len() == 0 {
   440  		return in
   441  	}
   442  
   443  	var b bytes.Buffer
   444  
   445  	// Newline = <br />
   446  	// Double newline = <p>...</p>
   447  	lines := strings.Split(in.String(), "\n")
   448  	lenlines := len(lines)
   449  
   450  	opened := false
   451  
   452  	for idx, line := range lines {
   453  
   454  		if !opened {
   455  			b.WriteString("<p>")
   456  			opened = true
   457  		}
   458  
   459  		b.WriteString(line)
   460  
   461  		if idx < lenlines-1 && strings.TrimSpace(lines[idx]) != "" {
   462  			// We've not reached the end
   463  			if strings.TrimSpace(lines[idx+1]) == "" {
   464  				// Next line is empty
   465  				if opened {
   466  					b.WriteString("</p>")
   467  					opened = false
   468  				}
   469  			} else {
   470  				b.WriteString("<br />")
   471  			}
   472  		}
   473  	}
   474  
   475  	if opened {
   476  		b.WriteString("</p>")
   477  	}
   478  
   479  	return exec.AsValue(b.String())
   480  }
   481  
   482  func filterSplit(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   483  	chunks := strings.Split(in.String(), params.Args[0].String())
   484  
   485  	return exec.AsValue(chunks)
   486  }
   487  
   488  func filterLinebreaksbr(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   489  	return exec.AsValue(strings.Replace(in.String(), "\n", "<br />", -1))
   490  }
   491  
   492  func filterLinenumbers(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   493  	lines := strings.Split(in.String(), "\n")
   494  	output := make([]string, 0, len(lines))
   495  	for idx, line := range lines {
   496  		output = append(output, fmt.Sprintf("%d. %s", idx+1, line))
   497  	}
   498  	return exec.AsValue(strings.Join(output, "\n"))
   499  }
   500  
   501  func filterLjust(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   502  	times := params.Args[0].Integer() - in.Len()
   503  	if times < 0 {
   504  		times = 0
   505  	}
   506  	return exec.AsValue(fmt.Sprintf("%s%s", in.String(), strings.Repeat(" ", times)))
   507  }
   508  
   509  func filterStringformat(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   510  	return exec.AsValue(fmt.Sprintf(params.Args[0].String(), in.Interface()))
   511  }
   512  
   513  // https://en.wikipedia.org/wiki/Phoneword
   514  var filterPhone2numericMap = map[string]string{
   515  	"a": "2", "b": "2", "c": "2", "d": "3", "e": "3", "f": "3", "g": "4", "h": "4", "i": "4", "j": "5", "k": "5",
   516  	"l": "5", "m": "6", "n": "6", "o": "6", "p": "7", "q": "7", "r": "7", "s": "7", "t": "8", "u": "8", "v": "8",
   517  	"w": "9", "x": "9", "y": "9", "z": "9",
   518  }
   519  
   520  func filterPhone2numeric(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   521  	sin := in.String()
   522  	for k, v := range filterPhone2numericMap {
   523  		sin = strings.Replace(sin, k, v, -1)
   524  		sin = strings.Replace(sin, strings.ToUpper(k), v, -1)
   525  	}
   526  	return exec.AsValue(sin)
   527  }
   528  
   529  func filterPluralize(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   530  	param := params.First()
   531  	if in.IsNumber() {
   532  		// Works only on numbers
   533  		if param.Len() > 0 {
   534  			endings := strings.Split(param.String(), ",")
   535  			if len(endings) > 2 {
   536  				return exec.AsValue(errors.New("you cannot pass more than 2 arguments to filter 'pluralize'"))
   537  				// return nil, &Error{
   538  				// 	Sender:    "filter:pluralize",
   539  				// 	OrigError: errors.New("you cannot pass more than 2 arguments to filter 'pluralize'"),
   540  				// }
   541  			}
   542  			if len(endings) == 1 {
   543  				// 1 argument
   544  				if in.Integer() != 1 {
   545  					return exec.AsValue(endings[0])
   546  				}
   547  			} else {
   548  				if in.Integer() != 1 {
   549  					// 2 arguments
   550  					return exec.AsValue(endings[1])
   551  				}
   552  				return exec.AsValue(endings[0])
   553  			}
   554  		} else {
   555  			if in.Integer() != 1 {
   556  				// return default 's'
   557  				return exec.AsValue("s")
   558  			}
   559  		}
   560  
   561  		return exec.AsValue("")
   562  	}
   563  	// return nil, &Error{
   564  	// 	Sender:    "filter:pluralize",
   565  	// 	OrigError: errors.New("filter 'pluralize' does only work on numbers"),
   566  	// }
   567  	return exec.AsValue(errors.New("filter 'pluralize' does only work on numbers"))
   568  }
   569  
   570  func filterRemovetags(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   571  	s := in.String()
   572  	tags := strings.Split(params.Args[0].String(), ",")
   573  
   574  	// Strip only specific tags
   575  	for _, tag := range tags {
   576  		re := regexp.MustCompile(fmt.Sprintf("</?%s/?>", tag))
   577  		s = re.ReplaceAllString(s, "")
   578  	}
   579  
   580  	return exec.AsValue(strings.TrimSpace(s))
   581  }
   582  
   583  func filterRjust(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   584  	return exec.AsValue(fmt.Sprintf(fmt.Sprintf("%%%ds", params.Args[0].Integer()), in.String()))
   585  }
   586  
   587  func filterYesno(e *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value {
   588  	if len(params.Args) > 1 {
   589  		// return nil, &Error{
   590  		// 	Sender:    "filter:getdigit",
   591  		// 	OrigError: errors.New("'getdigit' filter expect one and only one argument"),
   592  		// }
   593  		return exec.AsValue(errors.New("'getdigit' filter expect one and only one argument"))
   594  	}
   595  	choices := map[int]string{
   596  		0: "yes",
   597  		1: "no",
   598  		2: "maybe",
   599  	}
   600  	param := params.First()
   601  	paramString := param.String()
   602  	customChoices := strings.Split(paramString, ",")
   603  	if len(paramString) > 0 {
   604  		if len(customChoices) > 3 {
   605  			return exec.AsValue(errors.Errorf("You cannot pass more than 3 options to the 'yesno'-filter (got: '%s').", paramString))
   606  			// return nil, &Error{
   607  			// 	Sender:    "filter:yesno",
   608  			// 	OrigError: errors.Errorf("You cannot pass more than 3 options to the 'yesno'-filter (got: '%s').", paramString),
   609  			// }
   610  		}
   611  		if len(customChoices) < 2 {
   612  			// return nil, &Error{
   613  			// 	Sender:    "filter:yesno",
   614  			// 	OrigError: errors.Errorf("You must pass either no or at least 2 arguments to the 'yesno'-filter (got: '%s').", paramString),
   615  			// }
   616  			return exec.AsValue(errors.Errorf("You must pass either no or at least 2 arguments to the 'yesno'-filter (got: '%s').", paramString))
   617  		}
   618  
   619  		// Map to the options now
   620  		choices[0] = customChoices[0]
   621  		choices[1] = customChoices[1]
   622  		if len(customChoices) == 3 {
   623  			choices[2] = customChoices[2]
   624  		}
   625  	}
   626  
   627  	// maybe
   628  	if in.IsNil() {
   629  		return exec.AsValue(choices[2])
   630  	}
   631  
   632  	// yes
   633  	if in.IsTrue() {
   634  		return exec.AsValue(choices[0])
   635  	}
   636  
   637  	// no
   638  	return exec.AsValue(choices[1])
   639  }
   640  

View as plain text