Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Toggle main menu visibility
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
14
QT_BEGIN_NAMESPACE
15
16
using
namespace
Qt::Literals;
17
18
#
if
!
QDOC_TEMPLATE_LIBS_THROW
19
namespace
{
20
21
InjaBridge::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.
26
class
RenderContextScope
27
{
28
Q_DISABLE_COPY_MOVE(RenderContextScope)
29
30
public
:
31
explicit
RenderContextScope(
const
InjaBridge::RenderContext &context)
32
: m_previous(std::exchange(activeRenderContext, context))
33
{
34
}
35
~RenderContextScope() { activeRenderContext = std::move(m_previous); }
36
37
private
:
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
*/
47
QString 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
55
#
if
!
QDOC_TEMPLATE_LIBS_THROW
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
72
static
InjaBridge
::
RenderResult
renderWithPolicy
(
const
InjaBridge::RenderContext &context,
73
const
std::function<QString()> &render)
74
{
75
#
if
QDOC_TEMPLATE_LIBS_THROW
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
88
static
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 +=
"&"
;
96
break
;
97
case
'"'
:
98
buffer +=
"""
;
99
break
;
100
case
'\''
:
101
buffer +=
"'"
;
102
break
;
103
case
'<'
:
104
buffer +=
"<"
;
105
break
;
106
case
'>'
:
107
buffer +=
">"
;
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.
122
static
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.
138
static
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
154
static
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
238
static
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
*/
387
nlohmann::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
*/
420
nlohmann::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
*/
438
nlohmann::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
*/
460
InjaBridge::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
*/
497
InjaBridge::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
*/
535
InjaBridge::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
552
QT_END_NAMESPACE
renderSignatureSpans
static std::string renderSignatureSpans(const nlohmann::json &spans, const QString &format)
Definition
injabridge.cpp:154
escapeHtml
static std::string escapeHtml(const std::string &input)
Definition
injabridge.cpp:88
registerCallbacks
static void registerCallbacks(inja::Environment &env, const QString &format)
Definition
injabridge.cpp:238
renderWithPolicy
static InjaBridge::RenderResult renderWithPolicy(const InjaBridge::RenderContext &context, const std::function< QString()> &render)
Definition
injabridge.cpp:72
renderSpanMarkdown
static std::string renderSpanMarkdown(const nlohmann::json &s)
Definition
injabridge.cpp:138
renderLinkSpan
static std::string renderLinkSpan(const std::string &text, const std::string &href, bool hasHref, bool isMarkdown)
Definition
injabridge.cpp:122
QDOC_TEMPLATE_LIBS_THROW
#define QDOC_TEMPLATE_LIBS_THROW
Definition
injabridge.h:13
std
[33]
Definition
src_corelib_tools_qhash.cpp:421
qttools
src
qdoc
qdoc
src
qdoc
injabridge.cpp
Generated on
for Qt by
1.18.0