gview_parse.go 14 KB

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