Smithy Qt 0.1.0-pre
Internal development documentation
Loading...
Searching...
No Matches
renderer.cpp
1// SPDX-FileCopyrightText: 2013-2025 Paul Colby <git@colby.id.au>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include "renderer.h"
5
6#include <QDebug>
7#include <QDirIterator>
8#include <QJsonParseError>
9#include <QRegularExpression>
10
11#if defined USE_CUTELEE
12#include <cutelee/cachingloaderdecorator.h>
13#include <cutelee/templateloader.h>
14#elif defined USE_GRANTLEE
15#include <grantlee/cachingloaderdecorator.h>
16#include <grantlee/templateloader.h>
17#elif defined USE_KTEXTTEMPLATE
18#include <KTextTemplate/CachingLoaderDecorator>
19#include <KTextTemplate/TemplateLoader>
20#endif
21
22#define QSL(str) QStringLiteral(str) // Shorten the QStringLiteral macro for readability.
23
24Renderer::Renderer()
25{
26 engine.setSmartTrimEnabled(true);
27}
28
29bool Renderer::loadTemplates(const QString &dir)
30{
31 qCInfo(lc).noquote() << tr("Loading Textlee templates from %1").arg(dir);
32
33 // Setup the template loader.
34 #if defined USE_CUTELEE
35 auto loader = std::make_shared<Textlee::FileSystemTemplateLoader>();
36 auto cachedLoader = std::make_shared<Textlee::CachingLoaderDecorator>(loader);
37 #elif defined USE_GRANTLEE or defined USE_KTEXTTEMPLATE
38 auto loader = QSharedPointer<Textlee::FileSystemTemplateLoader>::create();
39 auto cachedLoader = QSharedPointer<Textlee::CachingLoaderDecorator>::create(loader);
40 #endif
41 // Note, {% include "<filename>" %} will look for files relative to templateDirs.
42 loader->setTemplateDirs(QStringList{dir});
43 engine.addTemplateLoader(cachedLoader);
44
45 // Load the templates.
46 QDirIterator iter{QDir::cleanPath(dir), QDir::Files, QDirIterator::Subdirectories};
47 while (iter.hasNext()) {
48 const QString name = iter.next().mid(iter.path().size()+1);
49 qCDebug(lc).noquote() << tr("Loading template: %1").arg(name);
50 const Textlee::Template tmplate = engine.loadByName(name);
51 if (tmplate->error()) {
52 qCritical().noquote() << tr("Error loading template %1: %2")
53 .arg(name, tmplate->errorString());
54 return false;
55 }
56 templates.append(name);
57 }
58 qDebug(lc).noquote() << tr("Loaded %n template/s from %1", nullptr, templates.size()).arg(dir);
59 return true;
60}
61
62QStringList Renderer::templatesNames() const
63{
64 return templates;
65}
66
67// Textlee output stream that does *no* content escaping.
68class NoEscapeStream : public Textlee::OutputStream {
69public:
70 explicit NoEscapeStream(QTextStream * stream) : Textlee::OutputStream(stream) { }
71
72 // cppcheck-suppress unusedFunction
73 virtual QString escape(const QString &input) const { return input; }
74
75 // cppcheck-suppress unusedFunction
76 #if defined USE_CUTELEE
77 virtual std::shared_ptr<OutputStream> clone(QTextStream *stream) const {
78 return std::shared_ptr<OutputStream>(new NoEscapeStream(stream));
79 }
80 #elif defined USE_GRANTLEE
81 virtual QSharedPointer<OutputStream> clone(QTextStream *stream) const {
82 return QSharedPointer<OutputStream>(new NoEscapeStream(stream));
83 }
84 #endif
85};
86
87void Renderer::push(const QVariantHash &context)
88{
89 this->context.push();
90 for (auto iter = context.constBegin(); iter != context.constEnd(); ++iter) {
91 this->context.insert(sanitise(iter.key()), sanitise(iter.value()));
92 }
93}
94
95void Renderer::pop()
96{
97 context.pop();
98}
99
100bool Renderer::render(const QString &templateName, const QString &outputPathName,
101 const QVariantHash &additionalContext)
102{
103 qCDebug(lc).noquote() << tr("Rendering %1 to %2").arg(templateName, outputPathName);
104 if (!templates.contains(templateName)) {
105 qCCritical(lc).noquote() << tr("Template %1 has not been loaded").arg(templateName);
106 return false;
107 }
108
109 const QDir dir = QFileInfo(outputPathName).dir();
110 if (!dir.mkpath(QSL("./"))) {
111 qCCritical(lc).noquote() << tr("Failed to create directory path %1").arg(dir.path());
112 return false;
113 }
114
115 QFile file(outputPathName);
116 if (!file.open(QFile::WriteOnly)) {
117 qCCritical(lc).noquote() << tr("Failed to open %1 for writing: %2")
118 .arg(outputPathName, file.errorString());
119 return false;
120 }
121
122 push(additionalContext);
123 QTextStream textStream(&file);
124 NoEscapeStream noEscapeStream(&textStream);
125 Textlee::Template tmplate = engine.loadByName(templateName);
126 if (!tmplate) {
127 qCCritical(lc).noquote() << tr("Failed to fetch template %1").arg(outputPathName);
128 pop();
129 return false;
130 }
131 tmplate->render(&noEscapeStream, &context);
132 if (tmplate->error()) {
133 qCCritical(lc).noquote() << tr("Failed to render %1: %2").arg(outputPathName, tmplate->errorString());
134 pop();
135 return false;
136 }
137 pop();
138 return true;
139}
140
141QString Renderer::sanitise(const QString &key)
142{
143 QString newKey = key;
144 int pos;
145 while ((pos = newKey.indexOf(QLatin1Char('.'))) >= 0) {
146 newKey = newKey.mid(0, pos) + newKey.mid(pos+1,1).toUpper() + newKey.mid(pos+2);
147 }
148 newKey.replace(QRegularExpression{QSL("[^a-zA-Z0-9_]")}, QSL("_"));
149 newKey.replace(QRegularExpression{QSL("^[0-9_]")}, QLatin1String());
150 return newKey;
151}
152
153QVariant Renderer::sanitise(const QVariant &variant)
154{
155 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
156 const int typeId = variant.typeId();
157 #else
158 const int typeId = variant.type();
159 #endif
160 if (typeId == QMetaType::QVariantHash) {
161 return sanitise(variant.toHash());
162 } else if (typeId == QMetaType::QVariantMap) {
163 return sanitise(variant.toMap());
164 }
165 return variant;
166}
167
168QVariantHash Renderer::sanitise(const QVariantHash &hash)
169{
170 QVariantHash newHash;
171 for (auto iter = hash.begin(); iter != hash.end(); ++iter) {
172 const QString saneKey = sanitise(iter.key());
173 QVariant saneValue = iter.value();
174 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
175 const int typeId = iter->typeId();
176 #else
177 const int typeId = iter->type();
178 #endif
179 if (typeId == QMetaType::QVariantHash) {
180 saneValue = sanitise(iter->toHash());
181 } else if (typeId == QMetaType::QVariantMap) {
182 saneValue = sanitise(iter->toMap());
183 }
184 newHash.insert(saneKey, saneValue);
185 }
186 Q_ASSERT(hash.size() == newHash.size());
187 return newHash;
188}
189
190QVariantMap Renderer::sanitise(const QVariantMap &map)
191{
192 QVariantMap newMap;
193 for (auto iter = map.begin(); iter != map.end(); ++iter) {
194 const QString saneKey = sanitise(iter.key());
195 QVariant saneValue = iter.value();
196 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
197 const int typeId = iter->typeId();
198 #else
199 const int typeId = iter->type();
200 #endif
201 if (typeId == QMetaType::QVariantHash) {
202 saneValue = sanitise(iter->toHash());
203 } else if (typeId == QMetaType::QVariantMap) {
204 saneValue = sanitise(iter->toMap());
205 }
206 newMap.insert(saneKey, saneValue);
207 }
208 Q_ASSERT(map.size() == newMap.size());
209 return newMap;
210}