// Package parser provides Architecture parsing based on architectureParser.ts package parser import ( "fmt" "strings" "mermaid-go/pkg/ast" "mermaid-go/pkg/lexer" ) // ArchitectureParser implements Architecture parsing type ArchitectureParser struct { tokens []lexer.Token current int diagram *ast.ArchitectureDiagram } // NewArchitectureParser creates a new Architecture parser func NewArchitectureParser() *ArchitectureParser { return &ArchitectureParser{ diagram: ast.NewArchitectureDiagram(), } } // Parse parses Architecture syntax func (p *ArchitectureParser) Parse(input string) (*ast.ArchitectureDiagram, error) { // Tokenize l := lexer.NewLexer(input) tokens, err := l.Tokenize() if err != nil { return nil, fmt.Errorf("lexical analysis failed: %w", err) } // Filter tokens p.tokens = lexer.FilterTokens(tokens) p.current = 0 p.diagram = ast.NewArchitectureDiagram() // Parse document err = p.parseDocument() if err != nil { return nil, fmt.Errorf("syntax analysis failed: %w", err) } return p.diagram, nil } // parseDocument parses the Architecture document func (p *ArchitectureParser) parseDocument() error { // Expect architecture if !p.check(lexer.TokenID) || p.peek().Value != "architecture" { return p.error("expected 'architecture'") } p.advance() // Parse statements for !p.isAtEnd() { if err := p.parseStatement(); err != nil { return err } } return nil } // parseStatement parses individual Architecture statements func (p *ArchitectureParser) parseStatement() error { if p.isAtEnd() { return nil } switch { case p.check(lexer.TokenNewline): p.advance() // Skip newlines return nil case p.checkKeyword("title"): return p.parseTitle() case p.checkKeyword("service"): return p.parseService() case p.checkKeyword("group"): return p.parseGroup() case p.check(lexer.TokenID): // Could be service definition or edge return p.parseServiceOrEdge() default: token := p.peek() return p.error(fmt.Sprintf("unexpected token: %s", token.Value)) } } // parseTitle parses title statements func (p *ArchitectureParser) parseTitle() error { p.advance() // consume 'title' var titleParts []string for !p.check(lexer.TokenNewline) && !p.isAtEnd() { titleParts = append(titleParts, p.advance().Value) } if len(titleParts) > 0 { title := strings.TrimSpace(strings.Join(titleParts, " ")) p.diagram.Title = &title } return nil } // parseService parses service statements func (p *ArchitectureParser) parseService() error { p.advance() // consume 'service' if !p.check(lexer.TokenID) { return p.error("expected service ID") } serviceID := p.advance().Value service := &ast.ArchitectureService{ ID: serviceID, } // Parse optional service properties for !p.check(lexer.TokenNewline) && !p.isAtEnd() { if p.check(lexer.TokenOpenBracket) { p.advance() // consume '[' // Parse service title var titleParts []string for !p.check(lexer.TokenCloseBracket) && !p.isAtEnd() { titleParts = append(titleParts, p.advance().Value) } if len(titleParts) > 0 { title := strings.TrimSpace(strings.Join(titleParts, " ")) service.Title = &title } if p.check(lexer.TokenCloseBracket) { p.advance() // consume ']' } } else if p.checkKeyword("in") { p.advance() // consume 'in' if p.check(lexer.TokenID) { groupID := p.advance().Value service.In = &groupID } } else { p.advance() // consume unknown token } } p.diagram.Services = append(p.diagram.Services, service) return nil } // parseGroup parses group statements func (p *ArchitectureParser) parseGroup() error { p.advance() // consume 'group' if !p.check(lexer.TokenID) { return p.error("expected group ID") } groupID := p.advance().Value group := &ast.ArchitectureGroup{ ID: groupID, } // Parse optional group properties for !p.check(lexer.TokenNewline) && !p.isAtEnd() { if p.check(lexer.TokenOpenBracket) { p.advance() // consume '[' // Parse group title var titleParts []string for !p.check(lexer.TokenCloseBracket) && !p.isAtEnd() { titleParts = append(titleParts, p.advance().Value) } if len(titleParts) > 0 { title := strings.TrimSpace(strings.Join(titleParts, " ")) group.Title = &title } if p.check(lexer.TokenCloseBracket) { p.advance() // consume ']' } } else if p.checkKeyword("in") { p.advance() // consume 'in' if p.check(lexer.TokenID) { parentID := p.advance().Value group.In = &parentID } } else { p.advance() // consume unknown token } } p.diagram.Groups = append(p.diagram.Groups, group) return nil } // parseServiceOrEdge parses service definition or edge func (p *ArchitectureParser) parseServiceOrEdge() error { serviceID := p.advance().Value // Check if this is an edge (has direction indicators) if p.checkDirection() { return p.parseEdge(serviceID) } // Otherwise, it's a simple service definition service := &ast.ArchitectureService{ ID: serviceID, } // Parse optional service properties for !p.check(lexer.TokenNewline) && !p.isAtEnd() { if p.check(lexer.TokenOpenBracket) { p.advance() // consume '[' // Parse service title var titleParts []string for !p.check(lexer.TokenCloseBracket) && !p.isAtEnd() { titleParts = append(titleParts, p.advance().Value) } if len(titleParts) > 0 { title := strings.TrimSpace(strings.Join(titleParts, " ")) service.Title = &title } if p.check(lexer.TokenCloseBracket) { p.advance() // consume ']' } } else { p.advance() // consume unknown token } } p.diagram.Services = append(p.diagram.Services, service) return nil } // parseEdge parses edge connections func (p *ArchitectureParser) parseEdge(lhsID string) error { // Parse left direction lhsDir := p.parseDirection() // Skip connection symbols for p.check(lexer.TokenMinus) || p.check(lexer.TokenEquals) { p.advance() } // Parse right direction rhsDir := p.parseDirection() // Parse target service/group if !p.check(lexer.TokenID) { return p.error("expected target service/group ID") } rhsID := p.advance().Value edge := &ast.ArchitectureEdge{ LhsID: lhsID, LhsDir: lhsDir, RhsID: rhsID, RhsDir: rhsDir, } // Parse optional edge title if p.check(lexer.TokenColon) { p.advance() // consume ':' var titleParts []string for !p.check(lexer.TokenNewline) && !p.isAtEnd() { titleParts = append(titleParts, p.advance().Value) } if len(titleParts) > 0 { title := strings.TrimSpace(strings.Join(titleParts, " ")) edge.Title = &title } } p.diagram.Edges = append(p.diagram.Edges, edge) return nil } // checkDirection checks if current token is a direction indicator func (p *ArchitectureParser) checkDirection() bool { if p.isAtEnd() { return false } token := p.peek() return token.Type == lexer.TokenID && (token.Value == "L" || token.Value == "R" || token.Value == "T" || token.Value == "B") } // parseDirection parses direction indicators func (p *ArchitectureParser) parseDirection() ast.ArchitectureDirection { if !p.check(lexer.TokenID) { return ast.ArchitectureDirectionRight // default } token := p.advance() switch token.Value { case "L": return ast.ArchitectureDirectionLeft case "R": return ast.ArchitectureDirectionRight case "T": return ast.ArchitectureDirectionTop case "B": return ast.ArchitectureDirectionBottom default: return ast.ArchitectureDirectionRight // default } } // Helper methods func (p *ArchitectureParser) check(tokenType lexer.TokenType) bool { if p.isAtEnd() { return false } return p.peek().Type == tokenType } func (p *ArchitectureParser) checkKeyword(keyword string) bool { if p.isAtEnd() { return false } token := p.peek() return token.Type == lexer.TokenID && strings.ToLower(token.Value) == strings.ToLower(keyword) } func (p *ArchitectureParser) advance() lexer.Token { if !p.isAtEnd() { p.current++ } return p.previous() } func (p *ArchitectureParser) isAtEnd() bool { return p.current >= len(p.tokens) || p.peek().Type == lexer.TokenEOF } func (p *ArchitectureParser) peek() lexer.Token { if p.current >= len(p.tokens) { return lexer.Token{Type: lexer.TokenEOF} } return p.tokens[p.current] } func (p *ArchitectureParser) previous() lexer.Token { if p.current <= 0 { return lexer.Token{Type: lexer.TokenEOF} } return p.tokens[p.current-1] } func (p *ArchitectureParser) error(message string) error { token := p.peek() return fmt.Errorf("parse error at line %d, column %d: %s (got %s)", token.Line, token.Column, message, token.Type.String()) }