package main import ( "encoding/json" "fmt" "log" "net/http" "os" "strings" "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 []Definition } type Definition struct { Definition string PartOfSpeech []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 []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{ Kanji: kanji, Reading: reading, Definitions: definitions, } } func Search(query string) queryResult { query = strings.TrimSpace(query) exactResults := make([]Entry, 0) otherResults := make([]Entry, 0) truncated := false 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 strings.Contains(reading.Reading, query) { goto match } } continue match: entry := ParseEntry(jmdictEntry) if exactMatch { exactResults = append(exactResults, entry) } else { otherResults = append(otherResults, entry) } count++ if count >= 500 { truncated = true break } } return queryResult{ Query: query, ExactResults: exactResults, OtherResults: otherResults, Truncated: truncated, Count: len(exactResults) + len(otherResults), } } 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 { fmt.Println(err) return } 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}, )) 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", 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 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") fmt.Fprint(w, string(jsonBytes)) return false }, // template data func(w http.ResponseWriter, r *http.Request) any { // Only runs if handler returns true query := mux.Vars(r)["query"] return Search(query) }, []string{http.MethodGet}, )) r.Handle("/", http.FileServer(http.Dir("static"))) log.Fatal(http.ListenAndServe(":3334", r)) }