feat: renderer
This commit is contained in:
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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=
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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
86
parser/parser_test.go
Normal 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)
|
||||
}
|
||||
}
|
16
parser/testdata/baguette.md
vendored
16
parser/testdata/baguette.md
vendored
@@ -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
|
22
parser/testdata/recipe_no_description/input.md
vendored
22
parser/testdata/recipe_no_description/input.md
vendored
@@ -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.
|
||||
|
@@ -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."
|
||||
]
|
||||
}
|
24
parser/testdata/recipe_salad/input.md
vendored
24
parser/testdata/recipe_salad/input.md
vendored
@@ -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.
|
||||
|
25
parser/testdata/recipe_salad/output.json
vendored
25
parser/testdata/recipe_salad/output.json
vendored
@@ -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."
|
||||
]
|
||||
}
|
@@ -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)
|
||||
}
|
Reference in New Issue
Block a user