feat: some updates

This commit is contained in:
Max Richter
2025-09-24 18:18:57 +02:00
parent b3c01bb43d
commit 26c303d9cf
32 changed files with 464 additions and 263 deletions

View File

@@ -5,6 +5,7 @@ url: https://dressaday.com/2006/10/20/you-dont-have-to-be-pretty/
rating: 5 rating: 5
date: December 7, 2023 date: December 7, 2023
image: https://i0.wp.com/old-dressaday-images.s3-website-us-west-2.amazonaws.com/6a0133ed1b1479970b0134809d9f8b970c.jpg 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 # You Dont Have to Be Pretty A Dress A Day

View File

@@ -9,7 +9,7 @@ itemReviewed:
@type: Book @type: Book
name: Anonymous Poems name: Anonymous Poems
reviewBody: "Short, haunting, and powerful verses." reviewBody: "Short, haunting, and powerful verses."
reviewRating: 4/5 reviewRating.ratingValue: 4
--- ---
# Anonymous Poems # Anonymous Poems

View File

@@ -11,7 +11,7 @@ itemReviewed:
author: author:
@type: Person @type: Person
name: George Orwell name: George Orwell
reviewRating: 10/10 reviewRating: 5
--- ---
# 1984 # 1984

View File

@@ -15,5 +15,10 @@ func Keywords(input string, block template.Block) (value any, error error) {
tags = append(tags, tag) tags = append(tags, tag)
} }
} }
if len(tags) == 0 {
return nil, nil
}
return tags, nil return tags, nil
} }

View File

@@ -1,6 +1,6 @@
module git.max-richter.dev/max/marka/parser module git.max-richter.dev/max/marka/parser
go 1.24.3 go 1.24.5
require ( require (
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567 git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567
@@ -14,6 +14,8 @@ require (
) )
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 github.com/google/go-cmp v0.7.0
go.yaml.in/yaml/v4 v4.0.0-rc.1 go.yaml.in/yaml/v4 v4.0.0-rc.1
) )

View File

@@ -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 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/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 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/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 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 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 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=

View File

@@ -1,3 +1,3 @@
module git.max-richter.dev/max/marka/registry module git.max-richter.dev/max/marka/registry
go 1.24.3 go 1.24.5

View File

@@ -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=

View File

@@ -22,6 +22,16 @@
hidden: true hidden: true
- path: datePublished - path: datePublished
- path: articleSection - 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
} }
--- ---

View File

@@ -39,4 +39,5 @@
# { itemReviewed.name } # { itemReviewed.name }
{ keywords | hashtags } { keywords | hashtags }
## Review
{ reviewBody } { reviewBody }

View File

@@ -1,3 +1,18 @@
module git.max-richter.dev/max/marka/renderer 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
)

20
renderer/go.sum Normal file
View File

@@ -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=

32
server/.air.toml Normal file
View File

@@ -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

1
server/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
tmp/

View File

@@ -17,13 +17,13 @@ func main() {
absRoot, err := filepath.Abs(*root) absRoot, err := filepath.Abs(*root)
must(err) must(err)
info, err := os.Stat(absRoot) info, err := os.Stat(absRoot)
must(err) must(err)
if !info.IsDir() { if !info.IsDir() {
log.Fatal("root is not a directory") log.Fatal("root is not a directory")
} }
// Catch-all route from "/" → serve files rooted in absRoot
http.Handle("/", handlers.NewFile(absRoot)) http.Handle("/", handlers.NewFile(absRoot))
log.Printf("listening on %s, root=%s", *addr, absRoot) log.Printf("listening on %s, root=%s", *addr, absRoot)

View File

@@ -1,3 +1,16 @@
module git.max-richter.dev/max/marka/server 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
)

22
server/go.sum Normal file
View File

@@ -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=

1
server/http/.env Normal file
View File

@@ -0,0 +1 @@
SERVER_URL=http://localhost:8080

View File

@@ -0,0 +1,5 @@
POST {{SERVER_URL}}/Recipes/bolognaise.md
{
}

View File

@@ -6,6 +6,17 @@ import (
"strings" "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 { func ContentTypeFor(name string) string {
ext := strings.ToLower(filepath.Ext(name)) ext := strings.ToLower(filepath.Ext(name))
switch ext { switch ext {
@@ -15,8 +26,7 @@ func ContentTypeFor(name string) string {
if ct := mime.TypeByExtension(ext); ct != "" { if ct := mime.TypeByExtension(ext); ct != "" {
return ct return ct
} }
switch ext { if textPlainExtensions[ext] {
case ".txt", ".log", ".json", ".yaml", ".yml", ".toml", ".xml", ".csv":
return "text/plain; charset=utf-8" return "text/plain; charset=utf-8"
} }
return "application/octet-stream" return "application/octet-stream"

View File

@@ -12,7 +12,7 @@ func CleanURLLike(p string) string {
return "/" return "/"
} }
parts := []string{} parts := []string{}
for _, seg := range strings.Split(strings.ReplaceAll(p, "\\", "/"), "/") { for seg := range strings.SplitSeq(strings.ReplaceAll(p, "/", "/"), "/") {
switch seg { switch seg {
case "", ".": case "", ".":
continue continue
@@ -29,8 +29,8 @@ func CleanURLLike(p string) string {
func SafeRel(root, requested string) (string, error) { func SafeRel(root, requested string) (string, error) {
s := CleanURLLike(requested) s := CleanURLLike(requested)
if strings.HasPrefix(s, "/") { if after, ok := strings.CutPrefix(s, "/"); ok {
s = strings.TrimPrefix(s, "/") s = after
} }
full := filepath.Join(root, filepath.FromSlash(s)) full := filepath.Join(root, filepath.FromSlash(s))
rel, err := filepath.Rel(root, full) rel, err := filepath.Rel(root, full)

View File

@@ -3,14 +3,9 @@ package handlers
import ( import (
"errors" "errors"
"fmt"
"io"
"net/http" "net/http"
"os"
"path/filepath" "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/fsx"
"git.max-richter.dev/max/marka/server/internal/httpx" "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()
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -1,11 +1,9 @@
// Package template contains the logic for parsing template blocks. // Package blocks contains the logic for parsing template blocks.
package template package template
import ( import (
"fmt" "fmt"
"strings" "strings"
"go.yaml.in/yaml/v4"
) )
// TemplateType represents whether a template is short, long, or invalid. // TemplateType represents whether a template is short, long, or invalid.
@@ -53,114 +51,6 @@ func cleanTemplate(input string) string {
return s 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) { func ParseTemplateBlock(template string, blockType BlockType) (block Block, err error) {
if blockType == MatchingBlock { if blockType == MatchingBlock {
return Block{ return Block{

38
template/blocks_short.go Normal file
View File

@@ -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
}

86
template/blocks_yaml.go Normal file
View File

@@ -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
}

View File

@@ -1,5 +1,8 @@
module git.max-richter.dev/max/marka/template 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
)

View File

@@ -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 h1:4J1+yLKUIPGexM/Si+9d3pij4hdc7aGO04NhrElqXbY=
go.yaml.in/yaml/v4 v4.0.0-rc.1/go.mod h1:CBdeces52/nUXndfQ5OY8GEQuNR9uEEOJPZj/Xq5IzU= go.yaml.in/yaml/v4 v4.0.0-rc.1/go.mod h1:CBdeces52/nUXndfQ5OY8GEQuNR9uEEOJPZj/Xq5IzU=

View File

@@ -2,6 +2,9 @@ module git.max-richter.dev/max/marka/validator
go 1.24.5 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 require golang.org/x/text v0.14.0 // indirect

View File

@@ -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 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= 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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=