0.6.0
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-2025 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
23#include "display_adapters.h"
24#include "util/error.h"
25#include "util/util.h"
26
27#include <inja/inja.hpp>
28
29#include <optional>
30#include <ostream>
31#include <regex>
32#include <string>
33
35
37 inja::json &context, const std::string &prefix);
38
39/**
40 * @brief Common diagram generator interface
41 *
42 * This class defines common interface for all diagram generators.
43 */
44template <typename ConfigType, typename DiagramType> class generator {
45public:
46 /**
47 * @brief Constructor
48 *
49 * @param config Reference to instance of @link clanguml::config::diagram
50 * @param model Reference to instance of @link clanguml::model::diagram
51 */
52 generator(ConfigType &config, DiagramType &model)
54 , model_{model}
55 {
57 init_env();
58 }
59
60 virtual ~generator() = default;
61
62 /**
63 * @brief Generate diagram
64 *
65 * This is the main diagram generation entrypoint. It is responsible for
66 * calling other methods in appropriate order to generate the diagram into
67 * the output stream. It generates diagram elements, that are common
68 * to all types of diagrams in a given generator.
69 *
70 * @param ostr Output stream
71 */
72 virtual void generate(std::ostream &ostr) const = 0;
73
74 /**
75 * @brief Get reference to diagram config
76 *
77 * @return Diagram config
78 */
79 const ConfigType &config() const { return config_; }
80
81 /**
82 * @brief Get reference to diagram model
83 *
84 * @return Diagram model
85 */
86 const DiagramType &model() const { return model_; }
87
88 std::optional<std::pair<std::string, std::string>> get_link_pattern(
89 const common::model::source_location &sl) const;
90
91 std::optional<std::pair<std::string, std::string>> get_tooltip_pattern(
92 const common::model::source_location &sl) const;
93
94 std::optional<std::string> render_link(
95 const common::model::diagram_element &e) const;
96
97 std::optional<std::string> render_link(
98 const common::model::relationship &e) const;
99
100 std::optional<std::string> render_tooltip(
101 const common::model::diagram_element &e) const;
102
103 std::optional<std::string> render_tooltip(
104 const common::model::relationship &e) const;
105
106 /**
107 * @brief Initialize diagram Jinja context
108 */
109 void init_context();
110
111 /**
112 * @brief Update diagram Jinja context
113 *
114 * This method updates the diagram context with models properties
115 * which can be used to render Jinja templates in the diagram (e.g.
116 * in notes or links)
117 */
118 void update_context() const;
119
120 void init_env();
121
122 const inja::json &context() const;
123
124 inja::Environment &env() const;
125
126protected:
127 mutable inja::json m_context;
128 mutable inja::Environment m_env;
129
130private:
131 ConfigType &config_;
132 DiagramType &model_;
133};
134
135template <typename C, typename D> void generator<C, D>::init_context()
136{
137 const auto &config = generators::generator<C, D>::config();
138
139 if (config.git) {
140 m_context["git"]["branch"] = config.git().branch;
141 m_context["git"]["revision"] = config.git().revision;
142 m_context["git"]["commit"] = config.git().commit;
143 m_context["git"]["toplevel"] = config.git().toplevel;
144 }
145
146 if (config.user_data) {
147 m_context["user_data"] = config.user_data();
148 }
149}
150
151template <typename C, typename D>
152const inja::json &generator<C, D>::context() const
153{
154 return m_context;
155}
156
157template <typename C, typename D>
158inja::Environment &generator<C, D>::env() const
159{
160 return m_env;
161}
162
163template <typename C, typename D> void generator<C, D>::init_env()
164{
165 const auto &model = generators::generator<C, D>::model();
166 const auto &config = generators::generator<C, D>::config();
167
168 //
169 // Add basic string functions to inja environment
170 //
171
172 // Check if string is empty
173 m_env.add_callback("empty", 1, [](inja::Arguments &args) {
174 return args.at(0)->get<std::string>().empty();
175 });
176
177 // Remove spaces from the left of a string
178 m_env.add_callback("ltrim", 1, [](inja::Arguments &args) {
179 return util::ltrim(args.at(0)->get<std::string>());
180 });
181
182 // Remove trailing spaces from a string
183 m_env.add_callback("rtrim", 1, [](inja::Arguments &args) {
184 return util::rtrim(args.at(0)->get<std::string>());
185 });
186
187 // Remove spaces before and after a string
188 m_env.add_callback("trim", 1, [](inja::Arguments &args) {
189 return util::trim(args.at(0)->get<std::string>());
190 });
191
192 // Make a string shorted with a limit to
193 m_env.add_callback("abbrv", 2, [](inja::Arguments &args) {
194 return util::abbreviate(
195 args.at(0)->get<std::string>(), args.at(1)->get<unsigned>());
196 });
197
198 m_env.add_callback("replace", 3, [](inja::Arguments &args) {
199 std::string result = args[0]->get<std::string>();
200 std::regex pattern(args[1]->get<std::string>());
201 return std::regex_replace(result, pattern, args[2]->get<std::string>());
202 });
203
204 m_env.add_callback("split", 2, [](inja::Arguments &args) {
205 return util::split(
206 args[0]->get<std::string>(), args[1]->get<std::string>());
207 });
208
209 //
210 // Add PlantUML specific functions
211 //
212
213 // Return the entire element JSON context based on element name
214 // e.g.:
215 // {{ element("clanguml::t00050::A").comment }}
216 //
217 m_env.add_callback("element", 1, [&model, &config](inja::Arguments &args) {
218 inja::json res{};
219 auto element_opt = model.get_with_namespace(
220 args[0]->get<std::string>(), config.using_namespace());
221
222 if (element_opt.has_value()) {
224 dynamic_cast<const model::element &>(element_opt.value())};
225 }
226
227 return res;
228 });
229
230 // Convert C++ entity to PlantUML alias, e.g.
231 // "note left of {{ alias("A") }}: This is a note"
232 // Shortcut to:
233 // {{ element("A").alias }}
234 //
235 m_env.add_callback("alias", 1, [&model, &config](inja::Arguments &args) {
236 const auto &element_name = args[0]->get<std::string>();
237 auto element_opt =
238 model.get_with_namespace(element_name, config.using_namespace());
239
240 if (!element_opt.has_value())
242 args[0]->get<std::string>());
243
244 return element_opt.value().alias();
245 });
246
247 // Get elements' comment:
248 // "note left of {{ alias("A") }}: {{ comment("A") }}"
249 // Shortcut to:
250 // {{ element("A").comment }}
251 //
252 m_env.add_callback("comment", 1, [&model, &config](inja::Arguments &args) {
253 inja::json res{};
254 auto element_opt = model.get_with_namespace(
255 args[0]->get<std::string>(), config.using_namespace());
256
257 if (!element_opt.has_value())
259 args[0]->get<std::string>());
260
261 auto comment = element_opt.value().comment();
262
263 if (comment.has_value()) {
264 assert(comment.value().is_object());
265 res = comment.value();
266 }
267
268 return res;
269 });
270}
271
272template <typename C, typename D>
273std::optional<std::pair<std::string, std::string>>
275 const common::model::source_location &sl) const
276{
277 if (sl.file_relative().empty()) {
278 return config().generate_links().get_link_pattern(sl.file());
279 }
280
281 return config().generate_links().get_link_pattern(sl.file_relative());
282}
283
284template <typename C, typename D>
285std::optional<std::pair<std::string, std::string>>
287 const common::model::source_location &sl) const
288{
289 if (sl.file_relative().empty()) {
290 return config().generate_links().get_tooltip_pattern(sl.file());
291 }
292
293 return config().generate_links().get_tooltip_pattern(sl.file_relative());
294}
295
296template <typename C, typename D>
297std::optional<std::string> generator<C, D>::render_link(
298 const common::model::diagram_element &e) const
299{
302
303 if (e.file().empty() && e.file_relative().empty())
304 return {};
305
306 auto maybe_link_pattern = generators::generator<C, D>::get_link_pattern(e);
307
308 if (!maybe_link_pattern)
309 return {};
310
311 const auto &[link_prefix, link_pattern] = *maybe_link_pattern;
312
315
316 make_context_source_relative(ec, link_prefix);
317
318 return render_template(
319 generators::generator<C, D>::env(), ec, link_pattern);
320}
321
322template <typename C, typename D>
323std::optional<std::string> generator<C, D>::render_link(
324 const common::model::relationship &e) const
325{
328
329 if (e.file().empty() && e.file_relative().empty())
330 return {};
331
332 auto maybe_link_pattern = generators::generator<C, D>::get_link_pattern(e);
333
334 if (!maybe_link_pattern)
335 return {};
336
337 const auto &[link_prefix, link_pattern] = *maybe_link_pattern;
338
341
342 make_context_source_relative(ec, link_prefix);
343
344 return render_template(
345 generators::generator<C, D>::env(), ec, link_pattern);
346}
347
348template <typename C, typename D>
349std::optional<std::string> generator<C, D>::render_tooltip(
350 const common::model::diagram_element &e) const
351{
354
355 if (e.file().empty() && e.file_relative().empty())
356 return {};
357
358 auto maybe_tooltip_pattern =
360
361 if (!maybe_tooltip_pattern)
362 return {};
363
364 const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
365
368
369 make_context_source_relative(ec, tooltip_prefix);
370
371 return render_template(
372 generators::generator<C, D>::env(), ec, tooltip_pattern);
373}
374
375template <typename C, typename D>
376std::optional<std::string> generator<C, D>::render_tooltip(
377 const common::model::relationship &e) const
378{
381
382 if (e.file().empty() && e.file_relative().empty())
383 return {};
384
385 auto maybe_tooltip_pattern =
387
388 if (!maybe_tooltip_pattern)
389 return {};
390
391 const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
392
395
396 make_context_source_relative(ec, tooltip_prefix);
397
398 return render_template(
399 generators::generator<C, D>::env(), ec, tooltip_pattern);
400}
401} // namespace clanguml::common::generators