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

pcolby / doxlee / 9041885859

11 May 2024 06:52AM UTC coverage: 74.501% (+0.2%) from 74.286%
9041885859

push

github

pcolby
Apply categories to all log output

11 of 46 new or added lines in 3 files covered. (23.91%)

6 existing lines in 3 files now uncovered.

336 of 451 relevant lines covered (74.5%)

1188.85 hits per line

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

67.4
/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);
528✔
34

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

47
    // Configure the template rendering engine.
48
    engine.setSmartTrimEnabled(true);
24✔
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);
24✔
53
    for (auto iter = map.constBegin(); iter != map.constEnd(); ++iter)
192✔
54
        context.insert(iter.key(), iter.value());
168✔
55
}
24✔
56

57
bool Renderer::loadTemplates(const QString &templatesDir)
24✔
58
{
59
    // Fetch the list of compound and member kinds supported by the Doxgen version.
60
    auto kinds = doxml::kinds(inputDir);
24✔
61
    if ((kinds.first.isEmpty()) && (kinds.second.isEmpty())) {
24✔
62
        return false; // doxml::kinds failed; and reported an appropriate error.
63
    }
64

65
    // Setup the template loader.
66
    #if defined USE_CUTELEE
67
    auto loader = std::make_shared<Textlee::FileSystemTemplateLoader>();
68
    auto cachedLoader = std::make_shared<Textlee::CachingLoaderDecorator>(loader);
69
    #elif defined USE_GRANTLEE
70
    auto loader = QSharedPointer<Textlee::FileSystemTemplateLoader>::create();
6✔
71
    auto cachedLoader = QSharedPointer<Textlee::CachingLoaderDecorator>::create(loader);
6✔
72
    #endif
73
    // Note, {% include "<filename>" %} will look for files relative to templateDirs.
74
    loader->setTemplateDirs(QStringList() << templatesDir);
24✔
75
    engine.addTemplateLoader(cachedLoader);
54✔
76

77
    // Load the templates.
78
    QDirIterator dir(templatesDir, QDir::Files|QDir::Readable, QDirIterator::Subdirectories);
24✔
79
    int otherFilesCount=0;
80
    while (dir.hasNext()) {
144✔
81
        // Fetch the next entry.
82
        const QString relativePathName = dir.next().mid(dir.path().size()+1);
240✔
83
        qCDebug(lc).noquote() << QTR("Inspecting template: %1 (%2)")
150✔
UNCOV
84
            .arg(dir.filePath(), relativePathName);
×
85

86
        // Check for 'static' directory names in the local path.
87
        if (relativePathName.split(QLatin1Char('/')).contains(QSL("static"))) {
240✔
88
            staticFileNames.append({dir.filePath(), relativePathName});
×
89
            continue;
×
90
        }
91

92
        // Extract the 'kind' string, if any (everything up to the first non-alphanemeric char).
93
        QString kind = getKindFromFileName(dir.fileName());
120✔
94
        qCDebug(lc).noquote() << QTR("Inspecting template: %1 (%2,%3)")
150✔
UNCOV
95
            .arg(dir.filePath(), relativePathName, kind);
×
96

97
        // If the 'kind' is static, just record it for copying to the output later.
98
        if (kind == QSL("static")) {
120✔
99
            staticFileNames.append({dir.filePath(), relativePathName});
×
100
            continue;
×
101
        }
102

103
        // Load the template, and store against the relevant compound kind.
104
        if ((kind == QSL("index")) || (kinds.first.contains(kind))) {
312✔
105
            qCDebug(lc).noquote() << QTR("Loading template: %1 (%2,%3)")
60✔
UNCOV
106
                .arg(dir.filePath(), relativePathName, kind);
×
107
            const Textlee::Template tmplate = engine.loadByName(relativePathName);
48✔
108
            if (tmplate->error()) {
48✔
NEW
109
                qCWarning(lc).noquote() << QTR("Error loading template: %1 - %2")
×
110
                    .arg(relativePathName, tmplate->errorString());
×
111
                return false;
112
            }
113
            if (kind == QSL("index")) {
48✔
114
                indexTemplateNames.append(relativePathName);
18✔
115
            } else {
116
                templateNamesByKind.insert(kind, relativePathName);
24✔
117
            }
118
            continue;
119
        }
120
        otherFilesCount++;
72✔
121
    }
30✔
122
    qCInfo(lc).noquote() << QTR("Loaded %1 template(s), alongside %2 static file(s)")
60✔
123
        .arg(indexTemplateNames.size() + templateNamesByKind.size() + otherFilesCount)
48✔
124
        .arg(staticFileNames.size());
42✔
125
    return true;
6✔
126
}
24✔
127

128
// Estimate number of files that will be generated.
129
// Also report if any compounds have no templates.
130
int Renderer::expectedFileCount() const
24✔
131
{
132
    int count = indexTemplateNames.count() + staticFileNames.count();
24✔
133
    const QVariantMap compoundsByKind = context.lookup(QSL("compoundsByKind")).toMap();
48✔
134
    for (auto iter = compoundsByKind.constBegin(); iter != compoundsByKind.constEnd(); ++iter) {
120✔
135
        const int templatesCount = templateNamesByKind.count(iter.key());
96✔
136
        const int itemsCounts = iter.value().toList().size();
96✔
137
        if (templatesCount == 0) {
96✔
138
            qCWarning(lc).noquote() << QTR("Found documentation for %1 %2 compound(s), "
180✔
139
                                        "but no specialised templates for %2 compounds")
140
                                        .arg(itemsCounts).arg(iter.key());
126✔
141
        }
142
        count += templatesCount * itemsCounts;
96✔
143
    }
144
    return count;
24✔
145
}
6✔
146

147
bool Renderer::render(const QDir &outputDir, ClobberMode clobberMode)
24✔
148
{
149
    // Render all compounds (and members) we have templates for.
150
    const QVariantMap compoundsByKind = context.lookup(QSL("compoundsByKind")).toMap();
48✔
151
    for (auto iter = compoundsByKind.constBegin(); iter != compoundsByKind.constEnd(); ++iter) {
120✔
152
        const QStringList templateNames = templateNamesByKind.values(iter.key());
120✔
153
        if (templateNames.empty()) continue;
96✔
154
        if (!render(iter.value().toList(), templateNames, outputDir, context, clobberMode))
42✔
155
            return false;
156
    }
157

158
    // Render all index templates.
159
    for (const QString &templateName: std::as_const(indexTemplateNames)) {
48✔
160
        Q_ASSERT(templateName.startsWith(QSL("index")));
161
        const QString outputPath = outputDir.absoluteFilePath(
162
            (templateName.lastIndexOf(QLatin1Char('.')) == 5) ? templateName : templateName.mid(6));
40✔
163
        if (!render(templateName, outputPath, context, clobberMode))
24✔
164
            return false;
165
    }
6✔
166

167
    // Copy all static files.
168
    for (auto iter = staticFileNames.begin(); iter != staticFileNames.end(); ++iter) {
30✔
169
        const QString destination = outputDir.absoluteFilePath(iter->second);
×
170
        if (!copy(iter->first, destination, clobberMode)) {
×
171
            return false;
172
        }
173
    }
174
    return true;
175
}
6✔
176

177
int Renderer::outputFileCount() const
24✔
178
{
179
    return filesWritten.size();
24✔
180
}
181

182
QString Renderer::compoundPathName(const QVariantMap &compound, const QString &templateName)
296✔
183
{
184
    // Start by breaking the path into the fileName and dirName (if any).
185
    const int pos = templateName.lastIndexOf(QLatin1Char('/'));
296✔
186
    QString fileName = (pos<0) ? templateName : templateName.mid(pos+1);
296✔
187
    QString dirName = (pos<0) ? QString() : templateName.left(pos);
296✔
188

189
    // Strip the "<kind>[char]" prefix from fileName, but remember which separator (if any) was used.
190
    const QString kind = compound.value(QSL("kind")).toString();
592✔
191
    Q_ASSERT(fileName.startsWith(kind));
192
    QChar separator;
193
    fileName = fileName.mid(kind.length());
518✔
194
    if ((!fileName.isEmpty()) && (fileName.at(0) != QLatin1Char('.'))) {
296✔
195
        separator = fileName.at(0);
196
        fileName.remove(0,1);
184✔
197
    }
198
    if ((fileName.isEmpty()) || (fileName.at(0) == QLatin1Char('.'))) {
296✔
199
        fileName.prepend(QSL("refid"));
128✔
200
    }
201

202
    // Replace the supported compound tokens.
203
    const QStringList tokens { QSL("kind"), QSL("name"), QSL("refid") };
1,406✔
204
    for (const QString &token: tokens) {
1,184✔
205
        const QRegularExpression before(QSL("(^|\\W|_)%1(\\W|_|$)").arg(token));
1,776✔
206
        const QString after = QSL("\\1%1\\2").arg(compound.value(token).toString());
1,776✔
207
        fileName.replace(before, after);
888✔
208
        dirName.replace(before, after);
888✔
209
    }
888✔
210
    if (!separator.isNull()) {
296✔
211
        fileName.remove(separator);
184✔
212
        dirName.remove(separator);
184✔
213
    }
214
    if (!dirName.isEmpty()) dirName.append(QLatin1Char('/'));
296✔
215
    return dirName + fileName;
592✔
216
}
74✔
217

218
QString Renderer::getKindFromFileName(const QString &fileName)
192✔
219
{
220
    QString kind = fileName.split(QLatin1Char('/')).last();
384✔
221
    const QString::ConstIterator pos = std::find_if_not(kind.constBegin(), kind.constEnd(),
222
        [](const QChar &c){ return c.isLetterOrNumber();});
223
    if (pos != kind.constEnd()) {
192✔
224
        kind.truncate(pos - kind.constBegin());
160✔
225
    }
226
    return kind;
192✔
227
}
228

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

259
bool Renderer::copy(const QString &fromPath, const QString &toPath, ClobberMode &clobberMode)
×
260
{
NEW
261
    qCDebug(lc) << __func__ << fromPath << toPath << clobberMode;
×
262

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

283
    if (!toFileInfo.dir().exists()) {
×
284
        toFileInfo.dir().mkpath(QSL("./"));
×
285
    }
286

287
    if (!QFile::copy(fromPath, toPath)) {
×
NEW
288
        qCWarning(lc) << QTR("Failed to copy %1 to %2").arg(fromPath, toPath);
×
289
        return false;
290
    }
291
    filesWritten.append(toPath);
292
    return true;
293
}
×
294

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

308
        // Render the output for each template.
309
        context.push();
72✔
310
        context.insert(QSL("compound"), compoundDefinition);
126✔
311
        for (const QString &templateName: templateNames) {
144✔
312
            const QString outputPath = outputDir.absoluteFilePath(
313
                compoundPathName(compound.toMap(), templateName));
144✔
314
            if (!render(templateName, outputPath, context, clobberMode)) {
72✔
315
                context.pop();
×
316
                return false;
317
            }
318
        }
18✔
319
        context.pop();
72✔
320
    }
18✔
321
    return true;
322
}
323

324
bool Renderer::render(const QString &templateName, const QString &outputPath,
96✔
325
                      Textlee::Context &context, ClobberMode &clobberMode)
326
{
327
    qCDebug(lc) << __func__ << templateName << outputPath << clobberMode;
120✔
328

329
    QFileInfo toFileInfo(outputPath);
96✔
330
    if (toFileInfo.exists()) {
96✔
331
        switch (clobberMode) {
×
332
        case Overwrite:
333
            // QFile::open below will happily overwrite (if we have write permission).
334
            break;
335
        case Prompt:
×
336
            if (promptToOverwrite(outputPath, clobberMode))
×
337
                break; // QFile::open below will happily overwrite (if we have write permission).
338
            __attribute__((fallthrough)); // else fall-through to Skip behaviour.
339
        case Skip:
NEW
340
            qCDebug(lc) << QTR("Skipping existing output file: %1").arg(outputPath);
×
341
            return true;
342
        }
343
    }
344

345
    const Textlee::Template tmplate = engine.loadByName(templateName);
96✔
346
    if (tmplate->error()) {
96✔
NEW
347
        qCWarning(lc).noquote() << QTR("Error loading template: %1 - %2")
×
348
            .arg(templateName, tmplate->errorString());
×
349
        return false;
350
    }
351

352
    if (!toFileInfo.dir().exists()) {
96✔
353
        toFileInfo.dir().mkpath(QSL("./"));
×
354
    }
355

356
    QFile file(outputPath);
192✔
357
    if (!file.open(QFile::WriteOnly)) {
96✔
NEW
358
        qCWarning(lc).noquote() << QTR("Failed to open file for writing: %1").arg(outputPath);
×
359
        return false;
360
    }
361

362
    QTextStream textStream(&file);
192✔
363
    //NoEscapeStream noEscapeStream(&textStream); ///< \todo Do we need this?
364
    Textlee::OutputStream outputStream(&textStream);
192✔
365
    tmplate->render(&outputStream, &context);
96✔
366
    if (tmplate->error()) {
96✔
NEW
367
        qCWarning(lc) << QTR("Failed to render: %1 - %2").arg(outputPath, tmplate->errorString());
×
368
        return false;
369
    }
370
    filesWritten.append(outputPath);
72✔
371
    return true;
372
}
96✔
373

374
/// Text output stream that does *no* content escaping.
375
//class NoEscapeStream : public Textlee::OutputStream {
376
//public:
377
//    explicit NoEscapeStream(QTextStream * stream) : Textlee::OutputStream(stream) { }
378

379
//    virtual QString escape(const QString &input) const { return input; }
380

381
//    virtual QSharedPointer<OutputStream> clone( QTextStream *stream ) const {
382
//        return QSharedPointer<OutputStream>(new NoEscapeStream(stream));
383
//    }
384
//};
385

386
} // 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