diff options
-rw-r--r-- | config.toml | 1 | ||||
-rw-r--r-- | default.nix | 2 | ||||
-rw-r--r-- | frontend/static/search.js | 26 | ||||
-rw-r--r-- | frontend/templates/blocks/options.gotmpl | 13 | ||||
-rw-r--r-- | internal/server/option.go | 15 | ||||
-rw-r--r-- | internal/server/server.go | 110 |
6 files changed, 152 insertions, 15 deletions
diff --git a/config.toml b/config.toml index 8c66184..9b6a35a 100644 --- a/config.toml +++ b/config.toml @@ -8,6 +8,7 @@ image-src = [ "http://gc.zgo.at", ] script-src = [ + "'self'", "http://gc.zgo.at", ] require-trusted-types-for = [ diff --git a/default.nix b/default.nix index d3585e0..4face77 100644 --- a/default.nix +++ b/default.nix @@ -54,7 +54,7 @@ in editorconfig-checker.enable = true; prettier = { enable = true; - types_or = [ "plain-text" "yaml" "gotmpl" ]; + types_or = [ "plain-text" "yaml" "gotmpl" "javascript" ]; settings = { plugins = with pkgs.nodePackages; [ "${prettier-plugin-go-template}/lib/node_modules/prettier-plugin-go-template/lib/index.js" diff --git a/frontend/static/search.js b/frontend/static/search.js new file mode 100644 index 0000000..20f1a7d --- /dev/null +++ b/frontend/static/search.js @@ -0,0 +1,26 @@ +const search = document.getElementById("search"); +const results = document.getElementById("results"); +search.addEventListener("submit", function (ev) { + const url = new URL(this.action); + url.search = new URLSearchParams(new FormData(this)).toString(); + const res = fetch(url, { + headers: { + fetch: "true", + }, + }) + .then(function (res) { + window.history.pushState(null, null, url); + if (res.ok) { + return res.text(); + } else { + throw new Error(res.statusText); + } + }) + .then(function (html) { + results.innerHTML = html; + }) + .catch(function (error) { + console.error("fetch failed", error); + }); + ev.preventDefault(); +}); diff --git a/frontend/templates/blocks/options.gotmpl b/frontend/templates/blocks/options.gotmpl new file mode 100644 index 0000000..3451eb3 --- /dev/null +++ b/frontend/templates/blocks/options.gotmpl @@ -0,0 +1,13 @@ +{{ define "results" }} + {{ range $opt, $data := .Results }} + <details> + <summary> + {{ $opt }} + </summary> + <p> + {{ $data.Description }} + </p> + </details> + {{ end }} +</div> +{{ end }} diff --git a/internal/server/option.go b/internal/server/option.go new file mode 100644 index 0000000..be42689 --- /dev/null +++ b/internal/server/option.go @@ -0,0 +1,15 @@ +package server + +type NixValue struct { + Kind string `json:"_type"` + Value string `json:"text"` +} + +type Option struct { + Declarations []string + Default NixValue + Description string + Example NixValue + ReadOnly bool + Kind string `json:"type"` +} diff --git a/internal/server/server.go b/internal/server/server.go index bb29ff3..09928b4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "fmt" "html/template" "io" @@ -11,7 +12,9 @@ import ( "net/http" "os" "path" + "path/filepath" "slices" + "strings" "time" cfg "searchix/internal/config" @@ -54,7 +57,18 @@ const jsSnippet = template.HTML(livereload.JsSnippet) // #nosec G203 type TemplateData struct { LiveReload template.HTML - Data map[string]interface{} + Query string +} + +type OptionResultData struct { + TemplateData + Query string + Results map[string]Option +} + +type TemplateCollection struct { + Pages map[string]*template.Template + Blocks map[string]*template.Template } func applyDevModeOverrides(config *cfg.Config) { @@ -62,6 +76,46 @@ func applyDevModeOverrides(config *cfg.Config) { config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'") } +const dummyTemplate = `{{ block "results" . }}{{ end }}` + +func loadTemplates() (*TemplateCollection, error) { + templateDir := path.Join("frontend", "templates") + templates := &TemplateCollection{ + Pages: make(map[string]*template.Template), + Blocks: make(map[string]*template.Template), + } + indexText, err := os.ReadFile(path.Join(templateDir, "index.gotmpl")) + if err != nil { + return nil, errors.WithMessage(err, "could not read index template") + } + index, err := template.New("index").Parse(string(indexText)) + if err != nil { + return nil, errors.WithMessage(err, "could not parse index template") + } + templates.Pages["index"] = index + templates.Blocks = make(map[string]*template.Template) + + 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) + if err != nil { + return nil, errors.WithMessagef(err, "could not read template file %s", fullname) + } + tpl, err := template.New(name).Parse(string(content)) + if err != nil { + return nil, errors.WithMessagef(err, "could not parse template file %s", fullname) + } + 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))) + } + + return templates, nil +} + func New(runtimeConfig *Config) (*Server, error) { var err error config, err = cfg.GetConfig() @@ -88,18 +142,48 @@ func New(runtimeConfig *Config) (*Server, error) { Repanic: true, }) - templatePaths := path.Join("frontend", "templates", "*.gotmpl") - tpl := template.Must(template.ParseGlob(templatePaths)) + templates, err := loadTemplates() + if err != nil { + log.Panicf("could not load templates: %v", err) + } top := http.NewServeMux() mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { - tdata := TemplateData{ - LiveReload: jsSnippet, - Data: make(map[string]interface{}), + indexData := TemplateData{ + LiveReload: jsSnippet, + } + mux.HandleFunc("/{$}", func(w http.ResponseWriter, _ *http.Request) { + err := templates.Pages["index"].Execute(w, indexData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + nixosOptions := make(map[string]Option) + jsonFile, err := os.ReadFile(path.Join("data", "test.json")) + if err != nil { + slog.Error(fmt.Sprintf("error reading json file: %v", err)) + } + err = json.Unmarshal(jsonFile, &nixosOptions) + if err != nil { + slog.Error(fmt.Sprintf("error parsing json file: %v", err)) + } + mux.HandleFunc("/options/results", func(w http.ResponseWriter, r *http.Request) { + tdata := OptionResultData{ + TemplateData: indexData, + Query: r.URL.Query().Get("query"), + Results: nixosOptions, + } + var err error + if r.Header.Get("Fetch") == "true" { + slog.Debug("rendering template", "block", true) + err = templates.Blocks["options"].ExecuteTemplate(w, "index", tdata) + } else { + slog.Debug("rendering template", "block", false) + err = templates.Pages["options"].ExecuteTemplate(w, "index", tdata) } - err := tpl.Execute(w, tdata) if err != nil { + slog.Error(fmt.Sprintf("template error: %v", err)) http.Error(w, err.Error(), http.StatusInternalServerError) } }) @@ -115,18 +199,16 @@ func New(runtimeConfig *Config) (*Server, error) { if err != nil { return nil, errors.WithMessage(err, "could not create file watcher") } - err = fw.AddRecursive("frontend") + err = fw.AddRecursive(path.Join("frontend", "templates")) if err != nil { return nil, errors.WithMessage(err, "could not add directory to file watcher") } go fw.Start(func() { - t, err := template.ParseGlob(path.Join("frontend", "templates", "*.tmpl")) + templates, err = loadTemplates() if err != nil { - slog.Error(fmt.Sprintf("could not parse template: %v", err)) - } else { - tpl = t - liveReload.Reload() + slog.Error(fmt.Sprintf("could not reload templates: %v", err)) } + liveReload.Reload() }) } |