1. package main
    
  2. 
    
  3. import (
    
  4. 	"embed"
    
  5. 	"encoding/json"
    
  6. 	"html/template"
    
  7. 	"log"
    
  8. 	"mime"
    
  9. 	"net/http"
    
  10. 	"sort"
    
  11. 	"strings"
    
  12. 
    
  13. 	"github.com/alecthomas/kong"
    
  14. 	konghcl "github.com/alecthomas/kong-hcl"
    
  15. 	"github.com/gorilla/csrf"
    
  16. 	"github.com/gorilla/handlers"
    
  17. 	"github.com/gorilla/mux"
    
  18. 
    
  19. 	"github.com/alecthomas/chroma/v2"
    
  20. 	"github.com/alecthomas/chroma/v2/formatters/html"
    
  21. 	"github.com/alecthomas/chroma/v2/lexers"
    
  22. 	"github.com/alecthomas/chroma/v2/styles"
    
  23. )
    
  24. 
    
  25. var (
    
  26. 	version = "devel"
    
  27. 
    
  28. 	//go:embed templates/index.html.tmpl
    
  29. 	indexTemplate string
    
  30. 	//go:embed static
    
  31. 	staticFiles embed.FS
    
  32. 
    
  33. 	htmlTemplate = template.Must(template.New("html").
    
  34. 			Funcs(template.FuncMap{
    
  35. 			"JS": func(filename string) template.JS {
    
  36. 				if version == "devel" {
    
  37. 					return template.JS(`import "./static/` + filename + "\";\n")
    
  38. 				}
    
  39. 				content, err := staticFiles.ReadFile("static/" + strings.TrimSuffix(filename, ".js") + ".min.js")
    
  40. 				if err != nil {
    
  41. 					panic(err)
    
  42. 				}
    
  43. 				return template.JS(content)
    
  44. 			},
    
  45. 			"CSS": func(filename string) template.CSS {
    
  46. 				if version == "devel" {
    
  47. 					return template.CSS(`@import url("./static/` + filename + "\");")
    
  48. 				}
    
  49. 				content, err := staticFiles.ReadFile("static/" + strings.TrimSuffix(filename, ".css") + ".min.css")
    
  50. 				if err != nil {
    
  51. 					panic(err)
    
  52. 				}
    
  53. 				return template.CSS(content)
    
  54. 			},
    
  55. 		}).Parse(indexTemplate))
    
  56. )
    
  57. 
    
  58. type context struct {
    
  59. 	Background       template.CSS
    
  60. 	SelectedLanguage string
    
  61. 	Languages        []string
    
  62. 	SelectedStyle    string
    
  63. 	Styles           []string
    
  64. 	CSRFField        template.HTML
    
  65. 	Version          string
    
  66. }
    
  67. 
    
  68. func index(w http.ResponseWriter, r *http.Request) {
    
  69. 	ctx := newContext(r)
    
  70. 	err := htmlTemplate.Execute(w, &ctx)
    
  71. 	if err != nil {
    
  72. 		panic(err)
    
  73. 	}
    
  74. }
    
  75. 
    
  76. type renderRequest struct {
    
  77. 	Language string `json:"language"`
    
  78. 	Style    string `json:"style"`
    
  79. 	Text     string `json:"text"`
    
  80. 	Classes  bool   `json:"classes"`
    
  81. }
    
  82. 
    
  83. type renderResponse struct {
    
  84. 	Error      string `json:"error,omitempty"`
    
  85. 	HTML       string `json:"html,omitempty"`
    
  86. 	Language   string `json:"language,omitempty"`
    
  87. 	Background string `json:"background,omitempty"`
    
  88. }
    
  89. 
    
  90. func renderHandler(w http.ResponseWriter, r *http.Request) {
    
  91. 	req := &renderRequest{}
    
  92. 	err := json.NewDecoder(r.Body).Decode(&req)
    
  93. 	var rep *renderResponse
    
  94. 	if err != nil {
    
  95. 		rep = &renderResponse{Error: err.Error()}
    
  96. 	} else {
    
  97. 		rep, err = render(req)
    
  98. 		if err != nil {
    
  99. 			rep = &renderResponse{Error: err.Error()}
    
  100. 		}
    
  101. 	}
    
  102. 	w.Header().Set("Content-Type", "application/json")
    
  103. 	_ = json.NewEncoder(w).Encode(rep)
    
  104. }
    
  105. 
    
  106. func render(req *renderRequest) (*renderResponse, error) {
    
  107. 	language := lexers.Get(req.Language)
    
  108. 	if language == nil {
    
  109. 		language = lexers.Analyse(req.Text)
    
  110. 		if language != nil {
    
  111. 			req.Language = language.Config().Name
    
  112. 		}
    
  113. 	}
    
  114. 	if language == nil {
    
  115. 		language = lexers.Fallback
    
  116. 	}
    
  117. 
    
  118. 	tokens, err := chroma.Coalesce(language).Tokenise(nil, req.Text)
    
  119. 	if err != nil {
    
  120. 		return nil, err
    
  121. 	}
    
  122. 
    
  123. 	style := styles.Get(req.Style)
    
  124. 	if style == nil {
    
  125. 		style = styles.Fallback
    
  126. 	}
    
  127. 
    
  128. 	buf := &strings.Builder{}
    
  129. 	options := []html.Option{}
    
  130. 	if req.Classes {
    
  131. 		options = append(options, html.WithClasses(true), html.Standalone(true))
    
  132. 	}
    
  133. 	formatter := html.New(options...)
    
  134. 	err = formatter.Format(buf, style, tokens)
    
  135. 	if err != nil {
    
  136. 		return nil, err
    
  137. 	}
    
  138. 	lang := language.Config().Name
    
  139. 	if language == lexers.Fallback {
    
  140. 		lang = ""
    
  141. 	}
    
  142. 	return &renderResponse{
    
  143. 		Language:   lang,
    
  144. 		HTML:       buf.String(),
    
  145. 		Background: html.StyleEntryToCSS(style.Get(chroma.Background)),
    
  146. 	}, nil
    
  147. }
    
  148. 
    
  149. func newContext(r *http.Request) context {
    
  150. 	ctx := context{
    
  151. 		SelectedStyle: "monokailight",
    
  152. 		CSRFField:     csrf.TemplateField(r),
    
  153. 		Version:       version,
    
  154. 	}
    
  155. 	style := styles.Get(ctx.SelectedStyle)
    
  156. 	if style == nil {
    
  157. 		style = styles.Fallback
    
  158. 	}
    
  159. 	ctx.Background = template.CSS(html.StyleEntryToCSS(style.Get(chroma.Background)))
    
  160. 	if ctx.SelectedStyle == "" {
    
  161. 		ctx.SelectedStyle = "monokailight"
    
  162. 	}
    
  163. 	for _, lexer := range lexers.GlobalLexerRegistry.Lexers {
    
  164. 		ctx.Languages = append(ctx.Languages, lexer.Config().Name)
    
  165. 	}
    
  166. 	sort.Strings(ctx.Languages)
    
  167. 	for _, style := range styles.Registry {
    
  168. 		ctx.Styles = append(ctx.Styles, style.Name)
    
  169. 	}
    
  170. 	sort.Strings(ctx.Styles)
    
  171. 	return ctx
    
  172. }
    
  173. 
    
  174. func main() {
    
  175. 	var cli struct {
    
  176. 		Version kong.VersionFlag `help:"Show version."`
    
  177. 		Config  kong.ConfigFlag  `help:"Load configuration." placeholder:"FILE"`
    
  178. 		Bind    string           `help:"HTTP bind address." default:"127.0.0.1:8080"`
    
  179. 		CSRFKey string           `help:"CSRF key." default:""`
    
  180. 	}
    
  181. 	ctx := kong.Parse(&cli, kong.Configuration(konghcl.Loader), kong.Vars{"version": version})
    
  182. 
    
  183. 	log.Printf("Starting chromad %s on http://%s\n", version, cli.Bind)
    
  184. 
    
  185. 	mime.AddExtensionType(".js", "application/javascript")
    
  186. 
    
  187. 	router := mux.NewRouter()
    
  188. 	router.Handle("/", http.HandlerFunc(index)).Methods("GET")
    
  189. 	router.Handle("/api/render", http.HandlerFunc(renderHandler)).Methods("POST")
    
  190. 	router.Handle("/static/{file:.*}", http.FileServer(http.FS(staticFiles))).Methods("GET")
    
  191. 
    
  192. 	options := []csrf.Option{}
    
  193. 	if cli.CSRFKey == "" {
    
  194. 		options = append(options, csrf.Secure(false))
    
  195. 	}
    
  196. 
    
  197. 	root := handlers.CORS()(csrf.Protect([]byte(cli.CSRFKey), options...)(router))
    
  198. 
    
  199. 	err := http.ListenAndServe(cli.Bind, root)
    
  200. 	ctx.FatalIfErrorf(err)
    
  201. }