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