• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pcolby / doxlee / 9219499662

24 May 2024 06:02AM UTC coverage: 74.892% (+0.1%) from 74.783%
9219499662

push

github

pcolby
Reduce the default GitHub Actions workflow permissions

346 of 462 relevant lines covered (74.89%)

6631.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.76
/src/renderer.cpp
1
// SPDX-FileCopyrightText: 2021-2024 Paul Colby <git@colby.id.au>
2
// SPDX-License-Identifier: LGPL-3.0-or-later
3

4
#include "renderer.h"
5

6
#include "doxml.h"
7

8
#if defined USE_CUTELEE
9
#include <cutelee/cachingloaderdecorator.h>
10
#include <cutelee/cutelee_version.h>
11
#include <cutelee/templateloader.h>
12
#elif defined USE_GRANTLEE
13
#include <grantlee/cachingloaderdecorator.h>
14
#include <grantlee/grantlee_version.h>
15
#include <grantlee/templateloader.h>
16
#endif
17

18
#include <QCoreApplication>
19
#include <QDebug>
20
#include <QDirIterator>
21
#include <QLoggingCategory>
22
#include <QRegularExpression>
23
#include <QXmlStreamReader>
24

25
/// Shorten the QStringLiteral macro for readability.
26
#define QSL(str) QStringLiteral(str)
27

28
/// Shorten QCoreApplication::translate calls for readability.
29
#define QTR(str) QCoreApplication::translate("Renderer", str)
30

31
namespace doxlee {
32

33
static Q_LOGGING_CATEGORY(lc, "doxlee.renderer", QtInfoMsg);
1,764✔
34

35
Renderer::Renderer(const QString &inputDir) : inputDir(inputDir)
72✔
36
{
37
    // Default context values.
38
    context.insert(QSL("doxleeVersion"), QStringLiteral(PROJECT_VERSION));
126✔
39
    #if defined USE_CUTELEE
40
    context.insert(QSL("templateLibraryName"), QStringLiteral("Cutelee"));
108✔
41
    context.insert(QSL("templateLibraryVersion"), QStringLiteral(CUTELEE_VERSION_STRING));
108✔
42
    #elif defined USE_GRANTLEE
43
    context.insert(QSL("templateLibraryName"), QStringLiteral("Grantlee"));
18✔
44
    context.insert(QSL("templateLibraryVersion"), QStringLiteral(GRANTLEE_VERSION_STRING));
18✔
45
    #endif
46

47
    // Configure the template rendering engine.
48
    engine.setSmartTrimEnabled(true);
72✔
49

50
    // Parse the XML index
51
    /// \todo the caller should be able to detect failure here.
52
    const QVariantMap map = doxml::parseIndex(this->inputDir);
72✔
53
    #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) // QMap::asKeyValueRange() added in Qt 6.4.
54
    for (const auto& [key, value] : map.asKeyValueRange()) {
216✔
55
        context.insert(key, value);
189✔
56
    }
57
    #else
58
    std::for_each(map.constKeyValueBegin(), map.constKeyValueEnd(), [this](const auto &kv){
90✔
59
        context.insert(kv.first, kv.second);
315✔
60
    });
61
    #endif
62
}
72✔
63

64
bool Renderer::loadTemplates(const QString &templatesDir)
72✔
65
{
66
    // Fetch the list of compound and member kinds supported by the Doxgen version.
67
    const auto [compoundKinds, memberKinds] = doxml::kinds(inputDir);
72✔
68
    if ((compoundKinds.isEmpty()) && (memberKinds.isEmpty())) {
72✔
69
        return false; // doxml::kinds failed; and reported an appropriate error.
70
    }
71

72
    // Setup the template loader.
73
    #if defined USE_CUTELEE
74
    auto loader = std::make_shared<Textlee::FileSystemTemplateLoader>();
75
    auto cachedLoader = std::make_shared<Textlee::CachingLoaderDecorator>(loader);
76
    #elif defined USE_GRANTLEE
77
    auto loader = QSharedPointer<Textlee::FileSystemTemplateLoader>::create();
18✔
78
    auto cachedLoader = QSharedPointer<Textlee::CachingLoaderDecorator>::create(loader);
18✔
79
    #endif
80
    // Note, {% include "<filename>" %} will look for files relative to templateDirs.
81
    loader->setTemplateDirs(QStringList() << templatesDir);
72✔
82
    engine.addTemplateLoader(cachedLoader);
162✔
83

84
    // Load the templates.
85
    QDirIterator dir(templatesDir, QDir::Files|QDir::Readable, QDirIterator::Subdirectories);
72✔
86
    int otherFilesCount=0;
87
    while (dir.hasNext()) {
312✔
88
        // Fetch the next entry.
89
        const QString relativePathName = dir.next().mid(dir.path().size()+1);
480✔
90
        qCDebug(lc).noquote() << QTR("Inspecting template: %1 (%2)")
300✔
91
            .arg(dir.filePath(), relativePathName);
×
92

93
        // Check for 'static' directory names in the local path.
94
        if (relativePathName.split(QLatin1Char('/')).contains(QSL("static"))) {
480✔
95
            staticFileNames.append({dir.filePath(), relativePathName});
×
96
            continue;
×
97
        }
98

99
        // Extract the 'kind' string, if any (everything up to the first non-alphanemeric char).
100
        QString kind = getKindFromFileName(dir.fileName());
240✔
101
        qCDebug(lc).noquote() << QTR("Inspecting template: %1 (%2,%3)")
300✔
102
            .arg(dir.filePath(), relativePathName, kind);
×
103

104
        // If the 'kind' is static, just record it for copying to the output later.
105
        if (kind == QSL("static")) {
240✔
106
            staticFileNames.append({dir.filePath(), relativePathName});
×
107
            continue;
×
108
        }
109

110
        // Load the template, and store against the relevant compound kind.
111
        if ((kind == QSL("index")) || (compoundKinds.contains(kind))) {
606✔
112
            qCDebug(lc).noquote() << QTR("Loading template: %1 (%2,%3)")
180✔
113
                .arg(dir.filePath(), relativePathName, kind);
×
114
            const Textlee::Template tmplate = engine.loadByName(relativePathName);
144✔
115
            if (tmplate->error()) {
144✔
116
                qCWarning(lc).noquote() << QTR("Error loading template: %1 - %2")
×
117
                    .arg(relativePathName, tmplate->errorString());
×
118
                return false;
119
            }
120
            if (kind == QSL("index")) {
144✔
121
                indexTemplateNames.append(relativePathName);
54✔
122
            } else {
123
                templateNamesByKind.insert(kind, relativePathName);
72✔
124
            }
125
            continue;
126
        }
127
        otherFilesCount++;
96✔
128
    }
60✔
129
    qCInfo(lc).noquote() << QTR("Loaded %1 template(s), alongside %2 static file(s)")
180✔
130
        .arg(indexTemplateNames.size() + templateNamesByKind.size() + otherFilesCount)
144✔
131
        .arg(staticFileNames.size());
126✔
132
    return true;
18✔
133
}
72✔
134

135
// Estimate number of files that will be generated.
136
// Also report if any compounds have no templates.
137
int Renderer::expectedFileCount() const
72✔
138
{
139
    int count = indexTemplateNames.count() + staticFileNames.count();
72✔
140
    const QVariantMap compoundsByKind = context.lookup(QSL("compoundsByKind")).toMap();
144✔
141
    for (auto iter = compoundsByKind.constBegin(); iter != compoundsByKind.constEnd(); ++iter) {
528✔
142
        const int templatesCount = templateNamesByKind.count(iter.key());
456✔
143
        const int itemsCounts = iter.value().toList().size();
456✔
144
        if (templatesCount == 0) {
456✔
145
            qCWarning(lc).noquote() << QTR("Found documentation for %1 %2 compound(s), "
960✔
146
                                        "but no specialised templates for %2 compounds")
147
                                        .arg(itemsCounts).arg(iter.key());
672✔
148
        }
149
        count += templatesCount * itemsCounts;
456✔
150
    }
151
    return count;
72✔
152
}
18✔
153

154
bool Renderer::render(const QDir &outputDir, ClobberMode clobberMode)
72✔
155
{
156
    // Render all compounds (and members) we have templates for.
157
    const QVariantMap compoundsByKind = context.lookup(QSL("compoundsByKind")).toMap();
144✔
158
    for (auto iter = compoundsByKind.constBegin(); iter != compoundsByKind.constEnd(); ++iter) {
528✔
159
        const QStringList templateNames = templateNamesByKind.values(iter.key());
570✔
160
        if (templateNames.empty()) continue;
456✔
161
        if (!render(iter.value().toList(), templateNames, outputDir, context, clobberMode))
126✔
162
            return false;
163
    }
164

165
    // Render all index templates.
166
    for (const QString &templateName: std::as_const(indexTemplateNames)) {
144✔
167
        Q_ASSERT(templateName.startsWith(QSL("index")));
168
        const QString outputPath = outputDir.absoluteFilePath(
169
            (templateName.lastIndexOf(QLatin1Char('.')) == 5) ? templateName : templateName.mid(6));
144✔
170
        if (!render(templateName, outputPath, context, clobberMode))
72✔
171
            return false;
172
    }
18✔
173

174
    // Copy all static files.
175
    for (auto iter = staticFileNames.begin(); iter != staticFileNames.end(); ++iter) {
90✔
176
        const QString destination = outputDir.absoluteFilePath(iter->second);
×
177
        if (!copy(iter->first, destination, clobberMode)) {
×
178
            return false;
179
        }
180
    }
181
    return true;
182
}
18✔
183

184
int Renderer::outputFileCount() const
72✔
185
{
186
    return filesWritten.size();
72✔
187
}
188

189
QString Renderer::compoundPathName(const QVariantMap &compound, const QString &templateName)
608✔
190
{
191
    // Start by breaking the path into the fileName and dirName (if any).
192
    const int pos = templateName.lastIndexOf(QLatin1Char('/'));
608✔
193
    QString fileName = (pos<0) ? templateName : templateName.mid(pos+1);
608✔
194
    QString dirName = (pos<0) ? QString() : templateName.left(pos);
608✔
195

196
    // Strip the "<kind>[char]" prefix from fileName, but remember which separator (if any) was used.
197
    const QString kind = compound.value(QSL("kind")).toString();
1,216✔
198
    Q_ASSERT(fileName.startsWith(kind));
199
    QChar separator;
200
    fileName = fileName.mid(kind.length());
1,064✔
201
    if ((!fileName.isEmpty()) && (fileName.at(0) != QLatin1Char('.'))) {
608✔
202
        separator = fileName.at(0);
203
        fileName.remove(0,1);
184✔
204
    }
205
    if ((fileName.isEmpty()) || (fileName.at(0) == QLatin1Char('.'))) {
608✔
206
        fileName.prepend(QSL("refid"));
440✔
207
    }
208

209
    // Replace the supported compound tokens.
210
    const QStringList tokens { QSL("kind"), QSL("name"), QSL("refid") };
2,888✔
211
    for (const QString &token: tokens) {
2,432✔
212
        const QRegularExpression before(QSL("(^|\\W|_)%1(\\W|_|$)").arg(token));
3,648✔
213
        const QString after = QSL("\\1%1\\2").arg(compound.value(token).toString());
3,648✔
214
        fileName.replace(before, after);
1,824✔
215
        dirName.replace(before, after);
1,824✔
216
    }
1,824✔
217
    if (!separator.isNull()) {
608✔
218
        fileName.remove(separator);
184✔
219
        dirName.remove(separator);
184✔
220
    }
221
    if (!dirName.isEmpty()) dirName.append(QLatin1Char('/'));
608✔
222
    return dirName + fileName;
1,216✔
223
}
152✔
224

225
QString Renderer::getKindFromFileName(const QString &fileName)
312✔
226
{
227
    QString kind = fileName.split(QLatin1Char('/')).last();
624✔
228
    const QString::ConstIterator pos = std::find_if_not(kind.constBegin(), kind.constEnd(),
229
        [](const QChar &c){ return c.isLetterOrNumber();});
230
    if (pos != kind.constEnd()) {
312✔
231
        kind.truncate(pos - kind.constBegin());
288✔
232
    }
233
    return kind;
312✔
234
}
235

236
bool Renderer::promptToOverwrite(const QString &pathName, ClobberMode &clobberMode)
×
237
{
238
    Q_ASSERT(clobberMode == Prompt);
239
    while (true) {
240
        qCWarning(lc).noquote() << QTR("Overwrite %1 [y,n,a,s,q,?]? ").arg(pathName);
×
241
        QTextStream stream(stdin);
×
242
        const QString response = stream.readLine();
×
243
        if (response == QSL("y")) {
×
244
            return true;
245
        } else if (response == QSL("n")) {
×
246
            return false;
247
        } else if (response == QSL("a")) {
×
248
            clobberMode = Overwrite;
×
249
            return true;
×
250
        } else if (response == QSL("s")) {
×
251
            clobberMode = Skip;
×
252
            return false;
×
253
        } else if (response == QSL("q")) {
×
254
            exit(255);
×
255
        } else {
256
            qCInfo(lc).noquote() << QTR("y - overwrite this file");
×
257
            qCInfo(lc).noquote() << QTR("n - do not overwrite this file");
×
258
            qCInfo(lc).noquote() << QTR("a - overwrite this, and all remaining files");
×
259
            qCInfo(lc).noquote() << QTR("s - do not overwrite this, or any remaining files");
×
260
            qCInfo(lc).noquote() << QTR("q - quit now, without writing any further files");
×
261
            qCInfo(lc).noquote() << QTR("? - print help");
×
262
        }
263
   }
×
264
}
265

266
bool Renderer::copy(const QString &fromPath, const QString &toPath, ClobberMode &clobberMode)
×
267
{
268
    qCDebug(lc) << __func__ << fromPath << toPath << clobberMode;
×
269

270
    QFileInfo toFileInfo(toPath);
×
271
    if (toFileInfo.exists()) {
×
272
        switch (clobberMode) {
×
273
        case Prompt:
×
274
            if (!promptToOverwrite(toPath, clobberMode)) {
×
275
                qCDebug(lc).noquote() << QTR("Skipping existing output file: %1").arg(toPath);
×
276
                return true;
277
            }
278
            __attribute__((fallthrough)); // Fall-through to Overwrite behaviour.
279
        case Overwrite:
280
            if (!QFile::remove(toPath)) {
×
281
                qCWarning(lc).noquote() << QTR("Failed to copy over existing file: %1").arg(toPath);
×
282
            }
283
            break;
284
        case Skip:
×
285
            qCDebug(lc).noquote() << QTR("Skipping existing output file: %1").arg(toPath);
×
286
            return true;
287
        }
288
    }
289

290
    if (!toFileInfo.dir().exists()) {
×
291
        toFileInfo.dir().mkpath(QSL("./"));
×
292
    }
293

294
    if (!QFile::copy(fromPath, toPath)) {
×
295
        qCWarning(lc).noquote() << QTR("Failed to copy %1 to %2").arg(fromPath, toPath);
×
296
        return false;
297
    }
298
    filesWritten.append(toPath);
299
    return true;
300
}
×
301

302
bool Renderer::render(const QVariantList &compounds, const QStringList &templateNames,
72✔
303
                      const QDir &outputDir, Textlee::Context &context, ClobberMode &clobberMode)
304
{
305
    // Note, we're effectively doing a product of compounds * templates here, which could be quite a lot of processing.
306
    // We choose to iterate compounds in the outer loop, so we only parse each compound once. Whereas repeatedly loading
307
    // templates in the inner loop is fairly cheap, since we allocated a caching template loader earlier. We could of
308
    // course, invert the loops for the same ouput, just an order of magnitude slower and/or using more RAM to cache
309
    // parsed Doxml.
310
    for (const QVariant &compound: compounds) {
456✔
311
        // Parse the item's Doxygen XML data.
312
        const QString refId = compound.toMap().value(QSL("refid")).toString();
768✔
313
        const QVariantMap compoundDefinition = doxml::parseCompound(inputDir, refId);
384✔
314

315
        // Render the output for each template.
316
        context.push();
384✔
317
        context.insert(QSL("compound"), compoundDefinition);
672✔
318
        for (const QString &templateName: templateNames) {
768✔
319
            const QString outputPath = outputDir.absoluteFilePath(
320
                compoundPathName(compound.toMap(), templateName));
768✔
321
            if (!render(templateName, outputPath, context, clobberMode)) {
384✔
322
                context.pop();
×
323
                return false;
324
            }
325
        }
96✔
326
        context.pop();
384✔
327
    }
96✔
328
    return true;
329
}
330

331
bool Renderer::render(const QString &templateName, const QString &outputPath,
456✔
332
                      Textlee::Context &context, ClobberMode &clobberMode)
333
{
334
    qCDebug(lc) << __func__ << templateName << outputPath << clobberMode;
570✔
335

336
    QFileInfo toFileInfo(outputPath);
456✔
337
    if (toFileInfo.exists()) {
456✔
338
        switch (clobberMode) {
×
339
        case Overwrite:
340
            // QFile::open below will happily overwrite (if we have write permission).
341
            break;
342
        case Prompt:
×
343
            if (promptToOverwrite(outputPath, clobberMode))
×
344
                break; // QFile::open below will happily overwrite (if we have write permission).
345
            __attribute__((fallthrough)); // else fall-through to Skip behaviour.
346
        case Skip:
347
            qCDebug(lc).noquote() << QTR("Skipping existing output file: %1").arg(outputPath);
×
348
            return true;
349
        }
350
    }
351

352
    const Textlee::Template tmplate = engine.loadByName(templateName);
456✔
353
    if (tmplate->error()) {
456✔
354
        qCWarning(lc).noquote() << QTR("Error loading template: %1 - %2")
×
355
            .arg(templateName, tmplate->errorString());
×
356
        return false;
357
    }
358

359
    if (!toFileInfo.dir().exists()) {
456✔
360
        toFileInfo.dir().mkpath(QSL("./"));
×
361
    }
362

363
    QFile file(outputPath);
912✔
364
    if (!file.open(QFile::WriteOnly)) {
456✔
365
        qCWarning(lc).noquote() << QTR("Failed to open file for writing: %1").arg(outputPath);
×
366
        return false;
367
    }
368

369
    QTextStream textStream(&file);
912✔
370
    //NoEscapeStream noEscapeStream(&textStream); ///< \todo Do we need this?
371
    Textlee::OutputStream outputStream(&textStream);
912✔
372
    tmplate->render(&outputStream, &context);
456✔
373
    if (tmplate->error()) {
456✔
374
        qCWarning(lc).noquote() << QTR("Failed to render: %1 - %2").arg(outputPath, tmplate->errorString());
×
375
        return false;
376
    }
377
    filesWritten.append(outputPath);
342✔
378
    return true;
379
}
456✔
380

381
/// Text output stream that does *no* content escaping.
382
//class NoEscapeStream : public Textlee::OutputStream {
383
//public:
384
//    explicit NoEscapeStream(QTextStream * stream) : Textlee::OutputStream(stream) { }
385

386
//    virtual QString escape(const QString &input) const { return input; }
387

388
//    virtual QSharedPointer<OutputStream> clone( QTextStream *stream ) const {
389
//        return QSharedPointer<OutputStream>(new NoEscapeStream(stream));
390
//    }
391
//};
392

393
} // namespace doxlee
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc