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