big tings

This commit is contained in:
Max Richter
2025-08-17 15:16:17 +02:00
parent 40b9be887d
commit c687eff53d
958 changed files with 32279 additions and 704 deletions

176
template/blocks.go Normal file
View File

@@ -0,0 +1,176 @@
// Package template contains the logic for parsing template blocks.
package template
import (
"fmt"
"strings"
"go.yaml.in/yaml/v4"
)
// TemplateType represents whether a template is short, long, or invalid.
type TemplateType int
const (
InvalidTemplate TemplateType = iota
ShortTemplate
ExtendedTemplate
)
// DetectTemplateType checks if the template is short or long.
func DetectTemplateType(tmpl string) TemplateType {
trimmed := strings.TrimSpace(tmpl)
// Short type: starts with "{" and ends with "}" on a single line,
// and contains "|" or "," inside for inline definition
// Matchs for example { name | text,required }
if strings.HasPrefix(trimmed, "{") &&
strings.HasSuffix(trimmed, "}") &&
!strings.Contains(trimmed, "\n") {
return ShortTemplate
}
// Long type: multiline and contains keys like "path:" or "codec:" inside
// Matches for example:
// {
// path: name
// codec: text
// required: true
// }
if strings.Contains(trimmed, "\n") &&
(strings.Contains(trimmed, "path:") || strings.Contains(trimmed, "codec:")) {
return ExtendedTemplate
}
return InvalidTemplate
}
func cleanTemplate(input string) string {
s := strings.TrimSpace(input)
s = strings.TrimPrefix(s, "{")
s = strings.TrimSuffix(s, "}")
s = strings.Trim(s, "\n")
return s
}
func parseShortTemplate(input string) (Block, error) {
split := strings.Split(cleanTemplate(input), "|")
if len(split) < 1 {
return Block{}, fmt.Errorf("invalid short template")
}
block := Block{
Type: DataBlock,
Path: strings.TrimSpace(split[0]),
Codec: CodecText,
content: input,
}
if len(split) > 1 {
optionSplit := strings.SplitSeq(split[1], ",")
for option := range optionSplit {
switch strings.TrimSpace(option) {
case "required":
block.Required = true
case "number":
block.Codec = CodecNumber
}
}
}
return block, nil
}
type yamlBlock struct {
Path string `yaml:"path"`
Codec string `yaml:"codec"`
Required bool `yaml:"required,omitempty"`
Value any `yaml:"value,omitempty"`
Fields []yamlField `yaml:"fields"`
ListTemplate string `yaml:"listTemplate,omitempty"`
}
type yamlField struct {
Path string `yaml:"path"`
Value any `yaml:"value,omitempty"`
Codec string `yaml:"codec"`
Required bool `yaml:"required"`
}
func parseYamlTemplate(input string) (block Block, err error) {
var blk yamlBlock
cleaned := cleanTemplate(input)
dec := yaml.NewDecoder(strings.NewReader(cleaned))
dec.KnownFields(true)
if err := dec.Decode(&blk); err != nil {
return block, fmt.Errorf("content '%q': %w", cleaned, err)
}
if blk.Path == "" {
return block, fmt.Errorf("missing top-level 'path'")
}
if blk.Codec == "" {
blk.Codec = "text"
}
codec, err := parseCodecType(blk.Codec)
if err != nil {
return block, fmt.Errorf("failed to parse codec: %w", err)
}
var fields []BlockField
for _, field := range blk.Fields {
if field.Path == "" {
return block, fmt.Errorf("failed to parse field: %v", field)
}
if field.Codec == "" {
field.Codec = "text"
}
fieldCodec, err := parseCodecType(field.Codec)
if err != nil {
return block, fmt.Errorf("failed to parse codec: %w", err)
}
fields = append(fields, BlockField{
Path: field.Path,
CodecType: fieldCodec,
Required: field.Required,
Value: field.Value,
})
}
return Block{
Type: DataBlock,
Path: blk.Path,
Codec: codec,
Fields: fields,
ListTemplate: blk.ListTemplate,
content: input,
}, nil
}
func ParseTemplateBlock(template string, blockType BlockType) (block Block, err error) {
if blockType == MatchingBlock {
return Block{
Type: MatchingBlock,
content: template,
}, nil
}
switch DetectTemplateType(template) {
case ShortTemplate:
return parseShortTemplate(template)
case ExtendedTemplate:
return parseYamlTemplate(template)
}
return block, fmt.Errorf("invalid template")
}

30
template/codecs.go Normal file
View File

@@ -0,0 +1,30 @@
package template
import "fmt"
// CodecType represents the type of codec used to encode/render a value
type CodecType string
const (
CodecText CodecType = "text"
CodecNumber CodecType = "number"
CodecYaml CodecType = "yaml"
CodecList CodecType = "list"
CodecConst CodecType = "const"
)
func parseCodecType(input string) (CodecType, error) {
switch input {
case "number":
return CodecNumber, nil
case "yaml":
return CodecYaml, nil
case "list":
return CodecList, nil
case "text":
return CodecText, nil
case "const":
return CodecConst, nil
}
return CodecText, fmt.Errorf("unknown codec: '%s'", input)
}

75
template/compile.go Normal file
View File

@@ -0,0 +1,75 @@
package template
import (
"fmt"
)
// CompileTemplate scans once, emitting:
// - data blocks: inner content between a line that's exactly "{" and a line that's exactly "}"
// - matching blocks: gaps between data blocks (excluding the brace lines themselves)
func CompileTemplate(template string) ([]Block, error) {
var out []Block
var curlyIndex int
const OPENING = '{'
const CLOSING = '}'
var start int
var blockType BlockType
if len(template) > 0 && template[0] == OPENING {
blockType = DataBlock
} else {
blockType = MatchingBlock
}
for i, r := range template {
nextCurlyIndex := curlyIndex
switch r {
case OPENING:
nextCurlyIndex++
case CLOSING:
nextCurlyIndex--
}
if curlyIndex == 0 && nextCurlyIndex == 1 {
if i > start {
block, err := ParseTemplateBlock(template[start:i], blockType)
if err != nil {
return nil, fmt.Errorf("cannot parse block: %w", err)
}
out = append(out, block)
}
start = i
blockType = DataBlock
} else if curlyIndex == 1 && nextCurlyIndex == 0 {
if i > start {
block, err := ParseTemplateBlock(template[start:i+1], blockType)
if err != nil {
return nil, fmt.Errorf("cannot parse block: %w", err)
}
out = append(out, block)
}
nextChar := ' '
if i+1 < len(template) {
nextChar = rune(template[i+1])
}
if nextChar == OPENING {
start = i + 1
blockType = DataBlock
} else {
start = i + 1
blockType = MatchingBlock
}
}
curlyIndex = nextCurlyIndex
}
return out, nil
}

100
template/compile_test.go Normal file
View File

@@ -0,0 +1,100 @@
package template_test
import (
"testing"
"git.max-richter.dev/max/marka/registry"
"git.max-richter.dev/max/marka/template"
)
func TestExtractBlocks(t *testing.T) {
src, err := registry.GetTemplate("Recipe")
if err != nil {
t.Errorf("Failed to extract blocks: %s", err.Error())
t.FailNow()
}
templateBlocks, err := template.CompileTemplate(src)
if err != nil {
t.Errorf("Failed to extract blocks: %s", err.Error())
t.FailNow()
}
expected := []template.Block{
{
Type: template.MatchingBlock,
},
{
Type: template.DataBlock,
Codec: "yaml",
Path: ".",
Fields: []template.BlockField{
{
Path: "@type",
},
{
Path: "image",
},
{
Path: "author.@type",
},
{
Path: "author.name",
},
{
Path: "datePublished",
},
{
Path: "prepTime",
},
{
Path: "cookTime",
},
{
Path: "recipeYield",
},
},
},
{Type: template.MatchingBlock},
{Type: template.DataBlock, Path: "name", Codec: "text"},
{Type: template.MatchingBlock},
{Type: template.DataBlock, Path: "description", Codec: "text"},
{Type: template.MatchingBlock},
{Type: template.DataBlock, Path: "recipeIngredient", Codec: "list", ListTemplate: "- { . }"},
{Type: template.MatchingBlock},
{Type: template.DataBlock, Path: "recipeInstructions", Codec: "list", ListTemplate: "{ @index }. { . }"},
}
if len(templateBlocks) != len(expected) {
t.Fatalf("expected %d blocks, got %d", len(expected), len(templateBlocks))
}
for i, b := range templateBlocks {
exp := expected[i]
if b.Type != exp.Type {
t.Errorf("Block#%d Type '%s' did not match expected type '%s'", i, b.Type, exp.Type)
}
if b.Path != exp.Path {
t.Errorf("Block#%d Path '%s' did not match expected path '%s'", i, b.Path, exp.Path)
}
if b.Codec != exp.Codec {
t.Errorf("Block#%d Codec '%s' did not match expected codec '%s'", i, b.Codec, exp.Codec)
}
}
}
func TestExtractInlineTemplate(t *testing.T) {
testCases := []string{
"- { . }",
"- { amount } { type }",
}
for _, tc := range testCases {
_, err := template.CompileTemplate(tc)
if err != nil {
t.Errorf("Failed to extract blocks: %s", err.Error())
t.FailNow()
}
}
}

5
template/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module git.max-richter.dev/max/marka/template
go 1.24.3
require go.yaml.in/yaml/v4 v4.0.0-rc.1

2
template/go.sum Normal file
View File

@@ -0,0 +1,2 @@
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=

30
template/structs.go Normal file
View File

@@ -0,0 +1,30 @@
package template
type BlockType string
const (
DataBlock BlockType = "data" // content between lines "{" and "}"
MatchingBlock BlockType = "matching" // everything outside data blocks
)
type BlockField struct {
Path string
CodecType CodecType
Required bool
Value any
}
type Block struct {
Type BlockType
Path string
Codec CodecType
Required bool
ListTemplate string
Fields []BlockField
Value any
content string
}
func (b Block) GetContent() string {
return b.content
}