From 01204ffc81072e51f608a6b540b9ed8a0df52d8d Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Thu, 20 Jul 2023 19:31:26 -0700 Subject: [PATCH] dict: MVP --- dict/.gitignore | 1 + dict/README.md | 14 +++++ dict/go.mod | 8 +++ dict/go.sum | 4 ++ dict/main.go | 112 +++++++++++++++++++++++++++++++++++++ dict/static/index.html | 25 +++++++++ dict/templates/search.html | 21 +++++++ httputils/handler.go | 7 ++- shiritori/api/submit.go | 3 +- 9 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 dict/.gitignore create mode 100644 dict/README.md create mode 100644 dict/go.mod create mode 100644 dict/go.sum create mode 100644 dict/main.go create mode 100644 dict/static/index.html create mode 100644 dict/templates/search.html diff --git a/dict/.gitignore b/dict/.gitignore new file mode 100644 index 0000000..30e4d8d --- /dev/null +++ b/dict/.gitignore @@ -0,0 +1 @@ +JMdict.xml \ No newline at end of file diff --git a/dict/README.md b/dict/README.md new file mode 100644 index 0000000..bb0c063 --- /dev/null +++ b/dict/README.md @@ -0,0 +1,14 @@ +# jichanorg/dict + +**jichanorg/dict** is a [hypermedia](https://hypermedia.systems/) and JSON API for parsing the JMDict Japanese dictionary project. + +Its primary goals are: + +- be a self-hostable dictionary solution to replace existing proprietary ([Jisho.org](https://jisho.org/)) and partially proprietary ([Jotoba](https://jotoba.de/)) dictionaries. +- Provide a hypermedia API for integration into other applications, such as [jichanorg/shiritori](../shiritori). + +### Configuration + +1. [Download the latest version of JMdict](https://www.edrdg.org/wiki/index.php/JMdict-EDICT_Dictionary_Project#CURRENT_VERSION_&_DOWNLOAD). For development, download the file with only English glosses, which will substantially decrease parsing time. +2. Extract the archive +3. Rename the file to JMDict.xml \ No newline at end of file diff --git a/dict/go.mod b/dict/go.mod new file mode 100644 index 0000000..c0b1214 --- /dev/null +++ b/dict/go.mod @@ -0,0 +1,8 @@ +module git.elnu.com/ElnuDev/jichanorg/dict + +go 1.20 + +require ( + foosoft.net/projects/jmdict v0.0.0-20220714211640-cc9bc30b68a3 // indirect + github.com/gorilla/mux v1.8.0 // indirect +) diff --git a/dict/go.sum b/dict/go.sum new file mode 100644 index 0000000..7ae614e --- /dev/null +++ b/dict/go.sum @@ -0,0 +1,4 @@ +foosoft.net/projects/jmdict v0.0.0-20220714211640-cc9bc30b68a3 h1:zjHGpgUR2WP3pf6NVZM38OKYNse0GjovCW2v23V72PQ= +foosoft.net/projects/jmdict v0.0.0-20220714211640-cc9bc30b68a3/go.mod h1:ZrjLCcE7ZrND28ZOSGYMd78tL+Dffiv2g+NjOMKgnew= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/dict/main.go b/dict/main.go new file mode 100644 index 0000000..77755a9 --- /dev/null +++ b/dict/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + "foosoft.net/projects/jmdict" + "git.elnu.com/ElnuDev/jichanorg/httputils" + "github.com/gorilla/mux" +) + +var dict jmdict.Jmdict + +func LoadDict() error { + const jmdictFile = "JMdict.xml" + reader, err := os.Open(jmdictFile) + if err != nil { + return err + } + dict, _, err = jmdict.LoadJmdict(reader) + if err != nil { + return err + } + return nil +} + +type Entry struct { + Kanji string + Reading string + Definitions []string +} + +func ParseEntry(entry jmdict.JmdictEntry) Entry { + kanji := "" + if len(entry.Kanji) > 0 { + kanji = entry.Kanji[0].Expression + } + reading := "" + 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 + } + } + return Entry{ + Kanji: kanji, + Reading: reading, + Definitions: definitions, + } +} + +func Search(query string) []Entry { + entries := make([]Entry, 0) + for _, jmdictEntry := range dict.Entries { + for _, kanji := range jmdictEntry.Kanji { + if kanji.Expression == query { + goto match + } + } + for _, reading := range jmdictEntry.Readings { + if reading.Reading == query { + goto match + } + } + continue + match: + entry := ParseEntry(jmdictEntry) + entries = append(entries, entry) + } + return entries +} + +func main() { + err := LoadDict() + if err != nil { + fmt.Println(err) + return + } + fmt.Println("JMdict loaded!") + r := mux.NewRouter() + r.HandleFunc("/api/search", httputils.GenerateHandler( + "search.html", + func(w http.ResponseWriter, r *http.Request) bool { + if r.Header.Get("Accept") != "application/json" { + return true + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + r.ParseMultipartForm(0) + query := r.FormValue("q") + entries := Search(query) + jsonBytes, _ := json.Marshal(entries) + 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 + }, + []string{http.MethodGet, http.MethodPost}, + )) + r.Handle("/", http.FileServer(http.Dir("static"))) + log.Fatal(http.ListenAndServe(":3334", r)) +} diff --git a/dict/static/index.html b/dict/static/index.html new file mode 100644 index 0000000..cfebb64 --- /dev/null +++ b/dict/static/index.html @@ -0,0 +1,25 @@ + + + + + + jidict + + + + +
+ + + +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/dict/templates/search.html b/dict/templates/search.html new file mode 100644 index 0000000..b87fb1c --- /dev/null +++ b/dict/templates/search.html @@ -0,0 +1,21 @@ +

{{ $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 diff --git a/httputils/handler.go b/httputils/handler.go index b9a2907..31bf89b 100644 --- a/httputils/handler.go +++ b/httputils/handler.go @@ -10,7 +10,7 @@ type Handler = func(http.ResponseWriter, *http.Request) func GenerateHandler( file string, - handler func(http.ResponseWriter, *http.Request), + handler func(http.ResponseWriter, *http.Request) bool, data func(http.ResponseWriter, *http.Request) any, methods []string, ) Handler { @@ -19,6 +19,7 @@ func GenerateHandler( 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))) for _, method := range methods { if method == r.Method { goto ok @@ -27,8 +28,8 @@ func GenerateHandler( w.WriteHeader(http.StatusMethodNotAllowed) return ok: - handler(w, r) - if tmpl != nil { + renderTemplate := handler(w, r) + if renderTemplate && tmpl != nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, data(w, r)) } diff --git a/shiritori/api/submit.go b/shiritori/api/submit.go index 782abc0..e1c9c5f 100644 --- a/shiritori/api/submit.go +++ b/shiritori/api/submit.go @@ -10,9 +10,10 @@ import ( func GenerateApiSubmit(clients *ClientSet) httputils.Handler { return httputils.GenerateHandler( "", - func(w http.ResponseWriter, r *http.Request) { + func(w http.ResponseWriter, r *http.Request) bool { r.ParseMultipartForm(0) clients.BroadcastWord(r.FormValue("word")) + return true }, nil, []string{http.MethodPost},