1
0
mirror of https://bitbucket.org/librepilot/librepilot.git synced 2025-01-18 03:52:11 +01:00

OP-1538 Added Mustache template renderer by Robert Knight.

Added #include "utils_global.h"
Added QTCREATOR_UTILS_EXPORT to all public classes in header file.
See file header comments for Original Authors Copyright information.
See https://github.com/robertknight/qt-mustache
See http://mustache.github.io/
This commit is contained in:
m_thread 2014-10-15 00:40:09 +02:00
parent d4d6789198
commit 26eff1bb4e
3 changed files with 829 additions and 2 deletions

View File

@ -0,0 +1,559 @@
/*
Copyright 2012, Robert Knight
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
*/
#include "mustache.h"
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <QtCore/QStringList>
#include <QtCore/QTextStream>
using namespace Mustache;
QString Mustache::renderTemplate(const QString & templateString, const QVariantHash & args)
{
Mustache::QtVariantContext context(args);
Mustache::Renderer renderer;
return renderer.render(templateString, &context);
}
QString escapeHtml(const QString & input)
{
QString escaped(input);
for (int i = 0; i < escaped.count();) {
const char *replacement = 0;
ushort ch = escaped.at(i).unicode();
if (ch == '&') {
replacement = "&amp;";
} else if (ch == '<') {
replacement = "&lt;";
} else if (ch == '>') {
replacement = "&gt;";
} else if (ch == '"') {
replacement = "&quot;";
}
if (replacement) {
escaped.replace(i, 1, QLatin1String(replacement));
i += strlen(replacement);
} else {
++i;
}
}
return escaped;
}
QString unescapeHtml(const QString & escaped)
{
QString unescaped(escaped);
unescaped.replace(QLatin1String("&lt;"), QLatin1String("<"));
unescaped.replace(QLatin1String("&gt;"), QLatin1String(">"));
unescaped.replace(QLatin1String("&amp;"), QLatin1String("&"));
unescaped.replace(QLatin1String("&quot;"), QLatin1String("\""));
return unescaped;
}
Context::Context(PartialResolver *resolver)
: m_partialResolver(resolver)
{}
PartialResolver *Context::partialResolver() const
{
return m_partialResolver;
}
QString Context::partialValue(const QString & key) const
{
if (!m_partialResolver) {
return QString();
}
return m_partialResolver->getPartial(key);
}
bool Context::canEval(const QString &) const
{
return false;
}
QString Context::eval(const QString & key, const QString & _template, Renderer *renderer)
{
Q_UNUSED(key);
Q_UNUSED(_template);
Q_UNUSED(renderer);
return QString();
}
QtVariantContext::QtVariantContext(const QVariant & root, PartialResolver *resolver)
: Context(resolver)
{
m_contextStack << root;
}
QVariant variantMapValue(const QVariant & value, const QString & key)
{
if (value.userType() == QVariant::Map) {
return value.toMap().value(key);
} else {
return value.toHash().value(key);
}
}
QVariant variantMapValueForKeyPath(const QVariant & value, const QStringList keyPath)
{
if (keyPath.count() > 1) {
QVariant firstValue = variantMapValue(value, keyPath.first());
return firstValue.isNull() ? QVariant() : variantMapValueForKeyPath(firstValue, keyPath.mid(1));
} else if (!keyPath.isEmpty()) {
return variantMapValue(value, keyPath.first());
}
return QVariant();
}
QVariant QtVariantContext::value(const QString & key) const
{
if (key == "." && !m_contextStack.isEmpty()) {
return m_contextStack.last();
}
QStringList keyPath = key.split(".");
for (int i = m_contextStack.count() - 1; i >= 0; i--) {
QVariant value = variantMapValueForKeyPath(m_contextStack.at(i), keyPath);
if (!value.isNull()) {
return value;
}
}
return QVariant();
}
bool QtVariantContext::isFalse(const QString & key) const
{
QVariant value = this->value(key);
switch (value.userType()) {
case QVariant::Bool:
return !value.toBool();
case QVariant::List:
return value.toList().isEmpty();
case QVariant::Hash:
return value.toHash().isEmpty();
case QVariant::Map:
return value.toMap().isEmpty();
default:
return value.toString().isEmpty();
}
}
QString QtVariantContext::stringValue(const QString & key) const
{
if (isFalse(key)) {
return QString();
}
return value(key).toString();
}
void QtVariantContext::push(const QString & key, int index)
{
QVariant mapItem = value(key);
if (index == -1) {
m_contextStack << mapItem;
} else {
QVariantList list = mapItem.toList();
m_contextStack << list.value(index, QVariant());
}
}
void QtVariantContext::pop()
{
m_contextStack.pop();
}
int QtVariantContext::listCount(const QString & key) const
{
if (value(key).userType() == QVariant::List) {
return value(key).toList().count();
}
return 0;
}
bool QtVariantContext::canEval(const QString & key) const
{
return value(key).canConvert<fn_t>();
}
QString QtVariantContext::eval(const QString & key, const QString & _template, Renderer *renderer)
{
QVariant fn = value(key);
if (fn.isNull()) {
return QString();
}
return fn.value<fn_t>() (_template, renderer, this);
}
PartialMap::PartialMap(const QHash<QString, QString> & partials)
: m_partials(partials)
{}
QString PartialMap::getPartial(const QString & name)
{
return m_partials.value(name);
}
PartialFileLoader::PartialFileLoader(const QString & basePath)
: m_basePath(basePath)
{}
QString PartialFileLoader::getPartial(const QString & name)
{
if (!m_cache.contains(name)) {
QString path = m_basePath + '/' + name + ".mustache";
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
QTextStream stream(&file);
m_cache.insert(name, stream.readAll());
}
}
return m_cache.value(name);
}
Renderer::Renderer()
: m_errorPos(-1)
, m_defaultTagStartMarker("{{")
, m_defaultTagEndMarker("}}")
{}
QString Renderer::error() const
{
return m_error;
}
int Renderer::errorPos() const
{
return m_errorPos;
}
QString Renderer::errorPartial() const
{
return m_errorPartial;
}
QString Renderer::render(const QString & _template, Context *context)
{
m_error.clear();
m_errorPos = -1;
m_errorPartial.clear();
m_tagStartMarker = m_defaultTagStartMarker;
m_tagEndMarker = m_defaultTagEndMarker;
return render(_template, 0, _template.length(), context);
}
QString Renderer::render(const QString & _template, int startPos, int endPos, Context *context)
{
QString output;
int lastTagEnd = startPos;
while (m_errorPos == -1) {
Tag tag = findTag(_template, lastTagEnd, endPos);
if (tag.type == Tag::Null) {
output += _template.midRef(lastTagEnd, endPos - lastTagEnd);
break;
}
output += _template.midRef(lastTagEnd, tag.start - lastTagEnd);
switch (tag.type) {
case Tag::Value:
{
QString value = context->stringValue(tag.key);
if (tag.escapeMode == Tag::Escape) {
value = escapeHtml(value);
} else if (tag.escapeMode == Tag::Unescape) {
value = unescapeHtml(value);
}
output += value;
lastTagEnd = tag.end;
}
break;
case Tag::SectionStart:
{
Tag endTag = findEndTag(_template, tag, endPos);
if (endTag.type == Tag::Null) {
if (m_errorPos == -1) {
setError("No matching end tag found for section", tag.start);
}
} else {
int listCount = context->listCount(tag.key);
if (listCount > 0) {
for (int i = 0; i < listCount; i++) {
context->push(tag.key, i);
output += render(_template, tag.end, endTag.start, context);
context->pop();
}
} else if (context->canEval(tag.key)) {
output += context->eval(tag.key, _template.mid(tag.end, endTag.start - tag.end), this);
} else if (!context->isFalse(tag.key)) {
context->push(tag.key);
output += render(_template, tag.end, endTag.start, context);
context->pop();
}
lastTagEnd = endTag.end;
}
}
break;
case Tag::InvertedSectionStart:
{
Tag endTag = findEndTag(_template, tag, endPos);
if (endTag.type == Tag::Null) {
if (m_errorPos == -1) {
setError("No matching end tag found for inverted section", tag.start);
}
} else {
if (context->isFalse(tag.key)) {
output += render(_template, tag.end, endTag.start, context);
}
lastTagEnd = endTag.end;
}
}
break;
case Tag::SectionEnd:
setError("Unexpected end tag", tag.start);
lastTagEnd = tag.end;
break;
case Tag::Partial:
{
QString tagStartMarker = m_tagStartMarker;
QString tagEndMarker = m_tagEndMarker;
m_tagStartMarker = m_defaultTagStartMarker;
m_tagEndMarker = m_defaultTagEndMarker;
m_partialStack.push(tag.key);
QString partial = context->partialValue(tag.key);
output += render(partial, 0, partial.length(), context);
lastTagEnd = tag.end;
m_partialStack.pop();
m_tagStartMarker = tagStartMarker;
m_tagEndMarker = tagEndMarker;
}
break;
case Tag::SetDelimiter:
lastTagEnd = tag.end;
break;
case Tag::Comment:
lastTagEnd = tag.end;
break;
case Tag::Null:
break;
}
}
return output;
}
void Renderer::setError(const QString & error, int pos)
{
Q_ASSERT(!error.isEmpty());
Q_ASSERT(pos >= 0);
m_error = error;
m_errorPos = pos;
if (!m_partialStack.isEmpty()) {
m_errorPartial = m_partialStack.top();
}
}
Tag Renderer::findTag(const QString & content, int pos, int endPos)
{
int tagStartPos = content.indexOf(m_tagStartMarker, pos);
if (tagStartPos == -1 || tagStartPos >= endPos) {
return Tag();
}
int tagEndPos = content.indexOf(m_tagEndMarker, tagStartPos + m_tagStartMarker.length());
if (tagEndPos == -1) {
return Tag();
}
tagEndPos += m_tagEndMarker.length();
Tag tag;
tag.type = Tag::Value;
tag.start = tagStartPos;
tag.end = tagEndPos;
pos = tagStartPos + m_tagStartMarker.length();
endPos = tagEndPos - m_tagEndMarker.length();
QChar typeChar = content.at(pos);
if (typeChar == '#') {
tag.type = Tag::SectionStart;
tag.key = readTagName(content, pos + 1, endPos);
} else if (typeChar == '^') {
tag.type = Tag::InvertedSectionStart;
tag.key = readTagName(content, pos + 1, endPos);
} else if (typeChar == '/') {
tag.type = Tag::SectionEnd;
tag.key = readTagName(content, pos + 1, endPos);
} else if (typeChar == '!') {
tag.type = Tag::Comment;
} else if (typeChar == '>') {
tag.type = Tag::Partial;
tag.key = readTagName(content, pos + 1, endPos);
} else if (typeChar == '=') {
tag.type = Tag::SetDelimiter;
readSetDelimiter(content, pos + 1, tagEndPos - m_tagEndMarker.length());
} else {
if (typeChar == '&') {
tag.escapeMode = Tag::Unescape;
++pos;
} else if (typeChar == '{') {
tag.escapeMode = Tag::Raw;
++pos;
int endTache = content.indexOf('}', pos);
if (endTache == tag.end - m_tagEndMarker.length()) {
++tag.end;
} else {
endPos = endTache;
}
}
tag.type = Tag::Value;
tag.key = readTagName(content, pos, endPos);
}
if (tag.type != Tag::Value) {
expandTag(tag, content);
}
return tag;
}
QString Renderer::readTagName(const QString & content, int pos, int endPos)
{
QString name;
name.reserve(endPos - pos);
while (content.at(pos).isSpace()) {
++pos;
}
while (!content.at(pos).isSpace() && pos < endPos) {
name += content.at(pos);
++pos;
}
return name;
}
void Renderer::readSetDelimiter(const QString & content, int pos, int endPos)
{
QString startMarker;
QString endMarker;
while (content.at(pos).isSpace() && pos < endPos) {
++pos;
}
while (!content.at(pos).isSpace() && pos < endPos) {
if (content.at(pos) == '=') {
setError("Custom delimiters may not contain '='.", pos);
return;
}
startMarker += content.at(pos);
++pos;
}
while (content.at(pos).isSpace() && pos < endPos) {
++pos;
}
while (!content.at(pos).isSpace() && pos < endPos - 1) {
if (content.at(pos) == '=') {
setError("Custom delimiters may not contain '='.", pos);
return;
}
endMarker += content.at(pos);
++pos;
}
m_tagStartMarker = startMarker;
m_tagEndMarker = endMarker;
}
Tag Renderer::findEndTag(const QString & content, const Tag & startTag, int endPos)
{
int tagDepth = 1;
int pos = startTag.end;
while (true) {
Tag nextTag = findTag(content, pos, endPos);
if (nextTag.type == Tag::Null) {
return nextTag;
} else if (nextTag.type == Tag::SectionStart || nextTag.type == Tag::InvertedSectionStart) {
++tagDepth;
} else if (nextTag.type == Tag::SectionEnd) {
--tagDepth;
if (tagDepth == 0) {
if (nextTag.key != startTag.key) {
setError("Tag start/end key mismatch", nextTag.start);
return Tag();
}
return nextTag;
}
}
pos = nextTag.end;
}
return Tag();
}
void Renderer::setTagMarkers(const QString & startMarker, const QString & endMarker)
{
m_defaultTagStartMarker = startMarker;
m_defaultTagEndMarker = endMarker;
}
void Renderer::expandTag(Tag & tag, const QString & content)
{
int start = tag.start;
int end = tag.end;
// Move start to beginning of line.
while (start > 0 && content.at(start - 1) != QLatin1Char('\n')) {
--start;
if (!content.at(start).isSpace()) {
return; // Not standalone.
}
}
// Move end to one past end of line.
while (end <= content.size() && content.at(end - 1) != QLatin1Char('\n')) {
if (end < content.size() && !content.at(end).isSpace()) {
return; // Not standalone.
}
++end;
}
tag.start = start;
tag.end = end;
}

View File

@ -0,0 +1,266 @@
/*
Copyright 2012, Robert Knight
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
*/
#pragma once
#include <QtCore/QStack>
#include <QtCore/QString>
#include <QtCore/QVariant>
#include "utils_global.h"
#if __cplusplus >= 201103L
#include <functional> /* for std::function */
#endif
namespace Mustache {
class PartialResolver;
class Renderer;
/** Context is an interface that Mustache::Renderer::render() uses to
* fetch substitutions for template tags.
*/
class QTCREATOR_UTILS_EXPORT Context {
public:
/** Create a context. @p resolver is used to fetch the expansions for any {{>partial}} tags
* which appear in a template.
*/
explicit Context(PartialResolver *resolver = 0);
virtual ~Context() {}
/** Returns a string representation of the value for @p key in the current context.
* This is used to replace a Mustache value tag.
*/
virtual QString stringValue(const QString & key) const = 0;
/** Returns true if the value for @p key is 'false' or an empty list.
* 'False' values typically include empty strings, the boolean value false etc.
*
* When processing a section Mustache tag, the section is not rendered if the key
* is false, or for an inverted section tag, the section is only rendered if the key
* is false.
*/
virtual bool isFalse(const QString & key) const = 0;
/** Returns the number of items in the list value for @p key or 0 if
* the value for @p key is not a list.
*/
virtual int listCount(const QString & key) const = 0;
/** Set the current context to the value for @p key.
* If index is >= 0, set the current context to the @p index'th value
* in the list value for @p key.
*/
virtual void push(const QString & key, int index = -1) = 0;
/** Exit the current context. */
virtual void pop() = 0;
/** Returns the partial template for a given @p key. */
QString partialValue(const QString & key) const;
/** Returns the partial resolver passed to the constructor. */
PartialResolver *partialResolver() const;
/** Returns true if eval() should be used to render section tags using @p key.
* If canEval() returns true for a key, the renderer will pass the literal, unrendered
* block of text for the section to eval() and replace the section with the result.
*
* canEval() and eval() are equivalents for callable objects (eg. lambdas) in other
* Mustache implementations.
*
* The default implementation always returns false.
*/
virtual bool canEval(const QString & key) const;
/** Callback used to render a template section with the given @p key.
* @p renderer will substitute the original section tag with the result of eval().
*
* The default implementation returns an empty string.
*/
virtual QString eval(const QString & key, const QString & _template, Renderer *renderer);
private:
PartialResolver *m_partialResolver;
};
/** A context implementation which wraps a QVariantHash or QVariantMap. */
class QTCREATOR_UTILS_EXPORT QtVariantContext : public Context {
public:
/** Construct a QtVariantContext which wraps a dictionary in a QVariantHash
* or a QVariantMap.
*/
#if __cplusplus >= 201103L
typedef std::function<QString(const QString &, Mustache::Renderer *, Mustache::Context *)> fn_t;
#else
typedef QString (*fn_t)(const QString &, Mustache::Renderer *, Mustache::Context *);
#endif
explicit QtVariantContext(const QVariant & root, PartialResolver *resolver = 0);
virtual QString stringValue(const QString & key) const;
virtual bool isFalse(const QString & key) const;
virtual int listCount(const QString & key) const;
virtual void push(const QString & key, int index = -1);
virtual void pop();
virtual bool canEval(const QString & key) const;
virtual QString eval(const QString & key, const QString & _template, Mustache::Renderer *renderer);
private:
QVariant value(const QString & key) const;
QStack<QVariant> m_contextStack;
};
/** Interface for fetching template partials. */
class QTCREATOR_UTILS_EXPORT PartialResolver {
public:
virtual ~PartialResolver() {}
/** Returns the partial template with a given @p name. */
virtual QString getPartial(const QString & name) = 0;
};
/** A simple partial fetcher which returns templates from a map of (partial name -> template)
*/
class QTCREATOR_UTILS_EXPORT PartialMap : public PartialResolver {
public:
explicit PartialMap(const QHash<QString, QString> & partials);
virtual QString getPartial(const QString & name);
private:
QHash<QString, QString> m_partials;
};
/** A partial fetcher when loads templates from '<name>.mustache' files
* in a given directory.
*
* Once a partial has been loaded, it is cached for future use.
*/
class QTCREATOR_UTILS_EXPORT PartialFileLoader : public PartialResolver {
public:
explicit PartialFileLoader(const QString & basePath);
virtual QString getPartial(const QString & name);
private:
QString m_basePath;
QHash<QString, QString> m_cache;
};
/** Holds properties of a tag in a mustache template. */
struct Tag {
enum Type {
Null,
Value, /// A {{key}} or {{{key}}} tag
SectionStart, /// A {{#section}} tag
InvertedSectionStart, /// An {{^inverted-section}} tag
SectionEnd, /// A {{/section}} tag
Partial, /// A {{^partial}} tag
Comment, /// A {{! comment }} tag
SetDelimiter /// A {{=<% %>=}} tag
};
enum EscapeMode {
Escape,
Unescape,
Raw
};
Tag()
: type(Null)
, start(0)
, end(0)
, escapeMode(Escape)
{}
Type type;
QString key;
int start;
int end;
EscapeMode escapeMode;
};
/** Renders Mustache templates, replacing mustache tags with
* values from a provided context.
*/
class QTCREATOR_UTILS_EXPORT Renderer {
public:
Renderer();
/** Render a Mustache template, using @p context to fetch
* the values used to replace Mustache tags.
*/
QString render(const QString & _template, Context *context);
/** Returns a message describing the last error encountered by the previous
* render() call.
*/
QString error() const;
/** Returns the position in the template where the last error occurred
* when rendering the template or -1 if no error occurred.
*
* If the error occurred in a partial template, the returned position is the offset
* in the partial template.
*/
int errorPos() const;
/** Returns the name of the partial where the error occurred, or an empty string
* if the error occurred in the main template.
*/
QString errorPartial() const;
/** Sets the default tag start and end markers.
* This can be overridden within a template.
*/
void setTagMarkers(const QString & startMarker, const QString & endMarker);
private:
QString render(const QString & _template, int startPos, int endPos, Context *context);
Tag findTag(const QString & content, int pos, int endPos);
Tag findEndTag(const QString & content, const Tag & startTag, int endPos);
void setError(const QString & error, int pos);
void readSetDelimiter(const QString & content, int pos, int endPos);
static QString readTagName(const QString & content, int pos, int endPos);
/** Expands @p tag to fill the line, but only if it is standalone.
*
* The start position is moved to the beginning of the line. The end position is
* moved to one past the end of the line. If @p tag is not standalone, it is
* left unmodified.
*
* A tag is standalone if it is the only non-whitespace token on the the line.
*/
static void expandTag(Tag & tag, const QString & content);
QStack<QString> m_partialStack;
QString m_error;
int m_errorPos;
QString m_errorPartial;
QString m_tagStartMarker;
QString m_tagEndMarker;
QString m_defaultTagStartMarker;
QString m_defaultTagEndMarker;
};
/** A convenience function which renders a template using the given data. */
QString renderTemplate(const QString & templateString, const QVariantHash & args);
};
Q_DECLARE_METATYPE(Mustache::QtVariantContext::fn_t)

View File

@ -56,7 +56,8 @@ SOURCES += reloadpromptutils.cpp \
svgimageprovider.cpp \
hostosinfo.cpp \
logfile.cpp \
crc.cpp
crc.cpp \
mustache.cpp
SOURCES += xmlconfig.cpp
@ -115,7 +116,8 @@ HEADERS += utils_global.h \
svgimageprovider.h \
hostosinfo.h \
logfile.h \
crc.h
crc.h \
mustache.h
HEADERS += xmlconfig.h