about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--frontend/templates/blocks/options.gotmpl20
-rw-r--r--go.mod3
-rw-r--r--go.sum6
-rw-r--r--gomod2nix.toml9
-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
-rw-r--r--justfile2
-rw-r--r--process/main.go37
10 files changed, 272 insertions, 54 deletions
diff --git a/frontend/templates/blocks/options.gotmpl b/frontend/templates/blocks/options.gotmpl
index d5171c3..1172e52 100644
--- a/frontend/templates/blocks/options.gotmpl
+++ b/frontend/templates/blocks/options.gotmpl
@@ -1,28 +1,32 @@
 {{ define "results" }}
-  {{- range $opt, $data := .Results }}
-    <details id="{{ $opt }}">
+  {{- range .Results }}
+    <details id="{{ .Option }}">
       <summary>
-        {{ $opt }}
+        {{ .Option }}
       </summary>
       <p>
-        {{ $data.Description }}
+        {{ HTML .Description.HTML }}
       </p>
       <dl>
-        {{- with $data.Type }}
+        {{- with .Type }}
           <dt>Type</dt>
           <dd>{{ . }}</dd>
         {{- end }}
-        {{- with $data.Default }}
+        {{- with .Default }}
           <dt>Default</dt>
           <dd><code>{{ .Text }}</code></dd>
         {{- end }}
-        {{- with $data.Example }}
+        {{- with .Example }}
           {{- if .Text }}
             <dt>Example</dt>
             <dd><code>{{ .Text }}</code></dd>
           {{- end }}
         {{- end }}
-        {{- with $data.Declarations }}
+        {{- with .RelatedPackages.HTML }}
+          <dt>Related Packages</dt>
+          <dd>{{ . }}</dd>
+        {{- end }}
+        {{- with .Declarations }}
           <dt>Declared</dt>
           {{- range . }}
             <dd>
diff --git a/go.mod b/go.mod
index c656f22..664c1a1 100644
--- a/go.mod
+++ b/go.mod
@@ -4,13 +4,16 @@ go 1.22.2
 
 require (
 	github.com/ardanlabs/conf/v3 v3.1.7
+	github.com/bcicen/jstream v1.0.1
 	github.com/crewjam/csp v0.0.2
 	github.com/fsnotify/fsnotify v1.7.0
 	github.com/getsentry/sentry-go v0.27.0
+	github.com/mitchellh/mapstructure v1.5.0
 	github.com/osdevisnot/sorvor v0.4.4
 	github.com/pelletier/go-toml/v2 v2.2.1
 	github.com/pkg/errors v0.9.1
 	github.com/shengyanli1982/law v0.1.15
+	github.com/yuin/goldmark v1.7.1
 )
 
 require (
diff --git a/go.sum b/go.sum
index 082f7c5..2e315cc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 github.com/ardanlabs/conf/v3 v3.1.7 h1:p232cF68TafoA5U9ZlbxUIhGJtGNdKHBXF80Fdqb5t0=
 github.com/ardanlabs/conf/v3 v3.1.7/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU=
+github.com/bcicen/jstream v1.0.1 h1:BXY7Cu4rdmc0rhyTVyT3UkxAiX3bnLpKLas9btbH5ck=
+github.com/bcicen/jstream v1.0.1/go.mod h1:9ielPxqFry7Y4Tg3j4BfjPocfJ3TbsRtXOAYXYmRuAQ=
 github.com/crewjam/csp v0.0.2 h1:fIq6o0Z6bkABlvLT3kB0XgPnVX9iNXSAGMILs6AqHVw=
 github.com/crewjam/csp v0.0.2/go.mod h1:0tirp4wHwMLZZtV+HXRqGFkUO7uD2ux+1ECvK+7/xFI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -15,6 +17,8 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/osdevisnot/sorvor v0.4.4 h1:hcMWsWOKpUtDUE3F7dra1Jf12ftLHfgDcxlyPeVlz0Y=
 github.com/osdevisnot/sorvor v0.4.4/go.mod h1:D/j+vvJEmjIXndJf37uwFWD0Hjcq9DiGojyt4yMo7H0=
 github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
@@ -36,6 +40,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
diff --git a/gomod2nix.toml b/gomod2nix.toml
index c808744..8d7d130 100644
--- a/gomod2nix.toml
+++ b/gomod2nix.toml
@@ -4,6 +4,9 @@ schema = 3
   [mod."github.com/ardanlabs/conf/v3"]
     version = "v3.1.7"
     hash = "sha256-7H53l0JN5Q6hkAgBivVQ8lFd03oNmP1IG8ihzLKm2CQ="
+  [mod."github.com/bcicen/jstream"]
+    version = "v1.0.1"
+    hash = "sha256-mm+/BuIEYYj6XOHCCJLxVMKd1XcBXCiRCWA+aTvr1sE="
   [mod."github.com/crewjam/csp"]
     version = "v0.0.2"
     hash = "sha256-4vlGmDdQjPiXmueCV51fJH/hRcG8eqhCi9TENCXjzfA="
@@ -16,6 +19,9 @@ schema = 3
   [mod."github.com/google/go-cmp"]
     version = "v0.6.0"
     hash = "sha256-qgra5jze4iPGP0JSTVeY5qV5AvEnEu39LYAuUCIkMtg="
+  [mod."github.com/mitchellh/mapstructure"]
+    version = "v1.5.0"
+    hash = "sha256-ztVhGQXs67MF8UadVvG72G3ly0ypQW0IRDdOOkjYwoE="
   [mod."github.com/osdevisnot/sorvor"]
     version = "v0.4.4"
     hash = "sha256-BhyO7bvwxIdEV+c6Eo1uqahhcgsHiS8nJpg2aT8t+8s="
@@ -28,6 +34,9 @@ schema = 3
   [mod."github.com/shengyanli1982/law"]
     version = "v0.1.15"
     hash = "sha256-Z5G3PtR7V0d04MN+kBge33Pv6VDjJryx+N7JGJkzfLQ="
+  [mod."github.com/yuin/goldmark"]
+    version = "v1.7.1"
+    hash = "sha256-3EUgwoZRRs2jNBWSbB0DGNmfBvx7CeAgEwyUdaRaeR4="
   [mod."golang.org/x/sys"]
     version = "v0.19.0"
     hash = "sha256-cmuL31TYLJmDm/fDnI2Sn0wB88cpdOHV1+urorsJWx4="
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))
diff --git a/justfile b/justfile
index b878de3..0168d99 100644
--- a/justfile
+++ b/justfile
@@ -5,7 +5,7 @@ prepare:
 	ln -sf $(nix-build --no-out-link -A css) frontend/static/base.css
 
 update-nixos-options:
-	ln -sf $(nix-build --no-out-link -A nixos-options)/share/doc/nixos/options.json data/nixos-options.json
+	wgo run -exit ./process --input $(nix-build --no-out-link -A nixos-options)/share/doc/nixos/options.json --output data/nixos-options.json
 
 checkformat:
 	gofmt -d .
diff --git a/process/main.go b/process/main.go
new file mode 100644
index 0000000..3c9b67a
--- /dev/null
+++ b/process/main.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"log"
+	"log/slog"
+	"os"
+	"searchix/internal/options"
+
+	"github.com/ardanlabs/conf/v3"
+	"github.com/pkg/errors"
+)
+
+type Config struct {
+	Input  string `conf:"short:i,required,help:NixOS options file (json)"`
+	Output string `conf:"short:o,default:/dev/stdout"`
+}
+
+func main() {
+	if os.Getenv("DEBUG") != "" {
+		slog.SetLogLoggerLevel(slog.LevelDebug)
+	}
+	log.SetFlags(0)
+
+	config := Config{}
+	help, err := conf.Parse("", &config)
+	if err != nil {
+		if errors.Is(err, conf.ErrHelpWanted) {
+			log.Fatalln(help)
+		}
+		log.Fatalf("parsing command line: %v", err)
+	}
+
+	err = options.Process(config.Input, config.Output)
+	if err != nil {
+		log.Fatalf("Error processing file: %v", err)
+	}
+}