about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--frontend/static/search.js31
-rw-r--r--frontend/templates/blocks/options.gotmpl112
-rw-r--r--frontend/templates/blocks/search.gotmpl17
-rw-r--r--frontend/templates/index.gotmpl13
-rw-r--r--internal/server/server.go11
-rw-r--r--internal/server/templates.go24
6 files changed, 128 insertions, 80 deletions
diff --git a/frontend/static/search.js b/frontend/static/search.js
index f034fe1..60fa30d 100644
--- a/frontend/static/search.js
+++ b/frontend/static/search.js
@@ -1,5 +1,10 @@
 const search = document.getElementById("search");
-const results = document.getElementById("results");
+let results = document.getElementById("results");
+
+const range = new Range();
+range.setStartAfter(search);
+range.setEndAfter(search.parentNode.lastChild);
+
 let state = history.state || {
   url: new URL(location).toJSON(),
   opened: [],
@@ -24,7 +29,7 @@ function detailsToggled(ev) {
   state.url = nextURL.toJSON();
   history.replaceState(state, "", nextURL);
 }
-function addToggleEventListeners() {
+function addToggleEventListeners(results) {
   results.querySelectorAll("details").forEach((details) =>
     // toggle event doesn't bubble :(
     details.addEventListener("toggle", detailsToggled, { passive: true }),
@@ -49,18 +54,25 @@ search.addEventListener("submit", function (ev) {
       }
     })
     .then(function (html) {
-      results.innerHTML =
-        escapePolicy !== null ? escapePolicy.createHTML(html) : html;
-      addToggleEventListeners();
+      const fragment = range.createContextualFragment(
+        escapePolicy !== null ? escapePolicy.createHTML(html) : html,
+      );
+      const results = fragment.firstElementChild;
+      range.deleteContents();
+      range.insertNode(results);
+      addToggleEventListeners(results);
     })
     .catch(function (error) {
-      results.innerText = error.message;
+      range.deleteContents();
+      range.insertNode(new Text(error.message));
       console.error("fetch failed", error);
     });
   ev.preventDefault();
 });
 
-addToggleEventListeners();
+if (results !== null) {
+  addToggleEventListeners(results);
+}
 
 if (state.opened.length > 0) {
   state.opened.forEach((id) =>
@@ -71,7 +83,8 @@ if (state.opened.length > 0) {
 }
 
 addEventListener("popstate", function (ev) {
-  if (ev.state == null || ev.state.url.pathname == "/") {
-    results.replaceChildren();
+  if (ev.state == null || ev.state.url.pathname.startsWith("/search/")) {
+    range.deleteContents();
+    search.reset();
   }
 });
diff --git a/frontend/templates/blocks/options.gotmpl b/frontend/templates/blocks/options.gotmpl
index 2f9d88a..e39d60c 100644
--- a/frontend/templates/blocks/options.gotmpl
+++ b/frontend/templates/blocks/options.gotmpl
@@ -1,59 +1,61 @@
-{{ template "results" . }}
-{{ define "results" }}
-  {{- with .Results }}
-    {{- range .Results }}
-      <details id="{{ .Option }}">
-        <summary>
-          {{ .Option }}
-        </summary>
-        <p>
-          {{ markdown .Description }}
-        </p>
-        <dl>
-          {{- with .Type }}
-            <dt>Type</dt>
-            <dd><code>{{ . }}</code></dd>
-          {{- end }}
-          {{- with .Default }}
-            {{- if or .Text .Markdown }}
-              <dt>Default</dt>
-              <dd>
-                {{- if .Markdown }}
-                  {{ markdown .Markdown }}
-                {{- else }}
-                  <pre><code>{{ .Text }}</code></pre>
-                {{- end }}
-              </dd>
+{{- template "results" .Results -}}
+{{- define "results" }}
+  <div id="results">
+    {{- with . }}
+      {{- range .Results }}
+        <details id="{{ .Option }}">
+          <summary>
+            {{ .Option }}
+          </summary>
+          <p>
+            {{ markdown .Description }}
+          </p>
+          <dl>
+            {{- with .Type }}
+              <dt>Type</dt>
+              <dd><code>{{ . }}</code></dd>
             {{- end }}
-          {{- end }}
-          {{- with .Example }}
-            {{- if or .Text .Markdown }}
-              <dt>Example</dt>
-              <dd>
-                {{- if .Markdown }}
-                  {{ markdown .Markdown }}
-                {{- else }}
-                  <pre><code>{{ .Text }}</code></pre>
-                {{- end }}
-              </dd>
+            {{- with .Default }}
+              {{- if or .Text .Markdown }}
+                <dt>Default</dt>
+                <dd>
+                  {{- if .Markdown }}
+                    {{ markdown .Markdown }}
+                  {{- else }}
+                    <pre><code>{{ .Text }}</code></pre>
+                  {{- end }}
+                </dd>
+              {{- end }}
             {{- end }}
-          {{- end }}
-          {{- with .RelatedPackages }}
-            <dt>Related Packages</dt>
-            <dd>{{ . }}</dd>
-          {{- end }}
-          {{- with .Declarations }}
-            <dt>Declared</dt>
-            {{- range . }}
-              <dd>
-                <a href="{{ .URL }}">{{ .Name }}</a>
-              </dd>
+            {{- with .Example }}
+              {{- if or .Text .Markdown }}
+                <dt>Example</dt>
+                <dd>
+                  {{- if .Markdown }}
+                    {{ markdown .Markdown }}
+                  {{- else }}
+                    <pre><code>{{ .Text }}</code></pre>
+                  {{- end }}
+                </dd>
+              {{- end }}
             {{- end }}
-          {{- end }}
-        </dl>
-      </details>
-    {{- else }}
-      Nothing found
+            {{- with .RelatedPackages }}
+              <dt>Related Packages</dt>
+              <dd>{{ . }}</dd>
+            {{- end }}
+            {{- with .Declarations }}
+              <dt>Declared</dt>
+              {{- range . }}
+                <dd>
+                  <a href="{{ .URL }}">{{ .Name }}</a>
+                </dd>
+              {{- end }}
+            {{- end }}
+          </dl>
+        </details>
+      {{- else }}
+        Nothing found
+      {{- end }}
     {{- end }}
-  {{- end }}
-{{ end }}
+  </div>
+{{- end }}
diff --git a/frontend/templates/blocks/search.gotmpl b/frontend/templates/blocks/search.gotmpl
new file mode 100644
index 0000000..5482e6b
--- /dev/null
+++ b/frontend/templates/blocks/search.gotmpl
@@ -0,0 +1,17 @@
+{{- template "main" . }}
+{{- template "js" . }}
+
+{{- define "main" }}
+  <label for="query">Search</label>
+  <form id="search" action="/options/results">
+    <input id="query" name="query" type="search" value="{{ .Query }}" />
+    <button>Search</button>
+  </form>
+  {{- with .Results }}
+    {{ block "results" . }}{{ end }}
+  {{- end }}
+{{- end }}
+
+{{- define "js" }}
+  <script src="/static/search.js" defer></script>
+{{- end }}
diff --git a/frontend/templates/index.gotmpl b/frontend/templates/index.gotmpl
index d5046e1..3c2840a 100644
--- a/frontend/templates/index.gotmpl
+++ b/frontend/templates/index.gotmpl
@@ -13,18 +13,17 @@
       <p>Search Nix Packages and options from NixOS, Darwin and Home-Manager</p>
       <nav>
         <a href="/">Home</a>
+        <a href="/search/nixos">NixOS Options</a>
       </nav>
     </header>
     <main>
-      <label for="query">Search </label>
-      <form id="search" action="/options/results">
-        <input id="query" name="query" type="search" value="{{ .Query }}" />
-        <button>Search</button>
-      </form>
-      <div id="results">{{ block "results" . }}{{ end }}</div>
+      {{ block "main" . }}
+        <p>Hello!</p>
+      {{ end }}
     </main>
     <footer>Made by <a href="https://alanpearce.eu">Alan Pearce</a></footer>
-    <script src="/static/search.js" defer></script>
+    {{ block "js" . }}
+    {{ end }}
     {{ .LiveReload }}
   </body>
 </html>
diff --git a/internal/server/server.go b/internal/server/server.go
index 3f87b9d..c0fbfd0 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -57,6 +57,7 @@ const jsSnippet = template.HTML(livereload.JsSnippet) // #nosec G203
 type TemplateData struct {
 	LiveReload template.HTML
 	Query      string
+	Results    bool
 }
 
 type ResultData[T options.NixOption] struct {
@@ -120,6 +121,14 @@ func New(runtimeConfig *Config) (*Server, error) {
 		}
 	})
 
+	mux.HandleFunc("/search/{source}", func(w http.ResponseWriter, r *http.Request) {
+		log.Println(r.PathValue("source"))
+		err := templates["search"].Execute(w, indexData)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	})
+
 	timeout := 1 * time.Second
 	mux.HandleFunc("/options/results", func(w http.ResponseWriter, r *http.Request) {
 		ctx, cancel := context.WithTimeoutCause(r.Context(), timeout, errors.New("timeout"))
@@ -141,7 +150,7 @@ func New(runtimeConfig *Config) (*Server, error) {
 		}
 		if r.Header.Get("Fetch") == "true" {
 			w.Header().Add("Content-Type", "text/html; charset=utf-8")
-			err = templates["options"].Execute(w, tdata)
+			err = templates["options"].ExecuteTemplate(w, "options.gotmpl", tdata)
 		} else {
 			err = templates["options"].ExecuteTemplate(w, "index.gotmpl", tdata)
 		}
diff --git a/internal/server/templates.go b/internal/server/templates.go
index 71fc5c7..8755e36 100644
--- a/internal/server/templates.go
+++ b/internal/server/templates.go
@@ -34,14 +34,20 @@ var templateFuncs = template.FuncMap{
 	},
 }
 
-func loadTemplate(filenames ...string) (*template.Template, error) {
-	tpl := template.New(path.Base(filenames[0]))
+func loadTemplate(layoutFile string, filename string) (*template.Template, error) {
+	tpl := template.New("index.gotmpl")
 
 	tpl.Funcs(templateFuncs)
-
-	_, err := tpl.ParseFiles(filenames...)
+	_, err := tpl.ParseFiles(layoutFile)
 	if err != nil {
-		return nil, errors.WithMessage(err, "could not parse template")
+		return nil, errors.WithMessage(err, "could not parse layout template")
+	}
+
+	if filename != "" {
+		_, err = tpl.ParseGlob(filename)
+		if err != nil {
+			return nil, errors.WithMessage(err, "could not parse template")
+		}
 	}
 
 	return tpl, nil
@@ -53,21 +59,23 @@ func loadTemplates() (TemplateCollection, error) {
 
 	layoutFile := path.Join(templateDir, "index.gotmpl")
 
-	index, err := loadTemplate(layoutFile)
+	index, err := loadTemplate(layoutFile, "")
 	if err != nil {
 		return nil, err
 	}
 	templates["index"] = index
 
-	templatePaths, err := filepath.Glob(path.Join(templateDir, "blocks", "*.gotmpl"))
+	glob := path.Join(templateDir, "blocks", "*.gotmpl")
+	templatePaths, err := filepath.Glob(glob)
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not glob block templates")
 	}
 	for _, fullname := range templatePaths {
-		tpl, err := loadTemplate(fullname, layoutFile)
+		tpl, err := loadTemplate(layoutFile, glob)
 		if err != nil {
 			return nil, err
 		}
+
 		name, _ := strings.CutSuffix(path.Base(fullname), ".gotmpl")
 		templates[name] = tpl
 	}