diff options
-rw-r--r-- | frontend/static/style.css | 4 | ||||
-rw-r--r-- | frontend/templates/blocks/option.gotmpl | 48 | ||||
-rw-r--r-- | frontend/templates/blocks/package.gotmpl | 69 | ||||
-rw-r--r-- | internal/config/importer-type.go | 11 | ||||
-rw-r--r-- | internal/index/search.go | 77 | ||||
-rw-r--r-- | internal/nix/importable.go | 9 | ||||
-rw-r--r-- | internal/server/mux.go | 62 |
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 { |