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 } // pushObject enters a new object scope func (p *Parser) pushObject(obj map[string]any) { p.stack = append(p.stack, p.currentObject) p.currentObject = obj } // popObject exits the current object scope func (p *Parser) popObject() { n := len(p.stack) if n > 0 { p.currentObject = p.stack[n-1] p.stack = p.stack[:n-1] } } // 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.pushObject(newMap) // Copy values from scanned map to our object for k, v := range mapValue { p.currentObject[k] = v } p.popObject() } 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) // Check if it's a float or int 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 } // If we find a name, it could be a map entry or array element if token.Type == 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) // Try to convert to appropriate type var value any = name // Do some type inference for common values if name == "true" { value = true } else if name == "false" { value = false } else if isDigit(name[0]) || (len(name) > 1 && name[0] == '-' && isDigit(name[1])) { // Try to parse as number if hasDot(name) { if f, err := strconv.ParseFloat(name, 64); err == nil { value = f } } else { if i, err := strconv.ParseInt(name, 10, 64); err == nil { value = i } } } arrayElements = append(arrayElements, value) } } else if token.Type == TokenString || token.Type == TokenNumber || token.Type == TokenBoolean { // Direct array element var value any switch token.Type { case TokenString: value = string(token.Value) case TokenNumber: strVal := string(token.Value) if hasDot(strVal) { f, _ := strconv.ParseFloat(strVal, 64) value = f } else { i, _ := strconv.ParseInt(strVal, 10, 64) value = i } case TokenBoolean: value = string(token.Value) == "true" } arrayElements = append(arrayElements, value) } else if token.Type == TokenOpenBrace { // Nested object in array nestedObj, err := p.parseObject() if err != nil { return nil, err } arrayElements = append(arrayElements, nestedObj) } else { return nil, p.Error(fmt.Sprintf("unexpected token in object: %v", token.Type)) } } }