File indexing completed on 2024-05-12 04:37:48

0001 /*
0002     SPDX-FileCopyrightText: 2012 Miha Čančula <miha@noughmad.eu>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "templaterenderer.h"
0008 
0009 #include "documentchangeset.h"
0010 #include "sourcefiletemplate.h"
0011 #include "templateengine.h"
0012 #include "templateengine_p.h"
0013 #include "archivetemplateloader.h"
0014 #include <debug.h>
0015 
0016 #include <serialization/indexedstring.h>
0017 
0018 #include <grantlee/context.h>
0019 
0020 #include <QDir>
0021 #include <QFile>
0022 #include <QUrl>
0023 
0024 #include <KArchive>
0025 
0026 using namespace Grantlee;
0027 
0028 class NoEscapeStream
0029     : public OutputStream
0030 {
0031 public:
0032     NoEscapeStream();
0033     explicit NoEscapeStream (QTextStream* stream);
0034 
0035     QString escape (const QString& input) const override;
0036     QSharedPointer<OutputStream> clone (QTextStream* stream) const override;
0037 };
0038 
0039 NoEscapeStream::NoEscapeStream() : OutputStream()
0040 {
0041 }
0042 
0043 NoEscapeStream::NoEscapeStream(QTextStream* stream) : OutputStream(stream)
0044 {
0045 }
0046 
0047 QString NoEscapeStream::escape(const QString& input) const
0048 {
0049     return input;
0050 }
0051 
0052 QSharedPointer<OutputStream> NoEscapeStream::clone(QTextStream* stream) const
0053 {
0054     QSharedPointer<OutputStream> clonedStream = QSharedPointer<OutputStream>(new NoEscapeStream(stream));
0055     return clonedStream;
0056 }
0057 
0058 using namespace KDevelop;
0059 
0060 namespace KDevelop {
0061 class TemplateRendererPrivate
0062 {
0063 public:
0064     Engine* engine;
0065     Grantlee::Context context;
0066     TemplateRenderer::EmptyLinesPolicy emptyLinesPolicy;
0067     QString errorString;
0068 };
0069 }
0070 
0071 TemplateRenderer::TemplateRenderer()
0072     : d_ptr(new TemplateRendererPrivate)
0073 {
0074     Q_D(TemplateRenderer);
0075 
0076     d->engine = &TemplateEngine::self()->d_ptr->engine;
0077     d->emptyLinesPolicy = KeepEmptyLines;
0078 }
0079 
0080 TemplateRenderer::~TemplateRenderer() = default;
0081 
0082 void TemplateRenderer::addVariables(const QVariantHash& variables)
0083 {
0084     Q_D(TemplateRenderer);
0085 
0086     QVariantHash::const_iterator it = variables.constBegin();
0087     QVariantHash::const_iterator end = variables.constEnd();
0088     for (; it != end; ++it) {
0089         d->context.insert(it.key(), it.value());
0090     }
0091 }
0092 
0093 void TemplateRenderer::addVariable(const QString& name, const QVariant& value)
0094 {
0095     Q_D(TemplateRenderer);
0096 
0097     d->context.insert(name, value);
0098 }
0099 
0100 QVariantHash TemplateRenderer::variables() const
0101 {
0102     Q_D(const TemplateRenderer);
0103 
0104     return d->context.stackHash(0);
0105 }
0106 
0107 QString TemplateRenderer::render(const QString& content, const QString& name)
0108 {
0109     Q_D(TemplateRenderer);
0110 
0111     Template t = d->engine->newTemplate(content, name);
0112 
0113     QString output;
0114     QTextStream textStream(&output);
0115     NoEscapeStream stream(&textStream);
0116     t->render(&stream, &d->context);
0117 
0118     if (t->error() != Grantlee::NoError) {
0119         d->errorString = t->errorString();
0120     } else
0121     {
0122         d->errorString.clear();
0123     }
0124 
0125     if (d->emptyLinesPolicy == TrimEmptyLines && output.contains(QLatin1Char('\n'))) {
0126         QStringList lines = output.split(QLatin1Char('\n'), Qt::KeepEmptyParts);
0127         QMutableStringListIterator it(lines);
0128 
0129         // Remove empty lines from the start of the document
0130         while (it.hasNext()) {
0131             if (it.next().trimmed().isEmpty()) {
0132                 it.remove();
0133             } else
0134             {
0135                 break;
0136             }
0137         }
0138 
0139         // Remove single empty lines
0140         it.toFront();
0141         bool prePreviousEmpty = false;
0142         bool previousEmpty = false;
0143         while (it.hasNext()) {
0144             bool currentEmpty = it.peekNext().trimmed().isEmpty();
0145             if (!prePreviousEmpty && previousEmpty && !currentEmpty) {
0146                 it.remove();
0147             }
0148             prePreviousEmpty = previousEmpty;
0149             previousEmpty = currentEmpty;
0150             it.next();
0151         }
0152 
0153         // Compress multiple empty lines
0154         it.toFront();
0155         previousEmpty = false;
0156         while (it.hasNext()) {
0157             bool currentEmpty = it.next().trimmed().isEmpty();
0158             if (currentEmpty && previousEmpty) {
0159                 it.remove();
0160             }
0161             previousEmpty = currentEmpty;
0162         }
0163 
0164         // Remove empty lines from the end
0165         it.toBack();
0166         while (it.hasPrevious()) {
0167             if (it.previous().trimmed().isEmpty()) {
0168                 it.remove();
0169             } else
0170             {
0171                 break;
0172             }
0173         }
0174 
0175         // Add a newline to the end of file
0176         it.toBack();
0177         it.insert(QString());
0178 
0179         output = lines.join(QLatin1Char('\n'));
0180     } else if (d->emptyLinesPolicy == RemoveEmptyLines) {
0181         QStringList lines = output.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0182         QMutableStringListIterator it(lines);
0183         while (it.hasNext()) {
0184             if (it.next().trimmed().isEmpty()) {
0185                 it.remove();
0186             }
0187         }
0188         it.toBack();
0189         if (lines.size() > 1) {
0190             it.insert(QString());
0191         }
0192         output = lines.join(QLatin1Char('\n'));
0193     }
0194 
0195     return output;
0196 }
0197 
0198 QString TemplateRenderer::renderFile(const QUrl& url, const QString& name)
0199 {
0200     QFile file(url.toLocalFile());
0201     file.open(QIODevice::ReadOnly);
0202 
0203     const QString content = QString::fromUtf8(file.readAll());
0204     qCDebug(LANGUAGE) << content;
0205 
0206     return render(content, name);
0207 }
0208 
0209 QStringList TemplateRenderer::render(const QStringList& contents)
0210 {
0211     Q_D(TemplateRenderer);
0212 
0213     qCDebug(LANGUAGE) << d->context.stackHash(0);
0214     QStringList ret;
0215     ret.reserve(contents.size());
0216     for (const QString& content : contents) {
0217         ret << render(content);
0218     }
0219 
0220     return ret;
0221 }
0222 
0223 void TemplateRenderer::setEmptyLinesPolicy(TemplateRenderer::EmptyLinesPolicy policy)
0224 {
0225     Q_D(TemplateRenderer);
0226 
0227     d->emptyLinesPolicy = policy;
0228 }
0229 
0230 TemplateRenderer::EmptyLinesPolicy TemplateRenderer::emptyLinesPolicy() const
0231 {
0232     Q_D(const TemplateRenderer);
0233 
0234     return d->emptyLinesPolicy;
0235 }
0236 
0237 DocumentChangeSet TemplateRenderer::renderFileTemplate(const SourceFileTemplate& fileTemplate,
0238                                                        const QUrl& baseUrl,
0239                                                        const QHash<QString, QUrl>& fileUrls)
0240 {
0241     Q_ASSERT(fileTemplate.isValid());
0242 
0243     DocumentChangeSet changes;
0244     const QDir baseDir(baseUrl.path());
0245 
0246     QRegExp nonAlphaNumeric(QStringLiteral("\\W"));
0247     for (QHash<QString, QUrl>::const_iterator it = fileUrls.constBegin(); it != fileUrls.constEnd(); ++it) {
0248         QString cleanName = it.key().toLower();
0249         cleanName.replace(nonAlphaNumeric, QStringLiteral("_"));
0250         const QString path = it.value().toLocalFile();
0251         addVariable(QLatin1String("output_file_") + cleanName, baseDir.relativeFilePath(path));
0252         addVariable(QLatin1String("output_file_") + cleanName + QLatin1String("_absolute"), path);
0253     }
0254 
0255     const KArchiveDirectory* directory = fileTemplate.directory();
0256     ArchiveTemplateLocation location(directory);
0257     const auto outputFiles = fileTemplate.outputFiles();
0258     for (const SourceFileTemplate::OutputFile& outputFile : outputFiles) {
0259         const KArchiveEntry* entry = directory->entry(outputFile.fileName);
0260         if (!entry) {
0261             qCWarning(LANGUAGE) << "Entry" << outputFile.fileName << "is mentioned in group" << outputFile.identifier <<
0262                 "but is not present in the archive";
0263             continue;
0264         }
0265 
0266         const auto* file = dynamic_cast<const KArchiveFile*>(entry);
0267         if (!file) {
0268             qCWarning(LANGUAGE) << "Entry" << entry->name() << "is not a file";
0269             continue;
0270         }
0271 
0272         QUrl url = fileUrls[outputFile.identifier];
0273         IndexedString document(url);
0274         KTextEditor::Range range(KTextEditor::Cursor(0, 0), 0);
0275 
0276         DocumentChange change(document, range, QString(),
0277             render(QString::fromUtf8(file->data()), outputFile.identifier));
0278         changes.addChange(change);
0279         qCDebug(LANGUAGE) << "Added change for file" << document.str();
0280     }
0281 
0282     return changes;
0283 }
0284 
0285 QString TemplateRenderer::errorString() const
0286 {
0287     Q_D(const TemplateRenderer);
0288 
0289     return d->errorString;
0290 }