about summary refs log tree commit diff stats
path: root/internal
diff options
context:
space:
mode:
authorAlan Pearce2024-05-05 18:11:56 +0200
committerAlan Pearce2024-05-05 18:11:56 +0200
commit2430f46a9948b38b06880606a95dec357d01f619 (patch)
tree2d8e9078b99ade7e9fe339be805890c635f0e235 /internal
parent158904f480e558ca00f680e7c577bb6329605eff (diff)
downloadsearchix-2430f46a9948b38b06880606a95dec357d01f619.tar.lz
searchix-2430f46a9948b38b06880606a95dec357d01f619.tar.zst
searchix-2430f46a9948b38b06880606a95dec357d01f619.zip
feat: render markdown in option descriptions
Diffstat (limited to 'internal')
-rw-r--r--internal/options/option.go57
-rw-r--r--internal/options/process.go109
-rw-r--r--internal/server/option.go15
-rw-r--r--internal/server/server.go68
4 files changed, 204 insertions, 45 deletions
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))