Skip to content

Commit f63df18

Browse files
committed
feat: mustache specs
1 parent d10a921 commit f63df18

File tree

13 files changed

+2398
-28
lines changed

13 files changed

+2398
-28
lines changed

include/mrdox/Support/Handlebars.hpp

+3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ struct HandlebarsOptions
7777
7878
When enabled, fields will be looked up recursively in objects
7979
and arrays.
80+
81+
This mode should be used to enable complete compatibility
82+
with Mustache templates.
8083
*/
8184
bool compat = false;
8285

src/lib/Support/Handlebars.cpp

+81-9
Original file line numberDiff line numberDiff line change
@@ -1203,11 +1203,13 @@ parseTag(
12031203
// ==============================================================
12041204
if (tagStr.starts_with('^')) {
12051205
t.type = '^';
1206+
t.type2 = '^';
12061207
tagStr.remove_prefix(1);
12071208
tagStr = trim_spaces(tagStr);
12081209
t.content = tagStr;
12091210
} else if (tagStr.starts_with("else")) {
12101211
t.type = '^';
1212+
t.type2 = 'e';
12111213
tagStr.remove_prefix(4);
12121214
tagStr = trim_spaces(tagStr);
12131215
t.content = tagStr;
@@ -1273,10 +1275,10 @@ parseTag(
12731275
// ==============================================================
12741276
// Check if tag is standalone
12751277
// ==============================================================
1276-
static constexpr std::array<char, 5> block_tag_types({'#', '^', '/', '>', '*'});
1277-
bool const isBlock = std::ranges::find(
1278-
block_tag_types, t.type) != block_tag_types.end();
1279-
if (isBlock)
1278+
static constexpr std::array<char, 6> standalone_tag_types({'#', '^', '/', '>', '*', '!'});
1279+
bool const checkStandalone = std::ranges::find(
1280+
standalone_tag_types, t.type) != standalone_tag_types.end();
1281+
if (checkStandalone)
12801282
{
12811283
MRDOX_ASSERT(t.buffer.data() >= context.data());
12821284
MRDOX_ASSERT(t.buffer.data() + t.buffer.size() <= context.data() + context.size());
@@ -1285,13 +1287,22 @@ parseTag(
12851287
std::string_view beforeTag = context.substr(
12861288
0, t.buffer.data() - context.data());
12871289
auto posL = beforeTag.find_last_not_of(' ');
1288-
bool const isStandaloneL =
1290+
bool isStandaloneL =
12891291
posL == std::string_view::npos || beforeTag[posL] == '\n';
1292+
if (!isStandaloneL && posL != 0)
1293+
{
1294+
isStandaloneL = beforeTag[posL - 1] == '\r' && beforeTag[posL] == '\n';
1295+
}
12901296
std::string_view afterTag = context.substr(
12911297
t.buffer.data() + t.buffer.size() - context.data());
12921298
auto posR = afterTag.find_first_not_of(' ');
1293-
bool const isStandaloneR =
1299+
bool isStandaloneR =
12941300
posR == std::string_view::npos || afterTag[posR] == '\n';
1301+
if (!isStandaloneR && posR != afterTag.size() - 1)
1302+
{
1303+
isStandaloneR = afterTag[posR] == '\r' && afterTag[posR + 1] == '\n';
1304+
}
1305+
12951306
t.isStandalone = isStandaloneL && isStandaloneR;
12961307

12971308
// Get standalone indent
@@ -1359,7 +1370,7 @@ render_to(
13591370
}
13601371
else if (!opt.ignoreStandalone && tag.isStandalone)
13611372
{
1362-
if (tag.type == '#' || tag.type == '^' || tag.type == '/')
1373+
if (tag.type == '#' || tag.type == '^' || tag.type == '/' || tag.type == '!')
13631374
{
13641375
beforeTag = trim_rdelimiters(beforeTag, " ");
13651376
}
@@ -1384,7 +1395,7 @@ render_to(
13841395
// ==============================================================
13851396
// Advance template text
13861397
// ==============================================================
1387-
if (tag.removeRWhitespace)
1398+
if (tag.removeRWhitespace && tag.type != '#')
13881399
{
13891400
state.templateText = trim_lspaces(state.templateText);
13901401
}
@@ -1829,6 +1840,32 @@ evalExpr(
18291840
// ==============================================================
18301841
if (opt.compat)
18311842
{
1843+
// Dotted names should be resolved against former resolutions
1844+
bool isDotted = isPathedValue;
1845+
std::string_view firstSeg;
1846+
if (!isDotted)
1847+
{
1848+
std::string_view expression0 = expression;
1849+
firstSeg = popFirstSegment(expression);
1850+
isDotted = !expression.empty();
1851+
expression = expression0;
1852+
}
1853+
1854+
if (isDotted)
1855+
{
1856+
if (context.kind() == dom::Kind::Object)
1857+
{
1858+
// Context has first segment of dotted object.
1859+
// -> Context has priority even if result is undefined.
1860+
auto& obj = context.getObject();
1861+
if (obj.exists(firstSeg))
1862+
{
1863+
return {r, false, false};
1864+
}
1865+
}
1866+
}
1867+
1868+
// Find in parent contexts
18321869
auto parentContexts = std::ranges::views::reverse(state.parentContext);
18331870
for (auto parentContext: parentContexts)
18341871
{
@@ -1926,6 +1963,10 @@ parseBlock(
19261963
{
19271964
fnBlock.remove_prefix(1);
19281965
}
1966+
else if (fnBlock.starts_with("\r\n"))
1967+
{
1968+
fnBlock.remove_prefix(2);
1969+
}
19291970
}
19301971

19311972
// ==============================================================
@@ -1956,7 +1997,14 @@ parseBlock(
19561997
// Update section level
19571998
// ==============================================================
19581999
if (!tag.rawBlock) {
1959-
if (curTag.type == '#' || curTag.type2 == '#') {
2000+
bool isRegularBlock = curTag.type == '#' || curTag.type2 == '#';
2001+
// Nested invert blocks are blocks considered inside the current
2002+
// block rather than a new "else" block.
2003+
// {{^bool}}A{{^bool}}B{{/bool}}C{{/bool}} -> nested
2004+
// {{^bool}}A{{else if bool}}B{{/bool}} -> not nested
2005+
bool isNestedInvert =
2006+
curTag.type == '^' && curTag.type2 == '^' && !curTag.content.empty();
2007+
if (isRegularBlock || isNestedInvert) {
19602008
// Opening a child section tag
19612009
++l;
19622010
} else if (curTag.type == '/') {
@@ -2084,6 +2132,10 @@ parseBlock(
20842132
{
20852133
templateText.remove_prefix(1);
20862134
}
2135+
else if (templateText.starts_with("\r\n"))
2136+
{
2137+
templateText.remove_prefix(2);
2138+
}
20872139
}
20882140

20892141
return true;
@@ -2115,6 +2167,22 @@ renderTag(
21152167
{
21162168
renderExpression(tag, out, context, opt, state);
21172169
}
2170+
else if ('!' == tag.type)
2171+
{
2172+
// Remove standalone whitespace
2173+
if (!opt.ignoreStandalone && tag.isStandalone)
2174+
{
2175+
state.templateText = trim_ldelimiters(state.templateText, " ");
2176+
if (state.templateText.starts_with('\n'))
2177+
{
2178+
state.templateText.remove_prefix(1);
2179+
}
2180+
else if (state.templateText.starts_with("\r\n"))
2181+
{
2182+
state.templateText.remove_prefix(2);
2183+
}
2184+
}
2185+
}
21182186
}
21192187

21202188
void
@@ -2623,6 +2691,10 @@ renderPartial(
26232691
{
26242692
state.templateText.remove_prefix(1);
26252693
}
2694+
else if (state.templateText.starts_with("\r\n"))
2695+
{
2696+
state.templateText.remove_prefix(2);
2697+
}
26262698
}
26272699
}
26282700

src/test/lib/Support/Handlebars.cpp

+174
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#include <mrdox/Support/Handlebars.hpp>
1414
#include <mrdox/Support/Path.hpp>
1515
#include <mrdox/Support/String.hpp>
16+
#include <llvm/Support/JSON.h>
17+
#include <llvm/Support/MemoryBuffer.h>
18+
#include <filesystem>
1619

1720
namespace clang {
1821
namespace mrdox {
@@ -1282,6 +1285,14 @@ whitespace_control()
12821285
ctx.set("foo", "bar");
12831286
BOOST_TEST(hbs.render(" {{~foo~}} {{foo}} {{foo}} ", ctx) == "barbar bar ");
12841287
}
1288+
1289+
// remove block right whitespace
1290+
{
1291+
std::string string = "{{#unless z ~}}\na\n{{~/unless}}\nb";
1292+
BOOST_TEST(hbs.render(string) == "ab");
1293+
string = "{{#unless z ~}}\na\n{{~/unless}}\n\nb";
1294+
BOOST_TEST(hbs.render(string) == "a\nb");
1295+
}
12851296
}
12861297

12871298
void
@@ -5218,10 +5229,173 @@ utils()
52185229
}
52195230
}
52205231

5232+
static
5233+
dom::Value
5234+
to_dom(llvm::json::Value& val)
5235+
{
5236+
dom::Value res;
5237+
// val is llvm::json::Object
5238+
llvm::json::Object* obj_ptr = val.getAsObject();
5239+
if (obj_ptr)
5240+
{
5241+
dom::Object obj;
5242+
auto it = obj_ptr->begin();
5243+
while (it != obj_ptr->end())
5244+
{
5245+
obj.set(it->first.str(), to_dom(it->second));
5246+
++it;
5247+
}
5248+
res = obj;
5249+
return res;
5250+
}
5251+
5252+
// val is array
5253+
llvm::json::Array* arr_ptr = val.getAsArray();
5254+
if (arr_ptr)
5255+
{
5256+
dom::Array arr;
5257+
for (auto& item: *arr_ptr)
5258+
{
5259+
arr.emplace_back(to_dom(item));
5260+
}
5261+
res = arr;
5262+
return res;
5263+
}
5264+
5265+
// val is string
5266+
std::optional<llvm::StringRef> str_opt = val.getAsString();
5267+
if (str_opt) {
5268+
return dom::Value(str_opt.value().str());
5269+
}
5270+
5271+
// val is integer
5272+
std::optional<std::int64_t> int_opt = val.getAsInteger();
5273+
if (int_opt) {
5274+
return dom::Value(int_opt.value());
5275+
}
5276+
5277+
// val is double (convert to string)
5278+
std::optional<double> num_opt = val.getAsNumber();
5279+
if (num_opt) {
5280+
std::string double_str = std::to_string(num_opt.value());
5281+
double_str.erase(double_str.find_last_not_of('0') + 1, std::string::npos);
5282+
return dom::Value(double_str);
5283+
}
5284+
5285+
// val is bool
5286+
std::optional<bool> bool_opt = val.getAsBoolean();
5287+
if (bool_opt) {
5288+
return dom::Value(bool_opt.value());
5289+
}
5290+
5291+
return res;
5292+
};
5293+
52215294
void
52225295
mustache_compat_spec()
52235296
{
52245297
// https://github.com/handlebars-lang/handlebars.js/blob/4.x/spec/spec.js
5298+
std::string_view mustache_specs_dir =
5299+
MRDOX_TEST_FILES_DIR "/handlebars/mustache/";
5300+
std::vector<std::string> spec_files;
5301+
for (auto& p: std::filesystem::directory_iterator(mustache_specs_dir))
5302+
{
5303+
if (p.is_regular_file())
5304+
{
5305+
spec_files.emplace_back(p.path().filename().string());
5306+
}
5307+
}
5308+
5309+
for (auto spec_file: spec_files) {
5310+
// Skip mustache extensions (handlebars knowingly deviates from these)
5311+
if (spec_file.starts_with('~'))
5312+
{
5313+
continue;
5314+
}
5315+
5316+
// Load JSON file
5317+
std::string spec_path = std::string(mustache_specs_dir) + std::string(spec_file);
5318+
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> FileOrErr =
5319+
llvm::MemoryBuffer::getFile(spec_path, true);
5320+
BOOST_TEST(FileOrErr);
5321+
std::unique_ptr<llvm::MemoryBuffer> Buffer = std::move(*FileOrErr);
5322+
5323+
// Parse the JSON content
5324+
llvm::Expected<llvm::json::Value> jsonObj =
5325+
llvm::json::parse(Buffer->getBuffer());
5326+
BOOST_TEST(jsonObj);
5327+
llvm::json::Value jsonData = std::move(*jsonObj);
5328+
BOOST_TEST(jsonData.getAsObject());
5329+
llvm::json::Object data = std::move(*jsonData.getAsObject());
5330+
5331+
// Iterate tests
5332+
llvm::json::Array tests = std::move(*data.get("tests")->getAsArray());
5333+
for (auto testPtr: tests) {
5334+
llvm::json::Object test = *testPtr.getAsObject();
5335+
// Skip invalid partial tests
5336+
llvm::StringRef test_name =
5337+
*test.get("name")->getAsString();
5338+
if (
5339+
// Handlebars throws if partials are not found
5340+
(spec_file == "partials.json" && test_name == "Failed Lookup") ||
5341+
// Handlebars nests the entire response from partials, not just the literals
5342+
(spec_file == "partials.json" && test_name == "Standalone Indentation"))
5343+
{
5344+
continue;
5345+
}
5346+
5347+
// Get template
5348+
std::string template_str =test.get("template")->getAsString()->str();
5349+
if (template_str.find("{{=") != std::string::npos)
5350+
{
5351+
// "{{=" not supported by handlebars
5352+
continue;
5353+
}
5354+
5355+
// Get partials
5356+
std::vector<std::pair<std::string, std::string>> partials;
5357+
llvm::json::Value* partialsPtr = test.get("partials");
5358+
llvm::json::Object* partialsObj = partialsPtr ? partialsPtr->getAsObject() : nullptr;
5359+
if (partialsObj)
5360+
{
5361+
auto it = partialsObj->begin();
5362+
bool incompatiblePartial = false;
5363+
while (it != partialsObj->end())
5364+
{
5365+
llvm::StringRef partial_string = *it->second.getAsString();
5366+
if (partial_string.find("{{=") != llvm::StringRef::npos)
5367+
{
5368+
// "{{=" not supported by handlebars
5369+
incompatiblePartial = true;
5370+
break;
5371+
}
5372+
else
5373+
{
5374+
partials.emplace_back(it->first.str(), partial_string.str());
5375+
}
5376+
++it;
5377+
}
5378+
if (incompatiblePartial) {
5379+
continue;
5380+
}
5381+
}
5382+
5383+
// Render
5384+
Handlebars hbs;
5385+
for (auto [name, partial]: partials)
5386+
{
5387+
hbs.registerPartial(name, partial);
5388+
}
5389+
dom::Value context = to_dom(*test.get("data"));
5390+
HandlebarsOptions opt;
5391+
opt.compat = true;
5392+
std::string expected = test.get("expected")->getAsString().value().str();
5393+
std::string rendered = hbs.render(template_str, context, opt);
5394+
if (!BOOST_TEST(rendered == expected)) {
5395+
return;
5396+
}
5397+
}
5398+
}
52255399
}
52265400

52275401
void

0 commit comments

Comments
 (0)