| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- // 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())
- }
|