1 /*
2 SPDX-FileCopyrightText: 2012 Miha Čančula <miha@noughmad.eu>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "templaterenderer.h"
8
9 #include "documentchangeset.h"
10 #include "sourcefiletemplate.h"
11 #include "templateengine.h"
12 #include "templateengine_p.h"
13 #include "archivetemplateloader.h"
14 #include <debug.h>
15
16 #include <serialization/indexedstring.h>
17
18 #include <grantlee/context.h>
19
20 #include <QDir>
21 #include <QFile>
22 #include <QUrl>
23
24 #include <KArchive>
25
26 using namespace Grantlee;
27
28 class NoEscapeStream
29 : public OutputStream
30 {
31 public:
32 NoEscapeStream();
33 explicit NoEscapeStream (QTextStream* stream);
34
35 QString escape (const QString& input) const override;
36 QSharedPointer<OutputStream> clone (QTextStream* stream) const override;
37 };
38
NoEscapeStream()39 NoEscapeStream::NoEscapeStream() : OutputStream()
40 {
41 }
42
NoEscapeStream(QTextStream * stream)43 NoEscapeStream::NoEscapeStream(QTextStream* stream) : OutputStream(stream)
44 {
45 }
46
escape(const QString & input) const47 QString NoEscapeStream::escape(const QString& input) const
48 {
49 return input;
50 }
51
clone(QTextStream * stream) const52 QSharedPointer<OutputStream> NoEscapeStream::clone(QTextStream* stream) const
53 {
54 QSharedPointer<OutputStream> clonedStream = QSharedPointer<OutputStream>(new NoEscapeStream(stream));
55 return clonedStream;
56 }
57
58 using namespace KDevelop;
59
60 namespace KDevelop {
61 class TemplateRendererPrivate
62 {
63 public:
64 Engine* engine;
65 Grantlee::Context context;
66 TemplateRenderer::EmptyLinesPolicy emptyLinesPolicy;
67 QString errorString;
68 };
69 }
70
TemplateRenderer()71 TemplateRenderer::TemplateRenderer()
72 : d_ptr(new TemplateRendererPrivate)
73 {
74 Q_D(TemplateRenderer);
75
76 d->engine = &TemplateEngine::self()->d_ptr->engine;
77 d->emptyLinesPolicy = KeepEmptyLines;
78 }
79
80 TemplateRenderer::~TemplateRenderer() = default;
81
addVariables(const QVariantHash & variables)82 void TemplateRenderer::addVariables(const QVariantHash& variables)
83 {
84 Q_D(TemplateRenderer);
85
86 QVariantHash::const_iterator it = variables.constBegin();
87 QVariantHash::const_iterator end = variables.constEnd();
88 for (; it != end; ++it) {
89 d->context.insert(it.key(), it.value());
90 }
91 }
92
addVariable(const QString & name,const QVariant & value)93 void TemplateRenderer::addVariable(const QString& name, const QVariant& value)
94 {
95 Q_D(TemplateRenderer);
96
97 d->context.insert(name, value);
98 }
99
variables() const100 QVariantHash TemplateRenderer::variables() const
101 {
102 Q_D(const TemplateRenderer);
103
104 return d->context.stackHash(0);
105 }
106
render(const QString & content,const QString & name)107 QString TemplateRenderer::render(const QString& content, const QString& name)
108 {
109 Q_D(TemplateRenderer);
110
111 Template t = d->engine->newTemplate(content, name);
112
113 QString output;
114 QTextStream textStream(&output);
115 NoEscapeStream stream(&textStream);
116 t->render(&stream, &d->context);
117
118 if (t->error() != Grantlee::NoError) {
119 d->errorString = t->errorString();
120 } else
121 {
122 d->errorString.clear();
123 }
124
125 if (d->emptyLinesPolicy == TrimEmptyLines && output.contains(QLatin1Char('\n'))) {
126 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
127 QStringList lines = output.split(QLatin1Char('\n'), Qt::KeepEmptyParts);
128 #else
129 QStringList lines = output.split(QLatin1Char('\n'), QString::KeepEmptyParts);
130 #endif
131 QMutableStringListIterator it(lines);
132
133 // Remove empty lines from the start of the document
134 while (it.hasNext()) {
135 if (it.next().trimmed().isEmpty()) {
136 it.remove();
137 } else
138 {
139 break;
140 }
141 }
142
143 // Remove single empty lines
144 it.toFront();
145 bool prePreviousEmpty = false;
146 bool previousEmpty = false;
147 while (it.hasNext()) {
148 bool currentEmpty = it.peekNext().trimmed().isEmpty();
149 if (!prePreviousEmpty && previousEmpty && !currentEmpty) {
150 it.remove();
151 }
152 prePreviousEmpty = previousEmpty;
153 previousEmpty = currentEmpty;
154 it.next();
155 }
156
157 // Compress multiple empty lines
158 it.toFront();
159 previousEmpty = false;
160 while (it.hasNext()) {
161 bool currentEmpty = it.next().trimmed().isEmpty();
162 if (currentEmpty && previousEmpty) {
163 it.remove();
164 }
165 previousEmpty = currentEmpty;
166 }
167
168 // Remove empty lines from the end
169 it.toBack();
170 while (it.hasPrevious()) {
171 if (it.previous().trimmed().isEmpty()) {
172 it.remove();
173 } else
174 {
175 break;
176 }
177 }
178
179 // Add a newline to the end of file
180 it.toBack();
181 it.insert(QString());
182
183 output = lines.join(QLatin1Char('\n'));
184 } else if (d->emptyLinesPolicy == RemoveEmptyLines) {
185 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
186 QStringList lines = output.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
187 #else
188 QStringList lines = output.split(QLatin1Char('\n'), QString::SkipEmptyParts);
189 #endif
190 QMutableStringListIterator it(lines);
191 while (it.hasNext()) {
192 if (it.next().trimmed().isEmpty()) {
193 it.remove();
194 }
195 }
196 it.toBack();
197 if (lines.size() > 1) {
198 it.insert(QString());
199 }
200 output = lines.join(QLatin1Char('\n'));
201 }
202
203 return output;
204 }
205
renderFile(const QUrl & url,const QString & name)206 QString TemplateRenderer::renderFile(const QUrl& url, const QString& name)
207 {
208 QFile file(url.toLocalFile());
209 file.open(QIODevice::ReadOnly);
210
211 const QString content = QString::fromUtf8(file.readAll());
212 qCDebug(LANGUAGE) << content;
213
214 return render(content, name);
215 }
216
render(const QStringList & contents)217 QStringList TemplateRenderer::render(const QStringList& contents)
218 {
219 Q_D(TemplateRenderer);
220
221 qCDebug(LANGUAGE) << d->context.stackHash(0);
222 QStringList ret;
223 ret.reserve(contents.size());
224 for (const QString& content : contents) {
225 ret << render(content);
226 }
227
228 return ret;
229 }
230
setEmptyLinesPolicy(TemplateRenderer::EmptyLinesPolicy policy)231 void TemplateRenderer::setEmptyLinesPolicy(TemplateRenderer::EmptyLinesPolicy policy)
232 {
233 Q_D(TemplateRenderer);
234
235 d->emptyLinesPolicy = policy;
236 }
237
emptyLinesPolicy() const238 TemplateRenderer::EmptyLinesPolicy TemplateRenderer::emptyLinesPolicy() const
239 {
240 Q_D(const TemplateRenderer);
241
242 return d->emptyLinesPolicy;
243 }
244
renderFileTemplate(const SourceFileTemplate & fileTemplate,const QUrl & baseUrl,const QHash<QString,QUrl> & fileUrls)245 DocumentChangeSet TemplateRenderer::renderFileTemplate(const SourceFileTemplate& fileTemplate,
246 const QUrl& baseUrl,
247 const QHash<QString, QUrl>& fileUrls)
248 {
249 DocumentChangeSet changes;
250 const QDir baseDir(baseUrl.path());
251
252 QRegExp nonAlphaNumeric(QStringLiteral("\\W"));
253 for (QHash<QString, QUrl>::const_iterator it = fileUrls.constBegin(); it != fileUrls.constEnd(); ++it) {
254 QString cleanName = it.key().toLower();
255 cleanName.replace(nonAlphaNumeric, QStringLiteral("_"));
256 const QString path = it.value().toLocalFile();
257 addVariable(QLatin1String("output_file_") + cleanName, baseDir.relativeFilePath(path));
258 addVariable(QLatin1String("output_file_") + cleanName + QLatin1String("_absolute"), path);
259 }
260
261 const KArchiveDirectory* directory = fileTemplate.directory();
262 ArchiveTemplateLocation location(directory);
263 const auto outputFiles = fileTemplate.outputFiles();
264 for (const SourceFileTemplate::OutputFile& outputFile : outputFiles) {
265 const KArchiveEntry* entry = directory->entry(outputFile.fileName);
266 if (!entry) {
267 qCWarning(LANGUAGE) << "Entry" << outputFile.fileName << "is mentioned in group" << outputFile.identifier <<
268 "but is not present in the archive";
269 continue;
270 }
271
272 const auto* file = dynamic_cast<const KArchiveFile*>(entry);
273 if (!file) {
274 qCWarning(LANGUAGE) << "Entry" << entry->name() << "is not a file";
275 continue;
276 }
277
278 QUrl url = fileUrls[outputFile.identifier];
279 IndexedString document(url);
280 KTextEditor::Range range(KTextEditor::Cursor(0, 0), 0);
281
282 DocumentChange change(document, range, QString(),
283 render(QString::fromUtf8(file->data()), outputFile.identifier));
284 changes.addChange(change);
285 qCDebug(LANGUAGE) << "Added change for file" << document.str();
286 }
287
288 return changes;
289 }
290
errorString() const291 QString TemplateRenderer::errorString() const
292 {
293 Q_D(const TemplateRenderer);
294
295 return d->errorString;
296 }
297