rendering.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. package engine
  2. import (
  3. "bytes"
  4. "fmt"
  5. "html/template"
  6. "net/http"
  7. "path/filepath"
  8. "strings"
  9. "github.com/eknkc/amber"
  10. "github.com/evanw/esbuild/pkg/api"
  11. "github.com/gomarkdown/markdown"
  12. "github.com/gomarkdown/markdown/parser"
  13. log "github.com/sirupsen/logrus"
  14. "github.com/wellington/sass/compiler"
  15. "github.com/xyproto/algernon/console"
  16. "github.com/xyproto/algernon/lua/convert"
  17. "github.com/xyproto/algernon/themes"
  18. "github.com/xyproto/algernon/utils"
  19. lua "github.com/xyproto/gopher-lua"
  20. "github.com/xyproto/pongo2"
  21. "github.com/xyproto/splash"
  22. "github.com/yosssi/gcss"
  23. )
  24. // ValidGCSS checks if the given data is valid GCSS.
  25. // The error value is returned on the channel.
  26. func ValidGCSS(gcssdata []byte, errorReturn chan error) {
  27. buf := bytes.NewBuffer(gcssdata)
  28. var w bytes.Buffer
  29. _, err := gcss.Compile(&w, buf)
  30. errorReturn <- err
  31. }
  32. // LoadRenderFunctions adds functions related to rendering text to the given
  33. // Lua state struct
  34. func (ac *Config) LoadRenderFunctions(w http.ResponseWriter, _ *http.Request, L *lua.LState) {
  35. // Output Markdown as HTML
  36. L.SetGlobal("mprint", L.NewFunction(func(L *lua.LState) int {
  37. // Retrieve all the function arguments as a bytes.Buffer
  38. buf := convert.Arguments2buffer(L, true)
  39. // Create a Markdown parser with the desired extensions
  40. extensions := parser.CommonExtensions | parser.AutoHeadingIDs
  41. mdParser := parser.NewWithExtensions(extensions)
  42. // Convert the buffer from Markdown to HTML
  43. htmlData := markdown.ToHTML(buf.Bytes(), mdParser, nil)
  44. // Apply syntax highlighting
  45. codeStyle := "base16-snazzy"
  46. if highlightedHTML, err := splash.Splash(htmlData, codeStyle); err == nil { // success
  47. htmlData = highlightedHTML
  48. }
  49. w.Write(htmlData)
  50. return 0 // number of results
  51. }))
  52. // Output text as rendered amber.
  53. L.SetGlobal("aprint", L.NewFunction(func(L *lua.LState) int {
  54. // Retrieve all the function arguments as a bytes.Buffer
  55. buf := convert.Arguments2buffer(L, true)
  56. // Use the buffer as a template.
  57. // Options are "Pretty printing, but without line numbers."
  58. tpl, err := amber.Compile(buf.String(), amber.Options{PrettyPrint: true, LineNumbers: false})
  59. if err != nil {
  60. if ac.debugMode {
  61. fmt.Fprint(w, "Could not compile Amber template:\n\t"+err.Error()+"\n\n"+buf.String())
  62. } else {
  63. log.Errorf("Could not compile Amber template:\n%s\n%s", err, buf.String())
  64. }
  65. return 0 // number of results
  66. }
  67. // Using "MISSING" instead of nil for a slightly better error message
  68. // if the values in the template should not be found.
  69. tpl.Execute(w, "MISSING")
  70. return 0 // number of results
  71. }))
  72. // Output text as rendered Pongo2
  73. L.SetGlobal("poprint", L.NewFunction(func(L *lua.LState) int {
  74. pongoMap := make(pongo2.Context)
  75. // Use the first argument as the template and the second argument as the data map
  76. templateString := L.CheckString(1)
  77. // If a table is given as the second argument, fill pongoMap with keys and values
  78. if L.GetTop() >= 2 {
  79. mapSS, mapSI, _, _ := convert.Table2maps(L.CheckTable(2))
  80. for k, v := range mapSI {
  81. pongoMap[k] = v
  82. }
  83. for k, v := range mapSS {
  84. pongoMap[k] = v
  85. }
  86. }
  87. // Retrieve all the function arguments as a bytes.Buffer
  88. buf := convert.Arguments2buffer(L, true)
  89. // Use the buffer as a template.
  90. // Options are "Pretty printing, but without line numbers."
  91. tpl, err := pongo2.FromString(templateString)
  92. if err != nil {
  93. if ac.debugMode {
  94. fmt.Fprint(w, "Could not compile Pongo2 template:\n\t"+err.Error()+"\n\n"+buf.String())
  95. } else {
  96. log.Errorf("Could not compile Pongo2 template:\n%s\n%s", err, buf.String())
  97. }
  98. return 0 // number of results
  99. }
  100. // nil is the template context (variables etc in a map)
  101. if err := tpl.ExecuteWriter(pongoMap, w); err != nil {
  102. if ac.debugMode {
  103. fmt.Fprint(w, "Could not compile Pongo2:\n\t"+err.Error()+"\n\n"+buf.String())
  104. } else {
  105. log.Errorf("Could not compile Pongo2:\n%s\n%s", err, buf.String())
  106. }
  107. }
  108. return 0 // number of results
  109. }))
  110. // Output text as rendered GCSS
  111. L.SetGlobal("gprint", L.NewFunction(func(L *lua.LState) int {
  112. // Retrieve all the function arguments as a bytes.Buffer
  113. buf := convert.Arguments2buffer(L, true)
  114. // Transform GCSS to CSS and output the result.
  115. // Ignoring the number of bytes written.
  116. if _, err := gcss.Compile(w, &buf); err != nil {
  117. if ac.debugMode {
  118. fmt.Fprint(w, "Could not compile GCSS:\n\t"+err.Error()+"\n\n"+buf.String())
  119. } else {
  120. log.Errorf("Could not compile GCSS:\n%s\n%s", err, buf.String())
  121. }
  122. // return 0 // number of results
  123. }
  124. return 0 // number of results
  125. }))
  126. // Output text as rendered JSX for React
  127. L.SetGlobal("jprint", L.NewFunction(func(L *lua.LState) int {
  128. // Retrieve all the function arguments as a bytes.Buffer
  129. buf := convert.Arguments2buffer(L, true)
  130. // Transform JSX to JavaScript and output the result.
  131. result := api.Transform(buf.String(), ac.jsxOptions)
  132. if len(result.Errors) > 0 {
  133. if ac.debugMode {
  134. // TODO: Use a similar error page as for Lua
  135. for _, errMsg := range result.Errors {
  136. fmt.Fprintf(w, "error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
  137. }
  138. for _, warnMsg := range result.Warnings {
  139. fmt.Fprintf(w, "warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
  140. }
  141. } else {
  142. // TODO: Use a similar error page as for Lua
  143. for _, errMsg := range result.Errors {
  144. log.Errorf("error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
  145. }
  146. for _, warnMsg := range result.Warnings {
  147. log.Errorf("warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
  148. }
  149. }
  150. return 0 // number of results
  151. }
  152. n, err := w.Write(result.Code)
  153. if err != nil || n == 0 {
  154. if ac.debugMode {
  155. fmt.Fprint(w, "Result from generated JavaScript is empty\n")
  156. } else {
  157. log.Error("Result from generated JavaScript is empty\n")
  158. }
  159. return 0 // number of results
  160. }
  161. return 0 // number of results
  162. }))
  163. // Output text as rendered JSX for HyperApp
  164. L.SetGlobal("hprint", L.NewFunction(func(L *lua.LState) int {
  165. // Retrieve all the function arguments as a bytes.Buffer
  166. buf := convert.Arguments2buffer(L, true)
  167. // Transform JSX to JavaScript and output the result.
  168. result := api.Transform(buf.String(), ac.jsxOptions)
  169. if len(result.Errors) > 0 {
  170. if ac.debugMode {
  171. // TODO: Use a similar error page as for Lua
  172. for _, errMsg := range result.Errors {
  173. fmt.Fprintf(w, "error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
  174. }
  175. for _, warnMsg := range result.Warnings {
  176. fmt.Fprintf(w, "warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
  177. }
  178. } else {
  179. // TODO: Use a similar error page as for Lua
  180. for _, errMsg := range result.Errors {
  181. log.Errorf("error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
  182. }
  183. for _, warnMsg := range result.Warnings {
  184. log.Errorf("warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
  185. }
  186. }
  187. return 0 // number of results
  188. }
  189. data := result.Code
  190. // Use "h" instead of "React.createElement" for hyperApp apps
  191. data = bytes.ReplaceAll(data, []byte("React.createElement("), []byte("h("))
  192. n, err := w.Write(data)
  193. if err != nil || n == 0 {
  194. if ac.debugMode {
  195. fmt.Fprint(w, "Result from generated JavaScript is empty\n")
  196. } else {
  197. log.Error("Result from generated JavaScript is empty\n")
  198. }
  199. return 0 // number of results
  200. }
  201. return 0 // number of results
  202. }))
  203. // Output a simple message HTML page.
  204. // The first argument is the message (ends up in the <body>).
  205. // The seconds argument is an optional title.
  206. // The third argument is an optional page style.
  207. L.SetGlobal("msgpage", L.NewFunction(func(L *lua.LState) int {
  208. title := ""
  209. body := ""
  210. if L.GetTop() < 2 {
  211. // Uses an empty string if no first argument is given
  212. body = L.ToString(1)
  213. } else {
  214. title = L.ToString(1)
  215. body = L.ToString(2)
  216. }
  217. // The default theme for single page messages
  218. theme := "redbox"
  219. if L.GetTop() >= 3 {
  220. theme = L.ToString(3)
  221. }
  222. // Write a simple HTML page to the client
  223. w.Write([]byte(themes.MessagePage(title, body, theme)))
  224. return 0 // number of results
  225. }))
  226. }
  227. // MarkdownPage write the given source bytes as markdown wrapped in HTML to a writer, with a title
  228. func (ac *Config) MarkdownPage(w http.ResponseWriter, req *http.Request, data []byte, filename string) {
  229. // Prepare for receiving title and codeStyle information
  230. searchKeywords := []string{"title", "codestyle", "theme", "replace_with_theme", "css", "favicon"}
  231. // Also prepare for receiving meta tag information
  232. searchKeywords = append(searchKeywords, themes.MetaKeywords...)
  233. // Extract keywords from the given data, and remove the lines with keywords,
  234. // but only the first time that keyword occurs.
  235. var kwmap map[string][]byte
  236. data, kwmap = utils.ExtractKeywords(data, searchKeywords)
  237. // Create a Markdown parser with the desired extensions
  238. extensions := parser.CommonExtensions | parser.AutoHeadingIDs
  239. mdParser := parser.NewWithExtensions(extensions)
  240. // Convert from Markdown to HTML
  241. htmlbody := markdown.ToHTML(data, mdParser, nil)
  242. // TODO: Check if handling "# title <tags" on the first line is valid
  243. // Markdown or not. Submit a patch to gomarkdown/markdown if it is.
  244. var h1title []byte
  245. if bytes.HasPrefix(htmlbody, []byte("<p>#")) {
  246. fields := bytes.Split(htmlbody, []byte("<"))
  247. if len(fields) > 2 {
  248. h1title = bytes.TrimPrefix(fields[1][2:], []byte("#"))
  249. htmlbody = htmlbody[3+len(h1title):] // 3 is the length of <p>
  250. }
  251. }
  252. // Checkboxes
  253. htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li>[ ] "), []byte("<li><input type=\"checkbox\" disabled> "))
  254. htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li><p>[ ] "), []byte("<li><p><input type=\"checkbox\" disabled> "))
  255. htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li>[x] "), []byte("<li><input type=\"checkbox\" disabled checked> "))
  256. htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li>[X] "), []byte("<li><input type=\"checkbox\" disabled checked> "))
  257. htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li><p>[x] "), []byte("<li><p><input type=\"checkbox\" disabled checked> "))
  258. // These should work by default, but does not.
  259. // TODO: Look into how gomarkdown/markdown handles this.
  260. htmlbody = bytes.ReplaceAll(htmlbody, []byte("&amp;gt;"), []byte("&gt;"))
  261. htmlbody = bytes.ReplaceAll(htmlbody, []byte("&amp;lt;"), []byte("&lt;"))
  262. // If there is no given title, use the h1title
  263. title := kwmap["title"]
  264. if len(title) == 0 {
  265. if len(h1title) != 0 {
  266. title = h1title
  267. } else {
  268. // If no title has been provided, use the filename
  269. title = []byte(filepath.Base(filename))
  270. }
  271. }
  272. // Find the theme that should be used
  273. theme := kwmap["theme"]
  274. if len(theme) == 0 {
  275. theme = []byte(ac.defaultTheme)
  276. }
  277. // Theme aliases. Use a map if there are more than 2 aliases in the future.
  278. if string(theme) == "default" {
  279. // Use the "material" theme by default for Markdown
  280. theme = []byte("material")
  281. }
  282. // Check if a specific string should be replaced with the current theme
  283. replaceWithTheme := kwmap["replace_with_theme"]
  284. if len(replaceWithTheme) != 0 {
  285. // Replace all instances of the value given with "replace_with_theme: ..." with the current theme name
  286. htmlbody = bytes.ReplaceAll(htmlbody, replaceWithTheme, []byte(theme))
  287. }
  288. // If the theme is a filename, create a custom theme where the file is imported from the CSS
  289. if bytes.Contains(theme, []byte(".")) {
  290. st := string(theme)
  291. themes.NewTheme(st, []byte("@import url("+st+");"), themes.DefaultCustomCodeStyle)
  292. }
  293. var head strings.Builder
  294. // If a favicon is specified, use that
  295. favicon := kwmap["favicon"]
  296. if len(favicon) > 0 {
  297. head.WriteString(`<link rel="shortcut icon" type="image/`)
  298. // Only support the most common mime formats for favicons
  299. switch {
  300. case bytes.HasSuffix(favicon, []byte(".ico")):
  301. head.WriteString("x-icon")
  302. case bytes.HasSuffix(favicon, []byte(".bmp")):
  303. head.WriteString("bmp")
  304. case bytes.HasSuffix(favicon, []byte(".gif")):
  305. head.WriteString("gif")
  306. case bytes.HasSuffix(favicon, []byte(".jpg")):
  307. head.WriteString("jpeg")
  308. default:
  309. head.WriteString("png")
  310. }
  311. head.WriteString(`" href="`)
  312. head.Write(favicon)
  313. head.WriteString(`"/>`)
  314. }
  315. // If style.gcss is present, use that style in <head>
  316. CSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultCSSFilename)
  317. GCSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultGCSSFilename)
  318. switch {
  319. case ac.fs.Exists(CSSFilename):
  320. // Link to stylesheet (without checking if the CSS file is valid first)
  321. head.WriteString(`<link href="`)
  322. head.WriteString(themes.DefaultCSSFilename)
  323. head.WriteString(`" rel="stylesheet" type="text/css">`)
  324. case ac.fs.Exists(GCSSFilename):
  325. if ac.debugMode {
  326. gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
  327. if err != nil {
  328. fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
  329. return
  330. }
  331. gcssdata := gcssblock.Bytes()
  332. // Try compiling the GCSS file first
  333. errChan := make(chan error)
  334. go ValidGCSS(gcssdata, errChan)
  335. err = <-errChan
  336. if err != nil {
  337. // Invalid GCSS, return an error page
  338. ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
  339. return
  340. }
  341. }
  342. // Link to stylesheet (without checking if the GCSS file is valid first)
  343. head.WriteString(`<link href="`)
  344. head.WriteString(themes.DefaultGCSSFilename)
  345. head.WriteString(`" rel="stylesheet" type="text/css">`)
  346. default:
  347. // If not, use the theme by inserting the CSS style directly
  348. head.Write(themes.StyleHead(string(theme)))
  349. }
  350. // Additional CSS file
  351. additionalCSSfile := string(kwmap["css"])
  352. if additionalCSSfile != "" {
  353. // If serving a single Markdown file, include the CSS file inline in a style tag
  354. if ac.markdownMode && ac.fs.Exists(additionalCSSfile) {
  355. // Cache the CSS only if Markdown should be cached
  356. cssblock, err := ac.cache.Read(additionalCSSfile, ac.shouldCache(".md"))
  357. if err != nil {
  358. fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
  359. return
  360. }
  361. cssdata := cssblock.Bytes()
  362. head.WriteString("<style>" + string(cssdata) + "</style>")
  363. } else {
  364. head.WriteString(`<link href="`)
  365. head.WriteString(additionalCSSfile)
  366. head.WriteString(`" rel="stylesheet" type="text/css">`)
  367. }
  368. }
  369. codeStyle := string(kwmap["codestyle"])
  370. // Add meta tags, if metadata information has been declared
  371. for _, keyword := range themes.MetaKeywords {
  372. if len(kwmap[keyword]) != 0 {
  373. // Add the meta tag
  374. head.WriteString(`<meta name="`)
  375. head.WriteString(keyword)
  376. head.WriteString(`" content="`)
  377. head.Write(kwmap[keyword])
  378. head.WriteString(`" />`)
  379. }
  380. }
  381. // Embed the style and rendered markdown into a simple HTML 5 page
  382. htmldata := themes.SimpleHTMLPage(title, h1title, []byte(head.String()), htmlbody)
  383. // Add syntax highlighting to the header, but only if "<pre" is present
  384. if bytes.Contains(htmlbody, []byte("<pre")) {
  385. // If codeStyle is not "none", highlight the current htmldata
  386. if codeStyle == "" {
  387. // Use the highlight style from the current theme
  388. highlighted, err := splash.UnescapeSplash(htmldata, themes.ThemeToCodeStyle(string(theme)))
  389. if err != nil {
  390. log.Error(err)
  391. } else {
  392. // Only use the new and highlighted HTML if there were no errors
  393. htmldata = highlighted
  394. }
  395. } else if codeStyle != "none" {
  396. // Use the highlight style from codeStyle
  397. highlighted, err := splash.UnescapeSplash(htmldata, codeStyle)
  398. if err != nil {
  399. log.Error(err)
  400. } else {
  401. // Only use the new HTML if there were no errors
  402. htmldata = highlighted
  403. }
  404. }
  405. }
  406. // If the auto-refresh feature has been enabled
  407. if ac.autoRefresh {
  408. // Insert JavaScript for refreshing the page into the generated HTML
  409. htmldata = ac.InsertAutoRefresh(req, htmldata)
  410. }
  411. // Write the rendered Markdown page to the client
  412. ac.DataToClient(w, req, filename, htmldata)
  413. }
  414. // PongoPage write the given source bytes (ina Pongo2) converted to HTML, to a writer.
  415. // The filename is only used in error messages, if any.
  416. func (ac *Config) PongoPage(w http.ResponseWriter, req *http.Request, filename string, pongodata []byte, funcs template.FuncMap) {
  417. var (
  418. buf bytes.Buffer
  419. linkInGCSS, linkInCSS bool
  420. dirName = filepath.Dir(filename)
  421. GCSSFilename = filepath.Join(dirName, themes.DefaultGCSSFilename)
  422. CSSFilename = filepath.Join(dirName, themes.DefaultCSSFilename)
  423. )
  424. // If style.gcss is present, and a header is present, and it has not already been linked in, link it in
  425. if ac.fs.Exists(CSSFilename) {
  426. linkInCSS = true
  427. } else if ac.fs.Exists(GCSSFilename) {
  428. if ac.debugMode {
  429. gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
  430. if err != nil {
  431. fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
  432. return
  433. }
  434. gcssdata := gcssblock.Bytes()
  435. // Try compiling the GCSS file before the Pongo2 file
  436. errChan := make(chan error)
  437. go ValidGCSS(gcssdata, errChan)
  438. err = <-errChan
  439. if err != nil {
  440. // Invalid GCSS, return an error page
  441. ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
  442. return
  443. }
  444. }
  445. linkInGCSS = true
  446. }
  447. // Set the base directory for Pongo2 to the one where the given filename is
  448. if err := pongo2.DefaultLoader.SetBaseDir(dirName); err != nil {
  449. if ac.debugMode {
  450. ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
  451. } else {
  452. log.Errorf("Could not set base directory for Pongo2 to %s:\n%s", dirName, err)
  453. }
  454. return
  455. }
  456. // Prepare a Pongo2 template
  457. tpl, err := pongo2.DefaultSet.FromBytes(pongodata)
  458. if err != nil {
  459. if ac.debugMode {
  460. ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
  461. } else {
  462. log.Errorf("Could not compile Pongo2 template:\n%s\n%s", err, string(pongodata))
  463. }
  464. return
  465. }
  466. okfuncs := make(pongo2.Context)
  467. // Go through the global Lua scope
  468. for k, v := range funcs {
  469. // Skip the ones starting with an underscore
  470. //if strings.HasPrefix(k, "_") {
  471. // continue
  472. //}
  473. // Check if the name in question is a function
  474. if f, ok := v.(func(...string) (any, error)); ok {
  475. // For the closure to correctly wrap the key value
  476. k := k
  477. // Wrap the Lua functions as Pongo2 functions
  478. wrapfunc := func(vals ...*pongo2.Value) *pongo2.Value {
  479. // Convert the Pongo2 arguments to string arguments
  480. strs := make([]string, len(vals))
  481. for i, sv := range vals {
  482. strs[i] = sv.String()
  483. }
  484. // Call the Lua function
  485. retval, err := f(strs...)
  486. // Return the error if things go wrong
  487. if err != nil {
  488. return pongo2.AsValue(err)
  489. }
  490. // Return the returned value if things went well
  491. return pongo2.AsValue(retval)
  492. }
  493. // Save the wrapped function for the pongo2 template execution
  494. okfuncs[k] = wrapfunc
  495. } else if s, ok := v.(string); ok {
  496. // String variables
  497. okfuncs[k] = s
  498. } else {
  499. // Exposing variable as it is.
  500. // TODO: Add more tests for this codepath
  501. okfuncs[k] = v
  502. }
  503. }
  504. // Make the Lua functions available to Pongo
  505. pongo2.Globals.Update(okfuncs)
  506. defer func() {
  507. if r := recover(); r != nil {
  508. errmsg := fmt.Sprintf("Pongo2 error: %s", r)
  509. if ac.debugMode {
  510. ac.PrettyError(w, req, filename, pongodata, errmsg, "pongo2")
  511. } else {
  512. log.Errorf("Could not execute Pongo2 template:\n%s", errmsg)
  513. }
  514. }
  515. }()
  516. // Render the Pongo2 template to the buffer
  517. err = tpl.ExecuteWriter(pongo2.Globals, &buf)
  518. if err != nil {
  519. // if err := tpl.ExecuteWriterUnbuffered(pongo2.Globals, &buf); err != nil {
  520. if ac.debugMode {
  521. ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
  522. } else {
  523. log.Errorf("Could not execute Pongo2 template:\n%s", err)
  524. }
  525. return
  526. }
  527. // Check if we are dealing with HTML
  528. if strings.Contains(buf.String(), "<html>") {
  529. if linkInCSS || linkInGCSS {
  530. // Link in stylesheet
  531. htmldata := buf.Bytes()
  532. if linkInCSS {
  533. htmldata = themes.StyleHTML(htmldata, themes.DefaultCSSFilename)
  534. } else if linkInGCSS {
  535. htmldata = themes.StyleHTML(htmldata, themes.DefaultGCSSFilename)
  536. }
  537. buf.Reset()
  538. _, err := buf.Write(htmldata)
  539. if err != nil {
  540. if ac.debugMode {
  541. ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
  542. } else {
  543. log.Errorf("Can not write bytes to a buffer! Out of memory?\n%s", err)
  544. }
  545. return
  546. }
  547. }
  548. // If the auto-refresh feature has been enabled
  549. if ac.autoRefresh {
  550. // Insert JavaScript for refreshing the page into the generated HTML
  551. changedBytes := ac.InsertAutoRefresh(req, buf.Bytes())
  552. buf.Reset()
  553. _, err := buf.Write(changedBytes)
  554. if err != nil {
  555. if ac.debugMode {
  556. ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
  557. } else {
  558. log.Errorf("Can not write bytes to a buffer! Out of memory?\n%s", err)
  559. }
  560. return
  561. }
  562. }
  563. // If doctype is missing, add doctype for HTML5 at the top
  564. changedBytes := themes.InsertDoctype(buf.Bytes())
  565. buf.Reset()
  566. buf.Write(changedBytes)
  567. }
  568. // Write the rendered template to the client
  569. ac.DataToClient(w, req, filename, buf.Bytes())
  570. }
  571. // AmberPage the given source bytes (in Amber) converted to HTML, to a writer.
  572. // The filename is only used in error messages, if any.
  573. func (ac *Config) AmberPage(w http.ResponseWriter, req *http.Request, filename string, amberdata []byte, funcs template.FuncMap) {
  574. var (
  575. buf bytes.Buffer
  576. // If style.gcss is present, and a header is present, and it has not already been linked in, link it in
  577. dirName = filepath.Dir(filename)
  578. GCSSFilename = filepath.Join(dirName, themes.DefaultGCSSFilename)
  579. CSSFilename = filepath.Join(dirName, themes.DefaultCSSFilename)
  580. )
  581. if ac.fs.Exists(CSSFilename) {
  582. // Link to stylesheet (without checking if the GCSS file is valid first)
  583. amberdata = themes.StyleAmber(amberdata, themes.DefaultCSSFilename)
  584. } else if ac.fs.Exists(GCSSFilename) {
  585. if ac.debugMode {
  586. gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
  587. if err != nil {
  588. fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
  589. return
  590. }
  591. gcssdata := gcssblock.Bytes()
  592. // Try compiling the GCSS file before the Amber file
  593. errChan := make(chan error)
  594. go ValidGCSS(gcssdata, errChan)
  595. err = <-errChan
  596. if err != nil {
  597. // Invalid GCSS, return an error page
  598. ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
  599. return
  600. }
  601. }
  602. // Link to stylesheet (without checking if the GCSS file is valid first)
  603. amberdata = themes.StyleAmber(amberdata, themes.DefaultGCSSFilename)
  604. }
  605. // Compile the given amber template
  606. tpl, err := amber.CompileData(amberdata, filename, amber.Options{PrettyPrint: true, LineNumbers: false})
  607. if err != nil {
  608. if ac.debugMode {
  609. ac.PrettyError(w, req, filename, amberdata, err.Error(), "amber")
  610. } else {
  611. log.Errorf("Could not compile Amber template:\n%s\n%s", err, string(amberdata))
  612. }
  613. return
  614. }
  615. // Render the Amber template to the buffer
  616. if err := tpl.Execute(&buf, funcs); err != nil {
  617. // If it was one particular error, where the template can not find the
  618. // function or variable name that is used, give the user a friendlier
  619. // message.
  620. if strings.TrimSpace(err.Error()) == "reflect: call of reflect.Value.Type on zero Value" {
  621. errortext := "Could not execute Amber template!<br>One of the functions called by the template is not available."
  622. if ac.debugMode {
  623. ac.PrettyError(w, req, filename, amberdata, errortext, "amber")
  624. } else {
  625. errortext = strings.Replace(errortext, "<br>", "\n", 1)
  626. log.Errorf("Could not execute Amber template:\n%s", errortext)
  627. }
  628. } else {
  629. if ac.debugMode {
  630. ac.PrettyError(w, req, filename, amberdata, err.Error(), "amber")
  631. } else {
  632. log.Errorf("Could not execute Amber template:\n%s", err)
  633. }
  634. }
  635. return
  636. }
  637. // If the auto-refresh feature has been enabled
  638. if ac.autoRefresh {
  639. // Insert JavaScript for refreshing the page into the generated HTML
  640. changedBytes := ac.InsertAutoRefresh(req, buf.Bytes())
  641. buf.Reset()
  642. _, err := buf.Write(changedBytes)
  643. if err != nil {
  644. if ac.debugMode {
  645. ac.PrettyError(w, req, filename, amberdata, err.Error(), "amber")
  646. } else {
  647. log.Errorf("Can not write bytes to a buffer! Out of memory?\n%s", err)
  648. }
  649. return
  650. }
  651. }
  652. // If doctype is missing, add doctype for HTML5 at the top
  653. changedBuf := bytes.NewBuffer(themes.InsertDoctype(buf.Bytes()))
  654. buf = *changedBuf
  655. // Write the rendered template to the client
  656. ac.DataToClient(w, req, filename, buf.Bytes())
  657. }
  658. // GCSSPage writes the given source bytes (in GCSS) converted to CSS, to a writer.
  659. // The filename is only used in the error message, if any.
  660. func (ac *Config) GCSSPage(w http.ResponseWriter, req *http.Request, filename string, gcssdata []byte) {
  661. var buf bytes.Buffer
  662. if _, err := gcss.Compile(&buf, bytes.NewReader(gcssdata)); err != nil {
  663. if ac.debugMode {
  664. fmt.Fprintf(w, "Could not compile GCSS:\n\n%s\n%s", err, string(gcssdata))
  665. } else {
  666. log.Errorf("Could not compile GCSS:\n%s\n%s", err, string(gcssdata))
  667. }
  668. return
  669. }
  670. // Write the resulting CSS to the client
  671. ac.DataToClient(w, req, filename, buf.Bytes())
  672. }
  673. // JSXPage writes the given source bytes (in JSX) converted to JS, to a writer.
  674. // The filename is only used in the error message, if any.
  675. func (ac *Config) JSXPage(w http.ResponseWriter, req *http.Request, filename string, jsxdata []byte) {
  676. var buf bytes.Buffer
  677. buf.Write(jsxdata)
  678. // Convert JSX to JS
  679. result := api.Transform(buf.String(), ac.jsxOptions)
  680. if len(result.Errors) > 0 {
  681. if ac.debugMode {
  682. var sb strings.Builder
  683. for _, errMsg := range result.Errors {
  684. sb.WriteString(fmt.Sprintf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column))
  685. }
  686. for _, warnMsg := range result.Warnings {
  687. sb.WriteString(fmt.Sprintf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column))
  688. }
  689. ac.PrettyError(w, req, filename, jsxdata, sb.String(), "jsx")
  690. } else {
  691. // TODO: Use a similar error page as for Lua
  692. for _, errMsg := range result.Errors {
  693. log.Errorf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column)
  694. }
  695. for _, warnMsg := range result.Warnings {
  696. log.Errorf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column)
  697. }
  698. }
  699. return
  700. }
  701. data := result.Code
  702. // Use "h" instead of "React.createElement" for hyperApp apps
  703. if ac.hyperApp {
  704. data = bytes.ReplaceAll(data, []byte("React.createElement("), []byte("h("))
  705. }
  706. ac.DataToClient(w, req, filename, data)
  707. }
  708. // HyperAppPage writes the given source bytes (in JSX for HyperApp) converted to JS, to a writer.
  709. // The filename is only used in the error message, if any.
  710. func (ac *Config) HyperAppPage(w http.ResponseWriter, req *http.Request, filename string, jsxdata []byte) {
  711. var (
  712. htmlbuf strings.Builder
  713. jsxbuf bytes.Buffer
  714. )
  715. // Wrap the rendered HyperApp JSX in some HTML
  716. htmlbuf.WriteString("<!doctype html><html><head>")
  717. // If style.gcss is present, use that style in <head>
  718. CSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultCSSFilename)
  719. GCSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultGCSSFilename)
  720. switch {
  721. case ac.fs.Exists(CSSFilename):
  722. // Link to stylesheet (without checking if the GCSS file is valid first)
  723. htmlbuf.WriteString(`<link href="`)
  724. htmlbuf.WriteString(themes.DefaultCSSFilename)
  725. htmlbuf.WriteString(`" rel="stylesheet" type="text/css">`)
  726. case ac.fs.Exists(GCSSFilename):
  727. if ac.debugMode {
  728. gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
  729. if err != nil {
  730. fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
  731. return
  732. }
  733. gcssdata := gcssblock.Bytes()
  734. // Try compiling the GCSS file first
  735. errChan := make(chan error)
  736. go ValidGCSS(gcssdata, errChan)
  737. err = <-errChan
  738. if err != nil {
  739. // Invalid GCSS, return an error page
  740. ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
  741. return
  742. }
  743. }
  744. // Link to stylesheet (without checking if the GCSS file is valid first)
  745. htmlbuf.WriteString(`<link href="`)
  746. htmlbuf.WriteString(themes.DefaultGCSSFilename)
  747. htmlbuf.WriteString(`" rel="stylesheet" type="text/css">`)
  748. default:
  749. // If not, use the default hyperapp theme by inserting the CSS style directly
  750. theme := ac.defaultTheme
  751. // Use the "neon" theme by default for HyperApp
  752. if theme == "default" {
  753. theme = "neon"
  754. }
  755. htmlbuf.Write(themes.StyleHead(theme))
  756. }
  757. // Convert JSX to JS
  758. jsxbuf.Write(jsxdata)
  759. jsxResult := api.Transform(jsxbuf.String(), ac.jsxOptions)
  760. if len(jsxResult.Errors) > 0 {
  761. if ac.debugMode {
  762. var sb strings.Builder
  763. for _, errMsg := range jsxResult.Errors {
  764. sb.WriteString(fmt.Sprintf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column))
  765. }
  766. for _, warnMsg := range jsxResult.Warnings {
  767. sb.WriteString(fmt.Sprintf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column))
  768. }
  769. ac.PrettyError(w, req, filename, jsxdata, sb.String(), "jsx")
  770. } else {
  771. // TODO: Use a similar error page as for Lua
  772. for _, errMsg := range jsxResult.Errors {
  773. log.Errorf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column)
  774. }
  775. for _, warnMsg := range jsxResult.Warnings {
  776. log.Errorf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column)
  777. }
  778. }
  779. return
  780. }
  781. // Include the hyperapp javascript from unpkg.com
  782. // htmlbuf.WriteString("</head><body><script src=\"https://unpkg.com/hyperapp\"></script><script>")
  783. // Embed the hyperapp script directly, for speed
  784. htmlbuf.WriteString("</head><body><script>")
  785. htmlbuf.Write(hyperAppJSBytes)
  786. // The HyperApp library + compiled JSX can live in the same script tag. No need for this:
  787. // htmlbuf.WriteString("</script><script>")
  788. jsxData := jsxResult.Code
  789. // Use "h" instead of "React.createElement"
  790. jsxData = bytes.ReplaceAll(jsxData, []byte("React.createElement("), []byte("h("))
  791. // If the file does not seem to contain the hyper app import: add it to the top of the script
  792. // TODO: Consider making a more robust (and slower) check that splits the data into words first
  793. if !bytes.Contains(jsxData, []byte("import { h,")) {
  794. htmlbuf.WriteString("const { h, app } = hyperapp;")
  795. }
  796. // Insert the JS data
  797. htmlbuf.Write(jsxData)
  798. // Tail of the HTML wrapper page
  799. htmlbuf.WriteString("</script></body>")
  800. // Output HTML + JS to browser
  801. ac.DataToClient(w, req, filename, []byte(htmlbuf.String()))
  802. }
  803. // SCSSPage writes the given source bytes (in SCSS) converted to CSS, to a writer.
  804. // The filename is only used in the error message, if any.
  805. func (ac *Config) SCSSPage(w http.ResponseWriter, req *http.Request, filename string, scssdata []byte) {
  806. // TODO: Gather stderr and print with log.Errorf if needed
  807. o := console.Output{}
  808. // Silence the compiler output
  809. if !ac.debugMode {
  810. o.Disable()
  811. }
  812. // Compile the given filename. Sass might want to import other file, which is probably
  813. // why the Sass compiler doesn't support just taking in a slice of bytes.
  814. cssString, err := compiler.Run(filename)
  815. if !ac.debugMode {
  816. o.Enable()
  817. }
  818. if err != nil {
  819. if ac.debugMode {
  820. fmt.Fprintf(w, "Could not compile SCSS:\n\n%s\n%s", err, string(scssdata))
  821. } else {
  822. log.Errorf("Could not compile SCSS:\n%s\n%s", err, string(scssdata))
  823. }
  824. return
  825. }
  826. // Write the resulting CSS to the client
  827. ac.DataToClient(w, req, filename, []byte(cssString))
  828. }