github.com/wangyougui/gf/v2@v2.6.5/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/wangyougui/gf. 6 7 package gview 8 9 import ( 10 "bytes" 11 "context" 12 "fmt" 13 htmltpl "html/template" 14 "strconv" 15 texttpl "text/template" 16 17 "github.com/wangyougui/gf/v2/container/gmap" 18 "github.com/wangyougui/gf/v2/encoding/ghash" 19 "github.com/wangyougui/gf/v2/errors/gcode" 20 "github.com/wangyougui/gf/v2/errors/gerror" 21 "github.com/wangyougui/gf/v2/internal/intlog" 22 "github.com/wangyougui/gf/v2/os/gfile" 23 "github.com/wangyougui/gf/v2/os/gfsnotify" 24 "github.com/wangyougui/gf/v2/os/glog" 25 "github.com/wangyougui/gf/v2/os/gmlock" 26 "github.com/wangyougui/gf/v2/os/gres" 27 "github.com/wangyougui/gf/v2/os/gspath" 28 "github.com/wangyougui/gf/v2/text/gstr" 29 "github.com/wangyougui/gf/v2/util/gutil" 30 ) 31 32 const ( 33 // Template name for content parsing. 34 templateNameForContentParsing = "TemplateContent" 35 ) 36 37 // fileCacheItem is the cache item for template file. 38 type fileCacheItem struct { 39 path string 40 folder string 41 content string 42 } 43 44 var ( 45 // Templates cache map for template folder. 46 // Note that there's no expiring logic for this map. 47 templates = gmap.NewStrAnyMap(true) 48 49 // Try-folders for resource template file searching. 50 resourceTryFolders = []string{ 51 "template/", "template", "/template", "/template/", 52 "resource/template/", "resource/template", "/resource/template", "/resource/template/", 53 } 54 55 // Prefix array for trying searching in local system. 56 localSystemTryFolders = []string{"", "template/", "resource/template"} 57 ) 58 59 // Parse parses given template file `file` with given template variables `params` 60 // and returns the parsed template content. 61 func (view *View) Parse(ctx context.Context, file string, params ...Params) (result string, err error) { 62 var usedParams Params 63 if len(params) > 0 { 64 usedParams = params[0] 65 } 66 return view.ParseOption(ctx, Option{ 67 File: file, 68 Content: "", 69 Orphan: false, 70 Params: usedParams, 71 }) 72 } 73 74 // ParseDefault parses the default template file with params. 75 func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) { 76 var usedParams Params 77 if len(params) > 0 { 78 usedParams = params[0] 79 } 80 return view.ParseOption(ctx, Option{ 81 File: view.config.DefaultFile, 82 Content: "", 83 Orphan: false, 84 Params: usedParams, 85 }) 86 } 87 88 // ParseContent parses given template content `content` with template variables `params` 89 // and returns the parsed content in []byte. 90 func (view *View) ParseContent(ctx context.Context, content string, params ...Params) (string, error) { 91 var usedParams Params 92 if len(params) > 0 { 93 usedParams = params[0] 94 } 95 return view.ParseOption(ctx, Option{ 96 Content: content, 97 Orphan: false, 98 Params: usedParams, 99 }) 100 } 101 102 // Option for template parsing. 103 type Option struct { 104 File string // Template file path in absolute or relative to searching paths. 105 Content string // Template content, it ignores `File` if `Content` is given. 106 Orphan bool // If true, the `File` is considered as a single file parsing without files recursively parsing from its folder. 107 Params Params // Template parameters map. 108 } 109 110 // ParseOption implements template parsing using Option. 111 func (view *View) ParseOption(ctx context.Context, option Option) (result string, err error) { 112 if option.Content != "" { 113 return view.doParseContent(ctx, option.Content, option.Params) 114 } 115 if option.File == "" { 116 return "", gerror.New(`template file cannot be empty`) 117 } 118 // It caches the file, folder and content to enhance performance. 119 r := view.fileCacheMap.GetOrSetFuncLock(option.File, func() interface{} { 120 var ( 121 path string 122 folder string 123 content string 124 resource *gres.File 125 ) 126 // Searching the absolute file path for `file`. 127 path, folder, resource, err = view.searchFile(ctx, option.File) 128 if err != nil { 129 return nil 130 } 131 if resource != nil { 132 content = string(resource.Content()) 133 } else { 134 content = gfile.GetContentsWithCache(path) 135 } 136 // Monitor template files changes using fsnotify asynchronously. 137 if resource == nil { 138 if _, err = gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) { 139 // CLEAR THEM ALL. 140 view.fileCacheMap.Clear() 141 templates.Clear() 142 gfsnotify.Exit() 143 }); err != nil { 144 intlog.Errorf(ctx, `%+v`, err) 145 } 146 } 147 return &fileCacheItem{ 148 path: path, 149 folder: folder, 150 content: content, 151 } 152 }) 153 if r == nil { 154 return 155 } 156 item := r.(*fileCacheItem) 157 // It's not necessary continuing parsing if template content is empty. 158 if item.content == "" { 159 return "", nil 160 } 161 // If it's Orphan option, it just parses the single file by ParseContent. 162 if option.Orphan { 163 return view.doParseContent(ctx, item.content, option.Params) 164 } 165 // Get the template object instance for `folder`. 166 var tpl interface{} 167 tpl, err = view.getTemplate(item.path, item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path))) 168 if err != nil { 169 return "", err 170 } 171 // Using memory lock to ensure concurrent safety for template parsing. 172 gmlock.LockFunc("gview.Parse:"+item.path, func() { 173 if view.config.AutoEncode { 174 tpl, err = tpl.(*htmltpl.Template).Parse(item.content) 175 } else { 176 tpl, err = tpl.(*texttpl.Template).Parse(item.content) 177 } 178 if err != nil && item.path != "" { 179 err = gerror.Wrap(err, item.path) 180 } 181 }) 182 if err != nil { 183 return "", err 184 } 185 // Note that the template variable assignment cannot change the value 186 // of the existing `params` or view.data because both variables are pointers. 187 // It needs to merge the values of the two maps into a new map. 188 variables := gutil.MapMergeCopy(option.Params) 189 if len(view.data) > 0 { 190 gutil.MapMerge(variables, view.data) 191 } 192 view.setI18nLanguageFromCtx(ctx, variables) 193 194 buffer := bytes.NewBuffer(nil) 195 if view.config.AutoEncode { 196 newTpl, err := tpl.(*htmltpl.Template).Clone() 197 if err != nil { 198 return "", err 199 } 200 if err = newTpl.Execute(buffer, variables); err != nil { 201 return "", err 202 } 203 } else { 204 if err = tpl.(*texttpl.Template).Execute(buffer, variables); err != nil { 205 return "", err 206 } 207 } 208 209 // TODO any graceful plan to replace "<no value>"? 210 result = gstr.Replace(buffer.String(), "<no value>", "") 211 result = view.i18nTranslate(ctx, result, variables) 212 return result, nil 213 } 214 215 // doParseContent parses given template content `content` with template variables `params` 216 // and returns the parsed content in []byte. 217 func (view *View) doParseContent(ctx context.Context, content string, params Params) (string, error) { 218 // It's not necessary continuing parsing if template content is empty. 219 if content == "" { 220 return "", nil 221 } 222 var ( 223 err error 224 key = fmt.Sprintf("%s_%v_%v", templateNameForContentParsing, view.config.Delimiters, view.config.AutoEncode) 225 tpl = templates.GetOrSetFuncLock(key, func() interface{} { 226 if view.config.AutoEncode { 227 return htmltpl.New(templateNameForContentParsing).Delims( 228 view.config.Delimiters[0], 229 view.config.Delimiters[1], 230 ).Funcs(view.funcMap) 231 } 232 return texttpl.New(templateNameForContentParsing).Delims( 233 view.config.Delimiters[0], 234 view.config.Delimiters[1], 235 ).Funcs(view.funcMap) 236 }) 237 ) 238 // Using memory lock to ensure concurrent safety for content parsing. 239 hash := strconv.FormatUint(ghash.DJB64([]byte(content)), 10) 240 gmlock.LockFunc("gview.ParseContent:"+hash, func() { 241 if view.config.AutoEncode { 242 tpl, err = tpl.(*htmltpl.Template).Parse(content) 243 } else { 244 tpl, err = tpl.(*texttpl.Template).Parse(content) 245 } 246 }) 247 if err != nil { 248 err = gerror.Wrapf(err, `template parsing failed`) 249 return "", err 250 } 251 // Note that the template variable assignment cannot change the value 252 // of the existing `params` or view.data because both variables are pointers. 253 // It needs to merge the values of the two maps into a new map. 254 variables := gutil.MapMergeCopy(params) 255 if len(view.data) > 0 { 256 gutil.MapMerge(variables, view.data) 257 } 258 view.setI18nLanguageFromCtx(ctx, variables) 259 260 buffer := bytes.NewBuffer(nil) 261 if view.config.AutoEncode { 262 var newTpl *htmltpl.Template 263 newTpl, err = tpl.(*htmltpl.Template).Clone() 264 if err != nil { 265 err = gerror.Wrapf(err, `template clone failed`) 266 return "", err 267 } 268 if err = newTpl.Execute(buffer, variables); err != nil { 269 err = gerror.Wrapf(err, `template parsing failed`) 270 return "", err 271 } 272 } else { 273 if err = tpl.(*texttpl.Template).Execute(buffer, variables); err != nil { 274 err = gerror.Wrapf(err, `template parsing failed`) 275 return "", err 276 } 277 } 278 // TODO any graceful plan to replace "<no value>"? 279 result := gstr.Replace(buffer.String(), "<no value>", "") 280 result = view.i18nTranslate(ctx, result, variables) 281 return result, nil 282 } 283 284 // getTemplate returns the template object associated with given template file `path`. 285 // It uses template cache to enhance performance, that is, it will return the same template object 286 // with the same given `path`. It will also automatically refresh the template cache 287 // if the template files under `path` changes (recursively). 288 func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) { 289 var ( 290 mapKey = fmt.Sprintf("%s_%v", filePath, view.config.Delimiters) 291 mapFunc = func() interface{} { 292 tplName := filePath 293 if view.config.AutoEncode { 294 tpl = htmltpl.New(tplName).Delims( 295 view.config.Delimiters[0], 296 view.config.Delimiters[1], 297 ).Funcs(view.funcMap) 298 } else { 299 tpl = texttpl.New(tplName).Delims( 300 view.config.Delimiters[0], 301 view.config.Delimiters[1], 302 ).Funcs(view.funcMap) 303 } 304 // Firstly checking the resource manager. 305 if !gres.IsEmpty() { 306 if files := gres.ScanDirFile(folderPath, pattern, true); len(files) > 0 { 307 if view.config.AutoEncode { 308 var t = tpl.(*htmltpl.Template) 309 for _, v := range files { 310 _, err = t.New(v.FileInfo().Name()).Parse(string(v.Content())) 311 if err != nil { 312 err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err) 313 return nil 314 } 315 } 316 } else { 317 var t = tpl.(*texttpl.Template) 318 for _, v := range files { 319 _, err = t.New(v.FileInfo().Name()).Parse(string(v.Content())) 320 if err != nil { 321 err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err) 322 return nil 323 } 324 } 325 } 326 return tpl 327 } 328 } 329 330 // Secondly checking the file system, 331 // and then automatically parsing all its sub-files recursively. 332 var files []string 333 files, err = gfile.ScanDir(folderPath, pattern, true) 334 if err != nil { 335 return nil 336 } 337 if view.config.AutoEncode { 338 t := tpl.(*htmltpl.Template) 339 for _, file := range files { 340 if _, err = t.Parse(gfile.GetContents(file)); err != nil { 341 err = view.formatTemplateObjectCreatingError(file, tplName, err) 342 return nil 343 } 344 } 345 } else { 346 t := tpl.(*texttpl.Template) 347 for _, file := range files { 348 if _, err = t.Parse(gfile.GetContents(file)); err != nil { 349 err = view.formatTemplateObjectCreatingError(file, tplName, err) 350 return nil 351 } 352 } 353 } 354 return tpl 355 } 356 ) 357 result := templates.GetOrSetFuncLock(mapKey, mapFunc) 358 if result != nil { 359 return result, nil 360 } 361 return 362 } 363 364 // formatTemplateObjectCreatingError formats the error that created from creating template object. 365 func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, err error) error { 366 if err != nil { 367 return gerror.NewSkip(1, gstr.Replace(err.Error(), tplName, filePath)) 368 } 369 return nil 370 } 371 372 // searchFile returns the found absolute path for `file` and its template folder path. 373 // Note that, the returned `folder` is the template folder path, but not the folder of 374 // the returned template file `path`. 375 func (view *View) searchFile(ctx context.Context, file string) (path string, folder string, resource *gres.File, err error) { 376 var tempPath string 377 // Firstly checking the resource manager. 378 if !gres.IsEmpty() { 379 // Try folders. 380 for _, tryFolder := range resourceTryFolders { 381 tempPath = tryFolder + file 382 if resource = gres.Get(tempPath); resource != nil { 383 path = resource.Name() 384 folder = tryFolder 385 return 386 } 387 } 388 // Search folders. 389 view.searchPaths.RLockFunc(func(array []string) { 390 for _, searchPath := range array { 391 for _, tryFolder := range resourceTryFolders { 392 tempPath = searchPath + tryFolder + file 393 if resFile := gres.Get(tempPath); resFile != nil { 394 path = resFile.Name() 395 folder = searchPath + tryFolder 396 return 397 } 398 } 399 } 400 }) 401 } 402 403 // Secondly checking the file system. 404 if path == "" { 405 // Absolute path. 406 path = gfile.RealPath(file) 407 if path != "" { 408 folder = gfile.Dir(path) 409 return 410 } 411 // In search paths. 412 view.searchPaths.RLockFunc(func(array []string) { 413 for _, searchPath := range array { 414 searchPath = gstr.TrimRight(searchPath, `\/`) 415 for _, tryFolder := range localSystemTryFolders { 416 relativePath := gstr.TrimRight( 417 gfile.Join(tryFolder, file), 418 `\/`, 419 ) 420 if path, _ = gspath.Search(searchPath, relativePath); path != "" { 421 folder = gfile.Join(searchPath, tryFolder) 422 return 423 } 424 } 425 } 426 }) 427 } 428 429 // Error checking. 430 if path == "" { 431 buffer := bytes.NewBuffer(nil) 432 if view.searchPaths.Len() > 0 { 433 buffer.WriteString(fmt.Sprintf("cannot find template file \"%s\" in following paths:", file)) 434 view.searchPaths.RLockFunc(func(array []string) { 435 index := 1 436 for _, searchPath := range array { 437 searchPath = gstr.TrimRight(searchPath, `\/`) 438 for _, tryFolder := range localSystemTryFolders { 439 buffer.WriteString(fmt.Sprintf( 440 "\n%d. %s", 441 index, gfile.Join(searchPath, tryFolder), 442 )) 443 index++ 444 } 445 } 446 }) 447 } else { 448 buffer.WriteString(fmt.Sprintf("cannot find template file \"%s\" with no path set/add", file)) 449 } 450 if errorPrint() { 451 glog.Error(ctx, buffer.String()) 452 } 453 err = gerror.NewCodef(gcode.CodeInvalidParameter, `template file "%s" not found`, file) 454 } 455 return 456 }