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 = `` ) 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(` 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 := `" 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
). // 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} } // Checkboxes htmlbody = bytes.ReplaceAll(htmlbody, []byte("
[ ] "), []byte("
")) htmlbody = bytes.ReplaceAll(htmlbody, []byte("
[x] "), []byte("
")) // These should work by default, but does not. // TODO: Look into how gomarkdown/markdown handles this. htmlbody = bytes.ReplaceAll(htmlbody, []byte(">"), []byte(">")) htmlbody = bytes.ReplaceAll(htmlbody, []byte("<"), []byte("<")) // 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(``) } // If style.gcss is present, use that style in
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("") } 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(``) } } // Check if lang or language has been specified for the 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(` tag } else if bytes.Contains(htmlbody, []byte("", "\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 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("")
// If style.css or style.gcss is present, use that in
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("")
}
mountID := parseMountElementID(jsxdata)
htmlbuf.WriteString("")
// Load the React runtime for the requested version
paths, ok := reactPaths[ver]
if !ok {
paths = reactPaths[defaultReactVersion]
}
if ac.debugMode {
htmlbuf.WriteString(``)
htmlbuf.WriteString(``)
} else {
htmlbuf.WriteString(``)
htmlbuf.WriteString(``)
}
// Inject the postForm and base64URL helper functions
htmlbuf.WriteString(``)
// 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("")
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("")
// If style.gcss is present, use that style in
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("")
// 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))
}