diff --git a/dict/main.go b/dict/main.go index 77755a9..8b4328b 100644 --- a/dict/main.go +++ b/dict/main.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "os" + "strings" "foosoft.net/projects/jmdict" "git.elnu.com/ElnuDev/jichanorg/httputils" @@ -30,7 +31,12 @@ func LoadDict() error { type Entry struct { Kanji string Reading string - Definitions []string + Definitions []Definition +} + +type Definition struct { + Definition string + PartOfSpeech []string } func ParseEntry(entry jmdict.JmdictEntry) Entry { @@ -42,11 +48,18 @@ func ParseEntry(entry jmdict.JmdictEntry) Entry { if len(entry.Readings) > 0 { reading = entry.Readings[0].Reading } - var definitions []string - if len(entry.Sense) > 0 && len(entry.Sense[0].Glossary) > 0 { - definitions = make([]string, len(entry.Sense[0].Glossary)) - for i, glossary := range entry.Sense[0].Glossary { - definitions[i] = glossary.Content + var definitions []Definition + definitions = make([]Definition, len(entry.Sense)) + for i, sense := range entry.Sense { + definition := sense.Glossary[0].Content + if len(sense.Glossary) > 1 { + for _, glossary := range sense.Glossary[1:] { + definition += "; " + glossary.Content + } + } + definitions[i] = Definition{ + Definition: definition, + PartOfSpeech: sense.PartsOfSpeech, } } return Entry{ @@ -56,25 +69,59 @@ func ParseEntry(entry jmdict.JmdictEntry) Entry { } } -func Search(query string) []Entry { - entries := make([]Entry, 0) +func Search(query string) (exactResults []Entry, otherResults []Entry, truncated bool) { + query = strings.TrimSpace(query) + exactResults = make([]Entry, 0) + otherResults = make([]Entry, 0) + count := 0 for _, jmdictEntry := range dict.Entries { + exactMatch := false for _, kanji := range jmdictEntry.Kanji { if kanji.Expression == query { + exactMatch = true + goto match + } + if strings.Contains(kanji.Expression, query) { goto match } } + // TODO: Skip if query contains kanji for _, reading := range jmdictEntry.Readings { - if reading.Reading == query { + if strings.Contains(reading.Reading, query) { goto match } } continue match: entry := ParseEntry(jmdictEntry) - entries = append(entries, entry) + if exactMatch { + exactResults = append(exactResults, entry) + } else { + otherResults = append(otherResults, entry) + } + count++ + if count >= 500 { + truncated = true + break + } + } + 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, + Count: len(exactResults) + len(otherResults), } - return entries } func main() { @@ -85,6 +132,34 @@ 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 }, + []string{http.MethodGet}, + )) + redirectToHome := func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/", http.StatusPermanentRedirect) + } + r.HandleFunc("/search", redirectToHome) + r.HandleFunc("/search/", redirectToHome) + 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", func(w http.ResponseWriter, r *http.Request) bool { @@ -94,18 +169,17 @@ func main() { w.Header().Set("Content-Type", "application/json; charset=utf-8") r.ParseMultipartForm(0) query := r.FormValue("q") - entries := Search(query) - jsonBytes, _ := json.Marshal(entries) + 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") - entry := Search(query) - return entry + return initSearchTemplateData(Search(query)) }, - []string{http.MethodGet, http.MethodPost}, + []string{http.MethodGet}, )) r.Handle("/", http.FileServer(http.Dir("static"))) log.Fatal(http.ListenAndServe(":3334", r)) diff --git a/dict/templates/definition.html b/dict/templates/definition.html new file mode 100644 index 0000000..86595c7 --- /dev/null +++ b/dict/templates/definition.html @@ -0,0 +1,3 @@ +{{- define "definition" -}} +{{ if .PartOfSpeech }}{{ .PartOfSpeech }}
{{ end }}{{ .Definition -}} +{{ end }} \ No newline at end of file diff --git a/dict/static/index.html b/dict/templates/index.html similarity index 55% rename from dict/static/index.html rename to dict/templates/index.html index cfebb64..ca01362 100644 --- a/dict/static/index.html +++ b/dict/templates/index.html @@ -5,6 +5,11 @@ jidict + @@ -13,12 +18,15 @@
- +
-
+
+ {{ with .Results }}{{ template "search" . }}{{ end }} +

diff --git a/dict/templates/search.html b/dict/templates/search.html index b87fb1c..90053b1 100644 --- a/dict/templates/search.html +++ b/dict/templates/search.html @@ -1,21 +1,11 @@ -

{{ $count := (len .) }}{{ if eq $count 0 }}No results{{ else }}{{ $count }} result{{ if ne $count 1}}s{{ end }}{{ end }}.

-{{- range . -}} -
-

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

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

{{- index .Definitions 0 -}}

- {{- else -}} -
    - {{- range .Definitions }} -
  1. {{- . -}}
  2. - {{- end }} -
- {{- end }} -
-{{ end -}} \ No newline at end of file +{{- 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 -}} +{{- end -}} +{{- template "search" . -}} \ No newline at end of file diff --git a/dict/templates/word.html b/dict/templates/word.html new file mode 100644 index 0000000..40d56f8 --- /dev/null +++ b/dict/templates/word.html @@ -0,0 +1,22 @@ +{{ 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 31bf89b..a98c763 100644 --- a/httputils/handler.go +++ b/httputils/handler.go @@ -3,23 +3,69 @@ package httputils import ( "fmt" "net/http" + "os" + "path/filepath" + "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) { + var modTimes map[string]time.Time = make(map[string]time.Time) + err := filepath.Walk(templateFolder, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(path, ".html") { + modTimes[path] = info.ModTime() + } + return nil + }) + paths := make([]string, len(modTimes)) + i := 0 + for path := range modTimes { + paths[i] = path + i++ + } + 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, methods []string, ) Handler { - var tmpl *template.Template - if file != "" { - tmpl = template.Must(template.ParseFiles(fmt.Sprintf("templates/%s", file))) - } return func(w http.ResponseWriter, r *http.Request) { - tmpl = template.Must(template.ParseFiles(fmt.Sprintf("templates/%s", file))) + // All templates must be reloaded in case of dependencies + if reloadTemplates { + reloadTemplatesIfModified() + } for _, method := range methods { if method == r.Method { goto ok @@ -29,9 +75,9 @@ func GenerateHandler( return ok: renderTemplate := handler(w, r) - if renderTemplate && tmpl != nil { + if renderTemplate && file != "" { w.Header().Set("Content-Type", "text/html; charset=utf-8") - tmpl.Execute(w, data(w, r)) + templates.ExecuteTemplate(w, file, data(w, r)) } } }