diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/component_note_alias_hidden_relation/component_note_alias_hidden_relation.puml b/plantuml/parser/integration_test/component_diagram/plantuml/component_note_alias_hidden_relation/component_note_alias_hidden_relation.puml new file mode 100644 index 00000000..783d3f01 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/component_note_alias_hidden_relation/component_note_alias_hidden_relation.puml @@ -0,0 +1,57 @@ +' ******************************************************************************* +' 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 + +component Baselibs +component Logging + +note "Component boundary" as N2 + +note as N1 + Repository boundary note. +end note + +note over Baselibs #PeachPuff + Over note. +end note + +note left + Left note. +end note + +note right + Right note. +end note + +note left of Baselibs + Left-of note. +end note + +note right of Logging + Right-of note. +end note + +note bottom of Baselibs + Bottom-of note. +end note + +note top of Logging + Top-of note. +end note + +Baselibs -[hidden]down-> N1 +Logging -[hidden]down-> N1 +Baselibs -[hidden]down-> N2 +Logging -[hidden]down-> N2 + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/component_note_alias_hidden_relation/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/component_note_alias_hidden_relation/output.json new file mode 100644 index 00000000..79616f4c --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/component_note_alias_hidden_relation/output.json @@ -0,0 +1,39 @@ +{ + "component_note_alias_hidden_relation.puml": { + "name": null, + "statements": [ + { + "Element": { + "identity": { + "name": "Baselibs", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 15 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "Logging", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 16 + } + }, + "style": null, + "statements": [] + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/element_modifier_order/element_modifier_order.puml b/plantuml/parser/integration_test/component_diagram/plantuml/element_modifier_order/element_modifier_order.puml new file mode 100644 index 00000000..83fb43d8 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/element_modifier_order/element_modifier_order.puml @@ -0,0 +1,16 @@ +' ******************************************************************************* +' 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 +component Backend <> as BackendAlias +component Storage as StorageAlias <> +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/element_modifier_order/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/element_modifier_order/output.json new file mode 100644 index 00000000..af62dcfe --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/element_modifier_order/output.json @@ -0,0 +1,39 @@ +{ + "element_modifier_order.puml": { + "name": null, + "statements": [ + { + "Element": { + "identity": { + "name": "Backend", + "alias": "BackendAlias", + "stereotype": "score_logging", + "element_kind": "component", + "source_location": { + "file": "", + "line": 14 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "Storage", + "alias": "StorageAlias", + "stereotype": "persistent", + "element_kind": "component", + "source_location": { + "file": "", + "line": 15 + } + }, + "style": null, + "statements": [] + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/element_style_keyword_prefix_color/element_style_keyword_prefix_color.puml b/plantuml/parser/integration_test/component_diagram/plantuml/element_style_keyword_prefix_color/element_style_keyword_prefix_color.puml new file mode 100644 index 00000000..46b7b1d9 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/element_style_keyword_prefix_color/element_style_keyword_prefix_color.puml @@ -0,0 +1,23 @@ +' ******************************************************************************* +' 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 element_style_keyword_prefix_color +component "Trace Collector" #linen +component "Signal Adapter" #line:linen +component "Status Fanout" #line.dashed:linen +component "Text Overlay" #text:linen +component "Control Plane" #LightSteelBlue +"Trace Collector" --> "Signal Adapter" +"Signal Adapter" --> "Status Fanout" +"Status Fanout" --> "Text Overlay" +"Text Overlay" --> "Control Plane" +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/element_style_keyword_prefix_color/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/element_style_keyword_prefix_color/output.json new file mode 100644 index 00000000..e3d3f580 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/element_style_keyword_prefix_color/output.json @@ -0,0 +1,190 @@ +{ + "element_style_keyword_prefix_color.puml": { + "name": "element_style_keyword_prefix_color", + "statements": [ + { + "Element": { + "identity": { + "name": "Trace Collector", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 14 + } + }, + "style": { + "color": null, + "attributes": [] + }, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "Signal Adapter", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 15 + } + }, + "style": { + "color": null, + "attributes": [] + }, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "Status Fanout", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 16 + } + }, + "style": { + "color": null, + "attributes": [] + }, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "Text Overlay", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 17 + } + }, + "style": { + "color": null, + "attributes": [] + }, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "Control Plane", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 18 + } + }, + "style": { + "color": null, + "attributes": [] + }, + "statements": [] + } + }, + { + "Relation": { + "lhs": "Trace Collector", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "rhs": "Signal Adapter", + "style": null, + "description": null, + "source_location": { + "file": "", + "line": 19 + } + } + }, + { + "Relation": { + "lhs": "Signal Adapter", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "rhs": "Status Fanout", + "style": null, + "description": null, + "source_location": { + "file": "", + "line": 20 + } + } + }, + { + "Relation": { + "lhs": "Status Fanout", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "rhs": "Text Overlay", + "style": null, + "description": null, + "source_location": { + "file": "", + "line": 21 + } + } + }, + { + "Relation": { + "lhs": "Text Overlay", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "rhs": "Control Plane", + "style": null, + "description": null, + "source_location": { + "file": "", + "line": 22 + } + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/ignored_legend_block/ignored_legend_block.puml b/plantuml/parser/integration_test/component_diagram/plantuml/ignored_legend_block/ignored_legend_block.puml new file mode 100644 index 00000000..5ff618bd --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/ignored_legend_block/ignored_legend_block.puml @@ -0,0 +1,32 @@ +' ******************************************************************************* +' 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_legend_block + +legend top left + rendered by PlantUML only + + supports blank lines + and multiline content + end legend + +legend right + compact terminator spelling +endlegend + +legend + direct newline after header +end legend + +[Producer] --> [Consumer] + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/ignored_legend_block/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/ignored_legend_block/output.json new file mode 100644 index 00000000..3a27e45d --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/ignored_legend_block/output.json @@ -0,0 +1,29 @@ +{ + "ignored_legend_block.puml": { + "name": "ignored_legend_block", + "statements": [ + { + "Relation": { + "lhs": "[Producer]", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "rhs": "[Consumer]", + "style": null, + "description": null, + "source_location": { + "file": "", + "line": 30 + } + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/interfaces/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/interfaces/output.json index f5917ee0..d0efc62f 100644 --- a/plantuml/parser/integration_test/component_diagram/plantuml/interfaces/output.json +++ b/plantuml/parser/integration_test/component_diagram/plantuml/interfaces/output.json @@ -5,7 +5,7 @@ { "Element": { "identity": { - "name": "\"Command Intake\"", + "name": "Command Intake", "alias": null, "stereotype": null, "element_kind": "interface", @@ -21,7 +21,7 @@ { "Element": { "identity": { - "name": "\"Status Broadcast\"", + "name": "Status Broadcast", "alias": "StatusOut", "stereotype": null, "element_kind": "interface", diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/relation_quoted_name/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/relation_quoted_name/output.json new file mode 100644 index 00000000..80b775c6 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/relation_quoted_name/output.json @@ -0,0 +1,61 @@ +{ + "relation_quoted_name.puml": { + "name": null, + "statements": [ + { + "Element": { + "identity": { + "name": "ApplicationLogic", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 14 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "score::mw::log", + "alias": null, + "stereotype": null, + "element_kind": "component", + "source_location": { + "file": "", + "line": 15 + } + }, + "style": null, + "statements": [] + } + }, + { + "Relation": { + "lhs": "ApplicationLogic", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "rhs": "score::mw::log", + "style": null, + "description": "verbose", + "source_location": { + "file": "", + "line": 16 + } + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/relation_quoted_name/relation_quoted_name.puml b/plantuml/parser/integration_test/component_diagram/plantuml/relation_quoted_name/relation_quoted_name.puml new file mode 100644 index 00000000..bdfafc42 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/relation_quoted_name/relation_quoted_name.puml @@ -0,0 +1,17 @@ +' ******************************************************************************* +' 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 +component ApplicationLogic +component "score::mw::log" +ApplicationLogic --> "score::mw::log" : verbose +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/skinparam_ignore/output.json b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam_ignore/output.json new file mode 100644 index 00000000..1bd74134 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam_ignore/output.json @@ -0,0 +1,109 @@ +{ + "skinparam_ignore.puml": { + "name": null, + "statements": [ + { + "Element": { + "identity": { + "name": "Backend", + "alias": null, + "stereotype": "score_logging", + "element_kind": "component", + "source_location": { + "file": "", + "line": 38 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "BaseLibs", + "alias": null, + "stereotype": "score_baselibs", + "element_kind": "component", + "source_location": { + "file": "", + "line": 39 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "SafetyGate", + "alias": null, + "stereotype": "safety critical", + "element_kind": "component", + "source_location": { + "file": "", + "line": 40 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "ReviewGate", + "alias": null, + "stereotype": "needs review", + "element_kind": "component", + "source_location": { + "file": "", + "line": 41 + } + }, + "style": null, + "statements": [] + } + }, + { + "Element": { + "identity": { + "name": "LogNode", + "alias": null, + "stereotype": "score_logging", + "element_kind": "node", + "source_location": { + "file": "", + "line": 42 + } + }, + "style": null, + "statements": [] + } + }, + { + "Relation": { + "lhs": "[Producer]", + "arrow": { + "left": null, + "line": { + "raw": "--" + }, + "middle": null, + "right": { + "raw": ">" + } + }, + "rhs": "[Consumer]", + "style": null, + "description": null, + "source_location": { + "file": "", + "line": 43 + } + } + } + ] + } +} diff --git a/plantuml/parser/integration_test/component_diagram/plantuml/skinparam_ignore/skinparam_ignore.puml b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam_ignore/skinparam_ignore.puml new file mode 100644 index 00000000..18c4930e --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/plantuml/skinparam_ignore/skinparam_ignore.puml @@ -0,0 +1,45 @@ +' ******************************************************************************* +' 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 + +skinparam component { + FontSize 12 +} +skinparam component { + BackgroundColor<> #FDEBD0 +} +skinparam component { + BackgroundColor<> #E8F4FA +} +skinparam node { + BackgroundColor<> #FDEBD0 +} +skinparam component { + backgroundColor<> Aquamarine + fontColor<> White +} +skinparam queue { + backgroundColor<> Aquamarine +} +skinparam componentStyle rectangle +skinparam packageStyle rectangle +skinparam linetype ortho + +component Backend <> +component BaseLibs <> +component SafetyGate <> +component ReviewGate <> +node LogNode <> +[Producer] --> [Consumer] + +@enduml diff --git a/plantuml/parser/integration_test/component_diagram/relation_quoted_name/output.json b/plantuml/parser/integration_test/component_diagram/relation_quoted_name/output.json new file mode 100644 index 00000000..f4a34de9 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_quoted_name/output.json @@ -0,0 +1,41 @@ +{ + "relation_quoted_name.puml": { + "ApplicationLogic": { + "id": "ApplicationLogic", + "name": "ApplicationLogic", + "alias": null, + "parent_id": null, + "comp_type": "Component", + "stereotype": null, + "relations": [ + { + "target": "score::mw::log", + "annotation": "verbose", + "relation_type": "Dependency", + "source_role": "None", + "source_location": { + "file": "", + "line": 16 + } + } + ], + "source_location": { + "file": "", + "line": 14 + } + }, + "score::mw::log": { + "id": "score::mw::log", + "name": "score::mw::log", + "alias": null, + "parent_id": null, + "comp_type": "Component", + "stereotype": null, + "relations": [], + "source_location": { + "file": "", + "line": 15 + } + } + } +} diff --git a/plantuml/parser/integration_test/component_diagram/relation_quoted_name/relation_quoted_name.puml b/plantuml/parser/integration_test/component_diagram/relation_quoted_name/relation_quoted_name.puml new file mode 100644 index 00000000..56b40318 --- /dev/null +++ b/plantuml/parser/integration_test/component_diagram/relation_quoted_name/relation_quoted_name.puml @@ -0,0 +1,17 @@ +' ******************************************************************************* +' 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 relation_quoted_name +component ApplicationLogic +component "score::mw::log" +ApplicationLogic --> "score::mw::log" : verbose +@enduml diff --git a/plantuml/parser/integration_test/sequence_diagram/sequence_message_direction/output.json b/plantuml/parser/integration_test/sequence_diagram/sequence_message_direction/output.json new file mode 100644 index 00000000..f88053a2 --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/sequence_message_direction/output.json @@ -0,0 +1,96 @@ +{ + "sequence_message_direction.puml": { + "name": "sequence_message_direction", + "participants": [ + { + "display_name": "Service", + "alias": null, + "participant_type": "Participant", + "source_location": { "file": "", "line": 16 }, + "stereotype": null + }, + { + "display_name": "Client1", + "alias": null, + "participant_type": "Participant", + "source_location": { "file": "", "line": 17 }, + "stereotype": null + }, + { + "display_name": "Client2", + "alias": null, + "participant_type": "Participant", + "source_location": { "file": "", "line": 18 }, + "stereotype": null + } + ], + "root": { + "items": [ + { + "Interaction": { + "sender": "Service", + "receiver": "Client1", + "message": "solid right", + "source_location": { "file": "", "line": 20 } + } + }, + { + "Interaction": { + "sender": "Client2", + "receiver": "Service", + "message": "solid left", + "source_location": { "file": "", "line": 21 } + } + }, + { + "Interaction": { + "sender": "Service", + "receiver": "Client1", + "message": "dashed right", + "source_location": { "file": "", "line": 22 } + } + }, + { + "Interaction": { + "sender": "Client2", + "receiver": "Service", + "message": "dashed left", + "source_location": { "file": "", "line": 23 } + } + }, + { + "Interaction": { + "sender": "Service", + "receiver": "Client1", + "message": "cross head right", + "source_location": { "file": "", "line": 24 } + } + }, + { + "Interaction": { + "sender": "Service", + "receiver": "Client1", + "message": "cross head right dashed", + "source_location": { "file": "", "line": 25 } + } + }, + { + "Interaction": { + "sender": "Client2", + "receiver": "Service", + "message": "cross head left", + "source_location": { "file": "", "line": 26 } + } + }, + { + "Interaction": { + "sender": "Client2", + "receiver": "Service", + "message": "cross head left dashed", + "source_location": { "file": "", "line": 27 } + } + } + ] + } + } +} diff --git a/plantuml/parser/integration_test/sequence_diagram/sequence_message_direction/sequence_message_direction.puml b/plantuml/parser/integration_test/sequence_diagram/sequence_message_direction/sequence_message_direction.puml new file mode 100644 index 00000000..80bc836b --- /dev/null +++ b/plantuml/parser/integration_test/sequence_diagram/sequence_message_direction/sequence_message_direction.puml @@ -0,0 +1,29 @@ +' ******************************************************************************* +' 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 sequence_message_direction + +participant Service +participant Client1 +participant Client2 + +Service -> Client1 : solid right +Service <-- Client2 : solid left +Service --> Client1 : dashed right +Service <--- Client2 : dashed left +Service -[#red]x Client1 : cross head right +Service --[#orange]x Client1 : cross head right dashed +Service x-[#blue] Client2 : cross head left +Service x--[#green] Client2 : cross head left dashed + +@enduml diff --git a/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs b/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs index 48645e76..785e7524 100644 --- a/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs +++ b/plantuml/parser/puml_parser/src/class_diagram/src/class_parser.rs @@ -22,7 +22,8 @@ use crate::source_map::{ use log::{debug, trace}; use parser_core::common_parser::{parse_arrow, PlantUmlCommonParser, Rule}; use parser_core::{ - format_parse_tree, pest_to_syntax_error, BaseParseError, DiagramParser, ErrorLocation, + find_note_alias, format_parse_tree, pest_to_syntax_error, BaseParseError, DiagramParser, + ErrorLocation, IgnoredNoteRegistry, }; use pest::Parser; use puml_utils::LogLevel; @@ -111,21 +112,6 @@ impl IgnoredObjectRegistry { } } -#[derive(Debug, Default)] -struct IgnoredNoteRegistry { - aliases: HashSet, -} - -impl IgnoredNoteRegistry { - fn register(&mut self, alias: Name) { - self.aliases.insert(alias.internal); - } - - fn filters_relationship(&self, relationship: &Relationship) -> bool { - self.aliases.contains(&relationship.left) || self.aliases.contains(&relationship.right) - } -} - struct ClassParseSession<'a> { normalized_content: &'a NormalizedContent, source_file: Rc, @@ -570,21 +556,6 @@ impl ClassParseSession<'_> { name } - fn parse_note_alias(pair: pest::iterators::Pair) -> Option { - fn find_alias(pair: pest::iterators::Pair) -> Option { - if pair.as_rule() == Rule::note_alias { - return Some(pair.as_str().to_string()); - } - - pair.into_inner().find_map(find_alias) - } - - find_alias(pair).map(|internal| Name { - internal, - ..Name::default() - }) - } - fn filter_relationships( relationships: Vec, ignored_objects: &IgnoredObjectRegistry, @@ -907,7 +878,7 @@ impl ClassParseSession<'_> { ) -> Result, ClassError> { match pair.as_rule() { Rule::note_declaration => { - if let Some(alias) = Self::parse_note_alias(pair) { + if let Some(alias) = find_note_alias(pair) { self.ignored_notes.register(alias); } Ok(vec![]) @@ -958,7 +929,7 @@ impl ClassParseSession<'_> { for top_level_inner in Self::flatten_top_level(inner) { match top_level_inner.as_rule() { Rule::note_declaration => { - if let Some(alias) = Self::parse_note_alias(top_level_inner) { + if let Some(alias) = find_note_alias(top_level_inner) { self.ignored_notes.register(alias); } } @@ -1013,7 +984,7 @@ impl ClassParseSession<'_> { for t in Self::flatten_top_level(inner) { match t.as_rule() { Rule::note_declaration => { - if let Some(alias) = Self::parse_note_alias(t) { + if let Some(alias) = find_note_alias(t) { self.ignored_notes.register(alias); } } @@ -1055,7 +1026,11 @@ impl ClassParseSession<'_> { &Some(package.name.internal.clone()), ) .into_iter() - .filter(|relationship| !self.ignored_notes.filters_relationship(relationship)) + .filter(|relationship| { + !self + .ignored_notes + .filters_endpoints(&relationship.left, &relationship.right) + }) .collect(); Ok((package, ignored_objects)) @@ -1202,7 +1177,11 @@ impl DiagramParser for PumlClassParser { &None, ) .into_iter() - .filter(|relationship| !session.ignored_notes.filters_relationship(relationship)) + .filter(|relationship| { + !session + .ignored_notes + .filters_endpoints(&relationship.left, &relationship.right) + }) .collect(); } Err(e) => { @@ -1416,7 +1395,7 @@ mod tests { let mut session = make_test_session(&normalized); session.parse_top_level_element(pair).unwrap(); - assert!(session.ignored_notes.aliases.contains("SyncNote")); + assert!(session.ignored_notes.contains("SyncNote")); } #[test] @@ -1431,7 +1410,7 @@ mod tests { let mut session = make_test_session(&normalized); session.parse_top_level_element(pair).unwrap(); - assert!(session.ignored_notes.aliases.contains("SyncNote")); + assert!(session.ignored_notes.contains("SyncNote")); } #[test] diff --git a/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs b/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs index b4b1f718..b49590c1 100644 --- a/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs +++ b/plantuml/parser/puml_parser/src/class_diagram/test/integration_test.rs @@ -100,6 +100,11 @@ fn test_friend_method() { run_class_diagram_parser_case("friend_method"); } +#[test] +fn test_ignored_visibility_statements() { + run_class_diagram_parser_case("ignored_visibility_statements"); +} + #[test] fn test_method_modifier_placement() { run_class_diagram_parser_case("method_modifier_placement"); diff --git a/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs b/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs index 13c77a3a..feb0ec04 100644 --- a/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs +++ b/plantuml/parser/puml_parser/src/component_diagram/src/component_parser.rs @@ -20,7 +20,8 @@ use crate::{ Statement, }; use parser_core::{ - format_parse_tree, pest_to_syntax_error, BaseParseError, DiagramParser, ErrorLocation, + find_note_alias, format_parse_tree, is_note_rule, pest_to_syntax_error, BaseParseError, + DiagramParser, ErrorLocation, IgnoredNoteRegistry, }; use puml_utils::LogLevel; use source_location::SourceLocation; @@ -56,17 +57,25 @@ pub struct PumlComponentParser; // lobster-trace: Tools.ArchitectureModelingComponentHierarchyComponent // lobster-trace: Tools.ArchitectureModelingComponentInteract impl PumlComponentParser { + fn register_ignored_note( + ignored_notes: &mut IgnoredNoteRegistry, + pair: pest::iterators::Pair, + ) { + if let Some(alias) = find_note_alias(pair) { + ignored_notes.register(alias); + } + } + fn parse_statement( pair: pest::iterators::Pair, source_file: &str, + ignored_notes: &mut IgnoredNoteRegistry, ) -> Result, ComponentError> { for inner in pair.into_inner() { match inner.as_rule() { Rule::element => { - return Ok(vec![Statement::Element(Self::parse_element( - inner, - source_file, - )?)]); + let element = Self::parse_element(inner, source_file, ignored_notes)?; + return Ok(vec![Statement::Element(element)]); } Rule::relation => { return Ok(vec![Statement::Relation(Self::parse_relation( @@ -79,7 +88,11 @@ impl PumlComponentParser { } Rule::together_block => { // Flatten children into the enclosing scope (drop the wrapper) - return Self::parse_together_block(inner, source_file); + return Self::parse_together_block(inner, source_file, ignored_notes); + } + _ if is_note_rule(inner.as_rule()) => { + Self::register_ignored_note(ignored_notes, inner); + return Ok(vec![]); } _ => {} } @@ -122,11 +135,22 @@ impl PumlComponentParser { fn parse_together_block( pair: pest::iterators::Pair, source_file: &str, + ignored_notes: &mut IgnoredNoteRegistry, ) -> Result, ComponentError> { let mut stmts = Vec::new(); for inner in pair.into_inner() { - if inner.as_rule() == Rule::diagram_statement { - stmts.append(&mut Self::parse_statement(inner, source_file)?); + match inner.as_rule() { + Rule::diagram_statement => { + stmts.append(&mut Self::parse_statement( + inner, + source_file, + ignored_notes, + )?); + } + _ if is_note_rule(inner.as_rule()) => { + Self::register_ignored_note(ignored_notes, inner); + } + _ => {} } } Ok(stmts) @@ -135,6 +159,7 @@ impl PumlComponentParser { fn parse_element( pair: pest::iterators::Pair, source_file: &str, + ignored_notes: &mut IgnoredNoteRegistry, ) -> Result { let source_location = SourceLocation::new(source_file, pair.line_col().0 as u32); let mut kind = String::new(); @@ -197,11 +222,25 @@ impl PumlComponentParser { Rule::stereotype => { stereotype = Self::extract_stereotype(inner); } + Rule::element_modifier => { + for modifier in inner.into_inner() { + match modifier.as_rule() { + Rule::alias_clause => { + alias = Self::extract_alias(modifier); + } + Rule::stereotype => { + stereotype = Self::extract_stereotype(modifier); + } + _ => {} + } + } + } Rule::element_style => { element.style = Some(Self::parse_component_style(inner)?); } Rule::statement_block => { - element.statements = Self::parse_statement_block(inner, source_file)?; + element.statements = + Self::parse_statement_block(inner, source_file, ignored_notes)?; } _ => {} } @@ -232,10 +271,10 @@ impl PumlComponentParser { for inner in pair.into_inner() { match inner.as_rule() { Rule::relation_left => { - lhs = inner.as_str().to_string(); + lhs = Self::strip_wrapping_quotes(inner.as_str()); } Rule::relation_right => { - rhs = inner.as_str().to_string(); + rhs = Self::strip_wrapping_quotes(inner.as_str()); } Rule::connection_arrow => { arrow = Self::parse_arrow(inner)?; @@ -288,7 +327,7 @@ impl PumlComponentParser { fn extract_interface_name(pair: pest::iterators::Pair) -> String { pair.into_inner() .find(|inner| inner.as_rule() == Rule::short_form_interface_name) - .map(|inner| inner.as_str().to_string()) + .map(|inner| Self::strip_wrapping_quotes(inner.as_str())) .unwrap_or_default() } @@ -311,6 +350,14 @@ impl PumlComponentParser { .map(|inner| inner.as_str().to_string()) } + fn strip_wrapping_quotes(raw: &str) -> String { + if let Some(stripped) = raw.strip_prefix('"').and_then(|s| s.strip_suffix('"')) { + return stripped.to_string(); + } + + raw.to_string() + } + fn parse_default_element( pair: pest::iterators::Pair, ) -> Result<(String, Option), ComponentError> { @@ -323,14 +370,7 @@ impl PumlComponentParser { kind = inner.as_str().to_string(); } Rule::default_element_name => { - let raw_name = inner.as_str().to_string(); - // Remove surrounding quotes if present - let clean_name = if raw_name.starts_with('"') && raw_name.ends_with('"') { - raw_name[1..raw_name.len() - 1].to_string() - } else { - raw_name - }; - name = Some(clean_name); + name = Some(Self::strip_wrapping_quotes(inner.as_str())); } _ => {} } @@ -366,23 +406,49 @@ impl PumlComponentParser { fn parse_statement_block( pair: pest::iterators::Pair, source_file: &str, + ignored_notes: &mut IgnoredNoteRegistry, ) -> Result, ComponentError> { let mut statements = Vec::new(); for inner in pair.into_inner() { match inner.as_rule() { Rule::diagram_statement => { - let mut stmts = Self::parse_statement(inner, source_file)?; + let mut stmts = Self::parse_statement(inner, source_file, ignored_notes)?; statements.append(&mut stmts); } - _ => { - // Skip empty lines and other rules like braces + _ if is_note_rule(inner.as_rule()) => { + Self::register_ignored_note(ignored_notes, inner); } + _ => {} } } Ok(statements) } + + fn filter_note_relations( + statements: Vec, + ignored_notes: &IgnoredNoteRegistry, + ) -> Vec { + statements + .into_iter() + .filter_map(|statement| match statement { + Statement::Relation(relation) => { + if ignored_notes.filters_endpoints(&relation.lhs, &relation.rhs) { + None + } else { + Some(Statement::Relation(relation)) + } + } + Statement::Element(mut element) => { + element.statements = + Self::filter_note_relations(element.statements, ignored_notes); + Some(Statement::Element(element)) + } + other => Some(other), + }) + .collect() + } } impl DiagramParser for PumlComponentParser { @@ -419,6 +485,7 @@ impl DiagramParser for PumlComponentParser { statements: Vec::new(), }; let source_file = path.as_ref().clone().to_string_lossy().to_string(); + let mut ignored_notes = IgnoredNoteRegistry::default(); for pair in pairs { for inner_pair in pair.into_inner() { @@ -432,9 +499,13 @@ impl DiagramParser for PumlComponentParser { } } Rule::diagram_statement => { - let mut stmts = Self::parse_statement(inner_pair, &source_file)?; + let mut stmts = + Self::parse_statement(inner_pair, &source_file, &mut ignored_notes)?; document.statements.append(&mut stmts); } + _ if is_note_rule(inner_pair.as_rule()) => { + Self::register_ignored_note(&mut ignored_notes, inner_pair); + } _ => { // Skip empty lines and other rules like enduml } @@ -442,6 +513,8 @@ impl DiagramParser for PumlComponentParser { } } + document.statements = Self::filter_note_relations(document.statements, &ignored_notes); + Ok(document) } } @@ -547,4 +620,82 @@ mod dispatch_style_tests { expected_file.as_str() ); } + + #[test] + fn test_single_line_note_alias_relation_is_filtered() { + let input = + "@startuml\ncomponent Baselibs\nnote \"Repository boundary\" as N1\nBaselibs -[hidden]down-> N1\n@enduml"; + let mut parser = PumlComponentParser; + let doc = parser + .parse_file(&Rc::new(PathBuf::from("t.puml")), input, LogLevel::Info) + .expect("valid input must parse"); + + assert_eq!(doc.statements.len(), 1); + assert!(matches!(doc.statements[0], Statement::Element(_))); + } + + #[test] + fn test_multiline_note_alias_relation_is_filtered() { + let input = "@startuml\ncomponent Baselibs\nnote as N1\n Repository boundary\nend note\nBaselibs -[hidden]down-> N1\n@enduml"; + let mut parser = PumlComponentParser; + let doc = parser + .parse_file(&Rc::new(PathBuf::from("t.puml")), input, LogLevel::Info) + .expect("valid input must parse"); + + assert_eq!(doc.statements.len(), 1); + assert!(matches!(doc.statements[0], Statement::Element(_))); + } + + #[test] + fn test_quoted_component_name_matches_quoted_relation_endpoint() { + let input = "@startuml\ncomponent \"score::mw::log\"\ncomponent ApplicationLogic\nApplicationLogic --> \"score::mw::log\"\n@enduml"; + let mut parser = PumlComponentParser; + let doc = parser + .parse_file(&Rc::new(PathBuf::from("t.puml")), input, LogLevel::Info) + .expect("valid input must parse"); + + let element_name = match &doc.statements[0] { + Statement::Element(element) => element + .identity + .name + .as_deref() + .expect("component name must be present"), + actual => panic!( + "expected first statement to be an element, got {:?}", + actual + ), + }; + + let relation_rhs = match &doc.statements[2] { + Statement::Relation(relation) => relation.rhs.as_str(), + actual => panic!( + "expected third statement to be a relation, got {:?}", + actual + ), + }; + + assert_eq!(element_name, "score::mw::log"); + assert_eq!(relation_rhs, "score::mw::log"); + } + + #[test] + fn test_nested_element_stereotype_before_alias_is_accepted() { + let input = "@startuml\ncomponent Example <> as ExampleAlias\n@enduml"; + let mut parser = PumlComponentParser; + let doc = parser + .parse_file(&Rc::new(PathBuf::from("t.puml")), input, LogLevel::Info) + .expect("valid input must parse"); + + let element = match &doc.statements[0] { + Statement::Element(element) => element, + actual => panic!( + "expected first statement to be an element, got {:?}", + actual + ), + }; + + assert_eq!(element.identity.name.as_deref(), Some("Example")); + assert_eq!(element.identity.alias.as_deref(), Some("ExampleAlias")); + assert_eq!(element.identity.stereotype.as_deref(), Some("component")); + } } diff --git a/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs b/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs index ab15d8fb..ed1f41b4 100644 --- a/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs +++ b/plantuml/parser/puml_parser/src/component_diagram/test/component_integration_test.rs @@ -101,6 +101,21 @@ fn test_individual_colors() { run_component_diagram_parser_case("individual_colors"); } +#[test] +fn test_element_style_keyword_prefix_color() { + run_component_diagram_parser_case("element_style_keyword_prefix_color"); +} + +#[test] +fn test_element_modifier_order() { + run_component_diagram_parser_case("element_modifier_order"); +} + +#[test] +fn test_ignored_legend_block() { + run_component_diagram_parser_case("ignored_legend_block"); +} + #[test] fn test_interfaces() { run_component_diagram_parser_case("interfaces"); @@ -115,3 +130,18 @@ fn test_long_description() { fn test_use_uml2_notation() { run_component_diagram_parser_case("use_uml2_notation"); } + +#[test] +fn test_skinparam_ignore() { + run_component_diagram_parser_case("skinparam_ignore"); +} + +#[test] +fn test_relation_quoted_name() { + run_component_diagram_parser_case("relation_quoted_name"); +} + +#[test] +fn test_component_note_alias_hidden_relation() { + run_component_diagram_parser_case("component_note_alias_hidden_relation"); +} diff --git a/plantuml/parser/puml_parser/src/grammar/activity.pest b/plantuml/parser/puml_parser/src/grammar/activity.pest index ba5aaed8..33d5e8d2 100644 --- a/plantuml/parser/puml_parser/src/grammar/activity.pest +++ b/plantuml/parser/puml_parser/src/grammar/activity.pest @@ -46,6 +46,7 @@ statement = _{ // base | title_stmt + | note_annotation_stmt | note_declaration | backward_stmt | arrow_stmt diff --git a/plantuml/parser/puml_parser/src/grammar/class.pest b/plantuml/parser/puml_parser/src/grammar/class.pest index 80931a2f..10d49986 100644 --- a/plantuml/parser/puml_parser/src/grammar/class.pest +++ b/plantuml/parser/puml_parser/src/grammar/class.pest @@ -68,12 +68,16 @@ class_start = { ~ (COMMENT | empty_line)* ~ startuml ~ EOL* - ~ (top_level ~ EOL* | ignored_block ~ EOL* | ignored_stmt ~ EOL+)* + ~ (top_level ~ EOL* | ignored_block ~ EOL* | ignored_stmt ~ EOL+ | class_ignored_stmt ~ EOL+)* ~ EOL* ~ enduml ~ EOI } +class_ignored_stmt = _{ + shared_hide_show_empty_member_core +} + top_level = { namespace_def | type_def @@ -95,16 +99,6 @@ together_def = { ~ "}" } -legend_block = @{ - ^"legend" - ~ ( - !(WHITESPACE* ~ (^"endlegend" | (^"end" ~ WHITESPACE+ ~ ^"legend"))) - ~ ANY - )* - ~ WHITESPACE* - ~ (^"endlegend" | (^"end" ~ WHITESPACE+ ~ ^"legend")) -} - /////////////////////////////////////////////////////////////// // namespace / package /////////////////////////////////////////////////////////////// diff --git a/plantuml/parser/puml_parser/src/grammar/common.pest b/plantuml/parser/puml_parser/src/grammar/common.pest index 2b75ceb5..6cca588a 100644 --- a/plantuml/parser/puml_parser/src/grammar/common.pest +++ b/plantuml/parser/puml_parser/src/grammar/common.pest @@ -97,18 +97,52 @@ COMMENT = _{ // scale 1.2 //////////////////////////////////////////////////////////////////////////////// ignored_block = _{ - style_block + style_block + | skinparam_block + | legend_block | note_multiline } style_block = { "" ~ EOL? } +legend_block = @{ + legend_block_start + ~ EOL + ~ ( + !(WHITESPACE* ~ legend_block_end) + ~ ANY + )* + ~ WHITESPACE* + ~ legend_block_end +} + +legend_block_start = { + ^"legend" + ~ (INLINE_WS+ ~ legend_pos)? + ~ (INLINE_WS+ ~ legend_align)? + ~ INLINE_WS* +} +legend_pos = { ^"top" | ^"bottom" } +legend_align = { ^"left" | ^"right" | ^"center" } +legend_block_end = { ^"endlegend" | (^"end" ~ WHITESPACE+ ~ ^"legend") } + +skinparam_block = @{ + ^"skinparam" ~ INLINE_WS+ ~ (!("{" | NEWLINE) ~ ANY)* ~ "{" ~ INLINE_WS* ~ NEWLINE + ~ ( + !(WHITESPACE* ~ "}" ~ EOL?) ~ + (!NEWLINE ~ ANY)* ~ + NEWLINE + )* ~ + WHITESPACE* ~ "}" ~ EOL? +} + ignored_stmt = _{ direction_stmt | skinparam_stmt | theme_stmt | title_stmt + | note_annotation_stmt | note_single_line | scale_stmt | namespace_separator_stmt @@ -117,6 +151,16 @@ ignored_stmt = _{ skinparam_stmt = @{ ^"skinparam" ~ LINE_REST } theme_stmt = @{ ^"!theme" ~ (INLINE_WS+ ~ (!EOL ~ ANY)*)? } title_stmt = @{ ^"title" ~ LINE_REST } +note_annotation_stmt = @{ ^"note:" ~ LINE_REST } +shared_hide_or_show = { ^"hide" | ^"show" } +shared_empty_kw = { ^"empty" } +shared_member_type = { + ^"members" | ^"member" | ^"attributes" | ^"attribute" | + ^"fields" | ^"field" | ^"methods" | ^"method" +} +shared_hide_show_empty_member_core = { + shared_hide_or_show ~ shared_empty_kw ~ shared_member_type +} direction_stmt = @{ ("left" | "right" | "top" | "bottom") ~ " to " @@ -196,7 +240,7 @@ note_multiline = { } note_multiline_alias = { - note_multiline_alias_head ~ note_alias ~ note_multiline_tail + note_multiline_alias_head ~ note_alias ~ note_multiline_body } note_multiline_alias_head = @{ @@ -208,12 +252,13 @@ note_multiline_alias_head = @{ note_multiline_plain = @{ ^"note" ~ - !(INLINE_WS+ ~ "as" ~ INLINE_WS+) ~ - note_multiline_tail + !(INLINE_WS+ ~ (STRING ~ INLINE_WS+)? ~ ^"as" ~ INLINE_WS+) ~ + INLINE_WS+ ~ + (("::") | (!(":" | NEWLINE) ~ ANY))* ~ + note_multiline_body } -note_multiline_tail = ${ - (!NEWLINE ~ ANY)* ~ // rest of header line +note_multiline_body = ${ NEWLINE ~ ( !(INLINE_WS* ~ note_end ~ (INLINE_WS* ~ EOL)) ~ diff --git a/plantuml/parser/puml_parser/src/grammar/component.pest b/plantuml/parser/puml_parser/src/grammar/component.pest index f438be49..651644d0 100644 --- a/plantuml/parser/puml_parser/src/grammar/component.pest +++ b/plantuml/parser/puml_parser/src/grammar/component.pest @@ -13,7 +13,7 @@ // PlantUML Component Diagram Grammar for Pest Parser -diagram_start = { empty_line* ~ startuml ~ (diagram_statement | empty_line)* ~ enduml } +diagram_start = { empty_line* ~ startuml ~ (ignored_block | diagram_statement | empty_line)* ~ enduml } diagram_statement = { ((ignored_stmt | relation | together_block | port_declaration | element | footer_line) ~ EOL) } @@ -21,10 +21,12 @@ port_declaration = { port_keyword ~ port_name ~ alias_clause? } port_keyword = @{ "portin" | "portout" | "port" } port_name = { CNAME } -together_block = { "together" ~ "{" ~ EOL ~ (diagram_statement | empty_line)* ~ "}" } +together_block = { "together" ~ "{" ~ EOL ~ (ignored_block | diagram_statement | empty_line)* ~ "}" } + +element_modifier = { (alias_clause ~ stereotype?) | (stereotype ~ alias_clause?) } element = { - (nested_element ~ alias_clause? ~ stereotype? ~ element_style? ~ (long_description | statement_block)? ) | + (nested_element ~ element_modifier? ~ element_style? ~ (long_description | statement_block)? ) | ((short_form_actor | short_form_interface | short_form_component | short_form_usecase) ~ alias_clause? ~ element_style?) } long_description = {"[" ~ EOL? ~ (ASCII_ALPHANUMERIC ~ EOL?)* ~ "]"} @@ -46,11 +48,11 @@ default_element_name = { CNAME } nested_element = { bracket_element | default_element } -statement_block = { "{" ~ EOL ~ (diagram_statement | empty_line)* ~ "}" } +statement_block = { "{" ~ EOL ~ (ignored_block | diagram_statement | empty_line)* ~ "}" } relation = { relation_left ~ connection_arrow ~ relation_right ~ element_style? ~ relation_description?} -relation_left = { QUALIFIED_NAME | short_form_actor | short_form_component | short_form_interface | short_form_usecase } -relation_right = { QUALIFIED_NAME | short_form_actor | short_form_component | short_form_interface | short_form_usecase } +relation_left = { QUALIFIED_NAME | CNAME | short_form_actor | short_form_component | short_form_interface | short_form_usecase } +relation_right = { QUALIFIED_NAME | CNAME | short_form_actor | short_form_component | short_form_interface | short_form_usecase } // Terminals QUALIFIED_NAME = @{ NAME ~ ("." ~ NAME)* } @@ -84,10 +86,10 @@ comment_start = _{ "//" | "'" | "/'" } // This is ~~wave-underlined~~ element_style = { - ("#" ~ component_color ~ (";" ~ component_attr)* ~ ";") | - ("#" ~ component_color ~ (";" ~ component_attr)*) | ("#" ~ component_attr ~ (";" ~ component_attr)* ~ ";") | - ("#" ~ component_attr ~ (";" ~ component_attr)*) + ("#" ~ component_attr ~ (";" ~ component_attr)*) | + ("#" ~ component_color ~ (";" ~ component_attr)* ~ ";") | + ("#" ~ component_color ~ (";" ~ component_attr)*) } component_attr = { @@ -98,8 +100,8 @@ component_attr = { component_color = { COLOR_COMPONENT_GRAD | COLOR_COMPONENT_COMMON } COMPONENT_LINE_ATTR = @{ "dashed" | "bold" | "dotted" } -COMPONENT_TEXT_COLOR = @{ "text" } -COMPONENT_LINE_COLOR = @{ "line" } +COMPONENT_TEXT_COLOR = @{ "text" ~ !ASCII_ALPHANUMERIC } +COMPONENT_LINE_COLOR = @{ "line" ~ !ASCII_ALPHANUMERIC } relation_description = { ":" ~ description_text } description_text = @{ (!("\n" | "\r") ~ ANY)* } diff --git a/plantuml/parser/puml_parser/src/grammar/sequence.pest b/plantuml/parser/puml_parser/src/grammar/sequence.pest index c3eb1fc0..d3742a86 100644 --- a/plantuml/parser/puml_parser/src/grammar/sequence.pest +++ b/plantuml/parser/puml_parser/src/grammar/sequence.pest @@ -211,20 +211,9 @@ 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)* @@ -269,8 +258,9 @@ sequence_layout_stmt = _{ sequence_visibility_stmt = _{ hide_unlinked | show_unlinked - | hide_show_member - | hide_show_stereotype + | shared_hide_show_empty_member_core + | sequence_hide_show_member + | sequence_hide_show_stereotype } sequence_rendering_stmt = _{ @@ -312,19 +302,17 @@ sprite_dimensions = { sprite_encoding = { ASCII_ALPHANUMERIC+ } sprite_data = { ANY+ } -hide_show_member = { - hide_or_show ~ visibility_list ~ member_type +sequence_hide_show_member = { + shared_hide_or_show ~ visibility_list ~ shared_member_type } -hide_show_stereotype = { - hide_or_show ~ stereotype_target* ~ empty_kw? ~ stereotype_elem +sequence_hide_show_stereotype = { + shared_hide_or_show ~ stereotype_target* ~ shared_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 } +stereotype_target = { class_type | sequence_target_name | quoted_string | stereotype } +sequence_target_name = { !shared_empty_kw ~ !stereotype_elem ~ sequence_qualified_name } 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" @@ -362,7 +350,7 @@ autonumber_inc = { ^"autonumber" ~ ^"inc" ~ (ASCII_ALPHA)? } autoactivate = { ^"autoactivate" ~ autoactivate_state? } autoactivate_state = { ^"on" | ^"off" } -footbox_cmd = { hide_or_show? ~ ^"footbox" ~ footbox_state? } +footbox_cmd = { shared_hide_or_show? ~ ^"footbox" ~ footbox_state? } footbox_state = { ^"on" | ^"off" } function_def = { ^"!function" ~ function_content } diff --git a/plantuml/parser/puml_parser/src/parser_core/BUILD b/plantuml/parser/puml_parser/src/parser_core/BUILD index 3613b7ea..2b4d67a5 100644 --- a/plantuml/parser/puml_parser/src/parser_core/BUILD +++ b/plantuml/parser/puml_parser/src/parser_core/BUILD @@ -19,6 +19,7 @@ rust_library( "src/common_parser.rs", "src/error.rs", "src/lib.rs", + "src/note_utils.rs", ], compile_data = [ "//plantuml/parser/puml_parser/src/grammar:grammar_files", diff --git a/plantuml/parser/puml_parser/src/parser_core/src/lib.rs b/plantuml/parser/puml_parser/src/parser_core/src/lib.rs index eda837f4..13e4fa4e 100644 --- a/plantuml/parser/puml_parser/src/parser_core/src/lib.rs +++ b/plantuml/parser/puml_parser/src/parser_core/src/lib.rs @@ -13,10 +13,12 @@ pub mod common_ast; pub mod common_parser; pub mod error; +pub mod note_utils; pub use common_ast::*; pub use common_parser::*; pub use error::{pest_to_syntax_error, BaseParseError, ErrorLocation}; +pub use note_utils::*; /// Recursively format a Pest parse tree into an indented string for diagnostic output. /// diff --git a/plantuml/parser/puml_parser/src/parser_core/src/note_utils.rs b/plantuml/parser/puml_parser/src/parser_core/src/note_utils.rs new file mode 100644 index 00000000..61aff919 --- /dev/null +++ b/plantuml/parser/puml_parser/src/parser_core/src/note_utils.rs @@ -0,0 +1,82 @@ +// ******************************************************************************* +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +use std::collections::HashSet; + +use crate::common_parser::Rule; + +#[derive(Debug, Default)] +pub struct IgnoredNoteRegistry { + aliases: HashSet, +} + +impl IgnoredNoteRegistry { + pub fn register(&mut self, alias: impl Into) { + self.aliases.insert(alias.into()); + } + + pub fn contains(&self, alias: &str) -> bool { + self.aliases.contains(alias) + } + + pub fn filters_endpoints(&self, left: &str, right: &str) -> bool { + self.contains(left) || self.contains(right) + } +} + +pub fn is_note_rule(rule: Rule) -> bool { + matches!( + rule, + Rule::note_single_line | Rule::note_multiline | Rule::note_declaration + ) +} + +pub fn find_note_alias(pair: pest::iterators::Pair) -> Option { + if pair.as_rule() == Rule::note_alias { + return Some(pair.as_str().to_string()); + } + + pair.into_inner().find_map(find_note_alias) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common_parser::PlantUmlCommonParser; + use pest::Parser; + + #[test] + fn test_find_note_alias_from_single_line_note() { + let pair = PlantUmlCommonParser::parse( + Rule::note_declaration, + "note \"Synchronised access only\" as SyncNote", + ) + .unwrap() + .next() + .unwrap(); + + assert_eq!(find_note_alias(pair).as_deref(), Some("SyncNote")); + } + + #[test] + fn test_find_note_alias_from_multiline_note() { + let pair = PlantUmlCommonParser::parse( + Rule::note_declaration, + "note as SyncNote\n Synchronised access only\nend note\n", + ) + .unwrap() + .next() + .unwrap(); + + assert_eq!(find_note_alias(pair).as_deref(), Some("SyncNote")); + } +} 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 c7de1f82..625f9963 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 @@ -1019,4 +1019,43 @@ mod dispatch_style_tests { actual => panic!("expected message after ref statement, got {:?}", actual), } } + + #[test] + fn test_single_line_note_does_not_consume_following_groups() { + let input = "@startuml\nparticipant P1\nparticipant P2\npar Concurrent Writing\n note over P1: WriteDataProducer1(acquired_data)\\nWriting on range [8:72)\nelse\n note over P2: WriteDataProducer2(acquired_data)\\nWriting on range [80:96)\nend\nP1 -> P2 : done\n@enduml"; + let mut parser = PumlSequenceParser; + let doc = parser + .parse_file( + &Rc::new(PathBuf::from("single_line_note_group.puml")), + input, + LogLevel::Info, + ) + .expect("single-line notes must not consume following group statements"); + + assert_eq!(doc.statements.len(), 6); + + match &doc.statements[2] { + Statement::GroupCmd(GroupCmd::Start(group)) => { + assert_eq!(group.kind, GroupKind::Par); + } + actual => panic!("expected par group start, got {:?}", actual), + } + + match &doc.statements[3] { + Statement::GroupCmd(GroupCmd::Else(_)) => {} + actual => panic!("expected else branch, got {:?}", actual), + } + + match &doc.statements[4] { + Statement::GroupCmd(GroupCmd::End(_)) => {} + actual => panic!("expected group end, got {:?}", actual), + } + + match &doc.statements[5] { + Statement::Message(message) => { + assert_eq!(message.description.as_deref(), Some("done")); + } + actual => panic!("expected trailing message, 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 faeef6bd..6e27f952 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 @@ -116,6 +116,11 @@ fn test_ignored_blocks() { run_sequence_diagram_parser_case("ignored_blocks"); } +#[test] +fn test_ignored_visibility_statements() { + run_sequence_diagram_parser_case("ignored_visibility_statements"); +} + #[test] fn test_note_unsupported() { run_sequence_diagram_parser_case("note_unsupported"); diff --git a/plantuml/parser/puml_parser/tests/class_diagram/ignored_visibility_statements/ignored_visibility_statements.puml b/plantuml/parser/puml_parser/tests/class_diagram/ignored_visibility_statements/ignored_visibility_statements.puml new file mode 100644 index 00000000..76913c29 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/ignored_visibility_statements/ignored_visibility_statements.puml @@ -0,0 +1,24 @@ +' ******************************************************************************* +' 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_visibility_statements + +hide empty members +show empty methods +hide empty attributes +show empty field + +class A +class B +class C + +@enduml diff --git a/plantuml/parser/puml_parser/tests/class_diagram/ignored_visibility_statements/output.json b/plantuml/parser/puml_parser/tests/class_diagram/ignored_visibility_statements/output.json new file mode 100644 index 00000000..3d864477 --- /dev/null +++ b/plantuml/parser/puml_parser/tests/class_diagram/ignored_visibility_statements/output.json @@ -0,0 +1,77 @@ +{ + "ignored_visibility_statements.puml": { + "name": "ignored_visibility_statements", + "elements": [ + { + "Types": { + "ClassDef": { + "name": { + "internal": "A", + "display": null + }, + "namespace": "", + "package": "", + "source_location": { + "file": "", + "line": 20 + }, + "is_abstract": false, + "type_aliases": [], + "attributes": [], + "methods": [], + "template_parameters": null, + "extends": [], + "implements": [] + } + } + }, + { + "Types": { + "ClassDef": { + "name": { + "internal": "B", + "display": null + }, + "namespace": "", + "package": "", + "source_location": { + "file": "", + "line": 21 + }, + "is_abstract": false, + "type_aliases": [], + "attributes": [], + "methods": [], + "template_parameters": null, + "extends": [], + "implements": [] + } + } + }, + { + "Types": { + "ClassDef": { + "name": { + "internal": "C", + "display": null + }, + "namespace": "", + "package": "", + "source_location": { + "file": "", + "line": 22 + }, + "is_abstract": false, + "type_aliases": [], + "attributes": [], + "methods": [], + "template_parameters": null, + "extends": [], + "implements": [] + } + } + } + ], + "relationships": [] + } +} diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_visibility_statements/ignored_visibility_statements.puml b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_visibility_statements/ignored_visibility_statements.puml new file mode 100644 index 00000000..ad15d27d --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_visibility_statements/ignored_visibility_statements.puml @@ -0,0 +1,25 @@ +' ******************************************************************************* +' 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_visibility_statements + +hide empty members +show empty methods +hide public methods +show class "My.Type" empty members +hide stereotype +hide circle +hide members + +Alice -> Bob : after ignored visibility statements + +@enduml diff --git a/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_visibility_statements/output.json b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_visibility_statements/output.json new file mode 100644 index 00000000..aee6039c --- /dev/null +++ b/plantuml/parser/puml_parser/tests/sequence_diagram/ignored_visibility_statements/output.json @@ -0,0 +1,39 @@ +{ + "ignored_visibility_statements.puml": { + "name": "ignored_visibility_statements", + "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 visibility statements", + "source_location": { + "file": "", + "line": 23 + } + } + } + ] + } +} diff --git a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs index 87550604..58424c9f 100644 --- a/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs +++ b/plantuml/parser/puml_resolver/src/component_diagram/tests/component_resolver_test.rs @@ -104,6 +104,11 @@ fn test_relation_absolute_fqn() { run_component_resolver_case("relation_absolute_fqn"); } +#[test] +fn test_relation_quoted_name() { + run_component_resolver_case("relation_quoted_name"); +} + #[test] fn test_invalid_unresolved_reference() { run_component_resolver_case("invalid_unresolved_reference"); diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/src/statement_resolver.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/src/statement_resolver.rs index 998e16da..83f250c5 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/src/statement_resolver.rs +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/src/statement_resolver.rs @@ -249,11 +249,17 @@ fn directed_endpoints( ) -> Result<(&MessageEndpoint, &MessageEndpoint), SequenceResolverError> { let arrow = &message.arrow; - let left_arrow = arrow.left.as_ref().is_some_and(|d| d.raw.contains('<')); + let left_points_left = arrow.left.as_ref().is_some_and(|d| d.raw.contains('<')); - let right_arrow = arrow.right.as_ref().is_some_and(|d| d.raw.contains('>')); + let right_points_right = arrow.right.as_ref().is_some_and(|d| d.raw.contains('>')); - match (left_arrow, right_arrow) { + let left_terminates = arrow.left.as_ref().is_some_and(|d| d.raw == "x"); + let right_terminates = arrow.right.as_ref().is_some_and(|d| d.raw == "x"); + + match ( + left_points_left || (left_terminates && arrow.right.is_none()), + right_points_right || (right_terminates && arrow.left.is_none()), + ) { (true, false) => Ok((&message.right, &message.left)), (false, true) => Ok((&message.left, &message.right)), _ => Err(SequenceResolverError::InvalidMessageDirection { diff --git a/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs b/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs index 4ac06f2f..be27c81a 100644 --- a/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs +++ b/plantuml/parser/puml_resolver/src/sequence_diagram/tests/sequence_resolver_test.rs @@ -86,6 +86,11 @@ fn test_sequence_interaction_node() { run_sequence_resolver_case("sequence_interaction_node"); } +#[test] +fn test_sequence_message_direction() { + run_sequence_resolver_case("sequence_message_direction"); +} + #[test] fn test_sequence_arrow_direction() { run_sequence_resolver_case("sequence_arrow_direction");