// Package parser provides BPMN parsing package parser import ( "fmt" "strings" "mermaid-go/pkg/ast" "mermaid-go/pkg/lexer" ) // BPMNParser implements BPMN parsing type BPMNParser struct { tokens []lexer.Token current int diagram *ast.BPMNDiagram } // NewBPMNParser creates a new BPMN parser func NewBPMNParser() *BPMNParser { return &BPMNParser{ diagram: ast.NewBPMNDiagram(), } } // Parse parses BPMN syntax func (p *BPMNParser) Parse(input string) (*ast.BPMNDiagram, 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.NewBPMNDiagram() // Parse document err = p.parseDocument() if err != nil { return nil, fmt.Errorf("syntax analysis failed: %w", err) } return p.diagram, nil } // parseDocument parses the BPMN document func (p *BPMNParser) parseDocument() error { // Expect bpmn if !p.check(lexer.TokenID) || p.peek().Value != "bpmn" { return p.error("expected 'bpmn'") } p.advance() // Parse statements for !p.isAtEnd() { if err := p.parseStatement(); err != nil { return err } } return nil } // parseStatement parses individual BPMN statements func (p *BPMNParser) 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("pool"): return p.parsePool() case p.checkKeyword("lane"): return p.parseLane() case p.check(lexer.TokenID): // Element definition or flow return p.parseElementOrFlow() default: token := p.peek() return p.error(fmt.Sprintf("unexpected token: %s", token.Value)) } } // parseTitle parses title statements func (p *BPMNParser) 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 } // parsePool parses pool statements func (p *BPMNParser) parsePool() error { p.advance() // consume 'pool' if !p.check(lexer.TokenID) { return p.error("expected pool ID") } poolID := p.advance().Value pool := &ast.BPMNPool{ ID: poolID, Name: poolID, Lanes: make([]string, 0), } // Parse pool properties for !p.check(lexer.TokenNewline) && !p.isAtEnd() { if p.check(lexer.TokenOpenBracket) { p.advance() // consume '[' // Parse pool name var nameParts []string for !p.check(lexer.TokenCloseBracket) && !p.isAtEnd() { nameParts = append(nameParts, p.advance().Value) } if len(nameParts) > 0 { pool.Name = strings.TrimSpace(strings.Join(nameParts, " ")) } if p.check(lexer.TokenCloseBracket) { p.advance() // consume ']' } } else { p.advance() // consume unknown token } } p.diagram.Pools = append(p.diagram.Pools, pool) return nil } // parseLane parses lane statements func (p *BPMNParser) parseLane() error { p.advance() // consume 'lane' if !p.check(lexer.TokenID) { return p.error("expected lane ID") } laneID := p.advance().Value lane := &ast.BPMNLane{ ID: laneID, Name: laneID, Elements: make([]string, 0), } // Parse lane properties for !p.check(lexer.TokenNewline) && !p.isAtEnd() { if p.check(lexer.TokenOpenBracket) { p.advance() // consume '[' // Parse lane name var nameParts []string for !p.check(lexer.TokenCloseBracket) && !p.isAtEnd() { nameParts = append(nameParts, p.advance().Value) } if len(nameParts) > 0 { lane.Name = strings.TrimSpace(strings.Join(nameParts, " ")) } if p.check(lexer.TokenCloseBracket) { p.advance() // consume ']' } } else if p.checkKeyword("in") { p.advance() // consume 'in' if p.check(lexer.TokenID) { lane.Pool = p.advance().Value } } else { p.advance() // consume unknown token } } p.diagram.Lanes = append(p.diagram.Lanes, lane) return nil } // parseElementOrFlow parses element definition or flow func (p *BPMNParser) parseElementOrFlow() error { elementID := p.advance().Value // Check if this is a flow (has arrow indicators) if p.checkFlow() { return p.parseFlow(elementID) } // Otherwise, it's an element definition element := &ast.BPMNElement{ ID: elementID, Name: elementID, Type: p.inferElementType(elementID), Properties: make(map[string]any), CssClasses: make([]string, 0), } // Parse element properties for !p.check(lexer.TokenNewline) && !p.isAtEnd() { if p.check(lexer.TokenOpenBracket) { p.advance() // consume '[' // Parse element name var nameParts []string for !p.check(lexer.TokenCloseBracket) && !p.isAtEnd() { nameParts = append(nameParts, p.advance().Value) } if len(nameParts) > 0 { element.Name = strings.TrimSpace(strings.Join(nameParts, " ")) } if p.check(lexer.TokenCloseBracket) { p.advance() // consume ']' } } else if p.check(lexer.TokenOpenParen) { // Parse element type p.advance() // consume '(' if p.check(lexer.TokenID) { typeStr := p.advance().Value element.Type = ast.BPMNElementType(typeStr) } if p.check(lexer.TokenCloseParen) { p.advance() // consume ')' } } else { p.advance() // consume unknown token } } p.diagram.AddElement(element) return nil } // parseFlow parses flow connections func (p *BPMNParser) parseFlow(fromID string) error { // Parse flow type and direction flowType := ast.BPMNFlowSequence // default // Skip flow indicators for p.checkFlow() { token := p.advance() if token.Value == "-->" { flowType = ast.BPMNFlowSequence } else if token.Value == "-.>" { flowType = ast.BPMNFlowMessage } } // Parse target element if !p.check(lexer.TokenID) { return p.error("expected target element ID") } toID := p.advance().Value flow := &ast.BPMNFlow{ ID: fmt.Sprintf("%s_to_%s", fromID, toID), From: fromID, To: toID, Type: flowType, Properties: make(map[string]any), } // Parse flow label if p.check(lexer.TokenColon) { p.advance() // consume ':' var labelParts []string for !p.check(lexer.TokenNewline) && !p.isAtEnd() { labelParts = append(labelParts, p.advance().Value) } if len(labelParts) > 0 { label := strings.TrimSpace(strings.Join(labelParts, " ")) flow.Name = &label } } p.diagram.AddFlow(flow) return nil } // checkFlow checks if current position looks like a flow func (p *BPMNParser) checkFlow() bool { if p.isAtEnd() { return false } token := p.peek() return token.Type == lexer.TokenArrowSolid || token.Type == lexer.TokenArrowDotted || token.Type == lexer.TokenMinus } // inferElementType infers BPMN element type from ID func (p *BPMNParser) inferElementType(id string) ast.BPMNElementType { lowerID := strings.ToLower(id) // Infer type from common naming patterns if strings.Contains(lowerID, "start") { return ast.BPMNElementStartEvent } if strings.Contains(lowerID, "end") { return ast.BPMNElementEndEvent } if strings.Contains(lowerID, "gateway") || strings.Contains(lowerID, "decision") { return ast.BPMNElementExclusiveGateway } if strings.Contains(lowerID, "task") { return ast.BPMNElementTask } if strings.Contains(lowerID, "user") { return ast.BPMNElementUserTask } if strings.Contains(lowerID, "service") { return ast.BPMNElementServiceTask } // Default to task return ast.BPMNElementTask } // Helper methods func (p *BPMNParser) check(tokenType lexer.TokenType) bool { if p.isAtEnd() { return false } return p.peek().Type == tokenType } func (p *BPMNParser) 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 *BPMNParser) advance() lexer.Token { if !p.isAtEnd() { p.current++ } return p.previous() } func (p *BPMNParser) isAtEnd() bool { return p.current >= len(p.tokens) || p.peek().Type == lexer.TokenEOF } func (p *BPMNParser) peek() lexer.Token { if p.current >= len(p.tokens) { return lexer.Token{Type: lexer.TokenEOF} } return p.tokens[p.current] } func (p *BPMNParser) previous() lexer.Token { if p.current <= 0 { return lexer.Token{Type: lexer.TokenEOF} } return p.tokens[p.current-1] } func (p *BPMNParser) 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()) }