| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- // Package parser provides class diagram parsing based on classDiagram.jison
- package parser
- import (
- "fmt"
- "strings"
- "mermaid-go/pkg/ast"
- "mermaid-go/pkg/lexer"
- )
- // ClassParser implements class diagram parsing following classDiagram.jison
- type ClassParser struct {
- tokens []lexer.Token
- current int
- diagram *ast.ClassDiagram
- }
- // NewClassParser creates a new class parser
- func NewClassParser() *ClassParser {
- return &ClassParser{
- diagram: ast.NewClassDiagram(),
- }
- }
- // Parse parses class diagram syntax
- func (p *ClassParser) Parse(input string) (*ast.ClassDiagram, 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.NewClassDiagram()
- // Parse document
- err = p.parseDocument()
- if err != nil {
- return nil, fmt.Errorf("syntax analysis failed: %w", err)
- }
- return p.diagram, nil
- }
- // parseDocument parses the class diagram document
- func (p *ClassParser) parseDocument() error {
- // Expect classDiagram
- if !p.check(lexer.TokenID) || p.peek().Value != "classDiagram" {
- return p.error("expected 'classDiagram'")
- }
- p.advance()
- // Parse statements
- for !p.isAtEnd() {
- if err := p.parseStatement(); err != nil {
- return err
- }
- }
- return nil
- }
- // parseStatement parses individual class diagram statements
- func (p *ClassParser) parseStatement() error {
- if p.isAtEnd() {
- return nil
- }
- switch {
- case p.check(lexer.TokenNewline):
- p.advance() // Skip newlines
- return nil
- case p.checkComment():
- return p.parseComment()
- case p.check(lexer.TokenClass) || p.checkKeyword("class"):
- return p.parseClass()
- case p.checkKeyword("direction"):
- return p.parseDirection()
- case p.checkKeyword("link"):
- return p.parseLink()
- case p.checkKeyword("click"):
- return p.parseClick()
- case p.checkKeyword("note"):
- return p.parseNote()
- case p.checkKeyword("classDef"):
- return p.parseClassDef()
- case p.check(lexer.TokenID):
- // Try to parse as class definition or relation
- return p.parseClassOrRelation()
- default:
- token := p.peek()
- return p.error(fmt.Sprintf("unexpected token: %s", token.Value))
- }
- }
- // parseClass parses class statements
- func (p *ClassParser) parseClass() error {
- // Consume 'class' token
- if p.check(lexer.TokenClass) {
- p.advance() // consume TokenClass
- } else {
- p.advance() // consume 'class' keyword
- }
- if !p.check(lexer.TokenID) {
- return p.error("expected class name")
- }
- className := p.advance().Value
- class := &ast.ClassNode{
- ID: className,
- Label: className,
- Type: ast.ClassTypeClass,
- Members: make([]*ast.ClassMember, 0),
- Methods: make([]*ast.ClassMethod, 0),
- Annotations: make([]string, 0),
- CssClasses: make([]string, 0),
- }
- // Check for class body
- if p.check(lexer.TokenOpenBrace) {
- p.advance() // consume '{'
- err := p.parseClassBody(class)
- if err != nil {
- return err
- }
- if !p.check(lexer.TokenCloseBrace) {
- return p.error("expected '}'")
- }
- p.advance() // consume '}'
- }
- p.diagram.Classes[className] = class
- return nil
- }
- // parseClassBody parses the contents of a class body
- func (p *ClassParser) parseClassBody(class *ast.ClassNode) error {
- for !p.check(lexer.TokenCloseBrace) && !p.isAtEnd() {
- if p.check(lexer.TokenNewline) {
- p.advance()
- continue
- }
- // Parse member or method
- visibility := ast.VisibilityPublic // default
- if p.checkVisibility() {
- switch p.peek().Value {
- case "+":
- visibility = ast.VisibilityPublic
- case "-":
- visibility = ast.VisibilityPrivate
- case "#":
- visibility = ast.VisibilityProtected
- case "~":
- visibility = ast.VisibilityPackage
- }
- p.advance()
- }
- // Check for annotations like <<interface>>, <<abstract>>
- if p.check(lexer.TokenOpenAngle) {
- // Look ahead to see if it's << (double angle)
- if p.checkNext(lexer.TokenOpenAngle) {
- p.advance() // consume first <
- p.advance() // consume second <
- annotation := ""
- for !p.check(lexer.TokenCloseAngle) && !p.isAtEnd() {
- if p.check(lexer.TokenID) {
- annotation += p.advance().Value
- } else {
- annotation += p.advance().Value
- }
- }
- // Consume closing >>
- if p.check(lexer.TokenCloseAngle) {
- p.advance() // consume first >
- if p.check(lexer.TokenCloseAngle) {
- p.advance() // consume second >
- class.Annotations = append(class.Annotations, annotation)
- // Update class type based on annotation
- switch strings.ToLower(annotation) {
- case "interface":
- class.Type = ast.ClassTypeInterface
- case "abstract":
- class.Type = ast.ClassTypeAbstract
- case "enumeration", "enum":
- class.Type = ast.ClassTypeEnum
- }
- }
- }
- continue
- }
- }
- if !p.check(lexer.TokenID) {
- return p.error("expected member or method name")
- }
- name := p.advance().Value
- // Check if it's a method (has parentheses)
- if p.check(lexer.TokenOpenParen) {
- method, err := p.parseMethod(name, visibility)
- if err != nil {
- return err
- }
- class.Methods = append(class.Methods, method)
- } else {
- // It's a member
- member, err := p.parseMember(name, visibility)
- if err != nil {
- return err
- }
- class.Members = append(class.Members, member)
- }
- }
- return nil
- }
- // parseMethod parses a method with parameters and return type
- func (p *ClassParser) parseMethod(name string, visibility ast.MemberVisibility) (*ast.ClassMethod, error) {
- method := &ast.ClassMethod{
- Name: name,
- Parameters: make([]string, 0),
- Visibility: visibility,
- }
- // Parse opening parenthesis
- if !p.check(lexer.TokenOpenParen) {
- return nil, p.error("expected '('")
- }
- p.advance() // consume '('
- // Parse parameters
- for !p.check(lexer.TokenCloseParen) && !p.isAtEnd() {
- if p.check(lexer.TokenID) {
- param := p.advance().Value
- // Check for parameter type
- if p.check(lexer.TokenID) {
- paramType := p.advance().Value
- method.Parameters = append(method.Parameters, param+" "+paramType)
- } else {
- method.Parameters = append(method.Parameters, param)
- }
- }
- // Skip commas
- if p.check(lexer.TokenComma) {
- p.advance()
- }
- }
- if !p.check(lexer.TokenCloseParen) {
- return nil, p.error("expected ')'")
- }
- p.advance() // consume ')'
- // Check for return type
- if p.check(lexer.TokenID) {
- returnType := p.advance().Value
- method.Type = returnType
- }
- return method, nil
- }
- // parseMember parses a class member/field
- func (p *ClassParser) parseMember(name string, visibility ast.MemberVisibility) (*ast.ClassMember, error) {
- member := &ast.ClassMember{
- Name: name,
- Visibility: visibility,
- }
- // Check for type annotation
- if p.check(lexer.TokenID) {
- memberType := p.advance().Value
- member.Type = memberType
- }
- return member, nil
- }
- // parseClassOrRelation parses either a class definition or relationship
- func (p *ClassParser) parseClassOrRelation() error {
- className := p.advance().Value
- // Ensure class exists
- p.ensureClass(className)
- // Check for relationship operators
- if p.checkRelation() {
- return p.parseRelation(className)
- }
- // Check for class body
- if p.check(lexer.TokenOpenBrace) {
- class := p.diagram.Classes[className]
- p.advance() // consume '{'
- err := p.parseClassBody(class)
- if err != nil {
- return err
- }
- if !p.check(lexer.TokenCloseBrace) {
- return p.error("expected '}'")
- }
- p.advance() // consume '}'
- }
- return nil
- }
- // parseRelation parses class relationships
- func (p *ClassParser) parseRelation(fromClass string) error {
- relationType := p.parseRelationType()
- if relationType == "" {
- return p.error("expected relationship operator")
- }
- if !p.check(lexer.TokenID) {
- return p.error("expected target class")
- }
- toClass := p.advance().Value
- // Ensure target class exists
- p.ensureClass(toClass)
- relation := &ast.ClassRelation{
- From: fromClass,
- To: toClass,
- Type: relationType,
- }
- // Check for 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, " "))
- relation.Label = &label
- }
- }
- p.diagram.Relations = append(p.diagram.Relations, relation)
- return nil
- }
- // parseRelationType parses relationship type tokens
- func (p *ClassParser) parseRelationType() ast.ClassRelationType {
- token := p.peek()
- // Check for direct arrow tokens
- if p.check(lexer.TokenArrowSolid) {
- p.advance() // consume -->
- return ast.RelationAssociation
- }
- if p.check(lexer.TokenArrowDotted) {
- p.advance() // consume -.->
- return ast.RelationDependency
- }
- // Check for inheritance: --|>
- if token.Value == "--" && p.checkNext(lexer.TokenPipe) && p.checkAt(2, lexer.TokenCloseAngle) {
- p.advance() // --
- p.advance() // |
- p.advance() // >
- return ast.RelationInheritance
- }
- // Check for composition: --*
- if token.Value == "--" && p.checkNextValue("*") {
- p.advance() // --
- p.advance() // *
- return ast.RelationComposition
- }
- // Check for aggregation: --o
- if token.Value == "--" && p.checkNextValue("o") {
- p.advance() // --
- p.advance() // o
- return ast.RelationAggregation
- }
- // Check for association: -->
- if token.Value == "--" && p.checkNext(lexer.TokenCloseAngle) {
- p.advance() // --
- p.advance() // >
- return ast.RelationAssociation
- }
- // Check for realization: ..|>
- if token.Value == ".." && p.checkNext(lexer.TokenPipe) && p.checkAt(2, lexer.TokenCloseAngle) {
- p.advance() // ..
- p.advance() // |
- p.advance() // >
- return ast.RelationRealization
- }
- // Check for dependency: ..>
- if token.Value == ".." && p.checkNext(lexer.TokenCloseAngle) {
- p.advance() // ..
- p.advance() // >
- return ast.RelationDependency
- }
- return ""
- }
- // parseDirection parses direction statements
- func (p *ClassParser) parseDirection() error {
- p.advance() // consume 'direction'
- // Check for direction tokens or ID
- var direction string
- if p.check(lexer.TokenTD) || p.check(lexer.TokenTB) || p.check(lexer.TokenBT) ||
- p.check(lexer.TokenRL) || p.check(lexer.TokenLR) {
- direction = p.advance().Value
- } else if p.check(lexer.TokenID) {
- direction = p.advance().Value
- } else {
- return p.error("expected direction value (TD, TB, BT, RL, LR)")
- }
- p.diagram.Direction = direction
- return nil
- }
- // parseLink, parseClick, parseNote, parseClassDef - placeholder implementations
- func (p *ClassParser) parseLink() error {
- return p.skipToNextStatement()
- }
- func (p *ClassParser) parseClick() error {
- return p.skipToNextStatement()
- }
- func (p *ClassParser) parseClassDef() error {
- return p.skipToNextStatement()
- }
- // ensureClass ensures a class exists, creating it if needed
- func (p *ClassParser) ensureClass(id string) {
- if _, exists := p.diagram.Classes[id]; !exists {
- class := &ast.ClassNode{
- ID: id,
- Label: id,
- Type: ast.ClassTypeClass,
- Members: make([]*ast.ClassMember, 0),
- Methods: make([]*ast.ClassMethod, 0),
- Annotations: make([]string, 0),
- CssClasses: make([]string, 0),
- }
- p.diagram.Classes[id] = class
- }
- }
- // Helper methods
- func (p *ClassParser) check(tokenType lexer.TokenType) bool {
- if p.isAtEnd() {
- return false
- }
- return p.peek().Type == tokenType
- }
- func (p *ClassParser) checkNext(tokenType lexer.TokenType) bool {
- if p.current+1 >= len(p.tokens) {
- return false
- }
- return p.tokens[p.current+1].Type == tokenType
- }
- func (p *ClassParser) checkAt(offset int, tokenType lexer.TokenType) bool {
- if p.current+offset >= len(p.tokens) {
- return false
- }
- return p.tokens[p.current+offset].Type == tokenType
- }
- func (p *ClassParser) checkNextValue(value string) bool {
- if p.current+1 >= len(p.tokens) {
- return false
- }
- return p.tokens[p.current+1].Value == value
- }
- func (p *ClassParser) 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 *ClassParser) checkVisibility() bool {
- if p.isAtEnd() {
- return false
- }
- token := p.peek()
- return (token.Value == "+" || token.Value == "-" || token.Value == "#" || token.Value == "~") ||
- (token.Type == lexer.TokenPlus || token.Type == lexer.TokenMinus ||
- token.Type == lexer.TokenHash || token.Type == lexer.TokenTilde)
- }
- func (p *ClassParser) checkRelation() bool {
- token := p.peek()
- // Check for various relation operators
- return token.Value == "--" || token.Value == ".." ||
- p.check(lexer.TokenArrowSolid) || p.check(lexer.TokenArrowDotted) ||
- token.Value == "--|>" || token.Value == "--*" || token.Value == "--o"
- }
- func (p *ClassParser) advance() lexer.Token {
- if !p.isAtEnd() {
- p.current++
- }
- return p.previous()
- }
- func (p *ClassParser) isAtEnd() bool {
- return p.current >= len(p.tokens) || p.peek().Type == lexer.TokenEOF
- }
- func (p *ClassParser) peek() lexer.Token {
- if p.current >= len(p.tokens) {
- return lexer.Token{Type: lexer.TokenEOF}
- }
- return p.tokens[p.current]
- }
- func (p *ClassParser) previous() lexer.Token {
- if p.current <= 0 {
- return lexer.Token{Type: lexer.TokenEOF}
- }
- return p.tokens[p.current-1]
- }
- func (p *ClassParser) 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)
- }
- func (p *ClassParser) skipToNextStatement() error {
- for !p.isAtEnd() && !p.check(lexer.TokenNewline) {
- p.advance()
- }
- if p.check(lexer.TokenNewline) {
- p.advance()
- }
- return nil
- }
- // checkComment checks if current token sequence is a comment (%%)
- func (p *ClassParser) checkComment() bool {
- return p.check(lexer.TokenPercent) && p.checkNext(lexer.TokenPercent)
- }
- // parseComment parses comment statements
- func (p *ClassParser) parseComment() error {
- p.advance() // consume first %
- p.advance() // consume second %
- // Skip everything until newline
- for !p.check(lexer.TokenNewline) && !p.isAtEnd() {
- p.advance()
- }
- if p.check(lexer.TokenNewline) {
- p.advance()
- }
- return nil
- }
- // Enhanced parseNote to support class-specific notes
- func (p *ClassParser) parseNote() error {
- p.advance() // consume 'note'
- var note *ast.ClassNote
- // Check if it's "note for ClassName"
- if p.checkKeyword("for") {
- p.advance() // consume 'for'
- if !p.check(lexer.TokenID) {
- return p.error("expected class name after 'note for'")
- }
- className := p.advance().Value
- // Parse note text
- text := ""
- for !p.check(lexer.TokenNewline) && !p.isAtEnd() {
- token := p.advance()
- if token.Type == lexer.TokenString {
- // Remove quotes from string tokens
- val := token.Value
- text += val[1 : len(val)-1] // Remove surrounding quotes
- } else {
- text += token.Value + " "
- }
- }
- note = &ast.ClassNote{
- ForClass: &className,
- Text: strings.TrimSpace(text),
- }
- } else {
- // General note for the whole diagram
- text := ""
- for !p.check(lexer.TokenNewline) && !p.isAtEnd() {
- token := p.advance()
- if token.Type == lexer.TokenString {
- // Remove quotes from string tokens
- val := token.Value
- text += val[1 : len(val)-1] // Remove surrounding quotes
- } else {
- text += token.Value + " "
- }
- }
- note = &ast.ClassNote{
- Text: strings.TrimSpace(text),
- }
- }
- p.diagram.Notes = append(p.diagram.Notes, note)
- return nil
- }
- // parseGeneric parses generic type parameters with ~Type~
- func (p *ClassParser) parseGeneric() (*ast.Generic, error) {
- if !p.check(lexer.TokenTilde) {
- return nil, p.error("expected '~' for generic type")
- }
- p.advance() // consume ~
- if !p.check(lexer.TokenID) {
- return nil, p.error("expected generic type name")
- }
- typeName := p.advance().Value
- generic := &ast.Generic{
- Name: typeName,
- Arguments: make([]*ast.Generic, 0),
- }
- // Check for nested generics like List<String>
- if p.check(lexer.TokenOpenAngle) {
- p.advance() // consume <
- for !p.check(lexer.TokenCloseAngle) && !p.isAtEnd() {
- if p.check(lexer.TokenTilde) {
- nestedGeneric, err := p.parseGeneric()
- if err != nil {
- return nil, err
- }
- generic.Arguments = append(generic.Arguments, nestedGeneric)
- } else if p.check(lexer.TokenID) {
- // Simple type argument
- argType := p.advance().Value
- generic.Arguments = append(generic.Arguments, &ast.Generic{
- Name: argType,
- })
- }
- if p.check(lexer.TokenComma) {
- p.advance() // consume comma
- }
- }
- if !p.check(lexer.TokenCloseAngle) {
- return nil, p.error("expected '>' to close generic")
- }
- p.advance() // consume >
- }
- if !p.check(lexer.TokenTilde) {
- return nil, p.error("expected closing '~' for generic type")
- }
- p.advance() // consume closing ~
- return generic, nil
- }
|