about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--frontend/static/style.css4
-rw-r--r--frontend/templates/blocks/option.gotmpl48
-rw-r--r--frontend/templates/blocks/package.gotmpl69
-rw-r--r--internal/config/importer-type.go11
-rw-r--r--internal/index/search.go77
-rw-r--r--internal/nix/importable.go9
-rw-r--r--internal/server/mux.go62
7 files changed, 249 insertions, 31 deletions
diff --git a/frontend/static/style.css b/frontend/static/style.css
index ff4cc6e..8d5977f 100644
--- a/frontend/static/style.css
+++ b/frontend/static/style.css
@@ -92,10 +92,6 @@ pre {
   padding: 1ch 1.4ch;
 }
 
-pre:has(> code) {
-  background: var(--bg);
-}
-
 #pagination {
   display: flex;
   justify-content: space-between;
diff --git a/frontend/templates/blocks/option.gotmpl b/frontend/templates/blocks/option.gotmpl
new file mode 100644
index 0000000..2248708
--- /dev/null
+++ b/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 }}
diff --git a/frontend/templates/blocks/package.gotmpl b/frontend/templates/blocks/package.gotmpl
new file mode 100644
index 0000000..a42a8b1
--- /dev/null
+++ b/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 }}
diff --git a/internal/config/importer-type.go b/internal/config/importer-type.go
index 8f64d58..9bd50b6 100644
--- a/internal/config/importer-type.go
+++ b/internal/config/importer-type.go
@@ -25,6 +25,17 @@ func (i ImporterType) String() string {
 	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":
diff --git a/internal/index/search.go b/internal/index/search.go
index 47f8ac1..3198afb 100644
--- a/internal/index/search.go
+++ b/internal/index/search.go
@@ -82,6 +82,42 @@ func setField(
 	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 @@ func (index *ReadIndex) Search(
 
 	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
 }
diff --git a/internal/nix/importable.go b/internal/nix/importable.go
index 37532cd..67ba96d 100644
--- a/internal/nix/importable.go
+++ b/internal/nix/importable.go
@@ -1,6 +1,9 @@
 package nix
 
-import "encoding/gob"
+import (
+	"encoding/gob"
+	"searchix/internal/config"
+)
 
 type Importable interface {
 	BleveType() string
@@ -12,6 +15,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() {
 	gob.Register(Option{})
 	gob.Register(Package{})
diff --git a/internal/server/mux.go b/internal/server/mux.go
index 7bddbe5..79e24cd 100644
--- a/internal/server/mux.go
+++ b/internal/server/mux.go
@@ -18,6 +18,7 @@ import (
 	"searchix/frontend"
 	"searchix/internal/config"
 	search "searchix/internal/index"
+	"searchix/internal/nix"
 
 	"github.com/blevesearch/bleve/v2"
 	sentryhttp "github.com/getsentry/sentry-go/http"
@@ -55,6 +56,12 @@ type ResultData struct {
 	Next           string
 }
 
+type DocumentData struct {
+	TemplateData
+	Document *nix.Importable
+	Children *search.Result
+}
+
 var templates TemplateCollection
 
 func applyDevModeOverrides(cfg *config.Config) {
@@ -222,6 +229,61 @@ func NewMux(
 	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) {
 			type openSearchData struct {