feat: whole bunch of shit

This commit is contained in:
Max Richter
2025-08-17 16:35:52 +02:00
parent 69c2550f44
commit b3c01bb43d
36 changed files with 878 additions and 12 deletions

View File

@@ -0,0 +1,28 @@
---
@type: Article
author.name: Erin Mckean
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
---
# You Dont Have to Be Pretty A Dress A Day
#positivity
![Vreeland](https://i0.wp.com/old-dressaday-images.s3-website-us-west-2.amazonaws.com/6a0133ed1b1479970b0134809d9f8b970c.jpg)
*image is by [Andy Warhol](http://www.warholfoundation.org/) © 2015 The Andy Warhol Foundation for the Visual Arts, Inc. / Artists Rights Society (ARS), New York*
So the other day, folks in the comments were [talking about leggings](http://www.dressaday.com/2006/10/open-letter-to-mr-mizrahi.html). Im pretty agnostic about leggings, but the whole discussion (which centered on the fact that it can be \*really\* hard to look good in leggings) got me thinking about the pervasive idea that women owe it to onlookers to maintain a certain standard of decorativeness.
Now, this may seem strange from someone who writes about pretty dresses (mostly) every day, but: You Dont Have to Be Pretty. You dont _owe_ prettiness to anyone. Not to your boyfriend/spouse/partner, not to your co-workers, especially not to random men on the street. You dont owe it to your mother, you dont owe it to your children, you dont owe it to civilization in general. Prettiness is not a rent you pay for occupying a space marked “female”.
Im not saying that you SHOULDNT be pretty if you want to. (You dont owe UN-prettiness to feminism, in other words.) Pretty is pleasant, and fun, and satisfying, and makes people smile, often even at you. But in the hierarchy of importance, _pretty_ stands several rungs down from _happy_, is way below _healthy_, and if done as a penance, or an obligation, can be so far away from _independent_ that you may have to squint really hard to see it in the haze.
But what does you-dont-have-to-be-pretty mean in practical, everyday terms? It means that you dont have to apologize for wearing things that are held to be “unflattering” or “unfashionable” — especially if, in fact, they make you happy on some level deeper than just being pretty does. So what if your favorite color isnt a “good” color on you? So what if you are “too fat” (by some arbitrary measure) for a sleeveless top? If you are clean, are covered enough to avoid a citation for public indecency, and have bandaged any open wounds, you can wear any color or style you please, if it makes you happy.
I was going to make a handy prettiness decision tree, but pretty much the end of every branch was a bubble that said “tell complainers to go to hell” so it wasnt much of a tool.
Pretty, its sad to say, can have a shelf life. Its so tied up with youth that, at some point (if youre lucky), youre going to have to graduate from pretty. Sometimes (as in the case with Diana Vreeland, above, you can go so far past pretty that you end up in _stylish_, or even _striking_ (or the fashion-y term _jolie laide_) before you know it. But you wont get there if you think you have to follow all the signs that say “this way to _Pretty_.” You get there by traveling the route you find most interesting. (And to hell with the naysayers who say “But thats not PRETTY”!)

19
examples/Books/Minimal.md Normal file
View File

@@ -0,0 +1,19 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Alice
datePublished: 2025-08-01
itemReviewed:
@type: Book
name: Untitled
author:
@type: Person
name: Unknown
---
# Untitled
## Review
A mysterious book that leaves everything to the imagination.

View File

@@ -0,0 +1,18 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Eve
datePublished: 2025-08-15
itemReviewed:
@type: Book
name: Anonymous Poems
reviewBody: "Short, haunting, and powerful verses."
reviewRating: 4/5
---
# Anonymous Poems
## Review
Short, haunting, and powerful verses.

19
examples/Books/NoBody.md Normal file
View File

@@ -0,0 +1,19 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Clara
datePublished: 2025-08-10
itemReviewed:
@type: Book
name: 1984
author:
@type: Person
name: George Orwell
reviewRating: 10/10
---
# 1984
## Review

View File

@@ -0,0 +1,28 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Bob
datePublished: 2025-08-05
itemReviewed:
@type: Book
name: War and Peace
author:
@type: Person
name: Leo Tolstoy
reviewAspect:
- Length
- Complexity
- Characters
positiveNotes:
- Rich storytelling
- Deep philosophical themes
negativeNotes:
- Overwhelmingly long
---
# War and Peace
## Review
An epic novel that rewards patient readers but can feel exhausting at times.

View File

@@ -0,0 +1,17 @@
---
@type: Review
author:
name: Max Richter
itemReviewed:
@type: Book
author:
name: F. Scott Fitzgerald
reviewRating: 5
---
# The Great Gatsby
## Review
A brilliant novel that captures the glamour and disillusionment of the Jazz Age.
The writing is lyrical and immersive, and the themes of wealth, love, and loss remain relevant today.
While some characters come across as emotionally flat, the overall impact is unforgettable.

View File

@@ -0,0 +1,16 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Frank
datePublished: 2025-08-01
itemReviewed:
@type: Movie
name: Untitled Film
---
# Untitled Film
## Review
Unclear, experimental, and strange.

View File

@@ -0,0 +1,23 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Grace
datePublished: 2025-08-02
itemReviewed:
@type: Movie
name: The Room
author:
@type: Person
name: Tommy Wiseau
negativeNotes:
- Awkward dialogue
- Strange pacing
- Inconsistent acting
---
# The Room
## Review
So bad its fascinating.

22
examples/Movies/NoBody.md Normal file
View File

@@ -0,0 +1,22 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Jack
datePublished: 2025-08-06
itemReviewed:
@type: Movie
name: Blade Runner
author:
@type: Person
name: Ridley Scott
reviewRating: 4.5/5
reviewAspect:
- Atmosphere
- Visual style
---
# Blade Runner
## Review

View File

@@ -0,0 +1,24 @@
---
@context: https://schema.org
@type: Review
author:
@type: Person
name: Henry
datePublished: 2025-08-03
itemReviewed:
@type: Movie
name: Inception
author:
@type: Person
name: Christopher Nolan
reviewAspect:
- Story
- Visuals
- Soundtrack
reviewRating: 9/10
---
# Inception
## Review
A mind-bending thriller that keeps you hooked.

View File

@@ -0,0 +1,16 @@
---
author.name: Max Richter
---
# Baguette
My favourite baguette recipe
## Ingredients
- Flour
- Water
- Salt
## Steps
1. Mix Flour Water and Salt
2. Bake the bread

21
examples/Recipes/cake.md Normal file
View File

@@ -0,0 +1,21 @@
---
author.name: Max Richter
---
# Chocolate Cake
A moist, rich cake for any occasion.
## Ingredients
- 200 g flour
- 200 g sugar
- 100 g cocoa powder
- 3 eggs
- 200 ml milk
## Steps
1. Preheat oven to 180°C.
2. Mix dry ingredients in a bowl.
3. Add eggs and milk, stir until smooth.
4. Pour into a cake tin.
5. Bake for 35 minutes.

View File

@@ -0,0 +1,20 @@
---
author.name: Max Richter
---
# Caprese Salad
A simple Italian starter with tomato, mozzarella, and basil.
## Ingredients
- 2 tomatoes
- 125 g mozzarella
- Fresh basil
- Olive oil
- Balsamic vinegar
## Steps
1. Slice tomatoes and mozzarella.
2. Arrange on a plate with basil leaves.
3. Drizzle with olive oil and balsamic vinegar.
4. Serve immediately.

View File

@@ -0,0 +1,18 @@
---
author.name: Max Richter
---
# Grilled Cheese Sandwich
Crispy, golden bread filled with melted cheese.
## Ingredients
- 2 slices bread
- 2 slices cheddar
- Butter
## Steps
1. Butter the bread slices.
2. Place cheese between bread.
3. Grill in a pan until golden on both sides.
4. Serve hot.

View File

@@ -0,0 +1,20 @@
---
author.name: Jane Doe
author.@type: Organization
recipeCategory: ["Dessert", "Vegan", "Quick"]
---
# Vegan Brownies
Rich chocolate brownies with no animal products.
## Ingredients
- 200 g flour
- 100 g cocoa powder
- 150 g sugar
- 250 ml almond milk
## Steps
1. Mix all dry ingredients.
2. Add almond milk and stir into batter.
3. Bake at 180°C for 30 minutes.

View File

@@ -0,0 +1,21 @@
---
author.name: Max Richter
---
# Lentil Curry
A hearty Indian-inspired curry with red lentils.
## Ingredients
- 200 g red lentils
- 1 onion
- 2 cloves garlic
- 1 tbsp curry powder
- 400 ml coconut milk
## Steps
1. Sauté onion and garlic with curry powder.
2. Add lentils and coconut milk.
3. Simmer for 20 minutes.
4. Stir occasionally until thick.
5. Serve with rice.

View File

@@ -0,0 +1,23 @@
---
author.name: Max Richter
---
# Ramen Broth
A rich broth requiring multiple steps in one ingredient description.
## Ingredients
- Chicken bones, roasted until golden
- Water, enough to cover bones
- Aromatics:
- Onion
- Ginger
- Garlic
- Soy sauce
- Mirin
## Steps
1. Roast bones until golden brown.
2. Simmer bones with aromatics and water for 46 hours.
3. Strain broth.
4. Add soy sauce and mirin before serving.

View File

@@ -0,0 +1,17 @@
---
author.name: Max Richter
prepTime:
cookTime:
recipeYield:
cookingMethod:
---
# Mystery Dish
A recipe with missing optional metadata.
## Ingredients
- ???
## Steps
1. ???

View File

@@ -0,0 +1,21 @@
---
author.name: Max Richter
---
# Pancakes
Fluffy breakfast pancakes perfect with maple syrup.
## Ingredients
- 200 g flour
- 2 eggs
- 300 ml milk
- 1 tsp baking powder
- Butter for frying
## Steps
1. Mix flour, baking powder, eggs, and milk into a smooth batter.
2. Heat butter in a pan.
3. Pour in batter and cook until bubbles form.
4. Flip and cook the other side.
5. Serve with syrup or fruit.

22
examples/Recipes/salad.md Normal file
View File

@@ -0,0 +1,22 @@
---
author.name: Max Richter
---
# Greek Salad
A fresh salad with feta, olives, and crisp vegetables.
## Ingredients
- 2 tomatoes
- 1 cucumber
- 1 red onion
- 100 g feta cheese
- Olives
- Olive oil and oregano
## Steps
1. Chop tomatoes, cucumber, and onion.
2. Mix with olives in a bowl.
3. Crumble feta on top.
4. Drizzle with olive oil and sprinkle oregano.
5. Serve chilled.

View File

@@ -0,0 +1,21 @@
---
author.name: Max Richter
---
# Spaghetti Carbonara
A creamy Roman pasta classic with pancetta, eggs, and cheese.
## Ingredients
- 200 g spaghetti
- 100 g pancetta
- 2 eggs
- 50 g pecorino romano
- Black pepper
## Steps
1. Boil the pasta until al dente.
2. Fry pancetta until crisp.
3. Whisk eggs and cheese in a bowl.
4. Combine hot pasta, pancetta, and egg mixture.
5. Season generously with pepper and serve.

View File

@@ -0,0 +1,21 @@
---
author.name: Max Richter
---
# Chicken Stir-Fry
Quick stir-fried chicken with vegetables and soy sauce.
## Ingredients
- 200 g chicken breast
- 1 bell pepper
- 1 carrot
- 2 tbsp soy sauce
- 1 tbsp sesame oil
## Steps
1. Slice chicken and vegetables.
2. Heat oil in a wok.
3. Stir-fry chicken until browned.
4. Add vegetables and soy sauce.
5. Cook until tender-crisp.

View File

@@ -0,0 +1,21 @@
---
author.name: Max Richter
---
# Tomato Soup
A simple, comforting soup made from fresh tomatoes.
## Ingredients
- 800 g tomatoes
- 1 onion
- 2 cloves garlic
- Olive oil
- Salt and pepper
## Steps
1. Sauté onion and garlic in olive oil.
2. Add chopped tomatoes and cook until softened.
3. Blend until smooth.
4. Simmer for 10 minutes.
5. Season and serve warm.

View File

@@ -0,0 +1,21 @@
---
author.name: Max Richter
datePublished: 2025-08-17
---
# Crème brûlée
A French dessert with tricky accents and formatting.
## Ingredients
- 500 ml cream
- 100 g sugar
- 4 egg yolks
- 1 vanilla pod
## Steps
1. Heat cream & vanilla until steaming.
2. Whisk yolks + sugar until pale.
3. Combine gently → do not curdle!
4. Bake in water bath at 150 °C.
5. Chill, then caramelize sugar with a torch 🔥.

11
examples/Recipes/water.md Normal file
View File

@@ -0,0 +1,11 @@
---
author.name: Max
---
# Water
## Ingredients
- Water
## Steps
1. Drink it

View File

@@ -4,6 +4,7 @@ use (
./parser
./registry
./renderer
./server
./template
./testdata
./validator

View File

@@ -23,8 +23,15 @@
"ingredients": {
"description": "A single ingredient used in the recipe, e.g. sugar, flour or garlic.",
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" } }
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"nutrition": {
@@ -42,27 +49,54 @@
"recipeIngredient": {
"description": "A single ingredient used in the recipe, e.g. sugar, flour or garlic.",
"oneOf": [
{ "type": "string" },
{ "type": "array", "items": { "type": "string" } }
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"recipeInstructions": {
"description": "A step in making the recipe, in the form of a single item (document, video, etc.) or an ordered list with HowToStep and/or HowToSection items.",
"anyOf": [
{ "type": "string" },
{ "$ref": "schema:CreativeWork" },
{ "$ref": "schema:ItemList" }
{
"type": "string"
},
{
"$ref": "schema:CreativeWork"
},
{
"$ref": "schema:ItemList"
}
]
},
"recipeYield": {
"description": "The quantity produced by the recipe (for example, number of people served, number of servings, etc).",
"anyOf": [{ "type": "string" }, { "$ref": "schema:QuantitativeValue" }]
"anyOf": [
{
"type": "string"
},
{
"$ref": "schema:QuantitativeValue"
}
]
},
"suitableForDiet": {
"description": "Indicates a dietary restriction or guideline for which this recipe or menu item is suitable, e.g. diabetic, halal etc.",
"oneOf": [
{ "$ref": "schema:RestrictedDiet" },
{ "type": "array", "items": { "$ref": "schema:RestrictedDiet" } }
{
"$ref": "schema:RestrictedDiet"
},
{
"type": "array",
"items": {
"$ref": "schema:RestrictedDiet"
}
}
]
}
}

View File

@@ -25,6 +25,10 @@
- path: prepTime
- path: cookTime
- path: recipeYield
- path: cookingMethod
- path: nutrition
- path: recipeCategory
- path: suitableForDiet
}
---

View File

@@ -4,7 +4,5 @@
codec: yaml
fields:
- path: "@type"
codec: const
value: Recipe
}
---

View File

@@ -0,0 +1,37 @@
package main
import (
"flag"
"log"
"net/http"
"os"
"path/filepath"
"git.max-richter.dev/max/marka/server/internal/handlers"
)
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")
}
// Catch-all route from "/" → serve files rooted in absRoot
http.Handle("/", handlers.NewFile(absRoot))
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)
}
}

3
server/go.mod Normal file
View File

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

View File

@@ -0,0 +1,23 @@
package fsx
import (
"mime"
"path/filepath"
"strings"
)
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
}
switch ext {
case ".txt", ".log", ".json", ".yaml", ".yml", ".toml", ".xml", ".csv":
return "text/plain; charset=utf-8"
}
return "application/octet-stream"
}

View File

@@ -0,0 +1,56 @@
package fsx
import (
"errors"
"path/filepath"
"strings"
)
func CleanURLLike(p string) string {
p = strings.TrimSpace(p)
if p == "" || p == "/" {
return "/"
}
parts := []string{}
for _, seg := range strings.Split(strings.ReplaceAll(p, "\\", "/"), "/") {
switch seg {
case "", ".":
continue
case "..":
if len(parts) > 0 {
parts = parts[:len(parts)-1]
}
default:
parts = append(parts, seg)
}
}
return "/" + strings.Join(parts, "/")
}
func SafeRel(root, requested string) (string, error) {
s := CleanURLLike(requested)
if strings.HasPrefix(s, "/") {
s = strings.TrimPrefix(s, "/")
}
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)
}

View File

@@ -0,0 +1,171 @@
// Package handlers provides HTTP handlers for the file system.
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"
)
type File struct{ root string }
func NewFile(root string) http.Handler { return &File{root: root} }
func (h *File) ServeHTTP(w http.ResponseWriter, r *http.Request) {
reqPath := r.URL.Path
if reqPath == "" {
reqPath = "/"
}
cleanRel, err := fsx.SafeRel(h.root, reqPath)
if err != nil {
httpx.WriteError(w, http.StatusBadRequest, err)
return
}
target := filepath.Join(h.root, filepath.FromSlash(cleanRel))
switch r.Method {
case http.MethodGet:
h.get(w, r, target)
case http.MethodPost:
h.post(w, r, target)
default:
httpx.WriteError(w, http.StatusMethodNotAllowed, errors.New("method not allowed"))
}
}
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,20 @@
package httpx
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Error string `json:"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)
}
func WriteError(w http.ResponseWriter, code int, err error) {
WriteJSON(w, code, ErrorResponse{Error: err.Error()})
}

View File

@@ -0,0 +1,11 @@
package httpx
import "net/http"
type Router struct{ mux *http.ServeMux }
func NewRouter() *Router { return &Router{mux: http.NewServeMux()} }
func (r *Router) Handle(pattern string, h http.Handler) { r.mux.Handle(pattern, h) }
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.mux.ServeHTTP(w, req)
}