Compare commits

..

No commits in common. "61c1ce55028d238bba4a91cb9b454218ea73ca62" and "01204ffc81072e51f608a6b540b9ed8a0df52d8d" have entirely different histories.

6 changed files with 47 additions and 190 deletions

View file

@ -6,7 +6,6 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"strings"
"foosoft.net/projects/jmdict" "foosoft.net/projects/jmdict"
"git.elnu.com/ElnuDev/jichanorg/httputils" "git.elnu.com/ElnuDev/jichanorg/httputils"
@ -31,12 +30,7 @@ func LoadDict() error {
type Entry struct { type Entry struct {
Kanji string Kanji string
Reading string Reading string
Definitions []Definition Definitions []string
}
type Definition struct {
Definition string
PartOfSpeech []string
} }
func ParseEntry(entry jmdict.JmdictEntry) Entry { func ParseEntry(entry jmdict.JmdictEntry) Entry {
@ -48,18 +42,11 @@ func ParseEntry(entry jmdict.JmdictEntry) Entry {
if len(entry.Readings) > 0 { if len(entry.Readings) > 0 {
reading = entry.Readings[0].Reading reading = entry.Readings[0].Reading
} }
var definitions []Definition var definitions []string
definitions = make([]Definition, len(entry.Sense)) if len(entry.Sense) > 0 && len(entry.Sense[0].Glossary) > 0 {
for i, sense := range entry.Sense { definitions = make([]string, len(entry.Sense[0].Glossary))
definition := sense.Glossary[0].Content for i, glossary := range entry.Sense[0].Glossary {
if len(sense.Glossary) > 1 { definitions[i] = glossary.Content
for _, glossary := range sense.Glossary[1:] {
definition += "; " + glossary.Content
}
}
definitions[i] = Definition{
Definition: definition,
PartOfSpeech: sense.PartsOfSpeech,
} }
} }
return Entry{ return Entry{
@ -69,59 +56,25 @@ func ParseEntry(entry jmdict.JmdictEntry) Entry {
} }
} }
func Search(query string) (exactResults []Entry, otherResults []Entry, truncated bool) { func Search(query string) []Entry {
query = strings.TrimSpace(query) entries := make([]Entry, 0)
exactResults = make([]Entry, 0)
otherResults = make([]Entry, 0)
count := 0
for _, jmdictEntry := range dict.Entries { for _, jmdictEntry := range dict.Entries {
exactMatch := false
for _, kanji := range jmdictEntry.Kanji { for _, kanji := range jmdictEntry.Kanji {
if kanji.Expression == query { if kanji.Expression == query {
exactMatch = true
goto match
}
if strings.Contains(kanji.Expression, query) {
goto match goto match
} }
} }
// TODO: Skip if query contains kanji
for _, reading := range jmdictEntry.Readings { for _, reading := range jmdictEntry.Readings {
if strings.Contains(reading.Reading, query) { if reading.Reading == query {
goto match goto match
} }
} }
continue continue
match: match:
entry := ParseEntry(jmdictEntry) entry := ParseEntry(jmdictEntry)
if exactMatch { entries = append(entries, entry)
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() { func main() {
@ -132,34 +85,6 @@ func main() {
} }
fmt.Println("JMdict loaded!") fmt.Println("JMdict loaded!")
r := mux.NewRouter() 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( r.HandleFunc("/api/search", httputils.GenerateHandler(
"search.html", "search.html",
func(w http.ResponseWriter, r *http.Request) bool { func(w http.ResponseWriter, r *http.Request) bool {
@ -169,17 +94,18 @@ func main() {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
r.ParseMultipartForm(0) r.ParseMultipartForm(0)
query := r.FormValue("q") query := r.FormValue("q")
exactResults, otherResults, _ := Search(query) entries := Search(query)
jsonBytes, _ := json.Marshal(append(exactResults, otherResults...)) jsonBytes, _ := json.Marshal(entries)
fmt.Fprint(w, string(jsonBytes)) fmt.Fprint(w, string(jsonBytes))
return false return false
}, },
func(w http.ResponseWriter, r *http.Request) any { func(w http.ResponseWriter, r *http.Request) any {
r.ParseMultipartForm(0) r.ParseMultipartForm(0)
query := r.FormValue("q") query := r.FormValue("q")
return initSearchTemplateData(Search(query)) entry := Search(query)
return entry
}, },
[]string{http.MethodGet}, []string{http.MethodGet, http.MethodPost},
)) ))
r.Handle("/", http.FileServer(http.Dir("static"))) r.Handle("/", http.FileServer(http.Dir("static")))
log.Fatal(http.ListenAndServe(":3334", r)) log.Fatal(http.ListenAndServe(":3334", r))

View file

@ -5,11 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jidict</title> <title>jidict</title>
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.0.9/dist/missing.min.css"> <link rel="stylesheet" href="https://unpkg.com/missing.css@1.0.9/dist/missing.min.css">
<style>
li {
margin-top: 0.75em;
}
</style>
<script src="https://unpkg.com/htmx.org@1.9.3"></script> <script src="https://unpkg.com/htmx.org@1.9.3"></script>
</head> </head>
<body> <body>
@ -18,15 +13,12 @@
<img src="https://jichan.org/logo.svg" style="height: 4em; display: block; margin: 1em auto 1em auto"> <img src="https://jichan.org/logo.svg" style="height: 4em; display: block; margin: 1em auto 1em auto">
</a> </a>
<form <form
hx-get="/api/search" hx-post="/api/search"
hx-on::before-request="this.setAttribute('hx-replace-url', `/search/${this.querySelector('input').value}`)"
hx-target="#results" hx-target="#results"
hx-swap="innerHTML"> hx-swap="innerHTML">
<input type="text" name="q"{{ with .Query }} value="{{ . }}"{{ end }} placeholder="辞書をサーチする" class="width:100%" autocomplete="false"> <input type="text" name="q" placeholder="辞書をサーチする" class="width:100%" autocomplete="false">
</form> </form>
<div id="results"> <div id="results"></div>
{{ with .Results }}{{ template "search" . }}{{ end }}
</div>
<br> <br>
</main> </main>
</body> </body>

View file

@ -1,3 +0,0 @@
{{- define "definition" -}}
{{ if .PartOfSpeech }}<small><chip>{{ .PartOfSpeech }}</chip></small><br>{{ end }}{{ .Definition -}}
{{ end }}

View file

@ -1,11 +1,21 @@
{{- define "search" -}} <p><i>{{ $count := (len .) }}{{ if eq $count 0 }}No results{{ else }}{{ $count }} result{{ if ne $count 1}}s{{ end }}{{ end }}.</i></p>
<p><i>{{ 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 }}.</i></p> {{- range . -}}
{{- range .ExactResults -}} <div class="box">
{{- template "word" . -}} <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 }} {{- end }}
<hr> </ol>
{{ range .OtherResults -}} {{- end }}
{{ template "word" . }} </div>
{{- end -}} {{ end -}}
{{- end -}}
{{- template "search" . -}}

View file

@ -1,22 +0,0 @@
{{ define "word" }}
<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>{{- template "definition" (index .Definitions 0) -}}</p>
{{- else -}}
<ol>
{{- range .Definitions }}
<li>
{{ template "definition" . }}
</li>
{{- end }}
</ol>
{{- end }}
</div>
{{ end }}

View file

@ -3,69 +3,23 @@ package httputils
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"text/template" "text/template"
"time"
) )
type Handler = func(http.ResponseWriter, *http.Request) 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( func GenerateHandler(
file string, file string,
handler func(http.ResponseWriter, *http.Request) bool, handler func(http.ResponseWriter, *http.Request) bool,
data func(http.ResponseWriter, *http.Request) any, data func(http.ResponseWriter, *http.Request) any,
methods []string, methods []string,
) Handler { ) Handler {
return func(w http.ResponseWriter, r *http.Request) { var tmpl *template.Template
// All templates must be reloaded in case of dependencies if file != "" {
if reloadTemplates { tmpl = template.Must(template.ParseFiles(fmt.Sprintf("templates/%s", file)))
reloadTemplatesIfModified()
} }
return func(w http.ResponseWriter, r *http.Request) {
tmpl = template.Must(template.ParseFiles(fmt.Sprintf("templates/%s", file)))
for _, method := range methods { for _, method := range methods {
if method == r.Method { if method == r.Method {
goto ok goto ok
@ -75,9 +29,9 @@ func GenerateHandler(
return return
ok: ok:
renderTemplate := handler(w, r) renderTemplate := handler(w, r)
if renderTemplate && file != "" { if renderTemplate && tmpl != nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.ExecuteTemplate(w, file, data(w, r)) tmpl.Execute(w, data(w, r))
} }
} }
} }