github.com/gogf/gf@v1.16.9/os/gview/gview_parse.go (about) 1 // Copyright GoFrame Author(https://goframe.org). All Rights Reserved. 2 // 3 // This Source Code Form is subject to the terms of the MIT License. 4 // If a copy of the MIT was not distributed with this file, 5 // You can obtain one at https://github.com/gogf/gf. 6 7 package gview 8 9 import ( 10 "bytes" 11 "context" 12 "fmt" 13 htmltpl "html/template" 14 "strconv" 15 "strings" 16 texttpl "text/template" 17 18 "github.com/gogf/gf/encoding/ghash" 19 "github.com/gogf/gf/errors/gcode" 20 "github.com/gogf/gf/errors/gerror" 21 "github.com/gogf/gf/internal/intlog" 22 "github.com/gogf/gf/os/gfsnotify" 23 "github.com/gogf/gf/os/gmlock" 24 "github.com/gogf/gf/text/gstr" 25 "github.com/gogf/gf/util/gutil" 26 27 "github.com/gogf/gf/os/gres" 28 29 "github.com/gogf/gf/container/gmap" 30 "github.com/gogf/gf/os/gfile" 31 "github.com/gogf/gf/os/glog" 32 "github.com/gogf/gf/os/gspath" 33 ) 34 35 const ( 36 // Template name for content parsing. 37 templateNameForContentParsing = "TemplateContent" 38 ) 39 40 // fileCacheItem is the cache item for template file. 41 type fileCacheItem struct { 42 path string 43 folder string 44 content string 45 } 46 47 var ( 48 // Templates cache map for template folder. 49 // Note that there's no expiring logic for this map. 50 templates = gmap.NewStrAnyMap(true) 51 52 // Try-folders for resource template file searching. 53 resourceTryFolders = []string{"template/", "template", "/template", "/template/"} 54 ) 55 56 // Parse parses given template file `file` with given template variables `params` 57 // and returns the parsed template content. 58 func (view *View) Parse(ctx context.Context, file string, params ...Params) (result string, err error) { 59 var tpl interface{} 60 // It caches the file, folder and its content to enhance performance. 61 r := view.fileCacheMap.GetOrSetFuncLock(file, func() interface{} { 62 var ( 63 path string 64 folder string 65 content string 66 resource *gres.File 67 ) 68 // Searching the absolute file path for `file`. 69 path, folder, resource, err = view.searchFile(file) 70 if err != nil { 71 return nil 72 } 73 if resource != nil { 74 content = string(resource.Content()) 75 } else { 76 content = gfile.GetContentsWithCache(path) 77 } 78 // Monitor template files changes using fsnotify asynchronously. 79 if resource == nil { 80 if _, err := gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) { 81 // CLEAR THEM ALL. 82 view.fileCacheMap.Clear() 83 templates.Clear() 84 gfsnotify.Exit() 85 }); err != nil { 86 intlog.Error(ctx, err) 87 } 88 } 89 return &fileCacheItem{ 90 path: path, 91 folder: folder, 92 content: content, 93 } 94 }) 95 if r == nil { 96 return 97 } 98 item := r.(*fileCacheItem) 99 // It's not necessary continuing parsing if template content is empty. 100 if item.content == "" { 101 return "", nil 102 } 103 // Get the template object instance for `folder`. 104 tpl, err = view.getTemplate(item.path, item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path))) 105 if err != nil { 106 return "", err 107 } 108 // Using memory lock to ensure concurrent safety for template parsing. 109 gmlock.LockFunc("gview.Parse:"+item.path, func() { 110 if view.config.AutoEncode { 111 tpl, err = tpl.(*htmltpl.Template).Parse(item.content) 112 } else { 113 tpl, err = tpl.(*texttpl.Template).Parse(item.content) 114 } 115 if err != nil && item.path != "" { 116 err = gerror.WrapCode(gcode.CodeInternalError, err, item.path) 117 } 118 }) 119 if err != nil { 120 return "", err 121 } 122 // Note that the template variable assignment cannot change the value 123 // of the existing `params` or view.data because both variables are pointers. 124 // It needs to merge the values of the two maps into a new map. 125 variables := gutil.MapMergeCopy(params...) 126 if len(view.data) > 0 { 127 gutil.MapMerge(variables, view.data) 128 } 129 view.setI18nLanguageFromCtx(ctx, variables) 130 131 buffer := bytes.NewBuffer(nil) 132 if view.config.AutoEncode { 133 newTpl, err := tpl.(*htmltpl.Template).Clone() 134 if err != nil { 135 return "", err 136 } 137 if err := newTpl.Execute(buffer, variables); err != nil { 138 return "", err 139 } 140 } else { 141 if err := tpl.(*texttpl.Template).Execute(buffer, variables); err != nil { 142 return "", err 143 } 144 } 145 146 // TODO any graceful plan to replace "<no value>"? 147 result = gstr.Replace(buffer.String(), "<no value>", "") 148 result = view.i18nTranslate(ctx, result, variables) 149 return result, nil 150 } 151 152 // ParseDefault parses the default template file with params. 153 func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) { 154 return view.Parse(ctx, view.config.DefaultFile, params...) 155 } 156 157 // ParseContent parses given template content `content` with template variables `params` 158 // and returns the parsed content in []byte. 159 func (view *View) ParseContent(ctx context.Context, content string, params ...Params) (string, error) { 160 // It's not necessary continuing parsing if template content is empty. 161 if content == "" { 162 return "", nil 163 } 164 err := (error)(nil) 165 key := fmt.Sprintf("%s_%v_%v", templateNameForContentParsing, view.config.Delimiters, view.config.AutoEncode) 166 tpl := templates.GetOrSetFuncLock(key, func() interface{} { 167 if view.config.AutoEncode { 168 return htmltpl.New(templateNameForContentParsing).Delims( 169 view.config.Delimiters[0], 170 view.config.Delimiters[1], 171 ).Funcs(view.funcMap) 172 } 173 return texttpl.New(templateNameForContentParsing).Delims( 174 view.config.Delimiters[0], 175 view.config.Delimiters[1], 176 ).Funcs(view.funcMap) 177 }) 178 // Using memory lock to ensure concurrent safety for content parsing. 179 hash := strconv.FormatUint(ghash.DJBHash64([]byte(content)), 10) 180 gmlock.LockFunc("gview.ParseContent:"+hash, func() { 181 if view.config.AutoEncode { 182 tpl, err = tpl.(*htmltpl.Template).Parse(content) 183 } else { 184 tpl, err = tpl.(*texttpl.Template).Parse(content) 185 } 186 }) 187 if err != nil { 188 return "", err 189 } 190 // Note that the template variable assignment cannot change the value 191 // of the existing `params` or view.data because both variables are pointers. 192 // It needs to merge the values of the two maps into a new map. 193 variables := gutil.MapMergeCopy(params...) 194 if len(view.data) > 0 { 195 gutil.MapMerge(variables, view.data) 196 } 197 view.setI18nLanguageFromCtx(ctx, variables) 198 199 buffer := bytes.NewBuffer(nil) 200 if view.config.AutoEncode { 201 newTpl, err := tpl.(*htmltpl.Template).Clone() 202 if err != nil { 203 return "", err 204 } 205 if err := newTpl.Execute(buffer, variables); err != nil { 206 return "", err 207 } 208 } else { 209 if err := tpl.(*texttpl.Template).Execute(buffer, variables); err != nil { 210 return "", err 211 } 212 } 213 // TODO any graceful plan to replace "<no value>"? 214 result := gstr.Replace(buffer.String(), "<no value>", "") 215 result = view.i18nTranslate(ctx, result, variables) 216 return result, nil 217 } 218 219 // getTemplate returns the template object associated with given template file `path`. 220 // It uses template cache to enhance performance, that is, it will return the same template object 221 // with the same given `path`. It will also automatically refresh the template cache 222 // if the template files under `path` changes (recursively). 223 func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) { 224 // Key for template cache. 225 key := fmt.Sprintf("%s_%v", filePath, view.config.Delimiters) 226 result := templates.GetOrSetFuncLock(key, func() interface{} { 227 tplName := filePath 228 if view.config.AutoEncode { 229 tpl = htmltpl.New(tplName).Delims( 230 view.config.Delimiters[0], 231 view.config.Delimiters[1], 232 ).Funcs(view.funcMap) 233 } else { 234 tpl = texttpl.New(tplName).Delims( 235 view.config.Delimiters[0], 236 view.config.Delimiters[1], 237 ).Funcs(view.funcMap) 238 } 239 // Firstly checking the resource manager. 240 if !gres.IsEmpty() { 241 if files := gres.ScanDirFile(folderPath, pattern, true); len(files) > 0 { 242 var err error 243 if view.config.AutoEncode { 244 t := tpl.(*htmltpl.Template) 245 for _, v := range files { 246 _, err = t.New(v.FileInfo().Name()).Parse(string(v.Content())) 247 if err != nil { 248 err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err) 249 return nil 250 } 251 } 252 } else { 253 t := tpl.(*texttpl.Template) 254 for _, v := range files { 255 _, err = t.New(v.FileInfo().Name()).Parse(string(v.Content())) 256 if err != nil { 257 err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err) 258 return nil 259 } 260 } 261 } 262 return tpl 263 } 264 } 265 266 // Secondly checking the file system. 267 var ( 268 files []string 269 ) 270 files, err = gfile.ScanDir(folderPath, pattern, true) 271 if err != nil { 272 return nil 273 } 274 if view.config.AutoEncode { 275 t := tpl.(*htmltpl.Template) 276 for _, file := range files { 277 if _, err = t.Parse(gfile.GetContents(file)); err != nil { 278 err = view.formatTemplateObjectCreatingError(file, tplName, err) 279 return nil 280 } 281 } 282 } else { 283 t := tpl.(*texttpl.Template) 284 for _, file := range files { 285 if _, err = t.Parse(gfile.GetContents(file)); err != nil { 286 err = view.formatTemplateObjectCreatingError(file, tplName, err) 287 return nil 288 } 289 } 290 } 291 return tpl 292 }) 293 if result != nil { 294 return result, nil 295 } 296 return 297 } 298 299 // formatTemplateObjectCreatingError formats the error that creted from creating template object. 300 func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, err error) error { 301 if err != nil { 302 return gerror.NewCodeSkip(gcode.CodeInternalError, 1, gstr.Replace(err.Error(), tplName, filePath)) 303 } 304 return nil 305 } 306 307 // searchFile returns the found absolute path for `file` and its template folder path. 308 // Note that, the returned `folder` is the template folder path, but not the folder of 309 // the returned template file `path`. 310 func (view *View) searchFile(file string) (path string, folder string, resource *gres.File, err error) { 311 // Firstly checking the resource manager. 312 if !gres.IsEmpty() { 313 // Try folders. 314 for _, folderPath := range resourceTryFolders { 315 if resource = gres.Get(folderPath + file); resource != nil { 316 path = resource.Name() 317 folder = folderPath 318 return 319 } 320 } 321 // Search folders. 322 view.paths.RLockFunc(func(array []string) { 323 for _, v := range array { 324 v = strings.TrimRight(v, "/"+gfile.Separator) 325 if resource = gres.Get(v + "/" + file); resource != nil { 326 path = resource.Name() 327 folder = v 328 break 329 } 330 if resource = gres.Get(v + "/template/" + file); resource != nil { 331 path = resource.Name() 332 folder = v + "/template" 333 break 334 } 335 } 336 }) 337 } 338 339 // Secondly checking the file system. 340 if path == "" { 341 view.paths.RLockFunc(func(array []string) { 342 for _, folderPath := range array { 343 folderPath = strings.TrimRight(folderPath, gfile.Separator) 344 if path, _ = gspath.Search(folderPath, file); path != "" { 345 folder = folderPath 346 break 347 } 348 if path, _ = gspath.Search(folderPath+gfile.Separator+"template", file); path != "" { 349 folder = folderPath + gfile.Separator + "template" 350 break 351 } 352 } 353 }) 354 } 355 356 // Error checking. 357 if path == "" { 358 buffer := bytes.NewBuffer(nil) 359 if view.paths.Len() > 0 { 360 buffer.WriteString(fmt.Sprintf("[gview] cannot find template file \"%s\" in following paths:", file)) 361 view.paths.RLockFunc(func(array []string) { 362 index := 1 363 for _, folderPath := range array { 364 folderPath = strings.TrimRight(folderPath, "/") 365 if folderPath == "" { 366 folderPath = "/" 367 } 368 buffer.WriteString(fmt.Sprintf("\n%d. %s", index, folderPath)) 369 index++ 370 buffer.WriteString(fmt.Sprintf("\n%d. %s", index, strings.TrimRight(folderPath, "/")+gfile.Separator+"template")) 371 index++ 372 } 373 }) 374 } else { 375 buffer.WriteString(fmt.Sprintf("[gview] cannot find template file \"%s\" with no path set/add", file)) 376 } 377 if errorPrint() { 378 glog.Error(buffer.String()) 379 } 380 err = gerror.NewCodef(gcode.CodeInvalidParameter, `template file "%s" not found`, file) 381 } 382 return 383 }