0.5.4
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
generator.h
Go to the documentation of this file.
1/**
2 * @file src/common/generators/generator.h
3 *
4 * Copyright (c) 2021-2024 Bartek Kryza <bkryza@gmail.com>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18#pragma once
19
21#include "util/error.h"
22#include "util/util.h"
23
24#include <inja/inja.hpp>
25
26#include <optional>
27#include <ostream>
28#include <regex>
29#include <string>
30
32
34 inja::json &context, const std::string &prefix);
35
36/**
37 * @brief Common diagram generator interface
38 *
39 * This class defines common interface for all diagram generators.
40 */
41template <typename ConfigType, typename DiagramType> class generator {
42public:
43 /**
44 * @brief Constructor
45 *
46 * @param config Reference to instance of @link clanguml::config::diagram
47 * @param model Reference to instance of @link clanguml::model::diagram
48 */
49 generator(ConfigType &config, DiagramType &model)
51 , model_{model}
52 {
54 init_env();
55 }
56
57 virtual ~generator() = default;
58
59 /**
60 * @brief Generate diagram
61 *
62 * This is the main diagram generation entrypoint. It is responsible for
63 * calling other methods in appropriate order to generate the diagram into
64 * the output stream. It generates diagram elements, that are common
65 * to all types of diagrams in a given generator.
66 *
67 * @param ostr Output stream
68 */
69 virtual void generate(std::ostream &ostr) const = 0;
70
71 /**
72 * @brief Get reference to diagram config
73 *
74 * @return Diagram config
75 */
76 const ConfigType &config() const { return config_; }
77
78 /**
79 * @brief Get reference to diagram model
80 *
81 * @return Diagram model
82 */
83 const DiagramType &model() const { return model_; }
84
85 template <typename E> inja::json element_context(const E &e) const;
86
87 std::optional<std::pair<std::string, std::string>> get_link_pattern(
88 const common::model::source_location &sl) const;
89
90 std::optional<std::pair<std::string, std::string>> get_tooltip_pattern(
91 const common::model::source_location &sl) const;
92
93 /**
94 * @brief Initialize diagram Jinja context
95 */
96 void init_context();
97
98 /**
99 * @brief Update diagram Jinja context
100 *
101 * This method updates the diagram context with models properties
102 * which can be used to render Jinja templates in the diagram (e.g.
103 * in notes or links)
104 */
105 void update_context() const;
106
107 void init_env();
108
109 const inja::json &context() const;
110
111 inja::Environment &env() const;
112
113protected:
114 mutable inja::json m_context;
115 mutable inja::Environment m_env;
116
117private:
118 ConfigType &config_;
119 DiagramType &model_;
120};
121
122template <typename C, typename D> void generator<C, D>::init_context()
123{
124 const auto &config = generators::generator<C, D>::config();
125
126 if (config.git) {
127 m_context["git"]["branch"] = config.git().branch;
128 m_context["git"]["revision"] = config.git().revision;
129 m_context["git"]["commit"] = config.git().commit;
130 m_context["git"]["toplevel"] = config.git().toplevel;
131 }
132}
133
134template <typename C, typename D> void generator<C, D>::update_context() const
135{
136 m_context["diagram"] = model().context();
137}
138
139template <typename C, typename D>
140const inja::json &generator<C, D>::context() const
141{
142 return m_context;
143}
144
145template <typename C, typename D>
146inja::Environment &generator<C, D>::env() const
147{
148 return m_env;
149}
150
151template <typename C, typename D> void generator<C, D>::init_env()
152{
153 const auto &model = generators::generator<C, D>::model();
154 const auto &config = generators::generator<C, D>::config();
155
156 //
157 // Add basic string functions to inja environment
158 //
159
160 // Check if string is empty
161 m_env.add_callback("empty", 1, [](inja::Arguments &args) {
162 return args.at(0)->get<std::string>().empty();
163 });
164
165 // Remove spaces from the left of a string
166 m_env.add_callback("ltrim", 1, [](inja::Arguments &args) {
167 return util::ltrim(args.at(0)->get<std::string>());
168 });
169
170 // Remove trailing spaces from a string
171 m_env.add_callback("rtrim", 1, [](inja::Arguments &args) {
172 return util::rtrim(args.at(0)->get<std::string>());
173 });
174
175 // Remove spaces before and after a string
176 m_env.add_callback("trim", 1, [](inja::Arguments &args) {
177 return util::trim(args.at(0)->get<std::string>());
178 });
179
180 // Make a string shorted with a limit to
181 m_env.add_callback("abbrv", 2, [](inja::Arguments &args) {
182 return util::abbreviate(
183 args.at(0)->get<std::string>(), args.at(1)->get<unsigned>());
184 });
185
186 m_env.add_callback("replace", 3, [](inja::Arguments &args) {
187 std::string result = args[0]->get<std::string>();
188 std::regex pattern(args[1]->get<std::string>());
189 return std::regex_replace(result, pattern, args[2]->get<std::string>());
190 });
191
192 m_env.add_callback("split", 2, [](inja::Arguments &args) {
193 return util::split(
194 args[0]->get<std::string>(), args[1]->get<std::string>());
195 });
196
197 //
198 // Add PlantUML specific functions
199 //
200
201 // Return the entire element JSON context based on element name
202 // e.g.:
203 // {{ element("clanguml::t00050::A").comment }}
204 //
205 m_env.add_callback("element", 1, [&model, &config](inja::Arguments &args) {
206 inja::json res{};
207 auto element_opt = model.get_with_namespace(
208 args[0]->get<std::string>(), config.using_namespace());
209
210 if (element_opt.has_value())
211 res = element_opt.value().context();
212
213 return res;
214 });
215
216 // Convert C++ entity to PlantUML alias, e.g.
217 // "note left of {{ alias("A") }}: This is a note"
218 // Shortcut to:
219 // {{ element("A").alias }}
220 //
221 m_env.add_callback("alias", 1, [&model, &config](inja::Arguments &args) {
222 auto element_opt = model.get_with_namespace(
223 args[0]->get<std::string>(), config.using_namespace());
224
225 if (!element_opt.has_value())
227 args[0]->get<std::string>());
228
229 return element_opt.value().alias();
230 });
231
232 // Get elements' comment:
233 // "note left of {{ alias("A") }}: {{ comment("A") }}"
234 // Shortcut to:
235 // {{ element("A").comment }}
236 //
237 m_env.add_callback("comment", 1, [&model, &config](inja::Arguments &args) {
238 inja::json res{};
239 auto element_opt = model.get_with_namespace(
240 args[0]->get<std::string>(), config.using_namespace());
241
242 if (!element_opt.has_value())
244 args[0]->get<std::string>());
245
246 auto comment = element_opt.value().comment();
247
248 if (comment.has_value()) {
249 assert(comment.value().is_object());
250 res = comment.value();
251 }
252
253 return res;
254 });
255}
256
257template <typename C, typename D>
258template <typename E>
259inja::json generator<C, D>::element_context(const E &e) const
260{
261 const auto &diagram_context = context();
262
263 inja::json ctx;
264 ctx["element"] = e.context();
265#if _MSC_VER
266 if (ctx.contains("git")) {
267#else
268 if (diagram_context.template contains("git")) {
269#endif
270 ctx["git"] = diagram_context["git"];
271 }
272
273 if (!e.file().empty()) {
274 std::filesystem::path file{e.file()};
275 std::string git_relative_path = file.string();
276 if (!e.file_relative().empty()) {
277#if _MSC_VER
278 if (file.is_absolute() && ctx.contains("git")) {
279#else
280 if (file.is_absolute() &&
281 diagram_context.template contains("git")) {
282#endif
283 git_relative_path = std::filesystem::relative(
284 file, diagram_context["git"]["toplevel"])
285 .string();
286 ctx["element"]["source"]["path"] =
287 util::path_to_url(git_relative_path);
288 }
289 else {
290 ctx["element"]["source"]["path"] = e.file();
291 }
292 }
293 else {
294 git_relative_path = "";
295 ctx["element"]["source"]["path"] = e.file();
296 }
297
298 ctx["element"]["source"]["full_path"] = file.string();
299 ctx["element"]["source"]["name"] = file.filename().string();
300 ctx["element"]["source"]["line"] = e.line();
301 }
302
303 const auto &maybe_comment = e.comment();
304 if (maybe_comment) {
305 ctx["element"]["comment"] = maybe_comment.value();
306 }
307
308 return ctx;
309}
310
311template <typename C, typename D>
312std::optional<std::pair<std::string, std::string>>
314 const common::model::source_location &sl) const
315{
316 if (sl.file_relative().empty()) {
317 return config().generate_links().get_link_pattern(sl.file());
318 }
319
320 return config().generate_links().get_link_pattern(sl.file_relative());
321}
322
323template <typename C, typename D>
324std::optional<std::pair<std::string, std::string>>
326 const common::model::source_location &sl) const
327{
328 if (sl.file_relative().empty()) {
329 return config().generate_links().get_tooltip_pattern(sl.file());
330 }
331
332 return config().generate_links().get_tooltip_pattern(sl.file_relative());
333}
334} // namespace clanguml::common::generators