diff --git a/src/class_diagram/generators/mermaid/class_diagram_generator.cc b/src/class_diagram/generators/mermaid/class_diagram_generator.cc new file mode 100644 index 00000000..f6c202a8 --- /dev/null +++ b/src/class_diagram/generators/mermaid/class_diagram_generator.cc @@ -0,0 +1,841 @@ +/** + * @file src/class_diagram/generators/mermaid/class_diagram_generator.cc + * + * 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. + */ + +#include "class_diagram_generator.h" + +#include "util/error.h" + +#include + +namespace clanguml::class_diagram::generators::mermaid { + +generator::generator(diagram_config &config, diagram_model &model) + : common_generator{config, model} + , together_group_stack_{!config.generate_packages()} +{ +} + +std::string generator::render_name(std::string name) const +{ + util::replace_all(name, "<", "<"); + util::replace_all(name, ">", ">"); + util::replace_all(name, "(", "("); + util::replace_all(name, ")", ")"); + util::replace_all(name, "##", "::"); + + return name; +} + +void generator::generate_alias( + const common::model::element &c, std::ostream &ostr) const +{ + std::string full_name; + if (config().generate_packages()) + full_name = c.full_name_no_ns(); + else + full_name = c.full_name(true); + + assert(!full_name.empty()); + + print_debug(c, ostr); + + auto class_label = config().simplify_template_type(render_name(full_name)); + + ostr << " class " << c.alias() << "[\"" << class_label << "\"]\n"; + + // Register the added alias + m_generated_aliases.emplace(c.alias()); +} + +void generator::generate(const class_ &c, std::ostream &ostr) const +{ + namespace mermaid_common = clanguml::common::generators::mermaid; + + std::string class_type{"class"}; + + ostr << " class " << c.alias(); + + ostr << " {" << '\n'; + + if (c.is_union()) + ostr << " <>\n"; + else if (c.is_abstract()) + ostr << " <>\n"; + + // + // Process methods + // + if (config().group_methods()) { + generate_methods(group_methods(c.methods()), ostr); + } + else { + generate_methods(c.methods(), ostr); + } + + // + // Process relationships - here only generate the set of + // rendered_relationships we'll generate them in a seperate method + // + std::set rendered_relations; + + std::stringstream all_relations_str; + for (const auto &r : c.relationships()) { + if (!model().should_include(r.type())) + continue; + + try { + generate_relationship(r, rendered_relations); + } + catch (error::uml_alias_missing &e) { + LOG_DBG("Skipping {} relation from {} to {} due " + "to: {}", + mermaid_common::to_mermaid(r.type(), r.style()), c.full_name(), + r.destination(), e.what()); + } + } + + // + // Process members + // + std::vector members{ + c.members()}; + + sort_class_elements(members); + + for (const auto &m : members) { + if (!model().should_include(m)) + continue; + + if (!config().include_relations_also_as_members() && + rendered_relations.find(m.name()) != rendered_relations.end()) + continue; + + generate_member(m, ostr); + + ostr << '\n'; + } + + ostr << " }" << '\n'; + + generate_notes(ostr, c); + + for (const auto &member : c.members()) + generate_member_notes(ostr, member, c.alias()); + + for (const auto &method : c.methods()) + generate_member_notes(ostr, method, c.alias()); +} + +void generator::generate_methods( + const method_groups_t &methods, std::ostream &ostr) const +{ + for (const auto &group : method_groups_) { + const auto &group_methods = methods.at(group); + if (!group_methods.empty()) { + generate_methods(group_methods, ostr); + } + } +} + +void generator::generate_methods( + const std::vector &methods, std::ostream &ostr) const +{ + auto sorted_methods = methods; + sort_class_elements(sorted_methods); + + for (const auto &m : sorted_methods) { + if (!model().should_include(m)) + continue; + + generate_method(m, ostr); + + ostr << '\n'; + } +} + +generator::method_groups_t generator::group_methods( + const std::vector &methods) const +{ + std::map> result; + + // First get rid of methods which don't pass the filters + std::vector filtered_methods; + std::copy_if(methods.cbegin(), methods.cend(), + std::back_inserter(filtered_methods), + [this](auto &m) { return model().should_include(m); }); + + for (const auto &g : method_groups_) { + result[g] = {}; + } + + for (const auto &m : filtered_methods) { + if (m.is_constructor() || m.is_destructor()) { + result["constructors"].push_back(m); + } + else if (m.is_copy_assignment() || m.is_move_assignment()) { + result["assignment"].push_back(m); + } + else if (m.is_operator()) { + result["operators"].push_back(m); + } + else { + result["other"].push_back(m); + } + } + + return result; +} + +void generator::generate_method( + const class_diagram::model::class_method &m, std::ostream &ostr) const +{ + namespace mermaid_common = clanguml::common::generators::mermaid; + const auto &uns = config().using_namespace(); + + constexpr auto kAbbreviatedMethodArgumentsLength{15}; + + print_debug(m, ostr); + + std::string intend = " "; + + std::string type{uns.relative(config().simplify_template_type(m.type()))}; + + ostr << intend << mermaid_common::to_mermaid(m.access()) << m.name(); + + if (!m.template_params().empty()) { + m.render_template_params(ostr, config().using_namespace(), false); + } + + ostr << "("; + if (config().generate_method_arguments() != + config::method_arguments::none) { + std::vector params; + std::transform(m.parameters().cbegin(), m.parameters().cend(), + std::back_inserter(params), [this](const auto &mp) { + return config().simplify_template_type( + mp.to_string(config().using_namespace())); + }); + auto args_string = fmt::format("{}", fmt::join(params, ", ")); + if (config().generate_method_arguments() == + config::method_arguments::abbreviated) { + args_string = clanguml::util::abbreviate( + args_string, kAbbreviatedMethodArgumentsLength); + } + ostr << args_string; + } + ostr << ")"; + + ostr << " : " << render_name(type); + + if (m.is_pure_virtual()) + ostr << "*"; + + if (m.is_static()) + ostr << "$"; +} + +void generator::generate_member( + const class_diagram::model::class_member &m, std::ostream &ostr) const +{ + namespace mermaid_common = clanguml::common::generators::mermaid; + const auto &uns = config().using_namespace(); + + print_debug(m, ostr); + + ostr << " " << mermaid_common::to_mermaid(m.access()) << m.name() + << " : " + << render_name( + uns.relative(config().simplify_template_type(m.type()))); +} + +void generator::generate(const concept_ &c, std::ostream &ostr) const +{ + std::string class_type{"class"}; + + ostr << class_type << " " << c.alias() << " <>"; + + if (!c.style().empty()) + ostr << " " << c.style(); + + ostr << " {" << '\n'; + + // TODO: add option to enable/disable this + if (c.requires_parameters().size() + c.requires_statements().size() > 0) { + std::vector parameters; + parameters.reserve(c.requires_parameters().size()); + for (const auto &p : c.requires_parameters()) { + parameters.emplace_back(p.to_string(config().using_namespace())); + } + + ostr << fmt::format("({})\n", fmt::join(parameters, ",")); + + ostr << "..\n"; + + ostr << fmt::format("{}\n", fmt::join(c.requires_statements(), "\n")); + } + + ostr << " }" << '\n'; +} + +void generator::generate_member_notes(std::ostream &ostr, + const class_element &member, const std::string &alias) const +{ + for (const auto &decorator : member.decorators()) { + auto note = std::dynamic_pointer_cast(decorator); + if (note && note->applies_to_diagram(config().name)) { + ostr << "note " << note->position << " of " << alias + << "::" << member.name() << '\n' + << note->text << '\n' + << "end note\n"; + } + } +} + +void generator::generate_relationships(std::ostream &ostr) const +{ + for (const auto &p : model()) { + if (auto *pkg = dynamic_cast(p.get()); pkg) { + generate_relationships(*pkg, ostr); + } + else if (auto *cls = dynamic_cast(p.get()); cls) { + if (model().should_include(*cls)) { + generate_relationships(*cls, ostr); + } + } + else if (auto *enm = dynamic_cast(p.get()); enm) { + if (model().should_include(*enm)) { + generate_relationships(*enm, ostr); + } + } + else if (auto *cpt = dynamic_cast(p.get()); cpt) { + if (model().should_include(*cpt)) { + generate_relationships(*cpt, ostr); + } + } + } +} + +void generator::generate_relationship( + const relationship &r, std::set &rendered_relations) const +{ + namespace mermaid_common = clanguml::common::generators::mermaid; + + LOG_DBG("Processing relationship {}", + mermaid_common::to_mermaid(r.type(), r.style())); + + std::string destination; + + auto target_element = model().get(r.destination()); + if (!target_element.has_value()) + throw error::uml_alias_missing{fmt::format( + "Missing element in the model for ID: {}", r.destination())}; + + destination = target_element.value().full_name(false); + + if (util::starts_with(destination, std::string{"::"})) + destination = destination.substr(2, destination.size()); + + std::string puml_relation; + if (!r.multiplicity_source().empty()) + puml_relation += "\"" + r.multiplicity_source() + "\" "; + + puml_relation += mermaid_common::to_mermaid(r.type(), r.style()); + + if (!r.multiplicity_destination().empty()) + puml_relation += " \"" + r.multiplicity_destination() + "\""; + + if (!r.label().empty()) { + rendered_relations.emplace(r.label()); + } +} + +void generator::generate_relationships( + const class_ &c, std::ostream &ostr) const +{ + namespace mermaid_common = clanguml::common::generators::mermaid; + + // + // Process relationships + // + std::set rendered_relations; + + std::stringstream all_relations_str; + std::set unique_relations; + + for (const auto &r : c.relationships()) { + if (!model().should_include(r.type())) + continue; + + LOG_DBG("== Processing relationship {}", + mermaid_common::to_mermaid(r.type(), r.style())); + + std::stringstream relstr; + clanguml::common::id_t destination{0}; + try { + destination = r.destination(); + + std::string puml_relation; + if (!r.multiplicity_source().empty()) + puml_relation += "\"" + r.multiplicity_source() + "\" "; + + puml_relation += mermaid_common::to_mermaid(r.type(), r.style()); + + if (!r.multiplicity_destination().empty()) + puml_relation += " \"" + r.multiplicity_destination() + "\""; + + std::string target_alias; + try { + target_alias = model().to_alias(destination); + } + catch (...) { + LOG_DBG("Failed to find alias to {}", destination); + continue; + } + + if (m_generated_aliases.find(target_alias) == + m_generated_aliases.end()) + continue; + + relstr << c.alias() << " " << puml_relation << " " << target_alias; + + if (!r.label().empty()) { + relstr << " : " << mermaid_common::to_mermaid(r.access()) + << r.label(); + rendered_relations.emplace(r.label()); + } + + if(r.type() == relationship_t::kContainment) { + relstr << " : [nested]\n"; + } + + if (unique_relations.count(relstr.str()) == 0) { + unique_relations.emplace(relstr.str()); + + relstr << '\n'; + + LOG_DBG("=== Adding relation {}", relstr.str()); + + all_relations_str << " " << relstr.str(); + } + } + catch (error::uml_alias_missing &e) { + LOG_DBG("=== Skipping {} relation from {} to {} due " + "to: {}", + mermaid_common::to_mermaid(r.type(), r.style()), c.full_name(), + destination, e.what()); + } + } + + if (model().should_include(relationship_t::kExtension)) { + for (const auto &b : c.parents()) { + std::stringstream relstr; + try { + auto target_alias = model().to_alias(b.id()); + + if (m_generated_aliases.find(target_alias) == + m_generated_aliases.end()) + continue; + + relstr << target_alias << " <|-- " << c.alias() << '\n'; + all_relations_str << " " << relstr.str(); + } + catch (error::uml_alias_missing &e) { + LOG_DBG("=== Skipping inheritance relation from {} to {} due " + "to: {}", + b.name(), c.name(), e.what()); + } + } + } + + ostr << all_relations_str.str(); +} + +void generator::generate_relationships( + const concept_ &c, std::ostream &ostr) const +{ + namespace mermaid_common = clanguml::common::generators::mermaid; + + // + // Process relationships + // + std::set rendered_relations; + + std::stringstream all_relations_str; + std::set unique_relations; + + for (const auto &r : c.relationships()) { + if (!model().should_include(r.type())) + continue; + + LOG_DBG("== Processing relationship {}", + mermaid_common::to_mermaid(r.type(), r.style())); + + std::stringstream relstr; + clanguml::common::id_t destination{0}; + try { + destination = r.destination(); + + std::string puml_relation; + if (!r.multiplicity_source().empty()) + puml_relation += "\"" + r.multiplicity_source() + "\" "; + + puml_relation += mermaid_common::to_mermaid(r.type(), r.style()); + + if (!r.multiplicity_destination().empty()) + puml_relation += " \"" + r.multiplicity_destination() + "\""; + + std::string target_alias; + try { + target_alias = model().to_alias(destination); + } + catch (...) { + LOG_DBG("Failed to find alias to {}", destination); + continue; + } + + if (m_generated_aliases.find(target_alias) == + m_generated_aliases.end()) + continue; + + relstr << c.alias() << " " << puml_relation << " " << target_alias; + + if (!r.label().empty()) { + relstr << " : " << mermaid_common::to_mermaid(r.access()) + << r.label(); + rendered_relations.emplace(r.label()); + } + + if (unique_relations.count(relstr.str()) == 0) { + unique_relations.emplace(relstr.str()); + + relstr << '\n'; + + LOG_DBG("=== Adding relation {}", relstr.str()); + + all_relations_str << relstr.str(); + } + } + catch (error::uml_alias_missing &e) { + LOG_DBG("=== Skipping {} relation from {} to {} due " + "to: {}", + mermaid_common::to_mermaid(r.type(), r.style()), c.full_name(), + destination, e.what()); + } + } + + ostr << all_relations_str.str(); +} + +void generator::generate_relationships(const enum_ &e, std::ostream &ostr) const +{ + for (const auto &r : e.relationships()) { + if (!model().should_include(r.type())) + continue; + + clanguml::common::id_t destination{0}; + std::stringstream relstr; + try { + destination = r.destination(); + + auto target_alias = model().to_alias(destination); + + if (m_generated_aliases.find(target_alias) == + m_generated_aliases.end()) + continue; + + relstr << e.alias() << " " + << clanguml::common::generators::mermaid::to_mermaid( + r.type(), r.style()) + << " " << target_alias; + + if (!r.label().empty()) + relstr << " : " << r.label(); + + relstr << '\n'; + + ostr << " " << relstr.str(); + } + catch (error::uml_alias_missing &ex) { + LOG_DBG("Skipping {} relation from {} to {} due " + "to: {}", + clanguml::common::generators::mermaid::to_mermaid( + r.type(), r.style()), + e.full_name(), destination, ex.what()); + } + } +} + +void generator::generate(const enum_ &e, std::ostream &ostr) const +{ + ostr << " class " << e.alias(); + + ostr << " {" << '\n'; + + ostr << " <>\n"; + + for (const auto &enum_constant : e.constants()) { + ostr << " " << enum_constant << '\n'; + } + + ostr << " }" << '\n'; + + generate_notes(ostr, e); +} + +void generator::generate(const package &p, std::ostream &ostr) const +{ + const auto &uns = config().using_namespace(); + + if (config().generate_packages()) { + LOG_DBG("Generating package {}", p.name()); + + // Don't generate packages from namespaces filtered out by + // using_namespace + if (!uns.starts_with({p.full_name(false)})) { + print_debug(p, ostr); + ostr << "package [" << p.name() << "] "; + ostr << "as " << p.alias(); + + if (p.is_deprecated()) + ostr << " <>"; + + if (!p.style().empty()) + ostr << " " << p.style(); + + ostr << " {" << '\n'; + } + } + + for (const auto &subpackage : p) { + if (dynamic_cast(subpackage.get()) != nullptr) { + // TODO: add option - generate_empty_packages + const auto &sp = dynamic_cast(*subpackage); + if (!sp.is_empty()) { + together_group_stack_.enter(); + + generate(sp, ostr); + + together_group_stack_.leave(); + } + } + else if (auto *cls = dynamic_cast(subpackage.get()); cls) { + if (model().should_include(*subpackage)) { + auto together_group = + config().get_together_group(cls->full_name(false)); + if (together_group) { + together_group_stack_.group_together( + together_group.value(), cls); + } + else { + generate_alias(*cls, ostr); + generate(*cls, ostr); + } + } + } + else if (auto *enm = dynamic_cast(subpackage.get()); enm) { + if (model().should_include(*subpackage)) { + auto together_group = + config().get_together_group(subpackage->full_name(false)); + if (together_group) { + together_group_stack_.group_together( + together_group.value(), enm); + } + else { + generate_alias(*enm, ostr); + generate(*enm, ostr); + } + } + } + else if (auto *cpt = dynamic_cast(subpackage.get()); cpt) { + if (model().should_include(*subpackage)) { + auto together_group = + config().get_together_group(cpt->full_name(false)); + if (together_group) { + together_group_stack_.group_together( + together_group.value(), cpt); + } + else { + generate_alias(*cpt, ostr); + generate(*cpt, ostr); + } + } + } + } + + if (config().generate_packages()) { + // Now generate any diagram elements which are in together + // groups + for (const auto &[group_name, group_elements] : + together_group_stack_.get_current_groups()) { + ostr << "together {\n"; + + for (auto *e : group_elements) { + if (auto *cls = dynamic_cast(e); cls) { + generate_alias(*cls, ostr); + generate(*cls, ostr); + } + if (auto *enm = dynamic_cast(e); enm) { + generate_alias(*enm, ostr); + generate(*enm, ostr); + } + if (auto *cpt = dynamic_cast(e); cpt) { + generate_alias(*cpt, ostr); + generate(*cpt, ostr); + } + } + + ostr << "}\n"; + } + + // Don't generate packages from namespaces filtered out by + // using_namespace + if (!uns.starts_with({p.full_name(false)})) { + ostr << "}" << '\n'; + generate_notes(ostr, p); + } + } +} + +void generator::generate_relationships( + const package &p, std::ostream &ostr) const +{ + for (const auto &subpackage : p) { + if (dynamic_cast(subpackage.get()) != nullptr) { + // TODO: add option - generate_empty_packages, currently + // packages which do not contain anything but other + // packages are skipped + const auto &sp = dynamic_cast(*subpackage); + if (!sp.is_empty() && + !sp.all_of([this](const common::model::element &e) { + return !model().should_include(e); + })) + generate_relationships(sp, ostr); + } + else if (dynamic_cast(subpackage.get()) != nullptr) { + if (model().should_include(*subpackage)) { + generate_relationships( + dynamic_cast(*subpackage), ostr); + } + } + else if (dynamic_cast(subpackage.get()) != nullptr) { + if (model().should_include(*subpackage)) { + generate_relationships( + dynamic_cast(*subpackage), ostr); + } + } + else if (dynamic_cast(subpackage.get()) != nullptr) { + if (model().should_include(*subpackage)) { + generate_relationships( + dynamic_cast(*subpackage), ostr); + } + } + } +} + +void generator::generate_diagram(std::ostream &ostr) const +{ + ostr << "classDiagram\n"; + + generate_top_level_elements(ostr); + + generate_groups(ostr); + + generate_relationships(ostr); +} + +void generator::generate_top_level_elements(std::ostream &ostr) const +{ + for (const auto &p : model()) { + if (auto *pkg = dynamic_cast(p.get()); pkg) { + if (!pkg->is_empty() && + !pkg->all_of([this](const common::model::element &e) { + return !model().should_include(e); + })) + generate(*pkg, ostr); + } + else if (auto *cls = dynamic_cast(p.get()); cls) { + if (model().should_include(*cls)) { + auto together_group = + config().get_together_group(cls->full_name(false)); + if (together_group) { + together_group_stack_.group_together( + together_group.value(), cls); + } + else { + generate_alias(*cls, ostr); + generate(*cls, ostr); + } + } + } + else if (auto *enm = dynamic_cast(p.get()); enm) { + if (model().should_include(*enm)) { + auto together_group = + config().get_together_group(enm->full_name(false)); + if (together_group) { + together_group_stack_.group_together( + together_group.value(), enm); + } + else { + generate_alias(*enm, ostr); + generate(*enm, ostr); + } + } + } + else if (auto *cpt = dynamic_cast(p.get()); cpt) { + if (model().should_include(*cpt)) { + auto together_group = + config().get_together_group(cpt->full_name(false)); + if (together_group) { + together_group_stack_.group_together( + together_group.value(), cpt); + } + else { + generate_alias(*cpt, ostr); + generate(*cpt, ostr); + } + } + } + } +} + +void generator::generate_groups(std::ostream &ostr) const +{ + for (const auto &[group_name, group_elements] : + together_group_stack_.get_current_groups()) { + ostr << "together {\n"; + + for (auto *e : group_elements) { + if (auto *cls = dynamic_cast(e); cls) { + generate_alias(*cls, ostr); + generate(*cls, ostr); + } + if (auto *enm = dynamic_cast(e); enm) { + generate_alias(*enm, ostr); + generate(*enm, ostr); + } + if (auto *cpt = dynamic_cast(e); cpt) { + generate_alias(*cpt, ostr); + generate(*cpt, ostr); + } + } + + ostr << "}\n"; + } +} + +} // namespace clanguml::class_diagram::generators::plantuml diff --git a/src/class_diagram/generators/mermaid/class_diagram_generator.h b/src/class_diagram/generators/mermaid/class_diagram_generator.h new file mode 100644 index 00000000..b08be5f7 --- /dev/null +++ b/src/class_diagram/generators/mermaid/class_diagram_generator.h @@ -0,0 +1,269 @@ +/** + * @file src/class_diagram/generators/mermaid/class_diagram_generator.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. + */ +#pragma once + +#include "class_diagram/model/class.h" +#include "class_diagram/model/concept.h" +#include "class_diagram/model/diagram.h" +#include "class_diagram/model/enum.h" +#include "class_diagram/visitor/translation_unit_visitor.h" +#include "common/generators/mermaid/generator.h" +#include "common/generators/nested_element_stack.h" +#include "common/model/relationship.h" +#include "config/config.h" +#include "util/util.h" + +#include + +#include +#include +#include +#include + +namespace clanguml { +namespace class_diagram { +namespace generators { +namespace mermaid { + +using diagram_config = clanguml::config::class_diagram; +using diagram_model = clanguml::class_diagram::model::diagram; +template +using common_generator = clanguml::common::generators::mermaid::generator; + +using clanguml::class_diagram::model::class_; +using clanguml::class_diagram::model::class_element; +using clanguml::class_diagram::model::class_member; +using clanguml::class_diagram::model::class_method; +using clanguml::class_diagram::model::concept_; +using clanguml::class_diagram::model::enum_; +using clanguml::common::model::access_t; +using clanguml::common::model::package; +using clanguml::common::model::relationship; +using clanguml::common::model::relationship_t; + +using namespace clanguml::util; + +/** + * @brief Class diagram MermaidJS generator + */ +class generator : public common_generator { + using method_groups_t = std::map>; + +public: + generator(diagram_config &config, diagram_model &model); + + using common_generator::generate; + + /** + * @brief Main generator method. + * + * This method is called first and coordinates the entire diagram + * generation. + * + * @param ostr Output stream. + */ + void generate_diagram(std::ostream &ostr) const override; + + /** + * @brief In a nested diagram, generate the top level elements. + * + * This method iterates over the top level elements. In case the diagram + * is nested (i.e. includes packages), for each package it recursively + * call generation of elements contained in each package. + * + * @param parent JSON node + */ + void generate_top_level_elements(std::ostream &ostr) const; + + /** + * @brief Generate PlantUML alias for a class element. + * + * @param c Class element + * @param ostr Output stream + */ + void generate_alias( + const common::model::element &e, std::ostream &ostr) const; + + /** + * @brief Render class element to PlantUML + * + * @param c Class element + * @param ostr Output stream + */ + void generate(const class_ &c, std::ostream &ostr) const; + + /** + * @brief Render class methods to PlantUML + * + * @param methods List of class methods + * @param ostr Output stream + */ + void generate_methods( + const std::vector &methods, std::ostream &ostr) const; + + /** + * @brief Render class methods to PlantUML in groups + * + * @param methods Methods grouped by method type + * @param ostr Output stream + */ + void generate_methods( + const method_groups_t &methods, std::ostream &ostr) const; + + /** + * @brief Render class method to PlantUML + * + * @param m Class method + * @param ostr Output stream + */ + void generate_method(const class_method &m, std::ostream &ostr) const; + + /** + * @brief Render class member to PlantUML + * + * @param m Class member + * @param ostr Output stream + */ + void generate_member(const class_member &m, std::ostream &ostr) const; + + /** + * @brief Render all relationships in the diagram to PlantUML + * + * @param ostr Output stream + */ + void generate_relationships(std::ostream &ostr) const; + + /** + * @brief Render all relationships originating from class element. + * + * @param c Class element + * @param ostr Output stream + */ + void generate_relationships(const class_ &c, std::ostream &ostr) const; + + /** + * @brief Render a specific relationship to PlantUML. + * + * @param r Relationship model + * @param rendered_relations Set of already rendered relationships, to + * ensure that there are no duplicate + * relationships + */ + void generate_relationship( + const relationship &r, std::set &rendered_relations) const; + + /** + * @brief Render enum element to PlantUML + * + * @param e Enum element + * @param ostr Output stream + */ + void generate(const enum_ &e, std::ostream &ostr) const; + + /** + * @brief Render all relationships originating from enum element. + * + * @param c Enum element + * @param ostr Output stream + */ + void generate_relationships(const enum_ &c, std::ostream &ostr) const; + + /** + * @brief Render concept element to PlantUML + * + * @param c Concept element + * @param ostr Output stream + */ + void generate(const concept_ &c, std::ostream &ostr) const; + + /** + * @brief Render all relationships originating from concept element. + * + * @param c Concept element + * @param ostr Output stream + */ + void generate_relationships(const concept_ &c, std::ostream &ostr) const; + + /** + * @brief Render package element to PlantUML + * + * @param p Package element + * @param ostr Output stream + */ + void generate(const package &p, std::ostream &ostr) const; + + /** + * @brief Render all relationships originating from package element. + * + * @param p Package element + * @param ostr Output stream + */ + void generate_relationships(const package &p, std::ostream &ostr) const; + + /** + * @brief Generate any notes attached specifically to some class element. + * + * @param ostream Output stream + * @param member Class element (member or method) + * @param alias PlantUML class alias + */ + void generate_member_notes(std::ostream &ostream, + const class_element &member, const std::string &alias) const; + + /** + * @brief Generate elements grouped together in `together` groups. + * + * @param ostr Output stream + */ + void generate_groups(std::ostream &ostr) const; + + /** + * @brief Group class methods based on method type. + * + * @param methods List of class methods. + * + * @return Map of method groups. + */ + method_groups_t group_methods( + const std::vector &methods) const; + +private: + const std::vector method_groups_{ + "constructors", "assignment", "operators", "other"}; + + std::string render_name(std::string name) const; + + template + void sort_class_elements(std::vector &elements) const + { + if (config().member_order() == config::member_order_t::lexical) { + std::sort(elements.begin(), elements.end(), + [](const auto &m1, const auto &m2) { + return m1.name() < m2.name(); + }); + } + } + + mutable common::generators::nested_element_stack + together_group_stack_; +}; + +} // namespace mermaid +} // namespace generators +} // namespace class_diagram +} // namespace clanguml diff --git a/src/common/generators/generators.h b/src/common/generators/generators.h index 8f36e386..b4d3b5c1 100644 --- a/src/common/generators/generators.h +++ b/src/common/generators/generators.h @@ -19,6 +19,7 @@ #include "class_diagram/generators/json/class_diagram_generator.h" #include "class_diagram/generators/plantuml/class_diagram_generator.h" +#include "class_diagram/generators/mermaid/class_diagram_generator.h" #include "cli/cli_handler.h" #include "common/compilation_database.h" #include "common/generators/generators.h" @@ -105,6 +106,9 @@ struct plantuml_generator_tag { struct json_generator_tag { inline static const std::string extension = "json"; }; +struct mermaid_generator_tag { + inline static const std::string extension = "mmd"; +}; /** @} */ /** @defgroup diagram_generator_t Diagram generator selector @@ -114,6 +118,7 @@ struct json_generator_tag { * * @{ */ +// plantuml template struct diagram_generator_t; template <> @@ -136,6 +141,7 @@ struct diagram_generator_t { using type = clanguml::include_diagram::generators::plantuml::generator; }; +// json template <> struct diagram_generator_t { @@ -156,6 +162,27 @@ struct diagram_generator_t { using type = clanguml::include_diagram::generators::json::generator; }; +// mermaid +template <> +struct diagram_generator_t { + using type = clanguml::class_diagram::generators::mermaid::generator; +}; +//template <> +//struct diagram_generator_t { +// using type = clanguml::sequence_diagram::generators::mermaid::generator; +//}; +//template <> +//struct diagram_generator_t { +// using type = clanguml::package_diagram::generators::mermaid::generator; +//}; +//template <> +//struct diagram_generator_t { +// using type = clanguml::include_diagram::generators::mermaid::generator; +//}; /** @} */ /** diff --git a/src/common/generators/mermaid/generator.cc b/src/common/generators/mermaid/generator.cc new file mode 100644 index 00000000..3eea909a --- /dev/null +++ b/src/common/generators/mermaid/generator.cc @@ -0,0 +1,75 @@ +/** + * @file src/common/generators/mermaid/generator.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. + */ +#include "generator.h" + +namespace clanguml::common::generators::mermaid { + +std::string to_mermaid(relationship_t r, const std::string &style) +{ + switch (r) { + case relationship_t::kOwnership: + case relationship_t::kComposition: + return style.empty() ? "*--" : fmt::format("*-[{}]-", style); + case relationship_t::kAggregation: + return style.empty() ? "o--" : fmt::format("o-[{}]-", style); + case relationship_t::kContainment: + return style.empty() ? "--" : fmt::format("-[{}]-", style); + case relationship_t::kAssociation: + return style.empty() ? "-->" : fmt::format("-[{}]->", style); + case relationship_t::kInstantiation: + return style.empty() ? "..|>" : fmt::format(".[{}].|>", style); + case relationship_t::kFriendship: + return style.empty() ? "<.." : fmt::format("<.[{}].", style); + case relationship_t::kDependency: + return style.empty() ? "..>" : fmt::format(".[{}].>", style); + case relationship_t::kConstraint: + return style.empty() ? "..>" : fmt::format(".[{}].>", style); + case relationship_t::kAlias: + return style.empty() ? ".." : fmt::format(".[{}].", style); + default: + return ""; + } +} + +std::string to_mermaid(access_t scope) +{ + switch (scope) { + case access_t::kPublic: + return "+"; + case access_t::kProtected: + return "#"; + case access_t::kPrivate: + return "-"; + default: + return ""; + } +} + +std::string to_mermaid(message_t r) +{ + switch (r) { + case message_t::kCall: + return "->"; + case message_t::kReturn: + return "-->"; + default: + return ""; + } +} + +} // namespace clanguml::common::generators::mermaid diff --git a/src/common/generators/mermaid/generator.h b/src/common/generators/mermaid/generator.h new file mode 100644 index 00000000..111448b8 --- /dev/null +++ b/src/common/generators/mermaid/generator.h @@ -0,0 +1,454 @@ +/** + * @file src/common/generators/mermaid/generator.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. + */ +#pragma once + +#include "common/generators/generator.h" +#include "common/model/diagram_filter.h" +#include "config/config.h" +#include "util/error.h" +#include "util/util.h" +#include "version.h" + +#include +#include +#include +#include +#include +#include + +namespace clanguml::common::generators::mermaid { + +using clanguml::common::model::access_t; +using clanguml::common::model::element; +using clanguml::common::model::message_t; +using clanguml::common::model::relationship_t; + +std::string to_mermaid(relationship_t r, const std::string &style); +std::string to_mermaid(access_t scope); +std::string to_mermaid(message_t r); + +/** + * @brief Base class for diagram generators + * + * @tparam ConfigType Configuration type + * @tparam DiagramType Diagram model type + */ +template +class generator + : public clanguml::common::generators::generator { +public: + /** + * @brief Constructor + * + * @param config Reference to instance of @link clanguml::config::diagram + * @param model Reference to instance of @link clanguml::model::diagram + */ + generator(ConfigType &config, DiagramType &model) + : clanguml::common::generators::generator{ + config, model} + { + init_context(); + init_env(); + } + + ~generator() override = default; + + /** + * @brief Generate diagram + * + * This is the main diagram generation entrypoint. It is responsible for + * calling other methods in appropriate order to generate the diagram into + * the output stream. It generates diagram elements, that are common + * to all types of diagrams in a given generator. + * + * @param ostr Output stream + */ + void generate(std::ostream &ostr) const override; + + /** + * @brief Generate diagram specific part + * + * This method must be implemented in subclasses for specific diagram + * types. + * + * @param ostr Output stream + */ + virtual void generate_diagram(std::ostream &ostr) const = 0; + + /** + * @brief Generate diagram layout hints + * + * This method adds to the diagram any layout hints that were provided + * in the configuration file. + * + * @param ostr Output stream + */ + void generate_config_layout_hints(std::ostream &ostr) const; + + /** + * @brief Generate MermaidJS directives from config file. + * + * This method renders the MermaidJS directives provided in the configuration + * file, including resolving any element aliases and Jinja templates. + * + * @param ostr Output stream + * @param directives List of directives from the configuration file + */ + void generate_mermaid_directives( + std::ostream &ostr, const std::vector &directives) const; + + /** + * @brief Generate diagram notes + * + * This method adds any notes in the diagram, which were declared in the + * code using inline directives + * + * @param ostr Output stream + * @param element Element to which the note should be attached + */ + void generate_notes( + std::ostream &ostr, const model::element &element) const; + + /** + * @brief Generate comment with diagram metadata + * + * @param ostr Output stream + */ + void generate_metadata(std::ostream &ostr) const; + + /** + * @brief Generate hyper link to element + * + * This method renders links to URL's based on templates provided + * in the configuration file (e.g. Git browser with specific line and + * column offset) + * + * @param ostr Output stream + * @param e Reference to diagram element + * @tparam E Diagram element type + */ + template + void generate_link(std::ostream &ostr, const E &e) const; + + /** + * @brief Print debug information in diagram comments + * + * @param m Diagram element to describe + * @param ostr Output stream + */ + void print_debug( + const common::model::source_location &e, std::ostream &ostr) const; + /** + * @brief Update diagram Jinja context + * + * This method updates the diagram context with models properties + * which can be used to render Jinja templates in the diagram (e.g. + * in notes or links) + */ + void update_context() const; + +protected: + const inja::json &context() const; + + inja::Environment &env() const; + + template inja::json element_context(const E &e) const; + +private: + void init_context(); + + void init_env(); + +protected: + mutable std::set m_generated_aliases; + mutable inja::json m_context; + mutable inja::Environment m_env; +}; + +template +const inja::json &generator::context() const +{ + return m_context; +} + +template +inja::Environment &generator::env() const +{ + return m_env; +} + +template +template +inja::json generator::element_context(const E &e) const +{ + auto ctx = context(); + + ctx["element"] = e.context(); + + if (!e.file().empty()) { + std::filesystem::path file{e.file()}; + std::string relative_path = file.string(); +#if _MSC_VER + if (file.is_absolute() && ctx.contains("git")) +#else + if (file.is_absolute() && ctx.template contains("git")) +#endif + relative_path = + std::filesystem::relative(file, ctx["git"]["toplevel"]) + .string(); + + ctx["element"]["source"]["path"] = util::path_to_url(relative_path); + ctx["element"]["source"]["full_path"] = file.string(); + ctx["element"]["source"]["name"] = file.filename().string(); + ctx["element"]["source"]["line"] = e.line(); + } + + const auto maybe_comment = e.comment(); + if (maybe_comment) { + ctx["element"]["comment"] = maybe_comment.value(); + } + + return ctx; +} + +template +void generator::generate(std::ostream &ostr) const +{ + const auto &config = generators::generator::config(); + + update_context(); + + // generate_mermaid_diagram_type(ostr, config); + + generate_mermaid_directives(ostr, config.puml().before); + + generate_diagram(ostr); + + generate_mermaid_directives(ostr, config.puml().after); + + generate_metadata(ostr); +} + +template +void generator::generate_config_layout_hints(std::ostream &ostr) const +{ + using namespace clanguml::util; + + const auto &config = generators::generator::config(); + + // Generate layout hints + for (const auto &[entity_name, hints] : config.layout()) { + for (const auto &hint : hints) { + try { + 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 {} due " + "to: {}", + to_string(hint.hint), entity_name, e.what()); + } + } + } +} + +template +void generator::generate_mermaid_directives( + std::ostream &ostr, const std::vector &directives) const +{ + +} + +template +void generator::generate_notes( + std::ostream &ostr, const model::element &e) const +{ +// const auto &config = generators::generator::config(); +// +// for (const auto &decorator : e.decorators()) { +// auto note = std::dynamic_pointer_cast(decorator); +// if (note && note->applies_to_diagram(config.name)) { +// ostr << "note " << note->position << " of " << e.alias() << '\n' +// << note->text << '\n' +// << "end note\n"; +// } +// } +} + +template +void generator::generate_metadata(std::ostream &ostr) const +{ + const auto &config = generators::generator::config(); + + if (config.generate_metadata()) { + ostr << '\n' + << " %% Generated with clang-uml, version " + << clanguml::version::CLANG_UML_VERSION << '\n' + << " %% LLVM version " << clang::getClangFullVersion() << '\n'; + } +} + +template +void generator::print_debug( + const common::model::source_location &e, std::ostream &ostr) const +{ + const auto &config = generators::generator::config(); + + if (config.debug_mode()) + ostr << " %% " << e.file() << ":" << e.line() << '\n'; +} + +template +std::ostream &operator<<( + std::ostream &os, const generator &g) +{ + g.generate(os); + return os; +} + +template void generator::init_context() +{ + const auto &config = generators::generator::config(); + + if (config.git) { + m_context["git"]["branch"] = config.git().branch; + m_context["git"]["revision"] = config.git().revision; + m_context["git"]["commit"] = config.git().commit; + m_context["git"]["toplevel"] = config.git().toplevel; + } +} + +template void generator::update_context() const +{ + m_context["diagram"] = generators::generator::model().context(); +} + +template void generator::init_env() +{ + const auto &model = generators::generator::model(); + const auto &config = generators::generator::config(); + + // + // Add basic string functions to inja environment + // + + // Check if string is empty + m_env.add_callback("empty", 1, [](inja::Arguments &args) { + return args.at(0)->get().empty(); + }); + + // Remove spaces from the left of a string + m_env.add_callback("ltrim", 1, [](inja::Arguments &args) { + return util::ltrim(args.at(0)->get()); + }); + + // Remove trailing spaces from a string + m_env.add_callback("rtrim", 1, [](inja::Arguments &args) { + return util::rtrim(args.at(0)->get()); + }); + + // Remove spaces before and after a string + m_env.add_callback("trim", 1, [](inja::Arguments &args) { + return util::trim(args.at(0)->get()); + }); + + // Make a string shorted with a limit to + m_env.add_callback("abbrv", 2, [](inja::Arguments &args) { + return util::abbreviate( + args.at(0)->get(), args.at(1)->get()); + }); + + m_env.add_callback("replace", 3, [](inja::Arguments &args) { + std::string result = args[0]->get(); + std::regex pattern(args[1]->get()); + return std::regex_replace(result, pattern, args[2]->get()); + }); + + m_env.add_callback("split", 2, [](inja::Arguments &args) { + return util::split( + args[0]->get(), args[1]->get()); + }); + + // + // Add PlantUML specific functions + // + + // Return the entire element JSON context based on element name + // e.g.: + // {{ element("clanguml::t00050::A").comment }} + // + m_env.add_callback("element", 1, [&model, &config](inja::Arguments &args) { + inja::json res{}; + auto element_opt = model.get_with_namespace( + args[0]->get(), config.using_namespace()); + + if (element_opt.has_value()) + res = element_opt.value().context(); + + return res; + }); + + // Convert C++ entity to PlantUML alias, e.g. + // "note left of {{ alias("A") }}: This is a note" + // Shortcut to: + // {{ element("A").alias }} + // + m_env.add_callback("alias", 1, [&model, &config](inja::Arguments &args) { + auto element_opt = model.get_with_namespace( + args[0]->get(), config.using_namespace()); + + if (!element_opt.has_value()) + throw clanguml::error::uml_alias_missing( + args[0]->get()); + + return element_opt.value().alias(); + }); + + // Get elements' comment: + // "note left of {{ alias("A") }}: {{ comment("A") }}" + // Shortcut to: + // {{ element("A").comment }} + // + m_env.add_callback("comment", 1, [&model, &config](inja::Arguments &args) { + inja::json res{}; + auto element_opt = model.get_with_namespace( + args[0]->get(), config.using_namespace()); + + if (!element_opt.has_value()) + throw clanguml::error::uml_alias_missing( + args[0]->get()); + + auto comment = element_opt.value().comment(); + + if (comment.has_value()) { + assert(comment.value().is_object()); + res = comment.value(); + } + + return res; + }); +} +} // namespace clanguml::common::generators::mermaid \ No newline at end of file diff --git a/src/common/types.h b/src/common/types.h index 3541d61f..a3f828d6 100644 --- a/src/common/types.h +++ b/src/common/types.h @@ -36,7 +36,8 @@ using id_t = int64_t; */ enum class generator_type_t { plantuml, /*!< Diagrams will be gnerated in PlantUML format */ - json /*!< Diagrams will be generated in JSON format */ + json, /*!< Diagrams will be generated in JSON format */ + mermaid /*!< Diagrams will be generated in MermaidJS format */ }; std::string to_string(const std::string &s); diff --git a/src/config/config.cc b/src/config/config.cc index a77571ea..9a7b39e4 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -172,6 +172,12 @@ void plantuml::append(const plantuml &r) after.insert(after.end(), r.after.begin(), r.after.end()); } +void mermaid::append(const mermaid &r) +{ + before.insert(before.end(), r.before.begin(), r.before.end()); + after.insert(after.end(), r.after.begin(), r.after.end()); +} + void inheritable_diagram_options::inherit( const inheritable_diagram_options &parent) { diff --git a/src/config/config.h b/src/config/config.h index 8a634c39..a6c95c35 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -125,6 +125,22 @@ struct plantuml { void append(const plantuml &r); }; +/** + * @brief MermaidJS diagram config section + * + * This configuration option can be used to add any MermaidJS directives + * before or after the generated diagram, such as diagram name, or custom + * notes. + */ +struct mermaid { + /*! List of directives to add before diagram */ + std::vector before; + /*! List of directives to add before diagram */ + std::vector after; + + void append(const mermaid &r); +}; + /** * @brief Definition of diagram template */ @@ -430,6 +446,7 @@ struct inheritable_diagram_options { option include{"include"}; option exclude{"exclude"}; option puml{"plantuml", option_inherit_mode::kAppend}; + option mermaid{"mermaid", option_inherit_mode::kAppend}; option generate_method_arguments{ "generate_method_arguments", method_arguments::full}; option group_methods{"group_methods", true}; diff --git a/tests/t00002/test_case.h b/tests/t00002/test_case.h index 4cfd2cfd..ec976f02 100644 --- a/tests/t00002/test_case.h +++ b/tests/t00002/test_case.h @@ -107,4 +107,10 @@ TEST_CASE("t00002", "[test-case][class]") save_json(config.output_directory() + "/" + diagram->name + ".json", j); } + { + auto mmd = generate_class_mermaid(diagram, *model); + + save_puml( + config.output_directory() + "/" + diagram->name + ".mmd", mmd); + } } \ No newline at end of file diff --git a/tests/t00003/test_case.h b/tests/t00003/test_case.h index 33f1774b..c7cb2b7a 100644 --- a/tests/t00003/test_case.h +++ b/tests/t00003/test_case.h @@ -95,4 +95,10 @@ TEST_CASE("t00003", "[test-case][class]") save_json(config.output_directory() + "/" + diagram->name + ".json", j); } + { + auto mmd = generate_class_mermaid(diagram, *model); + + save_puml( + config.output_directory() + "/" + diagram->name + ".mmd", mmd); + } } diff --git a/tests/t00004/test_case.h b/tests/t00004/test_case.h index a41d6a2b..316878fe 100644 --- a/tests/t00004/test_case.h +++ b/tests/t00004/test_case.h @@ -91,4 +91,10 @@ TEST_CASE("t00004", "[test-case][class]") save_json(config.output_directory() + "/" + diagram->name + ".json", j); } + { + auto mmd = generate_class_mermaid(diagram, *model); + + save_puml( + config.output_directory() + "/" + diagram->name + ".mmd", mmd); + } } diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 1c9b3919..64874fac 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -111,6 +111,24 @@ auto generate_diagram_json( return nlohmann::json::parse(ss.str()); } + +template +auto generate_diagram_mermaid( + std::shared_ptr config, DiagramModel &model) +{ + using diagram_config = DiagramConfig; + using diagram_model = DiagramModel; + using diagram_generator = + typename clanguml::common::generators::diagram_generator_t< + DiagramConfig, + clanguml::common::generators::mermaid_generator_tag>::type; + + std::stringstream ss; + + ss << diagram_generator(dynamic_cast(*config), model); + + return ss.str(); +} } std::unique_ptr generate_class_diagram( @@ -209,6 +227,14 @@ nlohmann::json generate_include_json( config, model); } +std::string generate_class_mermaid( + std::shared_ptr config, + clanguml::class_diagram::model::diagram &model) +{ + return detail::generate_diagram_mermaid( + config, model); +} + void save_puml(const std::string &path, const std::string &puml) { std::filesystem::path p{path}; @@ -229,6 +255,17 @@ void save_json(const std::string &path, const nlohmann::json &j) ofs.close(); } +void save_mermaid(const std::string &path, const std::string &mmd) +{ + std::filesystem::path p{path}; + std::filesystem::create_directory(p.parent_path()); + std::ofstream ofs; + ofs.open(p, std::ofstream::out | std::ofstream::trunc); + ofs << mmd; + ofs.close(); +} + + using namespace clanguml::test::matchers; /// diff --git a/tests/test_cases.h b/tests/test_cases.h index 980cde6f..8c9aa56d 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -20,6 +20,7 @@ #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG #include "class_diagram/generators/plantuml/class_diagram_generator.h" +#include "class_diagram/generators/mermaid/class_diagram_generator.h" #include "class_diagram/model/diagram.h" #include "class_diagram/visitor/translation_unit_visitor.h" #include "common/clang_utils.h"