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