From 26bdaeb8c2adc00080e2affe1bc92578f6a85d90 Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Thu, 30 Jul 2026 11:29:27 +0800 Subject: [PATCH 1/5] support sequence lifecycle commands --- .../puml_parser/src/grammar/sequence.pest | 33 +-- .../src/sequence_diagram/src/lib.rs | 4 +- .../src/sequence_diagram/src/sequence_ast.rs | 26 ++- .../sequence_diagram/src/sequence_parser.rs | 204 ++++++++++++------ .../test/sequence_integration_test.rs | 10 + .../create_participants.puml | 19 ++ .../create_participants/output.json | 49 +++++ .../lifecycle_commands.puml | 22 ++ .../lifecycle_commands/output.json | 49 +++++ .../participant_identifiers/output.json | 17 +- .../participant_identifiers.puml | 3 - .../sequence_diagram/src/sequence_resolver.rs | 36 +++- 12 files changed, 358 insertions(+), 114 deletions(-) create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/create_participants.puml create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/output.json create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/lifecycle_commands.puml create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/output.json diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest index 4d59c2cf..9f3eb895 100644 --- a/plantuml/parser/puml_parser/src/grammar/sequence.pest +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -24,7 +24,8 @@ sequence_statement = { sprite_inline | sprite_block_start | hide_show_member | hide_show_stereotype | participant_def | - activate_cmd | deactivate_cmd | destroy_cmd | create_cmd | + message | + lifecycle_cmd | box_start | box_end | group_cmd | divider | delay | @@ -32,9 +33,7 @@ sequence_statement = { ref_inline | ref_block_start | skin | autonumber | autonumber_stop | autonumber_resume | autonumber_inc | autoactivate | footbox_cmd | ellipsis | - function_def | function_return | function_end | - message | - activation_short + function_def | function_return | function_end ) ~ EOL } @@ -106,13 +105,11 @@ stereotype_elem = { // Participant definitions participant_def = { - create_kw? - ~ participant_type + participant_type ~ participant_identifier ~ stereotype? ~ order_clause? ~ color_spec? } -create_kw = { ^"create" } participant_type = @{ (^"participant" | ^"collections" | ^"database" | ^"boundary" | ^"control" | ^"entity" | ^"queue" | ^"actor") ~ &(" " | "\t" | "\n" | "\r") @@ -145,11 +142,24 @@ color_spec = @{ BASIC_COLOR } order_clause = { ^"order" ~ signed_number } signed_number = { "-"? ~ NUMBER } -// Activate/Deactivate/Destroy/Create commands -activate_cmd = { ^"activate" ~ participant_ref } +// Lifecycle commands +lifecycle_cmd = { + create_cmd + | activate_cmd + | deactivate_cmd + | destroy_cmd + | activation_short +} + +create_cmd = { + ^"create" + ~ participant_type? + ~ participant_identifier + ~ stereotype? ~ order_clause? ~ color_spec? +} +activate_cmd = { ^"activate" ~ participant_ref ~ color_spec? } deactivate_cmd = { ^"deactivate" ~ participant_ref? } destroy_cmd = { ^"destroy" ~ participant_ref } -create_cmd = { ^"create" ~ participant_ref } // Sequence Arrow SEQUENCE_ARROW_PREFIX_CHAR = _{ "<" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" } @@ -206,8 +216,7 @@ deactivate_suffix = { "--" } create_suffix = { "**" } destroy_suffix = { "!!" } -// Short activation syntax -activation_short = { participant_ref ~ ("++" | "--") } +activation_short = { participant_ref ~ (activate_suffix | deactivate_suffix) } participant_ref = { CNAME | quoted_string } // Return diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs index 58bcda12..90d8756b 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs @@ -16,8 +16,8 @@ mod sequence_parser; pub use sequence_ast::{ ActivateCmd, Arrow, CreateCmd, DeactivateCmd, DestroyCmd, GroupCmd, GroupType, Message, - MessageEndpoint, MessageSuffix, ParticipantIdentifier, ParticipantType, SeqPumlDocument, - Statement, + MessageEndpoint, MessageSuffix, ParticipantIdentifier, ParticipantRef, ParticipantType, + SeqPumlDocument, Statement, }; pub use sequence_parser::{PumlSequenceParser, SequenceError}; diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs index 099233f7..6d074efe 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs @@ -30,8 +30,8 @@ pub struct SeqPumlDocument { // expected to be revisited as the sequence parser/resolver model settles. #[allow(clippy::large_enum_variant)] pub enum Statement { - DestroyCmd(DestroyCmd), CreateCmd(CreateCmd), + DestroyCmd(DestroyCmd), ActivateCmd(ActivateCmd), DeactivateCmd(DeactivateCmd), ParticipantDef(ParticipantDef), @@ -42,8 +42,14 @@ pub enum Statement { // Participant definitions #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ParticipantDef { - #[serde(default)] - pub is_create: bool, + pub participant_type: ParticipantType, + pub identifier: ParticipantIdentifier, + pub stereotype: Option, + pub source_location: SourceLocation, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CreateCmd { pub participant_type: ParticipantType, pub identifier: ParticipantIdentifier, pub stereotype: Option, @@ -68,26 +74,24 @@ pub struct ParticipantIdentifier { pub alias: Option, } -// Destroy/Create commands #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct DestroyCmd { - pub participant: String, +pub struct ParticipantRef { + pub identifier: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CreateCmd { - pub participant: String, +pub struct DestroyCmd { + pub participant: ParticipantRef, } -// Activate/Deactivate commands #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ActivateCmd { - pub participant: String, + pub participant: ParticipantRef, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DeactivateCmd { - pub participant: Option, + pub participant: ParticipantRef, } // Messages (internal parsing structure) diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs index 21487db0..4a37a7e4 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs @@ -71,6 +71,7 @@ impl PumlSequenceParser { Rule::participant_def => Ok(vec![Statement::ParticipantDef( Self::parse_participant_def(inner, source_location)?, )]), + Rule::lifecycle_cmd => Self::parse_lifecycle_cmd(inner, source_location), Rule::message => Ok(vec![Statement::Message(Self::parse_message( inner, source_location, @@ -79,16 +80,36 @@ impl PumlSequenceParser { inner, source_location, )?)]), + // Grammar-valid directives that are intentionally not modeled as statements + _ => Ok(vec![]), + } + } + + fn parse_lifecycle_cmd( + pair: pest::iterators::Pair, + source_location: SourceLocation, + ) -> Result, SequenceError> { + let inner = pair.into_inner().next().ok_or_else(|| { + SequenceError::InvalidStatement("empty lifecycle command".to_string()) + })?; + + match inner.as_rule() { + Rule::create_cmd => Ok(vec![Statement::CreateCmd(Self::parse_create_cmd( + inner, + source_location, + )?)]), Rule::destroy_cmd => Ok(vec![Statement::DestroyCmd(Self::parse_destroy_cmd(inner)?)]), - Rule::create_cmd => Ok(vec![Statement::CreateCmd(Self::parse_create_cmd(inner)?)]), Rule::activate_cmd => Ok(vec![Statement::ActivateCmd(Self::parse_activate_cmd( inner, )?)]), Rule::deactivate_cmd => Ok(vec![Statement::DeactivateCmd(Self::parse_deactivate_cmd( inner, )?)]), - // Grammar-valid directives that are intentionally not modeled as statements - _ => Ok(vec![]), + Rule::activation_short => Self::parse_activation_short(inner), + _ => Err(SequenceError::InvalidStatement(format!( + "unsupported lifecycle command: {:?}", + inner.as_rule() + ))), } } @@ -96,16 +117,12 @@ impl PumlSequenceParser { pair: pest::iterators::Pair, source_location: SourceLocation, ) -> Result { - let mut is_create = false; let mut participant_type: Option = None; let mut identifier: Option = None; let mut stereotype: Option = None; for inner in pair.into_inner() { match inner.as_rule() { - Rule::create_kw => { - is_create = true; - } Rule::participant_type => { participant_type = Some(Self::parse_participant_type(inner)?); } @@ -123,7 +140,6 @@ impl PumlSequenceParser { } Ok(ParticipantDef { - is_create, participant_type: participant_type.ok_or_else(|| { SequenceError::InvalidStatement("missing participant type".to_string()) })?, @@ -138,10 +154,11 @@ impl PumlSequenceParser { fn parse_participant_identifier( pair: pest::iterators::Pair, ) -> Result { - let participant = pair - .into_inner() - .next() - .expect("participant_identifier must contain a participant identifier"); + let participant = pair.into_inner().next().ok_or_else(|| { + SequenceError::InvalidStatement( + "participant_identifier must contain a participant identifier".to_string(), + ) + })?; let participant_rule = participant.as_rule(); Ok(match participant_rule { @@ -264,7 +281,7 @@ impl PumlSequenceParser { arrow = Some(Self::parse_arrow(inner)?); } Rule::message_suffix => { - suffix = Some(Self::parse_message_suffix(inner)); + suffix = Some(Self::parse_message_suffix(inner)?); } Rule::sequence_description => { description = inner @@ -277,11 +294,15 @@ impl PumlSequenceParser { } Ok(Message { - left: left.expect("message must contain left endpoint"), + left: left.ok_or_else(|| { + SequenceError::InvalidStatement("message must contain left endpoint".to_string()) + })?, arrow: arrow.ok_or_else(|| { SequenceError::InvalidStatement("missing arrow in message".to_string()) })?, - right: right.expect("message must contain right endpoint"), + right: right.ok_or_else(|| { + SequenceError::InvalidStatement("message must contain right endpoint".to_string()) + })?, suffix, description, source_location, @@ -291,56 +312,61 @@ impl PumlSequenceParser { fn parse_message_endpoint( pair: pest::iterators::Pair, ) -> Result { - let endpoint = pair - .into_inner() - .next() - .expect("message_endpoint must contain an endpoint"); + let endpoint = pair.into_inner().next().ok_or_else(|| { + SequenceError::InvalidStatement( + "message_endpoint must contain an endpoint".to_string(), + ) + })?; Ok(match endpoint.as_rule() { Rule::inline_participant => { MessageEndpoint::Participant(Self::parse_participant_identifier(endpoint)?) } Rule::lost_found_marker => { - MessageEndpoint::LostFound(Self::parse_lost_found_marker(endpoint)) + MessageEndpoint::LostFound(Self::parse_lost_found_marker(endpoint)?) + } + _ => { + return Err(SequenceError::InvalidStatement(format!( + "message_endpoint grammar produced unsupported value: {:?}", + endpoint.as_rule() + ))); } - _ => unreachable!( - "message_endpoint grammar produced unsupported value: {:?}", - endpoint.as_rule() - ), }) } - fn parse_lost_found_marker(pair: pest::iterators::Pair) -> String { - let marker = pair - .into_inner() - .next() - .expect("lost_found_marker must contain a lost-found endpoint"); + fn parse_lost_found_marker(pair: pest::iterators::Pair) -> Result { + let marker = pair.into_inner().next().ok_or_else(|| { + SequenceError::InvalidStatement( + "lost_found_marker must contain a lost-found endpoint".to_string(), + ) + })?; match marker.as_rule() { - Rule::left_lost_found | Rule::right_lost_found => marker.as_str().trim().to_string(), - _ => unreachable!( + Rule::left_lost_found | Rule::right_lost_found => Ok(marker.as_str().trim().to_string()), + _ => Err(SequenceError::InvalidStatement(format!( "lost_found_marker grammar produced unsupported value: {:?}", marker.as_rule() - ), + ))), } } - fn parse_message_suffix(pair: pest::iterators::Pair) -> MessageSuffix { - let suffix = pair - .into_inner() - .next() - .expect("message_suffix must contain a suffix"); + fn parse_message_suffix(pair: pest::iterators::Pair) -> Result { + let suffix = pair.into_inner().next().ok_or_else(|| { + SequenceError::InvalidStatement("message_suffix must contain a suffix".to_string()) + })?; - match suffix.as_rule() { + Ok(match suffix.as_rule() { Rule::activate_suffix => MessageSuffix::Activate, Rule::deactivate_suffix => MessageSuffix::Deactivate, Rule::create_suffix => MessageSuffix::Create, Rule::destroy_suffix => MessageSuffix::Destroy, - _ => unreachable!( - "message_suffix grammar produced unsupported value: {:?}", - suffix.as_rule() - ), - } + _ => { + return Err(SequenceError::InvalidStatement(format!( + "message_suffix grammar produced unsupported value: {:?}", + suffix.as_rule() + ))); + } + }) } fn parse_arrow(pair: pest::iterators::Pair) -> Result { @@ -394,11 +420,11 @@ impl PumlSequenceParser { } fn parse_destroy_cmd(pair: pest::iterators::Pair) -> Result { - let mut participant: Option = None; + let mut participant: Option = None; for inner in pair.into_inner() { if inner.as_rule() == Rule::participant_ref { - participant = Some(Self::extract_participant_ref(inner)); + participant = Some(Self::parse_participant_ref(inner)); } } @@ -409,50 +435,106 @@ impl PumlSequenceParser { }) } - fn parse_create_cmd(pair: pest::iterators::Pair) -> Result { - let mut participant: Option = None; + fn parse_activate_cmd(pair: pest::iterators::Pair) -> Result { + let mut participant: Option = None; for inner in pair.into_inner() { if inner.as_rule() == Rule::participant_ref { - participant = Some(Self::extract_participant_ref(inner)); + participant = Some(Self::parse_participant_ref(inner)); } } - Ok(CreateCmd { + Ok(ActivateCmd { participant: participant.ok_or_else(|| { - SequenceError::InvalidStatement("missing participant in create".to_string()) + SequenceError::InvalidStatement("missing participant in activate".to_string()) })?, }) } - fn parse_activate_cmd(pair: pest::iterators::Pair) -> Result { - let mut participant: Option = None; + fn parse_deactivate_cmd( + pair: pest::iterators::Pair, + ) -> Result { + let mut participant: Option = None; for inner in pair.into_inner() { if inner.as_rule() == Rule::participant_ref { - participant = Some(Self::extract_participant_ref(inner)); + participant = Some(Self::parse_participant_ref(inner)); } } - Ok(ActivateCmd { + Ok(DeactivateCmd { participant: participant.ok_or_else(|| { - SequenceError::InvalidStatement("missing participant in activate".to_string()) + SequenceError::InvalidStatement("missing participant in deactivate".to_string()) })?, }) } - fn parse_deactivate_cmd( + fn parse_create_cmd( pair: pest::iterators::Pair, - ) -> Result { - let mut participant: Option = None; + source_location: SourceLocation, + ) -> Result { + let mut participant_type: Option = None; + let mut identifier: Option = None; + let mut stereotype: Option = None; for inner in pair.into_inner() { - if inner.as_rule() == Rule::participant_ref { - participant = Some(Self::extract_participant_ref(inner)); + match inner.as_rule() { + Rule::participant_type => { + participant_type = Some(Self::parse_participant_type(inner)?); + } + Rule::participant_identifier => { + identifier = Some(Self::parse_participant_identifier(inner)?); + } + Rule::stereotype => { + stereotype = Some(Self::extract_stereotype(inner.as_str())); + } + Rule::order_clause => {} + _ => {} } } - Ok(DeactivateCmd { participant }) + Ok(CreateCmd { + participant_type: participant_type.unwrap_or(ParticipantType::Participant), + identifier: identifier.ok_or_else(|| { + SequenceError::InvalidStatement("missing participant identifier".to_string()) + })?, + stereotype, + source_location, + }) + } + + fn parse_activation_short( + pair: pest::iterators::Pair, + ) -> Result, SequenceError> { + let mut parts = pair.into_inner(); + let participant = parts + .next() + .filter(|part| part.as_rule() == Rule::participant_ref) + .map(Self::parse_participant_ref) + .ok_or_else(|| { + SequenceError::InvalidStatement( + "missing participant in short activation".to_string(), + ) + })?; + + match parts.next().map(|part| part.as_rule()).ok_or_else(|| { + SequenceError::InvalidStatement("missing short activation suffix".to_string()) + })? { + Rule::activate_suffix => Ok(vec![Statement::ActivateCmd(ActivateCmd { participant })]), + Rule::deactivate_suffix => Ok(vec![Statement::DeactivateCmd(DeactivateCmd { + participant, + })]), + other => Err(SequenceError::InvalidStatement(format!( + "unsupported short activation suffix: {:?}", + other + ))), + } + } + + fn parse_participant_ref(pair: pest::iterators::Pair) -> ParticipantRef { + ParticipantRef { + identifier: Self::extract_participant_ref(pair), + } } // Helper functions diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs index 556c0862..cedef54b 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs @@ -75,3 +75,13 @@ fn test_message_participants() { fn test_message_lost_found() { run_sequence_diagram_parser_case("message_lost_found"); } + +#[test] +fn test_create_participants() { + run_sequence_diagram_parser_case("create_participants"); +} + +#[test] +fn test_lifecycle_commands() { + run_sequence_diagram_parser_case("lifecycle_commands"); +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/create_participants.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/create_participants.puml new file mode 100644 index 00000000..001101fc --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/create_participants.puml @@ -0,0 +1,19 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml create_participants + +create participant "Created Worker" as Worker +create Other +create control String + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/output.json new file mode 100644 index 00000000..a650ea00 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/create_participants/output.json @@ -0,0 +1,49 @@ +{ + "create_participants.puml": { + "name": "create_participants", + "statements": [ + { + "CreateCmd": { + "participant_type": "Participant", + "identifier": { + "display_name": "Created Worker", + "alias": "Worker" + }, + "stereotype": null, + "source_location": { + "file": "", + "line": 15 + } + } + }, + { + "CreateCmd": { + "participant_type": "Participant", + "identifier": { + "display_name": "Other", + "alias": null + }, + "stereotype": null, + "source_location": { + "file": "", + "line": 16 + } + } + }, + { + "CreateCmd": { + "participant_type": "Control", + "identifier": { + "display_name": "String", + "alias": null + }, + "stereotype": null, + "source_location": { + "file": "", + "line": 17 + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/lifecycle_commands.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/lifecycle_commands.puml new file mode 100644 index 00000000..ccd1f20d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/lifecycle_commands.puml @@ -0,0 +1,22 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml lifecycle_commands + +activate alice +activate bob #Gold +deactivate alice +destroy bob +peter++ +peter -- + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/output.json new file mode 100644 index 00000000..55c6431f --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/lifecycle_commands/output.json @@ -0,0 +1,49 @@ +{ + "lifecycle_commands.puml": { + "name": "lifecycle_commands", + "statements": [ + { + "ActivateCmd": { + "participant": { + "identifier": "alice" + } + } + }, + { + "ActivateCmd": { + "participant": { + "identifier": "bob" + } + } + }, + { + "DeactivateCmd": { + "participant": { + "identifier": "alice" + } + } + }, + { + "DestroyCmd": { + "participant": { + "identifier": "bob" + } + } + }, + { + "ActivateCmd": { + "participant": { + "identifier": "peter" + } + } + }, + { + "DeactivateCmd": { + "participant": { + "identifier": "peter" + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/output.json index beaab559..6ace0f3d 100644 --- a/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/output.json +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/output.json @@ -72,21 +72,6 @@ } } }, - { - "ParticipantDef": { - "is_create": true, - "participant_type": "Participant", - "identifier": { - "display_name": "Created Worker", - "alias": "Worker" - }, - "stereotype": null, - "source_location": { - "file": "", - "line": 23 - } - } - }, { "ParticipantDef": { "participant_type": "Participant", @@ -97,7 +82,7 @@ "stereotype": "component", "source_location": { "file": "", - "line": 26 + "line": 23 } } } diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/participant_identifiers.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/participant_identifiers.puml index aa5397d2..18e9c986 100644 --- a/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/participant_identifiers.puml +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/participant_identifiers/participant_identifiers.puml @@ -19,9 +19,6 @@ boundary Gateway as GatewayAlias control Controller queue "Work Queue" -' create participant declaration -create participant "Created Worker" as Worker - ' participant declaration with stereotype and order participant LastOrder <> order 1 diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs index f70be12e..5749cf9b 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/src/sequence_resolver.rs @@ -17,7 +17,7 @@ use sequence_logic::{ ParticipantType as LogicParticipantType, SequenceParticipant, SequenceTree, SourceLocation, }; use sequence_parser::sequence_ast::{ - MessageEndpoint, ParticipantDef, ParticipantIdentifier, + CreateCmd, MessageEndpoint, ParticipantDef, ParticipantIdentifier, ParticipantType as SyntaxParticipantType, Statement, }; use sequence_parser::SeqPumlDocument; @@ -76,12 +76,22 @@ fn add_explicit_participants( resolved_names: &mut HashSet, ) { for stmt in statements { - if let Statement::ParticipantDef(participant_def) = stmt { - add_participant( - participants, - resolved_names, - explicit_participant(participant_def), - ); + match stmt { + Statement::ParticipantDef(participant_def) => { + add_participant( + participants, + resolved_names, + explicit_participant(participant_def), + ); + } + Statement::CreateCmd(create_cmd) => { + add_participant( + participants, + resolved_names, + created_participant(create_cmd), + ); + } + _ => {} } } } @@ -150,6 +160,16 @@ fn explicit_participant(participant_def: &ParticipantDef) -> SequenceParticipant } } +fn created_participant(create_cmd: &CreateCmd) -> SequenceParticipant { + SequenceParticipant { + display_name: create_cmd.identifier.display_name.clone(), + alias: create_cmd.identifier.alias.clone(), + participant_type: map_parser_participant_type(&create_cmd.participant_type), + source_location: create_cmd.source_location.clone(), + stereotype: create_cmd.stereotype.clone(), + } +} + fn implicit_participant( identifier: &ParticipantIdentifier, source_location: &SourceLocation, @@ -315,7 +335,6 @@ mod sequence_resolver_tests { fn make_participant(name: &str) -> Statement { Statement::ParticipantDef(ParticipantDef { - is_create: false, participant_type: SyntaxParticipantType::Participant, identifier: ParticipantIdentifier { display_name: name.to_string(), @@ -328,7 +347,6 @@ mod sequence_resolver_tests { fn make_participant_with_alias(display_name: &str, alias: &str) -> Statement { Statement::ParticipantDef(ParticipantDef { - is_create: false, participant_type: SyntaxParticipantType::Participant, identifier: ParticipantIdentifier { display_name: display_name.to_string(), From 049c9a5037c9d2b3efdd24b1a3a791bfbaf02b2c Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Thu, 30 Jul 2026 11:22:29 +0800 Subject: [PATCH 2/5] support sequence ref over statements --- .../puml_parser/src/grammar/sequence.pest | 36 +++--- .../src/sequence_diagram/src/lib.rs | 2 +- .../src/sequence_diagram/src/sequence_ast.rs | 14 ++- .../sequence_diagram/src/sequence_parser.rs | 105 ++++++++++++------ .../test/sequence_integration_test.rs | 5 + .../ref_statement/output.json | 41 +++++++ .../ref_statement/ref_statement.puml | 21 ++++ 7 files changed, 167 insertions(+), 57 deletions(-) create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/output.json create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/ref_statement.puml diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest index 9f3eb895..4e3e4309 100644 --- a/plantuml/parser/puml_parser/src/grammar/sequence.pest +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -13,7 +13,7 @@ // PlantUML Sequence Diagram Grammar for Pest Parser -sequence_start = { empty_line* ~ startuml ~ (note_multiline | ref_multiline | sequence_statement | empty_line)* ~ enduml } +sequence_start = { empty_line* ~ startuml ~ (note_multiline | sequence_statement | empty_line)* ~ enduml } sequence_statement = { ( @@ -30,7 +30,7 @@ sequence_statement = { group_cmd | divider | delay | return_cmd | - ref_inline | ref_block_start | + ref_stmt | skin | autonumber | autonumber_stop | autonumber_resume | autonumber_inc | autoactivate | footbox_cmd | ellipsis | function_def | function_return | function_end @@ -217,7 +217,6 @@ create_suffix = { "**" } destroy_suffix = { "!!" } activation_short = { participant_ref ~ (activate_suffix | deactivate_suffix) } -participant_ref = { CNAME | quoted_string } // Return return_cmd = { parallel_marker? ~ ^"return" ~ sequence_text_content? } @@ -245,30 +244,31 @@ delay = { ("||" ~ NUMBER? ~ "|"+) | ("…" ~ sequence_text_content? ~ "…") | ( ellipsis = { ("..." ~ sequence_text_content? ~ "...") | ("…" ~ sequence_text_content? ~ "…") | "..." | "…" } // Reference +ref_stmt = { + ref_inline + | ref_block +} + ref_inline = { ^"ref" ~ ^"over" ~ participant_list ~ ":" ~ sequence_text_content } -ref_multiline = @{ - ^"ref" ~ - (!NEWLINE ~ ANY)* ~ // rest of header line ("over participant") - NEWLINE ~ - ( - !(WHITESPACE* ~ (^"endref" | (^"end" ~ WHITESPACE ~ ^"ref") | (^"end" ~ WHITESPACE* ~ NEWLINE))) ~ - (!NEWLINE ~ ANY)* ~ - NEWLINE - )* ~ - WHITESPACE* ~ (^"endref" | (^"end" ~ WHITESPACE ~ ^"ref") | (^"end" ~ WHITESPACE* ~ NEWLINE)) ~ - EOL? +ref_block = { + ^"ref" ~ ^"over" ~ participant_list ~ EOL + ~ ref_body + ~ ref_end } -ref_block_start = { - ^"ref" ~ ^"over" ~ participant_list +ref_body = { + ( + !(WHITESPACE* ~ ref_end) + ~ ANY + )* } - -ref_block_end = { ^"end" ~ WHITESPACE? ~ (^"ref")? } +ref_end = { ^"end" ~ ^"ref" } participant_list = { participant_ref ~ ("," ~ participant_ref)* } +participant_ref = { CNAME } // Skin skin = { ^"skin" ~ sequence_qualified_name } diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs index 90d8756b..a7df731e 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs @@ -17,7 +17,7 @@ mod sequence_parser; pub use sequence_ast::{ ActivateCmd, Arrow, CreateCmd, DeactivateCmd, DestroyCmd, GroupCmd, GroupType, Message, MessageEndpoint, MessageSuffix, ParticipantIdentifier, ParticipantRef, ParticipantType, - SeqPumlDocument, Statement, + RefCmd, SeqPumlDocument, Statement, }; pub use sequence_parser::{PumlSequenceParser, SequenceError}; diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs index 6d074efe..25479994 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs @@ -37,6 +37,7 @@ pub enum Statement { ParticipantDef(ParticipantDef), Message(Message), GroupCmd(GroupCmd), + RefCmd(RefCmd), } // Participant definitions @@ -94,6 +95,13 @@ pub struct DeactivateCmd { pub participant: ParticipantRef, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RefCmd { + pub participants: Vec, + pub text: Option, + pub source_location: SourceLocation, +} + // Messages (internal parsing structure) #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Message { @@ -119,12 +127,6 @@ pub enum MessageSuffix { Destroy, // !! } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ActivationType { - Activate, // ++ - Deactivate, // -- -} - // Group commands (alt, opt, loop, etc.) - internal parsing structure #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GroupCmd { diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs index 4a37a7e4..a52d0875 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs @@ -80,6 +80,10 @@ impl PumlSequenceParser { inner, source_location, )?)]), + Rule::ref_stmt => Ok(vec![Statement::RefCmd(Self::parse_ref_cmd( + inner, + source_location, + ))]), // Grammar-valid directives that are intentionally not modeled as statements _ => Ok(vec![]), } @@ -419,6 +423,48 @@ impl PumlSequenceParser { } } + fn parse_ref_cmd(pair: pest::iterators::Pair, source_location: SourceLocation) -> RefCmd { + let mut participants = Vec::new(); + let mut text = None; + + Self::extract_ref_parts(pair, &mut participants, &mut text); + + RefCmd { + participants, + text, + source_location, + } + } + + fn extract_ref_parts( + pair: pest::iterators::Pair, + participants: &mut Vec, + text: &mut Option, + ) { + match pair.as_rule() { + Rule::participant_list => { + participants.extend(pair.into_inner().filter_map(|inner| { + if inner.as_rule() == Rule::participant_ref { + Some(Self::parse_participant_ref(inner)) + } else { + None + } + })); + } + Rule::sequence_text_content | Rule::ref_body => { + let value = pair.as_str().trim(); + if !value.is_empty() { + *text = Some(value.to_string()); + } + } + _ => { + for inner in pair.into_inner() { + Self::extract_ref_parts(inner, participants, text); + } + } + } + } + fn parse_destroy_cmd(pair: pest::iterators::Pair) -> Result { let mut participant: Option = None; @@ -552,45 +598,16 @@ impl PumlSequenceParser { .to_string() } - /// Sequence participant names are stored as semantic identifiers, not as - /// source literals. Quotation marks are PlantUML delimiters and are not - /// part of the participant name in the parsed model. - fn normalize_participant_name(s: &str) -> String { - let value = s.trim(); - if value.starts_with('"') { - Self::extract_quoted_string(value) - } else { - value.to_string() - } - } - fn extract_participant_ref(pair: pest::iterators::Pair) -> String { match pair.as_rule() { Rule::participant_ref => { - let fallback = pair.as_str().trim(); - + let fallback = pair.as_str().trim().to_string(); pair.into_inner() .next() .map(Self::extract_participant_ref) - .unwrap_or_else(|| Self::normalize_participant_name(fallback)) + .unwrap_or(fallback) } - - Rule::quoted_string => Self::extract_quoted_string(pair.as_str()), - - Rule::CNAME => Self::normalize_participant_name(pair.as_str()), - - Rule::quoted_display_with_alias | Rule::display_with_alias => pair - .into_inner() - .nth(1) - .map(|p| p.as_str().trim().to_string()) - .unwrap_or_default(), - - Rule::alias_with_quoted_display => pair - .into_inner() - .next() - .map(|p| p.as_str().trim().to_string()) - .unwrap_or_default(), - + Rule::CNAME => pair.as_str().trim().to_string(), _ => pair.as_str().trim().to_string(), } } @@ -759,4 +776,28 @@ mod dispatch_style_tests { expected_file.as_str() ); } + + #[test] + fn test_statement_after_multiline_ref_is_preserved() { + let input = "@startuml\nref over Alice, Bob\n initialize service\nend ref\nAlice -> Bob : done\n@enduml"; + let mut parser = PumlSequenceParser; + let doc = parser + .parse_file(&Rc::new(PathBuf::from("t.puml")), input, LogLevel::Info) + .expect("statement after multiline ref must parse"); + + match &doc.statements[0] { + Statement::RefCmd(ref_cmd) => { + assert_eq!(ref_cmd.text.as_deref(), Some("initialize service")); + } + actual => panic!("expected ref statement, got {:?}", actual), + } + + match &doc.statements[1] { + Statement::Message(message) => { + assert_eq!(message.description.as_deref(), Some("done")); + assert_eq!(message.source_location.line, 5); + } + actual => panic!("expected message after ref statement, got {:?}", actual), + } + } } diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs index cedef54b..a71fa56e 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs @@ -85,3 +85,8 @@ fn test_create_participants() { fn test_lifecycle_commands() { run_sequence_diagram_parser_case("lifecycle_commands"); } + +#[test] +fn test_ref_statement() { + run_sequence_diagram_parser_case("ref_statement"); +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/output.json new file mode 100644 index 00000000..49b251a7 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/output.json @@ -0,0 +1,41 @@ +{ + "ref_statement.puml": { + "name": "ref_statement", + "statements": [ + { + "RefCmd": { + "participants": [ + { + "identifier": "Alice" + }, + { + "identifier": "Bob" + } + ], + "text": "initialize service", + "source_location": { + "file": "", + "line": 15 + } + } + }, + { + "RefCmd": { + "participants": [ + { + "identifier": "Alice" + }, + { + "identifier": "Bob" + } + ], + "text": "perform remote operation\n validate response", + "source_location": { + "file": "", + "line": 16 + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/ref_statement.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/ref_statement.puml new file mode 100644 index 00000000..3164b10d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/ref_statement/ref_statement.puml @@ -0,0 +1,21 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml ref_statement + +ref over Alice, Bob : initialize service +ref over Alice, Bob + perform remote operation + validate response +end ref + +@enduml From 6f6592d3f186d0b092ae655ade657d5194cc5165 Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Thu, 30 Jul 2026 11:32:18 +0800 Subject: [PATCH 3/5] support sequence message suffixes and missing endpoints --- .../puml_parser/src/grammar/sequence.pest | 36 ++- .../src/sequence_diagram/src/sequence_ast.rs | 1 + .../sequence_diagram/src/sequence_parser.rs | 136 ++++++--- .../test/sequence_integration_test.rs | 10 + .../message_lost_found.puml | 20 +- .../message_lost_found/output.json | 284 ++++++++++++++++-- .../message_missing_endpoints.puml | 20 ++ .../message_missing_endpoints/output.json | 123 ++++++++ .../message_suffixes/message_suffixes.puml | 19 ++ .../message_suffixes/output.json | 108 +++++++ 10 files changed, 679 insertions(+), 78 deletions(-) create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/message_missing_endpoints.puml create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/output.json create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/message_suffixes.puml create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/output.json diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest index 4e3e4309..191258c3 100644 --- a/plantuml/parser/puml_parser/src/grammar/sequence.pest +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -162,8 +162,8 @@ deactivate_cmd = { ^"deactivate" ~ participant_ref? } destroy_cmd = { ^"destroy" ~ participant_ref } // Sequence Arrow -SEQUENCE_ARROW_PREFIX_CHAR = _{ "<" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" } -SEQUENCE_ARROW_SUFFIX_CHAR = _{ ">" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" } +SEQUENCE_ARROW_PREFIX_CHAR = _{ "<" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" | "o" | "x" } +SEQUENCE_ARROW_SUFFIX_CHAR = _{ ">" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" | "o" | "x" } sequence_arrow_prefix = @{ SEQUENCE_ARROW_PREFIX_CHAR+ } sequence_arrow_suffix = @{ SEQUENCE_ARROW_SUFFIX_CHAR+ } @@ -178,9 +178,21 @@ sequence_arrow = { // Messages message = { - parallel_marker? ~ - message_endpoint ~ sequence_arrow ~ message_endpoint ~ - message_suffix? ~ sequence_description? + parallel_marker? ~ message_body ~ message_suffix? ~ sequence_description? +} +message_body = { + message_full + | message_missing_left + | message_missing_right +} +message_full = { + message_endpoint ~ sequence_arrow ~ message_endpoint +} +message_missing_left = { + sequence_arrow ~ message_endpoint +} +message_missing_right = { + message_endpoint ~ !deactivate_suffix ~ sequence_arrow } parallel_marker = { "&" } @@ -199,18 +211,16 @@ inline_participant = { lost_found_marker = { left_lost_found | right_lost_found + | short_lost_found } -// '[', '[o', '[x' -left_lost_found = { "[" ~ ("o" | "x")? } -// ']', 'o]', 'x]' -right_lost_found = { ("o" | "x")? ~ "]" } +left_lost_found = { "[" } +right_lost_found = { "]" } +short_lost_found = { "?" } message_suffix = { - activate_suffix - | deactivate_suffix - | create_suffix - | destroy_suffix + message_lifecycle_suffix+ } +message_lifecycle_suffix = _{ activate_suffix | deactivate_suffix | create_suffix | destroy_suffix } activate_suffix = { "++" ~ color_spec? } deactivate_suffix = { "--" } create_suffix = { "**" } diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs index 25479994..a1ee475f 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs @@ -125,6 +125,7 @@ pub enum MessageSuffix { Deactivate, // -- Create, // ** Destroy, // !! + Combined(Vec), } // Group commands (alt, opt, loop, etc.) - internal parsing structure diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs index a52d0875..dd1d7ec1 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs @@ -264,29 +264,14 @@ impl PumlSequenceParser { pair: pest::iterators::Pair, source_location: SourceLocation, ) -> Result { - let mut left: Option = None; - let mut arrow: Option = None; - let mut right: Option = None; + let mut body: Option> = None; let mut suffix: Option = None; let mut description: Option = None; for inner in pair.into_inner() { match inner.as_rule() { - Rule::message_endpoint => { - let endpoint = Self::parse_message_endpoint(inner)?; - // First participant goes to left, second to right - if arrow.is_none() { - left = Some(endpoint); - } else { - right = Some(endpoint); - } - } - Rule::sequence_arrow => { - arrow = Some(Self::parse_arrow(inner)?); - } - Rule::message_suffix => { - suffix = Some(Self::parse_message_suffix(inner)?); - } + Rule::message_body => body = Some(inner), + Rule::message_suffix => suffix = Some(Self::parse_message_suffix(inner)?), Rule::sequence_description => { description = inner .into_inner() @@ -297,29 +282,92 @@ impl PumlSequenceParser { } } + let (left, arrow, right) = Self::parse_message_body(body.ok_or_else(|| { + SequenceError::InvalidStatement("missing message body".to_string()) + })?)?; + Ok(Message { - left: left.ok_or_else(|| { - SequenceError::InvalidStatement("message must contain left endpoint".to_string()) - })?, - arrow: arrow.ok_or_else(|| { - SequenceError::InvalidStatement("missing arrow in message".to_string()) - })?, - right: right.ok_or_else(|| { - SequenceError::InvalidStatement("message must contain right endpoint".to_string()) - })?, + left, + arrow, + right, suffix, description, source_location, }) } + fn parse_message_body( + pair: pest::iterators::Pair, + ) -> Result<(MessageEndpoint, Arrow, MessageEndpoint), SequenceError> { + let body = pair + .into_inner() + .next() + .ok_or_else(|| SequenceError::InvalidStatement("empty message body".to_string()))?; + let body_rule = body.as_rule(); + let mut endpoints = Vec::new(); + let mut arrow = None; + + for inner in body.into_inner() { + match inner.as_rule() { + Rule::message_endpoint => endpoints.push(Self::parse_message_endpoint(inner)?), + Rule::sequence_arrow => arrow = Some(Self::parse_arrow(inner)?), + _ => {} + } + } + + let arrow = arrow.ok_or_else(|| { + SequenceError::InvalidStatement("message body must contain arrow".to_string()) + })?; + let mut endpoints = endpoints.into_iter(); + + match body_rule { + Rule::message_full => Ok(( + endpoints.next().ok_or_else(|| { + SequenceError::InvalidStatement( + "message_full must contain left endpoint".to_string(), + ) + })?, + arrow, + endpoints.next().ok_or_else(|| { + SequenceError::InvalidStatement( + "message_full must contain right endpoint".to_string(), + ) + })?, + )), + Rule::message_missing_left => Ok(( + Self::missing_message_endpoint(), + arrow, + endpoints.next().ok_or_else(|| { + SequenceError::InvalidStatement( + "message_missing_left must contain right endpoint".to_string(), + ) + })?, + )), + Rule::message_missing_right => Ok(( + endpoints.next().ok_or_else(|| { + SequenceError::InvalidStatement( + "message_missing_right must contain left endpoint".to_string(), + ) + })?, + arrow, + Self::missing_message_endpoint(), + )), + _ => Err(SequenceError::InvalidStatement(format!( + "unsupported message body: {:?}", + body_rule + ))), + } + } + + fn missing_message_endpoint() -> MessageEndpoint { + MessageEndpoint::LostFound("?".to_string()) + } + fn parse_message_endpoint( pair: pest::iterators::Pair, ) -> Result { let endpoint = pair.into_inner().next().ok_or_else(|| { - SequenceError::InvalidStatement( - "message_endpoint must contain an endpoint".to_string(), - ) + SequenceError::InvalidStatement("message_endpoint must contain an endpoint".to_string()) })?; Ok(match endpoint.as_rule() { @@ -346,7 +394,9 @@ impl PumlSequenceParser { })?; match marker.as_rule() { - Rule::left_lost_found | Rule::right_lost_found => Ok(marker.as_str().trim().to_string()), + Rule::left_lost_found | Rule::right_lost_found | Rule::short_lost_found => { + Ok(marker.as_str().trim().to_string()) + } _ => Err(SequenceError::InvalidStatement(format!( "lost_found_marker grammar produced unsupported value: {:?}", marker.as_rule() @@ -354,12 +404,24 @@ impl PumlSequenceParser { } } - fn parse_message_suffix(pair: pest::iterators::Pair) -> Result { - let suffix = pair.into_inner().next().ok_or_else(|| { - SequenceError::InvalidStatement("message_suffix must contain a suffix".to_string()) - })?; + fn parse_message_suffix( + pair: pest::iterators::Pair, + ) -> Result { + let suffixes: Vec<_> = pair + .into_inner() + .map(Self::parse_message_suffix_part) + .collect::>()?; - Ok(match suffix.as_rule() { + match suffixes.as_slice() { + [suffix] => Ok(suffix.clone()), + _ => Ok(MessageSuffix::Combined(suffixes)), + } + } + + fn parse_message_suffix_part( + pair: pest::iterators::Pair, + ) -> Result { + Ok(match pair.as_rule() { Rule::activate_suffix => MessageSuffix::Activate, Rule::deactivate_suffix => MessageSuffix::Deactivate, Rule::create_suffix => MessageSuffix::Create, @@ -367,7 +429,7 @@ impl PumlSequenceParser { _ => { return Err(SequenceError::InvalidStatement(format!( "message_suffix grammar produced unsupported value: {:?}", - suffix.as_rule() + pair.as_rule() ))); } }) diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs index a71fa56e..9db761db 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs @@ -71,11 +71,21 @@ fn test_message_participants() { run_sequence_diagram_parser_case("message_participants"); } +#[test] +fn test_message_suffixes() { + run_sequence_diagram_parser_case("message_suffixes"); +} + #[test] fn test_message_lost_found() { run_sequence_diagram_parser_case("message_lost_found"); } +#[test] +fn test_message_missing_endpoints() { + run_sequence_diagram_parser_case("message_missing_endpoints"); +} + #[test] fn test_create_participants() { run_sequence_diagram_parser_case("create_participants"); diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/message_lost_found.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/message_lost_found.puml index f3f46416..5f2b2189 100644 --- a/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/message_lost_found.puml +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/message_lost_found.puml @@ -12,11 +12,19 @@ ' ******************************************************************************* @startuml message_lost_found -[ -> Service : left lost bracket -[o -> Service ++ : left lost bracket o -[x -> Service ** : left lost bracket x -Worker -> ] : right lost bracket -Worker -> o] -- : right lost o bracket -Worker -> x] !! : right lost x bracket +[-> Service +[o-> Service +[o->o Service +[x-> Service +[<- Service +[x<- Service +Service ->] +Service ->o] +Service o->o] +Service ->x] +Service <-] +Service x<-] +?-> Worker : short to Worker +Worker ->? : short from Worker @enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/output.json index fc34b94d..f8a210f6 100644 --- a/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/output.json +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_lost_found/output.json @@ -24,7 +24,7 @@ } }, "suffix": null, - "description": "left lost bracket", + "description": null, "source_location": { "file": "", "line": 15 @@ -34,10 +34,12 @@ { "Message": { "left": { - "LostFound": "[o" + "LostFound": "[" }, "arrow": { - "left": null, + "left": { + "raw": "o" + }, "line": { "raw": "-" }, @@ -52,8 +54,8 @@ "alias": null } }, - "suffix": "Activate", - "description": "left lost bracket o", + "suffix": null, + "description": null, "source_location": { "file": "", "line": 16 @@ -63,16 +65,18 @@ { "Message": { "left": { - "LostFound": "[x" + "LostFound": "[" }, "arrow": { - "left": null, + "left": { + "raw": "o" + }, "line": { "raw": "-" }, "middle": null, "right": { - "raw": ">" + "raw": ">o" } }, "right": { @@ -81,8 +85,8 @@ "alias": null } }, - "suffix": "Create", - "description": "left lost bracket x", + "suffix": null, + "description": null, "source_location": { "file": "", "line": 17 @@ -92,8 +96,97 @@ { "Message": { "left": { + "LostFound": "[" + }, + "arrow": { + "left": { + "raw": "x" + }, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { "Participant": { - "display_name": "Worker", + "display_name": "Service", + "alias": null + } + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 18 + } + } + }, + { + "Message": { + "left": { + "LostFound": "[" + }, + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "-" + }, + "middle": null, + "right": null + }, + "right": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 19 + } + } + }, + { + "Message": { + "left": { + "LostFound": "[" + }, + "arrow": { + "left": { + "raw": "x<" + }, + "line": { + "raw": "-" + }, + "middle": null, + "right": null + }, + "right": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 20 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Service", "alias": null } }, @@ -111,10 +204,10 @@ "LostFound": "]" }, "suffix": null, - "description": "right lost bracket", + "description": null, "source_location": { "file": "", - "line": 18 + "line": 21 } } }, @@ -122,10 +215,154 @@ "Message": { "left": { "Participant": { - "display_name": "Worker", + "display_name": "Service", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">o" + } + }, + "right": { + "LostFound": "]" + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 22 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "arrow": { + "left": { + "raw": "o" + }, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">o" + } + }, + "right": { + "LostFound": "]" + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 23 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Service", "alias": null } }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">x" + } + }, + "right": { + "LostFound": "]" + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 24 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "-" + }, + "middle": null, + "right": null + }, + "right": { + "LostFound": "]" + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 25 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "arrow": { + "left": { + "raw": "x<" + }, + "line": { + "raw": "-" + }, + "middle": null, + "right": null + }, + "right": { + "LostFound": "]" + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 26 + } + } + }, + { + "Message": { + "left": { + "LostFound": "?" + }, "arrow": { "left": null, "line": { @@ -137,13 +374,16 @@ } }, "right": { - "LostFound": "o]" + "Participant": { + "display_name": "Worker", + "alias": null + } }, - "suffix": "Deactivate", - "description": "right lost o bracket", + "suffix": null, + "description": "short to Worker", "source_location": { "file": "", - "line": 19 + "line": 27 } } }, @@ -166,13 +406,13 @@ } }, "right": { - "LostFound": "x]" + "LostFound": "?" }, - "suffix": "Destroy", - "description": "right lost x bracket", + "suffix": null, + "description": "short from Worker", "source_location": { "file": "", - "line": 20 + "line": 28 } } } diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/message_missing_endpoints.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/message_missing_endpoints.puml new file mode 100644 index 00000000..80a03e2d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/message_missing_endpoints.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml message_missing_endpoints + +--> Service : missing left +Worker <-- : missing right +--> Service +Worker <-- + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/output.json new file mode 100644 index 00000000..c97e8679 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_missing_endpoints/output.json @@ -0,0 +1,123 @@ +{ + "message_missing_endpoints.puml": { + "name": "message_missing_endpoints", + "statements": [ + { + "Message": { + "left": { + "LostFound": "?" + }, + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "suffix": null, + "description": "missing left", + "source_location": { + "file": "", + "line": 15 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Worker", + "alias": null + } + }, + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "right": { + "LostFound": "?" + }, + "suffix": null, + "description": "missing right", + "source_location": { + "file": "", + "line": 16 + } + } + }, + { + "Message": { + "left": { + "LostFound": "?" + }, + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Service", + "alias": null + } + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 17 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Worker", + "alias": null + } + }, + "arrow": { + "left": { + "raw": "<" + }, + "line": { + "raw": "--" + }, + "middle": null, + "right": null + }, + "right": { + "LostFound": "?" + }, + "suffix": null, + "description": null, + "source_location": { + "file": "", + "line": 18 + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/message_suffixes.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/message_suffixes.puml new file mode 100644 index 00000000..10c2bfbb --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/message_suffixes.puml @@ -0,0 +1,19 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml message_suffixes + +alice -> bob ++ #LightBlue : hello1 +bob -> charlie --++ : hello2 +charlie --> alice -- : ok + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/output.json new file mode 100644 index 00000000..33aaa535 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/message_suffixes/output.json @@ -0,0 +1,108 @@ +{ + "message_suffixes.puml": { + "name": "message_suffixes", + "statements": [ + { + "Message": { + "left": { + "Participant": { + "display_name": "alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "bob", + "alias": null + } + }, + "suffix": "Activate", + "description": "hello1", + "source_location": { + "file": "", + "line": 15 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "bob", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "charlie", + "alias": null + } + }, + "suffix": { + "Combined": [ + "Deactivate", + "Activate" + ] + }, + "description": "hello2", + "source_location": { + "file": "", + "line": 16 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "charlie", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "alice", + "alias": null + } + }, + "suffix": "Deactivate", + "description": "ok", + "source_location": { + "file": "", + "line": 17 + } + } + } + ] + } +} From 19bda8b4837e291c459f245fc2b7f413f349b9a5 Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Thu, 30 Jul 2026 11:34:57 +0800 Subject: [PATCH 4/5] support sequence group and return commands --- .../puml_parser/src/grammar/sequence.pest | 29 +- .../src/sequence_diagram/src/lib.rs | 6 +- .../src/sequence_diagram/src/sequence_ast.rs | 44 +- .../sequence_diagram/src/sequence_parser.rs | 307 +++++++++----- .../test/sequence_integration_test.rs | 10 + .../group_commands/group_commands.puml | 41 ++ .../group_commands/output.json | 386 ++++++++++++++++++ .../return_commands/output.json | 89 ++++ .../return_commands/return_commands.puml | 20 + .../src/sequence_diagram/src/logic_parser.rs | 99 +++-- 10 files changed, 862 insertions(+), 169 deletions(-) create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/group_commands.puml create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/output.json create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/output.json create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/return_commands.puml diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest index 191258c3..cba2e79a 100644 --- a/plantuml/parser/puml_parser/src/grammar/sequence.pest +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -229,7 +229,7 @@ destroy_suffix = { "!!" } activation_short = { participant_ref ~ (activate_suffix | deactivate_suffix) } // Return -return_cmd = { parallel_marker? ~ ^"return" ~ sequence_text_content? } +return_cmd = { ^"return" ~ sequence_text_content? } // Box box_start = { "box" ~ quoted_string? ~ color_spec? } @@ -237,15 +237,28 @@ box_end = { ^"end" ~ WHITESPACE? ~ ^"box" } // Group commands (alt, opt, loop, etc.) group_cmd = { - parallel_marker? ~ group_type ~ group_condition? + group_start + | group_branch + | group_end } - -group_condition = @{ (!EOL ~ ANY)+ } - -group_type = @{ - (^"critical" | ^"par2" | ^"par" | ^"opt" | ^"alt" | ^"loop" | ^"break" | - ^"else" | ^"also" | ^"group" | ^"end") ~ &(" " | "\t" | "\n" | "\r") +group_start = { + parallel_marker? ~ group_start_type ~ group_label? +} +group_branch = { + group_branch_type ~ group_label? +} +group_end = { ^"end" ~ group_start_type? } +group_start_type = @{ + ^"alt" + | ^"opt" + | ^"loop" + | ^"par" + | ^"break" + | ^"critical" + | ^"group" } +group_branch_type = @{ ^"else" } +group_label = @{ (!EOL ~ ANY)+ } // Divider and delay divider = { "==" ~ divider_text? ~ "==" } diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs index a7df731e..85343bc5 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/lib.rs @@ -15,9 +15,9 @@ pub mod sequence_ast; mod sequence_parser; pub use sequence_ast::{ - ActivateCmd, Arrow, CreateCmd, DeactivateCmd, DestroyCmd, GroupCmd, GroupType, Message, - MessageEndpoint, MessageSuffix, ParticipantIdentifier, ParticipantRef, ParticipantType, - RefCmd, SeqPumlDocument, Statement, + ActivateCmd, Arrow, CreateCmd, DeactivateCmd, DestroyCmd, GroupCmd, GroupElse, GroupEnd, + GroupKind, GroupStart, Message, MessageEndpoint, MessageSuffix, ParticipantIdentifier, + ParticipantRef, ParticipantType, RefCmd, ReturnCmd, SeqPumlDocument, Statement, }; pub use sequence_parser::{PumlSequenceParser, SequenceError}; diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs index a1ee475f..4832b736 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_ast.rs @@ -38,6 +38,7 @@ pub enum Statement { Message(Message), GroupCmd(GroupCmd), RefCmd(RefCmd), + ReturnCmd(ReturnCmd), } // Participant definitions @@ -130,23 +131,48 @@ pub enum MessageSuffix { // Group commands (alt, opt, loop, etc.) - internal parsing structure #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct GroupCmd { - pub group_type: GroupType, - pub text: Option, +pub enum GroupCmd { + Start(GroupStart), + Else(GroupElse), + End(GroupEnd), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GroupStart { + pub kind: GroupKind, + pub label: Option, + /// alt success + pub is_parallel: bool, + /// "& alt ..." pub source_location: SourceLocation, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum GroupType { - Opt, +pub struct GroupElse { + pub label: Option, + /// else success + pub source_location: SourceLocation, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GroupEnd { + pub kind: Option, + pub source_location: SourceLocation, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum GroupKind { Alt, + Opt, Loop, Par, - Par2, Break, Critical, - Else, - Also, - End, Group, } + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ReturnCmd { + pub label: Option, + pub source_location: SourceLocation, +} diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs index dd1d7ec1..0459d4c1 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/src/sequence_parser.rs @@ -67,6 +67,7 @@ impl PumlSequenceParser { .into_inner() .next() .ok_or_else(|| SequenceError::InvalidStatement("empty statement".to_string()))?; + match inner.as_rule() { Rule::participant_def => Ok(vec![Statement::ParticipantDef( Self::parse_participant_def(inner, source_location)?, @@ -84,6 +85,10 @@ impl PumlSequenceParser { inner, source_location, ))]), + Rule::return_cmd => Ok(vec![Statement::ReturnCmd(Self::parse_return_cmd( + inner, + source_location, + ))]), // Grammar-valid directives that are intentionally not modeled as statements _ => Ok(vec![]), } @@ -282,9 +287,10 @@ impl PumlSequenceParser { } } - let (left, arrow, right) = Self::parse_message_body(body.ok_or_else(|| { - SequenceError::InvalidStatement("missing message body".to_string()) - })?)?; + let (left, arrow, right) = + Self::parse_message_body(body.ok_or_else(|| { + SequenceError::InvalidStatement("missing message body".to_string()) + })?)?; Ok(Message { left, @@ -444,45 +450,120 @@ impl PumlSequenceParser { pair: pest::iterators::Pair, source_location: SourceLocation, ) -> Result { - let mut group_type: Option = None; - let mut text: Option = None; + let inner = pair + .into_inner() + .next() + .ok_or_else(|| SequenceError::InvalidStatement("empty group command".to_string()))?; + + match inner.as_rule() { + Rule::group_start => Self::parse_group_start(inner, source_location), + Rule::group_branch => Self::parse_group_branch(inner, source_location), + Rule::group_end => Self::parse_group_end(inner, source_location), + _ => Err(SequenceError::InvalidStatement(format!( + "unsupported group command: {:?}", + inner.as_rule() + ))), + } + } + + fn parse_group_start( + pair: pest::iterators::Pair, + source_location: SourceLocation, + ) -> Result { + let mut kind: Option = None; + let mut label: Option = None; + let mut is_parallel = false; for inner in pair.into_inner() { match inner.as_rule() { - Rule::group_type => { - group_type = Self::parse_group_type(inner); + Rule::parallel_marker => { + is_parallel = true; } - Rule::group_condition => { - text = Some(inner.as_str().trim().to_string()); + Rule::group_start_type => { + kind = Some(Self::parse_group_kind(inner)?); + } + Rule::group_label => { + label = Some(inner.as_str().trim().to_string()); } _ => {} } } - Ok(GroupCmd { - group_type: group_type - .ok_or_else(|| SequenceError::InvalidStatement("missing group type".to_string()))?, - text, + Ok(GroupCmd::Start(GroupStart { + kind: kind.ok_or_else(|| { + SequenceError::InvalidStatement("missing group start kind".to_string()) + })?, + label, + is_parallel, source_location, - }) + })) } - fn parse_group_type(pair: pest::iterators::Pair) -> Option { - let text = pair.as_str().to_lowercase(); - match text.as_str() { - "opt" => Some(GroupType::Opt), - "alt" => Some(GroupType::Alt), - "loop" => Some(GroupType::Loop), - "par" => Some(GroupType::Par), - "par2" => Some(GroupType::Par2), - "break" => Some(GroupType::Break), - "critical" => Some(GroupType::Critical), - "else" => Some(GroupType::Else), - "also" => Some(GroupType::Also), - "end" => Some(GroupType::End), - "group" => Some(GroupType::Group), - _ => None, + fn parse_group_branch( + pair: pest::iterators::Pair, + source_location: SourceLocation, + ) -> Result { + let mut has_else = false; + let mut label: Option = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::group_branch_type => { + has_else = true; + } + Rule::group_label => { + label = Some(inner.as_str().trim().to_string()); + } + _ => {} + } + } + + if !has_else { + return Err(SequenceError::InvalidStatement( + "missing group branch kind".to_string(), + )); + } + + Ok(GroupCmd::Else(GroupElse { + label, + source_location, + })) + } + + fn parse_group_end( + pair: pest::iterators::Pair, + source_location: SourceLocation, + ) -> Result { + let mut kind: Option = None; + + for inner in pair.into_inner() { + if inner.as_rule() == Rule::group_start_type { + kind = Some(Self::parse_group_kind(inner)?); + } } + + Ok(GroupCmd::End(GroupEnd { + kind, + source_location, + })) + } + + fn parse_group_kind(pair: pest::iterators::Pair) -> Result { + let text = pair.as_str().to_lowercase(); + Ok(match text.as_str() { + "alt" => GroupKind::Alt, + "opt" => GroupKind::Opt, + "loop" => GroupKind::Loop, + "par" => GroupKind::Par, + "break" => GroupKind::Break, + "critical" => GroupKind::Critical, + "group" => GroupKind::Group, + _ => { + return Err(SequenceError::InvalidStatement(format!( + "unsupported group kind: {text}" + ))); + } + }) } fn parse_ref_cmd(pair: pest::iterators::Pair, source_location: SourceLocation) -> RefCmd { @@ -498,6 +579,22 @@ impl PumlSequenceParser { } } + fn parse_return_cmd( + pair: pest::iterators::Pair, + source_location: SourceLocation, + ) -> ReturnCmd { + let label = pair + .into_inner() + .find(|inner| inner.as_rule() == Rule::sequence_text_content) + .map(|inner| inner.as_str().trim().to_string()) + .filter(|text| !text.is_empty()); + + ReturnCmd { + label, + source_location, + } + } + fn extract_ref_parts( pair: pest::iterators::Pair, participants: &mut Vec, @@ -527,6 +624,40 @@ impl PumlSequenceParser { } } + fn parse_create_cmd( + pair: pest::iterators::Pair, + source_location: SourceLocation, + ) -> Result { + let mut participant_type: Option = None; + let mut identifier: Option = None; + let mut stereotype: Option = None; + + for inner in pair.into_inner() { + match inner.as_rule() { + Rule::participant_type => { + participant_type = Some(Self::parse_participant_type(inner)?); + } + Rule::participant_identifier => { + identifier = Some(Self::parse_participant_identifier(inner)?); + } + Rule::stereotype => { + stereotype = Some(Self::extract_stereotype(inner.as_str())); + } + Rule::order_clause => {} + _ => {} + } + } + + Ok(CreateCmd { + participant_type: participant_type.unwrap_or(ParticipantType::Participant), + identifier: identifier.ok_or_else(|| { + SequenceError::InvalidStatement("missing participant identifier".to_string()) + })?, + stereotype, + source_location, + }) + } + fn parse_destroy_cmd(pair: pest::iterators::Pair) -> Result { let mut participant: Option = None; @@ -577,40 +708,6 @@ impl PumlSequenceParser { }) } - fn parse_create_cmd( - pair: pest::iterators::Pair, - source_location: SourceLocation, - ) -> Result { - let mut participant_type: Option = None; - let mut identifier: Option = None; - let mut stereotype: Option = None; - - for inner in pair.into_inner() { - match inner.as_rule() { - Rule::participant_type => { - participant_type = Some(Self::parse_participant_type(inner)?); - } - Rule::participant_identifier => { - identifier = Some(Self::parse_participant_identifier(inner)?); - } - Rule::stereotype => { - stereotype = Some(Self::extract_stereotype(inner.as_str())); - } - Rule::order_clause => {} - _ => {} - } - } - - Ok(CreateCmd { - participant_type: participant_type.unwrap_or(ParticipantType::Participant), - identifier: identifier.ok_or_else(|| { - SequenceError::InvalidStatement("missing participant identifier".to_string()) - })?, - stereotype, - source_location, - }) - } - fn parse_activation_short( pair: pest::iterators::Pair, ) -> Result, SequenceError> { @@ -673,6 +770,52 @@ impl PumlSequenceParser { _ => pair.as_str().trim().to_string(), } } + + #[cfg(not(coverage))] + fn log_parse_tree_if_enabled( + pairs: &pest::iterators::Pairs, + path: &Rc, + log_level: LogLevel, + ) { + if matches!(log_level, LogLevel::Debug | LogLevel::Trace) { + let mut tree_output = String::new(); + format_parse_tree(pairs.clone(), 0, &mut tree_output); + debug!( + "\n=== Parse Tree for {} ===\n{}=== End Parse Tree ===", + path.display(), + tree_output + ); + } + } + + fn parse_document( + pairs: pest::iterators::Pairs, + source_path: &str, + ) -> Result { + let mut document = SeqPumlDocument { + name: None, + statements: Vec::new(), + }; + + for inner_pair in pairs + .filter(|pair| pair.as_rule() == Rule::sequence_start) + .flat_map(|pair| pair.into_inner()) + { + match inner_pair.as_rule() { + Rule::startuml => { + document.name = Self::parse_startuml(inner_pair); + } + Rule::sequence_statement => { + document + .statements + .extend(Self::parse_statement(inner_pair, source_path)?); + } + _ => {} + } + } + + Ok(document) + } } impl DiagramParser for PumlSequenceParser { @@ -695,45 +838,11 @@ impl DiagramParser for PumlSequenceParser { let pairs = PlantUmlCommonParser::parse(Rule::sequence_start, content) .map_err(|e| pest_to_syntax_error(e, path.as_ref().clone(), content))?; - // Debug-only, excluded to keep coverage focused on parser logic. #[cfg(not(coverage))] - if matches!(log_level, LogLevel::Debug | LogLevel::Trace) { - let mut tree_output = String::new(); - format_parse_tree(pairs.clone(), 0, &mut tree_output); - debug!( - "\n=== Parse Tree for {} ===\n{}=== End Parse Tree ===", - path.display(), - tree_output - ); - } + Self::log_parse_tree_if_enabled(&pairs, path, log_level); let source_path = path.as_ref().clone().to_string_lossy().to_string(); - let mut document = SeqPumlDocument { - name: None, - statements: Vec::new(), - }; - - for pair in pairs { - if pair.as_rule() == Rule::sequence_start { - for inner_pair in pair.into_inner() { - match inner_pair.as_rule() { - Rule::startuml => { - document.name = Self::parse_startuml(inner_pair); - } - Rule::sequence_statement => { - let mut stmts = Self::parse_statement(inner_pair, &source_path)?; - document.statements.append(&mut stmts); - } - Rule::empty_line => { - // Skip empty lines - } - _ => {} - } - } - } - } - - Ok(document) + Self::parse_document(pairs, &source_path) } } diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs index 9db761db..9eb67170 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs @@ -96,7 +96,17 @@ fn test_lifecycle_commands() { run_sequence_diagram_parser_case("lifecycle_commands"); } +#[test] +fn test_group_commands() { + run_sequence_diagram_parser_case("group_commands"); +} + #[test] fn test_ref_statement() { run_sequence_diagram_parser_case("ref_statement"); } + +#[test] +fn test_return_commands() { + run_sequence_diagram_parser_case("return_commands"); +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/group_commands.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/group_commands.puml new file mode 100644 index 00000000..7f8794ca --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/group_commands.puml @@ -0,0 +1,41 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml group_commands + +alt successful path + Alice -> Bob : success +else fallback path + Alice -> Bob : fallback +end alt + +loop retry while pending + Alice -> Bob : retry +end + +group custom section + Alice -> Bob : custom +end + +par parallel branch + Alice -> Bob : branch +end + +break stop early + Alice -> Bob : stop +end + +critical exclusive section + Alice -> Bob : exclusive +end critical + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/output.json new file mode 100644 index 00000000..f7b70ab3 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/group_commands/output.json @@ -0,0 +1,386 @@ +{ + "group_commands.puml": { + "name": "group_commands", + "statements": [ + { + "GroupCmd": { + "Start": { + "kind": "Alt", + "label": "successful path", + "is_parallel": false, + "source_location": { + "file": "", + "line": 15 + } + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "success", + "source_location": { + "file": "", + "line": 16 + } + } + }, + { + "GroupCmd": { + "Else": { + "label": "fallback path", + "source_location": { + "file": "", + "line": 17 + } + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "fallback", + "source_location": { + "file": "", + "line": 18 + } + } + }, + { + "GroupCmd": { + "End": { + "kind": "Alt", + "source_location": { + "file": "", + "line": 19 + } + } + } + }, + { + "GroupCmd": { + "Start": { + "kind": "Loop", + "label": "retry while pending", + "is_parallel": false, + "source_location": { + "file": "", + "line": 21 + } + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "retry", + "source_location": { + "file": "", + "line": 22 + } + } + }, + { + "GroupCmd": { + "End": { + "kind": null, + "source_location": { + "file": "", + "line": 23 + } + } + } + }, + { + "GroupCmd": { + "Start": { + "kind": "Group", + "label": "custom section", + "is_parallel": false, + "source_location": { + "file": "", + "line": 25 + } + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "custom", + "source_location": { + "file": "", + "line": 26 + } + } + }, + { + "GroupCmd": { + "End": { + "kind": null, + "source_location": { + "file": "", + "line": 27 + } + } + } + }, + { + "GroupCmd": { + "Start": { + "kind": "Par", + "label": "parallel branch", + "is_parallel": false, + "source_location": { + "file": "", + "line": 29 + } + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "branch", + "source_location": { + "file": "", + "line": 30 + } + } + }, + { + "GroupCmd": { + "End": { + "kind": null, + "source_location": { + "file": "", + "line": 31 + } + } + } + }, + { + "GroupCmd": { + "Start": { + "kind": "Break", + "label": "stop early", + "is_parallel": false, + "source_location": { + "file": "", + "line": 33 + } + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "stop", + "source_location": { + "file": "", + "line": 34 + } + } + }, + { + "GroupCmd": { + "End": { + "kind": null, + "source_location": { + "file": "", + "line": 35 + } + } + } + }, + { + "GroupCmd": { + "Start": { + "kind": "Critical", + "label": "exclusive section", + "is_parallel": false, + "source_location": { + "file": "", + "line": 37 + } + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "exclusive", + "source_location": { + "file": "", + "line": 38 + } + } + }, + { + "GroupCmd": { + "End": { + "kind": "Critical", + "source_location": { + "file": "", + "line": 39 + } + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/output.json new file mode 100644 index 00000000..8e2f3aa8 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/output.json @@ -0,0 +1,89 @@ +{ + "return_commands.puml": { + "name": "return_commands", + "statements": [ + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "call", + "source_location": { + "file": "", + "line": 15 + } + } + }, + { + "ReturnCmd": { + "label": "done", + "source_location": { + "file": "", + "line": 16 + } + } + }, + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "ping", + "source_location": { + "file": "", + "line": 17 + } + } + }, + { + "ReturnCmd": { + "label": "Result>>", + "source_location": { + "file": "", + "line": 18 + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/return_commands.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/return_commands.puml new file mode 100644 index 00000000..c2063002 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/return_commands/return_commands.puml @@ -0,0 +1,20 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml return_commands + +Alice -> Bob : call +return done +Alice -> Bob : ping +return Result>> + +@enduml diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs index ddd92617..f41d0e46 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/src/logic_parser.rs @@ -28,20 +28,16 @@ fn endpoint_name(endpoint: &MessageEndpoint) -> String { } } -/// Convert a syntax-level `GroupType` into the metamodel `ConditionType`. -fn group_type_to_condition(gt: &GroupType) -> ConditionType { - match gt { - GroupType::Opt => ConditionType::Opt, - GroupType::Alt => ConditionType::Alt, - GroupType::Loop => ConditionType::Loop, - GroupType::Par => ConditionType::Par, - GroupType::Par2 => ConditionType::Par2, - GroupType::Break => ConditionType::Break, - GroupType::Critical => ConditionType::Critical, - GroupType::Else => ConditionType::Else, - GroupType::Also => ConditionType::Also, - GroupType::End => ConditionType::End, - GroupType::Group => ConditionType::Group, +/// Convert a syntax-level `GroupKind` into the metamodel `ConditionType`. +fn group_kind_to_condition(kind: &GroupKind) -> ConditionType { + match kind { + GroupKind::Opt => ConditionType::Opt, + GroupKind::Alt => ConditionType::Alt, + GroupKind::Loop => ConditionType::Loop, + GroupKind::Par => ConditionType::Par, + GroupKind::Break => ConditionType::Break, + GroupKind::Critical => ConditionType::Critical, + GroupKind::Group => ConditionType::Group, } } @@ -55,12 +51,9 @@ pub fn build_tree(statements: &[Statement]) -> Vec { nodes.push(node); i += consumed; } else { - // Skip over else/also/end that are not handled + // Skip over branch/end markers that are not handled if let Some(Statement::GroupCmd(g)) = statements.get(i) { - if matches!( - g.group_type, - GroupType::Else | GroupType::Also | GroupType::End - ) { + if matches!(g, GroupCmd::Else(_) | GroupCmd::End(_)) { i += 1; continue; } @@ -77,20 +70,8 @@ pub(crate) fn box_nodes(nodes: Vec) -> Vec { nodes } -fn is_group_start(group_type: &GroupType) -> bool { - matches!( - group_type, - GroupType::Alt - | GroupType::Opt - | GroupType::Loop - | GroupType::Par - | GroupType::Par2 - | GroupType::Break - | GroupType::Critical - | GroupType::Group - | GroupType::Else - | GroupType::Also - ) +fn is_group_node(group: &GroupCmd) -> bool { + matches!(group, GroupCmd::Start(_) | GroupCmd::Else(_)) } fn collect_group_statements(statements: &[Statement]) -> (Vec, usize) { @@ -100,8 +81,8 @@ fn collect_group_statements(statements: &[Statement]) -> (Vec, usize) for stmt in &statements[1..] { if let Statement::GroupCmd(group) = stmt { - match group.group_type { - GroupType::End => { + match group { + GroupCmd::End(_) => { if nesting_depth > 0 { nesting_depth -= 1; group_statements.push(stmt.clone()); @@ -109,20 +90,17 @@ fn collect_group_statements(statements: &[Statement]) -> (Vec, usize) break; } } - GroupType::Else | GroupType::Also => { + GroupCmd::Else(_) => { if nesting_depth > 0 { group_statements.push(stmt.clone()); } else { break; } } - _ if is_group_start(&group.group_type) => { + GroupCmd::Start(_) => { nesting_depth += 1; group_statements.push(stmt.clone()); } - _ => { - group_statements.push(stmt.clone()); - } } } else { group_statements.push(stmt.clone()); @@ -134,22 +112,45 @@ fn collect_group_statements(statements: &[Statement]) -> (Vec, usize) } fn build_group_node(statements: &[Statement], group: &GroupCmd) -> (SequenceNode, usize) { - let condition = Condition { - condition_type: group_type_to_condition(&group.group_type), - condition_value: group.text.clone().unwrap_or_default(), - }; + let (condition, source_location) = group_condition_and_location(group); let (group_statements, consumed) = collect_group_statements(statements); ( SequenceNode { event: Event::Condition(condition), - source_location: group.source_location.clone(), + source_location, branches_node: box_nodes(build_tree(&group_statements)), }, consumed, ) } +fn group_condition_and_location(group: &GroupCmd) -> (Condition, SourceLocation) { + match group { + GroupCmd::Start(start) => ( + Condition { + condition_type: group_kind_to_condition(&start.kind), + condition_value: start.label.clone().unwrap_or_default(), + }, + start.source_location.clone(), + ), + GroupCmd::Else(else_cmd) => ( + Condition { + condition_type: ConditionType::Else, + condition_value: else_cmd.label.clone().unwrap_or_default(), + }, + else_cmd.source_location.clone(), + ), + GroupCmd::End(end) => ( + Condition { + condition_type: ConditionType::End, + condition_value: String::new(), + }, + end.source_location.clone(), + ), + } +} + /// Build a single sequence node and return how many statements were consumed fn build_node(statements: &[Statement]) -> Option<(SequenceNode, usize)> { if statements.is_empty() { @@ -235,11 +236,9 @@ fn build_node(statements: &[Statement]) -> Option<(SequenceNode, usize)> { } Statement::GroupCmd(group) => { // Handle group commands (alt, opt, loop, else, etc.) - match group.group_type { - GroupType::End => { - None // End markers signal the close of a branch - } - _ if is_group_start(&group.group_type) => Some(build_group_node(statements, group)), + match group { + GroupCmd::End(_) => None, // End markers signal the close of a branch + _ if is_group_node(group) => Some(build_group_node(statements, group)), _ => None, } } From 59b702748ce0017ab39054c1900ef80e21ccba4d Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Thu, 30 Jul 2026 11:36:54 +0800 Subject: [PATCH 5/5] ignore unmodeled sequence diagram syntax --- .../comprehensive_sequence_test.puml | 4 +- .../puml_parser/src/grammar/common.pest | 14 +- .../puml_parser/src/grammar/sequence.pest | 279 ++++++++++-------- .../test/sequence_integration_test.rs | 5 + .../ignored_blocks/ignored_blocks.puml | 35 +++ .../ignored_blocks/output.json | 39 +++ 6 files changed, 257 insertions(+), 119 deletions(-) create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/ignored_blocks.puml create mode 100644 plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/output.json diff --git a/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml b/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml index d9f680f2..23243e06 100644 --- a/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml +++ b/plantuml/parser/integration_test/sequence_diagram/comprehensive_sequence_test.puml @@ -339,7 +339,7 @@ group Extended Patterns: Lost and Found Messages ' Lost/found with brackets on different sides [o-> Actor1 : Right bracket on left - Actor1 -->o[ : Left bracket on right + Actor1 -->o] : Left bracket on right end group group Extended Patterns: Activation Markers @@ -364,7 +364,7 @@ group Extended Patterns: Ref Blocks ref over Builder Another reference block - end + end ref end group group Extended Patterns: Dividers with URLs diff --git a/plantuml/parser/puml_parser/src/grammar/common.pest b/plantuml/parser/puml_parser/src/grammar/common.pest index 7ca0de08..e9f54acb 100644 --- a/plantuml/parser/puml_parser/src/grammar/common.pest +++ b/plantuml/parser/puml_parser/src/grammar/common.pest @@ -84,23 +84,33 @@ COMMENT = _{ } //////////////////////////////////////////////////////////////////////////////// -// Ignored Statements +// Ignored Blocks and Statements // // Non-structural PlantUML directives such as title, style, layout, and scaling. // Parsed for completeness but generally skipped in AST construction. // // Examples: +// // title System Overview // skinparam shadowing false // left to right direction // scale 1.2 //////////////////////////////////////////////////////////////////////////////// +ignored_block = _{ + style_block + | note_multiline +} + +style_block = { "" ~ EOL? } + ignored_stmt = _{ direction_stmt | skinparam_stmt | title_stmt + | note_single_line | scale_stmt - | namespace_separator_stmt + | namespace_separator_stmt } skinparam_stmt = @{ ^"skinparam" ~ LINE_REST } diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest index cba2e79a..c3eb1fc0 100644 --- a/plantuml/parser/puml_parser/src/grammar/sequence.pest +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -13,94 +13,21 @@ // PlantUML Sequence Diagram Grammar for Pest Parser -sequence_start = { empty_line* ~ startuml ~ (note_multiline | sequence_statement | empty_line)* ~ enduml } - -sequence_statement = { - ( - note_declaration | - pragma | ignored_stmt | - minwidth | rotate | transformation | - hide_unlinked | show_unlinked | - sprite_inline | sprite_block_start | - hide_show_member | hide_show_stereotype | - participant_def | - message | - lifecycle_cmd | - box_start | box_end | - group_cmd | - divider | delay | - return_cmd | - ref_stmt | - skin | autonumber | autonumber_stop | autonumber_resume | autonumber_inc | - autoactivate | footbox_cmd | ellipsis | - function_def | function_return | function_end - ) ~ EOL -} - -// Basic elements -pragma = { ^"!pragma" ~ identifier ~ pragma_value? } -pragma_value = { (!EOL ~ ANY)+ } - -// Function definitions (preprocessor functions) -function_def = { ^"!function" ~ function_content } -function_return = { ^"!return" ~ function_content } -function_end = { ^"!endfunction" } -function_content = { (!EOL ~ ANY)+ } - -// Multi-line blocks - -legend_block_start = { ^"legend" ~ legend_pos? ~ legend_align? } -legend_pos = { ^"top" | ^"bottom" } -legend_align = { ^"left" | ^"right" | ^"center" } -legend_block_end = { ^"end" ~ WHITESPACE? ~ ^"legend" } - -// rotate, etc. -minwidth = { ^"minwidth" ~ NUMBER } -rotate = { ^"rotate" } - - -transformation = { ^"!transformation" ~ transformation_value } -transformation_value = { (!("{" | EOL) ~ ANY)+ } -transformation_block_start = { ^"!transformation" ~ "{" } -transformation_block_end = { "!" ~ "}" } - -hide_unlinked = { ^"hide" ~ ^"unlinked" } -show_unlinked = { ^"show" ~ ^"unlinked" } - -// Sprite -sprite_block_start = { - ^"sprite" ~ sprite_name ~ sprite_dimensions? ~ "{" -} -sprite_block_end = { (^"end" ~ WHITESPACE? ~ ^"sprite") | "}" } -sprite_inline = { - ^"sprite" ~ sprite_name ~ sprite_dimensions? ~ (sprite_encoding | sprite_data) -} -sprite_name = { "$"? ~ identifier } -sprite_dimensions = { - "[" ~ NUMBER ~ "x" ~ NUMBER ~ "/" ~ - (NUMBER ~ "z"? | ^"color") ~ - "]" +sequence_start = { + empty_line* ~ startuml + ~ (ignored_block | sequence_ignored_block | ignored_stmt | sequence_ignored_stmt | sequence_line | empty_line)* + ~ enduml } -sprite_encoding = { ASCII_ALPHANUMERIC+ } -sprite_data = { ANY+ } -// Hide/Show -hide_show_member = { - hide_or_show ~ visibility_list ~ member_type -} -hide_show_stereotype = { - hide_or_show ~ stereotype_target* ~ empty_kw? ~ stereotype_elem -} -hide_or_show = { ^"hide" | ^"show" } -visibility_list = { sequence_visibility ~ ("," ~ sequence_visibility)* } -sequence_visibility = { ^"public" | ^"private" | ^"protected" | ^"package" } -member_type = { ^"members" | ^"member" | ^"attributes" | ^"attribute" | ^"fields" | ^"field" | ^"methods" | ^"method" } -stereotype_target = { class_type | sequence_qualified_name | quoted_string | stereotype } -class_type = { ^"class" | ^"object" | ^"interface" | ^"enum" | ^"annotation" | ^"abstract" } -empty_kw = { ^"empty" } -stereotype_elem = { - ^"members" | ^"member" | ^"attributes" | ^"attribute" | ^"fields" | ^"field" | - ^"methods" | ^"method" | ^"circle" ~ ASCII_ALPHANUMERIC* | ^"stereotypes" | ^"stereotype" +sequence_line = _{ sequence_statement ~ EOL } + +sequence_statement = { + participant_def + | message + | lifecycle_cmd + | group_cmd + | return_cmd + | ref_stmt } // Participant definitions @@ -132,10 +59,7 @@ display_with_alias = { NAME ~ ^"as" ~ NAME } // participant Alias as "Display" alias_with_quoted_display = { NAME ~ ^"as" ~ quoted_string } -// participant "Display" quoted_display = { quoted_string } - -// participant Alias alias_only = { NAME } color_spec = @{ BASIC_COLOR } @@ -160,8 +84,9 @@ create_cmd = { activate_cmd = { ^"activate" ~ participant_ref ~ color_spec? } deactivate_cmd = { ^"deactivate" ~ participant_ref? } destroy_cmd = { ^"destroy" ~ participant_ref } +activation_short = { participant_ref ~ (activate_suffix | deactivate_suffix) } -// Sequence Arrow +// Sequence arrows SEQUENCE_ARROW_PREFIX_CHAR = _{ "<" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" | "o" | "x" } SEQUENCE_ARROW_SUFFIX_CHAR = _{ ">" | "/" | "\\" | "|" | "*" | "(" | ")" | "#" | "^" | "@" | "o" | "x" } @@ -226,15 +151,9 @@ deactivate_suffix = { "--" } create_suffix = { "**" } destroy_suffix = { "!!" } -activation_short = { participant_ref ~ (activate_suffix | deactivate_suffix) } - // Return return_cmd = { ^"return" ~ sequence_text_content? } -// Box -box_start = { "box" ~ quoted_string? ~ color_spec? } -box_end = { ^"end" ~ WHITESPACE? ~ ^"box" } - // Group commands (alt, opt, loop, etc.) group_cmd = { group_start @@ -242,7 +161,7 @@ group_cmd = { | group_end } group_start = { - parallel_marker? ~ group_start_type ~ group_label? + group_start_type ~ group_label? } group_branch = { group_branch_type ~ group_label? @@ -260,28 +179,19 @@ group_start_type = @{ group_branch_type = @{ ^"else" } group_label = @{ (!EOL ~ ANY)+ } -// Divider and delay -divider = { "==" ~ divider_text? ~ "==" } -divider_text = @{ (!"==" ~ !EOL ~ ANY)+ } -delay = { ("||" ~ NUMBER? ~ "|"+) | ("…" ~ sequence_text_content? ~ "…") | ("..." ~ sequence_text_content? ~ "...") } -ellipsis = { ("..." ~ sequence_text_content? ~ "...") | ("…" ~ sequence_text_content? ~ "…") | "..." | "…" } - // Reference ref_stmt = { ref_inline | ref_block } - ref_inline = { ^"ref" ~ ^"over" ~ participant_list ~ ":" ~ sequence_text_content } - ref_block = { ^"ref" ~ ^"over" ~ participant_list ~ EOL ~ ref_body ~ ref_end } - ref_body = { ( !(WHITESPACE* ~ ref_end) @@ -293,30 +203,169 @@ ref_end = { ^"end" ~ ^"ref" } participant_list = { participant_ref ~ ("," ~ participant_ref)* } participant_ref = { CNAME } -// Skin +sequence_qualified_name = @{ identifier ~ ("." ~ identifier)* } + +sequence_text_content = { (!EOL ~ ANY)+ } +sequence_description = { ":" ~ sequence_text_content? } + +// Ignored blocks +sequence_ignored_block = _{ + sequence_sprite_block + | sequence_legend_block + | sequence_transformation_block +} + +sequence_legend_block = { + legend_block_start ~ EOL + ~ (!legend_block_end ~ ANY)* + ~ legend_block_end ~ EOL? +} +legend_block_start = { ^"legend" ~ legend_pos? ~ legend_align? } +legend_pos = { ^"top" | ^"bottom" } +legend_align = { ^"left" | ^"right" | ^"center" } +legend_block_end = { ^"end" ~ WHITESPACE? ~ ^"legend" } + +sequence_transformation_block = { + transformation_block_start ~ EOL? + ~ (!transformation_block_end ~ ANY)* + ~ transformation_block_end ~ EOL? +} +transformation_block_start = { ^"!transformation" ~ "{" } +transformation_block_end = { "!" ~ "}" } + +sequence_sprite_block = { + sprite_block_start ~ EOL? + ~ (!sprite_block_end ~ ANY)* + ~ sprite_block_end ~ EOL? +} +sprite_block_start = { ^"sprite" ~ sprite_name ~ sprite_dimensions? ~ "{" } +sprite_block_end = { (^"end" ~ WHITESPACE? ~ ^"sprite") | "}" } + +// Ignored statements +sequence_ignored_stmt = _{ + sequence_preprocessor_stmt + | sequence_layout_stmt + | sequence_visibility_stmt + | sequence_rendering_stmt + | sequence_sprite_stmt +} + +sequence_preprocessor_stmt = _{ + function_def + | function_return + | function_end + | pragma +} + +sequence_layout_stmt = _{ + minwidth + | rotate + | box_cmd + | divider + | delay + | ellipsis +} + +sequence_visibility_stmt = _{ + hide_unlinked + | show_unlinked + | hide_show_member + | hide_show_stereotype +} + +sequence_rendering_stmt = _{ + transformation + | skin + | autonumber_stop + | autonumber_resume + | autonumber_inc + | autonumber + | autoactivate + | footbox_cmd +} + +sequence_sprite_stmt = _{ + sprite_inline +} + +pragma = { ^"!pragma" ~ identifier ~ pragma_value? } +pragma_value = { (!EOL ~ ANY)+ } + +minwidth = { ^"minwidth" ~ NUMBER } +rotate = { ^"rotate" } + +transformation = { ^"!transformation" ~ transformation_value } +transformation_value = { (!("{" | EOL) ~ ANY)+ } + +hide_unlinked = { ^"hide" ~ ^"unlinked" } +show_unlinked = { ^"show" ~ ^"unlinked" } + +sprite_inline = { + ^"sprite" ~ sprite_name ~ sprite_dimensions? ~ (sprite_encoding | sprite_data) +} +sprite_name = { "$"? ~ identifier } +sprite_dimensions = { + "[" ~ NUMBER ~ "x" ~ NUMBER ~ "/" ~ + (NUMBER ~ "z"? | ^"color") ~ + "]" +} +sprite_encoding = { ASCII_ALPHANUMERIC+ } +sprite_data = { ANY+ } + +hide_show_member = { + hide_or_show ~ visibility_list ~ member_type +} +hide_show_stereotype = { + hide_or_show ~ stereotype_target* ~ empty_kw? ~ stereotype_elem +} +hide_or_show = { ^"hide" | ^"show" } +visibility_list = { sequence_visibility ~ ("," ~ sequence_visibility)* } +sequence_visibility = { ^"public" | ^"private" | ^"protected" | ^"package" } +member_type = { ^"members" | ^"member" | ^"attributes" | ^"attribute" | ^"fields" | ^"field" | ^"methods" | ^"method" } +stereotype_target = { class_type | sequence_qualified_name | quoted_string | stereotype } +class_type = { ^"class" | ^"object" | ^"interface" | ^"enum" | ^"annotation" | ^"abstract" } +empty_kw = { ^"empty" } +stereotype_elem = { + ^"members" | ^"member" | ^"attributes" | ^"attribute" | ^"fields" | ^"field" | + ^"methods" | ^"method" | ^"circle" ~ ASCII_ALPHANUMERIC* | ^"stereotypes" | ^"stereotype" +} + +box_cmd = { + box_start + | box_end +} +box_start = { "box" ~ quoted_string? ~ color_spec? } +box_end = { ^"end" ~ ^"box" } + +divider = { "==" ~ divider_text? ~ "==" } +divider_text = @{ (!"==" ~ !EOL ~ ANY)+ } +delay = { + "||" ~ NUMBER? ~ "|"+ +} +ellipsis = { + "..." + | ("..." ~ sequence_text_content? ~ "...") +} + skin = { ^"skin" ~ sequence_qualified_name } -// Autonumber autonumber = { ^"autonumber" ~ autonumber_format? ~ autonumber_start? ~ autonumber_step? } autonumber_format = { (ASCII_DIGIT ~ (!(WHITESPACE | EOL) ~ ANY)* ~ ASCII_DIGIT) | ASCII_DIGIT } autonumber_start = { NUMBER } autonumber_step = { quoted_string } - autonumber_stop = { ^"autonumber" ~ ^"stop" } autonumber_resume = { ^"autonumber" ~ ^"resume" ~ NUMBER? ~ quoted_string? } autonumber_inc = { ^"autonumber" ~ ^"inc" ~ (ASCII_ALPHA)? } -// Autoactivate autoactivate = { ^"autoactivate" ~ autoactivate_state? } autoactivate_state = { ^"on" | ^"off" } -// Footbox footbox_cmd = { hide_or_show? ~ ^"footbox" ~ footbox_state? } footbox_state = { ^"on" | ^"off" } -sequence_qualified_name = @{ identifier ~ ("." ~ identifier)* } - -sequence_text_content = { (!EOL ~ ANY)+ } -sequence_description = { ":" ~ sequence_text_content? } +function_def = { ^"!function" ~ function_content } +function_return = { ^"!return" ~ function_content } +function_end = { ^"!endfunction" } +function_content = { (!EOL ~ ANY)+ } diff --git a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs index 9eb67170..2223d4fc 100644 --- a/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs +++ b/plantuml/parser/puml_parser/src/sequence_diagram/test/sequence_integration_test.rs @@ -110,3 +110,8 @@ fn test_ref_statement() { fn test_return_commands() { run_sequence_diagram_parser_case("return_commands"); } + +#[test] +fn test_ignored_blocks() { + run_sequence_diagram_parser_case("ignored_blocks"); +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/ignored_blocks.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/ignored_blocks.puml new file mode 100644 index 00000000..84170613 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/ignored_blocks.puml @@ -0,0 +1,35 @@ +' ******************************************************************************* +' Copyright (c) 2026 Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +@startuml ignored_blocks + + + +legend right + rendered by PlantUML only +end legend + +!transformation { + noop +!} + +sprite $icon { + 0000 +} + +Alice -> Bob : after ignored blocks + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/output.json new file mode 100644 index 00000000..c3b18064 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_blocks/output.json @@ -0,0 +1,39 @@ +{ + "ignored_blocks.puml": { + "name": "ignored_blocks", + "statements": [ + { + "Message": { + "left": { + "Participant": { + "display_name": "Alice", + "alias": null + } + }, + "arrow": { + "left": null, + "line": { + "raw": "-" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "right": { + "Participant": { + "display_name": "Bob", + "alias": null + } + }, + "suffix": null, + "description": "after ignored blocks", + "source_location": { + "file": "", + "line": 33 + } + } + } + ] + } +}