Better template loading, word page

See https://stackoverflow.com/a/11468132
main
Elnu 2 years ago
parent f558d7f0c1
commit e70916c6c1

@ -115,6 +115,16 @@ func Search(query string) queryResult {
} }
} }
func Lookup(word string) *Entry {
for _, jmdictEntry := range dict.Entries {
entry := ParseEntry(jmdictEntry)
if entry.Kanji == word {
return &entry
}
}
return nil
}
type queryResult struct { type queryResult struct {
// Fields must be capitalized // Fields must be capitalized
// to be accessible in templates // to be accessible in templates
@ -134,9 +144,9 @@ func main() {
fmt.Println("JMdict loaded!") fmt.Println("JMdict loaded!")
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/", httputils.GenerateHandler( r.HandleFunc("/", httputils.GenerateHandler(
"index.html",
func(w http.ResponseWriter, r *http.Request) bool { return true }, func(w http.ResponseWriter, r *http.Request) bool { return true },
func(w http.ResponseWriter, r *http.Request) any { return nil }, httputils.NewTemplateSet("index.html"),
func(w http.ResponseWriter, r *http.Request) (string, any) { return "index.html", nil },
[]string{http.MethodGet}, []string{http.MethodGet},
)) ))
rawSearchHandler := func(w http.ResponseWriter, r *http.Request) { rawSearchHandler := func(w http.ResponseWriter, r *http.Request) {
@ -153,13 +163,6 @@ func main() {
r.HandleFunc("/search", rawSearchHandler) r.HandleFunc("/search", rawSearchHandler)
r.HandleFunc("/search/", rawSearchHandler) r.HandleFunc("/search/", rawSearchHandler)
r.HandleFunc("/search/{query}", httputils.GenerateHandler( 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 // handler whether or not to use template
func(w http.ResponseWriter, r *http.Request) bool { func(w http.ResponseWriter, r *http.Request) bool {
// If Accept: applicaiton/json we'll use the template // If Accept: applicaiton/json we'll use the template
@ -177,11 +180,44 @@ func main() {
return false return false
}, },
httputils.NewTemplateSet("index.html", "search.html"),
// template data // template data
func(w http.ResponseWriter, r *http.Request) any { func(w http.ResponseWriter, r *http.Request) (template string, data any) {
if r.Header.Get("HX-Request") == "" {
template = "search.html"
} else {
template = "search"
}
// Only runs if handler returns true // Only runs if handler returns true
query := mux.Vars(r)["query"] query := mux.Vars(r)["query"]
return Search(query) data = Search(query)
return
},
[]string{http.MethodGet},
))
rawWordHandler := func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Redirecting raw word handler")
http.Redirect(w, r, "/", http.StatusMovedPermanently)
}
r.HandleFunc("/word", rawWordHandler)
r.HandleFunc("/word/", rawWordHandler)
r.HandleFunc("/word/{word}", httputils.GenerateHandler(
func(w http.ResponseWriter, r *http.Request) bool { return true },
// Order matters
// word.html overrided the results block in index.html
// so should be loaded second
httputils.NewTemplateSet("index.html", "word.html"),
func(w http.ResponseWriter, r *http.Request) (template string, data any) {
template = "word.html"
query := mux.Vars(r)["word"]
data = struct {
Query any
Entry *Entry
}{
Query: nil,
Entry: Lookup(query),
}
return
}, },
[]string{http.MethodGet}, []string{http.MethodGet},
)) ))

@ -1,18 +1,25 @@
{{- define "index" -}}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ with .Query }}{{ . }} search - {{ end }}jidict</title> <title>{{ block "title" . }}{{ template "sitetitle" . }}{{ end }}</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> <style>
li { li {
margin-top: 0.75em; margin-top: 0.75em;
} }
#results .box h3 a {
color: inherit;
}
#results .box h3 a:not(:hover) {
text-decoration: none;
}
</style> </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 hx-boost="true">
<main> <main>
<a href="/"> <a href="/">
<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">
@ -20,13 +27,16 @@
<form <form
hx-get="/search" hx-get="/search"
hx-replace-url="true" hx-replace-url="true"
hx-target="#results"> hx-target="#results"
<input type="text" name="q"{{ with .Query }} value="{{ . }}"{{ end }} placeholder="辞書をサーチする" class="width:100%" autocomplete="false"> hx-on::before-request="document.title = `${this.querySelector('input').value} search - jidict`">
<input type="text" name="q" value="{{ block "value" . }}{{ end }}" placeholder="辞書をサーチする" class="width:100%" autocomplete="false" required>
</form> </form>
<div id="results"> <div id="results">
{{ if .Count }}{{ template "search" . }}{{ end }} {{ block "results" . }}{{ if .Query }}{{ template "search" . }}{{ end }}{{ end }}
</div> </div>
<br> <br>
</main> </main>
</body> </body>
</html> </html>
{{- end -}}
{{- template "index" . -}}

@ -0,0 +1,23 @@
{{ define "entry" }}
<div class="box">
<h3>
{{- if .Kanji -}}
<a href="/word/{{ .Kanji }}"><ruby>{{- .Kanji -}}<rp>(</rp><rt>{{- .Reading -}}</rt><rp>)</rp></ruby></a>
{{- else -}}
{{- .Reading -}}
{{- end -}}
</h3>
{{- $count := len .Definitions -}}
{{ if eq $count 1 -}}
<p>{{- template "definition" (index .Definitions 0) -}}</p>
{{- else if ne $count 0 -}}
<ol>
{{- range .Definitions }}
<li>
{{ template "definition" . }}
</li>
{{- end }}
</ol>
{{- end }}
</div>
{{ end }}

@ -0,0 +1,23 @@
{{ define "entryfull" }}
<div class="box">
<h3>
{{- if .Kanji -}}
<ruby>{{- .Kanji -}}<rp>(</rp><rt>{{- .Reading -}}</rt><rp>)</rp></ruby>
{{- else -}}
{{- .Reading -}}
{{- end -}}
</h3>
{{- $count := len .Definitions -}}
{{ if eq $count 1 -}}
<p>{{- template "definition" (index .Definitions 0) -}}</p>
{{- else if ne $count 0 -}}
<ol>
{{- range .Definitions }}
<li>
{{ template "definition" . }}
</li>
{{- end }}
</ol>
{{- end }}
</div>
{{ end }}

@ -0,0 +1,11 @@
{{- define "search" -}}
<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 .ExactResults -}}
{{- template "entry" . -}}
{{- end }}
{{ if and (ne (len .ExactResults) 0) (ne (len .OtherResults) 0) }}<hr>{{ end }}
{{ range .OtherResults -}}
{{ template "entry" . }}
{{- end -}}
{{- end -}}
{{- template "search" . -}}

@ -0,0 +1 @@
{{ define "sitetitle" }}jidict{{ end }}

@ -1,11 +1,9 @@
{{- define "search" -}} {{- define "title" }}{{ .Query }} search - {{ template "sitetitle" . }}{{- end -}}
<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 .ExactResults -}} {{- define "value" }}{{ .Query }}{{- end -}}
{{- template "word" . -}}
{{- end }} {{- define "results" -}}
{{ if and (ne (len .ExactResults) 0) (ne (len .OtherResults) 0) }}<hr>{{ end }} {{- template "entryfull" .Entry -}}
{{ range .OtherResults -}}
{{ template "word" . }}
{{- end -}} {{- end -}}
{{- end -}}
{{- template "search" . -}} {{- template "index" . -}}

@ -1,22 +1,5 @@
{{ define "word" }} {{- define "title" }}{{ .Entry.Kanji }} - {{ template "sitetitle" . }}{{- end -}}
<div class="box"> {{- define "results" -}}
<h3> {{- template "entryfull" .Entry -}}
{{- if .Kanji -}}
<ruby>{{- .Kanji -}}<rp>(</rp><rt>{{- .Reading -}}</rt><rp>)</rp></ruby>
{{- else -}}
{{- .Reading -}}
{{- end -}} {{- end -}}
</h3> {{- template "index" . -}}
{{ 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 }}

@ -1,25 +1,21 @@
package httputils package httputils
import ( import (
"bytes"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"text/template"
"time" "time"
) )
type Handler = func(http.ResponseWriter, *http.Request) type Handler = func(http.ResponseWriter, *http.Request)
const templateFolder = "templates" func getPartials() ([]string, map[string]time.Time, error) {
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) var modTimes map[string]time.Time = make(map[string]time.Time)
err := filepath.Walk(templateFolder, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(partialsFolder, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
@ -37,35 +33,15 @@ func getTemplates() ([]string, map[string]time.Time, error) {
return paths, modTimes, err 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 const reloadTemplates = true
func GenerateHandler( func GenerateHandler(
file interface{}, // either string or func() string
handler func(http.ResponseWriter, *http.Request) bool, handler func(http.ResponseWriter, *http.Request) bool,
data func(http.ResponseWriter, *http.Request) any, templateSet TemplateSet,
template func(http.ResponseWriter, *http.Request) (template string, data any),
methods []string, methods []string,
) Handler { ) Handler {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// All templates must be reloaded in case of dependencies
if reloadTemplates {
reloadTemplatesIfModified()
}
for _, method := range methods { for _, method := range methods {
if method == r.Method { if method == r.Method {
goto ok goto ok
@ -75,16 +51,18 @@ func GenerateHandler(
return return
ok: ok:
renderTemplate := handler(w, r) renderTemplate := handler(w, r)
if renderTemplate && file != "" { if renderTemplate {
var file_path string file_path, data := template(w, r)
switch file.(type) { buf := &bytes.Buffer{}
case string: err := templateSet.ExecuteTemplate(buf, file_path, reflect.ValueOf(data))
file_path = file.(string) if err != nil {
case func(http.ResponseWriter, *http.Request) string: fmt.Println(err)
file_path = file.(func(http.ResponseWriter, *http.Request) string)(w, r) w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "500 Internal Server Error")
return
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.ExecuteTemplate(w, file_path, data(w, r)) fmt.Fprint(w, buf.String())
} }
} }
} }

@ -0,0 +1,86 @@
package httputils
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"text/template"
"time"
)
type TemplateSet struct {
templates *template.Template
paths []string
modTimes map[string]time.Time
}
func newTemplateSet(partials *TemplateSet, paths ...string) TemplateSet {
var partialPaths []string
if partials == nil {
partialPaths = make([]string, 0)
} else {
partialPaths = partials.paths
}
allPaths := append(partialPaths, paths...)
modTimes := make(map[string]time.Time)
for _, path := range allPaths {
fileInfo, _ := os.Stat(path)
modTimes[path] = fileInfo.ModTime()
}
templates := template.Must(template.ParseFiles(allPaths...))
return TemplateSet{
templates: templates,
paths: allPaths,
modTimes: modTimes,
}
}
func NewTemplateSet(paths ...string) TemplateSet {
for i, path := range paths {
paths[i] = fmt.Sprintf("%s/%s", templateFolder, path)
}
return newTemplateSet(&partials, paths...)
}
func (templateSet *TemplateSet) ExecuteTemplate(wr io.Writer, name string, data any) error {
templateSet.reloadTemplatesIfModified()
return templateSet.templates.ExecuteTemplate(wr, name, data)
}
func (templateSet *TemplateSet) reloadTemplateIfModified(path string) {
fileInfo, _ := os.Stat(path)
modTime := fileInfo.ModTime()
if modTime.After(templateSet.modTimes[path]) {
fmt.Printf("Reloading template %s...\n", path)
templateSet.templates.ParseFiles(path)
templateSet.modTimes[path] = modTime
}
}
func (templateSet *TemplateSet) reloadTemplatesIfModified() {
for _, path := range templateSet.paths {
templateSet.reloadTemplateIfModified(path)
}
}
func getTemplatePathsInDirectory(directory string) (paths []string, err error) {
paths = make([]string, 0)
err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(path, ".html") {
paths = append(paths, path)
}
return nil
})
return
}
const templateFolder = "templates"
const partialsFolder = templateFolder + "/partials"
var paths, _ = getTemplatePathsInDirectory(partialsFolder)
var partials = newTemplateSet(nil, paths...)