// Package parser provides Gantt chart parsing based on gantt.jison package parser import ( "fmt" "strings" "mermaid-go/pkg/ast" "mermaid-go/pkg/lexer" ) // GanttParser implements Gantt chart parsing following gantt.jison type GanttParser struct { tokens []lexer.Token current int diagram *ast.GanttDiagram } // NewGanttParser creates a new Gantt parser func NewGanttParser() *GanttParser { return &GanttParser{ diagram: &ast.GanttDiagram{ DateFormat: "YYYY-MM-DD", AxisFormat: "%Y-%m-%d", Sections: make([]*ast.GanttSection, 0), Tasks: make([]*ast.GanttTask, 0), Config: make(map[string]any), }, } } // Parse parses Gantt chart syntax func (p *GanttParser) Parse(input string) (*ast.GanttDiagram, 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.GanttDiagram{ DateFormat: "YYYY-MM-DD", AxisFormat: "%Y-%m-%d", Sections: make([]*ast.GanttSection, 0), Tasks: make([]*ast.GanttTask, 0), Config: make(map[string]any), } // Parse document err = p.parseDocument() if err != nil { return nil, fmt.Errorf("syntax analysis failed: %w", err) } return p.diagram, nil } // parseDocument parses the Gantt chart document func (p *GanttParser) parseDocument() error { // Expect gantt if !p.check(lexer.TokenID) || p.peek().Value != "gantt" { return p.error("expected 'gantt'") } p.advance() // Parse statements for !p.isAtEnd() { if err := p.parseStatement(); err != nil { return err } } return nil } // parseStatement parses individual Gantt chart statements func (p *GanttParser) 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("dateFormat"): return p.parseDateFormat() case p.checkKeyword("axisFormat"): return p.parseAxisFormat() case p.checkKeyword("section"): return p.parseSection() case p.check(lexer.TokenID): // Task definition return p.parseTask() default: token := p.peek() return p.error(fmt.Sprintf("unexpected token: %s", token.Value)) } } // parseTitle parses title statements func (p *GanttParser) 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 } // parseDateFormat parses dateFormat statements func (p *GanttParser) parseDateFormat() error { p.advance() // consume 'dateFormat' var formatParts []string for !p.check(lexer.TokenNewline) && !p.isAtEnd() { formatParts = append(formatParts, p.advance().Value) } if len(formatParts) > 0 { p.diagram.DateFormat = strings.TrimSpace(strings.Join(formatParts, " ")) } return nil } // parseAxisFormat parses axisFormat statements func (p *GanttParser) parseAxisFormat() error { p.advance() // consume 'axisFormat' var formatParts []string for !p.check(lexer.TokenNewline) && !p.isAtEnd() { formatParts = append(formatParts, p.advance().Value) } if len(formatParts) > 0 { p.diagram.AxisFormat = strings.TrimSpace(strings.Join(formatParts, " ")) } return nil } // parseSection parses section statements func (p *GanttParser) parseSection() error { p.advance() // consume 'section' var sectionParts []string for !p.check(lexer.TokenNewline) && !p.isAtEnd() { sectionParts = append(sectionParts, p.advance().Value) } if len(sectionParts) > 0 { sectionName := strings.TrimSpace(strings.Join(sectionParts, " ")) section := &ast.GanttSection{ Name: sectionName, Tasks: make([]*ast.GanttTask, 0), } p.diagram.Sections = append(p.diagram.Sections, section) } return nil } // parseTask parses task definitions func (p *GanttParser) parseTask() error { // Parse task name taskName := p.advance().Value // Expect colon if !p.check(lexer.TokenColon) { return p.error("expected ':' after task name") } p.advance() // Parse task data var taskDataParts []string for !p.check(lexer.TokenNewline) && !p.isAtEnd() { taskDataParts = append(taskDataParts, p.advance().Value) } taskData := strings.TrimSpace(strings.Join(taskDataParts, " ")) // Parse task data components task := &ast.GanttTask{ ID: generateTaskID(taskName), Name: taskName, Status: ast.GanttStatusActive, Dependencies: make([]string, 0), } // Parse task data (status, dates, dependencies) if taskData != "" { parts := strings.Fields(taskData) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } // Check for status keywords switch strings.ToLower(part) { case "active": task.Status = ast.GanttStatusActive case "done": task.Status = ast.GanttStatusDone case "crit": task.Status = ast.GanttStatusCrit default: // Try to parse as date or duration if strings.Contains(part, "-") && len(part) >= 8 { // Looks like a date if task.Start == nil { task.Start = &part } else if task.End == nil { task.End = &part } } else if strings.HasSuffix(part, "d") || strings.HasSuffix(part, "w") { // Looks like a duration task.Duration = &part } } } } // Add task to current section or global tasks if len(p.diagram.Sections) > 0 { currentSection := p.diagram.Sections[len(p.diagram.Sections)-1] currentSection.Tasks = append(currentSection.Tasks, task) } p.diagram.Tasks = append(p.diagram.Tasks, task) return nil } // generateTaskID generates a unique task ID from task name func generateTaskID(name string) string { // Simple ID generation - replace spaces with underscores and make lowercase id := strings.ToLower(strings.ReplaceAll(name, " ", "_")) return id } // Helper methods func (p *GanttParser) check(tokenType lexer.TokenType) bool { if p.isAtEnd() { return false } return p.peek().Type == tokenType } func (p *GanttParser) 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 *GanttParser) advance() lexer.Token { if !p.isAtEnd() { p.current++ } return p.previous() } func (p *GanttParser) isAtEnd() bool { return p.current >= len(p.tokens) || p.peek().Type == lexer.TokenEOF } func (p *GanttParser) peek() lexer.Token { if p.current >= len(p.tokens) { return lexer.Token{Type: lexer.TokenEOF} } return p.tokens[p.current] } func (p *GanttParser) previous() lexer.Token { if p.current <= 0 { return lexer.Token{Type: lexer.TokenEOF} } return p.tokens[p.current-1] } func (p *GanttParser) 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()) }