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 <QtCore/qlogging.h>
9
10#include <cmath>
11#include <functional>
12#include <utility>
13
14QT_BEGIN_NAMESPACE
15
16using namespace Qt::Literals;
17
19namespace {
20
21InjaBridge::RenderContext activeRenderContext;
22
23// Makes a render's context the one qdocFatalTemplateRenderError() reports
24// while that render runs, and restores the enclosing render's context when
25// a nested render returns.
26class RenderContextScope
27{
28 Q_DISABLE_COPY_MOVE(RenderContextScope)
29
30public:
31 explicit RenderContextScope(const InjaBridge::RenderContext &context)
32 : m_previous(std::exchange(activeRenderContext, context))
33 {
34 }
35 ~RenderContextScope() { activeRenderContext = std::move(m_previous); }
36
37private:
38 InjaBridge::RenderContext m_previous;
39};
40
41} // namespace
42#endif // !QDOC_TEMPLATE_LIBS_THROW
43
44/*!
45 \brief Formats \a message with the failing render's \a context.
46*/
47QString InjaBridge::renderErrorText(const RenderContext &context, const QString &message)
48{
49 const auto name = [](const QString &value) { return value.isEmpty() ? u"<unknown>"_s : value; };
50 return u"Failed to render page '"_s + name(context.page) + u"' with template '"_s
51 + name(context.templatePath) + u"' in format '"_s + name(context.format) + u"': "_s
52 + message;
53}
54
56/*!
57 \internal
58 The error-reporting entry point for template library errors in a
59 no-exceptions build. The header redefines INJA_THROW and
60 JSON_THROW_USER, the error-reporting entry points of Inja and
61 nlohmann::json, to this function before including the libraries, so
62 a library error terminates the run with the diagnostic for the
63 render in progress instead of a bare abort.
64*/
65[[noreturn]] void qdocFatalTemplateRenderError(const char *what)
66{
67 qFatal("%s", qPrintable(InjaBridge::renderErrorText(activeRenderContext,
68 QString::fromUtf8(what))));
69}
70#endif // !QDOC_TEMPLATE_LIBS_THROW
71
72static InjaBridge::RenderResult renderWithPolicy(const InjaBridge::RenderContext &context,
73 const std::function<QString()> &render)
74{
76 Q_UNUSED(context);
77 try {
78 return render();
79 } catch (const std::exception &e) {
80 return InjaBridge::RenderFailure{ QString::fromUtf8(e.what()) };
81 }
82#else
83 const RenderContextScope scope(context);
84 return render();
85#endif
86}
87
88static std::string escapeHtml(const std::string &input)
89{
90 std::string buffer;
91 buffer.reserve(input.size() + input.size() / 8);
92 for (char c : input) {
93 switch (c) {
94 case '&':
95 buffer += "&amp;";
96 break;
97 case '"':
98 buffer += "&quot;";
99 break;
100 case '\'':
101 buffer += "&apos;";
102 break;
103 case '<':
104 buffer += "&lt;";
105 break;
106 case '>':
107 buffer += "&gt;";
108 break;
109 default:
110 buffer += c;
111 break;
112 }
113 }
114 return buffer;
115}
116
117// Render a single link span for either output surface. HTML escapes both the
118// visible text and the href; a span with no usable href degrades to plain text
119// rather than a dangling link. Markdown emits a bracket-paren link with the
120// text and URL as-is: the concept names and generated hrefs in scope here carry
121// no Markdown-special characters, and general Markdown escaping is out of scope.
122static std::string renderLinkSpan(const std::string &text, const std::string &href,
123 bool hasHref, bool isMarkdown)
124{
125 if (isMarkdown)
126 return hasHref ? "[" + text + "](" + href + ")" : text;
127 if (hasHref)
128 return "<a href=\"" + escapeHtml(href) + "\">" + escapeHtml(text) + "</a>";
129 return escapeHtml(text);
130}
131
132// Render a span subtree as native Markdown. Any span carrying an href — a
133// concept link, a linked type, an external reference — becomes a Markdown
134// link; every other role contributes its text and recurses into its children,
135// so structural roles (type, name, template-decl, the [signal]/[slot] tags)
136// stay plain text instead of HTML markup. Markdown-special characters in the
137// signature text are not escaped here; see the Markdown-first-class follow-up.
138static std::string renderSpanMarkdown(const nlohmann::json &s)
139{
140 const auto text = s.value("text", "");
141 const auto href = s.value("href", "");
142 const bool hasHref = s.contains("href") && !href.empty();
143 if (hasHref)
144 return renderLinkSpan(text, href, hasHref, /*isMarkdown=*/true);
145
146 std::string result = text;
147 if (s.contains("children") && s["children"].is_array()) {
148 for (const auto &c : s["children"])
149 result += renderSpanMarkdown(c);
150 }
151 return result;
152}
153
154static std::string renderSignatureSpans(const nlohmann::json &spans, const QString &format)
155{
156 if (!spans.is_array()) {
157 qWarning("render_signature_spans: expected JSON array, got %s",
158 spans.type_name());
159 return {};
160 }
161
162 const bool isMarkdown = format.contains("markdown"_L1, Qt::CaseInsensitive);
163
164 std::string result;
165 for (const auto &s : spans) {
166 if (isMarkdown) {
167 result += renderSpanMarkdown(s);
168 continue;
169 }
170 const auto role = s.value("role", "");
171 const auto text = s.value("text", "");
172 const bool hasHref = s.contains("href");
173 const auto href = s.value("href", "");
174
175 if (role == "extra") {
176 result += R"(<code class="details extra" translate="no">)";
177 if (s.contains("children") && s["children"].is_array()) {
178 for (const auto &c : s["children"]) {
179 if (c.value("role", "") == "external-ref")
180 result += "<a href=\"" + c.value("href", "") + "\">"
181 + escapeHtml(c.value("text", "")) + "</a>";
182 else
183 result += escapeHtml(c.value("text", ""));
184 }
185 } else {
186 result += escapeHtml(text);
187 }
188 result += "</code>";
189 } else if (role == "type") {
190 result += R"(<span class="type">)";
191 if (hasHref)
192 result += "<a href=\"" + href + "\">";
193 result += escapeHtml(text);
194 if (hasHref)
195 result += "</a>";
196 result += "</span>";
197 } else if (role == "name") {
198 result += R"(<span class="name">)";
199 if (hasHref)
200 result += "<a href=\"" + href + "\">";
201 result += escapeHtml(text);
202 if (hasHref)
203 result += "</a>";
204 result += "</span>";
205 } else if (role == "parameter") {
206 result += "<i>" + escapeHtml(text) + "</i>";
207 } else if (role == "external-ref") {
208 result += "<a href=\"" + href + "\">" + escapeHtml(text) + "</a>";
209 } else if (role == "template-decl") {
210 result += R"(<span class="template-decl">)";
211 result += escapeHtml(text);
212 if (s.contains("children") && s["children"].is_array()) {
213 for (const auto &c : s["children"]) {
214 const auto childRole = c.value("role", "");
215 const auto childText = c.value("text", "");
216 const bool childHasHref = c.contains("href");
217 const auto childHref = c.value("href", "");
218 if (childRole == "type") {
219 result += R"(<span class="type">)" + escapeHtml(childText)
220 + "</span>";
221 } else if (childRole == "link") {
222 result += renderLinkSpan(childText, childHref, childHasHref, isMarkdown);
223 } else {
224 result += escapeHtml(childText);
225 }
226 }
227 }
228 result += "</span>";
229 } else if (role == "link") {
230 result += renderLinkSpan(text, href, hasHref, isMarkdown);
231 } else {
232 result += escapeHtml(text);
233 }
234 }
235 return result;
236}
237
238static void registerCallbacks(inja::Environment &env, const QString &format)
239{
240 env.add_callback("escape_html", 1, [](inja::Arguments &args) {
241 return escapeHtml(args.at(0)->get<std::string>());
242 });
243
244 env.add_callback("render_signature_spans", 1, [format](inja::Arguments &args) {
245 return renderSignatureSpans(*args.at(0), format);
246 });
247
248 // English-list punctuation for templates that iterate over a list of
249 // items. Emit {{ list_separator(loop.index, length(items)) }} after
250 // each item instead of a literal comma or period so the rendered
251 // output matches the legacy HTML generator's prose (for instance
252 // "See also a(), b(), and c." rather than a comma-less concatenation).
253 // loop.index is zero-based in Inja; TextUtils::separator expects the
254 // same convention.
255 env.add_callback("list_separator", 2, [](inja::Arguments &args) {
256 const auto pos = args.at(0)->get<qsizetype>();
257 const auto total = args.at(1)->get<qsizetype>();
258 return TextUtils::separator(pos, total).toStdString();
259 });
260
261 env.add_callback("escape_md_table", 1, [](inja::Arguments &args) {
262 auto input = args.at(0)->get<std::string>();
263 std::string buffer;
264 buffer.reserve(input.size() + input.size() / 8);
265 for (char c : input) {
266 switch (c) {
267 case '|':
268 buffer += "\\|";
269 break;
270 case '\n':
271 buffer += ' ';
272 break;
273 case '\r':
274 break;
275 default:
276 buffer += c;
277 break;
278 }
279 }
280 return buffer;
281 });
282
283 // Parity helper for alternating-row table styling. Templates use
284 // {{ is_odd(loop.index1) }} to choose between "odd" and "even"
285 // class names, matching the legacy HtmlGenerator's tr.odd / tr.even
286 // contract. Implemented as a callback rather than relying on Inja's
287 // expression grammar so the parity check is unambiguous regardless
288 // of which arithmetic operators Inja supports in any given release.
289 env.add_callback("is_odd", 1, [](inja::Arguments &args) {
290 return args.at(0)->get<qsizetype>() % 2 != 0;
291 });
292}
293
294/*!
295 \class InjaBridge
296 \brief Adapter for converting Qt JSON types to Inja template engine format.
297
298 InjaBridge provides static methods to convert between Qt's native JSON types
299 (QJsonObject, QJsonArray, QJsonValue) and nlohmann::json, which is the data
300 format expected by the Inja template engine.
301
302 This adapter allows QDoc to maintain its Qt-native API while leveraging
303 Inja for template-based documentation generation. All JSON data in QDoc's
304 intermediate representation (IR) uses Qt types, and InjaBridge handles the
305 conversion when rendering templates.
306
307 \note All numbers in QJsonValue are stored as doubles. Whole-number doubles
308 (e.g., 30.0, 2.0) are converted to int64_t so that template output renders
309 them as integers (e.g., "30" not "30.0"). Fractional values pass through
310 as doubles.
311
312 \note Inja and nlohmann::json may report template or data errors, such as
313 a user template accessing an IR field that a given page does not carry.
314 With exceptions, the render methods return a RenderFailure so the caller
315 can report the error and continue with later pages. Without exceptions,
316 both libraries' error-reporting entry points terminate the run with the
317 same contextual diagnostic.
318
319 All render methods register template callbacks:
320 \list
321 \li \c{escape_html()} escapes HTML special characters (\c{&}, \c{<},
322 \c{>}, \c{"}, \c{'}).
323 \li \c{escape_md_table()} escapes pipe characters and collapses newlines
324 for safe use inside Markdown table cells.
325 \li \c{render_signature_spans()} converts a JSON array of signature spans
326 (from SignatureSpan IR) into semantic HTML with role-based markup
327 (\c{<span class="type">}, \c{<span class="name">}, etc.).
328 \endlist
329 Escaping callbacks keep format-specific logic under template author control.
330 The \c{render_signature_spans()} callback centralizes signature rendering
331 that was previously duplicated across multiple templates.
332
333 \sa QJsonObject, QJsonArray, QJsonValue
334*/
335
336/*!
337 \struct InjaBridge::RenderContext
338 \brief Identifies the render a template error belongs to.
339
340 A \c RenderContext carries the \c format, \c page, and
341 \c templatePath of one render call.
342*/
343
344/*!
345 \variable InjaBridge::RenderContext::format
346 \brief The output format being rendered.
347*/
348
349/*!
350 \variable InjaBridge::RenderContext::page
351 \brief The output file name of the page being rendered.
352*/
353
354/*!
355 \variable InjaBridge::RenderContext::templatePath
356 \brief The location the template was loaded from.
357
358 This is the absolute path of an override in the configured template
359 directory or the Qt resource path of a built-in template.
360*/
361
362/*!
363 \struct InjaBridge::RenderFailure
364 \brief Carries the error reported by the template engine.
365*/
366
367/*!
368 \variable InjaBridge::RenderFailure::message
369 \brief The error message reported by the template engine.
370*/
371
372/*!
373 \typedef InjaBridge::RenderResult
374 \brief A rendered string or a template render failure.
375
376 An empty string is a successful result.
377*/
378
379/*!
380 \brief Converts a QJsonValue, \a value, to nlohmann::json.
381
382 Handles all QJsonValue types: Null, Bool, Double, String, Array, Object,
383 and Undefined. Undefined values are treated as null.
384
385 Returns the equivalent nlohmann::json representation.
386*/
387nlohmann::json InjaBridge::toInjaJson(const QJsonValue &value)
388{
389 switch (value.type()) {
390 case QJsonValue::Null:
391 return nullptr;
392 case QJsonValue::Bool:
393 return value.toBool();
394 case QJsonValue::Double: {
395 double d = value.toDouble();
396 if (std::fmod(d, 1.0) == 0.0)
397 return static_cast<int64_t>(d);
398 return d;
399 }
400 case QJsonValue::String:
401 return value.toString().toUtf8().toStdString();
402 case QJsonValue::Array:
403 return toInjaJson(value.toArray());
404 case QJsonValue::Object:
405 return toInjaJson(value.toObject());
406 case QJsonValue::Undefined:
407 return nullptr;
408 }
409 return nullptr;
410}
411
412/*!
413 \brief Converts a QJsonObject, \a obj, to nlohmann::json.
414
415 Recursively converts all values in the object, preserving the key-value
416 structure. Nested objects and arrays are handled correctly.
417
418 Returns the equivalent nlohmann::json object.
419*/
420nlohmann::json InjaBridge::toInjaJson(const QJsonObject &obj)
421{
422 nlohmann::json result = nlohmann::json::object();
423
424 for (const auto &[key, value] : obj.asKeyValueRange())
425 result[key.toString().toUtf8().toStdString()] = toInjaJson(value);
426
427 return result;
428}
429
430/*!
431 \brief Converts a QJsonArray, \a array, to nlohmann::json.
432
433 Recursively converts all elements in the array, preserving order.
434 Mixed-type arrays are supported.
435
436 Returns the equivalent nlohmann::json array.
437*/
438nlohmann::json InjaBridge::toInjaJson(const QJsonArray &array)
439{
440 nlohmann::json result = nlohmann::json::array();
441
442 for (const QJsonValue &value : array)
443 result.push_back(toInjaJson(value));
444
445 return result;
446}
447
448/*!
449 \brief Renders a template string, \a templateStr, with provided \a data.
450
451 Uses Inja to render the template with the given JSON data. The data
452 is automatically converted from QJsonObject to nlohmann::json.
453
454 The Inja template string, \a templateStr, supports Jinja2 syntax. \a data is
455 the JSON data to use for rendering. \a context identifies the render for
456 error reporting; see \l RenderContext.
457
458 Returns the rendered template or a render failure.
459*/
460InjaBridge::RenderResult InjaBridge::render(const QString &templateStr, const QJsonObject &data,
461 const RenderContext &context)
462{
463 auto render = [&] {
464 inja::Environment env;
465 // Replace Inja's default "##" line statement prefix, which conflicts
466 // with Markdown headings. "%!" echoes Jinja2's "%" (statement) and
467 // QDoc's "!" (documentation marker), and is inert in both HTML and
468 // Markdown.
469 env.set_line_statement("%!");
470 env.set_trim_blocks(true);
471 env.set_lstrip_blocks(true);
472 registerCallbacks(env, context.format);
473 nlohmann::json jsonData = toInjaJson(data);
474 std::string templateUtf8 = templateStr.toUtf8().toStdString();
475 return QString::fromUtf8(env.render(templateUtf8, jsonData).c_str());
476 };
477 return renderWithPolicy(context, render);
478}
479
480/*!
481 \brief Renders a template string, \a templateStr, with provided \a data,
482 using \a includeCallback to resolve \c{{% include %}} directives.
483
484 This overload configures the Inja environment with a custom include
485 callback so that templates can use \c{{% include "name" %}} directives.
486 The \a includeCallback receives the include name and returns the partial's
487 content. An empty string is a valid partial; no value means the include
488 wasn't found and reports a render failure.
489
490 This enables Inja's include mechanism to work with Qt's resource system,
491 where \c{std::ifstream} cannot open \c{:/} paths.
492
493 \a context identifies the render for error reporting; see \l RenderContext.
494
495 Returns the rendered template or a render failure.
496*/
497InjaBridge::RenderResult InjaBridge::render(const QString &templateStr, const QJsonObject &data,
498 const IncludeCallback &includeCallback,
499 const RenderContext &context)
500{
501 auto render = [&] {
502 inja::Environment env;
503 env.set_line_statement("%!");
504 env.set_trim_blocks(true);
505 env.set_lstrip_blocks(true);
506 registerCallbacks(env, context.format);
507 env.set_search_included_templates_in_files(false);
508 env.set_include_callback(
509 [&includeCallback, &env](const std::filesystem::path & /*path*/,
510 const std::string &name) -> inja::Template {
511 const auto content = includeCallback(QString::fromStdString(name));
512 if (!content)
513 INJA_THROW(inja::FileError("include not found: '" + name + "'"));
514 return env.parse(content->toUtf8().toStdString());
515 });
516
517 nlohmann::json jsonData = toInjaJson(data);
518 std::string templateUtf8 = templateStr.toUtf8().toStdString();
519 return QString::fromUtf8(env.render(templateUtf8, jsonData).c_str());
520 };
521 return renderWithPolicy(context, render);
522}
523
524/*!
525 \brief Renders a template file with provided data.
526
527 Loads and renders a template from the filesystem, using \a templatePath
528 which holds the absolute path to the template file. The file should use
529 Inja/Jinja2 syntax. \a data is the JSON data to use for rendering.
530
531 \a context identifies the render for error reporting; see \l RenderContext.
532
533 Returns the rendered template or a render failure.
534*/
535InjaBridge::RenderResult InjaBridge::renderFile(const QString &templatePath,
536 const QJsonObject &data,
537 const RenderContext &context)
538{
539 auto render = [&] {
540 inja::Environment env;
541 env.set_line_statement("%!");
542 env.set_trim_blocks(true);
543 env.set_lstrip_blocks(true);
544 registerCallbacks(env, context.format);
545 nlohmann::json jsonData = toInjaJson(data);
546 std::string pathUtf8 = templatePath.toUtf8().toStdString();
547 return QString::fromUtf8(env.render_file(pathUtf8, jsonData).c_str());
548 };
549 return renderWithPolicy(context, render);
550}
551
552QT_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 InjaBridge::RenderResult renderWithPolicy(const InjaBridge::RenderContext &context, const std::function< QString()> &render)
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 QDOC_TEMPLATE_LIBS_THROW
Definition injabridge.h:13