diff --git a/dict/main.go b/dict/main.go index 8b4328b..5234d26 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,26 @@ func initSearchTemplateData(exactResults []Entry, otherResults []Entry, truncate } } +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 { @@ -133,51 +144,80 @@ 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 }, - func(w http.ResponseWriter, r *http.Request) any { return nil }, + httputils.NewTemplateSet("index.html"), + func(w http.ResponseWriter, r *http.Request) (string, any) { return "index.html", 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)), - } - }, - []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 }, - func(w http.ResponseWriter, r *http.Request) any { - r.ParseMultipartForm(0) - query := r.FormValue("q") - return initSearchTemplateData(Search(query)) + 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 }, []string{http.MethodGet}, )) diff --git a/dict/templates/index.html b/dict/templates/index.html index ca01362..ea33ea8 100644 --- a/dict/templates/index.html +++ b/dict/templates/index.html @@ -1,33 +1,42 @@ +{{- define "index" -}} - jidict + {{ block "title" . }}{{ template "sitetitle" . }}{{ end }} - +
- + hx-on::before-request="document.title = `${this.querySelector('input').value} search - jidict`"> +
- {{ with .Results }}{{ 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 90053b1..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 }} -
-{{ 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 a98c763..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 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,9 +51,18 @@ func GenerateHandler( return ok: renderTemplate := handler(w, r) - if renderTemplate && file != "" { + 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, 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...)