diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 151971c..2c4ef98 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,28 +1,28 @@ name: Go -on: [push] +on: [push, pull_request] jobs: build: name: Build runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.21', '1.22', '1.23'] + steps: + - name: Check out code + uses: actions/checkout@v4 - - name: Set up Go 1.12 - uses: actions/setup-go@v1 + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 with: - go-version: 1.12 - id: go + go-version: ${{ matrix.go-version }} - - name: Check out code into the Go module directory - uses: actions/checkout@v1 + - name: Build + run: go build -v ./... - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi + - name: Test + run: go test -v ./... - - name: Build - run: go build -v . + - name: Vet + run: go vet ./... diff --git a/compile.go b/compile.go index ce5a21c..f170698 100644 --- a/compile.go +++ b/compile.go @@ -7,7 +7,6 @@ import ( "github.com/wendal/errors" "github.com/wendal/mustache" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -433,7 +432,9 @@ func WriteTo(url string, content string) { if HTML_EXT != "" { content += HTML_EXT } - ioutil.WriteFile(dstPath, []byte(content), os.ModePerm) + if err := os.WriteFile(dstPath, []byte(content), os.ModePerm); err != nil { + log.Println("ERR WriteTo", dstPath, err) + } } func PrapareAssets(theme string, layoutName string, topCtx mustache.Context) string { @@ -526,7 +527,7 @@ func copyDir(src string, target string) error { if !fst.IsDir() { return nil } - finfos, err := ioutil.ReadDir(src) + finfos, err := os.ReadDir(src) if err != nil { log.Println(err) return err diff --git a/config.go b/config.go index 770f2af..a47f6a1 100644 --- a/config.go +++ b/config.go @@ -5,7 +5,6 @@ import ( "encoding/json" "github.com/wendal/goyaml2" "io" - "io/ioutil" "log" "os" ) @@ -37,7 +36,7 @@ func ReadYml(path string) (cnf map[string]interface{}, err error) { func ReadYmlReader(r io.Reader) (cnf map[string]interface{}, err error) { err = nil - buf, err := ioutil.ReadAll(r) + buf, err := io.ReadAll(r) if err != nil || len(buf) < 3 { return } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cea821f --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/wendal/gor + +go 1.24.13 + +require ( + github.com/howeyc/fsnotify v0.9.0 + github.com/russross/blackfriday v1.6.0 + github.com/wendal/errors v0.0.0-20181209125328-7f31f4b264ec + github.com/wendal/goyaml2 v0.0.0-20190215120833-ee753311eeae + github.com/wendal/mustache v0.0.0-20130201095610-61b948aeb760 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..83e9ffa --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY= +github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/wendal/errors v0.0.0-20181209125328-7f31f4b264ec h1:bua919NvciYmjqfeZMsVkXTny1QvXMrri0X6NlqILRs= +github.com/wendal/errors v0.0.0-20181209125328-7f31f4b264ec/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= +github.com/wendal/goyaml2 v0.0.0-20190215120833-ee753311eeae h1:ke3nvVbPZDAsGLqBukiPAPdzgllyrQAZ4OtdzTf3z5U= +github.com/wendal/goyaml2 v0.0.0-20190215120833-ee753311eeae/go.mod h1:CoOAlRIb5zPgRa7NEsqTVNu/26+FJ1jrIj3/NWU3hM8= +github.com/wendal/mustache v0.0.0-20130201095610-61b948aeb760 h1:yBVQh3XiGSrCTosbppoAmxpCV2fnqE0bUjxVF9iqVdc= +github.com/wendal/mustache v0.0.0-20130201095610-61b948aeb760/go.mod h1:e+ZfVXOi5GtWL3bAoE3ErzEnNyguYqHVegUqgbFtMZM= diff --git a/gor/gor.go b/gor/gor.go index 29cbb14..b13ffe0 100644 --- a/gor/gor.go +++ b/gor/gor.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/howeyc/fsnotify" "github.com/wendal/gor" - "io/ioutil" "log" "net/http" "os" @@ -125,8 +124,14 @@ func main() { _compile() } case ".update.zip.go": - d, _ := ioutil.ReadFile("gor-content.zip") - _zip, _ := os.OpenFile("zip.go", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) + d, err := os.ReadFile("gor-content.zip") + if err != nil { + log.Fatal("failed to read gor-content.zip:", err) + } + _zip, err := os.OpenFile("zip.go", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm) + if err != nil { + log.Fatal("failed to open zip.go for writing:", err) + } header := `package main const INIT_ZIP="` _zip.Write([]byte(header)) diff --git a/gor/new.go b/gor/new.go index 601b65a..113f556 100644 --- a/gor/new.go +++ b/gor/new.go @@ -5,7 +5,6 @@ import ( "bytes" "encoding/base64" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -23,7 +22,10 @@ func CmdInit(path string) { } decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(INIT_ZIP)) - b, _ := ioutil.ReadAll(decoder) + b, err := io.ReadAll(decoder) + if err != nil { + log.Fatalf("failed to read embedded init zip data: %v", err) + } z, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) if err != nil { diff --git a/gor_test.go b/gor_test.go new file mode 100644 index 0000000..68da20b --- /dev/null +++ b/gor_test.go @@ -0,0 +1,207 @@ +package gor + +import ( + "testing" + "time" +) + +func TestEncodePathInfo(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"hello world", "hello-world"}, + {"go:lang", "go-lang"}, + {"simple", "simple"}, + {"a b:c", "a-b-c"}, + } + for _, tt := range tests { + got := EncodePathInfo(tt.input) + if got != tt.expected { + t.Errorf("EncodePathInfo(%q) = %q, want %q", tt.input, got, tt.expected) + } + } +} + +func TestDecodePathInfo(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"hello%20world", "hello world"}, + {"go-lang", "go-lang"}, + {"simple", "simple"}, + } + for _, tt := range tests { + got := DecodePathInfo(tt.input) + if got != tt.expected { + t.Errorf("DecodePathInfo(%q) = %q, want %q", tt.input, got, tt.expected) + } + } +} + +func TestEncodeDecodePathInfo(t *testing.T) { + original := "hello world" + encoded := EncodePathInfo(original) + // EncodePathInfo replaces spaces with dashes, not url-encode, so round-trip differs + if encoded == original { + t.Errorf("EncodePathInfo should transform %q", original) + } +} + +func TestAsStrings(t *testing.T) { + tests := []struct { + name string + input interface{} + expected []string + }{ + {"nil", nil, []string{}}, + {"string", "hello", []string{"hello"}}, + {"string slice", []string{"a", "b"}, []string{"a", "b"}}, + {"interface slice", []interface{}{"x", "y"}, []string{"x", "y"}}, + } + for _, tt := range tests { + got := AsStrings(tt.input) + if len(got) != len(tt.expected) { + t.Errorf("[%s] AsStrings(%v) len=%d, want %d", tt.name, tt.input, len(got), len(tt.expected)) + continue + } + for i := range got { + if got[i] != tt.expected[i] { + t.Errorf("[%s] AsStrings(%v)[%d] = %q, want %q", tt.name, tt.input, i, got[i], tt.expected[i]) + } + } + } +} + +func TestCreatePostURL(t *testing.T) { + db := make(map[string]interface{}) + date := time.Date(2023, 5, 15, 0, 0, 0, 0, time.Local) + post := map[string]interface{}{ + "permalink": "/:year/:month/:title/", + "_date": date, + "title": "Hello World", + "id": "posts/2023-05-15-hello-world.md", + "categories": []string{"go"}, + } + CreatePostURL(db, "/", post) + url, ok := post["url"].(string) + if !ok { + t.Fatal("post[url] should be a string") + } + if url != "/2023/05/Hello-World/" { + t.Errorf("unexpected url: %q", url) + } +} + +func TestCreatePostURL_StaticPermalink(t *testing.T) { + db := make(map[string]interface{}) + date := time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local) + post := map[string]interface{}{ + "permalink": "/about/", + "_date": date, + "title": "About", + "id": "pages/about.md", + "categories": []string{}, + } + CreatePostURL(db, "/", post) + url, ok := post["url"].(string) + if !ok { + t.Fatal("post[url] should be a string") + } + if url != "/about/" { + t.Errorf("unexpected url: %q", url) + } +} + +func TestMarkdownToHtml(t *testing.T) { + tests := []struct { + name string + input string + contains string + }{ + {"bold", "**hello**", "hello"}, + {"header", "# Title", "

Title

"}, + {"paragraph", "simple text", "simple text"}, + } + for _, tt := range tests { + got := MarkdownToHtml(tt.input) + if len(got) == 0 { + t.Errorf("[%s] MarkdownToHtml(%q) returned empty string", tt.name, tt.input) + continue + } + found := false + for i := 0; i <= len(got)-len(tt.contains); i++ { + if got[i:i+len(tt.contains)] == tt.contains { + found = true + break + } + } + if !found { + t.Errorf("[%s] MarkdownToHtml(%q) = %q, want it to contain %q", tt.name, tt.input, got, tt.contains) + } + } +} + +func TestSortPosts(t *testing.T) { + date1 := time.Date(2023, 1, 1, 0, 0, 0, 0, time.Local) + date2 := time.Date(2023, 6, 1, 0, 0, 0, 0, time.Local) + dict := map[string]Mapper{ + "posts/older.md": {"id": "posts/older.md", "_date": date1}, + "posts/newer.md": {"id": "posts/newer.md", "_date": date2}, + } + ids := []string{"posts/older.md", "posts/newer.md"} + sorted := SortPosts(dict, ids) + if sorted[0] != "posts/newer.md" { + t.Errorf("SortPosts: expected newer post first, got %v", sorted) + } +} + +func TestMapperGetString(t *testing.T) { + m := Mapper{"key": "value", "num": 42} + if m.GetString("key") != "value" { + t.Errorf("GetString(key) should return 'value'") + } + if m.GetString("missing") != "" { + t.Errorf("GetString(missing) should return ''") + } + if m.GetString("num") != "42" { + t.Errorf("GetString(num) should return '42'") + } +} + +func TestMapperGetInt(t *testing.T) { + m := Mapper{"a": int64(5), "b": 10, "c": "x"} + if m.GetInt("a") != 5 { + t.Errorf("GetInt(a) should return 5") + } + if m.GetInt("b") != 10 { + t.Errorf("GetInt(b) should return 10") + } + if m.GetInt("missing") != 0 { + t.Errorf("GetInt(missing) should return 0") + } +} + +func TestPostPath(t *testing.T) { + tests := []struct { + title string + contains string + }{ + {"my post", "my-post"}, + {"hello-world", "hello-world"}, + } + for _, tt := range tests { + got := postPath(tt.title) + found := false + for i := 0; i <= len(got)-len(tt.contains); i++ { + if got[i:i+len(tt.contains)] == tt.contains { + found = true + break + } + } + if !found { + t.Errorf("postPath(%q) = %q, want it to contain %q", tt.title, got, tt.contains) + } + } +} diff --git a/map2struct.go b/map2struct.go index dee4e1e..71cd161 100644 --- a/map2struct.go +++ b/map2struct.go @@ -40,7 +40,7 @@ func ToStruct(m map[string]interface{}, val reflect.Value) { if _str, ok := v.(string); ok { field.SetString(_str) } else { - field.SetString(fmt.Sprint("%v", v)) + field.SetString(fmt.Sprintf("%v", v)) } case reflect.Int: fallthrough diff --git a/payload.go b/payload.go index a663ab7..7c403d1 100644 --- a/payload.go +++ b/payload.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "log" URL "net/url" "os" @@ -478,7 +477,7 @@ func ReadMuPage(path string) (ctx map[string]interface{}, err error) { return } - d, err := ioutil.ReadAll(br) + d, err := io.ReadAll(br) if err != nil { err = errors.New(path + " --> " + err.Error()) return @@ -633,7 +632,9 @@ func LoadLayouts(root string, theme string) map[string]Mapper { if err != nil { return err } - f.Seek(0, os.SEEK_SET) + if _, err = f.Seek(0, io.SeekStart); err != nil { + return err + } if string(buf) == "---" { layout, err = ReadMuPage(path) if err != nil { diff --git a/payload_test.go b/payload_test.go index 3a413a6..89725c8 100644 --- a/payload_test.go +++ b/payload_test.go @@ -1,6 +1,7 @@ package gor import ( + "os" "testing" ) @@ -11,7 +12,11 @@ func TestDebug(*testing.T) { } func TestPayLoad(t *testing.T) { - _, err := MakePayLoad("H:/wendal_net") + root := "H:/wendal_net" + if _, err := os.Stat(root); os.IsNotExist(err) { + t.Skip("skipping: test data directory not available:", root) + } + _, err := MakePayLoad(root) if err != nil { t.Error(err) } diff --git a/post.go b/post.go index 9de027b..ec16128 100644 --- a/post.go +++ b/post.go @@ -3,7 +3,6 @@ package gor import ( "fmt" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -165,7 +164,7 @@ func CreateNewPost(title string) (path string) { fmt.Printf("Create Post File Error: \n %v\n", err) log.Fatal("Post File Exist ?: ", path) } - err = ioutil.WriteFile(path, []byte(fmt.Sprintf(TPL_NEW_POST, title, time.Now().Format("2006-01-02"))), os.ModePerm) + err = os.WriteFile(path, []byte(fmt.Sprintf(TPL_NEW_POST, title, time.Now().Format("2006-01-02"))), os.ModePerm) if err != nil { log.Fatal(err) } @@ -251,7 +250,7 @@ func AddImgs(title, imgsrc string, date string) { } func cpPostImgs(post string, imgsrc string, cfg Mapper) (imgtag []string) { - files, err := ioutil.ReadDir(imgsrc) + files, err := os.ReadDir(imgsrc) if files == nil || err != nil { log.Println("no img file exists.") return nil diff --git a/widgets.go b/widgets.go index 6044bef..11fcbf2 100644 --- a/widgets.go +++ b/widgets.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/wendal/errors" "github.com/wendal/mustache" - "io/ioutil" + "io" "log" "os" "path/filepath" @@ -288,7 +288,7 @@ func BuildCustomWidget(name string, dir string, cnf Mapper) (Widget, []string, e return nil, nil, errors.New("Fail to load Widget Layout" + dir + "\n" + err.Error()) } defer f.Close() - cont, err := ioutil.ReadAll(f) + cont, err := io.ReadAll(f) if err != nil { return nil, nil, errors.New("Fail to load Widget Layout" + dir + "\n" + err.Error()) }