package registry import ( "bytes" "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // rawFunction mirrors the YAML frontmatter of a function .md file. type rawFunction struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Lang string `yaml:"lang"` Domain string `yaml:"domain"` Version string `yaml:"version"` Purity string `yaml:"purity"` Signature string `yaml:"signature"` Description string `yaml:"description"` Tags []string `yaml:"tags"` UsesFunctions []string `yaml:"uses_functions"` UsesTypes []string `yaml:"uses_types"` Returns []string `yaml:"returns"` ReturnsOptional bool `yaml:"returns_optional"` ErrorType string `yaml:"error_type"` Imports []string `yaml:"imports"` Tested bool `yaml:"tested"` Tests []string `yaml:"tests"` TestFilePath string `yaml:"test_file_path"` FilePath string `yaml:"file_path"` // Component fields Props []PropDef `yaml:"props"` Emits []string `yaml:"emits"` HasState *bool `yaml:"has_state"` Framework string `yaml:"framework"` Variant []string `yaml:"variant"` } // rawType mirrors the YAML frontmatter of a type .md file. type rawType struct { Name string `yaml:"name"` Lang string `yaml:"lang"` Domain string `yaml:"domain"` Version string `yaml:"version"` Algebraic string `yaml:"algebraic"` Definition string `yaml:"definition"` Description string `yaml:"description"` Tags []string `yaml:"tags"` UsesTypes []string `yaml:"uses_types"` FilePath string `yaml:"file_path"` } // rawApp mirrors the YAML frontmatter of an app .md file. type rawApp struct { Name string `yaml:"name"` Lang string `yaml:"lang"` Domain string `yaml:"domain"` Description string `yaml:"description"` Tags []string `yaml:"tags"` UsesFunctions []string `yaml:"uses_functions"` UsesTypes []string `yaml:"uses_types"` Framework string `yaml:"framework"` EntryPoint string `yaml:"entry_point"` DirPath string `yaml:"dir_path"` } // extractFrontmatter splits a .md file into YAML frontmatter and body. func extractFrontmatter(data []byte) ([]byte, []byte, error) { content := data if !bytes.HasPrefix(content, []byte("---\n")) && !bytes.HasPrefix(content, []byte("---\r\n")) { return nil, nil, fmt.Errorf("missing opening --- in frontmatter") } // Skip opening --- rest := content[4:] idx := bytes.Index(rest, []byte("\n---")) if idx < 0 { return nil, nil, fmt.Errorf("missing closing --- in frontmatter") } fm := rest[:idx] body := rest[idx+4:] // skip \n--- return fm, body, nil } // ParseFunctionMD parses a function .md file into a Function. // root is the registry root directory, used to resolve file_path for code reading. func ParseFunctionMD(path string, root string) (*Function, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } fm, body, err := extractFrontmatter(data) if err != nil { return nil, fmt.Errorf("parsing %s: %w", path, err) } var raw rawFunction if err := yaml.Unmarshal(fm, &raw); err != nil { return nil, fmt.Errorf("parsing YAML in %s: %w", path, err) } if raw.Name == "" { return nil, fmt.Errorf("%s: name is required", path) } if raw.Kind == "" { return nil, fmt.Errorf("%s: kind is required", path) } if raw.Description == "" { return nil, fmt.Errorf("%s: description is required", path) } sections := extractSections(body) f := &Function{ ID: GenerateID(raw.Name, raw.Lang, raw.Domain), Name: raw.Name, Kind: Kind(raw.Kind), Lang: raw.Lang, Domain: raw.Domain, Version: raw.Version, Purity: Purity(raw.Purity), Signature: raw.Signature, Description: raw.Description, Tags: raw.Tags, UsesFunctions: raw.UsesFunctions, UsesTypes: raw.UsesTypes, Returns: raw.Returns, ReturnsOptional: raw.ReturnsOptional, ErrorType: raw.ErrorType, Imports: raw.Imports, Example: sections.example, Notes: sections.notes, Documentation: sections.documentation, Tested: raw.Tested, Tests: raw.Tests, TestFilePath: raw.TestFilePath, FilePath: raw.FilePath, Props: raw.Props, Emits: raw.Emits, HasState: raw.HasState, Framework: raw.Framework, Variant: raw.Variant, } if root != "" && raw.FilePath != "" { codePath := filepath.Join(root, raw.FilePath) if codeData, err := os.ReadFile(codePath); err == nil { f.Code = string(codeData) } } return f, nil } // ParseTypeMD parses a type .md file into a Type. // root is the registry root directory, used to resolve file_path for code reading. func ParseTypeMD(path string, root string) (*Type, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } fm, body, err := extractFrontmatter(data) if err != nil { return nil, fmt.Errorf("parsing %s: %w", path, err) } var raw rawType if err := yaml.Unmarshal(fm, &raw); err != nil { return nil, fmt.Errorf("parsing YAML in %s: %w", path, err) } if raw.Name == "" { return nil, fmt.Errorf("%s: name is required", path) } if raw.Description == "" { return nil, fmt.Errorf("%s: description is required", path) } sections := extractSections(body) t := &Type{ ID: GenerateID(raw.Name, raw.Lang, raw.Domain), Name: raw.Name, Lang: raw.Lang, Domain: raw.Domain, Version: raw.Version, Algebraic: Algebraic(raw.Algebraic), Definition: strings.TrimSpace(raw.Definition), Description: raw.Description, Tags: raw.Tags, UsesTypes: raw.UsesTypes, Examples: sections.example, Notes: sections.notes, Documentation: sections.documentation, FilePath: raw.FilePath, } if root != "" && raw.FilePath != "" { codePath := filepath.Join(root, raw.FilePath) if codeData, err := os.ReadFile(codePath); err == nil { t.Code = string(codeData) } } return t, nil } // ParseAppMD parses an app .md file into an App. func ParseAppMD(path string, root string) (*App, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } fm, body, err := extractFrontmatter(data) if err != nil { return nil, fmt.Errorf("parsing %s: %w", path, err) } var raw rawApp if err := yaml.Unmarshal(fm, &raw); err != nil { return nil, fmt.Errorf("parsing YAML in %s: %w", path, err) } if raw.Name == "" { return nil, fmt.Errorf("%s: name is required", path) } if raw.Description == "" { return nil, fmt.Errorf("%s: description is required", path) } sections := extractSections(body) a := &App{ ID: GenerateID(raw.Name, raw.Lang, raw.Domain), Name: raw.Name, Lang: raw.Lang, Domain: raw.Domain, Description: raw.Description, Tags: raw.Tags, UsesFunctions: raw.UsesFunctions, UsesTypes: raw.UsesTypes, Framework: raw.Framework, EntryPoint: raw.EntryPoint, Documentation: sections.documentation, Notes: sections.notes, DirPath: raw.DirPath, } return a, nil } // bodySections holds the extracted sections from a .md body. type bodySections struct { example string // content under ## Ejemplo notes string // content under ## Notas documentation string // everything else } // extractSections splits the markdown body into named sections. // Known sections (## Ejemplo, ## Notas) are extracted separately. // All other content (including unknown ## headings) goes into documentation. func extractSections(body []byte) bodySections { lines := strings.Split(string(body), "\n") var s bodySections type section struct { name string lines []string } var current *section var sections []section for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "## ") { if current != nil { sections = append(sections, *current) } current = §ion{name: trimmed} continue } if current != nil { current.lines = append(current.lines, line) } else { // Content before any ## heading goes to documentation sections = append(sections, section{name: "_preamble", lines: []string{line}}) } } if current != nil { sections = append(sections, *current) } var docParts []string for _, sec := range sections { content := strings.TrimSpace(strings.Join(sec.lines, "\n")) if content == "" && sec.name == "_preamble" { continue } switch { case strings.HasPrefix(sec.name, "## Ejemplo"): s.example = content case strings.HasPrefix(sec.name, "## Notas"): s.notes = content case sec.name == "_preamble": docParts = append(docParts, content) default: // Unknown sections go to documentation with their heading docParts = append(docParts, sec.name+"\n\n"+content) } } s.documentation = strings.TrimSpace(strings.Join(docParts, "\n\n")) return s }