diff options
author | Alan Pearce | 2024-05-05 18:11:56 +0200 |
---|---|---|
committer | Alan Pearce | 2024-05-05 18:11:56 +0200 |
commit | 2430f46a9948b38b06880606a95dec357d01f619 (patch) | |
tree | 2d8e9078b99ade7e9fe339be805890c635f0e235 | |
parent | 158904f480e558ca00f680e7c577bb6329605eff (diff) | |
download | searchix-2430f46a9948b38b06880606a95dec357d01f619.tar.lz searchix-2430f46a9948b38b06880606a95dec357d01f619.tar.zst searchix-2430f46a9948b38b06880606a95dec357d01f619.zip |
feat: render markdown in option descriptions
-rw-r--r-- | frontend/templates/blocks/options.gotmpl | 20 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | go.sum | 6 | ||||
-rw-r--r-- | gomod2nix.toml | 9 | ||||
-rw-r--r-- | internal/options/option.go | 57 | ||||
-rw-r--r-- | internal/options/process.go | 109 | ||||
-rw-r--r-- | internal/server/option.go | 15 | ||||
-rw-r--r-- | internal/server/server.go | 68 | ||||
-rw-r--r-- | justfile | 2 | ||||
-rw-r--r-- | process/main.go | 37 |
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) + } +} |