diff options
author | Tobias Hunger <[email protected]> | 2015-10-01 15:03:18 +0200 |
---|---|---|
committer | Tobias Hunger <[email protected]> | 2015-10-12 11:53:01 +0000 |
commit | 95b0dc9120dba73571ac6df9e5106f95b69b99d7 (patch) | |
tree | 40598171c0b86df32d5279ddb3b2360586ee25f9 /src/libs | |
parent | b899684a89b2c6880cf346b3ea24d5705fdf42ff (diff) |
Utils: Move template file processing from projectexplorer to utils
I want to use it e.g. for snippets and the TextEditor plugin may
not depend on the ProjectExplorer, so the code has to move.
This adds a dependency on QtQml to Utils, but that does not really
matter since that is loaded into QtCreator anyway.
Change-Id: Iada9f40b2966a1fc41631ab33da09812ad67d967
Reviewed-by: Daniel Teske <[email protected]>
Diffstat (limited to 'src/libs')
-rw-r--r-- | src/libs/utils/templateengine.cpp | 316 | ||||
-rw-r--r-- | src/libs/utils/templateengine.h | 57 | ||||
-rw-r--r-- | src/libs/utils/utils-lib.pri | 2 | ||||
-rw-r--r-- | src/libs/utils/utils.pro | 2 | ||||
-rw-r--r-- | src/libs/utils/utils.qbs | 2 |
5 files changed, 377 insertions, 2 deletions
diff --git a/src/libs/utils/templateengine.cpp b/src/libs/utils/templateengine.cpp new file mode 100644 index 00000000000..c372276e7a5 --- /dev/null +++ b/src/libs/utils/templateengine.cpp @@ -0,0 +1,316 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms and +** conditions see http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "templateengine.h" + +#include "qtcassert.h" + +#include <QJSEngine> +#include <QStack> + +namespace Utils { + +namespace Internal { + +// Preprocessor: Conditional section type. +enum PreprocessorSection { + IfSection, + ElsifSection, + ElseSection, + EndifSection, + OtherSection +}; + +// Preprocessor: Section stack entry containing nested '@if' section +// state. +class PreprocessStackEntry { +public: + PreprocessStackEntry(PreprocessorSection section = OtherSection, + bool parentEnabled = true, + bool condition = false, + bool anyIfClauseMatched = false); + + PreprocessorSection section; + bool parentEnabled; + bool condition; // Current 'if/elsif' section is enabled. + bool anyIfClauseMatched; // Determines if 'else' triggers +}; + +PreprocessStackEntry::PreprocessStackEntry(PreprocessorSection s, bool p, bool c, bool a) : + section(s), parentEnabled(p), condition(c), anyIfClauseMatched(a) +{ } + +// Context for preprocessing. +class PreprocessContext { +public: + PreprocessContext(); + bool process(const QString &in, QString *out, QString *errorMessage); + +private: + void reset(); + PreprocessorSection preprocessorLine(const QString & in, QString *ifExpression) const; + + mutable QRegExp m_ifPattern; + mutable QRegExp m_elsifPattern; + mutable QRegExp m_elsePattern; + mutable QRegExp m_endifPattern; + + QStack<PreprocessStackEntry> m_sectionStack; + QJSEngine m_scriptEngine; +}; + +PreprocessContext::PreprocessContext() : + // Cut out expression for 'if/elsif ' + m_ifPattern(QLatin1String("^([\\s]*@[\\s]*if[\\s]*)(.*)$")), + m_elsifPattern(QLatin1String("^([\\s]*@[\\s]*elsif[\\s]*)(.*)$")), + m_elsePattern(QLatin1String("^[\\s]*@[\\s]*else.*$")), + m_endifPattern(QLatin1String("^[\\s]*@[\\s]*endif.*$")) +{ + QTC_CHECK(m_ifPattern.isValid() && m_elsifPattern.isValid() + && m_elsePattern.isValid() && m_endifPattern.isValid()); +} + +void PreprocessContext::reset() +{ + m_sectionStack.clear(); + // Add a default, enabled section. + m_sectionStack.push(PreprocessStackEntry(OtherSection, true, true)); +} + +// Determine type of line and return enumeration, cut out +// expression for '@if/@elsif'. +PreprocessorSection PreprocessContext::preprocessorLine(const QString &in, + QString *ifExpression) const +{ + if (m_ifPattern.exactMatch(in)) { + *ifExpression = m_ifPattern.cap(2).trimmed(); + return IfSection; + } + if (m_elsifPattern.exactMatch(in)) { + *ifExpression = m_elsifPattern.cap(2).trimmed(); + return ElsifSection; + } + + ifExpression->clear(); + + if (m_elsePattern.exactMatch(in)) + return ElseSection; + if (m_endifPattern.exactMatch(in)) + return EndifSection; + return OtherSection; +} + +static inline QString msgEmptyStack(int line) +{ + return QString::fromLatin1("Unmatched '@endif' at line %1.").arg(line); +} + +bool PreprocessContext::process(const QString &in, QString *out, QString *errorMessage) +{ + out->clear(); + if (in.isEmpty()) + return true; + + out->reserve(in.size()); + reset(); + + const QChar newLine = QLatin1Char('\n'); + const QStringList lines = in.split(newLine, QString::KeepEmptyParts); + const int lineCount = lines.size(); + for (int l = 0; l < lineCount; l++) { + // Check for element of the stack (be it dummy, else something is wrong). + if (m_sectionStack.isEmpty()) { + if (errorMessage) + *errorMessage = msgEmptyStack(l); + return false; + } + QString expression; + bool expressionValue = false; + PreprocessStackEntry &top = m_sectionStack.back(); + + switch (preprocessorLine(lines.at(l), &expression)) { + case IfSection: + // '@If': Push new section + if (top.condition) { + if (!TemplateEngine::evaluateBooleanJavaScriptExpression(m_scriptEngine, expression, + &expressionValue, errorMessage)) { + if (errorMessage) + *errorMessage = QString::fromLatin1("Error in @if at %1: %2") + .arg(l + 1).arg(*errorMessage); + return false; + } + } + m_sectionStack.push(PreprocessStackEntry(IfSection, + top.condition, expressionValue, expressionValue)); + break; + case ElsifSection: // '@elsif': Check condition. + if (top.section != IfSection && top.section != ElsifSection) { + if (errorMessage) + *errorMessage = QString::fromLatin1("No preceding @if found for @elsif at %1") + .arg(l + 1); + return false; + } + if (top.parentEnabled) { + if (!TemplateEngine::evaluateBooleanJavaScriptExpression(m_scriptEngine, expression, + &expressionValue, errorMessage)) { + if (errorMessage) + *errorMessage = QString::fromLatin1("Error in @elsif at %1: %2") + .arg(l + 1).arg(*errorMessage); + return false; + } + } + top.section = ElsifSection; + // ignore consecutive '@elsifs' once something matched + if (top.anyIfClauseMatched) { + top.condition = false; + } else { + if ( (top.condition = expressionValue) ) + top.anyIfClauseMatched = true; + } + break; + case ElseSection: // '@else': Check condition. + if (top.section != IfSection && top.section != ElsifSection) { + if (errorMessage) + *errorMessage = QString::fromLatin1("No preceding @if/@elsif found for @else at %1") + .arg(l + 1); + return false; + } + expressionValue = top.parentEnabled && !top.anyIfClauseMatched; + top.section = ElseSection; + top.condition = expressionValue; + break; + case EndifSection: // '@endif': Discard section. + m_sectionStack.pop(); + break; + case OtherSection: // Rest: Append according to current condition. + if (top.condition) { + out->append(lines.at(l)); + out->append(newLine); + } + break; + } // switch section + + } // for lines + return true; +} + +} // namespace Internal + +bool TemplateEngine::preprocessText(const QString &in, QString *out, QString *errorMessage) +{ + Internal::PreprocessContext context; + return context.process(in, out, errorMessage); +} + +QString TemplateEngine::processText(MacroExpander *expander, const QString &input, + QString *errorMessage) +{ + if (errorMessage) + errorMessage->clear(); + + if (input.isEmpty()) + return input; + + // Recursively expand macros: + QString in = input; + QString oldIn; + for (int i = 0; i < 5 && in != oldIn; ++i) { + oldIn = in; + in = expander->expand(oldIn); + } + + QString out; + if (!preprocessText(in, &out, errorMessage)) + return QString(); + + // Expand \n, \t and handle line continuation: + QString result; + result.reserve(out.count()); + bool isEscaped = false; + for (int i = 0; i < out.count(); ++i) { + const QChar c = out.at(i); + + if (isEscaped) { + if (c == QLatin1Char('n')) + result.append(QLatin1Char('\n')); + else if (c == QLatin1Char('t')) + result.append(QLatin1Char('\t')); + else if (c != QLatin1Char('\n')) + result.append(c); + isEscaped = false; + } else { + if (c == QLatin1Char('\\')) + isEscaped = true; + else + result.append(c); + } + } + return result; +} + +bool TemplateEngine::evaluateBooleanJavaScriptExpression(QJSEngine &engine, + const QString &expression, bool *result, + QString *errorMessage) +{ + if (errorMessage) + errorMessage->clear(); + if (result) + *result = false; + const QJSValue value = engine.evaluate(expression); + if (value.isError()) { + if (errorMessage) + *errorMessage = QString::fromLatin1("Error in \"%1\": %2") + .arg(expression, value.toString()); + return false; + } + // Try to convert to bool, be that an int or whatever. + if (value.isBool()) { + if (result) + *result = value.toBool(); + return true; + } + if (value.isNumber()) { + if (result) + *result = !qFuzzyCompare(value.toNumber(), 0); + return true; + } + if (value.isString()) { + if (result) + *result = !value.toString().isEmpty(); + return true; + } + if (errorMessage) + *errorMessage = QString::fromLatin1("Cannot convert result of \"%1\" (\"%2\"to bool.") + .arg(expression, value.toString()); + + return false; +} + +} // namespace Utils diff --git a/src/libs/utils/templateengine.h b/src/libs/utils/templateengine.h new file mode 100644 index 00000000000..a277841ca09 --- /dev/null +++ b/src/libs/utils/templateengine.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms and +** conditions see http://www.qt.io/terms-conditions. For further information +** use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#ifndef TEMPLATEENGINE_H +#define TEMPLATEENGINE_H + +#include "utils_global.h" + +#include "macroexpander.h" + +#include <QString> + +QT_FORWARD_DECLARE_CLASS(QJSEngine); + +namespace Utils { + +class QTCREATOR_UTILS_EXPORT TemplateEngine { +public: + static bool preprocessText(const QString &input, QString *output, QString *errorMessage); + + static QString processText(MacroExpander *expander, const QString &input, + QString *errorMessage); + + static bool evaluateBooleanJavaScriptExpression(QJSEngine &engine, const QString &expression, + bool *result, QString *errorMessage); +}; + +} // namespace Utils + +#endif // TEMPLATEENGINE_H diff --git a/src/libs/utils/utils-lib.pri b/src/libs/utils/utils-lib.pri index a4b7ed13818..bb66f7b5a5a 100644 --- a/src/libs/utils/utils-lib.pri +++ b/src/libs/utils/utils-lib.pri @@ -16,6 +16,7 @@ SOURCES += $$PWD/environment.cpp \ $$PWD/shellcommandpage.cpp \ $$PWD/settingsselector.cpp \ $$PWD/stringutils.cpp \ + $$PWD/templateengine.cpp \ $$PWD/textfieldcheckbox.cpp \ $$PWD/textfieldcombobox.cpp \ $$PWD/filesearch.cpp \ @@ -107,6 +108,7 @@ HEADERS += \ $$PWD/shellcommand.h \ $$PWD/shellcommandpage.h \ $$PWD/stringutils.h \ + $$PWD/templateengine.h \ $$PWD/textfieldcheckbox.h \ $$PWD/textfieldcombobox.h \ $$PWD/filesearch.h \ diff --git a/src/libs/utils/utils.pro b/src/libs/utils/utils.pro index cb28e4ad43e..d69ff0ff44b 100644 --- a/src/libs/utils/utils.pro +++ b/src/libs/utils/utils.pro @@ -1,4 +1,4 @@ -QT += gui network +QT += gui network qml include(../../qtcreatorlibrary.pri) include(utils-lib.pri) diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index 5b705a26bcf..c0e1879ee46 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -28,7 +28,7 @@ QtcLibrary { cpp.frameworks: ["Foundation"] } - Depends { name: "Qt"; submodules: ["widgets", "network", "script", "concurrent"] } + Depends { name: "Qt"; submodules: ["gui", "network", "qml"] } Depends { name: "app_version_header" } files: [ |