2 Copyright 2012, Robert Knight
4 Redistribution and use in source and binary forms, with or without modification,
5 are permitted provided that the following conditions are met:
7 Redistributions of source code must retain the above copyright notice,
8 this list of conditions and the following disclaimer.
10 Redistributions in binary form must reproduce the above copyright notice,
11 this list of conditions and the following disclaimer in the documentation
12 and/or other materials provided with the distribution.
17 #include <QtCore/QDebug>
18 #include <QtCore/QFile>
19 #include <QtCore/QStringList>
20 #include <QtCore/QTextStream>
22 using namespace Mustache
;
24 QString
Mustache::renderTemplate(const QString
& templateString
, const QVariantHash
& args
)
26 Mustache::QtVariantContext
context(args
);
27 Mustache::Renderer renderer
;
29 return renderer
.render(templateString
, &context
);
32 QString
escapeHtml(const QString
& input
)
34 QString
escaped(input
);
36 for (int i
= 0; i
< escaped
.count();) {
37 const char *replacement
= 0;
38 ushort ch
= escaped
.at(i
).unicode();
40 replacement
= "&";
41 } else if (ch
== '<') {
43 } else if (ch
== '>') {
45 } else if (ch
== '"') {
46 replacement
= """;
49 escaped
.replace(i
, 1, QLatin1String(replacement
));
50 i
+= strlen(replacement
);
58 QString
unescapeHtml(const QString
& escaped
)
60 QString
unescaped(escaped
);
62 unescaped
.replace(QLatin1String("<"), QLatin1String("<"));
63 unescaped
.replace(QLatin1String(">"), QLatin1String(">"));
64 unescaped
.replace(QLatin1String("&"), QLatin1String("&"));
65 unescaped
.replace(QLatin1String("""), QLatin1String("\""));
69 Context::Context(PartialResolver
*resolver
)
70 : m_partialResolver(resolver
)
73 PartialResolver
*Context::partialResolver() const
75 return m_partialResolver
;
78 QString
Context::partialValue(const QString
& key
) const
80 if (!m_partialResolver
) {
83 return m_partialResolver
->getPartial(key
);
86 bool Context::canEval(const QString
&) const
91 QString
Context::eval(const QString
& key
, const QString
& _template
, Renderer
*renderer
)
100 QtVariantContext::QtVariantContext(const QVariant
& root
, PartialResolver
*resolver
)
103 m_contextStack
<< root
;
106 QVariant
variantMapValue(const QVariant
& value
, const QString
& key
)
108 if (value
.userType() == QVariant::Map
) {
109 return value
.toMap().value(key
);
111 return value
.toHash().value(key
);
115 QVariant
variantMapValueForKeyPath(const QVariant
& value
, const QStringList keyPath
)
117 if (keyPath
.count() > 1) {
118 QVariant firstValue
= variantMapValue(value
, keyPath
.first());
119 return firstValue
.isNull() ? QVariant() : variantMapValueForKeyPath(firstValue
, keyPath
.mid(1));
120 } else if (!keyPath
.isEmpty()) {
121 return variantMapValue(value
, keyPath
.first());
126 QVariant
QtVariantContext::value(const QString
& key
) const
128 if (key
== "." && !m_contextStack
.isEmpty()) {
129 return m_contextStack
.last();
131 QStringList keyPath
= key
.split(".");
132 for (int i
= m_contextStack
.count() - 1; i
>= 0; i
--) {
133 QVariant value
= variantMapValueForKeyPath(m_contextStack
.at(i
), keyPath
);
134 if (!value
.isNull()) {
141 bool QtVariantContext::isFalse(const QString
& key
) const
143 QVariant value
= this->value(key
);
145 switch (value
.userType()) {
147 return !value
.toBool();
150 return value
.toList().isEmpty();
153 return value
.toHash().isEmpty();
156 return value
.toMap().isEmpty();
159 return value
.toString().isEmpty();
163 QString
QtVariantContext::stringValue(const QString
& key
) const
168 return value(key
).toString();
171 void QtVariantContext::push(const QString
& key
, int index
)
173 QVariant mapItem
= value(key
);
176 m_contextStack
<< mapItem
;
178 QVariantList list
= mapItem
.toList();
179 m_contextStack
<< list
.value(index
, QVariant());
183 void QtVariantContext::pop()
185 m_contextStack
.pop();
188 int QtVariantContext::listCount(const QString
& key
) const
190 if (value(key
).userType() == QVariant::List
) {
191 return value(key
).toList().count();
196 bool QtVariantContext::canEval(const QString
& key
) const
198 return value(key
).canConvert
<fn_t
>();
201 QString
QtVariantContext::eval(const QString
& key
, const QString
& _template
, Renderer
*renderer
)
203 QVariant fn
= value(key
);
208 return fn
.value
<fn_t
>() (_template
, renderer
, this);
211 PartialMap::PartialMap(const QHash
<QString
, QString
> & partials
)
212 : m_partials(partials
)
215 QString
PartialMap::getPartial(const QString
& name
)
217 return m_partials
.value(name
);
220 PartialFileLoader::PartialFileLoader(const QString
& basePath
)
221 : m_basePath(basePath
)
224 QString
PartialFileLoader::getPartial(const QString
& name
)
226 if (!m_cache
.contains(name
)) {
227 QString path
= m_basePath
+ '/' + name
+ ".mustache";
229 if (file
.open(QIODevice::ReadOnly
)) {
230 QTextStream
stream(&file
);
231 m_cache
.insert(name
, stream
.readAll());
234 return m_cache
.value(name
);
239 , m_defaultTagStartMarker("{{")
240 , m_defaultTagEndMarker("}}")
243 QString
Renderer::error() const
248 int Renderer::errorPos() const
253 QString
Renderer::errorPartial() const
255 return m_errorPartial
;
258 QString
Renderer::render(const QString
& _template
, Context
*context
)
262 m_errorPartial
.clear();
264 m_tagStartMarker
= m_defaultTagStartMarker
;
265 m_tagEndMarker
= m_defaultTagEndMarker
;
267 return render(_template
, 0, _template
.length(), context
);
270 QString
Renderer::render(const QString
& _template
, int startPos
, int endPos
, Context
*context
)
273 int lastTagEnd
= startPos
;
275 while (m_errorPos
== -1) {
276 Tag tag
= findTag(_template
, lastTagEnd
, endPos
);
277 if (tag
.type
== Tag::Null
) {
278 output
+= _template
.midRef(lastTagEnd
, endPos
- lastTagEnd
);
281 output
+= _template
.midRef(lastTagEnd
, tag
.start
- lastTagEnd
);
285 QString value
= context
->stringValue(tag
.key
);
286 if (tag
.escapeMode
== Tag::Escape
) {
287 value
= escapeHtml(value
);
288 } else if (tag
.escapeMode
== Tag::Unescape
) {
289 value
= unescapeHtml(value
);
292 lastTagEnd
= tag
.end
;
295 case Tag::SectionStart
:
297 Tag endTag
= findEndTag(_template
, tag
, endPos
);
298 if (endTag
.type
== Tag::Null
) {
299 if (m_errorPos
== -1) {
300 setError("No matching end tag found for section", tag
.start
);
303 int listCount
= context
->listCount(tag
.key
);
305 for (int i
= 0; i
< listCount
; i
++) {
306 context
->push(tag
.key
, i
);
307 output
+= render(_template
, tag
.end
, endTag
.start
, context
);
310 } else if (context
->canEval(tag
.key
)) {
311 output
+= context
->eval(tag
.key
, _template
.mid(tag
.end
, endTag
.start
- tag
.end
), this);
312 } else if (!context
->isFalse(tag
.key
)) {
313 context
->push(tag
.key
);
314 output
+= render(_template
, tag
.end
, endTag
.start
, context
);
317 lastTagEnd
= endTag
.end
;
321 case Tag::InvertedSectionStart
:
323 Tag endTag
= findEndTag(_template
, tag
, endPos
);
324 if (endTag
.type
== Tag::Null
) {
325 if (m_errorPos
== -1) {
326 setError("No matching end tag found for inverted section", tag
.start
);
329 if (context
->isFalse(tag
.key
)) {
330 output
+= render(_template
, tag
.end
, endTag
.start
, context
);
332 lastTagEnd
= endTag
.end
;
336 case Tag::SectionEnd
:
337 setError("Unexpected end tag", tag
.start
);
338 lastTagEnd
= tag
.end
;
342 QString tagStartMarker
= m_tagStartMarker
;
343 QString tagEndMarker
= m_tagEndMarker
;
345 m_tagStartMarker
= m_defaultTagStartMarker
;
346 m_tagEndMarker
= m_defaultTagEndMarker
;
348 m_partialStack
.push(tag
.key
);
350 QString partial
= context
->partialValue(tag
.key
);
351 output
+= render(partial
, 0, partial
.length(), context
);
352 lastTagEnd
= tag
.end
;
354 m_partialStack
.pop();
356 m_tagStartMarker
= tagStartMarker
;
357 m_tagEndMarker
= tagEndMarker
;
360 case Tag::SetDelimiter
:
361 lastTagEnd
= tag
.end
;
364 lastTagEnd
= tag
.end
;
374 void Renderer::setError(const QString
& error
, int pos
)
376 Q_ASSERT(!error
.isEmpty());
382 if (!m_partialStack
.isEmpty()) {
383 m_errorPartial
= m_partialStack
.top();
387 Tag
Renderer::findTag(const QString
& content
, int pos
, int endPos
)
389 int tagStartPos
= content
.indexOf(m_tagStartMarker
, pos
);
391 if (tagStartPos
== -1 || tagStartPos
>= endPos
) {
395 int tagEndPos
= content
.indexOf(m_tagEndMarker
, tagStartPos
+ m_tagStartMarker
.length());
396 if (tagEndPos
== -1) {
399 tagEndPos
+= m_tagEndMarker
.length();
402 tag
.type
= Tag::Value
;
403 tag
.start
= tagStartPos
;
406 pos
= tagStartPos
+ m_tagStartMarker
.length();
407 endPos
= tagEndPos
- m_tagEndMarker
.length();
409 QChar typeChar
= content
.at(pos
);
411 if (typeChar
== '#') {
412 tag
.type
= Tag::SectionStart
;
413 tag
.key
= readTagName(content
, pos
+ 1, endPos
);
414 } else if (typeChar
== '^') {
415 tag
.type
= Tag::InvertedSectionStart
;
416 tag
.key
= readTagName(content
, pos
+ 1, endPos
);
417 } else if (typeChar
== '/') {
418 tag
.type
= Tag::SectionEnd
;
419 tag
.key
= readTagName(content
, pos
+ 1, endPos
);
420 } else if (typeChar
== '!') {
421 tag
.type
= Tag::Comment
;
422 } else if (typeChar
== '>') {
423 tag
.type
= Tag::Partial
;
424 tag
.key
= readTagName(content
, pos
+ 1, endPos
);
425 } else if (typeChar
== '=') {
426 tag
.type
= Tag::SetDelimiter
;
427 readSetDelimiter(content
, pos
+ 1, tagEndPos
- m_tagEndMarker
.length());
429 if (typeChar
== '&') {
430 tag
.escapeMode
= Tag::Unescape
;
432 } else if (typeChar
== '{') {
433 tag
.escapeMode
= Tag::Raw
;
435 int endTache
= content
.indexOf('}', pos
);
436 if (endTache
== tag
.end
- m_tagEndMarker
.length()) {
442 tag
.type
= Tag::Value
;
443 tag
.key
= readTagName(content
, pos
, endPos
);
446 if (tag
.type
!= Tag::Value
) {
447 expandTag(tag
, content
);
453 QString
Renderer::readTagName(const QString
& content
, int pos
, int endPos
)
457 name
.reserve(endPos
- pos
);
458 while (content
.at(pos
).isSpace()) {
461 while (!content
.at(pos
).isSpace() && pos
< endPos
) {
462 name
+= content
.at(pos
);
468 void Renderer::readSetDelimiter(const QString
& content
, int pos
, int endPos
)
473 while (content
.at(pos
).isSpace() && pos
< endPos
) {
477 while (!content
.at(pos
).isSpace() && pos
< endPos
) {
478 if (content
.at(pos
) == '=') {
479 setError("Custom delimiters may not contain '='.", pos
);
482 startMarker
+= content
.at(pos
);
486 while (content
.at(pos
).isSpace() && pos
< endPos
) {
490 while (!content
.at(pos
).isSpace() && pos
< endPos
- 1) {
491 if (content
.at(pos
) == '=') {
492 setError("Custom delimiters may not contain '='.", pos
);
495 endMarker
+= content
.at(pos
);
499 m_tagStartMarker
= startMarker
;
500 m_tagEndMarker
= endMarker
;
503 Tag
Renderer::findEndTag(const QString
& content
, const Tag
& startTag
, int endPos
)
506 int pos
= startTag
.end
;
509 Tag nextTag
= findTag(content
, pos
, endPos
);
510 if (nextTag
.type
== Tag::Null
) {
512 } else if (nextTag
.type
== Tag::SectionStart
|| nextTag
.type
== Tag::InvertedSectionStart
) {
514 } else if (nextTag
.type
== Tag::SectionEnd
) {
517 if (nextTag
.key
!= startTag
.key
) {
518 setError("Tag start/end key mismatch", nextTag
.start
);
530 void Renderer::setTagMarkers(const QString
& startMarker
, const QString
& endMarker
)
532 m_defaultTagStartMarker
= startMarker
;
533 m_defaultTagEndMarker
= endMarker
;
536 void Renderer::expandTag(Tag
& tag
, const QString
& content
)
538 int start
= tag
.start
;
541 // Move start to beginning of line.
542 while (start
> 0 && content
.at(start
- 1) != QLatin1Char('\n')) {
544 if (!content
.at(start
).isSpace()) {
545 return; // Not standalone.
549 // Move end to one past end of line.
550 while (end
<= content
.size() && content
.at(end
- 1) != QLatin1Char('\n')) {
551 if (end
< content
.size() && !content
.at(end
).isSpace()) {
552 return; // Not standalone.