From 896d844cac976afd0ee8aa73dd2fb28e15e7ac79 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 18 Mar 2025 22:40:46 +0100 Subject: feat: Convert templ components to gomponents --- .editorconfig | 2 +- .gitignore | 3 - .golangci.yaml | 5 ++ go.mod | 3 +- go.sum | 4 +- gomod2nix.toml | 9 +- internal/components/combined.go | 56 ++++++++++++ internal/components/combined.templ | 47 ---------- internal/components/data.go | 10 +++ internal/components/detail.go | 22 +++++ internal/components/detail.templ | 20 ----- internal/components/dev.go | 23 +++++ internal/components/dev.templ | 18 ---- internal/components/error.go | 17 ++++ internal/components/error.templ | 18 ---- internal/components/markdown.go | 15 ++++ internal/components/markdown.templ | 33 ------- internal/components/optionDetail.go | 71 +++++++++++++++ internal/components/optionDetail.templ | 58 ------------ internal/components/options.go | 50 +++++++++++ internal/components/options.templ | 45 ---------- internal/components/packageDetail.go | 120 +++++++++++++++++++++++++ internal/components/packageDetail.templ | 109 ----------------------- internal/components/packages.go | 53 +++++++++++ internal/components/packages.templ | 48 ---------- internal/components/page.go | 152 ++++++++++++++++++++++++++++++++ internal/components/page.templ | 123 -------------------------- internal/components/results.go | 65 ++++++++++++++ internal/components/results.templ | 64 -------------- internal/components/search.go | 63 +++++++++++++ internal/components/search.templ | 44 --------- internal/nix/option.go | 17 ++++ internal/server/error.go | 4 +- internal/server/mux.go | 12 ++- modd.conf | 5 +- nix/dev-shell.nix | 1 - nix/package.nix | 1 - nix/pre-commit-checks.nix | 36 +------- staticcheck.conf | 1 + 39 files changed, 764 insertions(+), 683 deletions(-) create mode 100644 internal/components/combined.go delete mode 100644 internal/components/combined.templ create mode 100644 internal/components/detail.go delete mode 100644 internal/components/detail.templ create mode 100644 internal/components/dev.go delete mode 100644 internal/components/dev.templ create mode 100644 internal/components/error.go delete mode 100644 internal/components/error.templ create mode 100644 internal/components/markdown.go delete mode 100644 internal/components/markdown.templ create mode 100644 internal/components/optionDetail.go delete mode 100644 internal/components/optionDetail.templ create mode 100644 internal/components/options.go delete mode 100644 internal/components/options.templ create mode 100644 internal/components/packageDetail.go delete mode 100644 internal/components/packageDetail.templ create mode 100644 internal/components/packages.go delete mode 100644 internal/components/packages.templ create mode 100644 internal/components/page.go delete mode 100644 internal/components/page.templ create mode 100644 internal/components/results.go delete mode 100644 internal/components/results.templ create mode 100644 internal/components/search.go delete mode 100644 internal/components/search.templ create mode 100644 staticcheck.conf diff --git a/.editorconfig b/.editorconfig index d0afaf3..34e76d0 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,*.templ,.gitmodules}] +[{justfile,go.mod,go.sum,*.go,.gitmodules}] indent_style = tab [*.yaml] diff --git a/.gitignore b/.gitignore index f92bc49..4953924 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,3 @@ go.work /frontend/static/base.css /data/ /config.toml - -*_templ.go -*_templ.txt diff --git a/.golangci.yaml b/.golangci.yaml index df90b0a..e44d5d8 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -36,3 +36,8 @@ linters-settings: - .WithMessagef( - .WithStack( - (context.Context).Err( +issues: + exclude-rules: + - linters: [revive] + path: internal/components + text: "dot-imports" diff --git a/go.mod b/go.mod index 106a618..eb4fc3f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.23.6 require ( badc0de.net/pkg/flagutil v1.0.1 - github.com/a-h/templ v0.3.833 github.com/andybalholm/brotli v1.1.1 github.com/bcicen/jstream v1.0.1 github.com/blevesearch/bleve/v2 v2.4.4 @@ -18,6 +17,7 @@ require ( github.com/stoewer/go-strcase v1.3.0 github.com/yuin/goldmark v1.7.8 gitlab.com/tozd/go/errors v0.10.0 + go.alanpearce.eu/gomponents v1.2.0 go.alanpearce.eu/x v0.0.0-20241203124832-a29434dba11a go.uber.org/zap v1.27.0 golang.org/x/net v0.33.0 @@ -65,6 +65,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.24.0 // indirect google.golang.org/protobuf v1.36.0 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/libc v1.55.3 // indirect diff --git a/go.sum b/go.sum index 60f4bba..54fa61a 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/Code-Hex/dd v1.1.0 h1:VEtTThnS9l7WhpKUIpdcWaf0B8Vp0LeeSEsxA1DZseI= github.com/Code-Hex/dd v1.1.0/go.mod h1:VaMyo/YjTJ3d4qm/bgtrUkT2w+aYwJ07Y7eCWyrJr1w= 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.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= -github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bcicen/jstream v1.0.1 h1:BXY7Cu4rdmc0rhyTVyT3UkxAiX3bnLpKLas9btbH5ck= @@ -142,6 +140,8 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= gitlab.com/tozd/go/errors v0.10.0 h1:A98kL+gaDvWnY6ZB/u8zP+sYaWsWUGBHeFMtamvW/74= gitlab.com/tozd/go/errors v0.10.0/go.mod h1:q3Ugr0C8dCzMEkrzjjlV2qNsm9e0KvqBjwcbcjCpBe4= +go.alanpearce.eu/gomponents v1.2.0 h1:5SoLlMMc04xvLcmHVgnScjX1DzBM4mbwyTDa0cOPiD8= +go.alanpearce.eu/gomponents v1.2.0/go.mod h1:uX96UAsHCut1cKMAYVWWxQ9ADt1CAPI8LpyAu0LRQPs= go.alanpearce.eu/x v0.0.0-20241203124832-a29434dba11a h1:NUv3AzGxwMVSq26takww8/nyl+sPO2BsESoVSU8G49U= go.alanpearce.eu/x v0.0.0-20241203124832-a29434dba11a/go.mod h1:FRM6J9HMQ/RV2Q5j+6RKBYWh/YNeEUriGSqDRchiHuQ= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= diff --git a/gomod2nix.toml b/gomod2nix.toml index 0f6e067..9079679 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -10,9 +10,6 @@ schema = 3 [mod."github.com/RoaringBitmap/roaring"] version = "v1.9.4" hash = "sha256-OKOLQ/PsH6630Vb5/9yG28TLIPGxdG9WDbAZxgK8EcI=" - [mod."github.com/a-h/templ"] - version = "v0.3.833" - hash = "sha256-OSGAJDVPYqgb72DZXeemLp37aGrrxMt+PQNKW5voKQ0=" [mod."github.com/andybalholm/brotli"] version = "v1.1.1" hash = "sha256-kCt+irK1gvz2lGQUeEolYa5+FbLsfWlJMCd5hm+RPgQ=" @@ -154,6 +151,9 @@ schema = 3 [mod."gitlab.com/tozd/go/errors"] version = "v0.10.0" hash = "sha256-oW37KsieVKJOWk9ZXbGuQvuU4nyJCZzgYrTZHFkoCs4=" + [mod."go.alanpearce.eu/gomponents"] + version = "v1.2.0" + hash = "sha256-pF+3We63loSMwhTUafgIdmBYc4cj5yVIVQRyaX1sWB4=" [mod."go.alanpearce.eu/x"] version = "v0.0.0-20241203124832-a29434dba11a" hash = "sha256-ojqWkz3VqeAOevFxOTO5S3acRItCA4pUrTaul887+x8=" @@ -175,6 +175,9 @@ schema = 3 [mod."golang.org/x/text"] version = "v0.21.0" hash = "sha256-QaMwddBRnoS2mv9Y86eVC2x2wx/GZ7kr2zAJvwDeCPc=" + [mod."golang.org/x/tools"] + version = "v0.24.0" + hash = "sha256-2LBEW//aW8qrHc26F6Ma7CsYJRaCALfi0xQl2KgWems=" [mod."google.golang.org/protobuf"] version = "v1.36.0" hash = "sha256-7LfDxB2/x6sSzJdQ3sixWMaY0WZ/juwuz3rPBJxNzXQ=" diff --git a/internal/components/combined.go b/internal/components/combined.go new file mode 100644 index 0000000..d8c6fea --- /dev/null +++ b/internal/components/combined.go @@ -0,0 +1,56 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/config" + "go.alanpearce.eu/searchix/internal/index" + "go.alanpearce.eu/searchix/internal/nix" + + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func CombinedData(data nix.Importable) g.Node { + switch data.(type) { + case nix.Option: + if o := convertMatch[nix.Option](data); o != nil { + return firstSentence(o.Description) + } + case nix.Package: + if p := convertMatch[nix.Package](data); p != nil { + return g.Text(firstSentence(p.Description)) + } + } + + return g.Text("") +} + +func Combined(result *index.Result) g.Node { + return Table( + THead( + Tr( + Th(Scope("col"), g.Text("Attribute")), + Th(Scope("col"), g.Text("Description")), + g.If(config.DevMode, + Th(Scope("col"), g.Text("Score")), + ), + ), + ), + TBody( + g.Map(result.Hits, func(hit index.DocumentMatch) g.Node { + return Tr( + Td( + openCombinedDialogLink(nix.GetKey(hit.Data)), + ), + Td( + CombinedData(hit.Data), + ), + g.If(config.DevMode, + Td( + Score(hit), + ), + ), + ) + }), + ), + ) +} diff --git a/internal/components/combined.templ b/internal/components/combined.templ deleted file mode 100644 index 3beddcd..0000000 --- a/internal/components/combined.templ +++ /dev/null @@ -1,47 +0,0 @@ -package components - -import ( - "go.alanpearce.eu/searchix/internal/config" - "go.alanpearce.eu/searchix/internal/index" - "go.alanpearce.eu/searchix/internal/nix" -) - -templ Combined(result *index.Result) { - - - - - - if config.DevMode { - - } - - - - for _, hit := range result.Hits { - - - - if config.DevMode { - - } - - } - -
AttributeDescriptionScore
- @openCombinedDialogLink(nix.GetKey(hit.Data)) - - switch hit.Data.(type) { - case nix.Option: - if o := convertMatch[nix.Option](hit.Data); o != nil { - @markdown(firstSentence(o.Description)) - } - case nix.Package: - if o := convertMatch[nix.Package](hit.Data); o != nil { - { firstSentence(o.Description) } - } - } - - @score(hit) -
-} diff --git a/internal/components/data.go b/internal/components/data.go index c34dfb8..977b90e 100644 --- a/internal/components/data.go +++ b/internal/components/data.go @@ -4,6 +4,7 @@ import ( "go.alanpearce.eu/searchix/frontend" "go.alanpearce.eu/searchix/internal/config" search "go.alanpearce.eu/searchix/internal/index" + "go.alanpearce.eu/searchix/internal/nix" ) type TemplateData struct { @@ -24,3 +25,12 @@ type ResultData struct { Next string All string } + +func convertMatch[I nix.Importable](m nix.Importable) *I { + i, ok := m.(I) + if !ok { + return nil + } + + return &i +} diff --git a/internal/components/detail.go b/internal/components/detail.go new file mode 100644 index 0000000..e6164fa --- /dev/null +++ b/internal/components/detail.go @@ -0,0 +1,22 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/nix" + + g "go.alanpearce.eu/gomponents" +) + +func Detail(thing nix.Importable) g.Node { + switch t := thing.(type) { + case nix.Option: + return OptionDetail(t) + case nix.Package: + return PackageDetail(t) + default: + return nil + } +} + +func DetailPage(tdata TemplateData, thing nix.Importable) g.Node { + return Page(tdata, Detail(thing)) +} diff --git a/internal/components/detail.templ b/internal/components/detail.templ deleted file mode 100644 index fa7206c..0000000 --- a/internal/components/detail.templ +++ /dev/null @@ -1,20 +0,0 @@ -package components - -import ( - "go.alanpearce.eu/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/dev.go b/internal/components/dev.go new file mode 100644 index 0000000..3d77094 --- /dev/null +++ b/internal/components/dev.go @@ -0,0 +1,23 @@ +package components + +import ( + "strconv" + + "go.alanpearce.eu/searchix/internal/index" + + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func Score(h index.DocumentMatch) g.Node { + return g.Group([]g.Node{ + A( + Class("open-sibling-dialog"), + g.Text(strconv.FormatFloat(h.Score, 'f', 2, 64)), + ), + Dialog( + Button(AutoFocus(), g.Text("Close")), + Pre(g.Text(h.Expl.String())), + ), + }) +} diff --git a/internal/components/dev.templ b/internal/components/dev.templ deleted file mode 100644 index a03eb4b..0000000 --- a/internal/components/dev.templ +++ /dev/null @@ -1,18 +0,0 @@ -package components - -import ( - "go.alanpearce.eu/searchix/internal/index" - "strconv" -) - -templ score(h index.DocumentMatch) { - - { strconv.FormatFloat(h.Score, 'f', 2, 64) } - - - -
-			{ h.Expl.String() }
-		
-
-} diff --git a/internal/components/error.go b/internal/components/error.go new file mode 100644 index 0000000..fe34919 --- /dev/null +++ b/internal/components/error.go @@ -0,0 +1,17 @@ +package components + +import ( + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func Error(tdata TemplateData) g.Node { + return P( + Class("notice error"), + g.Textf("%d %s", tdata.Code, tdata.Message), + ) +} + +func ErrorPage(tdata TemplateData) g.Node { + return Page(tdata, Error(tdata)) +} diff --git a/internal/components/error.templ b/internal/components/error.templ deleted file mode 100644 index 8e45095..0000000 --- a/internal/components/error.templ +++ /dev/null @@ -1,18 +0,0 @@ -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/markdown.go b/internal/components/markdown.go new file mode 100644 index 0000000..405ab52 --- /dev/null +++ b/internal/components/markdown.go @@ -0,0 +1,15 @@ +package components + +import ( + "regexp" +) + +var firstSentenceRegexp = regexp.MustCompile(`^.*?\.[[:space:]]`) + +func firstSentence[T ~string](text T) T { + if fs := firstSentenceRegexp.FindString(string(text)); fs != "" { + return T(fs) + } + + return text +} diff --git a/internal/components/markdown.templ b/internal/components/markdown.templ deleted file mode 100644 index 21b0aa0..0000000 --- a/internal/components/markdown.templ +++ /dev/null @@ -1,33 +0,0 @@ -package components - -import ( - "context" - "io" - "regexp" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/extension" - - "go.alanpearce.eu/searchix/internal/nix" -) - -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 { - return md.Convert([]byte(text), w) - }) -} diff --git a/internal/components/optionDetail.go b/internal/components/optionDetail.go new file mode 100644 index 0000000..d5f0c24 --- /dev/null +++ b/internal/components/optionDetail.go @@ -0,0 +1,71 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/nix" + + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func OptionDetail(option nix.Option) g.Node { + return g.Group([]g.Node{ + H2(g.Text(option.Name)), + option.Description, + Dl( + g.If(option.Type != "", + g.Group([]g.Node{ + Dt(g.Text("Type")), + Dd(Code(g.Text(option.Type))), + }), + ), + g.Iff(option.Default != nil, + func() g.Node { + return g.Group([]g.Node{ + Dt(g.Text("Default")), + Dd( + g.If(option.Default.Markdown != "", + option.Default.Markdown, + Pre(Code(g.Text(option.Default.Text))), + ), + ), + }) + }, + ), + g.Iff(option.Example != nil, + func() g.Node { + return g.Group([]g.Node{ + Dt(g.Text("Example")), + Dd( + g.If(option.Example.Markdown != "", + option.Example.Markdown, + Pre(Code(g.Text(option.Example.Text))), + ), + ), + }) + }, + ), + g.If(option.RelatedPackages != "", + g.Group([]g.Node{ + Dt(g.Text("Related Packages")), + Dd( + option.RelatedPackages, + ), + }), + ), + g.If(len(option.Declarations) > 0, + g.Group([]g.Node{ + Dt(g.Text("Declared")), + g.Map(option.Declarations, func(d nix.Link) g.Node { + return Dd( + A(Href(d.URL), g.Text(d.Name)), + ) + }), + }), + ), + ), + }) +} + +func OptionDetailPage(tdata TemplateData, option nix.Option) g.Node { + return Page(tdata, OptionDetail(option)) +} diff --git a/internal/components/optionDetail.templ b/internal/components/optionDetail.templ deleted file mode 100644 index 6eaafb4..0000000 --- a/internal/components/optionDetail.templ +++ /dev/null @@ -1,58 +0,0 @@ -package components - -import "go.alanpearce.eu/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.go b/internal/components/options.go new file mode 100644 index 0000000..af6c73f --- /dev/null +++ b/internal/components/options.go @@ -0,0 +1,50 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/config" + "go.alanpearce.eu/searchix/internal/index" + "go.alanpearce.eu/searchix/internal/nix" + + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func Options(result *index.Result) g.Node { + return Table( + THead( + Tr( + Th(Scope("col"), g.Text("Title")), + Th(Scope("col"), g.Text("Description")), + g.If(config.DevMode, + Th(Scope("col"), g.Text("Score")), + ), + ), + ), + TBody( + g.Map(result.Hits, func(hit index.DocumentMatch) g.Node { + if m := convertMatch[nix.Option](hit.Data); m != nil { + return optionRow(hit, *m) + } + + return nil + }), + ), + ) +} + +func optionRow(hit index.DocumentMatch, o nix.Option) g.Node { + return Tr( + Td( + openDialogLink(o.Name), + ), + Td( + firstSentence(o.Description), + Dialog(ID(o.Name)), + ), + g.If(config.DevMode, + Td( + Score(hit), + ), + ), + ) +} diff --git a/internal/components/options.templ b/internal/components/options.templ deleted file mode 100644 index 097f66f..0000000 --- a/internal/components/options.templ +++ /dev/null @@ -1,45 +0,0 @@ -package components - -import ( - "go.alanpearce.eu/searchix/internal/config" - "go.alanpearce.eu/searchix/internal/index" - "go.alanpearce.eu/searchix/internal/nix" -) - -templ Options(result *index.Result) { - - - - - - if config.DevMode { - - } - - - - for _, hit := range result.Hits { - if m := convertMatch[nix.Option](hit.Data); m != nil { - @optionRow(hit, *m) - } - } - -
TitleDescriptionScore
-} - -templ optionRow(hit index.DocumentMatch, o nix.Option) { - - - @openDialogLink(o.Name) - - - @markdown(firstSentence(o.Description)) - - - if config.DevMode { - - @score(hit) - - } - -} diff --git a/internal/components/packageDetail.go b/internal/components/packageDetail.go new file mode 100644 index 0000000..01b1f4d --- /dev/null +++ b/internal/components/packageDetail.go @@ -0,0 +1,120 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/nix" + + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func licenseName(l nix.License) string { + if l.FullName != "" { + return l.FullName + } + + return l.Name +} + +func PackageDetail(pkg nix.Package) g.Node { + return g.Group([]g.Node{ + H2( + g.If(pkg.Broken, + Del(g.Text(pkg.Attribute)), + g.Text(pkg.Attribute), + ), + ), + g.If(pkg.LongDescription != "", + pkg.LongDescription, + P(g.Text(pkg.Description)), + ), + Dl( + g.If(pkg.MainProgram != "", + g.Group([]g.Node{ + Dt(g.Text("Main Program")), + Dd(Code(g.Text(pkg.MainProgram))), + }), + ), + g.If(len(pkg.Programs) > 0, + g.Group([]g.Node{ + Dt(g.Text("Programs")), + Dd( + Ul( + g.Map(pkg.Programs, func(p string) g.Node { + return Li(Code(g.Text(p))) + }), + ), + ), + }), + ), + g.If(len(pkg.Homepages) > 0, + g.Group([]g.Node{ + Dt(g.Text("Homepage")), + Dd( + Ul( + g.Map(pkg.Homepages, func(u string) g.Node { + return Li(A(Href(u), g.Text(u))) + }), + ), + ), + }), + ), + g.If(pkg.Version != "", + g.Group([]g.Node{ + Dt(g.Text("Version")), + Dd(g.Text(pkg.Version)), + }), + ), + g.If(len(pkg.Licenses) > 0, + g.Group([]g.Node{ + Dt(g.Text("License")), + Dd( + Ul( + g.Map(pkg.Licenses, func(l nix.License) g.Node { + return Li( + g.If(l.URL != "", + A(Href(l.URL), g.Text(licenseName(l))), + g.Text(licenseName(l)), + ), + g.If(l.AppendixURL != "", + A(Href(l.AppendixURL), g.Text("Appendix")), + ), + ) + }), + ), + ), + }), + ), + g.If(len(pkg.Maintainers) > 0, + g.Group([]g.Node{ + Dt(g.Text("Maintainers")), + Dd( + Ul( + g.Map(pkg.Maintainers, func(m nix.Maintainer) g.Node { + return Li( + g.If( + m.Github != "", + A( + Href(joinPath("https://github.com", m.Github)), + g.Text(m.Name), + ), + g.Text(m.Name), + ), + ) + }), + ), + ), + }), + ), + g.If(pkg.Definition != "", + g.Group([]g.Node{ + Dt(g.Text("Defined")), + Dd(A(Href(pkg.Definition), g.Text("Source"))), + }), + ), + ), + }) +} + +func PackageDetailPage(tdata TemplateData, pkg nix.Package) g.Node { + return Page(tdata, PackageDetail(pkg)) +} diff --git a/internal/components/packageDetail.templ b/internal/components/packageDetail.templ deleted file mode 100644 index 84d2bdf..0000000 --- a/internal/components/packageDetail.templ +++ /dev/null @@ -1,109 +0,0 @@ -package components - -import "go.alanpearce.eu/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.Programs) > 0 { -
Programs
-
- -
- } - if len(pkg.Homepages) > 0 { -
Homepage
-
- -
- } - if pkg.Version != "" { -
Version
-
{ pkg.Version }
- } - if len(pkg.Licenses) > 0 { -
License
-
- -
- } - if len(pkg.Maintainers) > 0 { -
Maintainers
-
- -
- } - if pkg.Definition != "" { -
Defined
-
- Source -
- } -
-} - -templ PackageDetailPage(tdata TemplateData, pkg nix.Package) { - @Page(tdata) { - @PackageDetail(pkg) - } -} diff --git a/internal/components/packages.go b/internal/components/packages.go new file mode 100644 index 0000000..9bc3f99 --- /dev/null +++ b/internal/components/packages.go @@ -0,0 +1,53 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/config" + "go.alanpearce.eu/searchix/internal/index" + "go.alanpearce.eu/searchix/internal/nix" + + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func Packages(result *index.Result) g.Node { + return Table( + THead( + Tr( + Th(Scope("col"), g.Text("Attribute")), + Th(Scope("col"), g.Text("Name")), + Th(Scope("col"), g.Text("Description")), + g.If(config.DevMode, + Th(Scope("col"), g.Text("Score")), + ), + ), + ), + TBody( + g.Map(result.Hits, func(hit index.DocumentMatch) g.Node { + if m := convertMatch[nix.Package](hit.Data); m != nil { + return packageRow(hit, *m) + } + + return nil + }), + ), + ) +} + +func packageRow(hit index.DocumentMatch, p nix.Package) g.Node { + return Tr( + Td( + openDialogLink(p.Attribute), + ), + Td( + g.Text(p.Name), + ), + Td( + g.Text(p.Description), + ), + g.If(config.DevMode, + Td( + Score(hit), + ), + ), + ) +} diff --git a/internal/components/packages.templ b/internal/components/packages.templ deleted file mode 100644 index 6e14026..0000000 --- a/internal/components/packages.templ +++ /dev/null @@ -1,48 +0,0 @@ -package components - -import ( - "go.alanpearce.eu/searchix/internal/config" - "go.alanpearce.eu/searchix/internal/index" - "go.alanpearce.eu/searchix/internal/nix" -) - -templ Packages(result *index.Result) { - - - - - - - if config.DevMode { - - } - - - - for _, hit := range result.Hits { - if m := convertMatch[nix.Package](hit.Data); m != nil { - @packageRow(hit, *m) - } - } - -
AttributeNameDescriptionScore
-} - -templ packageRow(hit index.DocumentMatch, p nix.Package) { - - - @openDialogLink(p.Attribute) - - - { p.Name } - - - { p.Description } - - if config.DevMode { - - @score(hit) - - } - -} diff --git a/internal/components/page.go b/internal/components/page.go new file mode 100644 index 0000000..5cfa4ff --- /dev/null +++ b/internal/components/page.go @@ -0,0 +1,152 @@ +package components + +import ( + "net/url" + + "go.alanpearce.eu/searchix/frontend" + "go.alanpearce.eu/searchix/internal/config" + + g "go.alanpearce.eu/gomponents" + c "go.alanpearce.eu/gomponents/components" + . "go.alanpearce.eu/gomponents/html" +) + +func Page(tdata TemplateData, children ...g.Node) g.Node { + return Doctype( + HTML( + Lang("en-GB"), + Head( + Meta(Charset("utf-8")), + Meta(Name("viewport"), Content("width=device-width, initial-scale=1")), + TitleEl(g.Text("Searchix"), g.If(config.DevMode, g.Text(" (Dev)"))), + g.Map(tdata.Assets.Stylesheets, css), + g.Raw(tdata.ExtraHeadHTML), + Link( + Rel("search"), + Type("application/opensearchdescription+xml"), + TitleAttr("Searchix "+sourceNameAndType(nil)), + Href(joinPath("opensearch.xml")), + ), + g.Map(tdata.Sources, func(source *config.Source) g.Node { + return Link( + Rel("search"), + Type("application/opensearchdescription+xml"), + TitleAttr("Searchix "+sourceNameAndType(source)), + Href(joinPath("/", source.Importer.String(), source.Key, "opensearch.xml")), + ) + }), + ), + Body( + Header( + Nav( + H1(A(Href("/"), g.Text("Searchix"))), + A( + c.Classes{ + "current": tdata.Source == nil, + }, + g.If( + tdata.Source == nil, + Href("/"), + Href(joinPathQuery("/", tdata.Query)), + ), + g.Text("All"), + ), + g.Map(tdata.Sources, func(source *config.Source) g.Node { + if tdata.Source != nil && tdata.Source.Name == source.Name { + return A( + Class("current"), + Href( + joinPath( + "/", + source.Importer.String(), + source.Key, + "search", + ), + ), + g.Text(source.Name), + ) + } + + return A( + Href( + joinPathQuery( + joinPath( + "/", + source.Importer.String(), + source.Key, + "search", + ), + tdata.Query, + ), + ), + g.Text(source.Name), + ) + }), + ), + ), + Main(children...), + Footer( + g.If(config.Version != "", + g.Group([]g.Node{ + g.Text("Searchix "), + A( + Href("https://git.sr.ht/~alanpearce/searchix/refs/"+config.Version), + g.Text(config.Version), + ), + }), + ), + g.Text("Made by "), + A(Href("https://alanpearce.eu"), g.Text("Alan Pearce")), + g.Text(". "), + A(Href("https://git.sr.ht/~alanpearce/searchix"), g.Text("Source code")), + A(Href("https://todo.sr.ht/~alanpearce/searchix"), g.Text("Report issues")), + ), + ), + ), + ) +} + +func css(css *frontend.Asset) g.Node { + return Link(Href(css.URL), Rel("stylesheet"), + Integrity("sha256-"+css.Base64SHA256)) +} + +func script(s *frontend.Asset) g.Node { + return Script( + Src(s.URL), + Defer(), + g.Attr("integrity", "sha256-"+s.Base64SHA256), + ) +} + +func sourceNameAndType(source *config.Source) string { + if source == nil { + return "Combined" + } + + 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) string { + u, err := url.JoinPath(base, parts...) + if err != nil { + panic(err) + } + + return u +} + +func joinPathQuery(path string, query string) string { + if query == "" { + return path + } + + return path + "?query=" + url.QueryEscape(query) +} diff --git a/internal/components/page.templ b/internal/components/page.templ deleted file mode 100644 index 62b2937..0000000 --- a/internal/components/page.templ +++ /dev/null @@ -1,123 +0,0 @@ -package components - -import ( - "context" - "io" - "net/url" - - "go.alanpearce.eu/searchix/frontend" - "go.alanpearce.eu/searchix/internal/config" -) - -templ Page(tdata TemplateData) { - - - - - - - Searchix - if config.DevMode { - (Dev) - } - - 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 { - if source == nil { - return "Combined" - } - - 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.go b/internal/components/results.go new file mode 100644 index 0000000..4f07a78 --- /dev/null +++ b/internal/components/results.go @@ -0,0 +1,65 @@ +package components + +import ( + "go.alanpearce.eu/searchix/internal/config" + + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func Results(r ResultData) g.Node { + if r.Query == "" { + return Br() + } + + if r.Results == nil || r.Results.Total == 0 { + return Span(Role("status"), g.Text("Nothing found")) + } + + var content g.Node + if r.Source != nil { + switch r.Source.Importer { + case config.Options: + content = Options(r.Results) + case config.Packages: + content = Packages(r.Results) + } + } else { + content = Combined(r.Results) + } + + return g.Group([]g.Node{ + content, + Footer( + g.Attr("aria-label", "pagination"), + Nav( + ID("pagination"), + g.If(r.Prev != "", + A(Class("button"), Href(r.Prev), Rel("prev"), g.Text("Prev")), + ), + g.If(r.Next != "", + A(Class("button"), Href(r.Next), Rel("next"), g.Text("Next")), + ), + ), + Span( + Role("status"), + g.Textf("%d results", r.Results.Total), + ), + g.If(r.Next != r.Prev && r.Results.Total < config.MaxResultsShowAll, + A(Href(r.All), g.Text("Show All")), + ), + ), + }) +} + +func ResultsPage(r ResultData) g.Node { + return SearchPage(r.TemplateData, r, Results(r)) +} + +func openDialogLink(attr string) g.Node { + return A(Class("open-dialog"), Href(attr), g.Text(attr)) +} + +func openCombinedDialogLink(attr string) g.Node { + return A(Class("open-dialog"), Href("/"+attr), g.Text(attr)) +} diff --git a/internal/components/results.templ b/internal/components/results.templ deleted file mode 100644 index fee211c..0000000 --- a/internal/components/results.templ +++ /dev/null @@ -1,64 +0,0 @@ -package components - -import ( - "go.alanpearce.eu/searchix/internal/config" - "go.alanpearce.eu/searchix/internal/nix" - "strconv" -) - -func convertMatch[I nix.Importable](m nix.Importable) *I { - i, ok := m.(I) - if !ok { - return nil - } - return &i -} - -templ Results(r ResultData) { - if r.Query != "" { - if r.Results != nil && r.Results.Total > 0 { - if r.Source != nil { - switch r.Source.Importer { - case config.Options: - @Options(r.Results) - case config.Packages: - @Packages(r.Results) - } - } else { - @Combined(r.Results) - } - - } else { - Nothing found - } - } else { -
- } -} - -templ ResultsPage(r ResultData) { - @SearchPage(r.TemplateData, r) { - @Results(r) - } -} - -templ openDialogLink(attr string) { - { attr } -} - -templ openCombinedDialogLink(attr string) { - { attr } -} diff --git a/internal/components/search.go b/internal/components/search.go new file mode 100644 index 0000000..0a7c991 --- /dev/null +++ b/internal/components/search.go @@ -0,0 +1,63 @@ +package components + +import ( + g "go.alanpearce.eu/gomponents" + . "go.alanpearce.eu/gomponents/html" +) + +func Search(tdata TemplateData, r ResultData) g.Node { + return Form( + ID("search"), + Role("search"), + FieldSet( + Legend( + ID("legend"), + H2(g.Textf("%s search", sourceNameAndType(tdata.Source))), + ), + Input( + ID("query"), + Aria("labelledby", "legend"), + Name("query"), + Type("search"), + Value(r.Query), + AutoFocus(), + g.Attr("spellcheck", "false"), + g.Attr("autocapitalize", "none"), + ), + Button(g.Text("Search")), + ), + ) +} + +func SearchPage(tdata TemplateData, r ResultData, children ...g.Node) g.Node { + return Page( + tdata, + P( + g.Text("Search Nix packages and options from "), + A(Href("https://nixos.org"), g.Text("NixOS")), + g.Text(", "), + A(Href("https://github.com/LnL7/nix-darwin"), g.Text("nix-darwin")), + g.Text(" and "), + A(Href("https://github.com/nix-community/home-manager"), g.Text("home-manager")), + ), + script(tdata.Assets.ByPath["/static/search.js"]), + Search(tdata, r), + Section( + ID("results"), + Role("list"), + Aria("label", "search results"), + g.Group(children), + ), + Dialog( + ID("dialog"), + Button(AutoFocus(), g.Text("Close")), + ), + NoScript( + P( + Class("notice"), + g.Text("Everything should work fine without JavaScript. If that is not the case, "), + A(Href("https://todo.sr.ht/~alanpearce/searchix"), g.Text("report an issue")), + ), + ), + ) +} diff --git a/internal/components/search.templ b/internal/components/search.templ deleted file mode 100644 index 7076772..0000000 --- a/internal/components/search.templ +++ /dev/null @@ -1,44 +0,0 @@ -package components - -templ Search(tdata TemplateData, r ResultData) { - -} - -templ SearchPage(tdata TemplateData, r ResultData) { - @Page(tdata) { -

- Search Nix packages and options from NixOS, nix-darwin - and home-manager -

- @script(tdata.Assets.ByPath["/static/search.js"]) - @Search(tdata, r) -
- { children... } -
- - - - - } -} diff --git a/internal/nix/option.go b/internal/nix/option.go index c1cc4c3..96c4546 100644 --- a/internal/nix/option.go +++ b/internal/nix/option.go @@ -1,5 +1,13 @@ package nix +import ( + "io" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "gitlab.com/tozd/go/errors" +) + type Markdown string type Value struct { @@ -35,3 +43,12 @@ func (p Option) GetName() string { func (p Option) GetSource() string { return p.Source } + +var md = goldmark.New( + goldmark.WithExtensions(extension.NewLinkify()), +) + +// implements gomponent.Node +func (text Markdown) Render(w io.Writer) error { + return errors.WithStack(md.Convert([]byte(text), w)) +} diff --git a/internal/server/error.go b/internal/server/error.go index c2acf48..b51cfa9 100644 --- a/internal/server/error.go +++ b/internal/server/error.go @@ -29,9 +29,9 @@ func createErrorHandler( w.Header().Del("Vary") w.WriteHeader(code) if r.Header.Get("Fetch") == "true" { - err = components.Error(indexData).Render(r.Context(), w) + err = components.Error(indexData).Render(w) } else { - err = components.ErrorPage(indexData).Render(r.Context(), w) + err = components.ErrorPage(indexData).Render(w) } if err != nil { log.Error( diff --git a/internal/server/mux.go b/internal/server/mux.go index 1507860..2bbff8e 100644 --- a/internal/server/mux.go +++ b/internal/server/mux.go @@ -44,8 +44,6 @@ func applyDevModeOverrides(cfg *config.Config) { } cfg.Web.ContentSecurityPolicy.ScriptSrc = append( cfg.Web.ContentSecurityPolicy.ScriptSrc, - cfg.Web.BaseURL.JoinPath("_templ/reload/script.js").String(), - "http://localhost:7331", "'unsafe-inline'", ) } @@ -174,9 +172,9 @@ func NewMux( var baseErr error if r.Header.Get("Fetch") == "true" { w.Header().Add("Content-Type", "text/html; charset=utf-8") - baseErr = components.Results(tdata).Render(r.Context(), w) + baseErr = components.Results(tdata).Render(w) } else { - baseErr = components.ResultsPage(tdata).Render(r.Context(), w) + baseErr = components.ResultsPage(tdata).Render(w) } if baseErr != nil { log.Error("template error", "template", importerType, "error", baseErr) @@ -192,7 +190,7 @@ func NewMux( Assets: frontend.Assets, }, components.ResultData{}, - ).Render(r.Context(), w) + ).Render(w) if err != nil { errorHandler(w, r, err.Error(), http.StatusInternalServerError) @@ -248,9 +246,9 @@ func NewMux( var baseErr error if r.Header.Get("Fetch") == "true" { w.Header().Add("Content-Type", "text/html; charset=utf-8") - baseErr = components.Detail(*doc).Render(r.Context(), w) + baseErr = components.Detail(*doc).Render(w) } else { - baseErr = components.DetailPage(tdata, *doc).Render(r.Context(), w) + baseErr = components.DetailPage(tdata, *doc).Render(w) } if baseErr != nil { log.Error("template error", "template", importerSingular, "error", baseErr) diff --git a/modd.conf b/modd.conf index e26c754..0a3e6a1 100644 --- a/modd.conf +++ b/modd.conf @@ -2,7 +2,6 @@ internal/index/indexer.go { prep +onchange: "just reindex" } -**/*.go !**/*_templ.go config.toml { - daemon +sigint: templ generate --watch --proxy="http://localhost:3000" --open-browser=false \ - --cmd="go run ./cmd/searchix-web --dev --config config.toml" +**/*.go config.toml { + daemon +sigint: go run ./cmd/searchix-web --dev --config config.toml } diff --git a/nix/dev-shell.nix b/nix/dev-shell.nix index 7d21e9d..b9ede6c 100644 --- a/nix/dev-shell.nix +++ b/nix/dev-shell.nix @@ -12,7 +12,6 @@ mkShell { goEnv sd - templ modd brotli bleve diff --git a/nix/package.nix b/nix/package.nix index 081ddab..9bef057 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -37,7 +37,6 @@ buildGoApplication { patchPhase = '' rm -f frontend/static/base.css cp ${css} frontend/static/base.css - ${pkgs.templ}/bin/templ generate ''; tags = [ "embed" ]; ldflags = [ diff --git a/nix/pre-commit-checks.nix b/nix/pre-commit-checks.nix index 7e53f30..6bee0ae 100644 --- a/nix/pre-commit-checks.nix +++ b/nix/pre-commit-checks.nix @@ -4,38 +4,10 @@ rec { hooks = { gotest.enable = false; golangci-lint.enable = true; - staticcheck = - let - wrapper = pkgs.symlinkJoin { - name = "staticcheck-wrapped"; - paths = [ pkgs.go-tools ]; - nativeBuildInputs = [ pkgs.makeWrapper ]; - postBuild = '' - wrapProgram $out/bin/staticcheck \ - --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.go ]} - ''; - }; - in - { - enable = true; - package = wrapper; - entry = - let - script = pkgs.writeShellScript "precommit-staticcheck" '' - err=0 - for dir in $(echo "$@" | xargs -n1 dirname | sort -u); do - ${hooks.staticcheck.package}/bin/staticcheck ./"$dir" - code="$?" - if [[ "$err" -eq 0 ]]; then - err="$code" - fi - done - exit $err - ''; - in - builtins.toString script; - }; + staticcheck = { + enable = true; + }; statix = { enable = true; # ignore is a glob @@ -57,7 +29,7 @@ rec { check-symlinks.enable = true; editorconfig-checker = { enable = true; - excludes = [ "\.md$" ]; + excludes = [ "\.md$" "\.ya?ml$" ]; }; prettier = { enable = true; diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..8d86ee2 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1 @@ +dot_import_whitelist = ["go.alanpearce.eu/gomponents/html"] -- cgit 1.4.1