feat: renderer

This commit is contained in:
2025-08-19 17:20:24 +02:00
parent 6db87db325
commit 210b31aef8
38 changed files with 727 additions and 299 deletions

View File

@@ -7,13 +7,13 @@ import (
"git.max-richter.dev/max/marka/parser/decoders"
"git.max-richter.dev/max/marka/parser/matcher"
"git.max-richter.dev/max/marka/parser/utils"
"git.max-richter.dev/max/marka/registry"
"git.max-richter.dev/max/marka/template"
"git.max-richter.dev/max/marka/testdata"
)
func TestParseBaguette(t *testing.T) {
recipeMd := utils.ReadTestDataFile(t, "baguette.md")
recipeMd := testdata.Read(t, "baguette/input.md")
templateContent, err := registry.GetTemplate("Recipe")
if err != nil {
@@ -25,7 +25,7 @@ func TestParseBaguette(t *testing.T) {
t.Fatalf("Err: %s", err)
}
matches := matcher.MatchBlocksFuzzy(recipeMd, blocks, 0.3)
matches := matcher.MatchBlocksFuzzy(string(recipeMd), blocks, 0.3)
parsed, err := decoders.Parse(matches)
if err != nil {
t.Fatalf("Err: %s", err)

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"git.max-richter.dev/max/marka/parser/utils"
renderUtils "git.max-richter.dev/max/marka/renderer/utils"
"git.max-richter.dev/max/marka/template"
"go.yaml.in/yaml/v4"
)
@@ -17,12 +18,15 @@ func Yaml(input string, block template.Block) (value any, error error) {
var out any
for _, f := range block.Fields {
if f.Hidden {
continue
}
if f.CodecType == template.CodecConst {
if f.Value != nil {
out = utils.SetPathValue(f.Path, f.Value, out)
}
} else {
if value, ok := res[f.Path]; ok {
if value, ok := renderUtils.GetValueFromPath(res, f.Path); ok {
out = utils.SetPathValue(f.Path, value, out)
}
}

View File

@@ -2,9 +2,18 @@ module git.max-richter.dev/max/marka/parser
go 1.24.3
require github.com/agext/levenshtein v1.2.3
require (
git.max-richter.dev/max/marka/registry v0.0.0-20250817132016-6db87db32567
git.max-richter.dev/max/marka/template v0.0.0-20250817132016-6db87db32567
github.com/agext/levenshtein v1.2.3
)
require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
golang.org/x/text v0.14.0 // indirect
)
require (
github.com/google/go-cmp v0.7.0
go.yaml.in/yaml/v4 v4.0.0-rc.1 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.1
)

View File

@@ -1,6 +1,16 @@
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/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=
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=

View File

@@ -1,69 +0,0 @@
package parser_test
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"git.max-richter.dev/max/marka/parser"
"github.com/google/go-cmp/cmp"
)
func TestParseRecipe_Golden(t *testing.T) {
td := filepath.Join("testdata", "recipe_salad")
input := filepath.Join(td, "input.md")
output := filepath.Join(td, "output.json")
inputContent, err := os.ReadFile(input)
if err != nil {
t.Fatalf("read input.md: %v", err)
}
got, err := parser.ParseFile(string(inputContent))
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
var want map[string]any
b, err := os.ReadFile(output)
if err != nil {
t.Fatalf("read expected.json: %v", err)
}
if err := json.Unmarshal(b, &want); err != nil {
t.Fatalf("unmarshal expected.json: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
}
}
func TestParseRecipe_NoDescription(t *testing.T) {
td := filepath.Join("testdata", "recipe_no_description")
input := filepath.Join(td, "input.md")
output := filepath.Join(td, "output.json")
inputContent, err := os.ReadFile(input)
if err != nil {
t.Fatalf("read input.md: %v", err)
}
got, err := parser.ParseFile(string(inputContent))
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
var want map[string]any
b, err := os.ReadFile(output)
if err != nil {
t.Fatalf("read expected.json: %v", err)
}
if err := json.Unmarshal(b, &want); err != nil {
t.Fatalf("unmarshal expected.json: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
}
}

View File

@@ -5,13 +5,13 @@ import (
"testing"
"git.max-richter.dev/max/marka/parser/matcher"
"git.max-richter.dev/max/marka/parser/utils"
"git.max-richter.dev/max/marka/registry"
"git.max-richter.dev/max/marka/template"
"git.max-richter.dev/max/marka/testdata"
)
func TestFuzzyFindAll(t *testing.T) {
recipeMd := utils.ReadTestDataFile(t, "baguette.md")
recipeMd := testdata.Read(t, "baguette/input.md")
tests := []struct {
Needle string
@@ -28,7 +28,7 @@ func TestFuzzyFindAll(t *testing.T) {
}
for _, test := range tests {
start, end := matcher.FuzzyFind(recipeMd, test.StartIndex, test.Needle, 0.3) // allow 50% error
start, end := matcher.FuzzyFind(string(recipeMd), test.StartIndex, test.Needle, 0.3) // allow 50% error
if start != test.Start || end != test.End {
t.Errorf("Start or end do not match: Needle=%q Start=%d/%d End=%d/%d", test.Needle, test.Start, start, test.End, end)
@@ -37,7 +37,7 @@ func TestFuzzyFindAll(t *testing.T) {
}
func TestFuzzyBlockMatch(t *testing.T) {
recipeMd := utils.ReadTestDataFile(t, "baguette.md")
recipeMd := testdata.Read(t, "baguette/input.md")
schemaMd, err := registry.GetTemplate("Recipe")
if err != nil {
t.Errorf("Failed to load template: %s", err.Error())
@@ -53,7 +53,7 @@ func TestFuzzyBlockMatch(t *testing.T) {
fmt.Printf("block: %#v\n", b)
}
matches := matcher.MatchBlocksFuzzy(recipeMd, blocks, 0.3)
matches := matcher.MatchBlocksFuzzy(string(recipeMd), blocks, 0.3)
expected := []struct {
value string

View File

@@ -41,11 +41,15 @@ func DetectType(markdownContent string) (string, error) {
}
}
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 ParseFile(markdownContent string) (any, error) {
markdownContent = strings.TrimSuffix(
strings.ReplaceAll(markdownContent, "@type:", `"@type":`),
"\n",
)
markdownContent = prepareMarkdown(markdownContent)
contentType, err := DetectType(markdownContent)
if err != nil {

86
parser/parser_test.go Normal file
View File

@@ -0,0 +1,86 @@
package parser_test
import (
"encoding/json"
"testing"
"git.max-richter.dev/max/marka/parser"
"git.max-richter.dev/max/marka/testdata"
"github.com/google/go-cmp/cmp"
)
func TestParseRecipe_Golden(t *testing.T) {
inputContent := testdata.Read(t, "recipe_salad/input.md")
output := testdata.Read(t, "recipe_salad/output.json")
got, err := parser.ParseFile(string(inputContent))
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
var want map[string]any
if err := json.Unmarshal(output, &want); err != nil {
t.Fatalf("unmarshal expected.json: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
}
}
func TestParseRecipe_NoDescription(t *testing.T) {
inputContent := testdata.Read(t, "recipe_no_description/input.md")
got, err := parser.ParseFile(string(inputContent))
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
var want map[string]any
output := testdata.Read(t, "recipe_no_description/output.json")
if err := json.Unmarshal(output, &want); err != nil {
t.Fatalf("unmarshal expected.json: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
}
}
func TestParseRecipe_Baguette(t *testing.T) {
inputContent := testdata.Read(t, "baguette/input.md")
got, err := parser.ParseFile(string(inputContent))
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
var want map[string]any
output := testdata.Read(t, "baguette/output.json")
if err := json.Unmarshal(output, &want); err != nil {
t.Fatalf("unmarshal expected.json: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
}
}
func TestParseArticle_Simple(t *testing.T) {
inputContent := testdata.Read(t, "article_simple/input.md")
got, err := parser.ParseFile(string(inputContent))
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
var want map[string]any
output := testdata.Read(t, "article_simple/output.json")
if err := json.Unmarshal(output, &want); err != nil {
t.Fatalf("unmarshal expected.json: %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
}
}

View File

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

View File

@@ -1,22 +0,0 @@
---
@type: Recipe
image: https://example.com/salad.jpg
author.name: Alex Chef
prepTime: PT10M
cookTime: PT0M
recipeYield: 2 servings
---
# Simple Salad
## Ingredients
- 100 g lettuce
- 5 cherry tomatoes
- 1 tbsp olive oil
- Pinch of salt
## Steps
1. Wash and dry the lettuce.
2. Halve the cherry tomatoes.
3. Toss with olive oil and salt.

View File

@@ -1,24 +0,0 @@
{
"@context": "https://schema.org",
"@type": "Recipe",
"name": "Simple Salad",
"image": "https://example.com/salad.jpg",
"author": {
"@type": "Person",
"name": "Alex Chef"
},
"prepTime": "PT10M",
"cookTime": "PT0M",
"recipeYield": "2 servings",
"recipeIngredient": [
"100 g lettuce",
"5 cherry tomatoes",
"1 tbsp olive oil",
"Pinch of salt"
],
"recipeInstructions": [
"Wash and dry the lettuce.",
"Halve the cherry tomatoes.",
"Toss with olive oil and salt."
]
}

View File

@@ -1,24 +0,0 @@
---
@type: Recipe
image: https://example.com/salad.jpg
author.name: Alex Chef
prepTime: PT10M
cookTime: PT0M
recipeYield: 2 servings
---
# Simple Salad
A quick green salad.
## Ingredients
- 100 g lettuce
- 5 cherry tomatoes
- 1 tbsp olive oil
- Pinch of salt
## Steps
1. Wash and dry the lettuce.
2. Halve the cherry tomatoes.
3. Toss with olive oil and salt.

View File

@@ -1,25 +0,0 @@
{
"@context": "https://schema.org",
"@type": "Recipe",
"name": "Simple Salad",
"image": "https://example.com/salad.jpg",
"author": {
"@type": "Person",
"name": "Alex Chef"
},
"description": "A quick green salad.",
"prepTime": "PT10M",
"cookTime": "PT0M",
"recipeYield": "2 servings",
"recipeIngredient": [
"100 g lettuce",
"5 cherry tomatoes",
"1 tbsp olive oil",
"Pinch of salt"
],
"recipeInstructions": [
"Wash and dry the lettuce.",
"Halve the cherry tomatoes.",
"Toss with olive oil and salt."
]
}

View File

@@ -1,16 +0,0 @@
package utils
import (
"os"
"path/filepath"
"testing"
)
func ReadTestDataFile(t *testing.T, fileName string) string {
path := filepath.Join("../testdata", fileName)
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read test data file: %v", err)
}
return string(data)
}