github.com/coveo/gotemplate@v2.7.7+incompatible/template/razor.go (about)

     1  package template
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/fatih/color"
     9  	"github.com/op/go-logging"
    10  )
    11  
    12  // Add additional functions to the go template context
    13  func (t *Template) applyRazor(content []byte) []byte {
    14  	if !t.options[Razor] || !t.IsRazor(string(content)) {
    15  		return content
    16  	}
    17  	t.ensureInit()
    18  
    19  	for _, r := range replacementsInit[fmt.Sprint(t.delimiters)] {
    20  		printDebugInfo(r, string(content))
    21  		if r.parser == nil {
    22  			content = r.re.ReplaceAll(content, []byte(r.replace))
    23  		} else {
    24  			content = r.re.ReplaceAllFunc(content, func(match []byte) []byte {
    25  				return []byte(r.parser(r, string(match)))
    26  			})
    27  		}
    28  	}
    29  	content = []byte(strings.Replace(string(content), funcCall, "", -1))
    30  	log.Noticef("Generated content\n\n%s\n", color.HiCyanString(String(content).AddLineNumber(0).Str()))
    31  	return content
    32  }
    33  
    34  var highlight = color.New(color.BgHiBlack, color.FgBlack).SprintFunc()
    35  
    36  // This is indented to simplify the following regular expression for patterns that are repeated several times
    37  // Warning: The declaration order is important
    38  var customMetaclass = [][2]string{
    39  	{"function;", `@reduce;(?P<expr>[id]\([sp][expr]*[sp]\))`},
    40  	{"assign;", `(?P<assign>(?:\$[id][ \t,]*){1,2}:=[sp])`},       // Optional assignment
    41  	{"index;", `(?P<index>\[[expr]+\])`},                          // Extended index operator that support picky selection using ',' as element separator
    42  	{"selector;", `(?P<selection>\.[expr]+)`},                     // Optional selector following expression indicating that the expression must include the content after the closing ) (i.e. @function(args).selection)
    43  	{"reduce;", `(?P<reduce>((?P<reduce1>-)|_)?(?P<reduce2>-?))`}, // Optional reduces sign (-) indicating that the generated code must start with {{- (and end with -}} if two dashes are specified @--)
    44  	{"endexpr;", `(?:[sp];)?`},                                    // End expression (spaces + ; or end of line)
    45  	{"[sp]", `[[:blank:]]*`},                                      // Optional spaces
    46  	{"[id]", `[\p{L}\d_]+`},                                       // Go language id
    47  	{"[flexible_id]", `[map_id;][map_id;\.\-]*`},                  // Id with additional character that could be used to create variables in maps
    48  	{"[idSel]", `[\p{L}_][\p{L}\d_\.]*`},                          // Id with optional selection (object.selection.subselection)
    49  	{"map_id;", `\p{L}\d_\+\*%#!~`},                               // Id with additional character that could be used to create variables in maps
    50  }
    51  
    52  // Expression (any character that is not a new line, a start of razor expression or a semicolumn)
    53  var expressionList = []string{`[^\n]`, `[^@\n]`, `[^@;\n]`, `[^@{\n]`, `[^@;{\n]`, fmt.Sprintf(`[^@;{\n'%s]`, "`"), `[\p{L}_\.]`}
    54  
    55  const expressionKey = "[expr]"
    56  
    57  // Warning: The declaration order is important
    58  var expressions = [][]interface{}{
    59  	// Literals
    60  	{"Protect email", `(\W|^)[\w.!#$%&'*+/=?^_{|}~-]+@[\w-]{1,61}(?:\.[\w-]{1,61})+`, "", replacementFunc(protectEmail)},
    61  	{"", `@@`, literalAt},
    62  	{"", `@{{`, literalStart},
    63  	{"", "(?s)`+.*?`+", "", replacementFunc(protectMultiLineStrings)},
    64  	{"", `@<;`, `{{- $.NEWLINE }}`},
    65  	{"Auto indent", `(?m)^(?P<before>.*)@reduce;(?:autoIndent|aindent|aIndent)\(`, "@<-spaceIndent(`${before}`, "},
    66  	{"Auto wrap", `(?m)^(?P<before>.*)@(?P<nl><)?reduce;(?P<func>autoWrap|awrap|aWrap)(?P<context>\(.*)$`, "", replacementFunc(autoWrap)},
    67  	{`Inline content "@<...>"`, `"@reduce;<(?P<content>.*?)>"`, `"<<@${reduce}(${content})"`},
    68  	{"Newline expression", `@<`, `{{- $.NEWLINE }}@`},
    69  
    70  	// Comments
    71  	{"Pseudo line comments - #! @", `(?m)(?:[sp](?:#|//)![sp])@`, "@"},
    72  	{"Pseudo block comments - /*@  @*/", `(?s)/\*@(?P<content>.*?)@\*/`, "${content}"},
    73  	{"Real comments - ##|/// @ comment", `(?m)[sp](?:##|///)[sp]@.*$`, `{{- "" }}`},
    74  	{"Line comment - @// or @#", `(?m)@reduce;(#|//)[sp](?P<line_comment>.*)[sp]$`, "{{${reduce1} /* ${line_comment} */ ${reduce2}}}"},
    75  	{"Block comment - @/* */", `(?s)@reduce;/\*(?P<block_comment>.*?)\*/`, "{{${reduce1} /*${block_comment}*/ ${reduce2}}}"},
    76  	{"", `{{ /\*`, `{{/*`}, {"", `\*/ }}`, `*/}}`}, // Gotemplate is picky about spaces around comment {{- /* comment */ -}} and {{/* comment */}} are valid, but {{-/* comment */-}} and {{ /* comment */ }} are not.
    77  
    78  	// Commands
    79  	{"Foreach", `@reduce;for(?:[sp]each)?[sp]\(`, "@${reduce}range("},
    80  	{"Single line command - @command (expr) action;", `@reduce;(?P<command>if|with|range)[sp]\([sp]assign;?[sp](?P<expr>[expr]+)[sp]\)[sp](?P<action>[^\n]+?)[sp];`, `{{${reduce1} ${command} ${assign}${expr} ${reduce2}}}${action}{{${reduce1} end ${reduce2}}}`, replacementFunc(expressionParserSkipError), replacementFunc(expressionParser)},
    81  	{"Single line command - @command (expr) { action }", `(?m)@reduce;(?P<command>if|with|range)[sp]\([sp]assign;?[sp](?P<expr>[expr]+)[sp]\)[sp]{[sp](?P<action>[^\n]+?)}[sp]$`, `{{${reduce1} ${command} ${assign}${expr} ${reduce2}}}${action}{{${reduce1} end ${reduce2}}}`, replacementFunc(expressionParserSkipError), replacementFunc(expressionParser)},
    82  	{"Command(expr)", `@reduce;(?P<command>if|else[sp]if|block|with|define|range)[sp]\([sp]assign;?[sp](?P<expr>[expr]+)[sp]\)[sp]`, `{{${reduce1} ${command} ${assign}${expr} ${reduce2}}}`, replacementFunc(expressionParserSkipError), replacementFunc(expressionParser)},
    83  	{"else", `@reduce;else`, "{{${reduce1} else ${reduce2}}}"},
    84  	{"various ends", `@reduce;(?P<command>end[sp](if|range|define|block|with|for[sp]each|for|))endexpr;`, "{{${reduce1} end ${reduce2}}}"},
    85  
    86  	// Assignations
    87  	{"Assign - @$var := value", `(?P<type>@\$)(?P<id>[id])[sp](?P<assign>:=|=)[sp](?P<expr>[expr]+)endexpr;`, ``, replacementFunc(assignExpression)},
    88  	{"Assign - @var := value", `(?P<type>@[\$\.]?)(?P<id>[flexible_id])[sp](?P<assign>:=|=)[sp](?P<expr>[expr]+)endexpr;`, ``, replacementFunc(assignExpression)},
    89  	{"Assign - @{var} := value", `(?P<type>@{)(?P<id>[id])}[sp](?P<assign>:=|=)[sp](?P<expr>[expr]+)endexpr;`, ``, replacementFunc(assignExpression)},
    90  	{"Assign - @{var := expr}", `(?P<type>@{)(?P<id>[id])[sp](?P<assign>:=|=)[sp](?P<expr>[expr]+?)}endexpr;`, ``, replacementFunc(assignExpressionAcceptError)},
    91  	// TODO Remove in future version
    92  	{"DEPRECATED Assign - $var := value", `(?:{{-?[sp](?:if|range|with)?[sp](\$[id],)?[sp]|@\(\s*)?(?P<type>\$)(?P<id>[flexible_id])[sp](?P<assign>:=|=)[sp](?P<expr>[expr]+)endexpr;`, ``, replacementFunc(assignExpression)},
    93  
    94  	// Function calls
    95  	{"Function call followed by expression - @func(args...).args", `function;selector;endexpr;`, `@${reduce}((${expr})${selection});`, replacementFunc(expressionParserSkipError)},
    96  	{"Function call with slice - @func(args...)[...]", `function;index;endexpr;`, `{{${reduce1} ${slicer} (${expr}) ${index} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
    97  	{"Function call - @func(args...)", `function;endexpr;`, `{{${reduce1} ${expr} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
    98  	{"Function unmanaged - @func(value | func)", `@reduce;(?P<function>[id])\([sp](?P<args>[expr]+)[sp]\)endexpr;`, `{{${reduce1} ${function} ${args} ${reduce2}}}`},
    99  
   100  	// Variables
   101  	{"Local variables - @{var}", `@reduce;{[sp](?P<name>[\p{L}\d_\.]*)[sp]}(?P<end>endexpr;)`, `@${reduce}($$${name});`},
   102  	{"Global variables followed by expression", `@reduce;(?P<expr>[idSel]selector;index;?)(?P<end>endexpr;)`, `@${reduce}(${expr});`, replacementFunc(expressionParserSkipError)},
   103  	{"Context variables - @.var", `@reduce;\.(?P<name>[idSel])endexpr;`, `@${reduce}(.${name})`},
   104  	{"Global variables with slice - @var[...]", `@reduce;(?P<name>[idSel])index;endexpr;`, `{{${reduce1} ${slicer} $$.${name} ${index} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
   105  	{"Context variables special with slice", `@reduce;\.(?P<expr>(?P<name>[flexible_id])index;)endexpr;`, `{{${reduce1} ${slicer} (get . "${name}") ${index} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
   106  	{"Global variables special with slice", `@reduce;(?P<expr>(?P<name>[flexible_id])index;)endexpr;`, `{{${reduce1} ${slicer} (get $$ "${name}") ${index} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
   107  	{"Local variables with slice", `@reduce;(?P<expr>(?P<name>[\$\.][\p{L}\d_\.]*)index;)endexpr;`, `{{${reduce1} ${slicer} ${name} ${index} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
   108  	{"Global variables - @var", `@reduce;(?P<name>[idSel])endexpr;`, `{{${reduce1} $$.${name} ${reduce2}}}`},
   109  	{"Context variables special - @.var", `@reduce;\.(?P<name>[flexible_id])endexpr;`, `{{${reduce1} get . "${name}" ${reduce2}}}`},
   110  	{"Global variables special - @var", `@reduce;(?P<name>[flexible_id])endexpr;`, `{{${reduce1} get $$ "${name}" ${reduce2}}}`},
   111  	{"Local variables - @$var or @.var", `@reduce;(?P<name>[\$\.][\p{L}\d_\.]*)endexpr;`, `{{${reduce1} ${name} ${reduce2}}}`},
   112  
   113  	// Expressions
   114  	{"Expression @(var)[...]", `@reduce;(?P<expr>\([sp](?P<name>[idSel])[sp]\)index;)endexpr;`, `{{${reduce1} ${slicer} $$.${name} ${index} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
   115  	{"Expression @(var)", `@reduce;\([sp](?P<expr>[idSel])[sp]\)endexpr;`, `{{${reduce1} ${expr} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
   116  	{"Expression @(expr)[...]", `@reduce;\([sp](?P<expr>[expr]+)[sp]\)index;endexpr;`, `{{${reduce1} ${slicer} (${expr}) ${index} ${reduce2}}}`, replacementFunc(expressionParserSkipError)},
   117  	{"Expression @(expr)", `@reduce;\([sp](?P<assign>.*?:= ?)?[sp](?P<expr>[expr]+)[sp]\)endexpr;`, `{{${reduce1} ${assign}${expr} ${reduce2}}}`, replacementFunc(expressionParserSkipError), replacementFunc(expressionParser)},
   118  
   119  	{"Space eater", `@-`, `{{- "" -}}`},
   120  
   121  	// Inline contents: Render the content without its enclosing quotes
   122  	{`Inline content "<<..."`, `"<<(?P<content>{{[sp].*[sp]}})"`, `${content}`},
   123  
   124  	// Restoring literals
   125  	{"", `}}\\\.`, "}}."},
   126  	{"", literalAt, "@"},
   127  	{"", fmt.Sprintf(`\x60%s(?P<num>\d+)\x60`, protectString), "", replacementFunc(protectMultiLineStrings)},
   128  }
   129  
   130  var replacementsInit = make(map[string][]replacement)
   131  
   132  type replacementFunc func(replacement, string) string
   133  type replacement struct {
   134  	name       string
   135  	expr       string
   136  	replace    string
   137  	re         *regexp.Regexp
   138  	parser     replacementFunc
   139  	delimiters []string
   140  }
   141  
   142  func (t *Template) ensureInit() {
   143  	delimiters := fmt.Sprint(t.delimiters)
   144  	if _, ok := replacementsInit[delimiters]; !ok {
   145  		// We must ensure that search and replacement expression are compatible with the set of delimiters
   146  		replacements := make([]replacement, 0, len(expressions))
   147  		for _, expr := range expressions {
   148  			comment := expr[0].(string)
   149  			re := strings.Replace(expr[1].(string), "@", regexp.QuoteMeta(t.delimiters[2]), -1)
   150  			re = strings.Replace(re, "{{", regexp.QuoteMeta(t.delimiters[0]), -1)
   151  			re = strings.Replace(re, "}}", regexp.QuoteMeta(t.delimiters[1]), -1)
   152  			replace := strings.Replace(strings.Replace(strings.Replace(expr[2].(string), "{{", t.delimiters[0], -1), "}}", t.delimiters[1], -1), "@", t.delimiters[2], -1)
   153  			var exprParser replacementFunc
   154  			if len(expr) >= 4 {
   155  				exprParser = expr[3].(replacementFunc)
   156  			}
   157  
   158  			// We apply replacements in regular expression to make them regex compliant
   159  			for i := range customMetaclass {
   160  				key, value := customMetaclass[i][0], customMetaclass[i][1]
   161  				re = strings.Replace(re, key, value, -1)
   162  			}
   163  
   164  			subExpressions := []string{re}
   165  			if strings.Contains(re, expressionKey) {
   166  				// If regex contains the generic expression token [expr], we generate several expression evaluator
   167  				// that go from the most generic expression to the most specific one
   168  				subExpressions = make([]string, len(expressionList))
   169  				for i := range expressionList {
   170  					subExpressions[i] = strings.Replace(re, expressionKey, expressionList[i], -1)
   171  				}
   172  			}
   173  
   174  			for i := range subExpressions {
   175  				re := regexp.MustCompile(subExpressions[i])
   176  				replacements = append(replacements, replacement{comment, subExpressions[i], replace, re, exprParser, t.delimiters})
   177  			}
   178  
   179  			if len(subExpressions) > 1 && len(expr) == 5 {
   180  				// If there is a fallback expression evaluator, we apply it on the first replacement alternative
   181  				re := regexp.MustCompile(subExpressions[0])
   182  				replacements = append(replacements, replacement{comment, subExpressions[0], replace, re, expr[4].(replacementFunc), t.delimiters})
   183  			}
   184  		}
   185  		replacementsInit[delimiters] = replacements
   186  	}
   187  }
   188  
   189  func printDebugInfo(r replacement, content string) {
   190  	if r.name == "" || getLogLevelInternal() < logging.INFO {
   191  		return
   192  	}
   193  
   194  	debugMode = true
   195  	defer func() { debugMode = false }()
   196  
   197  	// We only report each match once
   198  	allUnique := make(map[string]int)
   199  	for _, found := range r.re.FindAllString(content, -1) {
   200  		if r.parser != nil {
   201  			newContent := r.re.ReplaceAllStringFunc(found, func(match string) string {
   202  				return r.parser(r, match)
   203  			})
   204  			if newContent == found && getLogLevelInternal() < 6 {
   205  				// There is no change
   206  				continue
   207  			}
   208  		}
   209  		lines := strings.Split(found, "\n")
   210  		for i := range lines {
   211  			lines[i] = fmt.Sprintf("%v%s", iif(i > 0, "  ", ""), highlight(lines[i]))
   212  		}
   213  		found = strings.Join(lines, "\n")
   214  		allUnique[found] = allUnique[found] + 1
   215  	}
   216  
   217  	if len(allUnique) == 0 && getLogLevelInternal() < 7 {
   218  		return
   219  	}
   220  
   221  	matches := make([]string, 0, len(allUnique)+1)
   222  	if len(allUnique) > 0 {
   223  		matches = append(matches, "")
   224  	}
   225  	for key, count := range allUnique {
   226  		if count > 1 {
   227  			key = fmt.Sprintf("%s (%d)", key, count)
   228  		}
   229  		matches = append(matches, key)
   230  	}
   231  
   232  	log.Infof("%s: %s%s", color.YellowString(r.name), r.expr, strings.Join(matches, "\n- "))
   233  	if len(matches) > 0 {
   234  		ErrPrintln()
   235  	}
   236  }
   237  
   238  var debugMode bool