diff --git a/examples/Article/A dress a day.md b/examples/Article/A dress a day.md index 7025759..668aa2f 100644 --- a/examples/Article/A dress a day.md +++ b/examples/Article/A dress a day.md @@ -5,6 +5,7 @@ url: https://dressaday.com/2006/10/20/you-dont-have-to-be-pretty/ rating: 5 date: December 7, 2023 image: https://i0.wp.com/old-dressaday-images.s3-website-us-west-2.amazonaws.com/6a0133ed1b1479970b0134809d9f8b970c.jpg +reviewRating.ratingValue: 5 --- # You Dont Have to Be Pretty – A Dress A Day diff --git a/examples/Books/NoAuthor.md b/examples/Books/NoAuthor.md index fece565..af24dc7 100644 --- a/examples/Books/NoAuthor.md +++ b/examples/Books/NoAuthor.md @@ -9,7 +9,7 @@ itemReviewed: @type: Book name: Anonymous Poems reviewBody: "Short, haunting, and powerful verses." -reviewRating: 4/5 +reviewRating.ratingValue: 4 --- # Anonymous Poems diff --git a/examples/Books/NoBody.md b/examples/Books/NoBody.md index 0953908..fc0bc0b 100644 --- a/examples/Books/NoBody.md +++ b/examples/Books/NoBody.md @@ -11,7 +11,7 @@ itemReviewed: author: @type: Person name: George Orwell -reviewRating: 10/10 +reviewRating: 5 --- # 1984 diff --git a/parser/decoders/hashtags.go b/parser/decoders/hashtags.go index d9e7f94..e0e63fa 100644 --- a/parser/decoders/hashtags.go +++ b/parser/decoders/hashtags.go @@ -15,5 +15,10 @@ func Keywords(input string, block template.Block) (value any, error error) { tags = append(tags, tag) } } + + if len(tags) == 0 { + return nil, nil + } + return tags, nil } diff --git a/parser/go.mod b/parser/go.mod index 8dba3bf..fe539c6 100644 --- a/parser/go.mod +++ b/parser/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/parser -go 1.24.3 +go 1.24.5 require ( git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 @@ -14,6 +14,8 @@ require ( ) require ( + git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e + git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a github.com/google/go-cmp v0.7.0 go.yaml.in/yaml/v4 v4.0.0-rc.1 ) diff --git a/parser/go.sum b/parser/go.sum index f65191b..7468956 100644 --- a/parser/go.sum +++ b/parser/go.sum @@ -1,7 +1,11 @@ git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 h1:oe7Xb8dE43S8mRla5hfEqagMnvhvEVHsvRlzl2v540w= git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567/go.mod h1:qGWl42P8mgEktfor/IjQp0aS9SqmpeIlhSuVTlUOXLQ= +git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e h1:9Eg81l8YMTXWZC3xlZ5L/NJRuK26bksrVtEHyCTV4sM= +git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e/go.mod h1:mjJqEqALg4YJoiebk3V21yJVUVEs3K2RiLO/IW6DGCM= git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567 h1:XIx89KqTgd/h14oe5mLvT9E8+jGEAjWgudqiMtQdcec= git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567/go.mod h1:Uxi5xcxtnjopsIZjjMlFaWJGuglB9JNL++FuaSbOf6U= +git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a h1:LAU2LlLZ96s8hcg1OEGD5HBshDspWVwWTa7YG5+A70w= +git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a/go.mod h1:88SkY5pTONkgfBy1FT10LoqRC8rt36iF1fk/rupjuJY= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= diff --git a/registry/go.mod b/registry/go.mod index 61e060e..7364278 100644 --- a/registry/go.mod +++ b/registry/go.mod @@ -1,3 +1,3 @@ module git.max-richter.dev/max/marka/registry -go 1.24.3 +go 1.24.5 diff --git a/registry/go.sum b/registry/go.sum index b46b517..e69de29 100644 --- a/registry/go.sum +++ b/registry/go.sum @@ -1,4 +0,0 @@ -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/registry/templates/Article.marka b/registry/templates/Article.marka index f6175f3..4be4c66 100644 --- a/registry/templates/Article.marka +++ b/registry/templates/Article.marka @@ -22,6 +22,16 @@ hidden: true - path: datePublished - path: articleSection + - path: reviewRating.ratingValue + pathAlias: rating + - path: reviewRating.bestRating + codec: const + value: 5 + hidden: true + - path: reviewRating.worstRating + codec: const + value: 1 + hidden: true } --- diff --git a/registry/templates/Review.marka b/registry/templates/Review.marka index cf49de2..d3f94db 100644 --- a/registry/templates/Review.marka +++ b/registry/templates/Review.marka @@ -39,4 +39,5 @@ # { itemReviewed.name } { keywords | hashtags } +## Review { reviewBody } diff --git a/renderer/go.mod b/renderer/go.mod index 2594145..558a176 100644 --- a/renderer/go.mod +++ b/renderer/go.mod @@ -1,3 +1,18 @@ module git.max-richter.dev/max/marka/renderer -go 1.24.3 +go 1.24.5 + +require ( + git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e + git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e + git.max-richter.dev/max/marka/template v0.0.0-20250819170608-69c2550f448e + git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a + git.max-richter.dev/max/marka/validator v0.0.0-20250819170608-69c2550f448e + github.com/google/go-cmp v0.7.0 + go.yaml.in/yaml/v4 v4.0.0-rc.1 +) + +require ( + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/renderer/go.sum b/renderer/go.sum new file mode 100644 index 0000000..11e46b8 --- /dev/null +++ b/renderer/go.sum @@ -0,0 +1,20 @@ +git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e h1:enZufetD3UoIVTnTNTQSFlr1Ir0jG7wObUAxb6+xwWg= +git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e/go.mod h1:xQK6tsgr9BOoeFw8JxjBwDkVENlOqapmcRkYyf/L+SQ= +git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e h1:eXAE0JHDvLGqtYSSlX5mw1XAuK+Cmu74c52PyveRhlE= +git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e/go.mod h1:n793S7TENIfgHpZLz0lm0qorM7eCx3zBLby3Fb++hZA= +git.max-richter.dev/max/marka/template v0.0.0-20250819170608-69c2550f448e h1:6327yeKE0dYbwsEBhIFcXOJEWTxBUWGBeB0uj9BTJqA= +git.max-richter.dev/max/marka/template v0.0.0-20250819170608-69c2550f448e/go.mod h1:Uxi5xcxtnjopsIZjjMlFaWJGuglB9JNL++FuaSbOf6U= +git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a h1:LAU2LlLZ96s8hcg1OEGD5HBshDspWVwWTa7YG5+A70w= +git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a/go.mod h1:88SkY5pTONkgfBy1FT10LoqRC8rt36iF1fk/rupjuJY= +git.max-richter.dev/max/marka/validator v0.0.0-20250819170608-69c2550f448e h1:MBxIk3jvbjSmpZk7xlR6Yog61375PMya3FzOxZ5TuYs= +git.max-richter.dev/max/marka/validator v0.0.0-20250819170608-69c2550f448e/go.mod h1:qdGfCFRzsGedmnd77vb7pu/EMx0W0DcQBMEfvNxMYsw= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +go.yaml.in/yaml/v4 v4.0.0-rc.1 h1:4J1+yLKUIPGexM/Si+9d3pij4hdc7aGO04NhrElqXbY= +go.yaml.in/yaml/v4 v4.0.0-rc.1/go.mod h1:CBdeces52/nUXndfQ5OY8GEQuNR9uEEOJPZj/Xq5IzU= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/server/.air.toml b/server/.air.toml new file mode 100644 index 0000000..c8e2c18 --- /dev/null +++ b/server/.air.toml @@ -0,0 +1,32 @@ +# Config file for Air - https://github.com/air-verse/air +# This file is for the marka-server application. + +root = "." +tmp_dir = "tmp" + +[build] +# Command to build the application. +cmd = "go build -o ./tmp/marka-server ./cmd/marka-server" + +# The binary to run. +bin = "./tmp/marka-server" + +# Command to run the application with arguments. +full_bin = "./tmp/marka-server -root=../examples -addr=:8080" + +# Watch these file extensions. +include_ext = ["go", "http"] + +# Ignore these directories. +exclude_dir = ["tmp"] + +# Log file for build errors. +log = "air_errors.log" + +[log] +# Show time in logs. +time = true + +[misc] +# Delete tmp directory on exit. +clean_on_exit = true diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/server/cmd/marka-server/main.go b/server/cmd/marka-server/main.go index ee01e49..ef8a90a 100644 --- a/server/cmd/marka-server/main.go +++ b/server/cmd/marka-server/main.go @@ -17,13 +17,13 @@ func main() { absRoot, err := filepath.Abs(*root) must(err) + info, err := os.Stat(absRoot) must(err) if !info.IsDir() { log.Fatal("root is not a directory") } - // Catch-all route from "/" → serve files rooted in absRoot http.Handle("/", handlers.NewFile(absRoot)) log.Printf("listening on %s, root=%s", *addr, absRoot) diff --git a/server/go.mod b/server/go.mod index 85f4d32..250391c 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,3 +1,16 @@ module git.max-richter.dev/max/marka/server -go 1.24.3 +go 1.24.5 + +require git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e + +require ( + git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 // indirect + git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e // indirect + git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567 // indirect + git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.1 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..8678458 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,22 @@ +git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e h1:enZufetD3UoIVTnTNTQSFlr1Ir0jG7wObUAxb6+xwWg= +git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e/go.mod h1:xQK6tsgr9BOoeFw8JxjBwDkVENlOqapmcRkYyf/L+SQ= +git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 h1:oe7Xb8dE43S8mRla5hfEqagMnvhvEVHsvRlzl2v540w= +git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567/go.mod h1:qGWl42P8mgEktfor/IjQp0aS9SqmpeIlhSuVTlUOXLQ= +git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e h1:9Eg81l8YMTXWZC3xlZ5L/NJRuK26bksrVtEHyCTV4sM= +git.max-richter.dev/max/marka/renderer v0.0.0-20250819170608-69c2550f448e/go.mod h1:mjJqEqALg4YJoiebk3V21yJVUVEs3K2RiLO/IW6DGCM= +git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567 h1:XIx89KqTgd/h14oe5mLvT9E8+jGEAjWgudqiMtQdcec= +git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567/go.mod h1:Uxi5xcxtnjopsIZjjMlFaWJGuglB9JNL++FuaSbOf6U= +git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a h1:LAU2LlLZ96s8hcg1OEGD5HBshDspWVwWTa7YG5+A70w= +git.max-richter.dev/max/marka/testdata v0.0.0-20250819195334-b3c01bb43d9a/go.mod h1:88SkY5pTONkgfBy1FT10LoqRC8rt36iF1fk/rupjuJY= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +go.yaml.in/yaml/v4 v4.0.0-rc.1 h1:4J1+yLKUIPGexM/Si+9d3pij4hdc7aGO04NhrElqXbY= +go.yaml.in/yaml/v4 v4.0.0-rc.1/go.mod h1:CBdeces52/nUXndfQ5OY8GEQuNR9uEEOJPZj/Xq5IzU= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/server/http/.env b/server/http/.env new file mode 100644 index 0000000..d417754 --- /dev/null +++ b/server/http/.env @@ -0,0 +1 @@ +SERVER_URL=http://localhost:8080 diff --git a/server/http/post-recipe.http b/server/http/post-recipe.http new file mode 100644 index 0000000..45154e2 --- /dev/null +++ b/server/http/post-recipe.http @@ -0,0 +1,5 @@ +POST {{SERVER_URL}}/Recipes/bolognaise.md + +{ + +} diff --git a/server/internal/fsx/contenttype.go b/server/internal/fsx/contenttype.go index 87a9d8b..2288c93 100644 --- a/server/internal/fsx/contenttype.go +++ b/server/internal/fsx/contenttype.go @@ -6,6 +6,17 @@ import ( "strings" ) +var textPlainExtensions = map[string]bool{ + ".txt": true, + ".log": true, + ".json": true, + ".yaml": true, + ".yml": true, + ".toml": true, + ".xml": true, + ".csv": true, +} + func ContentTypeFor(name string) string { ext := strings.ToLower(filepath.Ext(name)) switch ext { @@ -15,8 +26,7 @@ func ContentTypeFor(name string) string { if ct := mime.TypeByExtension(ext); ct != "" { return ct } - switch ext { - case ".txt", ".log", ".json", ".yaml", ".yml", ".toml", ".xml", ".csv": + if textPlainExtensions[ext] { return "text/plain; charset=utf-8" } return "application/octet-stream" diff --git a/server/internal/fsx/path.go b/server/internal/fsx/path.go index 3d8d089..6916a16 100644 --- a/server/internal/fsx/path.go +++ b/server/internal/fsx/path.go @@ -12,7 +12,7 @@ func CleanURLLike(p string) string { return "/" } parts := []string{} - for _, seg := range strings.Split(strings.ReplaceAll(p, "\\", "/"), "/") { + for seg := range strings.SplitSeq(strings.ReplaceAll(p, "/", "/"), "/") { switch seg { case "", ".": continue @@ -29,8 +29,8 @@ func CleanURLLike(p string) string { func SafeRel(root, requested string) (string, error) { s := CleanURLLike(requested) - if strings.HasPrefix(s, "/") { - s = strings.TrimPrefix(s, "/") + if after, ok := strings.CutPrefix(s, "/"); ok { + s = after } full := filepath.Join(root, filepath.FromSlash(s)) rel, err := filepath.Rel(root, full) diff --git a/server/internal/handlers/file.go b/server/internal/handlers/file.go index cd7f8e1..dac05bb 100644 --- a/server/internal/handlers/file.go +++ b/server/internal/handlers/file.go @@ -3,14 +3,9 @@ package handlers import ( "errors" - "fmt" - "io" "net/http" - "os" "path/filepath" - "time" - "git.max-richter.dev/max/marka/parser" "git.max-richter.dev/max/marka/server/internal/fsx" "git.max-richter.dev/max/marka/server/internal/httpx" ) @@ -41,131 +36,3 @@ func (h *File) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func read(path string) any { - data, err := os.ReadFile(path) - if err != nil { - return nil - } - res, err := parser.ParseFile(string(data)) - if err != nil { - return nil - } - - return res -} - -func (h *File) get(w http.ResponseWriter, r *http.Request, target string) { - fi, err := os.Stat(target) - if err != nil { - if os.IsNotExist(err) { - httpx.WriteError(w, http.StatusNotFound, errors.New("not found")) - } else { - httpx.WriteError(w, http.StatusInternalServerError, err) - } - return - } - - if fi.IsDir() { - entries, err := os.ReadDir(target) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - type item struct { - Name string `json:"name"` - Path string `json:"path"` - Content any `json:"content"` - IsDir bool `json:"isDir"` - Size int64 `json:"size"` - ModTime time.Time `json:"modTime"` - } - out := make([]item, 0, len(entries)) - for _, e := range entries { - info, _ := e.Info() - out = append(out, item{ - Name: e.Name(), - Path: fsx.ResponsePath(h.root, filepath.Join(target, e.Name())), - IsDir: e.IsDir(), - Content: read(filepath.Join(target, e.Name())), - Size: sizeOrZero(info), - ModTime: modTimeOrZero(info), - }) - } - httpx.WriteJSON(w, http.StatusOK, out) - return - } - - f, err := os.Open(target) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - defer f.Close() - - contentType := fsx.ContentTypeFor(target) - - if contentType == "application/markdown" { - data, err := io.ReadAll(f) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - res, err := parser.ParseFile(string(data)) - if err != nil { - fmt.Println(err) - } - w.Header().Set("Content-Type", "application/json") - httpx.WriteJSON(w, http.StatusOK, res) - return - } - - w.Header().Set("Content-Type", contentType) - http.ServeContent(w, r, filepath.Base(target), fi.ModTime(), f) -} - -func (h *File) post(w http.ResponseWriter, r *http.Request, target string) { - if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - tmp := target + ".tmp~" - f, err := os.Create(tmp) - if err != nil { - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - - if _, err := io.Copy(f, r.Body); err != nil { - _ = f.Close() - _ = os.Remove(tmp) - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - if err := f.Close(); err != nil { - _ = os.Remove(tmp) - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - if err := os.Rename(tmp, target); err != nil { - _ = os.Remove(tmp) - httpx.WriteError(w, http.StatusInternalServerError, err) - return - } - - w.Header().Set("Location", r.URL.Path) - w.WriteHeader(http.StatusCreated) -} - -func sizeOrZero(fi os.FileInfo) int64 { - if fi == nil { - return 0 - } - return fi.Size() -} - -func modTimeOrZero(fi os.FileInfo) time.Time { - if fi == nil { - return time.Time{} - } - return fi.ModTime() -} diff --git a/server/internal/handlers/get.go b/server/internal/handlers/get.go new file mode 100644 index 0000000..e1fc5cc --- /dev/null +++ b/server/internal/handlers/get.go @@ -0,0 +1,93 @@ +package handlers + +import ( + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "time" + + "git.max-richter.dev/max/marka/server/internal/fsx" + "git.max-richter.dev/max/marka/server/internal/httpx" +) + +func (h *File) get(w http.ResponseWriter, r *http.Request, target string) { + fi, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + httpx.WriteError(w, http.StatusNotFound, errors.New("not found")) + } else { + httpx.WriteError(w, http.StatusInternalServerError, err) + } + return + } + + if fi.IsDir() { + h.handleDir(w, r, target) + } else { + h.handleFile(w, r, target, fi) + } +} + +func (h *File) handleDir(w http.ResponseWriter, r *http.Request, dirPath string) { + entries, err := os.ReadDir(dirPath) + if err != nil { + httpx.WriteError(w, http.StatusInternalServerError, err) + return + } + + type item struct { + Name string `json:"name"` + Path string `json:"path"` + Content any `json:"content,omitempty"` + IsDir bool `json:"isDir"` + Size int64 `json:"size"` + ModTime time.Time `json:"modTime"` + } + + out := make([]item, 0, len(entries)) + for _, e := range entries { + info, _ := e.Info() // As in original, ignore error to get partial info + entryPath := filepath.Join(dirPath, e.Name()) + + var content any + if !e.IsDir() && fsx.ContentTypeFor(e.Name()) == "application/markdown" { + content, _ = parseMarkdownFile(entryPath) // As in original, ignore error + } + + out = append(out, item{ + Name: e.Name(), + Path: fsx.ResponsePath(h.root, entryPath), + IsDir: e.IsDir(), + Content: content, + Size: sizeOrZero(info), + ModTime: modTimeOrZero(info), + }) + } + httpx.WriteJSON(w, http.StatusOK, out) +} + +func (h *File) handleFile(w http.ResponseWriter, r *http.Request, target string, fi os.FileInfo) { + contentType := fsx.ContentTypeFor(target) + + if contentType == "application/markdown" { + res, err := parseMarkdownFile(target) + if err != nil { + httpx.WriteError(w, http.StatusInternalServerError, fmt.Errorf("failed to parse markdown: %w", err)) + return + } + httpx.WriteJSON(w, http.StatusOK, res) + return + } + + f, err := os.Open(target) + if err != nil { + httpx.WriteError(w, http.StatusInternalServerError, err) + return + } + defer f.Close() + + w.Header().Set("Content-Type", contentType) + http.ServeContent(w, r, filepath.Base(target), fi.ModTime(), f) +} diff --git a/server/internal/handlers/helpers.go b/server/internal/handlers/helpers.go new file mode 100644 index 0000000..8cca076 --- /dev/null +++ b/server/internal/handlers/helpers.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "os" + "time" + + "git.max-richter.dev/max/marka/parser" +) + +func parseMarkdownFile(path string) (any, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return parser.ParseFile(string(data)) +} + +func sizeOrZero(fi os.FileInfo) int64 { + if fi == nil { + return 0 + } + return fi.Size() +} + +func modTimeOrZero(fi os.FileInfo) time.Time { + if fi == nil { + return time.Time{} + } + return fi.ModTime() +} diff --git a/server/internal/handlers/post.go b/server/internal/handlers/post.go new file mode 100644 index 0000000..d7d23bc --- /dev/null +++ b/server/internal/handlers/post.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "io" + "net/http" + "os" + "path/filepath" + + "git.max-richter.dev/max/marka/server/internal/httpx" +) + +func (h *File) post(w http.ResponseWriter, r *http.Request, target string) { + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + httpx.WriteError(w, http.StatusInternalServerError, err) + return + } + + tmpPath := target + ".tmp~" + f, err := os.Create(tmpPath) + if err != nil { + httpx.WriteError(w, http.StatusInternalServerError, err) + return + } + + _, copyErr := io.Copy(f, r.Body) + closeErr := f.Close() + + if copyErr != nil { + os.Remove(tmpPath) + httpx.WriteError(w, http.StatusInternalServerError, copyErr) + return + } + if closeErr != nil { + os.Remove(tmpPath) + httpx.WriteError(w, http.StatusInternalServerError, closeErr) + return + } + + if err := os.Rename(tmpPath, target); err != nil { + os.Remove(tmpPath) + httpx.WriteError(w, http.StatusInternalServerError, err) + return + } + + w.Header().Set("Location", r.URL.Path) + w.WriteHeader(http.StatusCreated) +} diff --git a/template/blocks.go b/template/blocks.go index 37a5c87..37273bf 100644 --- a/template/blocks.go +++ b/template/blocks.go @@ -1,11 +1,9 @@ -// Package template contains the logic for parsing template blocks. +// Package blocks contains the logic for parsing template blocks. package template import ( "fmt" "strings" - - "go.yaml.in/yaml/v4" ) // TemplateType represents whether a template is short, long, or invalid. @@ -53,114 +51,6 @@ func cleanTemplate(input string) string { return s } -func parseShortTemplate(input string) (Block, error) { - split := strings.Split(cleanTemplate(input), "|") - if len(split) < 1 { - return Block{}, fmt.Errorf("invalid short template") - } - - block := Block{ - Type: DataBlock, - Path: strings.TrimSpace(split[0]), - Codec: CodecText, - content: input, - } - - if len(split) > 1 { - optionSplit := strings.SplitSeq(split[1], ",") - for option := range optionSplit { - switch strings.TrimSpace(option) { - case "number": - block.Codec = CodecNumber - case "text": - block.Codec = CodecText - case "hashtags": - block.Codec = CodecHashtags - default: - return block, fmt.Errorf("unknown codec option: %s", option) - } - } - } - - return block, nil -} - -type yamlBlock struct { - Path string `yaml:"path"` - Codec string `yaml:"codec"` - Value any `yaml:"value,omitempty"` - Fields []yamlField `yaml:"fields"` - ListTemplate string `yaml:"listTemplate,omitempty"` - Hidden bool `yaml:"hidden,omitempty"` -} - -type yamlField struct { - Path string `yaml:"path"` - Value any `yaml:"value,omitempty"` - Codec string `yaml:"codec"` - Hidden bool `yaml:"hidden,omitempty"` -} - -func parseYamlTemplate(input string) (block Block, err error) { - var blk yamlBlock - - cleaned := cleanTemplate(input) - - dec := yaml.NewDecoder(strings.NewReader(cleaned)) - dec.KnownFields(true) - - if err := dec.Decode(&blk); err != nil { - return block, fmt.Errorf("content '%q': %w", cleaned, err) - } - - if blk.Path == "" { - return block, fmt.Errorf("missing top-level 'path'") - } - - if blk.Codec == "" { - blk.Codec = "text" - } - - codec, err := parseCodecType(blk.Codec) - if err != nil { - return block, fmt.Errorf("failed to parse codec: %w", err) - } - - var fields []BlockField - - for _, field := range blk.Fields { - if field.Path == "" { - return block, fmt.Errorf("failed to parse field: %v", field) - } - - if field.Codec == "" { - field.Codec = "text" - } - - fieldCodec, err := parseCodecType(field.Codec) - if err != nil { - return block, fmt.Errorf("failed to parse codec: %w", err) - } - - fields = append(fields, BlockField{ - Path: field.Path, - CodecType: fieldCodec, - Value: field.Value, - Hidden: field.Hidden, - }) - - } - - return Block{ - Type: DataBlock, - Path: blk.Path, - Codec: codec, - Fields: fields, - ListTemplate: blk.ListTemplate, - content: input, - }, nil -} - func ParseTemplateBlock(template string, blockType BlockType) (block Block, err error) { if blockType == MatchingBlock { return Block{ diff --git a/template/blocks_short.go b/template/blocks_short.go new file mode 100644 index 0000000..08fb7fd --- /dev/null +++ b/template/blocks_short.go @@ -0,0 +1,38 @@ +package template + +import ( + "fmt" + "strings" +) + +func parseShortTemplate(input string) (Block, error) { + split := strings.Split(cleanTemplate(input), "|") + if len(split) < 1 { + return Block{}, fmt.Errorf("invalid short template") + } + + block := Block{ + Type: DataBlock, + Path: strings.TrimSpace(split[0]), + Codec: CodecText, + content: input, + } + + if len(split) > 1 { + optionSplit := strings.SplitSeq(split[1], ",") + for option := range optionSplit { + switch strings.TrimSpace(option) { + case "number": + block.Codec = CodecNumber + case "text": + block.Codec = CodecText + case "hashtags": + block.Codec = CodecHashtags + default: + return block, fmt.Errorf("unknown codec option: %s", option) + } + } + } + + return block, nil +} diff --git a/template/blocks_yaml.go b/template/blocks_yaml.go new file mode 100644 index 0000000..226a0ef --- /dev/null +++ b/template/blocks_yaml.go @@ -0,0 +1,86 @@ +package template + +import ( + "fmt" + "strings" + + "go.yaml.in/yaml/v4" +) + +type yamlBlock struct { + Path string `yaml:"path"` + Codec string `yaml:"codec"` + Value any `yaml:"value,omitempty"` + Fields []yamlField `yaml:"fields"` + ListTemplate string `yaml:"listTemplate,omitempty"` + Hidden bool `yaml:"hidden,omitempty"` + PathAlias []string `yaml:"pathAlias,omitempty"` +} + +type yamlField struct { + Path string `yaml:"path"` + Value any `yaml:"value,omitempty"` + Codec string `yaml:"codec"` + Hidden bool `yaml:"hidden,omitempty"` + PathAlias []string `yaml:"pathAlias,omitempty"` +} + +func parseYamlTemplate(input string) (block Block, err error) { + var blk yamlBlock + + cleaned := cleanTemplate(input) + + dec := yaml.NewDecoder(strings.NewReader(cleaned)) + dec.KnownFields(true) + + if err := dec.Decode(&blk); err != nil { + return block, fmt.Errorf("content '%q': %w", cleaned, err) + } + + if blk.Path == "" { + return block, fmt.Errorf("missing top-level 'path'") + } + + if blk.Codec == "" { + blk.Codec = "text" + } + + codec, err := parseCodecType(blk.Codec) + if err != nil { + return block, fmt.Errorf("failed to parse codec: %w", err) + } + + var fields []BlockField + + for _, field := range blk.Fields { + if field.Path == "" { + return block, fmt.Errorf("failed to parse field: %v", field) + } + + if field.Codec == "" { + field.Codec = "text" + } + + fieldCodec, err := parseCodecType(field.Codec) + if err != nil { + return block, fmt.Errorf("failed to parse codec: %w", err) + } + + fields = append(fields, BlockField{ + Path: field.Path, + CodecType: fieldCodec, + Value: field.Value, + Hidden: field.Hidden, + }) + + } + + return Block{ + Type: DataBlock, + Path: blk.Path, + Codec: codec, + Fields: fields, + ListTemplate: blk.ListTemplate, + content: input, + }, nil +} diff --git a/template/go.mod b/template/go.mod index 2a9c7a9..169d84c 100644 --- a/template/go.mod +++ b/template/go.mod @@ -1,5 +1,8 @@ module git.max-richter.dev/max/marka/template -go 1.24.3 +go 1.24.5 -require go.yaml.in/yaml/v4 v4.0.0-rc.1 +require ( + git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e + go.yaml.in/yaml/v4 v4.0.0-rc.1 +) diff --git a/template/go.sum b/template/go.sum index 1dd5057..b6217d4 100644 --- a/template/go.sum +++ b/template/go.sum @@ -1,2 +1,4 @@ +git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e h1:eXAE0JHDvLGqtYSSlX5mw1XAuK+Cmu74c52PyveRhlE= +git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e/go.mod h1:n793S7TENIfgHpZLz0lm0qorM7eCx3zBLby3Fb++hZA= go.yaml.in/yaml/v4 v4.0.0-rc.1 h1:4J1+yLKUIPGexM/Si+9d3pij4hdc7aGO04NhrElqXbY= go.yaml.in/yaml/v4 v4.0.0-rc.1/go.mod h1:CBdeces52/nUXndfQ5OY8GEQuNR9uEEOJPZj/Xq5IzU= diff --git a/validator/go.mod b/validator/go.mod index 36391e1..f278b87 100644 --- a/validator/go.mod +++ b/validator/go.mod @@ -2,6 +2,9 @@ module git.max-richter.dev/max/marka/validator go 1.24.5 -require github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 +require ( + git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 +) require golang.org/x/text v0.14.0 // indirect diff --git a/validator/go.sum b/validator/go.sum index b46b517..3ed7dc1 100644 --- a/validator/go.sum +++ b/validator/go.sum @@ -1,3 +1,7 @@ +git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e h1:eXAE0JHDvLGqtYSSlX5mw1XAuK+Cmu74c52PyveRhlE= +git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e/go.mod h1:n793S7TENIfgHpZLz0lm0qorM7eCx3zBLby3Fb++hZA= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=