diff --git a/src/common/generators/plantuml/generator.h b/src/common/generators/plantuml/generator.h index 20b80d29..6f548d19 100644 --- a/src/common/generators/plantuml/generator.h +++ b/src/common/generators/plantuml/generator.h @@ -149,6 +149,12 @@ protected: template inja::json element_context(const E &e) const; private: + void generate_row_column_hints(std::ostream &ostr, + const std::string &entity_name, const config::layout_hint &hint) const; + + void generate_position_hints(std::ostream &ostr, + const std::string &entity_name, const config::layout_hint &hint) const; + void init_context(); void init_env(); @@ -218,45 +224,102 @@ void generator::generate_config_layout_hints(std::ostream &ostr) const { using namespace clanguml::util; - const auto &uns = m_config.using_namespace(); - // Generate layout hints for (const auto &[entity_name, hints] : m_config.layout()) { for (const auto &hint : hints) { - std::stringstream hint_str; - - // 'together' layout hint is handled separately - if (hint.hint == config::hint_t::together) - continue; - - const auto &hint_entity = std::get(hint.entity); - try { - auto element_opt = m_model.get(entity_name); - if (!element_opt) - element_opt = m_model.get((uns | entity_name).to_string()); - - auto hint_element_opt = m_model.get(hint_entity); - if (!hint_element_opt) - hint_element_opt = - m_model.get((uns | hint_entity).to_string()); - - if (!element_opt || !hint_element_opt) - continue; - hint_str << element_opt.value().alias() << " -[hidden]" - << clanguml::config::to_string(hint.hint) << "- " - << hint_element_opt.value().alias() << '\n'; - ostr << hint_str.str(); + if (hint.hint == config::hint_t::together) { + // 'together' layout hint is handled separately + } + else if (hint.hint == config::hint_t::row || + hint.hint == config::hint_t::column) { + generate_row_column_hints(ostr, entity_name, hint); + } + else { + generate_position_hints(ostr, entity_name, hint); + } } catch (clanguml::error::uml_alias_missing &e) { - LOG_DBG("=== Skipping layout hint from {} to {} due " + LOG_DBG("=== Skipping layout hint '{}' from {} due " "to: {}", - entity_name, hint_entity, e.what()); + to_string(hint.hint), entity_name, e.what()); } } } } +template +void generator::generate_row_column_hints(std::ostream &ostr, + const std::string &entity_name, const config::layout_hint &hint) const +{ + const auto &uns = m_config.using_namespace(); + + std::vector group_elements; + std::vector> element_aliases_pairs; + + group_elements.push_back(entity_name); + const auto &group_tail = std::get>(hint.entity); + std::copy(group_tail.begin(), group_tail.end(), + std::back_inserter(group_elements)); + + auto element_opt = this->m_model.get(entity_name); + if (!element_opt) + element_opt = this->m_model.get((uns | entity_name).to_string()); + + for (auto it = cbegin(group_elements); + it != cend(group_elements) && std::next(it) != cend(group_elements); + ++it) { + const auto &first = *it; + const auto &second = *std::next(it); + + auto first_opt = this->m_model.get(first); + if (!first_opt) + first_opt = this->m_model.get((uns | first).to_string()); + + auto second_opt = this->m_model.get(second); + if (!second_opt) + second_opt = this->m_model.get((uns | second).to_string()); + + element_aliases_pairs.emplace_back( + first_opt.value().alias(), second_opt.value().alias()); + } + + std::string hint_direction = + hint.hint == clanguml::config::hint_t::row ? "right" : "down"; + + for (const auto &[f, s] : element_aliases_pairs) { + ostr << f << " -[hidden]" << hint_direction << "- " << s << '\n'; + } +} + +template +void generator::generate_position_hints(std::ostream &ostr, + const std::string &entity_name, const config::layout_hint &hint) const +{ + const auto &uns = m_config.using_namespace(); + + const auto &hint_entity = std::get(hint.entity); + + auto element_opt = m_model.get(entity_name); + if (!element_opt) + element_opt = m_model.get((uns | entity_name).to_string()); + + auto hint_element_opt = m_model.get(hint_entity); + if (!hint_element_opt) + hint_element_opt = m_model.get((uns | hint_entity).to_string()); + + if (!element_opt || !hint_element_opt) + return; + + std::stringstream hint_str; + + hint_str << element_opt.value().alias() << " -[hidden]" + << clanguml::config::to_string(hint.hint) << "- " + << hint_element_opt.value().alias() << '\n'; + + ostr << hint_str.str(); +} + template void generator::generate_plantuml_directives( std::ostream &ostr, const std::vector &directives) const diff --git a/src/config/config.cc b/src/config/config.cc index 367da492..21aebe84 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -34,6 +34,12 @@ std::string to_string(const hint_t t) return "left"; case hint_t::right: return "right"; + case hint_t::together: + return "together"; + case hint_t::row: + return "row"; + case hint_t::column: + return "column"; default: assert(false); return ""; diff --git a/src/config/config.h b/src/config/config.h index e9cd7d72..5cfd6779 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -80,7 +80,7 @@ struct filter { std::vector paths; }; -enum class hint_t { up, down, left, right, together }; +enum class hint_t { up, down, left, right, together, row, column }; std::string to_string(hint_t t); diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index 5c17af46..ddf59669 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -472,6 +472,14 @@ template <> struct convert { rhs.hint = hint_t::together; rhs.entity = node["together"].as>(); } + else if (node["row"]) { + rhs.hint = hint_t::row; + rhs.entity = node["row"].as>(); + } + else if (node["column"]) { + rhs.hint = hint_t::column; + rhs.entity = node["column"].as>(); + } else return false; diff --git a/tests/t00055/.clang-uml b/tests/t00055/.clang-uml new file mode 100644 index 00000000..f7bcd5b2 --- /dev/null +++ b/tests/t00055/.clang-uml @@ -0,0 +1,17 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00055_class: + type: class + glob: + - ../../tests/t00055/t00055.cc + include: + namespaces: + - clanguml::t00055 + using_namespace: + - clanguml::t00055 + layout: + A: + - row: [C, E, G, I] + B: + - column: [D, F, H, J] \ No newline at end of file diff --git a/tests/t00055/t00055.cc b/tests/t00055/t00055.cc new file mode 100644 index 00000000..fb20f94f --- /dev/null +++ b/tests/t00055/t00055.cc @@ -0,0 +1,24 @@ +namespace clanguml { +namespace t00055 { +struct A { +}; +struct B { +}; +struct C { +}; +struct D { +}; +struct E { +}; +struct F { +}; +struct G { +}; +struct H { +}; +struct I { +}; +struct J { +}; +} +} \ No newline at end of file diff --git a/tests/t00055/test_case.h b/tests/t00055/test_case.h new file mode 100644 index 00000000..e1dcb33d --- /dev/null +++ b/tests/t00055/test_case.h @@ -0,0 +1,60 @@ +/** + * tests/t00055/test_case.h + * + * Copyright (c) 2021-2023 Bartek Kryza + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +TEST_CASE("t00055", "[test-case][class]") +{ + auto [config, db] = load_config("t00055"); + + auto diagram = config.diagrams["t00055_class"]; + + REQUIRE(diagram->name == "t00055_class"); + + auto model = generate_class_diagram(*db, diagram); + + REQUIRE(model->name() == "t00055_class"); + + auto puml = generate_class_puml(diagram, *model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + + // Check if all classes exist + REQUIRE_THAT(puml, IsClass(_A("A"))); + REQUIRE_THAT(puml, IsClass(_A("B"))); + REQUIRE_THAT(puml, IsClass(_A("C"))); + REQUIRE_THAT(puml, IsClass(_A("D"))); + REQUIRE_THAT(puml, IsClass(_A("E"))); + REQUIRE_THAT(puml, IsClass(_A("F"))); + REQUIRE_THAT(puml, IsClass(_A("G"))); + REQUIRE_THAT(puml, IsClass(_A("H"))); + REQUIRE_THAT(puml, IsClass(_A("I"))); + REQUIRE_THAT(puml, IsClass(_A("J"))); + + REQUIRE_THAT(puml, IsLayoutHint(_A("A"), "right", _A("C"))); + REQUIRE_THAT(puml, IsLayoutHint(_A("C"), "right", _A("E"))); + REQUIRE_THAT(puml, IsLayoutHint(_A("E"), "right", _A("G"))); + REQUIRE_THAT(puml, IsLayoutHint(_A("G"), "right", _A("I"))); + + REQUIRE_THAT(puml, IsLayoutHint(_A("B"), "down", _A("D"))); + REQUIRE_THAT(puml, IsLayoutHint(_A("D"), "down", _A("F"))); + REQUIRE_THAT(puml, IsLayoutHint(_A("F"), "down", _A("H"))); + REQUIRE_THAT(puml, IsLayoutHint(_A("H"), "down", _A("J"))); + + save_puml(config.output_directory() + "/" + diagram->name + ".puml", puml); +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 91a64892..42448016 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -248,6 +248,7 @@ using namespace clanguml::test::matchers; #include "t00052/test_case.h" #include "t00053/test_case.h" #include "t00054/test_case.h" +#include "t00055/test_case.h" /// /// Sequence diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index 12de8099..df742152 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -159,6 +159,9 @@ test_cases: - name: t00054 title: Test case for `together` layout hint in class diagram with rendered namespaces description: + - name: t00055 + title: Test case for `row` and `column` layout hints + description: Sequence diagrams: - name: t20001 title: Basic sequence diagram test case