From 2430f46a9948b38b06880606a95dec357d01f619 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Sun, 5 May 2024 18:11:56 +0200 Subject: feat: render markdown in option descriptions --- internal/options/option.go | 57 +++++++++++++++++++++++ internal/options/process.go | 109 ++++++++++++++++++++++++++++++++++++++++++++ internal/server/option.go | 15 ------ internal/server/server.go | 68 +++++++++++++++------------ 4 files changed, 204 insertions(+), 45 deletions(-) create mode 100644 internal/options/option.go create mode 100644 internal/options/process.go delete mode 100644 internal/server/option.go (limited to 'internal') diff --git a/internal/options/option.go b/internal/options/option.go new file mode 100644 index 0000000..5255c65 --- /dev/null +++ b/internal/options/option.go @@ -0,0 +1,57 @@ +package options + +import ( + "encoding/json" + "strings" + + "github.com/pkg/errors" + "github.com/yuin/goldmark" +) + +type NixValue struct { + Type string `json:"_type" mapstructure:"_type"` + Text string `json:"text"` +} + +type HTML struct { + HTML string +} + +func (html *HTML) UnmarshalText(text []byte) error { + var out strings.Builder + err := goldmark.Convert(text, &out) + if err != nil { + return errors.WithMessage(err, "failed to convert markdown to HTML") + } + + html.HTML = out.String() + + return nil +} + +func (html *HTML) UnmarshalJSON(raw []byte) error { + var v struct { + HTML string + } + err := json.Unmarshal(raw, &v) + if err != nil { + return errors.WithMessage(err, "error unmarshaling json") + } + html.HTML = v.HTML + + return nil +} + +type NixOption struct { + Option string + Declarations []string + Default NixValue + Description HTML + Example NixValue + ReadOnly bool + Type string + Loc []string + RelatedPackages HTML +} + +type NixOptions []NixOption diff --git a/internal/options/process.go b/internal/options/process.go new file mode 100644 index 0000000..fde73e4 --- /dev/null +++ b/internal/options/process.go @@ -0,0 +1,109 @@ +package options + +import ( + "encoding/json" + "io" + "os" + + "github.com/bcicen/jstream" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func ValueTypeToString(valueType jstream.ValueType) string { + switch valueType { + case jstream.Unknown: + return "unknown" + case jstream.Null: + return "null" + case jstream.String: + return "string" + case jstream.Number: + return "number" + case jstream.Boolean: + return "boolean" + case jstream.Array: + return "array" + case jstream.Object: + return "object" + } + + return "very strange" +} + +func Process(inpath string, outpath string) error { + infile, err := os.Open(inpath) + if err != nil { + return errors.WithMessagef(err, "failed to open input file %s", inpath) + } + defer infile.Close() + outfile, err := os.Create(outpath) + if err != nil { + return errors.WithMessagef(err, "failed to open output file %s", outpath) + } + if outpath != "/dev/stdout" { + defer outfile.Close() + } + + dec := jstream.NewDecoder(infile, 1).EmitKV() + var opt NixOption + ms, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + ErrorUnused: true, + Result: &opt, + Squash: true, + DecodeHook: mapstructure.TextUnmarshallerHookFunc(), + }) + if err != nil { + return errors.WithMessage(err, "could not create mapstructure decoder") + } + + _, err = outfile.WriteString("[\n") + if err != nil { + return errors.WithMessage(err, "could not write to output") + } + for mv := range dec.Stream() { + if err := dec.Err(); err != nil { + return errors.WithMessage(err, "could not decode JSON") + } + if mv.ValueType != jstream.Object { + return errors.Errorf("unexpected object type %s", ValueTypeToString(mv.ValueType)) + } + kv := mv.Value.(jstream.KV) + if kv.Key == "_module.args" { + continue + } + x := kv.Value.(map[string]interface{}) + x["option"] = kv.Key + + err = ms.Decode(x) + if err != nil { + return errors.WithMessagef(err, "failed to decode option %#v", x) + } + + b, err := json.MarshalIndent(opt, "", " ") + if err != nil { + return errors.WithMessagef(err, "failed to encode option %#v", opt) + } + + _, err = outfile.Write(b) + if err != nil { + return errors.WithMessage(err, "failed to write to output") + } + _, err = outfile.WriteString(",\n") + if err != nil { + return errors.WithMessage(err, "failed to write to output") + } + } + + _, err = outfile.Seek(-2, io.SeekCurrent) + if err != nil { + return errors.WithMessage(err, "could not write to output") + } + + _, err = outfile.WriteString("]\n") + if err != nil { + return errors.WithMessage(err, "could not write to output") + } + + return nil +} diff --git a/internal/server/option.go b/internal/server/option.go deleted file mode 100644 index 2712d8a..0000000 --- a/internal/server/option.go +++ /dev/null @@ -1,15 +0,0 @@ -package server - -type NixValue struct { - Type string `json:"_type"` - Text string `json:"text"` -} - -type Option struct { - Declarations []string - Default NixValue - Description string - Example NixValue - ReadOnly bool - Type string `json:"type"` -} diff --git a/internal/server/server.go b/internal/server/server.go index 02b39a0..ad75ac1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -18,6 +18,7 @@ import ( "time" cfg "searchix/internal/config" + "searchix/internal/options" "github.com/getsentry/sentry-go" sentryhttp "github.com/getsentry/sentry-go/http" @@ -63,54 +64,62 @@ type TemplateData struct { type OptionResultData struct { TemplateData Query string - Results map[string]Option + Results options.NixOptions } -type TemplateCollection struct { - Pages map[string]*template.Template - Blocks map[string]*template.Template -} +type TemplateCollection map[string]*template.Template func applyDevModeOverrides(config *cfg.Config) { config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'") config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'") } -const dummyTemplate = `{{ block "results" . }}{{ end }}` +var templateFuncs = template.FuncMap{ + "HTML": func(input string) template.HTML { + return template.HTML(input) // #nosec G203 + }, +} -func loadTemplates() (*TemplateCollection, error) { - templateDir := path.Join("frontend", "templates") - templates := &TemplateCollection{ - Pages: make(map[string]*template.Template), - Blocks: make(map[string]*template.Template), +func loadTemplate(filename string) (*template.Template, error) { + text, err := os.ReadFile(filename) + if err != nil { + return nil, errors.WithMessage(err, "could not read template") } - indexText, err := os.ReadFile(path.Join(templateDir, "index.gotmpl")) + name, _ := strings.CutSuffix(path.Base(filename), ".gotmpl") + tpl := template.New(name) + tpl.Funcs(templateFuncs) + _, err = tpl.Parse(string(text)) if err != nil { - return nil, errors.WithMessage(err, "could not read index template") + return nil, errors.WithMessage(err, "could not parse template") } - index, err := template.New("index").Parse(string(indexText)) + + return tpl, nil +} + +func loadTemplates() (TemplateCollection, error) { + templateDir := path.Join("frontend", "templates") + templates := make(TemplateCollection, 0) + + index, err := loadTemplate(path.Join(templateDir, "index.gotmpl")) if err != nil { - return nil, errors.WithMessage(err, "could not parse index template") + return nil, err } - templates.Pages["index"] = index - templates.Blocks = make(map[string]*template.Template) + templates["index"] = index templatePaths, err := filepath.Glob(path.Join(templateDir, "blocks", "*.gotmpl")) if err != nil { return nil, errors.WithMessage(err, "could not glob block templates") } for _, fullname := range templatePaths { - name, _ := strings.CutSuffix(path.Base(fullname), ".gotmpl") - content, err := os.ReadFile(fullname) + tpl, err := loadTemplate(fullname) if err != nil { - return nil, errors.WithMessagef(err, "could not read template file %s", fullname) + return nil, err } - tpl, err := template.New(name).Parse(string(content)) + _, err = tpl.AddParseTree("index", index.Tree) if err != nil { - return nil, errors.WithMessagef(err, "could not parse template file %s", fullname) + return nil, errors.WithMessage(err, "could not add index template") } - templates.Blocks[name] = template.Must(template.Must(tpl.Clone()).New("index").Parse(dummyTemplate)) - templates.Pages[name] = template.Must(template.Must(tpl.Clone()).New("index").Parse(string(indexText))) + templates[tpl.Name()] = tpl } return templates, nil @@ -153,13 +162,13 @@ func New(runtimeConfig *Config) (*Server, error) { LiveReload: jsSnippet, } mux.HandleFunc("/{$}", func(w http.ResponseWriter, _ *http.Request) { - err := templates.Pages["index"].Execute(w, indexData) + err := templates["index"].Execute(w, indexData) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }) - nixosOptions := make(map[string]Option) + var nixosOptions = options.NixOptions{} jsonFile, err := os.ReadFile(path.Join("data", "test.json")) if err != nil { slog.Error(fmt.Sprintf("error reading json file: %v", err)) @@ -176,11 +185,10 @@ func New(runtimeConfig *Config) (*Server, error) { } var err error if r.Header.Get("Fetch") == "true" { - slog.Debug("rendering template", "block", true) - err = templates.Blocks["options"].ExecuteTemplate(w, "index", tdata) + w.Header().Add("Content-Type", "text/html; charset=utf-8") + err = templates["options"].ExecuteTemplate(w, "results", tdata) } else { - slog.Debug("rendering template", "block", false) - err = templates.Pages["options"].ExecuteTemplate(w, "index", tdata) + err = templates["options"].ExecuteTemplate(w, "index", tdata) } if err != nil { slog.Error(fmt.Sprintf("template error: %v", err)) -- cgit 1.4.1