From a1dfc548198a1326e71f1dd70303a5d3441f7a39 Mon Sep 17 00:00:00 2001
From: Alan Pearce
Date: Fri, 21 Jun 2024 13:02:08 +0200
Subject: refactor: switch to templ for HTML templates
---
.editorconfig | 2 +-
.gitignore | 3 +
defaults.toml | 8 +++
frontend/assets.go | 12 ++--
frontend/static/search.js | 4 +-
frontend/static/style.css | 11 ++++
frontend/templates/blocks/error.gotmpl | 6 --
frontend/templates/blocks/option.gotmpl | 48 ---------------
frontend/templates/blocks/options.gotmpl | 25 --------
frontend/templates/blocks/package.gotmpl | 69 ---------------------
frontend/templates/blocks/packages.gotmpl | 30 ----------
frontend/templates/blocks/results.gotmpl | 21 -------
frontend/templates/blocks/search.gotmpl | 37 ------------
frontend/templates/index.gotmpl | 55 -----------------
go.mod | 2 +-
go.sum | 2 +
gomod2nix.toml | 6 +-
internal/components/data.go | 37 ++++++++++++
internal/components/detail.templ | 20 +++++++
internal/components/error.templ | 18 ++++++
internal/components/homepage.templ | 10 ++++
internal/components/markdown.templ | 35 +++++++++++
internal/components/optionDetail.templ | 58 ++++++++++++++++++
internal/components/options.templ | 34 +++++++++++
internal/components/packageDetail.templ | 99 +++++++++++++++++++++++++++++++
internal/components/packages.templ | 37 ++++++++++++
internal/components/page.templ | 91 ++++++++++++++++++++++++++++
internal/components/results.templ | 44 ++++++++++++++
internal/components/search.templ | 34 +++++++++++
internal/config/default.go | 4 ++
internal/config/structs.go | 4 +-
internal/server/error.go | 10 ++--
internal/server/mux.go | 93 ++++++++++++-----------------
internal/server/templates.go | 25 +-------
modd.conf | 5 +-
nix/dev-shell.nix | 1 +
nix/package.nix | 1 +
37 files changed, 611 insertions(+), 390 deletions(-)
delete mode 100644 frontend/templates/blocks/error.gotmpl
delete mode 100644 frontend/templates/blocks/option.gotmpl
delete mode 100644 frontend/templates/blocks/options.gotmpl
delete mode 100644 frontend/templates/blocks/package.gotmpl
delete mode 100644 frontend/templates/blocks/packages.gotmpl
delete mode 100644 frontend/templates/blocks/results.gotmpl
delete mode 100644 frontend/templates/blocks/search.gotmpl
delete mode 100644 frontend/templates/index.gotmpl
create mode 100644 internal/components/data.go
create mode 100644 internal/components/detail.templ
create mode 100644 internal/components/error.templ
create mode 100644 internal/components/homepage.templ
create mode 100644 internal/components/markdown.templ
create mode 100644 internal/components/optionDetail.templ
create mode 100644 internal/components/options.templ
create mode 100644 internal/components/packageDetail.templ
create mode 100644 internal/components/packages.templ
create mode 100644 internal/components/page.templ
create mode 100644 internal/components/results.templ
create mode 100644 internal/components/search.templ
diff --git a/.editorconfig b/.editorconfig
index 34e76d0..d0afaf3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,7 +8,7 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
-[{justfile,go.mod,go.sum,*.go,.gitmodules}]
+[{justfile,go.mod,go.sum,*.go,*.templ,.gitmodules}]
indent_style = tab
[*.yaml]
diff --git a/.gitignore b/.gitignore
index 4953924..f92bc49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,6 @@ go.work
/frontend/static/base.css
/data/
/config.toml
+
+*_templ.go
+*_templ.txt
diff --git a/defaults.toml b/defaults.toml
index 66eb69d..f35b539 100644
--- a/defaults.toml
+++ b/defaults.toml
@@ -71,6 +71,8 @@ UpdateAt = '04:00:00'
[Importer.Sources.darwin]
# Human-readable name of source for generating links
Name = 'Darwin'
+# Order in which to show source in web interface.
+Order = 1
# Machine-readable name of source. Must be URL- and path-safe.
Key = 'darwin'
# Controls whether to show in the web interface and to run fetch/import jobs.
@@ -102,6 +104,8 @@ Repo = 'nix-darwin'
[Importer.Sources.home-manager]
# Human-readable name of source for generating links
Name = 'Home Manager'
+# Order in which to show source in web interface.
+Order = 2
# Machine-readable name of source. Must be URL- and path-safe.
Key = 'home-manager'
# Controls whether to show in the web interface and to run fetch/import jobs.
@@ -133,6 +137,8 @@ Repo = 'home-manager'
[Importer.Sources.nixos]
# Human-readable name of source for generating links
Name = 'NixOS'
+# Order in which to show source in web interface.
+Order = 0
# Machine-readable name of source. Must be URL- and path-safe.
Key = 'nixos'
# Controls whether to show in the web interface and to run fetch/import jobs.
@@ -164,6 +170,8 @@ Repo = 'nixpkgs'
[Importer.Sources.nixpkgs]
# Human-readable name of source for generating links
Name = 'Nix Packages'
+# Order in which to show source in web interface.
+Order = 3
# Machine-readable name of source. Must be URL- and path-safe.
Key = 'nixpkgs'
# Controls whether to show in the web interface and to run fetch/import jobs.
diff --git a/frontend/assets.go b/frontend/assets.go
index 46369fa..7a90d80 100644
--- a/frontend/assets.go
+++ b/frontend/assets.go
@@ -12,8 +12,8 @@ import (
)
var Assets = &AssetCollection{
- Scripts: make(map[string]*Asset),
- Stylesheets: make(map[string]*Asset),
+ Scripts: []*Asset{},
+ Stylesheets: []*Asset{},
ByPath: make(map[string]*Asset),
}
@@ -25,8 +25,8 @@ type Asset struct {
}
type AssetCollection struct {
- Scripts map[string]*Asset
- Stylesheets map[string]*Asset
+ Scripts []*Asset
+ Stylesheets []*Asset
ByPath map[string]*Asset
}
@@ -61,7 +61,7 @@ func hashScripts() error {
if err != nil {
return err
}
- Assets.Scripts[filename] = asset
+ Assets.Scripts = append(Assets.Scripts, asset)
Assets.ByPath[asset.URL] = asset
}
@@ -78,7 +78,7 @@ func hashStyles() error {
if err != nil {
return err
}
- Assets.Stylesheets[filename] = asset
+ Assets.Stylesheets = append(Assets.Stylesheets, asset)
Assets.ByPath[asset.URL] = asset
}
diff --git a/frontend/static/search.js b/frontend/static/search.js
index e3777d7..df11d09 100644
--- a/frontend/static/search.js
+++ b/frontend/static/search.js
@@ -6,8 +6,8 @@ const results = document.getElementById("results");
let pagination = document.getElementById("pagination");
const resultsRange = new Range();
-resultsRange.setStartBefore(results.firstChild);
-resultsRange.setEndAfter(results.lastChild);
+resultsRange.setStart(results, 0);
+resultsRange.setEnd(results, 0);
const detailsRange = new Range();
detailsRange.setStartAfter(dialog.firstElementChild);
diff --git a/frontend/static/style.css b/frontend/static/style.css
index a202b89..7cdc6c8 100644
--- a/frontend/static/style.css
+++ b/frontend/static/style.css
@@ -169,3 +169,14 @@ td,
th {
padding: 0.25rem 0.5rem;
}
+
+ul:only-child {
+ padding-inline-start: unset;
+ margin: unset;
+}
+
+li {
+ display: inline-block;
+ margin-right: 1ex;
+ list-style: none;
+}
diff --git a/frontend/templates/blocks/error.gotmpl b/frontend/templates/blocks/error.gotmpl
deleted file mode 100644
index 1352b2e..0000000
--- a/frontend/templates/blocks/error.gotmpl
+++ /dev/null
@@ -1,6 +0,0 @@
-{{- define "main" }}
-
- {{ .Code }}
- {{ .Message }}
-
-{{- end }}
diff --git a/frontend/templates/blocks/option.gotmpl b/frontend/templates/blocks/option.gotmpl
deleted file mode 100644
index 2248708..0000000
--- a/frontend/templates/blocks/option.gotmpl
+++ /dev/null
@@ -1,48 +0,0 @@
-{{- define "main" }}
- {{- with .Document }}
- {{ .Name }}
- {{ markdown .Description }}
-
- {{- with .Type }}
- - Type
- {{ . }}
- {{- end }}
- {{- with .Default }}
- {{- if or .Text .Markdown }}
- - Default
- -
- {{- if .Markdown }}
- {{ markdown .Markdown }}
- {{- else }}
-
{{ .Text }}
- {{- end }}
-
- {{- end }}
- {{- end }}
- {{- with .Example }}
- {{- if or .Text .Markdown }}
- - Example
- -
- {{- if .Markdown }}
- {{ markdown .Markdown }}
- {{- else }}
-
{{ .Text }}
- {{- end }}
-
- {{- end }}
- {{- end }}
- {{- with .RelatedPackages }}
- - Related Packages
- - {{ . }}
- {{- end }}
- {{- with .Declarations }}
- - Declared
- {{- range . }}
- -
- {{ .Name }}
-
- {{- end }}
- {{- end }}
-
- {{- end }}
-{{- end }}
diff --git a/frontend/templates/blocks/options.gotmpl b/frontend/templates/blocks/options.gotmpl
deleted file mode 100644
index 5a08bae..0000000
--- a/frontend/templates/blocks/options.gotmpl
+++ /dev/null
@@ -1,25 +0,0 @@
-{{- define "hits" }}
-
-
-
- Title |
- Description |
-
-
-
- {{- range . }}
- {{- with .Data }}
-
-
- {{ .Name }}
- |
-
- {{ markdown (firstSentence .Description) }}
-
- |
-
- {{- end }}
- {{- end }}
-
-
-{{- end }}
diff --git a/frontend/templates/blocks/package.gotmpl b/frontend/templates/blocks/package.gotmpl
deleted file mode 100644
index a42a8b1..0000000
--- a/frontend/templates/blocks/package.gotmpl
+++ /dev/null
@@ -1,69 +0,0 @@
-{{- define "main" }}
- {{- with .Document }}
-
- {{- if .Broken }}
- {{ .Attribute }}
- {{- else }}
- {{ .Attribute }}
- {{- end }}
-
- {{- if .LongDescription }}
- {{ markdown .LongDescription }}
- {{- else }}
- {{ .Description }}
- {{- end }}
-
- {{- with .MainProgram }}
- - Main Program
- -
-
{{ . }}
-
- {{- end }}
- {{- with .Homepages }}
- - Homepage
- -
- {{- range . }}
- {{ . }}
- {{- end }}
-
- {{- end }}
- {{- with .Version }}
- - Version
- - {{ . }}
- {{- end }}
- {{- with .Licenses }}
- - License
- -
- {{- range . }}
- {{- if .URL }}
- {{ or .FullName .Name }}
- {{- else }}
- {{ or .FullName .Name }}
- {{- end }}
- {{- with .AppendixURL }}
- Appendix
- {{- end }}
- {{- end }}
-
- {{- end }}
- {{- with .Maintainers }}
- - Maintainer{{ if gt (len .) 1 }}s{{ end }}
- -
- {{- range . }}
- {{- if .Github }}
- {{ .Name }}
- {{- else }}
- {{ .Name }}
- {{- end }}
- {{- end }}
-
- {{- end }}
- {{- with .Definition }}
- - Defined
- -
- Source
-
- {{- end }}
-
- {{- end }}
-{{- end }}
diff --git a/frontend/templates/blocks/packages.gotmpl b/frontend/templates/blocks/packages.gotmpl
deleted file mode 100644
index cce97a0..0000000
--- a/frontend/templates/blocks/packages.gotmpl
+++ /dev/null
@@ -1,30 +0,0 @@
-{{- define "hits" }}
-
-
-
- Attribute |
- Name |
- Description |
-
-
-
- {{- range . }}
- {{- with .Data }}
-
-
- {{- with .Attribute }}
- {{ . }}
- {{- end }}
- |
-
- {{ .Name }}
- |
-
- {{ .Description }}
- |
-
- {{- end }}
- {{- end }}
-
-
-{{- end }}
diff --git a/frontend/templates/blocks/results.gotmpl b/frontend/templates/blocks/results.gotmpl
deleted file mode 100644
index ef6e1f1..0000000
--- a/frontend/templates/blocks/results.gotmpl
+++ /dev/null
@@ -1,21 +0,0 @@
-{{- define "results" }}
- {{- with .Results }}
- {{- if gt .Total 0 }}
- {{ block "hits" .Hits }}
- {{ end }}
-
- {{- else }}
- Nothing found
- {{- end }}
- {{- end }}
-{{- end }}
diff --git a/frontend/templates/blocks/search.gotmpl b/frontend/templates/blocks/search.gotmpl
deleted file mode 100644
index 93ae545..0000000
--- a/frontend/templates/blocks/search.gotmpl
+++ /dev/null
@@ -1,37 +0,0 @@
-{{- define "main" }}
-
-
- {{- if .Results }}
- {{ block "results" . }}{{ end }}
- {{- end }}
-
-
-{{- end }}
-
-{{- define "head" }}
- {{- with (index .Assets.Scripts "static/search.js") }}
-
- {{- end }}
-{{- end }}
diff --git a/frontend/templates/index.gotmpl b/frontend/templates/index.gotmpl
deleted file mode 100644
index 7732dc8..0000000
--- a/frontend/templates/index.gotmpl
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
- Searchix
- {{- range .Assets.Stylesheets }}
-
- {{- end }}
- {{ block "head" . }}
- {{ end }}
- {{ .ExtraHeadHTML }}
- {{- range $key, $value := .Sources }}
-
- {{- end }}
-
-
-
-
- {{ block "main" . }}
-
- Search Nix Packages and options from NixOS, Darwin and Home-Manager
-
- Source code
- {{ end }}
-
-
-
-
diff --git a/go.mod b/go.mod
index c049e9e..d7d7807 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.22.2
require (
badc0de.net/pkg/flagutil v1.0.1
+ github.com/a-h/templ v0.2.707
github.com/andybalholm/brotli v1.1.0
github.com/bcicen/jstream v1.0.1
github.com/blevesearch/bleve/v2 v2.4.0
@@ -43,7 +44,6 @@ require (
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
- github.com/google/go-cmp v0.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
diff --git a/go.sum b/go.sum
index 110e7fd..8cdbe2e 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ badc0de.net/pkg/flagutil v1.0.1 h1:0ZgBzd3FehDUA8DJ70/phsnDH61/3aYMyx8Wd84KqQo=
badc0de.net/pkg/flagutil v1.0.1/go.mod h1:HwwkfbImu+u288bnLaYDGqBxkJzvqi5YzKofmgkMLvk=
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
+github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
+github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bcicen/jstream v1.0.1 h1:BXY7Cu4rdmc0rhyTVyT3UkxAiX3bnLpKLas9btbH5ck=
diff --git a/gomod2nix.toml b/gomod2nix.toml
index f433811..e9650a5 100644
--- a/gomod2nix.toml
+++ b/gomod2nix.toml
@@ -7,6 +7,9 @@ schema = 3
[mod."github.com/RoaringBitmap/roaring"]
version = "v1.9.4"
hash = "sha256-OKOLQ/PsH6630Vb5/9yG28TLIPGxdG9WDbAZxgK8EcI="
+ [mod."github.com/a-h/templ"]
+ version = "v0.2.707"
+ hash = "sha256-UoM2qj8E7C4NBAMhS/2jrOw0Dj/gnsyZRL4NpRCWaMo="
[mod."github.com/andybalholm/brotli"]
version = "v1.1.0"
hash = "sha256-njLViV4v++ZdgOWGWzlvkefuFvA/nkugl3Ta/h1nu/0="
@@ -88,9 +91,6 @@ schema = 3
[mod."github.com/golang/snappy"]
version = "v0.0.4"
hash = "sha256-Umx+5xHAQCN/Gi4HbtMhnDCSPFAXSsjVbXd8n5LhjAA="
- [mod."github.com/google/go-cmp"]
- version = "v0.6.0"
- hash = "sha256-qgra5jze4iPGP0JSTVeY5qV5AvEnEu39LYAuUCIkMtg="
[mod."github.com/json-iterator/go"]
version = "v1.1.12"
hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM="
diff --git a/internal/components/data.go b/internal/components/data.go
new file mode 100644
index 0000000..64caeaa
--- /dev/null
+++ b/internal/components/data.go
@@ -0,0 +1,37 @@
+package components
+
+import (
+ "searchix/frontend"
+ "searchix/internal/config"
+ search "searchix/internal/index"
+ "searchix/internal/nix"
+
+ "github.com/blevesearch/bleve/v2"
+)
+
+type TemplateData struct {
+ Sources []*config.Source
+ Source config.Source
+ Query string
+ Results bool
+ SourceResult *bleve.SearchResult
+ ExtraHeadHTML string
+ Code int
+ Message string
+ Assets *frontend.AssetCollection
+}
+
+type ResultData struct {
+ TemplateData
+ Query string
+ ResultsPerPage int
+ Results *search.Result
+ Prev string
+ Next string
+}
+
+type DocumentData struct {
+ TemplateData
+ Document *nix.Importable
+ Children *search.Result
+}
diff --git a/internal/components/detail.templ b/internal/components/detail.templ
new file mode 100644
index 0000000..6d6710c
--- /dev/null
+++ b/internal/components/detail.templ
@@ -0,0 +1,20 @@
+package components
+
+import (
+ "searchix/internal/nix"
+)
+
+templ Detail(thing nix.Importable) {
+ switch thing.(type) {
+ case nix.Option:
+ @OptionDetail(thing.(nix.Option))
+ case nix.Package:
+ @PackageDetail(thing.(nix.Package))
+ }
+}
+
+templ DetailPage(tdata TemplateData, thing nix.Importable) {
+ @Page(tdata) {
+ @Detail(thing)
+ }
+}
diff --git a/internal/components/error.templ b/internal/components/error.templ
new file mode 100644
index 0000000..8e45095
--- /dev/null
+++ b/internal/components/error.templ
@@ -0,0 +1,18 @@
+package components
+
+import (
+ "strconv"
+)
+
+templ Error(tdata TemplateData) {
+
+ { strconv.Itoa(tdata.Code) }
+ { tdata.Message }
+
+}
+
+templ ErrorPage(tdata TemplateData) {
+ @Page(tdata) {
+ @Error(tdata)
+ }
+}
diff --git a/internal/components/homepage.templ b/internal/components/homepage.templ
new file mode 100644
index 0000000..1cc2b9e
--- /dev/null
+++ b/internal/components/homepage.templ
@@ -0,0 +1,10 @@
+package components
+
+templ Homepage(tdata TemplateData) {
+ @Page(tdata) {
+
+ Search Nix Packages and options from NixOS, Darwin and Home-Manager
+
+ Source code
+ }
+}
diff --git a/internal/components/markdown.templ b/internal/components/markdown.templ
new file mode 100644
index 0000000..2a8787d
--- /dev/null
+++ b/internal/components/markdown.templ
@@ -0,0 +1,35 @@
+package components
+
+import (
+ "regexp"
+
+ "searchix/internal/nix"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ "context"
+ "io"
+)
+
+var (
+ md = goldmark.New(
+ goldmark.WithExtensions(extension.NewLinkify()),
+ )
+ firstSentenceRegexp = regexp.MustCompile(`^.*?\.[[:space:]]`)
+)
+
+func firstSentence[T ~string](text T) T {
+ if fs := firstSentenceRegexp.FindString(string(text)); fs != "" {
+ return T(fs)
+ }
+
+ return text
+}
+
+func markdown(text nix.Markdown) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+ err := md.Convert([]byte(text), w)
+
+ return err
+ })
+}
diff --git a/internal/components/optionDetail.templ b/internal/components/optionDetail.templ
new file mode 100644
index 0000000..52ce859
--- /dev/null
+++ b/internal/components/optionDetail.templ
@@ -0,0 +1,58 @@
+package components
+
+import "searchix/internal/nix"
+
+templ OptionDetail(option nix.Option) {
+ { option.Name }
+ @markdown(option.Description)
+
+ if option.Type != "" {
+ - Type
+ { option.Type }
+ }
+ if option.Default != nil {
+ if option.Default.Text != "" || option.Default.Markdown != "" {
+ - Default
+ -
+ if option.Default.Markdown != "" {
+ @markdown(option.Default.Markdown)
+ } else {
+
{ option.Default.Text }
+ }
+
+ }
+ }
+ if option.Example != nil {
+ if option.Example.Text != "" || option.Example.Markdown != "" {
+ - Example
+ -
+ if option.Example.Markdown != "" {
+ @markdown(option.Example.Markdown)
+ } else {
+
{ option.Example.Text }
+ }
+
+ }
+ }
+ if option.RelatedPackages != "" {
+ - Related Packages
+ -
+ @markdown(option.RelatedPackages)
+
+ }
+ if len(option.Declarations) > 0 {
+ - Declared
+ for _, d := range option.Declarations {
+ -
+ { d.Name }
+
+ }
+ }
+
+}
+
+templ OptionDetailPage(tdata TemplateData, option nix.Option) {
+ @Page(tdata) {
+ @OptionDetail(option)
+ }
+}
diff --git a/internal/components/options.templ b/internal/components/options.templ
new file mode 100644
index 0000000..726d328
--- /dev/null
+++ b/internal/components/options.templ
@@ -0,0 +1,34 @@
+package components
+
+import (
+ "searchix/internal/index"
+ "searchix/internal/nix"
+)
+
+templ Options(result *index.Result) {
+
+
+
+ Title |
+ Description |
+
+
+
+ for _, hit := range result.Hits {
+ @optionRow(hit.Data.(nix.Option))
+ }
+
+
+}
+
+templ optionRow(o nix.Option) {
+
+
+ @openDialogLink(o.Name)
+ |
+
+ @markdown(firstSentence(o.Description))
+
+ |
+
+}
diff --git a/internal/components/packageDetail.templ b/internal/components/packageDetail.templ
new file mode 100644
index 0000000..7b4a5cb
--- /dev/null
+++ b/internal/components/packageDetail.templ
@@ -0,0 +1,99 @@
+package components
+
+import (
+ "searchix/internal/nix"
+)
+
+func licenseName(l nix.License) string {
+ if l.FullName != "" {
+ return l.FullName
+ } else {
+ return l.Name
+ }
+}
+
+templ PackageDetail(pkg nix.Package) {
+
+ if pkg.Broken {
+ { pkg.Attribute }
+ } else {
+ { pkg.Attribute }
+ }
+
+ if pkg.LongDescription != "" {
+ @markdown(pkg.LongDescription)
+ } else {
+ { pkg.Description }
+ }
+
+ if pkg.MainProgram != "" {
+ - Main Program
+ -
+
{ pkg.MainProgram }
+
+ }
+ if len(pkg.Homepages) > 0 {
+ - Homepage
+ -
+
+ for _, u := range pkg.Homepages {
+ -
+ { u }
+
+ }
+
+
+ }
+ if pkg.Version != "" {
+ - Version
+ - { pkg.Version }
+ }
+ if len(pkg.Licenses) > 0 {
+ - License
+ -
+
+ for _, l := range pkg.Licenses {
+ -
+ if l.URL != "" {
+ { licenseName(l) }
+ } else {
+ { licenseName(l) }
+ }
+ if l.AppendixURL != "" {
+ Appendix
+ }
+
+ }
+
+
+ }
+ if len(pkg.Maintainers) > 0 {
+ - Maintainers
+ -
+
+ for _, m := range pkg.Maintainers {
+ -
+ if m.Github != "" {
+ { m.Name }
+ } else {
+ { m.Name }
+ }
+
+ }
+
+
+ }
+ if pkg.Definition != "" {
+ - Defined
+ -
+ Source
+
+ }
+
+}
+
+templ PackageDetailPage(tdata TemplateData, pkg nix.Package) {
+ @Page(tdata) {
+ @PackageDetail(pkg)
+ }
+}
diff --git a/internal/components/packages.templ b/internal/components/packages.templ
new file mode 100644
index 0000000..4e00a5a
--- /dev/null
+++ b/internal/components/packages.templ
@@ -0,0 +1,37 @@
+package components
+
+import (
+ "searchix/internal/index"
+ "searchix/internal/nix"
+)
+
+templ Packages(result *index.Result) {
+
+
+
+ Attribute |
+ Name |
+ Description |
+
+
+
+ for _, hit := range result.Hits {
+ @packageRow(hit.Data.(nix.Package))
+ }
+
+
+}
+
+templ packageRow(p nix.Package) {
+
+
+ @openDialogLink(p.Attribute)
+ |
+
+ { p.Name }
+ |
+
+ { p.Description }
+ |
+
+}
diff --git a/internal/components/page.templ b/internal/components/page.templ
new file mode 100644
index 0000000..9b278e2
--- /dev/null
+++ b/internal/components/page.templ
@@ -0,0 +1,91 @@
+package components
+
+import (
+ "net/url"
+
+ "searchix/internal/config"
+ "searchix/frontend"
+)
+
+templ Page(tdata TemplateData) {
+
+
+
+
+
+ Searchix
+ for _, sheet := range tdata.Assets.Stylesheets {
+
+ }
+ @Unsafe(tdata.ExtraHeadHTML)
+ for _, source := range tdata.Sources {
+
+ }
+
+
+
+
+ { children... }
+
+
+
+
+}
+
+templ script(s *frontend.Asset) {
+
+}
+
+func Unsafe(html string) templ.Component {
+ return templ.ComponentFunc(func(_ context.Context, w io.Writer) (err error) {
+ _, err = io.WriteString(w, html)
+ return
+ })
+}
+
+func sourceNameAndType(source config.Source) string {
+ switch source.Importer {
+ case config.Options:
+ return source.Name + " " + source.Importer.String()
+ case config.Packages:
+ return source.Name
+ }
+ return ""
+}
+
+func joinPath(base string, parts ...string) templ.SafeURL {
+ u, err := url.JoinPath(base, parts...)
+ if err != nil {
+ panic(err)
+ }
+ return templ.SafeURL(u)
+}
+
+func joinPathQuery[T ~string](path T, query string) templ.SafeURL {
+ if query == "" {
+ return templ.SafeURL(path)
+ }
+ return templ.SafeURL(string(path) + "?query=" + url.QueryEscape(query))
+}
diff --git a/internal/components/results.templ b/internal/components/results.templ
new file mode 100644
index 0000000..3953cc3
--- /dev/null
+++ b/internal/components/results.templ
@@ -0,0 +1,44 @@
+package components
+
+import (
+ "strconv"
+ "searchix/internal/nix"
+)
+
+templ Results(r ResultData) {
+ if r.Query != "" {
+ if r.Results != nil && r.Results.Total > 0 {
+ switch r.Results.Hits[0].Data.(type) {
+ case nix.Option:
+ @Options(r.Results)
+ case nix.Package:
+ @Packages(r.Results)
+ }
+
+ } else {
+ Nothing found
+ }
+ } else {
+
+ }
+}
+
+templ ResultsPage(r ResultData) {
+ @SearchPage(r.TemplateData, r) {
+ @Results(r)
+ }
+}
+
+templ openDialogLink(attr string) {
+ { attr }
+}
diff --git a/internal/components/search.templ b/internal/components/search.templ
new file mode 100644
index 0000000..2cae754
--- /dev/null
+++ b/internal/components/search.templ
@@ -0,0 +1,34 @@
+package components
+
+templ Search(tdata TemplateData, r ResultData) {
+
+}
+
+templ SearchPage(tdata TemplateData, r ResultData) {
+ @Page(tdata) {
+ @script(tdata.Assets.ByPath["/static/search.js"])
+ @Search(tdata, r)
+
+
+ }
+}
diff --git a/internal/config/default.go b/internal/config/default.go
index 5b924a9..9a0c670 100644
--- a/internal/config/default.go
+++ b/internal/config/default.go
@@ -53,6 +53,7 @@ var DefaultConfig = Config{
Sources: map[string]*Source{
"nixos": {
Name: "NixOS",
+ Order: 0,
Key: "nixos",
Enable: true,
Importer: Options,
@@ -67,6 +68,7 @@ var DefaultConfig = Config{
},
"darwin": {
Name: "Darwin",
+ Order: 1,
Key: "darwin",
Enable: false,
Importer: Options,
@@ -85,6 +87,7 @@ var DefaultConfig = Config{
},
"home-manager": {
Name: "Home Manager",
+ Order: 2,
Key: "home-manager",
Enable: false,
Importer: Options,
@@ -103,6 +106,7 @@ var DefaultConfig = Config{
},
"nixpkgs": {
Name: "Nix Packages",
+ Order: 3,
Key: "nixpkgs",
Enable: true,
Importer: Packages,
diff --git a/internal/config/structs.go b/internal/config/structs.go
index 70283f2..6c6bc13 100644
--- a/internal/config/structs.go
+++ b/internal/config/structs.go
@@ -4,7 +4,6 @@ package config
// keep config structs here so that lll ignores the long lines (go doesn't support multi-line struct tags)
import (
- "html/template"
"log/slog"
)
@@ -22,7 +21,7 @@ type Web struct {
BaseURL URL `comment:"Absolute URL to this instance, useful if behind a reverse proxy"`
SentryDSN string `comment:"If set, will send server errors to Sentry"`
Environment string `comment:"Affects logging parameters. One of 'development' or 'production'"`
- ExtraHeadHTML template.HTML `comment:"Content to add to HTML . Can be used to override styling, add scripts, etc."`
+ ExtraHeadHTML string `comment:"Content to add to HTML . Can be used to override styling, add scripts, etc."`
Headers map[string]string `comment:"Extra headers to send with HTTP requests"`
}
@@ -35,6 +34,7 @@ type Importer struct {
type Source struct {
Name string `comment:"Human-readable name of source for generating links"`
+ Order uint `comment:"Order in which to show source in web interface."`
Key string `comment:"Machine-readable name of source. Must be URL- and path-safe."`
Enable bool `comment:"Controls whether to show in the web interface and to run fetch/import jobs."`
Fetcher Fetcher `comment:"How to fetch options.json. One of 'channel', 'channel-nixpkgs' or 'download'."`
diff --git a/internal/server/error.go b/internal/server/error.go
index e700d3b..4a8acbc 100644
--- a/internal/server/error.go
+++ b/internal/server/error.go
@@ -3,6 +3,8 @@ package server
import (
"log/slog"
"net/http"
+
+ "searchix/internal/components"
"searchix/internal/config"
)
@@ -14,9 +16,9 @@ func createErrorHandler(
if message == "" {
message = http.StatusText(code)
}
- indexData := TemplateData{
+ indexData := components.TemplateData{
ExtraHeadHTML: config.Web.ExtraHeadHTML,
- Sources: config.Importer.Sources,
+ Sources: sources,
Code: code,
Message: message,
}
@@ -24,9 +26,9 @@ func createErrorHandler(
w.Header().Del("Vary")
w.WriteHeader(code)
if r.Header.Get("Fetch") == "true" {
- err = templates["error"].ExecuteTemplate(w, "main", indexData)
+ err = components.Error(indexData).Render(r.Context(), w)
} else {
- err = templates["error"].Execute(w, indexData)
+ err = components.ErrorPage(indexData).Render(r.Context(), w)
}
if err != nil {
slog.Error(
diff --git a/internal/server/mux.go b/internal/server/mux.go
index 79e24cd..89ce952 100644
--- a/internal/server/mux.go
+++ b/internal/server/mux.go
@@ -3,7 +3,6 @@ package server
import (
"context"
"fmt"
- "html/template"
"io"
"log"
"log/slog"
@@ -16,11 +15,10 @@ import (
"time"
"searchix/frontend"
+ "searchix/internal/components"
"searchix/internal/config"
search "searchix/internal/index"
- "searchix/internal/nix"
- "github.com/blevesearch/bleve/v2"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/osdevisnot/sorvor/pkg/livereload"
"github.com/pkg/errors"
@@ -33,36 +31,10 @@ type HTTPError struct {
Code int
}
-const jsSnippet = template.HTML(livereload.JsSnippet) // #nosec G203
-
-type TemplateData struct {
- Sources map[string]*config.Source
- Source config.Source
- Query string
- Results bool
- SourceResult *bleve.SearchResult
- ExtraHeadHTML template.HTML
- Code int
- Message string
- Assets *frontend.AssetCollection
-}
-
-type ResultData struct {
- TemplateData
- Query string
- ResultsPerPage int
- Results *search.Result
- Prev string
- Next string
-}
-
-type DocumentData struct {
- TemplateData
- Document *nix.Importable
- Children *search.Result
-}
-
-var templates TemplateCollection
+var (
+ templates TemplateCollection
+ sources []*config.Source
+)
func applyDevModeOverrides(cfg *config.Config) {
if len(cfg.Web.ContentSecurityPolicy.ScriptSrc) == 0 {
@@ -70,10 +42,18 @@ func applyDevModeOverrides(cfg *config.Config) {
}
cfg.Web.ContentSecurityPolicy.ScriptSrc = append(
cfg.Web.ContentSecurityPolicy.ScriptSrc,
+ "http://localhost:7331",
"'unsafe-inline'",
)
}
+func sortSources(ss map[string]*config.Source) {
+ sources = make([]*config.Source, len(ss))
+ for _, v := range ss {
+ sources[v.Order] = v
+ }
+}
+
func NewMux(
cfg *config.Config,
index *search.ReadIndex,
@@ -93,19 +73,20 @@ func NewMux(
if err != nil {
log.Panicf("could not load templates: %v", err)
}
+ sortSources(cfg.Importer.Sources)
errorHandler := createErrorHandler(cfg)
top := http.NewServeMux()
mux := http.NewServeMux()
mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
- indexData := TemplateData{
+ indexData := components.TemplateData{
ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
- Sources: cfg.Importer.Sources,
+ Sources: sources,
Assets: frontend.Assets,
}
w.Header().Add("Cache-Control", "max-age=86400")
- err := templates["index"].Execute(w, indexData)
+ err := components.Homepage(indexData).Render(r.Context(), w)
if err != nil {
errorHandler(w, r, err.Error(), http.StatusInternalServerError)
}
@@ -146,12 +127,13 @@ func NewMux(
errorHandler(w, r, err.Error(), http.StatusInternalServerError)
}
- tdata := ResultData{
- TemplateData: TemplateData{
+ tdata := components.ResultData{
+ TemplateData: components.TemplateData{
ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
Source: *source,
- Sources: cfg.Importer.Sources,
+ Sources: sources,
Assets: frontend.Assets,
+ Query: qs,
},
ResultsPerPage: search.ResultsPerPage,
Query: qs,
@@ -193,9 +175,9 @@ func NewMux(
w.Header().Add("Vary", "Fetch")
if r.Header.Get("Fetch") == "true" {
w.Header().Add("Content-Type", "text/html; charset=utf-8")
- err = templates[importerType.String()].ExecuteTemplate(w, "results", tdata)
+ err = components.Results(tdata).Render(r.Context(), w)
} else {
- err = templates[importerType.String()].Execute(w, tdata)
+ err = components.ResultsPage(tdata).Render(r.Context(), w)
}
if err != nil {
slog.Error("template error", "template", importerType, "error", err)
@@ -210,13 +192,16 @@ func NewMux(
}
w.Header().Add("Cache-Control", "max-age=14400")
- err = templates["search"].Execute(w, TemplateData{
- ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
- Sources: cfg.Importer.Sources,
- Source: *source,
- SourceResult: sourceResult,
- Assets: frontend.Assets,
- })
+ err = components.SearchPage(
+ components.TemplateData{
+ ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
+ Sources: sources,
+ Source: *source,
+ SourceResult: sourceResult,
+ Assets: frontend.Assets,
+ },
+ components.ResultData{},
+ ).Render(r.Context(), w)
if err != nil {
errorHandler(w, r, err.Error(), http.StatusInternalServerError)
@@ -260,20 +245,20 @@ func NewMux(
return
}
- tdata := DocumentData{
- TemplateData: TemplateData{
+ tdata := components.DocumentData{
+ TemplateData: components.TemplateData{
ExtraHeadHTML: cfg.Web.ExtraHeadHTML,
Source: *source,
- Sources: cfg.Importer.Sources,
+ Sources: 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)
+ err = components.Detail(*doc).Render(r.Context(), w)
} else {
- err = templates[importerSingular].Execute(w, tdata)
+ err = components.DetailPage(tdata.TemplateData, *doc).Render(r.Context(), w)
}
if err != nil {
slog.Error("template error", "template", importerSingular, "error", err)
@@ -337,7 +322,7 @@ func NewMux(
if liveReload {
applyDevModeOverrides(cfg)
- cfg.Web.ExtraHeadHTML = jsSnippet
+ cfg.Web.ExtraHeadHTML = livereload.JsSnippet
liveReload := livereload.New()
liveReload.Start()
top.Handle("/livereload", liveReload)
diff --git a/internal/server/templates.go b/internal/server/templates.go
index 38ff5d4..fa95425 100644
--- a/internal/server/templates.go
+++ b/internal/server/templates.go
@@ -95,21 +95,13 @@ func loadTemplates() (TemplateCollection, error) {
templateDir := "templates"
templates := make(TemplateCollection, 0)
- layoutFile := path.Join(templateDir, "index.gotmpl")
-
- index, err := loadTemplate(layoutFile)
- if err != nil {
- return nil, err
- }
- templates["index"] = index
-
glob := path.Join(templateDir, "*.gotmpl")
templatePaths, err := fs.Glob(frontend.Files, glob)
if err != nil {
return nil, errors.WithMessage(err, "could not glob main templates")
}
for _, fullname := range templatePaths {
- tpl, err := loadTemplate(layoutFile, fullname)
+ tpl, err := loadTemplate(fullname)
if err != nil {
return nil, err
}
@@ -117,20 +109,5 @@ func loadTemplates() (TemplateCollection, error) {
templates[name] = tpl
}
- glob = path.Join(templateDir, "blocks", "*.gotmpl")
- templatePaths, err = fs.Glob(frontend.Files, glob)
- if err != nil {
- return nil, errors.WithMessage(err, "could not glob block templates")
- }
- for _, fullname := range templatePaths {
- tpl, err := loadTemplate(layoutFile, glob, fullname)
- if err != nil {
- return nil, err
- }
-
- name, _ := strings.CutSuffix(path.Base(fullname), ".gotmpl")
- templates[name] = tpl
- }
-
return templates, nil
}
diff --git a/modd.conf b/modd.conf
index 8fced57..ab01682 100644
--- a/modd.conf
+++ b/modd.conf
@@ -1,3 +1,4 @@
-**/*.go config.toml {
- daemon: wgo run -exit ./cmd/searchix-web --live --config config.toml
+**/*.go !**/*_templ.go config.toml {
+ daemon +sigint: templ generate --watch --proxy="http://localhost:3000" --open-browser=false \
+ --cmd="go run ./cmd/searchix-web --live --config config.toml"
}
diff --git a/nix/dev-shell.nix b/nix/dev-shell.nix
index 58bf467..eb2021f 100644
--- a/nix/dev-shell.nix
+++ b/nix/dev-shell.nix
@@ -11,6 +11,7 @@ mkShell {
packages = with pkgs; [
goEnv
+ templ
modd
brotli
bleve
diff --git a/nix/package.nix b/nix/package.nix
index bb5a255..068e5e1 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -37,6 +37,7 @@ buildGoApplication {
patchPhase = ''
rm -f frontend/static/base.css
cp ${css} frontend/static/base.css
+ ${pkgs.templ}/bin/templ generate
'';
tags = [ "embed" ];
ldflags = [
--
cgit 1.4.1