diff --git a/dict/main.go b/dict/main.go index 5234d26..8b4328b 100644 --- a/dict/main.go +++ b/dict/main.go @@ -69,11 +69,10 @@ func ParseEntry(entry jmdict.JmdictEntry) Entry { } } -func Search(query string) queryResult { +func Search(query string) (exactResults []Entry, otherResults []Entry, truncated bool) { query = strings.TrimSpace(query) - exactResults := make([]Entry, 0) - otherResults := make([]Entry, 0) - truncated := false + exactResults = make([]Entry, 0) + otherResults = make([]Entry, 0) count := 0 for _, jmdictEntry := range dict.Entries { exactMatch := false @@ -106,8 +105,18 @@ func Search(query string) queryResult { break } } - return queryResult{ - Query: query, + return +} + +type searchTemplateData struct { + ExactResults []Entry + OtherResults []Entry + Truncated bool + Count int +} + +func initSearchTemplateData(exactResults []Entry, otherResults []Entry, truncated bool) searchTemplateData { + return searchTemplateData{ ExactResults: exactResults, OtherResults: otherResults, Truncated: truncated, @@ -115,26 +124,6 @@ func Search(query string) queryResult { } } -func Lookup(word string) *Entry { - for _, jmdictEntry := range dict.Entries { - entry := ParseEntry(jmdictEntry) - if entry.Kanji == word { - return &entry - } - } - return nil -} - -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 { @@ -144,80 +133,51 @@ func main() { fmt.Println("JMdict loaded!") r := mux.NewRouter() r.HandleFunc("/", httputils.GenerateHandler( + "index.html", func(w http.ResponseWriter, r *http.Request) bool { return true }, - httputils.NewTemplateSet("index.html"), - func(w http.ResponseWriter, r *http.Request) (string, any) { return "index.html", nil }, + func(w http.ResponseWriter, r *http.Request) any { return nil }, []string{http.MethodGet}, )) - 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) + redirectToHome := func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/", http.StatusPermanentRedirect) } - r.HandleFunc("/search", rawSearchHandler) - r.HandleFunc("/search/", rawSearchHandler) + r.HandleFunc("/search", redirectToHome) + r.HandleFunc("/search/", redirectToHome) r.HandleFunc("/search/{query}", httputils.GenerateHandler( - // handler whether or not to use template + "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)), + } + }, + []string{http.MethodGet}, + )) + r.HandleFunc("/api/search", httputils.GenerateHandler( + "search.html", 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 }, - httputils.NewTemplateSet("index.html", "search.html"), - // template data - func(w http.ResponseWriter, r *http.Request) (template string, data any) { - if r.Header.Get("HX-Request") == "" { - template = "search.html" - } else { - template = "search" - } - // Only runs if handler returns true - query := mux.Vars(r)["query"] - data = Search(query) - return - }, - []string{http.MethodGet}, - )) - rawWordHandler := func(w http.ResponseWriter, r *http.Request) { - fmt.Println("Redirecting raw word handler") - http.Redirect(w, r, "/", http.StatusMovedPermanently) - } - r.HandleFunc("/word", rawWordHandler) - r.HandleFunc("/word/", rawWordHandler) - r.HandleFunc("/word/{word}", httputils.GenerateHandler( - func(w http.ResponseWriter, r *http.Request) bool { return true }, - // Order matters - // word.html overrided the results block in index.html - // so should be loaded second - httputils.NewTemplateSet("index.html", "word.html"), - func(w http.ResponseWriter, r *http.Request) (template string, data any) { - template = "word.html" - query := mux.Vars(r)["word"] - data = struct { - Query any - Entry *Entry - }{ - Query: nil, - Entry: Lookup(query), - } - return + func(w http.ResponseWriter, r *http.Request) any { + r.ParseMultipartForm(0) + query := r.FormValue("q") + return initSearchTemplateData(Search(query)) }, []string{http.MethodGet}, )) diff --git a/dict/templates/partials/definition.html b/dict/templates/definition.html similarity index 100% rename from dict/templates/partials/definition.html rename to dict/templates/definition.html diff --git a/dict/templates/index.html b/dict/templates/index.html index ea33ea8..ca01362 100644 --- a/dict/templates/index.html +++ b/dict/templates/index.html @@ -1,42 +1,33 @@ -{{- define "index" -}} - {{ block "title" . }}{{ template "sitetitle" . }}{{ end }} + jidict - +
- + hx-swap="innerHTML"> +
- {{ block "results" . }}{{ if .Query }}{{ template "search" . }}{{ end }}{{ end }} + {{ with .Results }}{{ template "search" . }}{{ end }}

- -{{- end -}} -{{- template "index" . -}} \ No newline at end of file + \ No newline at end of file diff --git a/dict/templates/partials/entry.html b/dict/templates/partials/entry.html deleted file mode 100644 index 9d74569..0000000 --- a/dict/templates/partials/entry.html +++ /dev/null @@ -1,23 +0,0 @@ -{{ 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 deleted file mode 100644 index 8488cbc..0000000 --- a/dict/templates/partials/entryfull.html +++ /dev/null @@ -1,23 +0,0 @@ -{{ 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 deleted file mode 100644 index 2c6b809..0000000 --- a/dict/templates/partials/search.html +++ /dev/null @@ -1,11 +0,0 @@ -{{- 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 deleted file mode 100644 index 67e6c0a..0000000 --- a/dict/templates/partials/sitetitle.html +++ /dev/null @@ -1 +0,0 @@ -{{ define "sitetitle" }}jidict{{ end }} \ No newline at end of file diff --git a/dict/templates/search.html b/dict/templates/search.html index ffa8680..90053b1 100644 --- a/dict/templates/search.html +++ b/dict/templates/search.html @@ -1,9 +1,11 @@ -{{- define "title" }}{{ .Query }} search - {{ template "sitetitle" . }}{{- end -}} - -{{- define "value" }}{{ .Query }}{{- end -}} - -{{- define "results" -}} -{{- template "entryfull" .Entry -}} +{{- 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 }} +
+{{ range .OtherResults -}} +{{ template "word" . }} {{- end -}} - -{{- template "index" . -}} \ No newline at end of file +{{- end -}} +{{- template "search" . -}} \ No newline at end of file diff --git a/dict/templates/word.html b/dict/templates/word.html index 245c10b..40d56f8 100644 --- a/dict/templates/word.html +++ b/dict/templates/word.html @@ -1,5 +1,22 @@ -{{- define "title" }}{{ .Entry.Kanji }} - {{ template "sitetitle" . }}{{- end -}} -{{- define "results" -}} -{{- template "entryfull" .Entry -}} -{{- end -}} -{{- template "index" . -}} \ No newline at end of file +{{ 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 diff --git a/httputils/handler.go b/httputils/handler.go index d1f152b..a98c763 100644 --- a/httputils/handler.go +++ b/httputils/handler.go @@ -1,21 +1,25 @@ package httputils import ( - "bytes" "fmt" "net/http" "os" "path/filepath" - "reflect" "strings" + "text/template" "time" ) type Handler = func(http.ResponseWriter, *http.Request) -func getPartials() ([]string, map[string]time.Time, error) { +const templateFolder = "templates" + +var templatePaths, templateModTimes, _ = getTemplates() +var templates *template.Template = template.Must(template.ParseFiles(templatePaths...)) + +func getTemplates() ([]string, map[string]time.Time, error) { var modTimes map[string]time.Time = make(map[string]time.Time) - err := filepath.Walk(partialsFolder, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(templateFolder, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -33,15 +37,35 @@ func getPartials() ([]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 string, handler func(http.ResponseWriter, *http.Request) bool, - templateSet TemplateSet, - template func(http.ResponseWriter, *http.Request) (template string, data any), + data func(http.ResponseWriter, *http.Request) 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 @@ -51,18 +75,9 @@ func GenerateHandler( return ok: renderTemplate := handler(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 - } + if renderTemplate && file != "" { w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprint(w, buf.String()) + templates.ExecuteTemplate(w, file, data(w, r)) } } } diff --git a/httputils/templates.go b/httputils/templates.go deleted file mode 100644 index c4e8eb2..0000000 --- a/httputils/templates.go +++ /dev/null @@ -1,86 +0,0 @@ -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...)