Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
injabridge.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "injabridge.h"
5
6#include <cmath>
7
8QT_BEGIN_NAMESPACE
9
10static std::string escapeHtml(const std::string &input)
11{
12 std::string buffer;
13 buffer.reserve(input.size() + input.size() / 8);
14 for (char c : input) {
15 switch (c) {
16 case '&':
17 buffer += "&amp;";
18 break;
19 case '"':
20 buffer += "&quot;";
21 break;
22 case '\'':
23 buffer += "&apos;";
24 break;
25 case '<':
26 buffer += "&lt;";
27 break;
28 case '>':
29 buffer += "&gt;";
30 break;
31 default:
32 buffer += c;
33 break;
34 }
35 }
36 return buffer;
37}
38
39static std::string renderSignatureSpans(const nlohmann::json &spans)
40{
41 if (!spans.is_array()) {
42 qWarning("render_signature_spans: expected JSON array, got %s",
43 spans.type_name());
44 return {};
45 }
46
47 std::string result;
48 for (const auto &s : spans) {
49 const auto role = s.value("role", "");
50 const auto text = s.value("text", "");
51 const bool hasHref = s.contains("href");
52 const auto href = s.value("href", "");
53
54 if (role == "extra") {
55 result += R"(<code class="details extra" translate="no">)";
56 if (s.contains("children") && s["children"].is_array()) {
57 for (const auto &c : s["children"]) {
58 if (c.value("role", "") == "external-ref")
59 result += "<a href=\"" + c.value("href", "") + "\">"
60 + escapeHtml(c.value("text", "")) + "</a>";
61 else
62 result += escapeHtml(c.value("text", ""));
63 }
64 } else {
65 result += escapeHtml(text);
66 }
67 result += "</code>";
68 } else if (role == "type") {
69 result += R"(<span class="type">)";
70 if (hasHref)
71 result += "<a href=\"" + href + "\">";
72 result += escapeHtml(text);
73 if (hasHref)
74 result += "</a>";
75 result += "</span>";
76 } else if (role == "name") {
77 result += R"(<span class="name">)";
78 if (hasHref)
79 result += "<a href=\"" + href + "\">";
80 result += escapeHtml(text);
81 if (hasHref)
82 result += "</a>";
83 result += "</span>";
84 } else if (role == "parameter") {
85 result += "<i>" + escapeHtml(text) + "</i>";
86 } else if (role == "external-ref") {
87 result += "<a href=\"" + href + "\">" + escapeHtml(text) + "</a>";
88 } else if (role == "template-decl") {
89 result += R"(<span class="template-decl">)";
90 if (s.contains("children") && s["children"].is_array()) {
91 for (const auto &c : s["children"]) {
92 if (c.value("role", "") == "type")
93 result += R"(<span class="type">)" + escapeHtml(c.value("text", ""))
94 + "</span>";
95 else
96 result += escapeHtml(c.value("text", ""));
97 }
98 }
99 result += "</span>";
100 } else {
101 result += escapeHtml(text);
102 }
103 }
104 return result;
105}
106
107static void registerCallbacks(inja::Environment &env)
108{
109 env.add_callback("escape_html", 1, [](inja::Arguments &args) {
110 return escapeHtml(args.at(0)->get<std::string>());
111 });
112
113 env.add_callback("render_signature_spans", 1, [](inja::Arguments &args) {
114 return renderSignatureSpans(*args.at(0));
115 });
116
117 env.add_callback("escape_md_table", 1, [](inja::Arguments &args) {
118 auto input = args.at(0)->get<std::string>();
119 std::string buffer;
120 buffer.reserve(input.size() + input.size() / 8);
121 for (char c : input) {
122 switch (c) {
123 case '|':
124 buffer += "\\|";
125 break;
126 case '\n':
127 buffer += ' ';
128 break;
129 case '\r':
130 break;
131 default:
132 buffer += c;
133 break;
134 }
135 }
136 return buffer;
137 });
138}
139
140/*!
141 \class InjaBridge
142 \brief Adapter for converting Qt JSON types to Inja template engine format.
143
144 InjaBridge provides static methods to convert between Qt's native JSON types
145 (QJsonObject, QJsonArray, QJsonValue) and nlohmann::json, which is the data
146 format expected by the Inja template engine.
147
148 This adapter allows QDoc to maintain its Qt-native API while leveraging
149 Inja for template-based documentation generation. All JSON data in QDoc's
150 intermediate representation (IR) uses Qt types, and InjaBridge handles the
151 conversion when rendering templates.
152
153 \note All numbers in QJsonValue are stored as doubles. Whole-number doubles
154 (e.g., 30.0, 2.0) are converted to int64_t so that template output renders
155 them as integers (e.g., "30" not "30.0"). Fractional values pass through
156 as doubles.
157
158 \note Inja and nlohmann::json may report template or data errors. QDoc is
159 built with exceptions disabled (\c{-fno-exceptions}), so such errors are
160 treated as fatal and will terminate the process. A custom \c INJA_THROW
161 override in the header ensures that error details (including source
162 location) are logged via \c qFatal() before termination, rather than
163 calling \c std::abort() silently.
164
165 All render methods register template callbacks:
166 \list
167 \li \c{escape_html()} escapes HTML special characters (\c{&}, \c{<},
168 \c{>}, \c{"}, \c{'}).
169 \li \c{escape_md_table()} escapes pipe characters and collapses newlines
170 for safe use inside Markdown table cells.
171 \li \c{render_signature_spans()} converts a JSON array of signature spans
172 (from SignatureSpan IR) into semantic HTML with role-based markup
173 (\c{<span class="type">}, \c{<span class="name">}, etc.).
174 \endlist
175 Escaping callbacks keep format-specific logic under template author control.
176 The \c{render_signature_spans()} callback centralizes signature rendering
177 that was previously duplicated across multiple templates.
178
179 \sa QJsonObject, QJsonArray, QJsonValue
180*/
181
182/*!
183 \brief Converts a QJsonValue, \a value, to nlohmann::json.
184
185 Handles all QJsonValue types: Null, Bool, Double, String, Array, Object,
186 and Undefined. Undefined values are treated as null.
187
188 Returns the equivalent nlohmann::json representation.
189*/
190nlohmann::json InjaBridge::toInjaJson(const QJsonValue &value)
191{
192 switch (value.type()) {
193 case QJsonValue::Null:
194 return nullptr;
195 case QJsonValue::Bool:
196 return value.toBool();
197 case QJsonValue::Double: {
198 double d = value.toDouble();
199 if (std::fmod(d, 1.0) == 0.0)
200 return static_cast<int64_t>(d);
201 return d;
202 }
203 case QJsonValue::String:
204 return value.toString().toUtf8().toStdString();
205 case QJsonValue::Array:
206 return toInjaJson(value.toArray());
207 case QJsonValue::Object:
208 return toInjaJson(value.toObject());
209 case QJsonValue::Undefined:
210 return nullptr;
211 }
212 return nullptr;
213}
214
215/*!
216 \brief Converts a QJsonObject, \a obj, to nlohmann::json.
217
218 Recursively converts all values in the object, preserving the key-value
219 structure. Nested objects and arrays are handled correctly.
220
221 Returns the equivalent nlohmann::json object.
222*/
223nlohmann::json InjaBridge::toInjaJson(const QJsonObject &obj)
224{
225 nlohmann::json result = nlohmann::json::object();
226
227 for (const auto &[key, value] : obj.asKeyValueRange())
228 result[key.toString().toUtf8().toStdString()] = toInjaJson(value);
229
230 return result;
231}
232
233/*!
234 \brief Converts a QJsonArray, \a array, to nlohmann::json.
235
236 Recursively converts all elements in the array, preserving order.
237 Mixed-type arrays are supported.
238
239 Returns the equivalent nlohmann::json array.
240*/
241nlohmann::json InjaBridge::toInjaJson(const QJsonArray &array)
242{
243 nlohmann::json result = nlohmann::json::array();
244
245 for (const QJsonValue &value : array)
246 result.push_back(toInjaJson(value));
247
248 return result;
249}
250
251/*!
252 \brief Renders a template string, \a templateStr, with provided \a data.
253
254 Uses Inja to render the template with the given JSON data. The data
255 is automatically converted from QJsonObject to nlohmann::json.
256
257 The Inja template string, \a templateStr, supports Jinja2 syntax. \a data is
258 the JSON data to use for rendering.
259
260 Returns the rendered template as a QString.
261*/
262QString InjaBridge::render(const QString &templateStr, const QJsonObject &data)
263{
264 inja::Environment env;
265 // Replace Inja's default "##" line statement prefix, which conflicts
266 // with Markdown headings. "%!" echoes Jinja2's "%" (statement) and
267 // QDoc's "!" (documentation marker), and is inert in both HTML and
268 // Markdown.
269 env.set_line_statement("%!");
270 env.set_trim_blocks(true);
271 env.set_lstrip_blocks(true);
272 registerCallbacks(env);
273 nlohmann::json jsonData = toInjaJson(data);
274
275 std::string templateUtf8 = templateStr.toUtf8().toStdString();
276 std::string resultUtf8 = env.render(templateUtf8, jsonData);
277
278 return QString::fromUtf8(resultUtf8.c_str());
279}
280
281/*!
282 \brief Renders a template string, \a templateStr, with provided \a data,
283 using \a includeCallback to resolve \c{{% include %}} directives.
284
285 This overload configures the Inja environment with a custom include
286 callback so that templates can use \c{{% include "name" %}} directives.
287 The \a includeCallback receives the include name and returns the partial's
288 content as a QString. If the callback returns an empty string, the include
289 is treated as missing and a fatal error is raised.
290
291 This enables Inja's include mechanism to work with Qt's resource system,
292 where \c{std::ifstream} cannot open \c{:/} paths.
293
294 Returns the rendered template as a QString.
295*/
296QString InjaBridge::render(const QString &templateStr, const QJsonObject &data,
297 const IncludeCallback &includeCallback)
298{
299 inja::Environment env;
300 env.set_line_statement("%!");
301 env.set_trim_blocks(true);
302 env.set_lstrip_blocks(true);
303 registerCallbacks(env);
304 env.set_search_included_templates_in_files(false);
305 env.set_include_callback(
306 [&includeCallback, &env](const std::filesystem::path & /*path*/,
307 const std::string &name) -> inja::Template {
308 QString content = includeCallback(QString::fromStdString(name));
309 if (content.isEmpty()) {
311 inja::FileError("include not found: '" + name + "'"));
312 }
313 return env.parse(content.toUtf8().toStdString());
314 });
315
316 nlohmann::json jsonData = toInjaJson(data);
317 std::string templateUtf8 = templateStr.toUtf8().toStdString();
318 std::string resultUtf8 = env.render(templateUtf8, jsonData);
319
320 return QString::fromUtf8(resultUtf8.c_str());
321}
322
323/*!
324 \brief Renders a template file with provided data.
325
326 Loads and renders a template from the filesystem, using \a templatePath
327 which holds the absolute path to the template file. The file should use
328 Inja/Jinja2 syntax. \a data is the JSON data to use for rendering.
329
330 Returns the rendered template as a QString.
331*/
332QString InjaBridge::renderFile(const QString &templatePath, const QJsonObject &data)
333{
334 inja::Environment env;
335 env.set_line_statement("%!");
336 env.set_trim_blocks(true);
337 env.set_lstrip_blocks(true);
338 registerCallbacks(env);
339 nlohmann::json jsonData = toInjaJson(data);
340
341 std::string pathUtf8 = templatePath.toUtf8().toStdString();
342 std::string resultUtf8 = env.render_file(pathUtf8, jsonData);
343
344 return QString::fromUtf8(resultUtf8.c_str());
345}
346
347QT_END_NAMESPACE
static void registerCallbacks(inja::Environment &env)
static std::string renderSignatureSpans(const nlohmann::json &spans)
#define INJA_THROW(exception)
Definition injabridge.h:14