diff --git a/parser/parser.go b/parser/parser.go index d1c8e09..0834599 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -16,19 +16,19 @@ 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 { @@ -45,7 +45,7 @@ func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, erro 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) } return matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3), nil @@ -59,21 +59,21 @@ func ParseFile(markdownContent string) (any, error) { 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) } timings["detect_type"] = time.Since(startDetectType).Milliseconds() startGetTemplate := time.Now() 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 schema -> %w", err) } timings["get_template"] = time.Since(startGetTemplate).Milliseconds() startTemplate := time.Now() 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) } timings["template_compilation"] = time.Since(startTemplate).Milliseconds() @@ -82,7 +82,7 @@ func ParseFile(markdownContent string) (any, error) { result, err := decoders.Parse(blocks) if err != nil { - return nil, fmt.Errorf("failed to parse blocks: %w", err) + return nil, fmt.Errorf("failed to parse blocks -> %w", err) } timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds() @@ -102,7 +102,7 @@ func ParseFileWithTemplate(markdownContent string, templateContent string) (any, 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) } timings["template_compilation"] = time.Since(startTemplate).Milliseconds() @@ -111,7 +111,7 @@ func ParseFileWithTemplate(markdownContent string, templateContent string) (any, result, err := decoders.Parse(blocks) if err != nil { - return nil, fmt.Errorf("failed to parse blocks: %w", err) + return nil, fmt.Errorf("failed to parse blocks -> %w", err) } timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds() diff --git a/playground/src/lib/components/EditorPanel.svelte b/playground/src/lib/components/EditorPanel.svelte index d692853..7a89082 100644 --- a/playground/src/lib/components/EditorPanel.svelte +++ b/playground/src/lib/components/EditorPanel.svelte @@ -15,6 +15,7 @@ children?: Snippet; headerActions?: Snippet; subtitle?: string; + error?: string; status?: 'success' | 'error' | 'indeterminate'; timing?: number; pillText?: string; @@ -23,6 +24,7 @@ let { title, + error, value = $bindable(), placeholder = '', readonly = false, @@ -69,21 +71,20 @@
{timing}ms
{/if} -
-
- -
-
-
+ {#if error} +
+
{error}
+
+ {/if} + + {#if children} {@render children()} diff --git a/playground/src/lib/components/Playground.svelte b/playground/src/lib/components/Playground.svelte index d88aeed..2e9d09a 100644 --- a/playground/src/lib/components/Playground.svelte +++ b/playground/src/lib/components/Playground.svelte @@ -4,7 +4,6 @@ import { getTemplate, listTemplates, - matchBlocks, parseMarkdown, parseMarkdownWithTemplate, wasmReady, @@ -57,6 +56,7 @@ My favourite baguette recipe let timings = $state(null); let templateStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined); let dataStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined); + let templateError = $state(); $effect(() => { if (typeof window !== 'undefined') { @@ -91,11 +91,13 @@ My favourite baguette recipe : parseMarkdown(markdownValue); if ('error' in result) { - jsonOutput = result.error; + 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'; } @@ -138,6 +140,7 @@ My favourite baguette recipe title="Template" bind:value={templateValue} placeholder="Enter your Marka template here..." + error={templateError} status={templateStatus} timing={timings?.template_compilation} subtitle="Define your mapping schema" diff --git a/playground/static/logo.svg b/playground/static/logo.svg index 19a6963..e4f85db 100644 --- a/playground/static/logo.svg +++ b/playground/static/logo.svg @@ -1,15 +1,15 @@ - - - + + + - - - - - - - - + + + + + + + + diff --git a/playground/static/main.wasm b/playground/static/main.wasm index 58c60bd..143cb50 100644 Binary files a/playground/static/main.wasm and b/playground/static/main.wasm differ diff --git a/template/blocks.go b/template/blocks.go index 37273bf..16a9fc1 100644 --- a/template/blocks.go +++ b/template/blocks.go @@ -1,8 +1,7 @@ -// Package blocks contains the logic for parsing template blocks. +// Package template contains the logic for parsing templates. package template import ( - "fmt" "strings" ) @@ -16,8 +15,8 @@ const ( ) // DetectTemplateType checks if the template is short or long. -func DetectTemplateType(tmpl string) TemplateType { - trimmed := strings.TrimSpace(tmpl) +func DetectTemplateType(tmpl Slice) TemplateType { + trimmed := strings.TrimSpace(tmpl.String()) // Short type: starts with "{" and ends with "}" on a single line, // and contains "|" or "," inside for inline definition @@ -43,15 +42,15 @@ func DetectTemplateType(tmpl string) TemplateType { return InvalidTemplate } -func cleanTemplate(input string) string { - s := strings.TrimSpace(input) +func cleanTemplate(input Slice) string { + s := strings.TrimSpace(input.String()) s = strings.TrimPrefix(s, "{") s = strings.TrimSuffix(s, "}") s = strings.Trim(s, "\n") return s } -func ParseTemplateBlock(template string, blockType BlockType) (block Block, err error) { +func ParseTemplateBlock(template Slice, blockType BlockType) (block Block, err error) { if blockType == MatchingBlock { return Block{ Type: MatchingBlock, @@ -66,5 +65,5 @@ func ParseTemplateBlock(template string, blockType BlockType) (block Block, err return parseYamlTemplate(template) } - return block, fmt.Errorf("invalid template") + return block, NewErrorf("invalid template").WithPosition(template.start, template.end) } diff --git a/template/blocks_short.go b/template/blocks_short.go index 692a235..972552b 100644 --- a/template/blocks_short.go +++ b/template/blocks_short.go @@ -5,7 +5,7 @@ import ( "strings" ) -func parseShortTemplate(input string) (Block, error) { +func parseShortTemplate(input Slice) (Block, error) { split := strings.Split(cleanTemplate(input), "|") if len(split) < 1 { return Block{}, fmt.Errorf("invalid short template") diff --git a/template/blocks_yaml.go b/template/blocks_yaml.go index 226a0ef..4f58f3c 100644 --- a/template/blocks_yaml.go +++ b/template/blocks_yaml.go @@ -25,7 +25,7 @@ type yamlField struct { PathAlias []string `yaml:"pathAlias,omitempty"` } -func parseYamlTemplate(input string) (block Block, err error) { +func parseYamlTemplate(input Slice) (block Block, err error) { var blk yamlBlock cleaned := cleanTemplate(input) @@ -34,7 +34,7 @@ func parseYamlTemplate(input string) (block Block, err error) { dec.KnownFields(true) if err := dec.Decode(&blk); err != nil { - return block, fmt.Errorf("content '%q': %w", cleaned, err) + return block, NewErrorf("content '%q' -> %w", cleaned, err).WithPosition(input.start, input.end) } if blk.Path == "" { @@ -47,7 +47,7 @@ func parseYamlTemplate(input string) (block Block, err error) { codec, err := parseCodecType(blk.Codec) if err != nil { - return block, fmt.Errorf("failed to parse codec: %w", err) + return block, fmt.Errorf("failed to parse codec -> %w", err) } var fields []BlockField @@ -63,7 +63,7 @@ func parseYamlTemplate(input string) (block Block, err error) { fieldCodec, err := parseCodecType(field.Codec) if err != nil { - return block, fmt.Errorf("failed to parse codec: %w", err) + return block, fmt.Errorf("failed to parse codec -> %w", err) } fields = append(fields, BlockField{ diff --git a/template/compile.go b/template/compile.go index ff22bb4..22c3d99 100644 --- a/template/compile.go +++ b/template/compile.go @@ -1,13 +1,9 @@ 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) { +func CompileTemplate(templateSource string) ([]Block, error) { var out []Block var curlyIndex int @@ -16,14 +12,15 @@ func CompileTemplate(template string) ([]Block, error) { var start int var blockType BlockType + template := NewSlice(templateSource) - if len(template) > 0 && template[0] == OPENING { + if template.Len() > 0 && template.At(0) == OPENING { blockType = DataBlock } else { blockType = MatchingBlock } - for i, r := range template { + for i, r := range template.Chars() { nextCurlyIndex := curlyIndex @@ -36,27 +33,26 @@ func CompileTemplate(template string) ([]Block, error) { if curlyIndex == 0 && nextCurlyIndex == 1 { if i > start { - block, err := ParseTemplateBlock(template[start:i], blockType) + block, err := ParseTemplateBlock(template.Slice(start, i), blockType) if err != nil { - return nil, fmt.Errorf("cannot parse block: %w", err) + return nil, NewErrorf("cannot parse block @pos -> %w", err).WithPosition(start, i) } 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) + block, err := ParseTemplateBlock(template.Slice(start, i), blockType) if err != nil { - return nil, fmt.Errorf("cannot parse block: %w", err) + return nil, NewErrorf("cannot parse block @pos -> %w", err).WithPosition(start, i) } out = append(out, block) } nextChar := ' ' - if i+1 < len(template) { - nextChar = rune(template[i+1]) + if i+1 < template.Len() { + nextChar = rune(template.At(i + 1)) } if nextChar == OPENING { diff --git a/template/error.go b/template/error.go new file mode 100644 index 0000000..df45db8 --- /dev/null +++ b/template/error.go @@ -0,0 +1,36 @@ +package template + +import ( + "fmt" + "strings" +) + +type Error struct { + err error + start, end int +} + +func (e Error) Error() string { + content := e.err.Error() + + if strings.Contains(content, " @pos ") { + if e.start == e.end { + return strings.ReplaceAll(content, " @pos ", " ") + } + return strings.ReplaceAll(content, " @pos ", fmt.Sprintf(" position=%d:%d ", e.start, e.end)) + } + + return content +} + +func NewErrorf(msg string, args ...any) Error { + return Error{ + err: fmt.Errorf(msg, args...), + } +} + +func (e Error) WithPosition(start, end int) Error { + e.start = start + e.end = end + return e +} diff --git a/template/slice.go b/template/slice.go new file mode 100644 index 0000000..d1908e4 --- /dev/null +++ b/template/slice.go @@ -0,0 +1,46 @@ +package template + +func NewSlice(s string) Slice { + return Slice{ + source: &s, + start: 0, + end: len(s), + } +} + +type Slice struct { + source *string + start, end int +} + +func (s Slice) Chars() []byte { + return []byte((*s.source)[s.start:s.end]) +} + +func (s Slice) Len() int { + return s.end - s.start +} + +func (s Slice) At(i int) byte { + return (*s.source)[s.start+i] +} + +func SliceFromString(s string, start, end int) Slice { + return Slice{ + source: &s, + start: start, + end: end, + } +} + +func (s Slice) String() string { + return (*s.source)[s.start:s.end] +} + +func (s Slice) Slice(start, end int) Slice { + return Slice{ + source: s.source, + start: s.start + start, + end: s.start + end, + } +} diff --git a/template/structs.go b/template/structs.go index f48de20..ff8d6be 100644 --- a/template/structs.go +++ b/template/structs.go @@ -22,9 +22,9 @@ type Block struct { Fields []BlockField Optional bool Value any - content string + content Slice } func (b Block) GetContent() string { - return b.content + return b.content.String() }