main
Elnu 10 months ago
parent f8ccf62570
commit 01204ffc81

1
dict/.gitignore vendored

@ -0,0 +1 @@
JMdict.xml

@ -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

@ -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
)

@ -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=

@ -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))
}

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jidict</title>
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.0.9/dist/missing.min.css">
<script src="https://unpkg.com/htmx.org@1.9.3"></script>
</head>
<body>
<main>
<a href="/">
<img src="https://jichan.org/logo.svg" style="height: 4em; display: block; margin: 1em auto 1em auto">
</a>
<form
hx-post="/api/search"
hx-target="#results"
hx-swap="innerHTML">
<input type="text" name="q" placeholder="辞書をサーチする" class="width:100%" autocomplete="false">
</form>
<div id="results"></div>
<br>
</main>
</body>
</html>

@ -0,0 +1,21 @@
<p><i>{{ $count := (len .) }}{{ if eq $count 0 }}No results{{ else }}{{ $count }} result{{ if ne $count 1}}s{{ end }}{{ end }}.</i></p>
{{- range . -}}
<div class="box">
<h3>
{{- if .Kanji -}}
<ruby>{{- .Kanji -}}<rp>(</rp><rt>{{- .Reading -}}</rt><rp>)</rp></ruby>
{{- else -}}
{{- .Reading -}}
{{- end -}}
</h3>
{{ if le (len .Definitions) 2 -}}
<p>{{- index .Definitions 0 -}}</p>
{{- else -}}
<ol>
{{- range .Definitions }}
<li>{{- . -}}</li>
{{- end }}
</ol>
{{- end }}
</div>
{{ end -}}

@ -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))
}

@ -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},