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 #")) { 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

} } // Checkboxes htmlbody = bytes.ReplaceAll(htmlbody, []byte("

  • [ ] "), []byte("
  • ")) htmlbody = bytes.ReplaceAll(htmlbody, []byte("
  • [ ] "), []byte("

  • ")) htmlbody = bytes.ReplaceAll(htmlbody, []byte("

  • [x] "), []byte("
  • ")) htmlbody = bytes.ReplaceAll(htmlbody, []byte("
  • [X] "), []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("&gt;"), []byte(">")) htmlbody = bytes.ReplaceAll(htmlbody, []byte("&lt;"), []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)) }