feat: some experiments

This commit is contained in:
Max Richter
2025-09-26 17:32:16 +02:00
parent ea978b48f6
commit 8ca5152c3c
12 changed files with 147 additions and 66 deletions

View File

@@ -16,19 +16,19 @@ import (
func DetectType(markdownContent string) (string, error) { func DetectType(markdownContent string) (string, error) {
defaultSchemaContent, err := registry.GetTemplate("_default") defaultSchemaContent, err := registry.GetTemplate("_default")
if err != nil { 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) defaultSchema, err := template.CompileTemplate(defaultSchemaContent)
if err != nil { 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) blocks := matcher.MatchBlocksFuzzy(markdownContent, defaultSchema, 0.3)
result, err := decoders.Parse(blocks) result, err := decoders.Parse(blocks)
if err != nil { 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 result, ok := result.(map[string]any); ok {
@@ -45,7 +45,7 @@ func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, erro
tpl, err := template.CompileTemplate(templateContent) tpl, err := template.CompileTemplate(templateContent)
if err != nil { 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 return matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3), nil
@@ -59,21 +59,21 @@ func ParseFile(markdownContent string) (any, error) {
contentType, err := DetectType(markdownContent) contentType, err := DetectType(markdownContent)
if err != nil { 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() timings["detect_type"] = time.Since(startDetectType).Milliseconds()
startGetTemplate := time.Now() startGetTemplate := time.Now()
templateContent, err := registry.GetTemplate(contentType) templateContent, err := registry.GetTemplate(contentType)
if err != nil { 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() timings["get_template"] = time.Since(startGetTemplate).Milliseconds()
startTemplate := time.Now() startTemplate := time.Now()
tpl, err := template.CompileTemplate(templateContent) tpl, err := template.CompileTemplate(templateContent)
if err != nil { 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() timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
@@ -82,7 +82,7 @@ func ParseFile(markdownContent string) (any, error) {
result, err := decoders.Parse(blocks) result, err := decoders.Parse(blocks)
if err != nil { 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() timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
@@ -102,7 +102,7 @@ func ParseFileWithTemplate(markdownContent string, templateContent string) (any,
tpl, err := template.CompileTemplate(templateContent) tpl, err := template.CompileTemplate(templateContent)
if err != nil { 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() timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
@@ -111,7 +111,7 @@ func ParseFileWithTemplate(markdownContent string, templateContent string) (any,
result, err := decoders.Parse(blocks) result, err := decoders.Parse(blocks)
if err != nil { 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() timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()

View File

@@ -15,6 +15,7 @@
children?: Snippet; children?: Snippet;
headerActions?: Snippet; headerActions?: Snippet;
subtitle?: string; subtitle?: string;
error?: string;
status?: 'success' | 'error' | 'indeterminate'; status?: 'success' | 'error' | 'indeterminate';
timing?: number; timing?: number;
pillText?: string; pillText?: string;
@@ -23,6 +24,7 @@
let { let {
title, title,
error,
value = $bindable(), value = $bindable(),
placeholder = '', placeholder = '',
readonly = false, readonly = false,
@@ -69,8 +71,13 @@
<div class="ml-4 text-xs text-gray-500">{timing}ms</div> <div class="ml-4 text-xs text-gray-500">{timing}ms</div>
{/if} {/if}
</div> </div>
<div class="group relative flex flex-1 flex-col overflow-hidden">
<div class="flex-1 overflow-auto"> {#if error}
<div class="border-b border-gray-200 bg-red-300 p-4 text-xs">
<pre>{error}</pre>
</div>
{/if}
<CodeMirror <CodeMirror
bind:value bind:value
extensions={[basicSetup, langExtension].filter(Boolean) as Extension[]} extensions={[basicSetup, langExtension].filter(Boolean) as Extension[]}
@@ -78,12 +85,6 @@
{readonly} {readonly}
class="text-sm" class="text-sm"
/> />
</div>
<div
class="pointer-events-none absolute inset-0 bg-black opacity-0 transition-opacity duration-200 group-hover:opacity-5"
></div>
</div>
{#if children} {#if children}
{@render children()} {@render children()}

View File

@@ -4,7 +4,6 @@
import { import {
getTemplate, getTemplate,
listTemplates, listTemplates,
matchBlocks,
parseMarkdown, parseMarkdown,
parseMarkdownWithTemplate, parseMarkdownWithTemplate,
wasmReady, wasmReady,
@@ -57,6 +56,7 @@ My favourite baguette recipe
let timings = $state<ParseResultSuccess['timings'] | null>(null); let timings = $state<ParseResultSuccess['timings'] | null>(null);
let templateStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined); let templateStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
let dataStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined); let dataStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
let templateError = $state<string | undefined>();
$effect(() => { $effect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -91,11 +91,13 @@ My favourite baguette recipe
: parseMarkdown(markdownValue); : parseMarkdown(markdownValue);
if ('error' in result) { if ('error' in result) {
jsonOutput = result.error; jsonOutput = '';
if (result.error.startsWith('failed to compile template')) { if (result.error.startsWith('failed to compile template')) {
templateError = result.error.replaceAll(' -> ', '\n ⟶ ');
templateStatus = 'error'; templateStatus = 'error';
dataStatus = 'indeterminate'; dataStatus = 'indeterminate';
} else { } else {
templateError = undefined;
templateStatus = undefined; templateStatus = undefined;
dataStatus = 'error'; dataStatus = 'error';
} }
@@ -138,6 +140,7 @@ My favourite baguette recipe
title="Template" title="Template"
bind:value={templateValue} bind:value={templateValue}
placeholder="Enter your Marka template here..." placeholder="Enter your Marka template here..."
error={templateError}
status={templateStatus} status={templateStatus}
timing={timings?.template_compilation} timing={timings?.template_compilation}
subtitle="Define your mapping schema" subtitle="Define your mapping schema"

View File

@@ -1,15 +1,15 @@
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <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 19.9406 13.3665 43.3168 13.3665C52.852 13.3665 71.2871 17.0793 79.703 30.1981C79.703 30.1981 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 15.6639 65.9462 19.6134 71.7442C27.299 83.0278 41.2416 90.5942 56.6832 90.5942C80.0594 90.5942 100 71.3259 100 50.0001C100 77.6145 77.6144 100 50 100C22.3856 100 0 77.6145 0 50.0001Z" fill="url(#paint0_linear_12_27)"/> <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_linear_12_27)"/> <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> <defs>
<linearGradient id="paint0_linear_12_27" x1="75.1033" y1="92.9796" x2="22.9842" y2="7.71589" gradientUnits="userSpaceOnUse"> <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="white"/> <stop stop-color="#8E8E8E"/>
<stop offset="1" stop-color="#545454"/> <stop offset="1" stop-color="#2C2C2C"/>
</linearGradient> </radialGradient>
<linearGradient id="paint1_linear_12_27" x1="23.9405" y1="7.17252" x2="75.3423" y2="92.0288" gradientUnits="userSpaceOnUse"> <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="white"/> <stop stop-color="#8E8E8E"/>
<stop offset="1" stop-color="#545454"/> <stop offset="1" stop-color="#2C2C2C"/>
</linearGradient> </radialGradient>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

View File

@@ -1,8 +1,7 @@
// Package blocks contains the logic for parsing template blocks. // Package template contains the logic for parsing templates.
package template package template
import ( import (
"fmt"
"strings" "strings"
) )
@@ -16,8 +15,8 @@ const (
) )
// DetectTemplateType checks if the template is short or long. // DetectTemplateType checks if the template is short or long.
func DetectTemplateType(tmpl string) TemplateType { func DetectTemplateType(tmpl Slice) TemplateType {
trimmed := strings.TrimSpace(tmpl) trimmed := strings.TrimSpace(tmpl.String())
// Short type: starts with "{" and ends with "}" on a single line, // Short type: starts with "{" and ends with "}" on a single line,
// and contains "|" or "," inside for inline definition // and contains "|" or "," inside for inline definition
@@ -43,15 +42,15 @@ func DetectTemplateType(tmpl string) TemplateType {
return InvalidTemplate return InvalidTemplate
} }
func cleanTemplate(input string) string { func cleanTemplate(input Slice) string {
s := strings.TrimSpace(input) s := strings.TrimSpace(input.String())
s = strings.TrimPrefix(s, "{") s = strings.TrimPrefix(s, "{")
s = strings.TrimSuffix(s, "}") s = strings.TrimSuffix(s, "}")
s = strings.Trim(s, "\n") s = strings.Trim(s, "\n")
return s 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 { if blockType == MatchingBlock {
return Block{ return Block{
Type: MatchingBlock, Type: MatchingBlock,
@@ -66,5 +65,5 @@ func ParseTemplateBlock(template string, blockType BlockType) (block Block, err
return parseYamlTemplate(template) return parseYamlTemplate(template)
} }
return block, fmt.Errorf("invalid template") return block, NewErrorf("invalid template").WithPosition(template.start, template.end)
} }

View File

@@ -5,7 +5,7 @@ import (
"strings" "strings"
) )
func parseShortTemplate(input string) (Block, error) { func parseShortTemplate(input Slice) (Block, error) {
split := strings.Split(cleanTemplate(input), "|") split := strings.Split(cleanTemplate(input), "|")
if len(split) < 1 { if len(split) < 1 {
return Block{}, fmt.Errorf("invalid short template") return Block{}, fmt.Errorf("invalid short template")

View File

@@ -25,7 +25,7 @@ type yamlField struct {
PathAlias []string `yaml:"pathAlias,omitempty"` PathAlias []string `yaml:"pathAlias,omitempty"`
} }
func parseYamlTemplate(input string) (block Block, err error) { func parseYamlTemplate(input Slice) (block Block, err error) {
var blk yamlBlock var blk yamlBlock
cleaned := cleanTemplate(input) cleaned := cleanTemplate(input)
@@ -34,7 +34,7 @@ func parseYamlTemplate(input string) (block Block, err error) {
dec.KnownFields(true) dec.KnownFields(true)
if err := dec.Decode(&blk); err != nil { 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 == "" { if blk.Path == "" {
@@ -47,7 +47,7 @@ func parseYamlTemplate(input string) (block Block, err error) {
codec, err := parseCodecType(blk.Codec) codec, err := parseCodecType(blk.Codec)
if err != nil { 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 var fields []BlockField
@@ -63,7 +63,7 @@ func parseYamlTemplate(input string) (block Block, err error) {
fieldCodec, err := parseCodecType(field.Codec) fieldCodec, err := parseCodecType(field.Codec)
if err != nil { 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{ fields = append(fields, BlockField{

View File

@@ -1,13 +1,9 @@
package template package template
import (
"fmt"
)
// CompileTemplate scans once, emitting: // CompileTemplate scans once, emitting:
// - data blocks: inner content between a line that's exactly "{" and a line that's exactly "}" // - 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) // - 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 out []Block
var curlyIndex int var curlyIndex int
@@ -16,14 +12,15 @@ func CompileTemplate(template string) ([]Block, error) {
var start int var start int
var blockType BlockType var blockType BlockType
template := NewSlice(templateSource)
if len(template) > 0 && template[0] == OPENING { if template.Len() > 0 && template.At(0) == OPENING {
blockType = DataBlock blockType = DataBlock
} else { } else {
blockType = MatchingBlock blockType = MatchingBlock
} }
for i, r := range template { for i, r := range template.Chars() {
nextCurlyIndex := curlyIndex nextCurlyIndex := curlyIndex
@@ -36,27 +33,26 @@ func CompileTemplate(template string) ([]Block, error) {
if curlyIndex == 0 && nextCurlyIndex == 1 { if curlyIndex == 0 && nextCurlyIndex == 1 {
if i > start { if i > start {
block, err := ParseTemplateBlock(template[start:i], blockType) block, err := ParseTemplateBlock(template.Slice(start, i), blockType)
if err != nil { 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) out = append(out, block)
} }
start = i start = i
blockType = DataBlock blockType = DataBlock
} else if curlyIndex == 1 && nextCurlyIndex == 0 { } else if curlyIndex == 1 && nextCurlyIndex == 0 {
if i > start { if i > start {
block, err := ParseTemplateBlock(template[start:i+1], blockType) block, err := ParseTemplateBlock(template.Slice(start, i), blockType)
if err != nil { 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) out = append(out, block)
} }
nextChar := ' ' nextChar := ' '
if i+1 < len(template) { if i+1 < template.Len() {
nextChar = rune(template[i+1]) nextChar = rune(template.At(i + 1))
} }
if nextChar == OPENING { if nextChar == OPENING {

36
template/error.go Normal file
View File

@@ -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
}

46
template/slice.go Normal file
View File

@@ -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,
}
}

View File

@@ -22,9 +22,9 @@ type Block struct {
Fields []BlockField Fields []BlockField
Optional bool Optional bool
Value any Value any
content string content Slice
} }
func (b Block) GetContent() string { func (b Block) GetContent() string {
return b.content return b.content.String()
} }