diff --git a/dict/main.go b/dict/main.go index 8d375d3..5234d26 100644 --- a/dict/main.go +++ b/dict/main.go @@ -115,6 +115,16 @@ 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 @@ -134,9 +144,9 @@ 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}, )) rawSearchHandler := func(w http.ResponseWriter, r *http.Request) { @@ -153,13 +163,6 @@ func main() { r.HandleFunc("/search", rawSearchHandler) r.HandleFunc("/search/", rawSearchHandler) r.HandleFunc("/search/{query}", httputils.GenerateHandler( - // template file - func(w http.ResponseWriter, r *http.Request) string { - if r.Header.Get("HX-Request") == "" { - return "index.html" - } - return "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 @@ -177,11 +180,44 @@ func main() { return false }, + httputils.NewTemplateSet("index.html", "search.html"), // template data - func(w http.ResponseWriter, r *http.Request) any { + 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"] - return Search(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 446cfc0..ea33ea8 100644 --- a/dict/templates/index.html +++ b/dict/templates/index.html @@ -1,18 +1,25 @@ +{{- define "index" -}} - {{ with .Query }}{{ . }} search - {{ end }}jidict + {{ block "title" . }}{{ template "sitetitle" . }}{{ end }} - +
@@ -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...)