github.com/coveo/gotemplate@v2.7.7+incompatible/template/template.go (about) 1 package template 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "reflect" 9 "strings" 10 "sync" 11 "text/template" 12 13 "github.com/coveo/gotemplate/collections" 14 "github.com/coveo/gotemplate/utils" 15 logging "github.com/op/go-logging" 16 ) 17 18 // String is an alias to collections.String 19 type String = collections.String 20 21 var templateMutex sync.Mutex 22 23 // Template let us extend the functionalities of base go template library. 24 type Template struct { 25 *template.Template 26 TempFolder string 27 substitutes []utils.RegexReplacer 28 context interface{} 29 delimiters []string 30 parent *Template 31 folder string 32 children map[string]*Template 33 aliases funcTableMap 34 functions funcTableMap 35 options OptionsSet 36 optionsEnabled OptionsSet 37 } 38 39 // Environment variables that could be defined to override default behaviors. 40 const ( 41 EnvAcceptNoValue = "GOTEMPLATE_NO_VALUE" 42 EnvStrictErrorCheck = "GOTEMPLATE_STRICT_ERROR" 43 EnvSubstitutes = "GOTEMPLATE_SUBSTITUTES" 44 EnvDebug = "GOTEMPLATE_DEBUG" 45 EnvExtensionPath = "GOTEMPLATE_PATH" 46 // TODO: Deprecated, to remove in future version 47 EnvDeprecatedAssign = "GOTEMPLATE_DEPRECATED_ASSIGN" 48 ) 49 50 const ( 51 noGoTemplate = "no-gotemplate!" 52 noRazor = "no-razor!" 53 explicitGoTemplate = "gotemplate!" 54 ) 55 56 // Common variables 57 var ( 58 // ExtensionDepth the depth level of search of gotemplate extension from the current directory (default = 2). 59 ExtensionDepth = 2 60 toStrings = collections.ToStrings 61 acceptNoValue = String(os.Getenv(EnvAcceptNoValue)).ParseBool() 62 strictError = String(os.Getenv(EnvStrictErrorCheck)).ParseBool() 63 Print = utils.ColorPrint 64 Printf = utils.ColorPrintf 65 Println = utils.ColorPrintln 66 ErrPrintf = utils.ColorErrorPrintf 67 ErrPrintln = utils.ColorErrorPrintln 68 ErrPrint = utils.ColorErrorPrint 69 ) 70 71 // IsRazor determines if the supplied code appears to have Razor code (using default delimiters). 72 func IsRazor(code string) bool { return strings.Contains(code, "@") } 73 74 // IsCode determines if the supplied code appears to have gotemplate code (using default delimiters). 75 func IsCode(code string) bool { 76 return IsRazor(code) || strings.Contains(code, "{{") || strings.Contains(code, "}}") 77 } 78 79 // NewTemplate creates an Template object with default initialization. 80 func NewTemplate(folder string, context interface{}, delimiters string, options OptionsSet, substitutes ...string) (result *Template, err error) { 81 defer func() { 82 if rec := recover(); rec != nil { 83 result, err = nil, fmt.Errorf("%v", rec) 84 } 85 }() 86 t := Template{Template: template.New("Main")} 87 must(t.Parse("")) 88 t.options = iif(options != nil, options, DefaultOptions()).(OptionsSet) 89 if acceptNoValue { 90 t.options[AcceptNoValue] = true 91 } 92 if strictError { 93 t.options[StrictErrorCheck] = true 94 } 95 t.optionsEnabled = make(OptionsSet) 96 t.folder, _ = filepath.Abs(iif(folder != "", folder, utils.Pwd()).(string)) 97 t.context = iif(context != nil, context, collections.CreateDictionary()) 98 t.aliases = make(funcTableMap) 99 t.delimiters = []string{"{{", "}}", "@"} 100 101 // Set the regular expression replacements 102 baseSubstitutesRegex := []string{`/(?m)^\s*#!\s*$/`} 103 if substitutesFromEnv := os.Getenv(EnvSubstitutes); substitutesFromEnv != "" { 104 baseSubstitutesRegex = append(baseSubstitutesRegex, strings.Split(substitutesFromEnv, "\n")...) 105 } 106 t.substitutes = utils.InitReplacers(append(baseSubstitutesRegex, substitutes...)...) 107 108 if t.options[Extension] { 109 t.initExtension() 110 } 111 112 // Set the options supplied by caller 113 t.init("") 114 if delimiters != "" { 115 for i, delimiter := range strings.Split(delimiters, ",") { 116 if i == len(t.delimiters) { 117 return nil, fmt.Errorf("Invalid delimiters '%s', must be a maximum of three comma separated parts", delimiters) 118 } 119 if delimiter != "" { 120 t.delimiters[i] = delimiter 121 } 122 } 123 } 124 return &t, nil 125 } 126 127 // MustNewTemplate creates an Template object with default initialization. 128 // It panics if an error occurs. 129 func MustNewTemplate(folder string, context interface{}, delimiters string, options OptionsSet, substitutes ...string) *Template { 130 return must(NewTemplate(folder, context, delimiters, options, substitutes...)).(*Template) 131 } 132 133 // GetNewContext returns a distint context for each folder. 134 func (t Template) GetNewContext(folder string, useCache bool) *Template { 135 folder = iif(folder != "", folder, t.folder).(string) 136 if context, found := t.children[folder]; useCache && found { 137 return context 138 } 139 140 newTemplate := Template(t) 141 newTemplate.Template = template.New(folder) 142 newTemplate.init(folder) 143 newTemplate.parent = &t 144 newTemplate.addFunctions(t.aliases) 145 newTemplate.importTemplates(t) 146 newTemplate.options = make(OptionsSet) 147 // We duplicate the options because the new context may alter them afterwhile and 148 // it should not modify the original values. 149 for k, v := range t.options { 150 newTemplate.options[k] = v 151 } 152 153 if !useCache { 154 return &newTemplate 155 } 156 // We register the new template as a child of the main template 157 t.children[folder] = &newTemplate 158 return t.children[folder] 159 } 160 161 // IsCode determines if the supplied code appears to have gotemplate code. 162 func (t Template) IsCode(code string) bool { 163 return !strings.Contains(code, noGoTemplate) && (t.IsRazor(code) || strings.Contains(code, t.LeftDelim()) || strings.Contains(code, t.RightDelim())) 164 } 165 166 // IsRazor determines if the supplied code appears to have Razor code. 167 func (t Template) IsRazor(code string) bool { 168 return strings.Contains(code, t.RazorDelim()) && !strings.Contains(code, noGoTemplate) && !strings.Contains(code, noRazor) 169 } 170 171 // LeftDelim returns the left delimiter. 172 func (t Template) LeftDelim() string { return t.delimiters[0] } 173 174 // RightDelim returns the right delimiter. 175 func (t Template) RightDelim() string { return t.delimiters[1] } 176 177 // RazorDelim returns the razor delimiter. 178 func (t Template) RazorDelim() string { return t.delimiters[2] } 179 180 // SetOption allows setting of template option after initialization. 181 func (t *Template) SetOption(option Options, value bool) { t.options[option] = value } 182 183 func (t Template) isTemplate(file string) bool { 184 for i := range templateExt { 185 if strings.HasSuffix(file, templateExt[i]) { 186 return true 187 } 188 } 189 return false 190 } 191 192 func (t *Template) initExtension() { 193 ext := t.GetNewContext("", false) 194 ext.options = DefaultOptions() 195 196 // We temporary set the logging level one grade lower 197 logLevel := logging.GetLevel(logger) 198 logging.SetLevel(logLevel-1, logger) 199 defer func() { logging.SetLevel(logLevel, logger) }() 200 201 var extensionfiles []string 202 if extensionFolders := strings.TrimSpace(os.Getenv(EnvExtensionPath)); extensionFolders != "" { 203 for _, path := range strings.Split(extensionFolders, string(os.PathListSeparator)) { 204 if path != "" { 205 files, _ := utils.FindFilesMaxDepth(path, ExtensionDepth, false, "*.gte") 206 extensionfiles = append(extensionfiles, files...) 207 } 208 } 209 } 210 extensionfiles = append(extensionfiles, utils.MustFindFilesMaxDepth(ext.folder, ExtensionDepth, false, "*.gte")...) 211 212 // Retrieve the template extension files 213 for _, file := range extensionfiles { 214 // We just load all the template files available to ensure that all template definition are loaded 215 // We do not use ParseFiles because it names the template with the base name of the file 216 // which result in overriding templates with the same base name in different folders. 217 content := string(must(ioutil.ReadFile(file)).([]byte)) 218 219 // We execute the content, but we ignore errors. The goal is only to register the sub templates and aliases properly 220 // We also do not ask to clone the context as we wish to let extension to be able to alter the supplied context 221 if _, err := ext.processContentInternal(content, file, nil, 0, false); err != nil { 222 log.Error(err) 223 } 224 } 225 226 // Add the children contexts to the main context 227 for _, context := range ext.children { 228 t.importTemplates(*context) 229 } 230 231 // We reset the list of templates 232 t.children = make(map[string]*Template) 233 } 234 235 // Initialize a new template with same attributes as the current context. 236 func (t *Template) init(folder string) { 237 if folder != "" { 238 t.folder, _ = filepath.Abs(folder) 239 } 240 t.addFuncs() 241 t.Parse("") 242 t.children = make(map[string]*Template) 243 t.Delims(t.delimiters[0], t.delimiters[1]) 244 t.setConstant(false, "\n", "NL", "CR", "NEWLINE") 245 t.setConstant(false, true, "true") 246 t.setConstant(false, false, "false") 247 t.setConstant(false, nil, "null") 248 } 249 250 func (t *Template) setConstant(stopOnFirst bool, value interface{}, names ...string) { 251 c, err := collections.TryAsDictionary(t.context) 252 if err != nil { 253 return 254 } 255 256 context := c.AsMap() 257 for i := range names { 258 if val, isSet := context[names[i]]; !isSet { 259 context[names[i]] = value 260 if stopOnFirst { 261 return 262 } 263 } else if isSet && reflect.DeepEqual(value, val) { 264 return 265 } 266 } 267 } 268 269 // Import templates from another template. 270 func (t *Template) importTemplates(source Template) { 271 for _, subTemplate := range source.Templates() { 272 if subTemplate.Name() != subTemplate.ParseName { 273 t.AddParseTree(subTemplate.Name(), subTemplate.Tree) 274 } 275 } 276 }