diff --git a/go.work b/go.work index 3fa22ab..d1bdf96 100644 --- a/go.work +++ b/go.work @@ -1,10 +1,11 @@ -go 1.24.5 +go 1.25.1 use ( ./parser ./registry ./renderer ./server + ./server-new ./template ./testdata ./validator diff --git a/parser/go.mod b/parser/go.mod index fe539c6..769354b 100644 --- a/parser/go.mod +++ b/parser/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/parser -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 diff --git a/parser/parser.go b/parser/parser.go index 74150a7..b80065b 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -23,6 +23,8 @@ func DetectType(markdownContent string) (string, error) { return "", fmt.Errorf("failed to compile template: %w", err) } + fmt.Println("Content:", markdownContent) + blocks := matcher.MatchBlocksFuzzy(markdownContent, defaultSchema, 0.3) result, err := decoders.Parse(blocks) @@ -30,6 +32,8 @@ func DetectType(markdownContent string) (string, error) { return "", fmt.Errorf("failed to parse blocks: %w", err) } + fmt.Println("Result: ", result) + if result, ok := result.(map[string]any); ok { if contentType, ok := result["@type"]; ok { return contentType.(string), nil diff --git a/registry/go.mod b/registry/go.mod index 7364278..94c016b 100644 --- a/registry/go.mod +++ b/registry/go.mod @@ -1,3 +1,3 @@ module git.max-richter.dev/max/marka/registry -go 1.24.5 +go 1.25.1 diff --git a/renderer/go.mod b/renderer/go.mod index 558a176..b14358a 100644 --- a/renderer/go.mod +++ b/renderer/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/renderer -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e diff --git a/server-new/.air.toml b/server-new/.air.toml new file mode 100644 index 0000000..b2eced4 --- /dev/null +++ b/server-new/.air.toml @@ -0,0 +1,29 @@ +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-new/.gitignore b/server-new/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/server-new/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/server-new/cmd/marka-server/main.go b/server-new/cmd/marka-server/main.go new file mode 100644 index 0000000..b001bfd --- /dev/null +++ b/server-new/cmd/marka-server/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "flag" + "log" + "net/http" + "os" + "path/filepath" + + "git.max-richter.dev/max/marka/server-new/internal/adapters" + "git.max-richter.dev/max/marka/server-new/internal/handler" +) + +func main() { + root := flag.String("root", ".", "filesystem root to serve") + addr := flag.String("addr", ":8080", "listen address") + flag.Parse() + + absRoot, err := filepath.Abs(*root) + must(err) + + info, err := os.Stat(absRoot) + must(err) + if !info.IsDir() { + log.Fatal("root is not a directory") + } + + fsAdapter, err := adapters.NewLocalFsAdapter(absRoot) + must(err) + + http.Handle("/", handler.NewHandler(fsAdapter)) + + log.Printf("listening on %s, root=%s", *addr, absRoot) + log.Fatal(http.ListenAndServe(*addr, nil)) +} + +func must(err error) { + if err != nil { + log.Fatal(err) + } +} diff --git a/server-new/go.mod b/server-new/go.mod new file mode 100644 index 0000000..341a24f --- /dev/null +++ b/server-new/go.mod @@ -0,0 +1,3 @@ +module git.max-richter.dev/max/marka/server-new + +go 1.25.1 diff --git a/server-new/internal/adapters/errors.go b/server-new/internal/adapters/errors.go new file mode 100644 index 0000000..c121579 --- /dev/null +++ b/server-new/internal/adapters/errors.go @@ -0,0 +1,5 @@ +package adapters + +import "errors" + +var ErrNotFound = errors.New("not found") diff --git a/server-new/internal/adapters/fs.go b/server-new/internal/adapters/fs.go new file mode 100644 index 0000000..cfe95dd --- /dev/null +++ b/server-new/internal/adapters/fs.go @@ -0,0 +1,97 @@ +package adapters + +import ( + "os" + "path/filepath" +) + +type LocalFsAdapter struct { + root string +} + +func (l LocalFsAdapter) readDir(path string) (FsResponse, error) { + dirInfo, _ := os.Stat(path) + + entries, err := os.ReadDir(path) + if err != nil { + return FsResponse{}, err + } + + out := make([]FsDirEntry, 0, len(entries)) + for _, e := range entries { + info, _ := e.Info() + + entryType := "dir" + if !e.IsDir() { + entryType = contentTypeFor(e.Name()) + } + + out = append(out, FsDirEntry{ + Name: e.Name(), + Type: entryType, + ModTime: info.ModTime(), + }) + } + + return FsResponse{ + Dir: &FsDir{ + Files: out, + Name: ResponsePath(l.root, path), + ModTime: dirInfo.ModTime(), + }, + }, nil +} + +func (l LocalFsAdapter) readFile(path string) (FsResponse, error) { + fi, err := os.Stat(path) + if err != nil { + return FsResponse{}, err + } + + data, err := os.ReadFile(path) + if err != nil { + return FsResponse{}, err + } + + return FsResponse{ + File: &FsFile{ + Name: ResponsePath(l.root, path), + Type: contentTypeFor(path), + ModTime: fi.ModTime(), + Content: data, + }, + }, nil +} + +func (l LocalFsAdapter) Read(path string) (FsResponse, error) { + cleanRel, err := SafeRel(l.root, path) + if err != nil { + return FsResponse{}, err + } + target := filepath.Join(l.root, filepath.FromSlash(cleanRel)) + + fi, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + return FsResponse{}, ErrNotFound + } + + return FsResponse{}, err + } + + if fi.IsDir() { + return l.readDir(target) + } + + return l.readFile(target) +} + +func (LocalFsAdapter) Write(path string, content []byte) error { + return nil +} + +func NewLocalFsAdapter(root string) (FileAdapter, error) { + return LocalFsAdapter{ + root: root, + }, nil +} diff --git a/server-new/internal/adapters/fs_utils.go b/server-new/internal/adapters/fs_utils.go new file mode 100644 index 0000000..8ebdcde --- /dev/null +++ b/server-new/internal/adapters/fs_utils.go @@ -0,0 +1,78 @@ +package adapters + +import ( + "errors" + "mime" + "os" + "path/filepath" + "strings" + "time" +) + +func SafeRel(root, requested string) (string, error) { + s := requested + if after, ok := strings.CutPrefix(s, "/"); ok { + s = after + } + full := filepath.Join(root, filepath.FromSlash(s)) + rel, err := filepath.Rel(root, full) + if err != nil { + return "", err + } + if rel == "." { + return "/", nil + } + sep := string(filepath.Separator) + if strings.HasPrefix(rel, "..") || strings.Contains(rel, ".."+sep) { + return "", errors.New("path escapes root") + } + return "/" + filepath.ToSlash(rel), nil +} + +func ResponsePath(root, full string) string { + rel, err := filepath.Rel(root, full) + if err != nil || rel == "." { + return "/" + } + return "/" + filepath.ToSlash(rel) +} + +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() +} + +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 { + case ".md", ".markdown", ".mdown": + return "application/markdown" + } + if ct := mime.TypeByExtension(ext); ct != "" { + return ct + } + if textPlainExtensions[ext] { + return "text/plain; charset=utf-8" + } + return "application/octet-stream" +} diff --git a/server-new/internal/adapters/interface.go b/server-new/internal/adapters/interface.go new file mode 100644 index 0000000..a482676 --- /dev/null +++ b/server-new/internal/adapters/interface.go @@ -0,0 +1,34 @@ +// Package adapters are the backend to that connects the marka server to a storage +package adapters + +import "time" + +type FileAdapter interface { + Read(path string) (FsResponse, error) + Write(path string, content []byte) error +} + +type FsFile struct { + Name string `json:"name"` + Type string `json:"type"` + Content []byte `json:"content"` + ModTime time.Time `json:"modTime"` +} + +type FsDirEntry struct { + Name string `json:"name"` + Type string `json:"type"` + IsDir bool `json:"isDir,omitempty"` + ModTime time.Time `json:"modTime"` +} + +type FsDir struct { + Files []FsDirEntry `json:"files"` + Name string `json:"name"` + ModTime time.Time `json:"modTime"` +} + +type FsResponse struct { + Dir *FsDir `json:"dir,omitempty"` + File *FsFile `json:"file,omitempty"` +} diff --git a/server-new/internal/handler/error.go b/server-new/internal/handler/error.go new file mode 100644 index 0000000..37fcc3d --- /dev/null +++ b/server-new/internal/handler/error.go @@ -0,0 +1,20 @@ +package handler + +import ( + "encoding/json" + "net/http" +) + +type ErrorResponse struct { + Error string `json:"error"` +} + +func writeError(w http.ResponseWriter, code int, err error) { + writeJSON(w, code, ErrorResponse{Error: err.Error()}) +} + +func writeJSON(w http.ResponseWriter, code int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(v) +} diff --git a/server-new/internal/handler/handler.go b/server-new/internal/handler/handler.go new file mode 100644 index 0000000..9a1f13e --- /dev/null +++ b/server-new/internal/handler/handler.go @@ -0,0 +1,104 @@ +// Package handler provides the HTTP handler for the marka server +package handler + +import ( + "errors" + "net/http" + "time" + + "git.max-richter.dev/max/marka/parser" + "git.max-richter.dev/max/marka/server-new/internal/adapters" +) + +type ResponseItem struct { + Name string `json:"name"` + Content any `json:"content,omitempty"` + Type string `json:"type,omitempty"` + IsDir bool `json:"isDir"` + Size int64 `json:"size,omitempty"` + ModTime time.Time `json:"modTime"` +} + +type Handler struct { + adapter adapters.FileAdapter +} + +func (h *Handler) get(w http.ResponseWriter, target string) { + fsEntry, err := h.adapter.Read(target) + if err != nil { + writeError(w, 500, err) + return + } + + if fsEntry.File != nil { + + if fsEntry.File.Content != nil && fsEntry.File.Type == "application/markdown" { + data, err := parser.ParseFile(string(fsEntry.File.Content)) + if err != nil { + writeError(w, 500, err) + return + } + + res := ResponseItem{ + Name: fsEntry.File.Name, + Type: fsEntry.File.Type, + Content: data, + IsDir: false, + Size: int64(len(fsEntry.File.Content)), + ModTime: fsEntry.File.ModTime, + } + + writeJSON(w, 200, res) + return + } + + res := ResponseItem{ + Name: fsEntry.File.Name, + Content: fsEntry.File.Content, + Type: fsEntry.File.Type, + IsDir: false, + Size: int64(len(fsEntry.File.Content)), + ModTime: fsEntry.File.ModTime, + } + writeJSON(w, 200, res) + return + } + + if fsEntry.Dir != nil { + res := ResponseItem{ + Name: fsEntry.Dir.Name, + Content: fsEntry.Dir.Files, + IsDir: true, + ModTime: fsEntry.Dir.ModTime, + } + writeJSON(w, 200, res) + return + } +} + +func (h *Handler) post(w http.ResponseWriter, target string) { +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + reqPath := r.URL.Path + if reqPath == "" { + reqPath = "/" + } + + target := cleanURLLike(reqPath) + + switch r.Method { + case http.MethodGet: + h.get(w, target) + case http.MethodPost: + h.post(w, target) + default: + writeError(w, http.StatusMethodNotAllowed, errors.New("method not allowed")) + } +} + +func NewHandler(adapter adapters.FileAdapter) http.Handler { + return &Handler{ + adapter: adapter, + } +} diff --git a/server-new/internal/handler/utils.go b/server-new/internal/handler/utils.go new file mode 100644 index 0000000..1869ced --- /dev/null +++ b/server-new/internal/handler/utils.go @@ -0,0 +1,24 @@ +package handler + +import "strings" + +func cleanURLLike(p string) string { + p = strings.TrimSpace(p) + if p == "" || p == "/" { + return "/" + } + parts := []string{} + for seg := range strings.SplitSeq(p, "/") { + switch seg { + case "", ".": + continue + case "..": + if len(parts) > 0 { + parts = parts[:len(parts)-1] + } + default: + parts = append(parts, seg) + } + } + return "/" + strings.Join(parts, "/") +} diff --git a/server/go.mod b/server/go.mod index 250391c..359a2fe 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/server -go 1.24.5 +go 1.25.1 require git.max-richter.dev/max/marka/parser v0.0.0-20250819170608-69c2550f448e diff --git a/server/internal/fsx/path.go b/server/internal/fsx/path.go index 6916a16..3950deb 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.SplitSeq(strings.ReplaceAll(p, "/", "/"), "/") { + for seg := range strings.SplitSeq(p, "/") { switch seg { case "", ".": continue diff --git a/template/go.mod b/template/go.mod index 169d84c..2cd6cd1 100644 --- a/template/go.mod +++ b/template/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/template -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e diff --git a/testdata/go.mod b/testdata/go.mod index dd01c9b..6515ee9 100644 --- a/testdata/go.mod +++ b/testdata/go.mod @@ -1,3 +1,3 @@ module git.max-richter.dev/max/marka/testdata -go 1.24.5 +go 1.25.1 diff --git a/validator/go.mod b/validator/go.mod index f278b87..358e582 100644 --- a/validator/go.mod +++ b/validator/go.mod @@ -1,6 +1,6 @@ module git.max-richter.dev/max/marka/validator -go 1.24.5 +go 1.25.1 require ( git.max-richter.dev/max/marka/registry v0.0.0-20250819170608-69c2550f448e