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