package config import ( "fmt" "os" "strconv" "strings" ) // LoadMarkdown decodes a Markdown config file with YAML frontmatter // (R-014). The file format is: // // --- // listen_addr: 127.0.0.1:9999 // node_capacity: // cpu: 4 // memory_mb: 8192 // db_path: /tmp/orca/test.db // --- // // body prose (ignored) // // The frontmatter parser is a minimal hand-rolled key:value parser // (no new dependencies; gopkg.in/yaml.v3 is not in go.mod). It supports // flat scalar keys and one level of nested mapping (for node_capacity). // The Markdown body after the closing "---" is ignored. // // The returned *Config is the same struct the HCL loader produces, so // downstream consumers are unchanged. func LoadMarkdown(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config %s: %w", path, err) } return parseFrontmatter(string(data), path) } // LoadMarkdownYAML decodes a bare YAML file (no Markdown body) using the // same minimal frontmatter parser. .yaml/.yml files are routed here by // the dispatcher. func LoadMarkdownYAML(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config %s: %w", path, err) } // Treat the whole file as the frontmatter block (no surrounding ---). return parseFrontmatterBlock(string(data), path) } func parseFrontmatter(content, path string) (*Config, error) { block, ok := extractFrontmatter(content) if !ok { // No frontmatter delimiters: treat whole file as a bare block. return parseFrontmatterBlock(content, path) } return parseFrontmatterBlock(block, path) } // extractFrontmatter returns the YAML block between the first pair of // "---" delimiters and whether a frontmatter block was present. func extractFrontmatter(content string) (string, bool) { trimmed := strings.TrimLeft(content, "\r\n\t ") if !strings.HasPrefix(trimmed, "---") { return "", false } // Skip the opening delimiter line. rest := trimmed[3:] rest = strings.TrimLeft(rest, "\r\n") // Find the closing delimiter line. idx := strings.Index(rest, "\n---") if idx < 0 { return "", false } return rest[:idx], true } // parseFrontmatterBlock parses a minimal YAML-ish block into *Config. // Supported shapes: // // key: value // node_capacity: // cpu: 4 // memory_mb: 8192 // // Comments (# ...) and blank lines are ignored. Quoted scalar values // ("..." or '...') are unwrapped. No flow collections, anchors, or // multi-line strings are supported — by design, to avoid adding a YAML // dependency for this small config surface. func parseFrontmatterBlock(block, path string) (*Config, error) { cfg := &Config{} var inCapacity bool lines := strings.Split(block, "\n") for lineNo, raw := range lines { line := stripComment(raw) if strings.TrimSpace(line) == "" { continue } indent := countIndent(line) trimmed := strings.TrimSpace(line) // A top-level key (no leading indent). if indent == 0 { inCapacity = false key, val, ok := splitKV(trimmed) if !ok { continue } if val == "" { // key with no value → nested mapping header (e.g. node_capacity:) if key == "node_capacity" { cfg.NodeCapacity = &CapacityConfig{} inCapacity = true } continue } applyScalar(cfg, key, val, path, lineNo) continue } // Indented line under a nested mapping. if inCapacity && cfg.NodeCapacity != nil { key, val, hasVal := splitKV(trimmed) if !hasVal { continue } switch key { case "cpu": if n, err := strconv.Atoi(strings.TrimSpace(val)); err == nil { cfg.NodeCapacity.CPU = n } case "memory_mb": if n, err := strconv.Atoi(strings.TrimSpace(val)); err == nil { cfg.NodeCapacity.MemoryMB = n } } } } return cfg, nil } func applyScalar(cfg *Config, key, val, path string, lineNo int) { val = strings.TrimSpace(val) switch key { case "db_path": cfg.DBPath = unquote(val) case "listen_addr": cfg.ListenAddr = unquote(val) case "ca_path": cfg.CAPath = unquote(val) case "server_cert_path": cfg.ServerCertPath = unquote(val) case "server_key_path": cfg.ServerKeyPath = unquote(val) } _ = path _ = lineNo } func splitKV(s string) (key, val string, ok bool) { idx := strings.Index(s, ":") if idx < 0 { return "", "", false } key = strings.TrimSpace(s[:idx]) val = strings.TrimSpace(s[idx+1:]) if key == "" { return "", "", false } return key, val, true } func countIndent(s string) int { n := 0 for _, r := range s { if r == ' ' || r == '\t' { n++ continue } break } return n } func stripComment(s string) string { // Strip inline comments not inside quotes. Minimal: only strip // when the '#' is preceded by whitespace or at line start. inSingle := false inDouble := false for i := 0; i < len(s); i++ { c := s[i] switch c { case '\'': if !inDouble { inSingle = !inSingle } case '"': if !inSingle { inDouble = !inDouble } case '#': if !inSingle && !inDouble { if i == 0 || s[i-1] == ' ' || s[i-1] == '\t' { return s[:i] } } } } return s } func unquote(s string) string { if len(s) >= 2 { if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') { return s[1 : len(s)-1] } } return s }