From f558d7f0c13410c8227fea368d97b28d3fc8a68c Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Fri, 21 Jul 2023 17:13:07 -0700 Subject: [PATCH 1/2] Better API handling --- dict/main.go | 88 ++++++++++++++++++++------------------ dict/templates/index.html | 11 +++-- dict/templates/search.html | 4 +- httputils/handler.go | 11 ++++- 4 files changed, 62 insertions(+), 52 deletions(-) diff --git a/dict/main.go b/dict/main.go index 8b4328b..8d375d3 100644 --- a/dict/main.go +++ b/dict/main.go @@ -69,10 +69,11 @@ func ParseEntry(entry jmdict.JmdictEntry) Entry { } } -func Search(query string) (exactResults []Entry, otherResults []Entry, truncated bool) { +func Search(query string) queryResult { query = strings.TrimSpace(query) - exactResults = make([]Entry, 0) - otherResults = make([]Entry, 0) + exactResults := make([]Entry, 0) + otherResults := make([]Entry, 0) + truncated := false count := 0 for _, jmdictEntry := range dict.Entries { exactMatch := false @@ -105,18 +106,8 @@ func Search(query string) (exactResults []Entry, otherResults []Entry, truncated break } } - return -} - -type searchTemplateData struct { - ExactResults []Entry - OtherResults []Entry - Truncated bool - Count int -} - -func initSearchTemplateData(exactResults []Entry, otherResults []Entry, truncated bool) searchTemplateData { - return searchTemplateData{ + return queryResult{ + Query: query, ExactResults: exactResults, OtherResults: otherResults, Truncated: truncated, @@ -124,6 +115,16 @@ func initSearchTemplateData(exactResults []Entry, otherResults []Entry, truncate } } +type queryResult struct { + // Fields must be capitalized + // to be accessible in templates + Query string + ExactResults []Entry + OtherResults []Entry + Truncated bool + Count int +} + func main() { err := LoadDict() if err != nil { @@ -138,46 +139,49 @@ func main() { func(w http.ResponseWriter, r *http.Request) any { return nil }, []string{http.MethodGet}, )) - redirectToHome := func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/", http.StatusPermanentRedirect) + rawSearchHandler := func(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(0) + q := r.FormValue("q") + var redirect string + if q == "" { + redirect = "/" + } else { + redirect = "/search/" + q + } + http.Redirect(w, r, redirect, http.StatusMovedPermanently) } - r.HandleFunc("/search", redirectToHome) - r.HandleFunc("/search/", redirectToHome) + r.HandleFunc("/search", rawSearchHandler) + r.HandleFunc("/search/", rawSearchHandler) r.HandleFunc("/search/{query}", httputils.GenerateHandler( - "index.html", - func(w http.ResponseWriter, r *http.Request) bool { - return true - }, - func(w http.ResponseWriter, r *http.Request) any { - query := mux.Vars(r)["query"] - return struct { - Query string - Results searchTemplateData - }{ - Query: query, - Results: initSearchTemplateData(Search(query)), + // template file + func(w http.ResponseWriter, r *http.Request) string { + if r.Header.Get("HX-Request") == "" { + return "index.html" } + return "search.html" }, - []string{http.MethodGet}, - )) - r.HandleFunc("/api/search", httputils.GenerateHandler( - "search.html", + // handler whether or not to use template func(w http.ResponseWriter, r *http.Request) bool { + // If Accept: applicaiton/json we'll use the template if r.Header.Get("Accept") != "application/json" { return true } + + // Otherwise, let's send JSON + query := mux.Vars(r)["query"] + result := Search(query) + jsonBytes, _ := json.Marshal(append(result.ExactResults, result.OtherResults...)) + w.Header().Set("Content-Type", "application/json; charset=utf-8") - r.ParseMultipartForm(0) - query := r.FormValue("q") - exactResults, otherResults, _ := Search(query) - jsonBytes, _ := json.Marshal(append(exactResults, otherResults...)) fmt.Fprint(w, string(jsonBytes)) + return false }, + // template data func(w http.ResponseWriter, r *http.Request) any { - r.ParseMultipartForm(0) - query := r.FormValue("q") - return initSearchTemplateData(Search(query)) + // Only runs if handler returns true + query := mux.Vars(r)["query"] + return Search(query) }, []string{http.MethodGet}, )) diff --git a/dict/templates/index.html b/dict/templates/index.html index ca01362..446cfc0 100644 --- a/dict/templates/index.html +++ b/dict/templates/index.html @@ -3,7 +3,7 @@ - jidict + {{ with .Query }}{{ . }} search - {{ end }}jidict - +
@@ -20,13 +27,16 @@
- + hx-target="#results" + hx-on::before-request="document.title = `${this.querySelector('input').value} search - jidict`"> +
- {{ if .Count }}{{ template "search" . }}{{ end }} + {{ block "results" . }}{{ if .Query }}{{ template "search" . }}{{ end }}{{ end }}

- \ No newline at end of file + +{{- end -}} +{{- template "index" . -}} \ No newline at end of file diff --git a/dict/templates/definition.html b/dict/templates/partials/definition.html similarity index 100% rename from dict/templates/definition.html rename to dict/templates/partials/definition.html diff --git a/dict/templates/partials/entry.html b/dict/templates/partials/entry.html new file mode 100644 index 0000000..9d74569 --- /dev/null +++ b/dict/templates/partials/entry.html @@ -0,0 +1,23 @@ +{{ define "entry" }} +
+

+ {{- if .Kanji -}} + {{- .Kanji -}}({{- .Reading -}}) + {{- else -}} + {{- .Reading -}} + {{- end -}} +

+ {{- $count := len .Definitions -}} + {{ if eq $count 1 -}} +

{{- template "definition" (index .Definitions 0) -}}

+ {{- else if ne $count 0 -}} +
    + {{- range .Definitions }} +
  1. + {{ template "definition" . }} +
  2. + {{- end }} +
+ {{- end }} +
+{{ end }} \ No newline at end of file diff --git a/dict/templates/partials/entryfull.html b/dict/templates/partials/entryfull.html new file mode 100644 index 0000000..8488cbc --- /dev/null +++ b/dict/templates/partials/entryfull.html @@ -0,0 +1,23 @@ +{{ define "entryfull" }} +
+

+ {{- if .Kanji -}} + {{- .Kanji -}}({{- .Reading -}}) + {{- else -}} + {{- .Reading -}} + {{- end -}} +

+ {{- $count := len .Definitions -}} + {{ if eq $count 1 -}} +

{{- template "definition" (index .Definitions 0) -}}

+ {{- else if ne $count 0 -}} +
    + {{- range .Definitions }} +
  1. + {{ template "definition" . }} +
  2. + {{- end }} +
+ {{- end }} +
+{{ end }} \ No newline at end of file diff --git a/dict/templates/partials/search.html b/dict/templates/partials/search.html new file mode 100644 index 0000000..2c6b809 --- /dev/null +++ b/dict/templates/partials/search.html @@ -0,0 +1,11 @@ +{{- define "search" -}} +

{{ if .Truncated }}Truncated results, showing first {{ .Count }}{{ else }}{{ if eq .Count 0 }}No results{{ else }}{{ .Count }} result{{ if ne .Count 1}}s{{ end }}{{ end }}{{ end }}.

+{{ range .ExactResults -}} +{{- template "entry" . -}} +{{- end }} +{{ if and (ne (len .ExactResults) 0) (ne (len .OtherResults) 0) }}
{{ end }} +{{ range .OtherResults -}} +{{ template "entry" . }} +{{- end -}} +{{- end -}} +{{- template "search" . -}} \ No newline at end of file diff --git a/dict/templates/partials/sitetitle.html b/dict/templates/partials/sitetitle.html new file mode 100644 index 0000000..67e6c0a --- /dev/null +++ b/dict/templates/partials/sitetitle.html @@ -0,0 +1 @@ +{{ define "sitetitle" }}jidict{{ end }} \ No newline at end of file diff --git a/dict/templates/search.html b/dict/templates/search.html index 94524e4..ffa8680 100644 --- a/dict/templates/search.html +++ b/dict/templates/search.html @@ -1,11 +1,9 @@ -{{- define "search" -}} -

{{ if .Truncated }}Truncated results, showing first {{ .Count }}{{ else }}{{ if eq .Count 0 }}No results{{ else }}{{ .Count }} result{{ if ne .Count 1}}s{{ end }}{{ end }}{{ end }}.

-{{ range .ExactResults -}} -{{- template "word" . -}} -{{- end }} -{{ if and (ne (len .ExactResults) 0) (ne (len .OtherResults) 0) }}
{{ end }} -{{ range .OtherResults -}} -{{ template "word" . }} +{{- define "title" }}{{ .Query }} search - {{ template "sitetitle" . }}{{- end -}} + +{{- define "value" }}{{ .Query }}{{- end -}} + +{{- define "results" -}} +{{- template "entryfull" .Entry -}} {{- end -}} -{{- end -}} -{{- template "search" . -}} \ No newline at end of file + +{{- template "index" . -}} \ No newline at end of file diff --git a/dict/templates/word.html b/dict/templates/word.html index 40d56f8..245c10b 100644 --- a/dict/templates/word.html +++ b/dict/templates/word.html @@ -1,22 +1,5 @@ -{{ define "word" }} -
-

- {{- if .Kanji -}} - {{- .Kanji -}}({{- .Reading -}}) - {{- else -}} - {{- .Reading -}} - {{- end -}} -

- {{ if le (len .Definitions) 2 -}} -

{{- template "definition" (index .Definitions 0) -}}

- {{- else -}} -
    - {{- range .Definitions }} -
  1. - {{ template "definition" . }} -
  2. - {{- end }} -
- {{- end }} -
-{{ end }} \ No newline at end of file +{{- define "title" }}{{ .Entry.Kanji }} - {{ template "sitetitle" . }}{{- end -}} +{{- define "results" -}} +{{- template "entryfull" .Entry -}} +{{- end -}} +{{- template "index" . -}} \ No newline at end of file diff --git a/httputils/handler.go b/httputils/handler.go index ea6ef78..d1f152b 100644 --- a/httputils/handler.go +++ b/httputils/handler.go @@ -1,25 +1,21 @@ package httputils import ( + "bytes" "fmt" "net/http" "os" "path/filepath" + "reflect" "strings" - "text/template" "time" ) type Handler = func(http.ResponseWriter, *http.Request) -const templateFolder = "templates" - -var templatePaths, templateModTimes, _ = getTemplates() -var templates *template.Template = template.Must(template.ParseFiles(templatePaths...)) - -func getTemplates() ([]string, map[string]time.Time, error) { +func getPartials() ([]string, map[string]time.Time, error) { var modTimes map[string]time.Time = make(map[string]time.Time) - err := filepath.Walk(templateFolder, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(partialsFolder, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -37,35 +33,15 @@ func getTemplates() ([]string, map[string]time.Time, error) { return paths, modTimes, err } -func reloadTemplateIfModified(path string) { - fileInfo, _ := os.Stat(path) - modTime := fileInfo.ModTime() - if modTime.After(templateModTimes[path]) { - fmt.Printf("Reloading template %s...\n", path) - templates.ParseFiles(path) - templateModTimes[path] = modTime - } -} - -func reloadTemplatesIfModified() { - for _, path := range templatePaths { - reloadTemplateIfModified(path) - } -} - const reloadTemplates = true func GenerateHandler( - file interface{}, // either string or func() string handler func(http.ResponseWriter, *http.Request) bool, - data func(http.ResponseWriter, *http.Request) any, + templateSet TemplateSet, + template func(http.ResponseWriter, *http.Request) (template string, data any), methods []string, ) Handler { return func(w http.ResponseWriter, r *http.Request) { - // All templates must be reloaded in case of dependencies - if reloadTemplates { - reloadTemplatesIfModified() - } for _, method := range methods { if method == r.Method { goto ok @@ -75,16 +51,18 @@ func GenerateHandler( return ok: renderTemplate := handler(w, r) - if renderTemplate && file != "" { - var file_path string - switch file.(type) { - case string: - file_path = file.(string) - case func(http.ResponseWriter, *http.Request) string: - file_path = file.(func(http.ResponseWriter, *http.Request) string)(w, r) + if renderTemplate { + file_path, data := template(w, r) + buf := &bytes.Buffer{} + err := templateSet.ExecuteTemplate(buf, file_path, reflect.ValueOf(data)) + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, "500 Internal Server Error") + return } w.Header().Set("Content-Type", "text/html; charset=utf-8") - templates.ExecuteTemplate(w, file_path, data(w, r)) + fmt.Fprint(w, buf.String()) } } } diff --git a/httputils/templates.go b/httputils/templates.go new file mode 100644 index 0000000..c4e8eb2 --- /dev/null +++ b/httputils/templates.go @@ -0,0 +1,86 @@ +package httputils + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "text/template" + "time" +) + +type TemplateSet struct { + templates *template.Template + paths []string + modTimes map[string]time.Time +} + +func newTemplateSet(partials *TemplateSet, paths ...string) TemplateSet { + var partialPaths []string + if partials == nil { + partialPaths = make([]string, 0) + } else { + partialPaths = partials.paths + } + allPaths := append(partialPaths, paths...) + modTimes := make(map[string]time.Time) + for _, path := range allPaths { + fileInfo, _ := os.Stat(path) + modTimes[path] = fileInfo.ModTime() + } + templates := template.Must(template.ParseFiles(allPaths...)) + return TemplateSet{ + templates: templates, + paths: allPaths, + modTimes: modTimes, + } +} + +func NewTemplateSet(paths ...string) TemplateSet { + for i, path := range paths { + paths[i] = fmt.Sprintf("%s/%s", templateFolder, path) + } + return newTemplateSet(&partials, paths...) +} + +func (templateSet *TemplateSet) ExecuteTemplate(wr io.Writer, name string, data any) error { + templateSet.reloadTemplatesIfModified() + return templateSet.templates.ExecuteTemplate(wr, name, data) +} + +func (templateSet *TemplateSet) reloadTemplateIfModified(path string) { + fileInfo, _ := os.Stat(path) + modTime := fileInfo.ModTime() + if modTime.After(templateSet.modTimes[path]) { + fmt.Printf("Reloading template %s...\n", path) + templateSet.templates.ParseFiles(path) + templateSet.modTimes[path] = modTime + } +} + +func (templateSet *TemplateSet) reloadTemplatesIfModified() { + for _, path := range templateSet.paths { + templateSet.reloadTemplateIfModified(path) + } +} + +func getTemplatePathsInDirectory(directory string) (paths []string, err error) { + paths = make([]string, 0) + err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(path, ".html") { + paths = append(paths, path) + } + return nil + }) + return +} + +const templateFolder = "templates" +const partialsFolder = templateFolder + "/partials" + +var paths, _ = getTemplatePathsInDirectory(partialsFolder) +var partials = newTemplateSet(nil, paths...)