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