feat: merge origin
This commit is contained in:
@@ -44,7 +44,7 @@ A Docker image is available to transform a specified directory into a CRUD API.
|
||||
|
||||
To run the Docker image:
|
||||
```bash
|
||||
docker run -p 8080:8080 -v /path/to/your/data:/app/data marka-crud-api
|
||||
docker run -p 8080:8080 -v /path/to/your/data:/app/data max/marka-server
|
||||
```
|
||||
*(Replace `/path/to/your/data` with the absolute path to the directory you want to expose.)*
|
||||
|
||||
|
29
examples/Article/A dress a day.md
Normal file
29
examples/Article/A dress a day.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
_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
|
||||
reviewRating.ratingValue: 5
|
||||
---
|
||||
|
||||
# You Dont Have to Be Pretty – A Dress A Day
|
||||
|
||||
#positivity
|
||||
|
||||

|
||||
|
||||
*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). I’m 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 Don’t Have to Be Pretty. You don’t _owe_ prettiness to anyone. Not to your boyfriend/spouse/partner, not to your co-workers, especially not to random men on the street. You don’t owe it to your mother, you don’t owe it to your children, you don’t owe it to civilization in general. Prettiness is not a rent you pay for occupying a space marked “female”.
|
||||
|
||||
I’m not saying that you SHOULDN’T be pretty if you want to. (You don’t 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-don’t-have-to-be-pretty mean in practical, everyday terms? It means that you don’t 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 isn’t 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 wasn’t much of a tool.
|
||||
|
||||
Pretty, it’s sad to say, can have a shelf life. It’s so tied up with youth that, at some point (if you’re lucky), you’re 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 won’t 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 that’s not PRETTY”!)
|
18
examples/Books/Minimal.md
Normal file
18
examples/Books/Minimal.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
_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.
|
17
examples/Books/NoAuthor.md
Normal file
17
examples/Books/NoAuthor.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
_type: Review
|
||||
author:
|
||||
_type: Person
|
||||
name: Eve
|
||||
datePublished: 2025-08-15
|
||||
itemReviewed:
|
||||
_type: Book
|
||||
name: Anonymous Poems
|
||||
reviewBody: "Short, haunting, and powerful verses."
|
||||
reviewRating.ratingValue: 4
|
||||
---
|
||||
|
||||
# Anonymous Poems
|
||||
|
||||
## Review
|
||||
Short, haunting, and powerful verses.
|
18
examples/Books/NoBody.md
Normal file
18
examples/Books/NoBody.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
_type: Review
|
||||
author:
|
||||
_type: Person
|
||||
name: Clara
|
||||
datePublished: 2025-08-10
|
||||
itemReviewed:
|
||||
_type: Book
|
||||
name: 1984
|
||||
author:
|
||||
_type: Person
|
||||
name: George Orwell
|
||||
reviewRating: 5
|
||||
---
|
||||
|
||||
# 1984
|
||||
|
||||
## Review
|
27
examples/Books/NoRating.md
Normal file
27
examples/Books/NoRating.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
_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.
|
17
examples/Books/TheGreatGatsby.md
Normal file
17
examples/Books/TheGreatGatsby.md
Normal 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.
|
15
examples/Movies/Minimal.md
Normal file
15
examples/Movies/Minimal.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
_type: Review
|
||||
author:
|
||||
_type: Person
|
||||
name: Frank
|
||||
datePublished: 2025-08-01
|
||||
itemReviewed:
|
||||
_type: Movie
|
||||
name: Untitled Film
|
||||
---
|
||||
|
||||
# Untitled Film
|
||||
|
||||
## Review
|
||||
Unclear, experimental, and strange.
|
22
examples/Movies/Negative.md
Normal file
22
examples/Movies/Negative.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
_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 it’s fascinating.
|
21
examples/Movies/NoBody.md
Normal file
21
examples/Movies/NoBody.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
_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
|
23
examples/Movies/Structured.md
Normal file
23
examples/Movies/Structured.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
_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.
|
17
examples/Recipes/baguette.md
Normal file
17
examples/Recipes/baguette.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
_type: Recipe
|
||||
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
21
examples/Recipes/cake.md
Normal 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.
|
20
examples/Recipes/caprese-salad.md
Normal file
20
examples/Recipes/caprese-salad.md
Normal 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.
|
18
examples/Recipes/cheese-sandwich.md
Normal file
18
examples/Recipes/cheese-sandwich.md
Normal 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.
|
20
examples/Recipes/complex.md
Normal file
20
examples/Recipes/complex.md
Normal 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.
|
21
examples/Recipes/lentil-curry.md
Normal file
21
examples/Recipes/lentil-curry.md
Normal 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.
|
23
examples/Recipes/multiline.md
Normal file
23
examples/Recipes/multiline.md
Normal 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 4–6 hours.
|
||||
3. Strain broth.
|
||||
4. Add soy sauce and mirin before serving.
|
17
examples/Recipes/optional-fields.md
Normal file
17
examples/Recipes/optional-fields.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
author.name: Max Richter
|
||||
prepTime:
|
||||
cookTime:
|
||||
recipeYield:
|
||||
cookingMethod:
|
||||
---
|
||||
|
||||
# Mystery Dish
|
||||
|
||||
A recipe with missing optional metadata.
|
||||
|
||||
## Ingredients
|
||||
- ???
|
||||
|
||||
## Steps
|
||||
1. ???
|
21
examples/Recipes/pancakes.md
Normal file
21
examples/Recipes/pancakes.md
Normal 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
22
examples/Recipes/salad.md
Normal 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.
|
21
examples/Recipes/spaghetti-carbonara.md
Normal file
21
examples/Recipes/spaghetti-carbonara.md
Normal 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.
|
21
examples/Recipes/stir-fry.md
Normal file
21
examples/Recipes/stir-fry.md
Normal 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.
|
21
examples/Recipes/tomato-soup.md
Normal file
21
examples/Recipes/tomato-soup.md
Normal 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.
|
21
examples/Recipes/unicode.md
Normal file
21
examples/Recipes/unicode.md
Normal 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
11
examples/Recipes/water.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
author.name: Max
|
||||
---
|
||||
|
||||
# Water
|
||||
|
||||
## Ingredients
|
||||
- Water
|
||||
|
||||
## Steps
|
||||
1. Drink it
|
4
go.work
4
go.work
@@ -1,9 +1,11 @@
|
||||
go 1.24.5
|
||||
go 1.24.7
|
||||
|
||||
use (
|
||||
./parser
|
||||
./playground/wasm
|
||||
./registry
|
||||
./renderer
|
||||
./server
|
||||
./template
|
||||
./testdata
|
||||
./validator
|
||||
|
@@ -1,4 +1,8 @@
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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/go.mod h1:n793S7TENIfgHpZLz0lm0qorM7eCx3zBLby3Fb++hZA=
|
||||
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/go.mod h1:88SkY5pTONkgfBy1FT10LoqRC8rt36iF1fk/rupjuJY=
|
||||
git.max-richter.dev/max/marka/validator v0.0.0-20250819170608-69c2550f448e/go.mod h1:qdGfCFRzsGedmnd77vb7pu/EMx0W0DcQBMEfvNxMYsw=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
|
@@ -20,21 +20,27 @@ func ParseBlock(input string, block template.Block) (any, error) {
|
||||
case template.CodecHashtags:
|
||||
return Keywords(input, block)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown codec: %s", block.Codec)
|
||||
fmt.Printf("%#v\n", block)
|
||||
return nil, fmt.Errorf("unknown codec '%s'", block.Codec)
|
||||
}
|
||||
|
||||
func Parse(matches []matcher.Block) (any, error) {
|
||||
var result any
|
||||
|
||||
for _, m := range matches {
|
||||
for i, m := range matches {
|
||||
if m.Block.Path == "@index" {
|
||||
continue
|
||||
}
|
||||
|
||||
input := m.GetContent()
|
||||
value, err := ParseBlock(input, m.Block)
|
||||
var blockIdentifier any
|
||||
blockIdentifier = m.Block.Path
|
||||
if blockIdentifier == "" {
|
||||
blockIdentifier = fmt.Sprintf("#%d", i)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse block(%s): %w", m.Block.Path, err)
|
||||
return nil, fmt.Errorf("failed to parse block(%s) -> %w", blockIdentifier, err)
|
||||
}
|
||||
result = utils.SetPathValue(m.Block.Path, value, result)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
module git.max-richter.dev/max/marka/parser
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.7
|
||||
|
||||
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
|
||||
)
|
||||
|
@@ -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=
|
||||
|
@@ -50,7 +50,6 @@ func MatchBlocksFuzzy(markdown string, templateBlocks []template.Block, maxDist
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the last block
|
||||
if len(templateBlocks) > 0 {
|
||||
lastBlock := templateBlocks[len(templateBlocks)-1]
|
||||
if lastBlock.Type == template.DataBlock {
|
||||
|
@@ -10,21 +10,21 @@ import (
|
||||
"git.max-richter.dev/max/marka/testdata"
|
||||
)
|
||||
|
||||
func TestFuzzyFindAll(t *testing.T) {
|
||||
func TestMatch_FuzzyFindAll(t *testing.T) {
|
||||
recipeMd := testdata.Read(t, "baguette/input.md")
|
||||
|
||||
tests := []struct {
|
||||
Needle string
|
||||
Start, End, StartIndex int
|
||||
}{
|
||||
{StartIndex: 0, Needle: "# Ingredients\n", Start: 77, End: 91},
|
||||
{StartIndex: 0, Needle: "# Ingrdients\n", Start: 77, End: 91},
|
||||
{StartIndex: 0, Needle: "# Inrdients\n", Start: 77, End: 91},
|
||||
{StartIndex: 0, Needle: "## Ingredients\n", Start: 90, End: 105},
|
||||
{StartIndex: 0, Needle: "## Ingrdients\n", Start: 90, End: 105},
|
||||
{StartIndex: 0, Needle: "## Inrdients\n", Start: 90, End: 105},
|
||||
{StartIndex: 0, Needle: "---\n", Start: 0, End: 4},
|
||||
{StartIndex: 4, Needle: "---\n", Start: 29, End: 33},
|
||||
{StartIndex: 0, Needle: "# Steps\n", Start: 116, End: 124},
|
||||
{StartIndex: 0, Needle: "# Stps\n", Start: 116, End: 124},
|
||||
{StartIndex: 0, Needle: "# Step\n", Start: 116, End: 124},
|
||||
{StartIndex: 4, Needle: "---\n", Start: 43, End: 47},
|
||||
{StartIndex: 0, Needle: "## Steps\n", Start: 129, End: 138},
|
||||
{StartIndex: 0, Needle: "## Stps\n", Start: 129, End: 138},
|
||||
{StartIndex: 0, Needle: "## Step\n", Start: 129, End: 138},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -36,13 +36,14 @@ func TestFuzzyFindAll(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyBlockMatch(t *testing.T) {
|
||||
func TestMatch_FuzzyBlockBaguette(t *testing.T) {
|
||||
recipeMd := testdata.Read(t, "baguette/input.md")
|
||||
schemaMd, err := registry.GetTemplate("Recipe")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load template: %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
blocks, err := template.CompileTemplate(schemaMd)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to compile template: %s", err.Error())
|
||||
@@ -51,20 +52,21 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
||||
|
||||
matches := matcher.MatchBlocksFuzzy(string(recipeMd), blocks, 0.3)
|
||||
|
||||
for _, m := range matches {
|
||||
fmt.Printf("Content: '%s'->'%q'\n\n", m.Block.Path, m.GetContent())
|
||||
}
|
||||
|
||||
expected := []struct {
|
||||
value string
|
||||
}{
|
||||
{
|
||||
value: "@type: Recipe\nauthor.name: Max Richter",
|
||||
value: "_type: Recipe\nauthor.name: Max Richter",
|
||||
},
|
||||
{
|
||||
value: "Baguette",
|
||||
},
|
||||
{
|
||||
value: "\nMy favourite baguette recipe",
|
||||
},
|
||||
{
|
||||
value: "",
|
||||
value: "My favourite baguette recipe",
|
||||
},
|
||||
{
|
||||
value: "- Flour\n- Water\n- Salt",
|
||||
@@ -80,13 +82,12 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
||||
t.FailNow()
|
||||
}
|
||||
if expected[i].value != m.GetContent() {
|
||||
t.Errorf("Match %d did not match expected: %q", i, m.GetContent())
|
||||
t.Errorf("Match %d did not match expected: %q", i, expected[i].value)
|
||||
}
|
||||
fmt.Printf("match: %s->%q\n", m.Block.Path, m.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyBlockMatchSalad(t *testing.T) {
|
||||
func TestMatch_FuzzyBlockSalad(t *testing.T) {
|
||||
recipeMd := testdata.Read(t, "recipe_salad/input.md")
|
||||
schemaMd, err := registry.GetTemplate("Recipe")
|
||||
if err != nil {
|
||||
@@ -95,7 +96,7 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
|
||||
}
|
||||
blocks, err := template.CompileTemplate(schemaMd)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to compile template: %s", err.Error())
|
||||
t.Errorf("failed to compile template: %s", err.Error())
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
@@ -105,14 +106,11 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
|
||||
value string
|
||||
}{
|
||||
{
|
||||
value: "@type: Recipe\nauthor.name: Alex Chef\ncookTime: PT0M\nimage: https://example.com/salad.jpg\nprepTime: PT10M\nrecipeYield: 2 servings",
|
||||
value: "_type: Recipe\nauthor.name: Alex Chef\ncookTime: PT0M\nimage: https://example.com/salad.jpg\nprepTime: PT10M\nrecipeYield: 2 servings",
|
||||
},
|
||||
{
|
||||
value: "Simple Salad",
|
||||
},
|
||||
{
|
||||
value: "#healthy #salad",
|
||||
},
|
||||
{
|
||||
value: "A quick green salad.",
|
||||
},
|
||||
|
@@ -15,62 +15,69 @@ import (
|
||||
func DetectType(markdownContent string) (string, error) {
|
||||
defaultSchemaContent, err := registry.GetTemplate("_default")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not get schema: %w", err)
|
||||
return "", fmt.Errorf("could not get schema -> %w", err)
|
||||
}
|
||||
|
||||
defaultSchema, err := template.CompileTemplate(defaultSchemaContent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to compile template: %w", err)
|
||||
return "", fmt.Errorf("failed to compile template -> %w", err)
|
||||
}
|
||||
|
||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, defaultSchema, 0.3)
|
||||
|
||||
result, err := decoders.Parse(blocks)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse blocks: %w", err)
|
||||
return "", fmt.Errorf("failed to parse blocks -> %w", err)
|
||||
}
|
||||
|
||||
if result, ok := result.(map[string]any); ok {
|
||||
if contentType, ok := result["@type"]; ok {
|
||||
if contentType, ok := result["_type"]; ok {
|
||||
return contentType.(string), nil
|
||||
} else {
|
||||
return "", fmt.Errorf("frontmatter did not contain '@type'")
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("frontmatter did not contain '_type'")
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not parse frontmatter")
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMarkdown(input string) string {
|
||||
input = strings.TrimSuffix(input, "\n")
|
||||
input = strings.ReplaceAll(input, "@type:", `"@type":`)
|
||||
input = strings.ReplaceAll(input, "@context:", `"@context":`)
|
||||
return input
|
||||
func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, error) {
|
||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||
|
||||
tpl, err := template.CompileTemplate(templateContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
||||
}
|
||||
|
||||
return matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3), nil
|
||||
}
|
||||
|
||||
func ParseFile(markdownContent string) (any, error) {
|
||||
markdownContent = prepareMarkdown(markdownContent)
|
||||
|
||||
contentType, err := DetectType(markdownContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not detect type: %w", err)
|
||||
return nil, fmt.Errorf("could not detect type -> %w", err)
|
||||
}
|
||||
|
||||
templateContent, err := registry.GetTemplate(contentType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get schema: %w", err)
|
||||
return nil, fmt.Errorf("could not get template -> %w", err)
|
||||
}
|
||||
|
||||
template, err := template.CompileTemplate(templateContent)
|
||||
return ParseFileWithTemplate(markdownContent, templateContent)
|
||||
}
|
||||
|
||||
func ParseFileWithTemplate(markdownContent string, templateContent string) (any, error) {
|
||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||
|
||||
tpl, err := template.CompileTemplate(templateContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile template: %w", err)
|
||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
||||
}
|
||||
|
||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, template, 0.3)
|
||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
||||
|
||||
result, err := decoders.Parse(blocks)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse blocks: %w", err)
|
||||
return nil, fmt.Errorf("failed to compile blocks -> %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
@@ -9,7 +9,30 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestParseRecipe_Golden(t *testing.T) {
|
||||
func TestParse_DetectType(t *testing.T) {
|
||||
recipe := testdata.Read(t, "recipe_salad/input.md")
|
||||
article := testdata.Read(t, "article_simple/input.md")
|
||||
|
||||
recipeType, err := parser.DetectType(string(recipe))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect recipeType: %v", err)
|
||||
}
|
||||
|
||||
articleType, err := parser.DetectType(string(article))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to detect articleType: %v", err)
|
||||
}
|
||||
|
||||
if recipeType != "Recipe" {
|
||||
t.Errorf("recipeType did not match expected type 'Recipe' -> %s", recipeType)
|
||||
}
|
||||
|
||||
if articleType != "Article" {
|
||||
t.Errorf("articleType did not match expected type 'Article' -> %s", articleType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_RecipeSalad(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "recipe_salad/input.md")
|
||||
output := testdata.Read(t, "recipe_salad/output.json")
|
||||
|
||||
@@ -28,7 +51,7 @@ func TestParseRecipe_Golden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRecipe_NoDescription(t *testing.T) {
|
||||
func TestParse_RecipeNoDescription(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "recipe_no_description/input.md")
|
||||
|
||||
got, err := parser.ParseFile(string(inputContent))
|
||||
@@ -47,7 +70,7 @@ func TestParseRecipe_NoDescription(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRecipe_Baguette(t *testing.T) {
|
||||
func TestParse_Baguette(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "baguette/input.md")
|
||||
|
||||
got, err := parser.ParseFile(string(inputContent))
|
||||
@@ -66,7 +89,7 @@ func TestParseRecipe_Baguette(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArticle_Simple(t *testing.T) {
|
||||
func TestParse_Article(t *testing.T) {
|
||||
inputContent := testdata.Read(t, "article_simple/input.md")
|
||||
|
||||
got, err := parser.ParseFile(string(inputContent))
|
||||
|
23
playground/.gitignore
vendored
Normal file
23
playground/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
1
playground/.npmrc
Normal file
1
playground/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
engine-strict=true
|
9
playground/.prettierignore
Normal file
9
playground/.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
|
||||
# Miscellaneous
|
||||
/static/
|
16
playground/.prettierrc
Normal file
16
playground/.prettierrc
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tailwindStylesheet": "./src/app.css"
|
||||
}
|
38
playground/README.md
Normal file
38
playground/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# sv
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```sh
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
42
playground/eslint.config.js
Normal file
42
playground/eslint.config.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default defineConfig(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
{ ignores: ['static/wasm_exec.js'] },
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: {
|
||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
58
playground/package.json
Normal file
58
playground/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "playground",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.9",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/node": "^22",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.3.4",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.3",
|
||||
"@fsegurai/codemirror-theme-github-light": "^6.2.2",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-theme-github": "^1.1.0",
|
||||
"lucide-svelte": "^0.544.0",
|
||||
"svelte-codemirror-editor": "^2.0.0"
|
||||
}
|
||||
}
|
2933
playground/pnpm-lock.yaml
generated
Normal file
2933
playground/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
playground/src/app.css
Normal file
6
playground/src/app.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
|
||||
.cm-foldGutter + .cm-foldGutter {
|
||||
display: none !important;
|
||||
}
|
30
playground/src/app.d.ts
vendored
Normal file
30
playground/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
class Go {
|
||||
new(): {
|
||||
run: (inst: WebAssembly.Instance) => Promise<void>;
|
||||
importObject: WebAssembly.Imports;
|
||||
};
|
||||
}
|
||||
|
||||
const marka: {
|
||||
matchBlocks(s: string, t: string): string;
|
||||
detectType(markdown: string): string;
|
||||
parseFile(input: string): string;
|
||||
parseFileWithTemplate(markdown: string, template: string): string;
|
||||
listTemplates(): string;
|
||||
getTemplate(name: string): string;
|
||||
compileTemplate(source: string): string;
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
12
playground/src/app.html
Normal file
12
playground/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="/wasm_exec.js"></script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
1
playground/src/lib/assets/favicon.svg
Normal file
1
playground/src/lib/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
94
playground/src/lib/components/EditorPanel.svelte
Normal file
94
playground/src/lib/components/EditorPanel.svelte
Normal file
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import CheckCircleIcon from '$lib/icons/CheckCircleIcon.svelte';
|
||||
import MinusCircleIcon from '$lib/icons/MinusCircleIcon.svelte';
|
||||
import XCircleIcon from '$lib/icons/XCircleIcon.svelte';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { githubLight } from '@fsegurai/codemirror-theme-github-light';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import type { Snippet } from 'svelte';
|
||||
import CodeMirror from 'svelte-codemirror-editor';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
children?: Snippet;
|
||||
headerActions?: Snippet;
|
||||
subtitle?: string;
|
||||
error?: string;
|
||||
status?: 'success' | 'error' | 'indeterminate';
|
||||
timing?: number;
|
||||
pillText?: string;
|
||||
langExtension?: Extension;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
error,
|
||||
value = $bindable(),
|
||||
placeholder = '',
|
||||
readonly = false,
|
||||
children,
|
||||
headerActions,
|
||||
subtitle,
|
||||
status,
|
||||
timing,
|
||||
pillText,
|
||||
langExtension
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="relative flex flex-1 flex-col overflow-hidden border-r border-gray-200 last:border-r-0">
|
||||
<div class="flex items-center border-b border-gray-200 bg-gray-50/50 px-4 py-3">
|
||||
{#if status === 'success'}
|
||||
<CheckCircleIcon class="mr-2 h-5 w-5 text-green-500" />
|
||||
{:else if status === 'error'}
|
||||
<XCircleIcon class="mr-2 h-5 w-5 text-red-500" />
|
||||
{:else if status === 'indeterminate'}
|
||||
<MinusCircleIcon class="mr-2 h-5 w-5 text-gray-400" />
|
||||
{/if}
|
||||
<div class="flex-1">
|
||||
<h2 class="flex items-center text-sm font-semibold tracking-wide text-gray-900 uppercase">
|
||||
{title}
|
||||
{#if pillText}
|
||||
<span
|
||||
class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800"
|
||||
>
|
||||
{pillText}
|
||||
</span>
|
||||
{/if}
|
||||
</h2>
|
||||
{#if subtitle}
|
||||
<p class="text-xs text-gray-500">{subtitle}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if headerActions}
|
||||
<div class="ml-4">
|
||||
{@render headerActions()}
|
||||
</div>
|
||||
{/if}
|
||||
{#if timing !== undefined}
|
||||
<div class="ml-4 text-xs text-gray-500">{timing}ms</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="border-b border-gray-200 bg-red-300 p-4 text-xs">
|
||||
<pre>{error}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CodeMirror
|
||||
bind:value
|
||||
extensions={[basicSetup, langExtension].filter(Boolean) as Extension[]}
|
||||
theme={githubLight}
|
||||
{placeholder}
|
||||
{readonly}
|
||||
class="text-sm"
|
||||
/>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</div>
|
21
playground/src/lib/components/Header.svelte
Normal file
21
playground/src/lib/components/Header.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$lib/icons/Logo.svelte';
|
||||
import { GithubIcon } from 'lucide-svelte';
|
||||
</script>
|
||||
|
||||
<header class="sticky top-0 z-10 border-b border-gray-200 bg-white/80 backdrop-blur-sm">
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Logo />
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-black">Marka</h1>
|
||||
<p class="text-sm text-gray-600">Bidirectional Markdown ↔ JSON Parser</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/jim-fx/marka" target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon class="h-6 w-6 text-gray-600 transition-colors duration-200 hover:text-black" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
197
playground/src/lib/components/Playground.svelte
Normal file
197
playground/src/lib/components/Playground.svelte
Normal file
@@ -0,0 +1,197 @@
|
||||
<script lang="ts">
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import {
|
||||
compileTemplate,
|
||||
getTemplate,
|
||||
listTemplates,
|
||||
parseMarkdown,
|
||||
parseMarkdownWithTemplate,
|
||||
wasmReady,
|
||||
type ParseResultSuccess
|
||||
} from '../wasm';
|
||||
import EditorPanel from './EditorPanel.svelte';
|
||||
|
||||
let templates = $state([] as string[]);
|
||||
|
||||
const DEFAULT_MARKDOWN_VALUE = `---
|
||||
_type: Recipe
|
||||
author.name: Max Richter
|
||||
---
|
||||
|
||||
# Baguette
|
||||
|
||||
My favourite baguette recipe
|
||||
|
||||
## Ingredients
|
||||
- Flour
|
||||
- Water
|
||||
- Salt
|
||||
|
||||
## Steps
|
||||
1. Mix Flour Water and Salt
|
||||
2. Bake the bread`;
|
||||
|
||||
const DEFAULT_TEMPLATE_VALUE = '';
|
||||
|
||||
let templateValue = $state(
|
||||
typeof window !== 'undefined'
|
||||
? localStorage.getItem('templateValue') || DEFAULT_TEMPLATE_VALUE
|
||||
: DEFAULT_TEMPLATE_VALUE
|
||||
);
|
||||
let markdownValue = $state(
|
||||
typeof window !== 'undefined'
|
||||
? localStorage.getItem('markdownValue') || DEFAULT_MARKDOWN_VALUE
|
||||
: DEFAULT_MARKDOWN_VALUE
|
||||
);
|
||||
|
||||
let jsonOutput = $state('');
|
||||
let detectedSchemaName = $derived.by(() => {
|
||||
try {
|
||||
return JSON.parse(jsonOutput)['_schema'];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
let timings = $state<ParseResultSuccess['timings'] | null>(null);
|
||||
let templateStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
|
||||
let dataStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
|
||||
let templateError = $state<string | undefined>();
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('templateValue', templateValue);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('markdownValue', markdownValue);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($wasmReady) {
|
||||
try {
|
||||
templates = listTemplates();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
if (!$wasmReady) {
|
||||
jsonOutput = 'Loading wasm...';
|
||||
timings = null;
|
||||
templateStatus = undefined;
|
||||
dataStatus = undefined;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
compileTemplate(templateValue);
|
||||
|
||||
const result = templateValue
|
||||
? parseMarkdownWithTemplate(markdownValue, templateValue)
|
||||
: parseMarkdown(markdownValue);
|
||||
|
||||
if ('error' in result) {
|
||||
jsonOutput = '';
|
||||
if (result.error.startsWith('failed to compile template')) {
|
||||
templateError = result.error.replaceAll(' -> ', '\n ⟶ ');
|
||||
templateStatus = 'error';
|
||||
dataStatus = 'indeterminate';
|
||||
} else {
|
||||
templateError = undefined;
|
||||
templateStatus = undefined;
|
||||
dataStatus = 'error';
|
||||
}
|
||||
} else {
|
||||
templateError = undefined;
|
||||
jsonOutput = JSON.stringify(result, null, 2);
|
||||
timings = result.timings;
|
||||
templateStatus = 'success';
|
||||
dataStatus = 'success';
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.log({ e });
|
||||
jsonOutput = (e as Error).message;
|
||||
timings = null;
|
||||
if (jsonOutput.startsWith('failed to compile template')) {
|
||||
templateStatus = 'error';
|
||||
dataStatus = 'indeterminate';
|
||||
} else {
|
||||
templateStatus = undefined;
|
||||
dataStatus = 'error';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function loadTemplate(name: string) {
|
||||
if (!name) return;
|
||||
try {
|
||||
templateValue = getTemplate(name);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function resetMarkdown() {
|
||||
markdownValue = DEFAULT_MARKDOWN_VALUE;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<div class="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-3">
|
||||
<EditorPanel
|
||||
title="Template"
|
||||
bind:value={templateValue}
|
||||
placeholder="Enter your Marka template here..."
|
||||
error={templateError}
|
||||
status={templateStatus}
|
||||
timing={timings?.template_compilation}
|
||||
subtitle="Define your mapping schema"
|
||||
langExtension={markdown()}
|
||||
>
|
||||
{#snippet headerActions()}
|
||||
<select
|
||||
onchange={(e) => loadTemplate(e.currentTarget.value)}
|
||||
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
||||
>
|
||||
<option value="">Load a template</option>
|
||||
{#each templates as template (template)}
|
||||
<option value={template}>{template}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/snippet}
|
||||
</EditorPanel>
|
||||
|
||||
<EditorPanel
|
||||
title="Markdown"
|
||||
bind:value={markdownValue}
|
||||
placeholder="Enter your markdown content here..."
|
||||
timing={timings?.markdown_parsing}
|
||||
subtitle="Your source content"
|
||||
langExtension={markdown()}
|
||||
>
|
||||
{#snippet headerActions()}
|
||||
<button
|
||||
onclick={resetMarkdown}
|
||||
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 focus:outline-none"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{/snippet}
|
||||
</EditorPanel>
|
||||
|
||||
<EditorPanel
|
||||
title="Data"
|
||||
value={jsonOutput}
|
||||
readonly={true}
|
||||
status={dataStatus}
|
||||
subtitle="Parsed JSON output"
|
||||
pillText={!templateValue && detectedSchemaName
|
||||
? `Detected Template: ${detectedSchemaName}`
|
||||
: undefined}
|
||||
langExtension={json()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
25
playground/src/lib/components/StatusBar.svelte
Normal file
25
playground/src/lib/components/StatusBar.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
// import { CheckCircleIcon, AlertCircleIcon } from "lucide-svelte";
|
||||
|
||||
let status = $state<'success' | 'error' | 'idle'>('success');
|
||||
let message = $state('Template parsed successfully');
|
||||
</script>
|
||||
|
||||
<div class="border-t border-gray-200 bg-gray-50/50 px-6 py-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if status === 'success'}
|
||||
<!-- <CheckCircleIcon class="h-4 w-4 text-green-600" /> -->
|
||||
<span class="text-green-700">{message}</span>
|
||||
{:else if status === 'error'}
|
||||
<!-- <AlertCircleIcon class="h-4 w-4 text-red-600" /> -->
|
||||
<span class="text-red-700">{message}</span>
|
||||
{:else}
|
||||
<div class="h-4 w-4 rounded-full bg-gray-300"></div>
|
||||
<span class="text-gray-600">Ready</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-gray-500">Schema.org validation enabled</div>
|
||||
</div>
|
||||
</div>
|
18
playground/src/lib/icons/CheckCircleIcon.svelte
Normal file
18
playground/src/lib/icons/CheckCircleIcon.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
let { class: className = '' } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
10
playground/src/lib/icons/Logo.svelte
Normal file
10
playground/src/lib/icons/Logo.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<img src="/logo-2.svg" alt="logo" width="100%" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
aspect-ratio: 1;
|
||||
filter: drop-shadow(0px 0px 8px #0002);
|
||||
}
|
||||
</style>
|
14
playground/src/lib/icons/MinusCircleIcon.svelte
Normal file
14
playground/src/lib/icons/MinusCircleIcon.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
let { class: className = '' } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18 12H6" />
|
||||
</svg>
|
18
playground/src/lib/icons/XCircleIcon.svelte
Normal file
18
playground/src/lib/icons/XCircleIcon.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
let { class: className = '' } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
1
playground/src/lib/index.ts
Normal file
1
playground/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
114
playground/src/lib/wasm.ts
Normal file
114
playground/src/lib/wasm.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { readable } from "svelte/store";
|
||||
|
||||
|
||||
|
||||
export const wasmReady = readable(false, (set) => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadWasm = async () => {
|
||||
const go = new globalThis.Go();
|
||||
try {
|
||||
const result = await WebAssembly.instantiateStreaming(
|
||||
fetch("/main.wasm"),
|
||||
go.importObject,
|
||||
);
|
||||
go.run(result.instance);
|
||||
set(true);
|
||||
} catch (error) {
|
||||
console.error("Error loading wasm module:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
loadWasm();
|
||||
} else {
|
||||
globalThis.addEventListener("load", loadWasm);
|
||||
}
|
||||
});
|
||||
|
||||
export type ParseResultSuccess = {
|
||||
data: unknown;
|
||||
timings: { [key: string]: number };
|
||||
};
|
||||
|
||||
export type ParseResultError = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type ParseResult = ParseResultSuccess | ParseResultError;
|
||||
|
||||
export function parseMarkdown(markdown: string): ParseResult {
|
||||
if (typeof globalThis.marka?.parseFile !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const resultString = globalThis.marka.parseFile(markdown);
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function compileTemplate(templateSource: string) {
|
||||
if (typeof globalThis.marka?.compileTemplate !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const resultString = globalThis.marka.compileTemplate(templateSource);
|
||||
const result = JSON.parse(resultString);
|
||||
console.log({ result });
|
||||
return result;
|
||||
}
|
||||
|
||||
export function matchBlocks(markdown: string, template: string): ParseResult {
|
||||
if (typeof globalThis.marka?.matchBlocks !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const resultString = globalThis.marka.matchBlocks(markdown, template);
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function parseMarkdownWithTemplate(
|
||||
markdown: string,
|
||||
template: string,
|
||||
): ParseResult {
|
||||
if (typeof globalThis.marka?.parseFileWithTemplate !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const resultString = globalThis.marka.parseFileWithTemplate(
|
||||
markdown,
|
||||
template,
|
||||
);
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function listTemplates(): string[] {
|
||||
if (typeof globalThis.marka?.listTemplates !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const resultString = globalThis.marka.listTemplates();
|
||||
return JSON.parse(resultString);
|
||||
}
|
||||
|
||||
export function getTemplate(name: string): string {
|
||||
if (typeof globalThis.marka?.getTemplate !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
return globalThis.marka.getTemplate(name);
|
||||
}
|
||||
|
||||
export function detectType(markdown: string): string | ParseResultError {
|
||||
if (typeof globalThis.marka?.detectType !== "function") {
|
||||
throw new Error("Wasm module not ready");
|
||||
}
|
||||
const result = globalThis.marka.detectType(markdown);
|
||||
try {
|
||||
// If the result is a JSON string with an error, parse and return it
|
||||
const parsed = JSON.parse(result);
|
||||
if (parsed.error) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
// Otherwise, it's a plain string for success
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
14
playground/src/routes/+layout.svelte
Normal file
14
playground/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<title>Marka Playground</title>
|
||||
<script src="/wasm_exec.js"></script>
|
||||
</svelte:head>
|
||||
|
||||
{@render children?.()}
|
1
playground/src/routes/+layout.ts
Normal file
1
playground/src/routes/+layout.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
33
playground/src/routes/+page.svelte
Normal file
33
playground/src/routes/+page.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import Playground from '$lib/components/Playground.svelte';
|
||||
</script>
|
||||
|
||||
<div class="bg-background text-foreground flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<Playground />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: white;
|
||||
}
|
||||
|
||||
:global(*) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(textarea:focus) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:global(pre) {
|
||||
margin: 0;
|
||||
font-family:
|
||||
'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
3
playground/static/logo-2.svg
Normal file
3
playground/static/logo-2.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="202" height="202" viewBox="0 0 202 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M198 101C198 47.4284 154.572 4 101 4C63.6769 4 31.2771 25.0794 15.0566 55.9824C31.5802 36.8304 58.3585 25 87.5 25C107.135 25 125.868 30.5962 141.081 40.3643C163.17 54.5475 177.915 77.5995 176.999 105.066C176.961 122.231 171.229 138.064 161.595 150.771L159.861 153.057L133.999 112.606L100.778 164.568L65.0068 108.618L42.0176 144.873C57.3037 166.504 84.4388 181 114.5 181C149.395 181 180.331 159.267 193.398 130.6C196.385 121.268 198 111.323 198 101ZM29.0127 106.222C29.2632 118.862 33.0866 130.771 39.5967 141.223L64.9932 101.171L100.778 157.143L134 105.182L160.106 146.016C168.233 134.357 173 120.186 173 104.895H173.002C173.825 79.7876 160.762 58.4662 140.614 44.8486L101 108.688L61.3604 44.7793C41.8587 57.6651 29 79.7788 29 104.895L29.0127 106.222ZM202 101C202 156.781 156.781 202 101 202V198C134.1 198 163.327 181.42 180.831 156.113C164.258 173.559 140.379 185 114.5 185C82.6725 185 53.8784 169.41 37.9658 146.05C30.0365 134.409 25.3017 120.829 25.0137 106.303L25 104.895C25 77.6227 39.3659 53.7077 60.9336 40.3018L62.6338 39.2441L101 101.101L137.249 42.6855C123.002 33.9863 105.671 29 87.5 29C51.3264 29 19.6837 47.7188 7.41992 75.377C5.19019 83.5392 4 92.1306 4 101C4 154.572 47.4284 198 101 198V202C45.2192 202 0 156.781 0 101C0 45.2192 45.2192 0 101 0C156.781 0 202 45.2192 202 101Z" fill="#555"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
15
playground/static/logo.svg
Normal file
15
playground/static/logo.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 50.0001C0 28.9476 20.1238 13 43.5 13C53.0351 13 71.5842 16.8812 80 30C80 30 75.9901 41.3368 62.8713 43.5645C49.7525 45.7922 40.5941 30.4457 30.9406 30.4457C21.2871 30.4457 13.3663 38.7833 13.3663 51.9283C13.3663 59.1669 16.0505 65.702 20 71.5C27.6856 82.7837 41.5584 90 57 90C80.3762 90 100 71.3259 100 50.0001C100 77.6145 77.6144 100 50 100C22.3856 100 0 77.6145 0 50.0001Z" fill="url(#paint0_radial_13_17)"/>
|
||||
<path d="M86.6337 51.9282C86.6337 60.2351 82.1782 70.1022 72.7723 74.505C63.3663 78.9077 57.4257 68.0693 43.3168 64.1089C29.2079 60.1485 19.6134 71.7441 19.6134 71.7441C27.299 83.0277 41.2416 90.5941 56.6832 90.5941C80.0594 90.5941 100 71.3257 100 50C100 22.3856 77.6144 0 50 0C22.3856 0 0 22.3856 0 50C0 28.9475 19.9406 13.3663 43.3168 13.3663C52.852 13.3663 61.9391 16.0842 69.3069 20.8153C80.0015 27.6822 87.0733 38.7896 86.6337 51.9802" fill="url(#paint1_radial_13_17)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_13_17" cx="0" cy="0" r="1" gradientTransform="matrix(-59 -89 76.5985 -50.3819 82 91)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8E8E8E"/>
|
||||
<stop offset="1" stop-color="#2C2C2C"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_13_17" cx="0" cy="0" r="1" gradientTransform="matrix(58 89.5 -74.0064 52.2522 26 5.5)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8E8E8E"/>
|
||||
<stop offset="1" stop-color="#2C2C2C"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
playground/static/main.wasm
Normal file
BIN
playground/static/main.wasm
Normal file
Binary file not shown.
3
playground/static/robots.txt
Normal file
3
playground/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
553
playground/static/wasm_exec.js
Normal file
553
playground/static/wasm_exec.js
Normal file
@@ -0,0 +1,553 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// This file has been modified for use by the TinyGo compiler.
|
||||
|
||||
(() => {
|
||||
// Map multiple JavaScript environments to a single common API,
|
||||
// preferring web standards over Node.js API.
|
||||
//
|
||||
// Environments considered:
|
||||
// - Browsers
|
||||
// - Node.js
|
||||
// - Electron
|
||||
// - Parcel
|
||||
|
||||
if (typeof global !== "undefined") {
|
||||
// global already exists
|
||||
} else if (typeof window !== "undefined") {
|
||||
window.global = window;
|
||||
} else if (typeof self !== "undefined") {
|
||||
self.global = self;
|
||||
} else {
|
||||
throw new Error("cannot export Go (neither global, window nor self is defined)");
|
||||
}
|
||||
|
||||
if (!global.require && typeof require !== "undefined") {
|
||||
global.require = require;
|
||||
}
|
||||
|
||||
if (!global.fs && global.require) {
|
||||
global.fs = require("node:fs");
|
||||
}
|
||||
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!global.fs) {
|
||||
let outputBuf = "";
|
||||
global.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substr(0, nl));
|
||||
outputBuf = outputBuf.substr(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.process) {
|
||||
global.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!global.crypto) {
|
||||
const nodeCrypto = require("node:crypto");
|
||||
global.crypto = {
|
||||
getRandomValues(b) {
|
||||
nodeCrypto.randomFillSync(b);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.performance) {
|
||||
global.performance = {
|
||||
now() {
|
||||
const [sec, nsec] = process.hrtime();
|
||||
return sec * 1000 + nsec / 1000000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.TextEncoder) {
|
||||
global.TextEncoder = require("node:util").TextEncoder;
|
||||
}
|
||||
|
||||
if (!global.TextDecoder) {
|
||||
global.TextDecoder = require("node:util").TextDecoder;
|
||||
}
|
||||
|
||||
// End of polyfills for common API.
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let reinterpretBuf = new DataView(new ArrayBuffer(8));
|
||||
var logLine = [];
|
||||
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
|
||||
|
||||
global.Go = class {
|
||||
constructor() {
|
||||
this._callbackTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const mem = () => {
|
||||
// The buffer may change when requesting more memory.
|
||||
return new DataView(this._inst.exports.memory.buffer);
|
||||
}
|
||||
|
||||
const unboxValue = (v_ref) => {
|
||||
reinterpretBuf.setBigInt64(0, v_ref, true);
|
||||
const f = reinterpretBuf.getFloat64(0, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = v_ref & 0xffffffffn;
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
|
||||
const loadValue = (addr) => {
|
||||
let v_ref = mem().getBigUint64(addr, true);
|
||||
return unboxValue(v_ref);
|
||||
}
|
||||
|
||||
const boxValue = (v) => {
|
||||
const nanHead = 0x7FF80000n;
|
||||
|
||||
if (typeof v === "number") {
|
||||
if (isNaN(v)) {
|
||||
return nanHead << 32n;
|
||||
}
|
||||
if (v === 0) {
|
||||
return (nanHead << 32n) | 1n;
|
||||
}
|
||||
reinterpretBuf.setFloat64(0, v, true);
|
||||
return reinterpretBuf.getBigInt64(0, true);
|
||||
}
|
||||
|
||||
switch (v) {
|
||||
case undefined:
|
||||
return 0n;
|
||||
case null:
|
||||
return (nanHead << 32n) | 2n;
|
||||
case true:
|
||||
return (nanHead << 32n) | 3n;
|
||||
case false:
|
||||
return (nanHead << 32n) | 4n;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = BigInt(this._values.length);
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 1n;
|
||||
switch (typeof v) {
|
||||
case "string":
|
||||
typeFlag = 2n;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3n;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4n;
|
||||
break;
|
||||
}
|
||||
return id | ((nanHead | typeFlag) << 32n);
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
let v_ref = boxValue(v);
|
||||
mem().setBigUint64(addr, v_ref, true);
|
||||
}
|
||||
|
||||
const loadSlice = (array, len, cap) => {
|
||||
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (array, len, cap) => {
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (ptr, len) => {
|
||||
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
wasi_snapshot_preview1: {
|
||||
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
|
||||
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
|
||||
let nwritten = 0;
|
||||
if (fd == 1) {
|
||||
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
|
||||
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
|
||||
let ptr = mem().getUint32(iov_ptr + 0, true);
|
||||
let len = mem().getUint32(iov_ptr + 4, true);
|
||||
nwritten += len;
|
||||
for (let i=0; i<len; i++) {
|
||||
let c = mem().getUint8(ptr+i);
|
||||
if (c == 13) { // CR
|
||||
// ignore
|
||||
} else if (c == 10) { // LF
|
||||
// write line
|
||||
let line = decoder.decode(new Uint8Array(logLine));
|
||||
logLine = [];
|
||||
console.log(line);
|
||||
} else {
|
||||
logLine.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('invalid file descriptor:', fd);
|
||||
}
|
||||
mem().setUint32(nwritten_ptr, nwritten, true);
|
||||
return 0;
|
||||
},
|
||||
fd_close: () => 0, // dummy
|
||||
fd_fdstat_get: () => 0, // dummy
|
||||
fd_seek: () => 0, // dummy
|
||||
proc_exit: (code) => {
|
||||
this.exited = true;
|
||||
this.exitCode = code;
|
||||
this._resolveExitPromise();
|
||||
throw wasmExit;
|
||||
},
|
||||
random_get: (bufPtr, bufLen) => {
|
||||
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
gojs: {
|
||||
// func ticks() float64
|
||||
"runtime.ticks": () => {
|
||||
return timeOrigin + performance.now();
|
||||
},
|
||||
|
||||
// func sleepTicks(timeout float64)
|
||||
"runtime.sleepTicks": (timeout) => {
|
||||
// Do not sleep, only reactivate scheduler after the given timeout.
|
||||
setTimeout(() => {
|
||||
if (this.exited) return;
|
||||
try {
|
||||
this._inst.exports.go_scheduler();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
}, timeout);
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (v_ref) => {
|
||||
// Note: TinyGo does not support finalizers so this is only called
|
||||
// for one specific case, by js.go:jsString. and can/might leak memory.
|
||||
const id = v_ref & 0xffffffffn;
|
||||
if (this._goRefCounts?.[id] !== undefined) {
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
} else {
|
||||
console.error("syscall/js.finalizeRef: unknown id", id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (value_ptr, value_len) => {
|
||||
value_ptr >>>= 0;
|
||||
const s = loadString(value_ptr, value_len);
|
||||
return boxValue(s);
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
|
||||
let prop = loadString(p_ptr, p_len);
|
||||
let v = unboxValue(v_ref);
|
||||
let result = Reflect.get(v, prop);
|
||||
return boxValue(result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
const x = unboxValue(x_ref);
|
||||
Reflect.set(v, p, x);
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
Reflect.deleteProperty(v, p);
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (v_ref, i) => {
|
||||
return boxValue(Reflect.get(unboxValue(v_ref), i));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
|
||||
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const name = loadString(m_ptr, m_len);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
try {
|
||||
const m = Reflect.get(v, name);
|
||||
storeValue(ret_addr, Reflect.apply(m, v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
try {
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
storeValue(ret_addr, Reflect.apply(v, undefined, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
try {
|
||||
storeValue(ret_addr, Reflect.construct(v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr+ 8, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (v_ref) => {
|
||||
return unboxValue(v_ref).length;
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
|
||||
const s = String(unboxValue(v_ref));
|
||||
const str = encoder.encode(s);
|
||||
storeValue(ret_addr, str);
|
||||
mem().setInt32(ret_addr + 8, str.length, true);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
|
||||
const str = unboxValue(v_ref);
|
||||
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
|
||||
return unboxValue(v_ref) instanceof unboxValue(t_ref);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = loadSlice(dest_addr, dest_len);
|
||||
const src = unboxValue(src_ref);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
},
|
||||
|
||||
// copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
// Originally copied from upstream Go project, then modified:
|
||||
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
|
||||
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = unboxValue(dst_ref);
|
||||
const src = loadSlice(src_addr, src_len);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
|
||||
// For compatibility, we use both as long as Go 1.20 is supported.
|
||||
this.importObject.env = this.importObject.gojs;
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
this._inst = instance;
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
global,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map(); // mapping from JS values to reference ids
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
this.exitCode = 0;
|
||||
|
||||
if (this._inst.exports._start) {
|
||||
let exitPromise = new Promise((resolve, reject) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
|
||||
// Run program, but catch the wasmExit exception that's thrown
|
||||
// to return back here.
|
||||
try {
|
||||
this._inst.exports._start();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
|
||||
await exitPromise;
|
||||
return this.exitCode;
|
||||
} else {
|
||||
this._inst.exports._initialize();
|
||||
}
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
try {
|
||||
this._inst.exports.resume();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
global.require &&
|
||||
global.require.main === module &&
|
||||
global.process &&
|
||||
global.process.versions &&
|
||||
!global.process.versions.electron
|
||||
) {
|
||||
if (process.argv.length != 3) {
|
||||
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
|
||||
let exitCode = await go.run(result.instance);
|
||||
process.exit(exitCode);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
})();
|
18
playground/svelte.config.js
Normal file
18
playground/svelte.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
19
playground/tsconfig.json
Normal file
19
playground/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||
}
|
14
playground/vite.config.ts
Normal file
14
playground/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
"svelte-codemirror-editor",
|
||||
"codemirror",
|
||||
"@codemirror/language",
|
||||
],
|
||||
},
|
||||
});
|
18
playground/wasm/build.sh
Executable file
18
playground/wasm/build.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OUT_DIR="$SCRIPT_DIR/../static"
|
||||
OUT_WASM="$OUT_DIR/main.wasm"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
tinygo build -target=wasm \
|
||||
-no-debug -panic=print -gc=leaking \
|
||||
-o "$OUT_WASM" "$SCRIPT_DIR"
|
||||
|
||||
command -v wasm-opt >/dev/null && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers \
|
||||
-o "$OUT_WASM.tmp" "$OUT_WASM" && mv "$OUT_WASM.tmp" "$OUT_WASM"
|
||||
# command -v wasm-strip >/dev/null && wasm-strip "$OUT_WASM"
|
||||
|
||||
cp -f "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" "$OUT_DIR/wasm_exec.js"
|
3
playground/wasm/go.mod
Normal file
3
playground/wasm/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module git.max-richter.dev/max/marka/playground-wasm
|
||||
|
||||
go 1.24.7
|
115
playground/wasm/main.go
Normal file
115
playground/wasm/main.go
Normal file
@@ -0,0 +1,115 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"syscall/js"
|
||||
|
||||
p "git.max-richter.dev/max/marka/parser"
|
||||
"git.max-richter.dev/max/marka/registry"
|
||||
"git.max-richter.dev/max/marka/template"
|
||||
)
|
||||
|
||||
func wrapError(err error) string {
|
||||
errMap := map[string]any{"error": err.Error()}
|
||||
errJSON, _ := json.Marshal(errMap)
|
||||
return string(errJSON)
|
||||
}
|
||||
|
||||
func MatchBlocks(this js.Value, args []js.Value) any {
|
||||
s := args[0].String()
|
||||
t := args[1].String()
|
||||
matched, err := p.MatchBlocks(s, t)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
jsonString, _ := json.Marshal(matched)
|
||||
return string(jsonString)
|
||||
}
|
||||
|
||||
func DetectType(this js.Value, args []js.Value) any {
|
||||
markdown := args[0].String()
|
||||
t, err := p.DetectType(markdown)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func ParseFile(this js.Value, args []js.Value) any {
|
||||
markdown := args[0].String()
|
||||
res, err := p.ParseFile(markdown)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func ParseFileWithTemplate(this js.Value, args []js.Value) any {
|
||||
markdown := args[0].String()
|
||||
template := args[1].String()
|
||||
res, err := p.ParseFileWithTemplate(markdown, template)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func ListTemplates(this js.Value, args []js.Value) any {
|
||||
templates, err := registry.ListTemplates()
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(templates)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func GetTemplate(this js.Value, args []js.Value) any {
|
||||
name := args[0].String()
|
||||
template, err := registry.GetTemplate(name)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func CompileTemplate(this js.Value, args []js.Value) any {
|
||||
source := args[0].String()
|
||||
template, err := template.CompileTemplate(source)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
b, err := json.Marshal(template)
|
||||
if err != nil {
|
||||
return wrapError(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func main() {
|
||||
marka := js.Global().Get("Object").New()
|
||||
|
||||
marka.Set("matchBlocks", js.FuncOf(MatchBlocks))
|
||||
marka.Set("detectType", js.FuncOf(DetectType))
|
||||
marka.Set("parseFile", js.FuncOf(ParseFile))
|
||||
marka.Set("parseFileWithTemplate", js.FuncOf(ParseFileWithTemplate))
|
||||
marka.Set("listTemplates", js.FuncOf(ListTemplates))
|
||||
marka.Set("getTemplate", js.FuncOf(GetTemplate))
|
||||
marka.Set("compileTemplate", js.FuncOf(CompileTemplate))
|
||||
|
||||
js.Global().Set("marka", marka)
|
||||
|
||||
select {}
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
module git.max-richter.dev/max/marka/registry
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.7
|
||||
|
@@ -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=
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,9 @@ package registry
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed templates/*.marka
|
||||
@@ -23,3 +26,20 @@ func GetTemplate(name string) (string, error) {
|
||||
|
||||
return string(templateBytes), nil
|
||||
}
|
||||
|
||||
func ListTemplates() ([]string, error) {
|
||||
var templateNames []string
|
||||
err := fs.WalkDir(templates, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.HasSuffix(path, ".marka") {
|
||||
templateNames = append(templateNames, strings.TrimSuffix(filepath.Base(path), ".marka"))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return templateNames, nil
|
||||
}
|
||||
|
@@ -3,29 +3,32 @@
|
||||
path: .
|
||||
codec: yaml
|
||||
fields:
|
||||
- path: "@context"
|
||||
codec: const
|
||||
value: https://schema.org
|
||||
hidden: true
|
||||
- path: "@schema"
|
||||
- path: "_schema"
|
||||
codec: const
|
||||
value: Article
|
||||
hidden: true
|
||||
- path: "@type"
|
||||
- path: "_type"
|
||||
codec: const
|
||||
value: Article
|
||||
- path: image
|
||||
- path: author.name
|
||||
- path: author.@type
|
||||
- path: author._type
|
||||
codec: const
|
||||
value: Person
|
||||
hidden: true
|
||||
- path: datePublished
|
||||
- path: articleSection
|
||||
- path: reviewRating.ratingValue
|
||||
pathAlias: rating
|
||||
- path: reviewRating.bestRating
|
||||
codec: const
|
||||
hidden: true
|
||||
- path: reviewRating.worstRating
|
||||
codec: const
|
||||
hidden: true
|
||||
}
|
||||
---
|
||||
|
||||
# { headline }
|
||||
{ keywords | hashtags }
|
||||
|
||||
{ articleBody }
|
||||
|
@@ -3,19 +3,15 @@
|
||||
path: .
|
||||
codec: yaml
|
||||
fields:
|
||||
- path: "@context"
|
||||
codec: const
|
||||
value: https://schema.org
|
||||
hidden: true
|
||||
- path: "@schema"
|
||||
- path: "_schema"
|
||||
codec: const
|
||||
value: Recipe
|
||||
hidden: true
|
||||
- path: "@type"
|
||||
- path: "_type"
|
||||
codec: const
|
||||
value: Recipe
|
||||
- path: image
|
||||
- path: author.@type
|
||||
- path: author._type
|
||||
codec: const
|
||||
hidden: true
|
||||
value: Person
|
||||
@@ -25,11 +21,14 @@
|
||||
- path: prepTime
|
||||
- path: cookTime
|
||||
- path: recipeYield
|
||||
- path: cookingMethod
|
||||
- path: nutrition
|
||||
- path: recipeCategory
|
||||
- path: suitableForDiet
|
||||
}
|
||||
---
|
||||
|
||||
# { name | text }
|
||||
{ keywords | hashtags }
|
||||
|
||||
{ description | text }
|
||||
|
||||
|
@@ -3,21 +3,17 @@
|
||||
path: .
|
||||
codec: yaml
|
||||
fields:
|
||||
- path: "@context"
|
||||
codec: const
|
||||
value: https://schema.org
|
||||
hidden: true
|
||||
- path: "@schema"
|
||||
- path: "_schema"
|
||||
codec: const
|
||||
value: Review
|
||||
hidden: true
|
||||
- path: "@type"
|
||||
- path: "_type"
|
||||
codec: const
|
||||
value: Review
|
||||
- path: tmdbId
|
||||
- path: image
|
||||
- path: author.name
|
||||
- path: author.@type
|
||||
- path: author._type
|
||||
codec: const
|
||||
value: Person
|
||||
hidden: true
|
||||
@@ -39,4 +35,5 @@
|
||||
# { itemReviewed.name }
|
||||
{ keywords | hashtags }
|
||||
|
||||
## Review
|
||||
{ reviewBody }
|
||||
|
@@ -3,8 +3,6 @@
|
||||
path: .
|
||||
codec: yaml
|
||||
fields:
|
||||
- path: "@type"
|
||||
codec: const
|
||||
value: Recipe
|
||||
- path: "_type"
|
||||
}
|
||||
---
|
||||
|
@@ -13,8 +13,7 @@ import (
|
||||
const emptyBlock = "\uE000"
|
||||
|
||||
func fixRenderedBlock(input string) string {
|
||||
input = strings.ReplaceAll(input, "'@type':", "@type:")
|
||||
input = strings.ReplaceAll(input, "'@context':", "@context:")
|
||||
input = strings.ReplaceAll(input, "'_type':", "_type:")
|
||||
if len(input) == 0 {
|
||||
return emptyBlock
|
||||
}
|
||||
|
@@ -1,3 +1,18 @@
|
||||
module git.max-richter.dev/max/marka/renderer
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.7
|
||||
|
||||
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
20
renderer/go.sum
Normal 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=
|
@@ -18,10 +18,10 @@ func RenderFile(rawJSON []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||||
}
|
||||
|
||||
// 2) extract type from "@type" Property
|
||||
contentType, ok := data["@type"].(string)
|
||||
// 2) extract type from "_type" Property
|
||||
contentType, ok := data["_type"].(string)
|
||||
if !ok || contentType == "" {
|
||||
return nil, fmt.Errorf("JSON does not contain a valid '@type' property")
|
||||
return nil, fmt.Errorf("JSON does not contain a valid '_type' property")
|
||||
}
|
||||
|
||||
// 3) get the template from the registry
|
||||
|
@@ -41,10 +41,10 @@ func TestRenderFile_MissingType(t *testing.T) {
|
||||
rawJSON := []byte(`{"name": "Test"}`)
|
||||
_, err := renderer.RenderFile(rawJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing @type, got nil")
|
||||
t.Fatal("expected error for missing _type, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "JSON does not contain a valid '@type' property") {
|
||||
t.Errorf("expected missing @type error, got: %v", err)
|
||||
if !strings.Contains(err.Error(), "JSON does not contain a valid '_type' property") {
|
||||
t.Errorf("expected missing _type error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
29
server/.air.toml
Normal file
29
server/.air.toml
Normal file
@@ -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
|
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tmp/
|
16
server/Dockerfile
Normal file
16
server/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM golang:1.24.7 AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -trimpath -ldflags="-s -w" \
|
||||
-o /out/server ./server/cmd/marka-server
|
||||
|
||||
FROM scratch
|
||||
WORKDIR /app
|
||||
COPY --from=build /out/server /app/server
|
||||
USER 65532:65532
|
||||
EXPOSE 8080
|
||||
CMD ["/server","-root=/app/data","-addr=:8080"]
|
||||
|
41
server/cmd/marka-server/main.go
Normal file
41
server/cmd/marka-server/main.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
3
server/go.mod
Normal file
3
server/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module git.max-richter.dev/max/marka/server-new
|
||||
|
||||
go 1.24.7
|
22
server/go.sum
Normal file
22
server/go.sum
Normal 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
1
server/http/.env
Normal file
@@ -0,0 +1 @@
|
||||
SERVER_URL=http://localhost:8080
|
5
server/http/post-recipe.http
Normal file
5
server/http/post-recipe.http
Normal file
@@ -0,0 +1,5 @@
|
||||
POST {{SERVER_URL}}/Recipes/bolognaise.md
|
||||
|
||||
{
|
||||
|
||||
}
|
5
server/internal/adapters/errors.go
Normal file
5
server/internal/adapters/errors.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package adapters
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
97
server/internal/adapters/fs.go
Normal file
97
server/internal/adapters/fs.go
Normal file
@@ -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
|
||||
}
|
78
server/internal/adapters/fs_utils.go
Normal file
78
server/internal/adapters/fs_utils.go
Normal file
@@ -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"
|
||||
}
|
34
server/internal/adapters/interface.go
Normal file
34
server/internal/adapters/interface.go
Normal file
@@ -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"`
|
||||
}
|
20
server/internal/handler/error.go
Normal file
20
server/internal/handler/error.go
Normal file
@@ -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)
|
||||
}
|
104
server/internal/handler/handler.go
Normal file
104
server/internal/handler/handler.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user