gview_parse.go 11 KB

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