diff options
author | Alan Pearce | 2024-06-21 13:02:08 +0200 |
---|---|---|
committer | Alan Pearce | 2024-06-21 15:33:38 +0200 |
commit | fc5fd2edd9b8282497e33a18300eab694d8a89c6 (patch) | |
tree | 18af097c037ef781cc8f6148d7c1ba37e10877c1 /internal/components | |
parent | cac323d9ae70f55a43fd99b73e60cf614be11797 (diff) | |
download | searchix-fc5fd2edd9b8282497e33a18300eab694d8a89c6.tar.lz searchix-fc5fd2edd9b8282497e33a18300eab694d8a89c6.tar.zst searchix-fc5fd2edd9b8282497e33a18300eab694d8a89c6.zip |
refactor: switch to templ for HTML templates
Diffstat (limited to 'internal/components')
-rw-r--r-- | internal/components/data.go | 37 | ||||
-rw-r--r-- | internal/components/detail.templ | 20 | ||||
-rw-r--r-- | internal/components/error.templ | 18 | ||||
-rw-r--r-- | internal/components/homepage.templ | 10 | ||||
-rw-r--r-- | internal/components/markdown.templ | 35 | ||||
-rw-r--r-- | internal/components/optionDetail.templ | 58 | ||||
-rw-r--r-- | internal/components/options.templ | 34 | ||||
-rw-r--r-- | internal/components/packageDetail.templ | 99 | ||||
-rw-r--r-- | internal/components/packages.templ | 37 | ||||
-rw-r--r-- | internal/components/page.templ | 91 | ||||
-rw-r--r-- | internal/components/results.templ | 44 | ||||
-rw-r--r-- | internal/components/search.templ | 34 |
12 files changed, 517 insertions, 0 deletions
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) { + <p class="notice error"> + { strconv.Itoa(tdata.Code) } + { tdata.Message } + </p> +} + +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) { + <p> + Search Nix Packages and options from NixOS, Darwin and Home-Manager + </p> + <a href="https://git.sr.ht/~alanpearce/searchix">Source code</a> + } +} 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) { + <h2>{ option.Name }</h2> + @markdown(option.Description) + <dl> + if option.Type != "" { + <dt>Type</dt> + <dd><code>{ option.Type }</code></dd> + } + if option.Default != nil { + if option.Default.Text != "" || option.Default.Markdown != "" { + <dt>Default</dt> + <dd> + if option.Default.Markdown != "" { + @markdown(option.Default.Markdown) + } else { + <pre><code>{ option.Default.Text }</code></pre> + } + </dd> + } + } + if option.Example != nil { + if option.Example.Text != "" || option.Example.Markdown != "" { + <dt>Example</dt> + <dd> + if option.Example.Markdown != "" { + @markdown(option.Example.Markdown) + } else { + <pre><code>{ option.Example.Text }</code></pre> + } + </dd> + } + } + if option.RelatedPackages != "" { + <dt>Related Packages</dt> + <dd> + @markdown(option.RelatedPackages) + </dd> + } + if len(option.Declarations) > 0 { + <dt>Declared</dt> + for _, d := range option.Declarations { + <dd> + <a href={ templ.SafeURL(d.URL) }>{ d.Name }</a> + </dd> + } + } + </dl> +} + +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) { + <table> + <thead> + <tr> + <td scope="col">Title</td> + <td scope="col">Description</td> + </tr> + </thead> + <tbody> + for _, hit := range result.Hits { + @optionRow(hit.Data.(nix.Option)) + } + </tbody> + </table> +} + +templ optionRow(o nix.Option) { + <tr> + <td> + @openDialogLink(o.Name) + </td> + <td> + @markdown(firstSentence(o.Description)) + <dialog id={ o.Name }></dialog> + </td> + </tr> +} 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) { + <h2> + if pkg.Broken { + <del>{ pkg.Attribute }</del> + } else { + { pkg.Attribute } + } + </h2> + if pkg.LongDescription != "" { + @markdown(pkg.LongDescription) + } else { + <p>{ pkg.Description }</p> + } + <dl> + if pkg.MainProgram != "" { + <dt>Main Program</dt> + <dd> + <code>{ pkg.MainProgram }</code> + </dd> + } + if len(pkg.Homepages) > 0 { + <dt>Homepage</dt> + <dd> + <ul> + for _, u := range pkg.Homepages { + <li> + <a href={ templ.SafeURL(u) }>{ u }</a> + </li> + } + </ul> + </dd> + } + if pkg.Version != "" { + <dt>Version</dt> + <dd>{ pkg.Version }</dd> + } + if len(pkg.Licenses) > 0 { + <dt>License</dt> + <dd> + <ul> + for _, l := range pkg.Licenses { + <li> + if l.URL != "" { + <a href={ templ.SafeURL(l.URL) }>{ licenseName(l) }</a> + } else { + { licenseName(l) } + } + if l.AppendixURL != "" { + <a href={ templ.SafeURL(l.AppendixURL) }>Appendix</a> + } + </li> + } + </ul> + </dd> + } + if len(pkg.Maintainers) > 0 { + <dt>Maintainers</dt> + <dd> + <ul> + for _, m := range pkg.Maintainers { + <li> + if m.Github != "" { + <a href={ joinPath("https://github.com", m.Github) }>{ m.Name }</a> + } else { + { m.Name } + } + </li> + } + </ul> + </dd> + } + if pkg.Definition != "" { + <dt>Defined</dt> + <dd> + <a href={ templ.SafeURL(pkg.Definition) }>Source</a> + </dd> + } + </dl> +} + +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) { + <table> + <thead> + <tr> + <th scope="col">Attribute</th> + <th scope="col">Name</th> + <th scope="col">Description</th> + </tr> + </thead> + <tbody> + for _, hit := range result.Hits { + @packageRow(hit.Data.(nix.Package)) + } + </tbody> + </table> +} + +templ packageRow(p nix.Package) { + <tr> + <td> + @openDialogLink(p.Attribute) + </td> + <td> + { p.Name } + </td> + <td> + { p.Description } + </td> + </tr> +} 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) { + <!DOCTYPE html> + <html lang="en-GB"> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>Searchix</title> + for _, sheet := range tdata.Assets.Stylesheets { + <link href={ sheet.URL } rel="stylesheet" integrity={ "sha256-" + sheet.Base64SHA256 }/> + } + @Unsafe(tdata.ExtraHeadHTML) + for _, source := range tdata.Sources { + <link + rel="search" + type="application/opensearchdescription+xml" + title={ "Searchix " + sourceNameAndType(*source) } + href={ string(joinPath("/", source.Importer.String(), source.Key, "opensearch.xml")) } + /> + } + </head> + <body> + <header> + <nav> + <h1><a href="/">Searchix</a></h1> + for _, source := range tdata.Sources { + <a + if tdata.Source.Name == source.Name { + class="current" + href={ joinPath("/", source.Importer.String(), source.Key, "search") } + } else { + href={ joinPathQuery(joinPath("/", source.Importer.String(), source.Key, "search"), tdata.Query) } + } + >{ source.Name }</a> + } + </nav> + </header> + <main> + { children... } + </main> + <footer> + Made by <a href="https://alanpearce.eu">Alan Pearce</a>. + <a href="https://todo.sr.ht/~alanpearce/searchix">Report issues</a> + </footer> + </body> + </html> +} + +templ script(s *frontend.Asset) { + <script src={ s.URL } defer integrity={ "sha256-" + s.Base64SHA256 }></script> +} + +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) + } + <footer aria-label="pagination"> + <nav id="pagination"> + if r.Prev != "" { + <a class="button" href={ templ.SafeURL(r.Prev) } rel="prev">Prev</a> + } + if r.Next != "" { + <a class="button" href={ templ.SafeURL(r.Next) } rel="next">Next</a> + } + </nav> + <span role="status">{ strconv.FormatUint(r.Results.Total, 10) } results</span> + </footer> + } else { + <span role="status">Nothing found</span> + } + } else { + <br/> + } +} + +templ ResultsPage(r ResultData) { + @SearchPage(r.TemplateData, r) { + @Results(r) + } +} + +templ openDialogLink(attr string) { + <a class="open-dialog" href={ templ.SafeURL(attr) }>{ attr }</a> +} diff --git a/internal/components/search.templ b/internal/components/search.templ new file mode 100644 index 0000000..f1a5b8b --- /dev/null +++ b/internal/components/search.templ @@ -0,0 +1,34 @@ +package components + +templ Search(tdata TemplateData, r ResultData) { + <form id="search" role="search"> + <fieldset> + <legend id="legend"> + <h2>{ sourceNameAndType(tdata.Source) } search</h2> + </legend> + <input + id="query" + aria-labelledby="legend" + name="query" + type="search" + value={ r.Query } + autofocus + spellcheck="false" + /> + <button>Search</button> + </fieldset> + </form> +} + +templ SearchPage(tdata TemplateData, r ResultData) { + @Page(tdata) { + @script(tdata.Assets.Scripts["static/search.js"]) + @Search(tdata, r) + <section id="results" role="list" aria-label="search results"> + { children... } + </section> + <dialog id="dialog"> + <button autofocus>Close</button> + </dialog> + } +} |