Repos / s4g / 3795bcef38
commit 3795bcef3804a456d93c3464b88c03a217a4e024
Author: Nhân <hi@imnhan.com>
Date: Wed Jul 19 00:33:34 2023 +0700
standardize recoverable user errors as UserFileErr
diff --git a/README.md b/README.md
index 1945f08..34f8944 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
- [x] Livereload with no browser plugin (works but currently polls which is
noisy, should probably upgrade to websockets)
-- [ ] Shows user error messages on the livereloaded web page
+- [x] Shows user error messages on the livereloaded web page
- [ ] Just enough GUI so user doesn't have to touch a terminal
- [ ] 1-click deploy to popular static hosting targets (git push, rsync, etc.)
diff --git a/errors.go b/errors.go
deleted file mode 100644
index a698240..0000000
--- a/errors.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package main
-
-import (
- "fmt"
- "html/template"
-)
-
-type UserFileErr struct {
- File string
- Field string
- Msg string
-}
-
-func (e *UserFileErr) Error() string {
- return fmt.Sprintf("UserFileErr - %s - %s: %s", e.File, e.Field, e.Msg)
-}
-
-func (e *UserFileErr) Html() template.HTML {
- return template.HTML(fmt.Sprintf(
- "<p>In file <b>%s</b>, field <b>%s</b>: %s </p>",
- e.File, e.Field, e.Msg,
- ))
-}
diff --git a/errs/errs.go b/errs/errs.go
new file mode 100644
index 0000000..6ad5572
--- /dev/null
+++ b/errs/errs.go
@@ -0,0 +1,45 @@
+package errs
+
+import (
+ "fmt"
+ "html/template"
+)
+
+// Represents a user input error, which in webmaker2000's case is almost
+// always some malformed file.
+type UserFileErr struct {
+ File string
+ Msg string
+
+ // Optional. Zero value means unavailable.
+ Line int
+
+ // Optional. Zero value means unavailable.
+ Column int
+
+ // Optional. Zero value means unavailable.
+ Field string
+}
+
+func (e *UserFileErr) Error() string {
+ return fmt.Sprintf(
+ "UserFileErr: %s - %d:%d:%s %s",
+ e.File, e.Line, e.Column, e.Field, e.Msg,
+ )
+}
+
+func (e *UserFileErr) Html() template.HTML {
+ content := fmt.Sprintf("In file <b>%s</b>", e.File)
+ if e.Line != 0 {
+ content += fmt.Sprintf(", line %d", e.Line)
+ }
+ if e.Column != 0 {
+ content += fmt.Sprintf(", column %d", e.Column)
+ }
+ if e.Field != "" {
+ content += fmt.Sprintf(", field <b>%s</b>", e.Field)
+ }
+
+ content = fmt.Sprintf("<p>%s: %s</p>", content, e.Msg)
+ return template.HTML(content)
+}
diff --git a/livereload/error.go b/livereload/error.go
index ac650db..ac929ec 100644
--- a/livereload/error.go
+++ b/livereload/error.go
@@ -3,8 +3,11 @@
import (
"bytes"
_ "embed"
+ "errors"
"html/template"
"net/http"
+
+ "go.imnhan.com/webmaker2000/errs"
)
//go:embed error.html
@@ -12,26 +15,27 @@
var errTmpl = template.Must(template.New("error").Parse(errorTmpl))
-// Error that has a user-friendly HTML representation.
-type htmlErr interface {
- error
- Html() template.HTML
+type errTmplInput struct {
+ Text string
+ Html template.HTML
}
-func serveError(w http.ResponseWriter, r *http.Request, err error) {
+func serveError(w http.ResponseWriter, r *http.Request, e error) {
var buf bytes.Buffer
- _, ok := err.(htmlErr)
+ var uerr *errs.UserFileErr
+ ok := errors.As(e, &uerr)
+
+ var tmplInput errTmplInput
if ok {
- errTmpl.Execute(&buf, err)
+ tmplInput.Text = uerr.Error()
+ tmplInput.Html = uerr.Html()
} else {
- // Shim for errors that don't support HTML output
- errTmpl.Execute(&buf, struct {
- Error string
- Html template.HTML
- }{
- Error: err.Error(),
- Html: template.HTML(err.Error()),
- })
+ tmplInput.Text = e.Error()
+ tmplInput.Html = template.HTML(e.Error())
+ }
+ err := errTmpl.Execute(&buf, tmplInput)
+ if err != nil {
+ panic(err)
}
body := withLiveReload(buf.Bytes())
w.Write(body)
diff --git a/livereload/error.html b/livereload/error.html
index e59eb99..ab65a2d 100644
--- a/livereload/error.html
+++ b/livereload/error.html
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
- <title>{{.Error}}</title>
+ <title>{{.Text}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
diff --git a/main.go b/main.go
index 05cbe31..95f00f1 100644
--- a/main.go
+++ b/main.go
@@ -18,6 +18,7 @@
"time"
"go.imnhan.com/webmaker2000/djot"
+ "go.imnhan.com/webmaker2000/errs"
"go.imnhan.com/webmaker2000/gui"
"go.imnhan.com/webmaker2000/livereload"
"go.imnhan.com/webmaker2000/writablefs"
@@ -30,7 +31,7 @@
func main() {
invalidCommand := func() {
- fmt.Println("Usage: webfolder2000 new|serve [...]")
+ fmt.Println("Usage: webfolder2000 new|serve|gui [...]")
os.Exit(1)
}
@@ -210,7 +211,7 @@ func regenerate(fsys writablefs.FS) (site *SiteMetadata, err error) {
for _, link := range site.NavbarLinks {
a, ok := articles[link]
if !ok {
- return nil, &UserFileErr{
+ return nil, &errs.UserFileErr{
File: SiteFileName,
Field: "NavbarLinks",
Msg: fmt.Sprintf(`"%s" does not exist`, link),
@@ -383,9 +384,10 @@ func findArticles(fsys writablefs.FS, site *SiteMetadata) (map[string]Article, e
},
ShowInFeed: true,
}
- err = UnmarshalMetadata(metaText, &meta)
- if err != nil {
- return err
+ userErr := UnmarshalMetadata(metaText, &meta)
+ if userErr != nil {
+ userErr.File = path
+ return fmt.Errorf("findArticles failed to unmarshall metadata: %w", userErr)
}
article := Article{
diff --git a/metadata.go b/metadata.go
index 6050bc7..2952388 100644
--- a/metadata.go
+++ b/metadata.go
@@ -11,6 +11,7 @@
"strings"
"time"
+ "go.imnhan.com/webmaker2000/errs"
"go.imnhan.com/webmaker2000/writablefs"
)
@@ -64,7 +65,7 @@ func ReadSiteMetadata(fsys writablefs.FS) (*SiteMetadata, error) {
}
// Similar API to json.Unmarshal but supports neither struct tags nor nesting.
-func UnmarshalMetadata(data []byte, dest any) error {
+func UnmarshalMetadata(data []byte, dest any) *errs.UserFileErr {
m := metaTextToMap(data)
s := reflect.ValueOf(dest).Elem()
@@ -81,7 +82,7 @@ func UnmarshalMetadata(data []byte, dest any) error {
case "int":
intVal, err := strconv.Atoi(val)
if err != nil {
- return &UserFileErr{
+ return &errs.UserFileErr{
Field: fieldName,
Msg: fmt.Sprintf(`invalid int: "%s"`, err),
}
@@ -90,7 +91,7 @@ func UnmarshalMetadata(data []byte, dest any) error {
case "bool":
if val != "true" && val != "false" {
- return &UserFileErr{
+ return &errs.UserFileErr{
Field: fieldName,
Msg: fmt.Sprintf(
`invalid boolean: expected true/false, got "%s"`,
@@ -104,7 +105,7 @@ func UnmarshalMetadata(data []byte, dest any) error {
tVal, err := time.ParseInLocation("2006-01-02", val, time.Local)
tVal = tVal.Local()
if err != nil {
- return &UserFileErr{
+ return &errs.UserFileErr{
Field: fieldName,
Msg: fmt.Sprintf(
`invalid date: expected YYYY-MM-DD, got "%s"`, val,