Ctrl K
ComunidadGuías…

engine/rendering.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package engine

import (
	"bytes"
	"fmt"
	"html/template"
	"net/http"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/eknkc/amber"
	"github.com/evanw/esbuild/pkg/api"
	"github.com/flosch/pongo2/v6"
	"github.com/gomarkdown/markdown"
	"github.com/gomarkdown/markdown/parser"
	"github.com/sirupsen/logrus"
	"github.com/wellington/sass/compiler"
	"github.com/xyproto/algernon/console"
	"github.com/xyproto/algernon/lua/convert"
	"github.com/xyproto/algernon/themes"
	"github.com/xyproto/algernon/utils"
	lua "github.com/xyproto/gopher-lua"
	"github.com/xyproto/huldra"
	"github.com/xyproto/splash"
	"github.com/yosssi/gcss"

	// For embedding the MathJax JavaScript
	_ "embed"
)

const (
	markdownCodeStyle = "base16-snazzy" // using xyproto/splash
	linkHref          = `<link href="`
	stylesheetCSS     = `" rel="stylesheet" type="text/css">`
)

var (
	//go:embed assets/mathjax/tex-svg.js
	mathJaxScript []byte

	// For detecting mathematical formulas
	formulaPattern = regexp.MustCompile(`(?s)\$\$.*?\$\$|\\\(.*?\\\)|\\\[.*?\\\]`)

	// Available Markdown extensions: https://github.com/gomarkdown/markdown/blob/master/parser/parser.go#L20
	enabledMarkdownExtensions = parser.NoIntraEmphasis | parser.Tables | parser.FencedCode | parser.Autolink | parser.Strikethrough | parser.SpaceHeadings | parser.HeadingIDs | parser.BackslashLineBreak | parser.DefinitionLists | parser.AutoHeadingIDs | parser.Mmark | parser.MathJax
)

// containsFormula checks if the given Markdown content contains at least one mathematical formula (LaTeX style)
func containsFormula(mdContent []byte) bool {
	return formulaPattern.Match(mdContent)
}

// ValidGCSS checks if the given data is valid GCSS.
// The error value is returned on the channel.
func ValidGCSS(gcssdata []byte, errorReturn chan error) {
	defer func() {
		if r := recover(); r != nil {
			errorReturn <- fmt.Errorf("panic while compiling GCSS: %v", r)
		}
	}()
	buf := bytes.NewBuffer(gcssdata)
	var w bytes.Buffer
	_, err := gcss.Compile(&w, buf)
	errorReturn <- err
}

// LoadRenderFunctions adds functions related to rendering text to the given
// Lua state struct
func (ac *Config) LoadRenderFunctions(w http.ResponseWriter, _ *http.Request, L *lua.LState) {

	// Output Markdown as HTML
	L.SetGlobal("mprint", L.NewFunction(func(L *lua.LState) int {
		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Create a Markdown parser with the desired extensions
		extensions := parser.CommonExtensions | parser.AutoHeadingIDs
		mdParser := parser.NewWithExtensions(extensions)
		mdContent := buf.Bytes()
		// Convert the buffer from Markdown to HTML
		htmlData := markdown.ToHTML(mdContent, mdParser, nil)
		// Apply syntax highlighting
		if highlightedHTML, err := splash.Splash(htmlData, markdownCodeStyle); err == nil { // success
			htmlData = highlightedHTML
		}
		// Add a script for rendering MathJax, but only if at least one mathematical formula is present
		if containsFormula(mdContent) {
			js := append([]byte(`<script id="MathJax-script">`), mathJaxScript...)
			htmlData = InsertScriptTag(htmlData, js) // also adds the closing </script> tag
		}
		// Output the generated HTML
		w.Write(htmlData)
		return 0 // number of results
	}))

	// Output Markdown as HTML, but if any scripts are added, return them as a string
	L.SetGlobal("mprint_ret", L.NewFunction(func(L *lua.LState) int {
		var results int
		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Create a Markdown parser with the desired extensions
		extensions := parser.CommonExtensions | parser.AutoHeadingIDs
		mdParser := parser.NewWithExtensions(extensions)
		mdContent := buf.Bytes()
		// Convert the buffer from Markdown to HTML
		htmlData := markdown.ToHTML(mdContent, mdParser, nil)
		// Apply syntax highlighting
		if highlightedHTML, err := splash.Splash(htmlData, markdownCodeStyle); err == nil { // success
			htmlData = highlightedHTML
		}
		// Return a script for rendering MathJax, but only if at least one mathematical formula is present
		if containsFormula(mdContent) {
			scriptTag := `<script id="MathJax-script">` + string(mathJaxScript) + "</script>"
			L.Push(lua.LString(scriptTag))
			results++
		}
		// Output the generated HTML
		w.Write(htmlData)
		return results // number of results
	}))

	// Output text as rendered amber.
	L.SetGlobal("aprint", L.NewFunction(func(L *lua.LState) int {
		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Use the buffer as a template.
		// Options are "Pretty printing, but without line numbers."
		tpl, err := amber.Compile(buf.String(), amber.Options{PrettyPrint: true, LineNumbers: false})
		if err != nil {
			if ac.debugMode {
				fmt.Fprint(w, "Could not compile Amber template:\n\t"+err.Error()+"\n\n"+buf.String())
			} else {
				logrus.Errorf("Could not compile Amber template:\n%s\n%s", err, buf.String())
			}
			return 0 // number of results
		}
		// Using "MISSING" instead of nil for a slightly better error message
		// if the values in the template should not be found.
		tpl.Execute(w, "MISSING")
		return 0 // number of results
	}))

	// Output text as rendered Pongo2
	L.SetGlobal("poprint", L.NewFunction(func(L *lua.LState) int {
		pongoMap := make(pongo2.Context)

		// Use the first argument as the template and the second argument as the data map
		templateString := L.CheckString(1)

		// If a table is given as the second argument, fill pongoMap with keys and values
		if L.GetTop() >= 2 {
			mapSS, mapSI, _, _ := convert.Table2maps(L.CheckTable(2))
			for k, v := range mapSI {
				pongoMap[k] = v
			}
			for k, v := range mapSS {
				pongoMap[k] = v
			}
		}

		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Use the buffer as a template.
		// Options are "Pretty printing, but without line numbers."
		tpl, err := pongo2.FromString(templateString)
		if err != nil {
			if ac.debugMode {
				fmt.Fprint(w, "Could not compile Pongo2 template:\n\t"+err.Error()+"\n\n"+buf.String())
			} else {
				logrus.Errorf("Could not compile Pongo2 template:\n%s\n%s", err, buf.String())
			}
			return 0 // number of results
		}
		// nil is the template context (variables etc in a map)
		if err := tpl.ExecuteWriter(pongoMap, w); err != nil {
			if ac.debugMode {
				fmt.Fprint(w, "Could not compile Pongo2:\n\t"+err.Error()+"\n\n"+buf.String())
			} else {
				logrus.Errorf("Could not compile Pongo2:\n%s\n%s", err, buf.String())
			}
		}
		return 0 // number of results
	}))

	// Output text as rendered GCSS
	L.SetGlobal("gprint", L.NewFunction(func(L *lua.LState) int {
		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Transform GCSS to CSS and output the result.
		// Ignoring the number of bytes written.
		if _, err := gcss.Compile(w, &buf); err != nil {
			if ac.debugMode {
				fmt.Fprint(w, "Could not compile GCSS:\n\t"+err.Error()+"\n\n"+buf.String())
			} else {
				logrus.Errorf("Could not compile GCSS:\n%s\n%s", err, buf.String())
			}

			// return 0 // number of results
		}
		return 0 // number of results
	}))

	// Output text as rendered JSX for React
	L.SetGlobal("jprint", L.NewFunction(func(L *lua.LState) int {
		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Transform JSX to JavaScript and output the result.
		result := api.Transform(buf.String(), ac.jsxOptions)
		if len(result.Errors) > 0 {
			if ac.debugMode {
				// TODO: Use a similar error page as for Lua
				for _, errMsg := range result.Errors {
					fmt.Fprintf(w, "error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
				}
				for _, warnMsg := range result.Warnings {
					fmt.Fprintf(w, "warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
				}
			} else {
				// TODO: Use a similar error page as for Lua
				for _, errMsg := range result.Errors {
					logrus.Errorf("error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
				}
				for _, warnMsg := range result.Warnings {
					logrus.Errorf("warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
				}
			}
			return 0 // number of results

		}
		n, err := w.Write(result.Code)
		if err != nil || n == 0 {
			if ac.debugMode {
				fmt.Fprint(w, "Result from generated JavaScript is empty\n")
			} else {
				logrus.Error("Result from generated JavaScript is empty\n")
			}
			return 0 // number of results
		}

		return 0 // number of results
	}))

	// Output text as rendered JSX for HyperApp
	L.SetGlobal("hprint", L.NewFunction(func(L *lua.LState) int {
		// Retrieve all the function arguments as a bytes.Buffer
		buf := convert.Arguments2buffer(L, true)
		// Transform JSX to JavaScript and output the result.
		result := api.Transform(buf.String(), ac.jsxOptions)
		if len(result.Errors) > 0 {
			if ac.debugMode {
				// TODO: Use a similar error page as for Lua
				for _, errMsg := range result.Errors {
					fmt.Fprintf(w, "error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
				}
				for _, warnMsg := range result.Warnings {
					fmt.Fprintf(w, "warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
				}
			} else {
				// TODO: Use a similar error page as for Lua
				for _, errMsg := range result.Errors {
					logrus.Errorf("error: %s %d:%d\n", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
				}
				for _, warnMsg := range result.Warnings {
					logrus.Errorf("warning: %s %d:%d\n", warnMsg.Text, warnMsg.Location.Line, warnMsg.Location.Column)
				}
			}
			return 0 // number of results
		}
		data := result.Code
		// Use "h" instead of "React.createElement" for hyperApp apps
		data = bytes.ReplaceAll(data, []byte("React.createElement("), []byte("h("))
		n, err := w.Write(data)
		if err != nil || n == 0 {
			if ac.debugMode {
				fmt.Fprint(w, "Result from generated JavaScript is empty\n")
			} else {
				logrus.Error("Result from generated JavaScript is empty\n")
			}
			return 0 // number of results
		}
		return 0 // number of results
	}))

	// Output a simple message HTML page.
	// The first argument is the message (ends up in the <body>).
	// The seconds argument is an optional title.
	// The third argument is an optional page style.
	L.SetGlobal("msgpage", L.NewFunction(func(L *lua.LState) int {
		title := ""
		body := ""
		if L.GetTop() < 2 {
			// Uses an empty string if no first argument is given
			body = L.ToString(1)
		} else {
			title = L.ToString(1)
			body = L.ToString(2)
		}

		// The default theme for single page messages
		theme := "redbox"
		if L.GetTop() >= 3 {
			theme = L.ToString(3)
		}

		// Write a simple HTML page to the client
		w.Write([]byte(themes.MessagePage(title, body, theme)))

		return 0 // number of results
	}))
}

// MarkdownPage write the given source bytes as markdown wrapped in HTML to a writer, with a title
func (ac *Config) MarkdownPage(w http.ResponseWriter, req *http.Request, mdContent []byte, filename string) {
	// Prepare for receiving title and codeStyle information
	searchKeywords := []string{"title", "codestyle", "theme", "replace_with_theme", "css", "favicon", "language", "lang"}

	// Also prepare for receiving meta tag information
	searchKeywords = append(searchKeywords, themes.MetaKeywords...)

	// Extract keywords from the given data, and remove the lines with keywords,
	// but only the first time that keyword occurs.
	var kwmap map[string][]byte

	mdContent, kwmap = utils.ExtractKeywords(mdContent, searchKeywords)

	// Create a Markdown parser with the desired extensions
	mdParser := parser.NewWithExtensions(enabledMarkdownExtensions)
	// Convert from Markdown to HTML
	htmlbody := markdown.ToHTML(mdContent, mdParser, nil)

	// TODO: Check if handling "# title <tags" on the first line is valid
	// Markdown or not. Submit a patch to gomarkdown/markdown if it is.

	var h1title []byte
	if bytes.HasPrefix(htmlbody, []byte("<p>#")) {
		fields := bytes.Split(htmlbody, []byte("<"))
		if len(fields) > 2 {
			h1title = bytes.TrimPrefix(fields[1][2:], []byte("#"))
			htmlbody = htmlbody[3+len(h1title):] // 3 is the length of <p>
		}
	}

	// Checkboxes
	htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li>[ ] "), []byte("<li><input type=\"checkbox\" disabled> "))
	htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li><p>[ ] "), []byte("<li><p><input type=\"checkbox\" disabled> "))
	htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li>[x] "), []byte("<li><input type=\"checkbox\" disabled checked> "))
	htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li>[X] "), []byte("<li><input type=\"checkbox\" disabled checked> "))
	htmlbody = bytes.ReplaceAll(htmlbody, []byte("<li><p>[x] "), []byte("<li><p><input type=\"checkbox\" disabled checked> "))

	// These should work by default, but does not.
	// TODO: Look into how gomarkdown/markdown handles this.
	htmlbody = bytes.ReplaceAll(htmlbody, []byte("&amp;gt;"), []byte("&gt;"))
	htmlbody = bytes.ReplaceAll(htmlbody, []byte("&amp;lt;"), []byte("&lt;"))

	// If there is no given title, use the h1title
	title := kwmap["title"]
	if len(title) == 0 {
		if len(h1title) != 0 {
			title = h1title
		} else {
			// If no title has been provided, use the filename
			title = []byte(filepath.Base(filename))
		}
	}

	// Find the theme that should be used
	theme := kwmap["theme"]
	if len(theme) == 0 {
		theme = []byte(ac.defaultTheme)
	}

	// Theme aliases. Use a map if there are more than 2 aliases in the future.
	if string(theme) == "default" {
		// Use the "material" theme by default for Markdown
		theme = []byte("material")
	}

	// Check if a specific string should be replaced with the current theme
	replaceWithTheme := kwmap["replace_with_theme"]
	if len(replaceWithTheme) != 0 {
		// Replace all instances of the value given with "replace_with_theme: ..." with the current theme name
		htmlbody = bytes.ReplaceAll(htmlbody, replaceWithTheme, []byte(theme))
	}

	// If the theme is a filename, create a custom theme where the file is imported from the CSS
	if bytes.Contains(theme, []byte(".")) {
		st := string(theme)
		themes.NewTheme(st, []byte("@import url("+st+");"), themes.DefaultCustomCodeStyle)
	}

	var head strings.Builder

	// If a favicon is specified, use that
	favicon := string(kwmap["favicon"])
	if len(favicon) > 0 {
		head.WriteString(`<link rel="shortcut icon" type="image/`)

		// Switch on the lowercase file extension of the favicon
		switch lowerExt := strings.TrimPrefix(strings.ToLower(filepath.Ext(favicon)), "."); lowerExt {
		case "ico":
			head.WriteString("x-icon")
		case "jpg":
			head.WriteString("jpeg")
		case "svg":
			head.WriteString("svg+xml")
		default:
			// rudimentary support for unrecognized image formats
			head.WriteString(lowerExt)
		}

		head.WriteString(`" href="`)
		head.WriteString(favicon)
		head.WriteString(`"/>`)
	}

	// If style.gcss is present, use that style in <head>
	CSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultCSSFilename)
	GCSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultGCSSFilename)
	switch {
	case ac.fs.Exists(CSSFilename):
		// Link to stylesheet (without checking if the CSS file is valid first)
		head.WriteString(linkHref)
		head.WriteString(themes.DefaultCSSFilename)
		head.WriteString(stylesheetCSS)
	case ac.fs.Exists(GCSSFilename):
		if ac.debugMode {
			gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
			if err != nil {
				fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
				return
			}
			gcssdata := gcssblock.Bytes()

			// Try compiling the GCSS file first
			errChan := make(chan error)
			go ValidGCSS(gcssdata, errChan)
			err = <-errChan
			if err != nil {
				// Invalid GCSS, return an error page
				ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
				return
			}
		}
		// Link to stylesheet (without checking if the GCSS file is valid first)
		head.WriteString(linkHref)
		head.WriteString(themes.DefaultGCSSFilename)
		head.WriteString(stylesheetCSS)
	default:
		// If not, use the theme by inserting the CSS style directly
		head.Write(themes.StyleHead(string(theme)))
	}

	// Additional CSS file
	additionalCSSfile := string(kwmap["css"])
	if additionalCSSfile != "" {
		// If serving a single Markdown file, include the CSS file inline in a style tag
		if ac.markdownMode && ac.fs.Exists(additionalCSSfile) {
			// Cache the CSS only if Markdown should be cached
			cssblock, err := ac.cache.Read(additionalCSSfile, ac.shouldCache(".md"))
			if err != nil {
				fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
				return
			}
			cssdata := cssblock.Bytes()
			head.WriteString("<style>" + string(cssdata) + "</style>")
		} else {
			head.WriteString(linkHref)
			head.WriteString(additionalCSSfile)
			head.WriteString(stylesheetCSS)
		}
	}

	codeStyle := string(kwmap["codestyle"])

	// Add meta tags, if metadata information has been declared
	for _, keyword := range themes.MetaKeywords {
		if len(kwmap[keyword]) != 0 {
			// Add the meta tag
			head.WriteString(`<meta name="`)
			head.WriteString(keyword)
			head.WriteString(`" content="`)
			head.Write(kwmap[keyword])
			head.WriteString(`" />`)
		}
	}

	// Check if lang or language has been specified for the <html> tag
	var language []byte // the lang string, like "en", for example
	if lang, ok := kwmap["lang"]; ok {
		language = lang
	} else if lang, ok := kwmap["language"]; ok {
		language = lang
	}

	// Embed the style and rendered markdown into a simple HTML 5 page
	htmldata := themes.SimpleHTMLPage(title, h1title, []byte(head.String()), htmlbody, language)

	// TODO: Fix the issue in the splash package so that both MathJax and applying syntax highlighting to code can be used at the same time

	// Add a script for rendering MathJax, but only if at least one mathematical formula is present
	if containsFormula(mdContent) {
		js := append([]byte(`<script id="MathJax-script">`), []byte(mathJaxScript)...)
		htmldata = InsertScriptTag(htmldata, js) // also adds the closing </script> tag
	} else if bytes.Contains(htmlbody, []byte("<pre><code ")) { // Add syntax highlighting to the header, but only if '<pre><code ' is present
		// If codeStyle is not "none", highlight the current htmldata
		if codeStyle == "" {
			// Use the highlight style from the current theme
			highlighted, err := splash.UnescapeSplash(htmldata, themes.ThemeToCodeStyle(string(theme)))
			if err != nil {
				logrus.Error(err)
			} else {
				// Only use the new and highlighted HTML if there were no errors
				//htmldata = highlighted
				htmldata = highlighted
			}
		} else if codeStyle != "none" {
			// Use the highlight style from codeStyle
			highlighted, err := splash.UnescapeSplash(htmldata, codeStyle)
			if err != nil {
				logrus.Error(err)
			} else {
				// Only use the new HTML if there were no errors
				htmldata = highlighted
			}
		}
	}

	// If the auto-refresh feature has been enabled
	if ac.autoRefresh {
		// Insert JavaScript for refreshing the page into the generated HTML
		htmldata = ac.InsertAutoRefresh(req, htmldata)
	}

	// Write the rendered Markdown page to the client
	ac.DataToClient(w, req, filename, htmldata)
}

// PongoPage write the given source bytes (ina Pongo2) converted to HTML, to a writer.
// The filename is only used in error messages, if any.
func (ac *Config) PongoPage(w http.ResponseWriter, req *http.Request, filename string, pongodata []byte, funcs template.FuncMap) {
	var (
		buf                   bytes.Buffer
		linkInGCSS, linkInCSS bool
		dirName               = filepath.Dir(filename)
		GCSSFilename          = filepath.Join(dirName, themes.DefaultGCSSFilename)
		CSSFilename           = filepath.Join(dirName, themes.DefaultCSSFilename)
	)

	// If style.gcss is present, and a header is present, and it has not already been linked in, link it in
	if ac.fs.Exists(CSSFilename) {
		linkInCSS = true
	} else if ac.fs.Exists(GCSSFilename) {
		if ac.debugMode {
			gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
			if err != nil {
				fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
				return
			}
			gcssdata := gcssblock.Bytes()

			// Try compiling the GCSS file before the Pongo2 file
			errChan := make(chan error)
			go ValidGCSS(gcssdata, errChan)
			err = <-errChan
			if err != nil {
				// Invalid GCSS, return an error page
				ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
				return
			}
		}
		linkInGCSS = true
	}

	// Per-request Pongo2 template set: avoids global mutation (formerly guarded by pongomutex)
	loader, err := pongo2.NewLocalFileSystemLoader(dirName)
	if err != nil {
		if ac.debugMode {
			ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
		} else {
			logrus.Errorf("Could not create Pongo2 loader for %s:\n%s", dirName, err)
		}
		return
	}
	pongoSet := pongo2.NewSet(filename, loader)

	// Prepare a Pongo2 template
	tpl, err := pongoSet.FromBytes(pongodata)
	if err != nil {
		if ac.debugMode {
			ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
		} else {
			logrus.Errorf("Could not compile Pongo2 template:\n%s\n%s", err, string(pongodata))
		}
		return
	}

	okfuncs := make(pongo2.Context)

	// Go through the global Lua scope
	for k, v := range funcs {

		// Check if the name in question is a function
		if f, ok := v.(func(...string) (any, error)); ok {

			// For the closure to correctly wrap the key value
			k := k

			// Wrap the Lua functions as Pongo2 functions
			wrapfunc := func(vals ...*pongo2.Value) *pongo2.Value {
				// Convert the Pongo2 arguments to string arguments
				strs := make([]string, len(vals))
				for i, sv := range vals {
					strs[i] = sv.String()
				}

				// Call the Lua function
				retval, err := f(strs...)
				// Return the error if things go wrong
				if err != nil {
					return pongo2.AsValue(err)
				}
				// Return the returned value if things went well
				return pongo2.AsValue(retval)
			}
			// Save the wrapped function for the pongo2 template execution
			okfuncs[k] = wrapfunc

		} else if s, ok := v.(string); ok {
			// String variables
			okfuncs[k] = s
		} else {
			// Exposing variable as it is.
			// TODO: Add more tests for this codepath
			okfuncs[k] = v
		}
	}

	// Make the Lua functions available to this request's Pongo2 set
	pongoSet.Globals.Update(okfuncs)

	defer func() {
		if r := recover(); r != nil {
			errmsg := fmt.Sprintf("Pongo2 error: %s", r)
			if ac.debugMode {
				ac.PrettyError(w, req, filename, pongodata, errmsg, "pongo2")
			} else {
				logrus.Errorf("Could not execute Pongo2 template:\n%s", errmsg)
			}
		}
	}()

	// Render the Pongo2 template to the buffer
	err = tpl.ExecuteWriter(pongoSet.Globals, &buf)
	if err != nil {
		// if err := tpl.ExecuteWriterUnbuffered(pongo2.Globals, &buf); err != nil {
		if ac.debugMode {
			ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
		} else {
			logrus.Errorf("Could not execute Pongo2 template:\n%s", err)
		}
		return
	}

	// Check if we are dealing with HTML
	if huldra.IsHTML(buf.Bytes()) {

		if linkInCSS || linkInGCSS {
			// Link in stylesheet
			htmldata := buf.Bytes()
			if linkInCSS {
				htmldata = themes.StyleHTML(htmldata, themes.DefaultCSSFilename)
			} else if linkInGCSS {
				htmldata = themes.StyleHTML(htmldata, themes.DefaultGCSSFilename)
			}
			buf.Reset()
			_, err := buf.Write(htmldata)
			if err != nil {
				if ac.debugMode {
					ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
				} else {
					logrus.Errorf("Can not write bytes to a buffer! Out of memory?\n%s", err)
				}
				return
			}
		}

		// If the auto-refresh feature has been enabled
		if ac.autoRefresh {
			// Insert JavaScript for refreshing the page into the generated HTML
			changedBytes := ac.InsertAutoRefresh(req, buf.Bytes())

			buf.Reset()
			_, err := buf.Write(changedBytes)
			if err != nil {
				if ac.debugMode {
					ac.PrettyError(w, req, filename, pongodata, err.Error(), "pongo2")
				} else {
					logrus.Errorf("Can not write bytes to a buffer! Out of memory?\n%s", err)
				}
				return
			}
		}

		// If doctype is missing, add doctype for HTML5 at the top
		changedBytes := themes.InsertDoctype(buf.Bytes())
		buf.Reset()
		buf.Write(changedBytes)
	}

	// Write the rendered template to the client
	ac.DataToClient(w, req, filename, buf.Bytes())
}

// AmberPage the given source bytes (in Amber) converted to HTML, to a writer.
// The filename is only used in error messages, if any.
func (ac *Config) AmberPage(w http.ResponseWriter, req *http.Request, filename string, amberdata []byte, funcs template.FuncMap) {
	var (
		buf bytes.Buffer
		// If style.gcss is present, and a header is present, and it has not already been linked in, link it in
		dirName      = filepath.Dir(filename)
		GCSSFilename = filepath.Join(dirName, themes.DefaultGCSSFilename)
		CSSFilename  = filepath.Join(dirName, themes.DefaultCSSFilename)
	)

	if ac.fs.Exists(CSSFilename) {
		// Link to stylesheet (without checking if the GCSS file is valid first)
		amberdata = themes.StyleAmber(amberdata, themes.DefaultCSSFilename)
	} else if ac.fs.Exists(GCSSFilename) {
		if ac.debugMode {
			gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
			if err != nil {
				fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
				return
			}
			gcssdata := gcssblock.Bytes()

			// Try compiling the GCSS file before the Amber file
			errChan := make(chan error)
			go ValidGCSS(gcssdata, errChan)
			err = <-errChan
			if err != nil {
				// Invalid GCSS, return an error page
				ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
				return
			}
		}
		// Link to stylesheet (without checking if the GCSS file is valid first)
		amberdata = themes.StyleAmber(amberdata, themes.DefaultGCSSFilename)
	}

	// Compile the given amber template
	tpl, err := amber.CompileData(amberdata, filename, amber.Options{PrettyPrint: true, LineNumbers: false})
	if err != nil {
		if ac.debugMode {
			ac.PrettyError(w, req, filename, amberdata, err.Error(), "amber")
		} else {
			logrus.Errorf("Could not compile Amber template:\n%s\n%s", err, string(amberdata))
		}
		return
	}

	// Render the Amber template to the buffer
	if err := tpl.Execute(&buf, funcs); err != nil {

		// If it was one particular error, where the template can not find the
		// function or variable name that is used, give the user a friendlier
		// message.
		if strings.TrimSpace(err.Error()) == "reflect: call of reflect.Value.Type on zero Value" {
			errortext := "Could not execute Amber template!<br>One of the functions called by the template is not available."
			if ac.debugMode {
				ac.PrettyError(w, req, filename, amberdata, errortext, "amber")
			} else {
				errortext = strings.Replace(errortext, "<br>", "\n", 1)
				logrus.Errorf("Could not execute Amber template:\n%s", errortext)
			}
		} else {
			if ac.debugMode {
				ac.PrettyError(w, req, filename, amberdata, err.Error(), "amber")
			} else {
				logrus.Errorf("Could not execute Amber template:\n%s", err)
			}
		}
		return
	}

	// If the auto-refresh feature has been enabled
	if ac.autoRefresh {
		// Insert JavaScript for refreshing the page into the generated HTML
		changedBytes := ac.InsertAutoRefresh(req, buf.Bytes())

		buf.Reset()
		_, err := buf.Write(changedBytes)
		if err != nil {
			if ac.debugMode {
				ac.PrettyError(w, req, filename, amberdata, err.Error(), "amber")
			} else {
				logrus.Errorf("Can not write bytes to a buffer! Out of memory?\n%s", err)
			}
			return
		}
	}

	// If doctype is missing, add doctype for HTML5 at the top
	changedBuf := bytes.NewBuffer(themes.InsertDoctype(buf.Bytes()))
	buf = *changedBuf

	// Write the rendered template to the client
	ac.DataToClient(w, req, filename, buf.Bytes())
}

// GCSSPage writes the given source bytes (in GCSS) converted to CSS, to a writer.
// The filename is only used in the error message, if any.
func (ac *Config) GCSSPage(w http.ResponseWriter, req *http.Request, filename string, gcssdata []byte) {
	var buf bytes.Buffer
	if _, err := gcss.Compile(&buf, bytes.NewReader(gcssdata)); err != nil {
		if ac.debugMode {
			fmt.Fprintf(w, "Could not compile GCSS:\n\n%s\n%s", err, string(gcssdata))
		} else {
			logrus.Errorf("Could not compile GCSS:\n%s\n%s", err, string(gcssdata))
		}
		return
	}
	// Write the resulting CSS to the client
	ac.DataToClient(w, req, filename, buf.Bytes())
}

// JSXPage writes the given source bytes (in JSX) converted to JS, to a writer.
// The filename is only used in the error message, if any.
func (ac *Config) JSXPage(w http.ResponseWriter, req *http.Request, filename string, jsxdata []byte) {
	// If the source contains import/require statements, use the full bundler
	// with on-the-fly caching rather than a single-file transform.
	if needsBundling(jsxdata) {
		data, err := ac.bundleFile(filename, jsxdata, false)
		if err != nil {
			if ac.debugMode {
				ac.PrettyError(w, req, filename, jsxdata, err.Error(), "jsx")
			} else {
				logrus.Error(err)
			}
			return
		}
		ac.DataToClient(w, req, filename, data)
		return
	}

	// When auto-refresh is active, inject react-refresh registrations
	src := string(jsxdata)
	if ac.autoRefresh {
		src = injectRefreshRegistrations(src, filepath.Base(filename))
	}

	// Convert JSX to JS
	result := api.Transform(src, ac.jsxOptions)
	if len(result.Errors) > 0 {
		if ac.debugMode {
			var sb strings.Builder
			for _, errMsg := range result.Errors {
				sb.WriteString(fmt.Sprintf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column))
			}
			for _, warnMsg := range result.Warnings {
				sb.WriteString(fmt.Sprintf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column))
			}
			ac.PrettyError(w, req, filename, jsxdata, sb.String(), "jsx")
		} else {
			// TODO: Use a similar error page as for Lua
			for _, errMsg := range result.Errors {
				logrus.Errorf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column)
			}
			for _, warnMsg := range result.Warnings {
				logrus.Errorf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column)
			}
		}
		return
	}
	data := result.Code

	// Use "h" instead of "React.createElement" for hyperApp apps
	if ac.hyperApp {
		data = bytes.ReplaceAll(data, []byte("React.createElement("), []byte("h("))
	}

	ac.DataToClient(w, req, filename, data)
}

// TSXPage writes the given source bytes (in TypeScript/TSX) converted to JS, to a writer.
// The filename is only used in the error message, if any.
func (ac *Config) TSXPage(w http.ResponseWriter, req *http.Request, filename string, tsxdata []byte) {
	// If the source contains import/require statements, use the full bundler
	// with on-the-fly caching rather than a single-file transform.
	if needsBundling(tsxdata) {
		data, err := ac.bundleFile(filename, tsxdata, false)
		if err != nil {
			if ac.debugMode {
				ac.PrettyError(w, req, filename, tsxdata, err.Error(), "tsx")
			} else {
				logrus.Error(err)
			}
			return
		}
		ac.DataToClient(w, req, filename, data)
		return
	}

	// When auto-refresh is active, inject react-refresh registrations
	src := string(tsxdata)
	if ac.autoRefresh {
		src = injectRefreshRegistrations(src, filepath.Base(filename))
	}

	// Convert TS/TSX to JS using the appropriate loader
	opts := ac.jsxOptions
	opts.Loader = loaderForFile(filename)
	result := api.Transform(src, opts)
	if len(result.Errors) > 0 {
		if ac.debugMode {
			var sb strings.Builder
			for _, errMsg := range result.Errors {
				sb.WriteString(fmt.Sprintf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column))
			}
			for _, warnMsg := range result.Warnings {
				sb.WriteString(fmt.Sprintf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column))
			}
			ac.PrettyError(w, req, filename, tsxdata, sb.String(), "tsx")
		} else {
			for _, errMsg := range result.Errors {
				logrus.Errorf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column)
			}
			for _, warnMsg := range result.Warnings {
				logrus.Errorf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column)
			}
		}
		return
	}

	ac.DataToClient(w, req, filename, result.Code)
}

// defaultReactCSS is a minimal CSS used when no style.css or style.gcss is present
const defaultReactCSS = `*{margin:0;padding:0;box-sizing:border-box}` +
	`body{font-family:system-ui,-apple-system,sans-serif;line-height:1.6;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#f5f5f5}` +
	`.card{background:#fff;padding:2em;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);width:320px;text-align:center}` +
	`h1{margin-bottom:.5em}` +
	`input{display:block;width:100%;padding:.5em;margin-bottom:.75em;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}` +
	`button{width:100%;padding:.6em;background:#3b82f6;color:#fff;border:none;border-radius:4px;cursor:pointer}` +
	`button:hover{background:#2563eb}` +
	`.msg{color:#c00;font-size:.9em}` +
	`a{color:#3b82f6}`

// ReactPage wraps a JSX/TSX source file in a full HTML page with React loaded,
// a <div id="root"> mount point, and an optional stylesheet.
func (ac *Config) ReactPage(w http.ResponseWriter, req *http.Request, filename string, jsxdata []byte, ver int) {
	var htmlbuf strings.Builder

	htmlbuf.WriteString("<!doctype html><html><head><meta charset=\"utf-8\">")

	// If style.css or style.gcss is present, use that in <head>
	dir := filepath.Dir(filename)
	CSSFilename := filepath.Join(dir, themes.DefaultCSSFilename)
	GCSSFilename := filepath.Join(dir, themes.DefaultGCSSFilename)
	switch {
	case ac.fs.Exists(CSSFilename):
		htmlbuf.WriteString(linkHref)
		htmlbuf.WriteString(themes.DefaultCSSFilename)
		htmlbuf.WriteString(stylesheetCSS)
	case ac.fs.Exists(GCSSFilename):
		htmlbuf.WriteString(linkHref)
		htmlbuf.WriteString(themes.DefaultGCSSFilename)
		htmlbuf.WriteString(stylesheetCSS)
	default:
		// Inject a minimal default style
		htmlbuf.WriteString("<style>")
		htmlbuf.WriteString(defaultReactCSS)
		htmlbuf.WriteString("</style>")
	}

	mountID := parseMountElementID(jsxdata)
	htmlbuf.WriteString("</head><body><div id=\"" + mountID + "\"></div>")

	// Load the React runtime for the requested version
	paths, ok := reactPaths[ver]
	if !ok {
		paths = reactPaths[defaultReactVersion]
	}
	if ac.debugMode {
		htmlbuf.WriteString(`<script src="` + paths.reactDev + `"></script>`)
		htmlbuf.WriteString(`<script src="` + paths.reactDOMDev + `"></script>`)
	} else {
		htmlbuf.WriteString(`<script src="` + paths.reactProd + `"></script>`)
		htmlbuf.WriteString(`<script src="` + paths.reactDOMProd + `"></script>`)
	}

	// Inject the postForm and base64URL helper functions
	htmlbuf.WriteString(`<script>` +
		`function postForm(u,d){var b=[];for(var k in d)b.push(encodeURIComponent(k)+"="+encodeURIComponent(d[k]));return fetch(u,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:b.join("&")}).then(function(r){return r.json()})}` +
		`function bufferToBase64URL(buf){var b=new Uint8Array(buf),s="";for(var i=0;i<b.length;i++)s+=String.fromCharCode(b[i]);return btoa(s).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}` +
		`function base64URLToBuffer(s){s=s.replace(/-/g,"+").replace(/_/g,"/");while(s.length%4)s+="=";var d=atob(s),b=new Uint8Array(d.length);for(var i=0;i<d.length;i++)b[i]=d.charCodeAt(i);return b.buffer}` +
		`</script>`)

	// Bundle the JSX/TSX source with esbuild, shimming react imports to globals
	bundled, err := ac.bundleFile(filename, jsxdata, true)
	if err != nil {
		if ac.debugMode {
			ac.PrettyError(w, req, filename, jsxdata, err.Error(), "jsx")
		} else {
			logrus.Error(err)
		}
		return
	}

	htmlbuf.WriteString("<script>")
	htmlbuf.Write(bundled)
	htmlbuf.WriteString("</script></body></html>")

	htmldata := []byte(htmlbuf.String())

	// If the auto-refresh feature has been enabled
	if ac.autoRefresh {
		htmldata = ac.InsertAutoRefresh(req, htmldata)
	}

	ac.DataToClient(w, req, filename, htmldata)
}

// HyperAppPage writes the given source bytes (in JSX for HyperApp) converted to JS, to a writer.
// The filename is only used in the error message, if any.
func (ac *Config) HyperAppPage(w http.ResponseWriter, req *http.Request, filename string, jsxdata []byte) {
	var (
		htmlbuf strings.Builder
		jsxbuf  bytes.Buffer
	)

	// Wrap the rendered HyperApp JSX in some HTML
	htmlbuf.WriteString("<!doctype html><html><head>")

	// If style.gcss is present, use that style in <head>
	CSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultCSSFilename)
	GCSSFilename := filepath.Join(filepath.Dir(filename), themes.DefaultGCSSFilename)
	switch {
	case ac.fs.Exists(CSSFilename):
		// Link to stylesheet (without checking if the GCSS file is valid first)
		htmlbuf.WriteString(linkHref)
		htmlbuf.WriteString(themes.DefaultCSSFilename)
		htmlbuf.WriteString(stylesheetCSS)
	case ac.fs.Exists(GCSSFilename):
		if ac.debugMode {
			gcssblock, err := ac.cache.Read(GCSSFilename, ac.shouldCache(".gcss"))
			if err != nil {
				fmt.Fprintf(w, "Unable to read %s: %s", filename, err)
				return
			}
			gcssdata := gcssblock.Bytes()

			// Try compiling the GCSS file first
			errChan := make(chan error)
			go ValidGCSS(gcssdata, errChan)
			err = <-errChan
			if err != nil {
				// Invalid GCSS, return an error page
				ac.PrettyError(w, req, GCSSFilename, gcssdata, err.Error(), "gcss")
				return
			}
		}
		// Link to stylesheet (without checking if the GCSS file is valid first)
		htmlbuf.WriteString(linkHref)
		htmlbuf.WriteString(themes.DefaultGCSSFilename)
		htmlbuf.WriteString(stylesheetCSS)
	default:
		// If not, use the default hyperapp theme by inserting the CSS style directly
		theme := ac.defaultTheme

		// Use the "neon" theme by default for HyperApp
		if theme == "default" {
			theme = "neon"
		}

		htmlbuf.Write(themes.StyleHead(theme))
	}

	// Convert JSX to JS
	jsxbuf.Write(jsxdata)
	jsxResult := api.Transform(jsxbuf.String(), ac.jsxOptions)

	if len(jsxResult.Errors) > 0 {
		if ac.debugMode {
			var sb strings.Builder
			for _, errMsg := range jsxResult.Errors {
				sb.WriteString(fmt.Sprintf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column))
			}
			for _, warnMsg := range jsxResult.Warnings {
				sb.WriteString(fmt.Sprintf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column))
			}
			ac.PrettyError(w, req, filename, jsxdata, sb.String(), "jsx")
		} else {
			// TODO: Use a similar error page as for Lua
			for _, errMsg := range jsxResult.Errors {
				logrus.Errorf("error: %s %s:%d:%d\n", errMsg.Text, filename, errMsg.Location.Line, errMsg.Location.Column)
			}
			for _, warnMsg := range jsxResult.Warnings {
				logrus.Errorf("warning: %s %s:%d:%d\n", warnMsg.Text, filename, warnMsg.Location.Line, warnMsg.Location.Column)
			}
		}
		return
	}

	// Include the hyperapp javascript from unpkg.com
	// htmlbuf.WriteString("</head><body><script src=\"https://unpkg.com/hyperapp\"></script><script>")

	// Embed the hyperapp script directly, for speed
	htmlbuf.WriteString("</head><body><script>")
	htmlbuf.Write(hyperAppJSBytes)

	// The HyperApp library + compiled JSX can live in the same script tag. No need for this:
	// htmlbuf.WriteString("</script><script>")

	jsxData := jsxResult.Code

	// Use "h" instead of "React.createElement"
	jsxData = bytes.ReplaceAll(jsxData, []byte("React.createElement("), []byte("h("))

	// If the file does not seem to contain the hyper app import: add it to the top of the script
	// TODO: Consider making a more robust (and slower) check that splits the data into words first
	if !bytes.Contains(jsxData, []byte("import { h,")) {
		htmlbuf.WriteString("const { h, app } = hyperapp;")
	}

	// Insert the JS data
	htmlbuf.Write(jsxData)

	// Tail of the HTML wrapper page
	htmlbuf.WriteString("</script></body>")

	// Output HTML + JS to browser
	ac.DataToClient(w, req, filename, []byte(htmlbuf.String()))
}

// SCSSPage writes the given source bytes (in SCSS) converted to CSS, to a writer.
// The filename is only used in the error message, if any.
func (ac *Config) SCSSPage(w http.ResponseWriter, req *http.Request, filename string, scssdata []byte) {
	// TODO: Gather stderr and print with logrus.Errorf if needed
	o := console.Output{}
	// Silence the compiler output
	if !ac.debugMode {
		o.Disable()
	}
	// Compile the given filename. Sass might want to import other file, which is probably
	// why the Sass compiler doesn't support just taking in a slice of bytes.
	cssString, err := compiler.Run(filename)
	if !ac.debugMode {
		o.Enable()
	}
	if err != nil {
		if ac.debugMode {
			fmt.Fprintf(w, "Could not compile SCSS:\n\n%s\n%s", err, string(scssdata))
		} else {
			logrus.Errorf("Could not compile SCSS:\n%s\n%s", err, string(scssdata))
		}
		return
	}
	// Write the resulting CSS to the client
	ac.DataToClient(w, req, filename, []byte(cssString))
}