all repos — searchix @ d40c0e188a7fe1b36887f59c4a9958faa81b3d44

Search engine for NixOS, nix-darwin, home-manager and NUR users

feat: add detail pages for packages/options
Alan Pearce alan@alanpearce.eu
Sat, 08 Jun 2024 20:31:47 +0200
commit

d40c0e188a7fe1b36887f59c4a9958faa81b3d44

parent

ef6c98da84c2327e0a0003fb3b1b64a5d1e2d550

M frontend/static/style.cssfrontend/static/style.css
@@ -92,10 +92,6 @@ margin: unset;   padding: 1ch 1.4ch;
 }
 
-pre:has(> code) {
-  background: var(--bg);
-}
-
 #pagination {
   display: flex;
   justify-content: space-between;
A frontend/templates/blocks/option.gotmpl
@@ -0,0 +1,48 @@+{{- define "main" }}
+  {{- with .Document }}
+    <h2>{{ .Name }}</h2>
+    {{ markdown .Description }}
+    <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>
+        {{- end }}
+      {{- end }}
+      {{- with .Example }}
+        {{- if or .Text .Markdown }}
+          <dt>Example</dt>
+          <dd>
+            {{- if .Markdown }}
+              {{ markdown .Markdown }}
+            {{- else }}
+              <pre><code>{{ .Text }}</code></pre>
+            {{- end }}
+          </dd>
+        {{- end }}
+      {{- end }}
+      {{- 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>
+  {{- end }}
+{{- end }}
A frontend/templates/blocks/package.gotmpl
@@ -0,0 +1,69 @@+{{- define "main" }}
+  {{- with .Document }}
+    <h2>
+      {{- if .Broken }}
+        <del>{{ .Attribute }}</del>
+      {{- else }}
+        {{ .Attribute }}
+      {{- end }}
+    </h2>
+    {{- if .LongDescription }}
+      {{ markdown .LongDescription }}
+    {{- else }}
+      <p>{{ .Description }}</p>
+    {{- end }}
+    <dl>
+      {{- with .MainProgram }}
+        <dt>Main Program</dt>
+        <dd>
+          <code>{{ . }}</code>
+        </dd>
+      {{- end }}
+      {{- with .Homepages }}
+        <dt>Homepage</dt>
+        <dd>
+          {{- range . }}
+            <a href="{{ . }}">{{ . }}</a>
+          {{- end }}
+        </dd>
+      {{- end }}
+      {{- with .Version }}
+        <dt>Version</dt>
+        <dd>{{ . }}</dd>
+      {{- end }}
+      {{- with .Licenses }}
+        <dt>License</dt>
+        <dd>
+          {{- range . }}
+            {{- if .URL }}
+              <a href="{{ .URL }}">{{ or .FullName .Name }}</a>
+            {{- else }}
+              {{ or .FullName .Name }}
+            {{- end }}
+            {{- with .AppendixURL }}
+              <a href="{{ . }}">Appendix</a>
+            {{- end }}
+          {{- end }}
+        </dd>
+      {{- end }}
+      {{- with .Maintainers }}
+        <dt>Maintainer{{ if gt (len .) 1 }}s{{ end }}</dt>
+        <dd>
+          {{- range . }}
+            {{- if .Github }}
+              <a href="https://github.com/{{ .Github }}">{{ .Name }}</a>
+            {{- else }}
+              {{ .Name }}
+            {{- end }}
+          {{- end }}
+        </dd>
+      {{- end }}
+      {{- with .Definition }}
+        <dt>Defined</dt>
+        <dd>
+          <a href="{{ . }}">Source</a>
+        </dd>
+      {{- end }}
+    </dl>
+  {{- end }}
+{{- end }}
M internal/config/importer-type.gointernal/config/importer-type.go
@@ -25,6 +25,17 @@ 	return fmt.Sprintf("Type(%d)", i)
 }
 
+func (i ImporterType) Singular() string {
+	switch i {
+	case Packages:
+		return "package"
+	case Options:
+		return "option"
+	}
+
+	return fmt.Sprintf("Type(%d)", i)
+}
+
 func ParseImporterType(name string) (ImporterType, error) {
 	switch strcase.KebabCase(name) {
 	case "packages":
M internal/index/search.gointernal/index/search.go
@@ -82,6 +82,42 @@ 	return q
 }
 
+func (index *ReadIndex) search(
+	ctx context.Context,
+	request *bleve.SearchRequest,
+) (*Result, error) {
+	request.Fields = []string{"_data"}
+
+	bleveResult, err := index.index.SearchInContext(ctx, request)
+	select {
+	case <-ctx.Done():
+		return nil, ctx.Err()
+	default:
+		if err != nil {
+			return nil, errors.WithMessage(err, "failed to execute search query")
+		}
+
+		results := make([]DocumentMatch, min(ResultsPerPage, bleveResult.Total))
+		var buf bytes.Buffer
+		for i, result := range bleveResult.Hits {
+			_, err = buf.WriteString(result.Fields["_data"].(string))
+			if err != nil {
+				return nil, errors.WithMessage(err, "error fetching result data")
+			}
+			err = gob.NewDecoder(&buf).Decode(&results[i].Data)
+			if err != nil {
+				return nil, errors.WithMessagef(err, "error decoding gob data: %s", buf.String())
+			}
+			buf.Reset()
+		}
+
+		return &Result{
+			SearchResult: bleveResult,
+			Hits:         results,
+		}, nil
+	}
+}
+
 func (index *ReadIndex) Search(
 	ctx context.Context,
 	source *config.Source,
@@ -127,38 +163,27 @@ } 
 	search := bleve.NewSearchRequest(query)
 	search.Size = ResultsPerPage
-	search.Fields = []string{"_data"}
 
 	if from != 0 {
 		search.From = int(from)
 	}
 
-	bleveResult, err := index.index.SearchInContext(ctx, search)
-	select {
-	case <-ctx.Done():
-		return nil, ctx.Err()
-	default:
-		if err != nil {
-			return nil, errors.WithMessage(err, "failed to execute search query")
-		}
+	return index.search(ctx, search)
+}
 
-		results := make([]DocumentMatch, min(ResultsPerPage, bleveResult.Total))
-		var buf bytes.Buffer
-		for i, result := range bleveResult.Hits {
-			_, err = buf.WriteString(result.Fields["_data"].(string))
-			if err != nil {
-				return nil, errors.WithMessage(err, "error fetching result data")
-			}
-			err = gob.NewDecoder(&buf).Decode(&results[i].Data)
-			if err != nil {
-				return nil, errors.WithMessagef(err, "error decoding gob data: %s", buf.String())
-			}
-			buf.Reset()
-		}
+func (index *ReadIndex) GetDocument(
+	ctx context.Context,
+	source *config.Source,
+	id string,
+) (*nix.Importable, error) {
+	key := nix.MakeKey(source, id)
+	query := bleve.NewDocIDQuery([]string{key})
+	search := bleve.NewSearchRequest(query)
 
-		return &Result{
-			SearchResult: bleveResult,
-			Hits:         results,
-		}, nil
+	result, err := index.search(ctx, search)
+	if err != nil {
+		return nil, err
 	}
+
+	return &result.Hits[0].Data, err
 }
M internal/nix/importable.gointernal/nix/importable.go
@@ -1,6 +1,9 @@ package nix
 
-import "encoding/gob"
+import (
+	"encoding/gob"
+	"searchix/internal/config"
+)
 
 type Importable interface {
 	BleveType() string
@@ -10,6 +13,10 @@ } 
 func GetKey(i Importable) string {
 	return i.BleveType() + "/" + i.GetSource() + "/" + i.GetName()
+}
+
+func MakeKey(source *config.Source, id string) string {
+	return source.Importer.Singular() + "/" + source.Key + "/" + id
 }
 
 func init() {
M internal/server/mux.gointernal/server/mux.go
@@ -18,6 +18,7 @@ 	"searchix/frontend"
 	"searchix/internal/config"
 	search "searchix/internal/index"
+	"searchix/internal/nix"
 
 	"github.com/blevesearch/bleve/v2"
 	sentryhttp "github.com/getsentry/sentry-go/http"
@@ -53,6 +54,12 @@ ResultsPerPage int 	Results        *search.Result
 	Prev           string
 	Next           string
+}
+
+type DocumentData struct {
+	TemplateData
+	Document *nix.Importable
+	Children *search.Result
 }
 
 var templates TemplateCollection
@@ -221,6 +228,61 @@ } 
 	mux.HandleFunc("/options/{source}/search", createSearchHandler(config.Options))
 	mux.HandleFunc("/packages/{source}/search", createSearchHandler(config.Packages))
+
+	createSourceIDHandler := func(importerType config.ImporterType) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			source := cfg.Importer.Sources[r.PathValue("source")]
+			if source == nil || source.Importer != importerType {
+				errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+
+				return
+			}
+			importerSingular := importerType.Singular()
+
+			ctx, cancel := context.WithTimeout(r.Context(), searchTimeout)
+			defer cancel()
+
+			doc, err := index.GetDocument(ctx, source, r.PathValue("id"))
+			if err != nil {
+				errorHandler(
+					w,
+					r,
+					http.StatusText(http.StatusInternalServerError),
+					http.StatusInternalServerError,
+				)
+
+				return
+			}
+
+			if doc == nil {
+				errorHandler(w, r, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+
+				return
+			}
+
+			tdata := DocumentData{
+				TemplateData: TemplateData{
+					ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
+					Source:        *source,
+					Sources:       cfg.Importer.Sources,
+					Assets:        frontend.Assets,
+				},
+				Document: doc,
+			}
+			if r.Header.Get("Fetch") == "true" {
+				w.Header().Add("Content-Type", "text/html; charset=utf-8")
+				err = templates[importerSingular].ExecuteTemplate(w, "main", tdata)
+			} else {
+				err = templates[importerSingular].Execute(w, tdata)
+			}
+			if err != nil {
+				slog.Error("template error", "template", importerSingular, "error", err)
+				errorHandler(w, r, err.Error(), http.StatusInternalServerError)
+			}
+		}
+	}
+	mux.HandleFunc("/options/{source}/{id}", createSourceIDHandler(config.Options))
+	mux.HandleFunc("/packages/{source}/{id}", createSourceIDHandler(config.Packages))
 
 	createOpenSearchXMLHandler := func(importerType config.ImporterType) func(http.ResponseWriter, *http.Request) {
 		return func(w http.ResponseWriter, r *http.Request) {