package config import ( "fmt" "io" "strconv" ) // Parser parses configuration files type Parser struct { scanner *Scanner config *Config currentObject map[string]any stack []map[string]any currentToken Token } // NewParser creates a new parser with a reader and empty config func NewParser(r io.Reader) *Parser { config := NewConfig() return &Parser{ scanner: NewScanner(r), config: config, currentObject: config.data, stack: make([]map[string]any, 0, 8), // Pre-allocate stack with reasonable capacity } } // Error creates an error with line information from the current token func (p *Parser) Error(msg string) error { return fmt.Errorf("line %d, column %d: %s", p.currentToken.Line, p.currentToken.Column, msg) } // Parse parses the config file and returns a Config func (p *Parser) Parse() (*Config, error) { err := p.parseContent() if err != nil { return nil, err } return p.config, nil } // nextToken gets the next meaningful token (skipping comments) func (p *Parser) nextToken() (Token, error) { for { token, err := p.scanner.NextToken() if err != nil { return token, err } // Skip comment tokens if token.Type != TokenComment { p.currentToken = token return token, nil } } } // parseContent is the main parsing function func (p *Parser) parseContent() error { for { token, err := p.nextToken() if err != nil { return err } // Check for end of file if token.Type == TokenEOF { break } // We expect top level entries to be names if token.Type != TokenName { return p.Error("expected name at top level") } // Get the property name name := string(token.Value) // Get the next token (should be = or {) token, err = p.nextToken() if err != nil { return err } var value any if token.Type == TokenEquals { // It's a standard key=value assignment value, err = p.parseValue() if err != nil { return err } } else if token.Type == TokenOpenBrace { // It's a map/array without '=' value, err = p.parseObject() if err != nil { return err } } else { return p.Error("expected '=' or '{' after name") } // Store the value in the config if mapValue, ok := value.(map[string]any); ok { // Add an entry in current object newMap := make(map[string]any, 8) // Pre-allocate with capacity p.currentObject[name] = newMap // Process the map contents p.stack = append(p.stack, p.currentObject) p.currentObject = newMap // Copy values from scanned map to our object for k, v := range mapValue { p.currentObject[k] = v } // Restore parent object n := len(p.stack) if n > 0 { p.currentObject = p.stack[n-1] p.stack = p.stack[:n-1] } } else { // Direct storage for primitives and arrays p.currentObject[name] = value } } return nil } // parseValue parses a value after an equals sign func (p *Parser) parseValue() (any, error) { token, err := p.nextToken() if err != nil { return nil, err } switch token.Type { case TokenString: return string(token.Value), nil case TokenNumber: strValue := string(token.Value) for i := 0; i < len(strValue); i++ { if strValue[i] == '.' { // It's a float val, err := strconv.ParseFloat(strValue, 64) if err != nil { return nil, p.Error(fmt.Sprintf("invalid float: %s", strValue)) } return val, nil } } // It's an integer val, err := strconv.ParseInt(strValue, 10, 64) if err != nil { return nil, p.Error(fmt.Sprintf("invalid integer: %s", strValue)) } return val, nil case TokenBoolean: return string(token.Value) == "true", nil case TokenOpenBrace: // It's a map or array return p.parseObject() case TokenName: // Treat as a string value return string(token.Value), nil default: return nil, p.Error(fmt.Sprintf("unexpected token: %v", token.Type)) } } // parseObject parses a map or array func (p *Parser) parseObject() (any, error) { contents := make(map[string]any) var arrayElements []any isArray := true for { token, err := p.nextToken() if err != nil { return nil, err } // Check for end of object if token.Type == TokenCloseBrace { if isArray && len(contents) == 0 { return arrayElements, nil } return contents, nil } // Handle based on token type switch token.Type { case TokenName: // Get the name value name := string(token.Value) // Get the next token to determine if it's a map entry or array element nextToken, err := p.nextToken() if err != nil { return nil, err } if nextToken.Type == TokenEquals { // It's a key-value pair value, err := p.parseValue() if err != nil { return nil, err } isArray = false contents[name] = value } else if nextToken.Type == TokenOpenBrace { // It's a nested object objValue, err := p.parseObject() if err != nil { return nil, err } isArray = false contents[name] = objValue } else { // Put the token back and treat the name as an array element p.scanner.UnreadToken(nextToken) // Convert to appropriate type if possible var value any = name // Try to infer type if name == "true" { value = true } else if name == "false" { value = false } else if isDigitOrMinus(name) { // Try to parse as number numValue, err := parseStringAsNumber(name) if err == nil { value = numValue } } arrayElements = append(arrayElements, value) } case TokenString, TokenNumber, TokenBoolean: // Direct array element var value any switch token.Type { case TokenString: value = string(token.Value) case TokenNumber: strVal := string(token.Value) value, _ = parseStringAsNumber(strVal) case TokenBoolean: value = string(token.Value) == "true" } arrayElements = append(arrayElements, value) case TokenOpenBrace: // Nested object in array nestedObj, err := p.parseObject() if err != nil { return nil, err } arrayElements = append(arrayElements, nestedObj) default: return nil, p.Error(fmt.Sprintf("unexpected token in object: %v", token.Type)) } } }