Skip to content

Commit deaa47c

Browse files
committed
feat: strict specs
1 parent 53273a0 commit deaa47c

File tree

6 files changed

+301
-64
lines changed

6 files changed

+301
-64
lines changed

include/mrdox/Support/Handlebars.hpp

+12
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,20 @@ struct HandlebarsOptions
3838
bool noEscape = false;
3939

4040
/** Templates will throw rather than ignore missing fields
41+
42+
Run in strict mode. In this mode, templates will throw rather than
43+
silently ignore missing fields.
44+
4145
*/
4246
bool strict = false;
4347

48+
/** Removes object existence checks when traversing paths
49+
50+
This is a subset of strict mode that generates optimized
51+
templates when the data inputs are known to be safe.
52+
*/
53+
bool assumeObjects = false;
54+
4455
/** Disable the auto-indent feature
4556
4657
By default, an indented partial-call causes the output of the
@@ -333,6 +344,7 @@ class MRDOX_DECL HandlebarsCallback
333344
std::vector<std::string_view> blockParamIds_;
334345
std::function<void(dom::Value, dom::Array const&)> const* logger_;
335346
detail::RenderState* renderState_;
347+
HandlebarsOptions const* opt_;
336348
friend class Handlebars;
337349

338350
friend

src/lib/Dom/Array.cpp

+8-15
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,17 @@ toString(
5454
Array const& arr)
5555
{
5656
if(arr.empty())
57-
return "[]";
58-
std::string s = "[";
59-
{
60-
auto const n = arr.size();
61-
auto insert = std::back_inserter(s);
62-
for(std::size_t i = 0; i < n; ++i)
57+
return "";
58+
std::string s;
59+
auto const n = arr.size();
60+
if (n != 0) {
61+
s += toString(arr.at(0));
62+
for(std::size_t i = 1; i < n; ++i)
6363
{
64-
if(i != 0)
65-
fmt::format_to(insert,
66-
", {}",
67-
toString(arr.at(i)));
68-
else
69-
fmt::format_to(insert,
70-
" {}",
71-
toStringChild(arr.at(i)));
64+
s += ',';
65+
s += toString(arr.at(i));
7266
}
7367
}
74-
s += " ]";
7568
return s;
7669
}
7770

src/lib/Dom/Object.cpp

+3-20
Original file line numberDiff line numberDiff line change
@@ -89,26 +89,9 @@ exists(std::string_view key) const
8989
}
9090

9191
std::string
92-
toString(
93-
Object const& obj)
94-
{
95-
if(obj.empty())
96-
return "{}";
97-
std::string s = "{";
98-
{
99-
auto insert = std::back_inserter(s);
100-
for(auto const& kv : RangeFor(obj))
101-
{
102-
if(! kv.first)
103-
s.push_back(',');
104-
fmt::format_to(insert,
105-
" {} : {}",
106-
kv.value.key,
107-
toStringChild(kv.value.value));
108-
}
109-
}
110-
s += " }";
111-
return s;
92+
toString(Object const&)
93+
{
94+
return "[object Object]";
11295
}
11396

11497
//------------------------------------------------

src/lib/Dom/Value.cpp

+1-5
Original file line numberDiff line numberDiff line change
@@ -471,11 +471,7 @@ toStringChild(
471471
return "[]";
472472
}
473473
case Value::Kind::Object:
474-
{
475-
if(! value.obj_.empty())
476-
return "{...}";
477-
return "{}";
478-
}
474+
return "[object Object]";
479475
case Value::Kind::Function:
480476
return "[function]";
481477
default:

src/lib/Support/Handlebars.cpp

+103-21
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ find_position_in_text(
606606
if (res.line == 1)
607607
res.column = res.pos;
608608
else
609-
res.column = res.pos - text.rfind('\n', res.pos);
609+
res.column = res.pos - text.rfind('\n', res.pos) - 1;
610610
}
611611
return res;
612612
}
@@ -648,7 +648,8 @@ std::pair<dom::Value, bool>
648648
lookupPropertyImpl(
649649
dom::Object const& context,
650650
std::string_view path,
651-
detail::RenderState const& state)
651+
detail::RenderState const& state,
652+
HandlebarsOptions const& opt)
652653
{
653654
// Get first value from Object
654655
std::string_view segment = popFirstSegment(path);
@@ -661,7 +662,21 @@ lookupPropertyImpl(
661662
}
662663
else if (!context.exists(literalSegment))
663664
{
664-
return {nullptr, false};
665+
if (opt.strict || (opt.assumeObjects && !path.empty()))
666+
{
667+
std::string msg = fmt::format(
668+
"\"{}\" not defined in {}", literalSegment, toString(context));
669+
auto res = find_position_in_text(state.templateText0, literalSegment);
670+
if (res)
671+
{
672+
throw HandlebarsError(msg, res.line, res.column, res.pos);
673+
}
674+
throw HandlebarsError(msg);
675+
}
676+
else
677+
{
678+
return {nullptr, false};
679+
}
665680
}
666681
else
667682
{
@@ -679,9 +694,27 @@ lookupPropertyImpl(
679694
{
680695
auto obj = cur.getObject();
681696
if (obj.exists(literalSegment))
697+
{
682698
cur = obj.find(literalSegment);
699+
}
683700
else
684-
return {nullptr, false};
701+
{
702+
if (opt.strict)
703+
{
704+
std::string msg = fmt::format(
705+
"\"{}\" not defined in {}", literalSegment, toString(cur));
706+
auto res = find_position_in_text(state.templateText0, literalSegment);
707+
if (res)
708+
{
709+
throw HandlebarsError(msg, res.line, res.column, res.pos);
710+
}
711+
throw HandlebarsError(msg);
712+
}
713+
else
714+
{
715+
return {nullptr, false};
716+
}
717+
}
685718
}
686719
// If current value is an Array, get the next value the stripped index
687720
else if (cur.isArray())
@@ -717,7 +750,9 @@ std::pair<dom::Value, bool>
717750
lookupPropertyImpl(
718751
dom::Value const& context,
719752
std::string_view path,
720-
detail::RenderState const& state) {
753+
detail::RenderState const& state,
754+
HandlebarsOptions const& opt)
755+
{
721756
checkPath(path, state);
722757
// ==============================================================
723758
// "." / "this"
@@ -728,40 +763,52 @@ lookupPropertyImpl(
728763
// Non-object key
729764
// ==============================================================
730765
if (context.kind() != dom::Kind::Object) {
766+
if (opt.strict || opt.assumeObjects)
767+
{
768+
std::string msg = fmt::format("\"{}\" not defined in {}", path, context);
769+
auto res = find_position_in_text(state.templateText0, path);
770+
if (res)
771+
{
772+
throw HandlebarsError(msg, res.line, res.column, res.pos);
773+
}
774+
throw HandlebarsError(msg);
775+
}
731776
return {nullptr, false};
732777
}
733778
// ==============================================================
734779
// Object path
735780
// ==============================================================
736-
return lookupPropertyImpl(context.getObject(), path, state);
781+
return lookupPropertyImpl(context.getObject(), path, state, opt);
737782
}
738783

739784
template <std::convertible_to<std::string_view> S>
740785
std::pair<dom::Value, bool>
741786
lookupPropertyImpl(
742787
dom::Value const& data,
743788
S const& path,
744-
detail::RenderState const& state)
789+
detail::RenderState const& state,
790+
HandlebarsOptions const& opt)
745791
{
746-
return lookupPropertyImpl(data, std::string_view(path), state);
792+
return lookupPropertyImpl(data, std::string_view(path), state, opt);
747793
}
748794

749795
std::pair<dom::Value, bool>
750796
lookupPropertyImpl(
751797
dom::Value const& context,
752798
dom::Value const& path,
753-
detail::RenderState const& state)
799+
detail::RenderState const& state,
800+
HandlebarsOptions const& opt)
754801
{
755802
if (path.isString())
756-
return lookupPropertyImpl(context, path.getString(), state);
803+
return lookupPropertyImpl(context, path.getString(), state, opt);
757804
if (path.isInteger()) {
758805
if (context.isArray()) {
759806
auto& arr = context.getArray();
760807
if (path.getInteger() >= static_cast<std::int64_t>(arr.size()))
761808
return {nullptr, false};
762809
return {arr.at(path.getInteger()), true};
763810
}
764-
return lookupPropertyImpl(context, std::to_string(path.getInteger()), state);
811+
return lookupPropertyImpl(context, std::to_string(path.getInteger()), state, opt);
765812
}
766813
return {nullptr, false};
767814
}
@@ -772,7 +819,7 @@ lookupProperty(
772819
dom::Value const& context,
773820
dom::Value const& path) const
774821
{
775-
return lookupPropertyImpl(context, path, *renderState_);
822+
return lookupPropertyImpl(context, path, *renderState_, *opt_);
776823
}
777824

778825

@@ -1679,12 +1726,15 @@ evalExpr(
16791726
break;
16801727
}
16811728
}
1682-
auto [res, found] = lookupPropertyImpl(data, expression, state);
1729+
auto [res, found] = lookupPropertyImpl(data, expression, state, opt);
16831730
return {res, found, false};
16841731
}
16851732
// ==============================================================
16861733
// Dotdot context path
16871734
// ==============================================================
1735+
HandlebarsOptions noStrict = opt;
1736+
noStrict.strict = false;
1737+
noStrict.assumeObjects = false;
16881738
if (expression.starts_with("..")) {
16891739
// Get value from parent helper contexts
16901740
std::size_t dotdots = 1;
@@ -1704,7 +1754,7 @@ evalExpr(
17041754
}
17051755
dom::Value parentCtx =
17061756
state.parentContext[state.parentContext.size() - dotdots];
1707-
auto [res, found] = lookupPropertyImpl(parentCtx, expression, state);
1757+
auto [res, found] = lookupPropertyImpl(parentCtx, expression, state, noStrict);
17081758
return {res, found, false};
17091759
}
17101760
// ==============================================================
@@ -1730,7 +1780,7 @@ evalExpr(
17301780
bool defined;
17311781
if (isPathedValue)
17321782
{
1733-
std::tie(r, defined) = lookupPropertyImpl(context, expression, state);
1783+
std::tie(r, defined) = lookupPropertyImpl(context, expression, state, noStrict);
17341784
if (defined) {
17351785
return {r, defined, false};
17361786
}
@@ -1739,7 +1789,7 @@ evalExpr(
17391789
// ==============================================================
17401790
// Block values
17411791
// ==============================================================
1742-
std::tie(r, defined) = lookupPropertyImpl(state.blockValues, expression, state);
1792+
std::tie(r, defined) = lookupPropertyImpl(state.blockValues, expression, state, noStrict);
17431793
if (defined)
17441794
{
17451795
return {r, defined, false, false, true};
@@ -1759,7 +1809,10 @@ evalExpr(
17591809
// ==============================================================
17601810
// Context values
17611811
// ==============================================================
1762-
std::tie(r, defined) = lookupPropertyImpl(context, expression, state);
1812+
HandlebarsOptions strictOpt = opt;
1813+
strictOpt.strict = opt.strict && !opt.compat;
1814+
strictOpt.assumeObjects = opt.assumeObjects && !opt.compat;
1815+
std::tie(r, defined) = lookupPropertyImpl(context, expression, state, strictOpt);
17631816
if (defined) {
17641817
return {r, defined, false};
17651818
}
@@ -1772,13 +1825,19 @@ evalExpr(
17721825
auto parentContexts = std::ranges::views::reverse(state.parentContext);
17731826
for (auto parentContext: parentContexts)
17741827
{
1775-
std::tie(r, defined) = lookupPropertyImpl(parentContext, expression, state);
1828+
std::tie(r, defined) = lookupPropertyImpl(parentContext, expression, state, noStrict);
17761829
if (defined)
17771830
{
17781831
return {r, defined, false};
17791832
}
17801833
}
17811834
}
1835+
1836+
if (opt.strict)
1837+
{
1838+
std::string msg = fmt::format("\"{}\" not defined", expression);
1839+
throw HandlebarsError(msg);
1840+
}
17821841
return {nullptr, false, false};
17831842
}
17841843

@@ -2093,7 +2152,9 @@ renderExpression(
20932152
cb.context_ = &context;
20942153
cb.data_ = &state.data;
20952154
cb.logger_ = &logger_;
2096-
setupArgs(tag.arguments, context, state, args, cb, opt);
2155+
HandlebarsOptions noStrict = opt;
2156+
noStrict.strict = false;
2157+
setupArgs(tag.arguments, context, state, args, cb, noStrict);
20972158
auto [res, render] = fn(args, cb);
20982159
if (render == HelperBehavior::RENDER_RESULT) {
20992160
format_to(out, res, opt2);
@@ -2125,7 +2186,9 @@ renderExpression(
21252186
dom::Array args = dom::newArray<dom::DefaultArrayImpl>();
21262187
HandlebarsCallback cb;
21272188
cb.name_ = helper_expr;
2128-
setupArgs(tag.arguments, context, state, args, cb, opt);
2189+
HandlebarsOptions noStrict = opt;
2190+
noStrict.strict = false;
2191+
setupArgs(tag.arguments, context, state, args, cb, noStrict);
21292192
auto v2 = resV.value.getFunction().call(args).value();
21302193
format_to(out, v2, opt2);
21312194
}
@@ -2135,6 +2198,11 @@ renderExpression(
21352198
}
21362199
return;
21372200
}
2201+
else if (opt.strict)
2202+
{
2203+
std::string msg = fmt::format("\"{}\" not defined in {}", helper_expr, toString(context));
2204+
throw HandlebarsError(msg);
2205+
}
21382206

21392207
// ==============================================================
21402208
// helperMissing hook
@@ -2252,6 +2320,7 @@ setupArgs(
22522320
}
22532321
}
22542322
cb.renderState_ = &state;
2323+
cb.opt_ = &opt;
22552324
}
22562325

22572326
void
@@ -2604,7 +2673,20 @@ renderBlock(
26042673
arguments = tag.helper;
26052674
}
26062675
}
2607-
setupArgs(arguments, context, state, args, cb, opt);
2676+
else if (opt.strict && !found)
2677+
{
2678+
std::string msg = fmt::format(
2679+
"\"{}\" not defined in {}", tag.helper, toString(context));
2680+
auto res = find_position_in_text(state.templateText0, tag.helper);
2681+
if (res)
2682+
{
2683+
throw HandlebarsError(msg, res.line, res.column, res.pos);
2684+
}
2685+
throw HandlebarsError(msg);
2686+
}
2687+
HandlebarsOptions noStrict = opt;
2688+
noStrict.strict = opt.strict && emulateMustache;
2689+
setupArgs(arguments, context, state, args, cb, noStrict);
26082690

26092691
// ==============================================================
26102692
// Setup block parameters

0 commit comments

Comments
 (0)