Class: Iodine::Mustache
- Inherits:
-
Object
- Object
- Iodine::Mustache
- Defined in:
- lib/iodine/mustache.rb,
ext/iodine_ext/iodine_mustache.c
Overview
Iodine includes a safe and fast Mustache templating engine.
The engine is simpler and safer to use (and often faster) than the official and feature richer Ruby engine.
Note: Mustache behaves differently than the official Ruby templating engine in a number of ways:
When a partial template can't be found, a
LoadError
exception is raised (the official implementation outputs an empty String).HTML escaping is more agressive, increasing XSS protection. Read why at: https://wonko.com/post/html-escaping .
Partial template padding in Iodine adds padding to dynamic text as well as static text, unlike the official Ruby mustache engine. i.e., if an argument contains a new line marker, the new line will be padded to match the partial template padding.
Lambda support is significantly different. For example, the text returned from a lambda isn't parsed (no lambda interpolation).
Dot notation is tested in whole as well as in part (i.e.
user.name.first
will be tested as is, than the couplet"user","name.first"
and than as each"use","name","first"
), allowing for the Hash data to contain keys with dots while still supporting dot notation shortcuts.Dot notation supports method names (even chained method names) as long as they don't have or require arguments. For example,
user.class.to_s
will behave differently on Iodine (returns call name asString
) than on the official mustache engine (fails / returns empty string).
Iodine Mustache's engine was designed to play best with basic data structures, such as results from the JSON parser and doesn't require any special classes or types.
Hash data is tested for Symbol keys before being tested for String keys and methods. This means that :key
has precedence over "key"
.
Note: Although using methods as "keys" (or argument names) is supported, no Ruby code is evaluated. This means that only trusted (pre-existing) code will execute.
Iodine's Mustache engine performes about 5-7 times faster(!) than the official Ruby mustache engine. Tests performed with Ruby 2.6.0, comparing iodine 0.7.33 against mustache 1.1.0 using a 2.9 GHz Intel Core i9 CPU.
You can benchmark the Iodine Mustache performance and decide if you wish to switch from the official Ruby implementation.
require 'benchmark/ips'
require 'mustache'
require 'iodine'
# Benchmark code was copied, in part, from:
# https://github.com/mustache/mustache/blob/master/benchmarks/render_collection_benchmark.rb
# The test is, sadly, biased and doesn't test for missing elements, proc/method resolution or template partials.
def benchmark_mustache
template = """
{{#products}}
<div class='product_brick'>
<div class='container'>
<div class='element'>
<img src='images/{{image}}' class='product_miniature' />
</div>
<div class='element description'>
<a href={{url}} class='product_name block bold'>
{{external_index}}
</a>
</div>
</div>
</div>
{{/products}}
"""
IO.write "test_template.mustache", template
filename = "test_template.mustache"
data_1000 = {
products: []
}
data_1000_escaped = {
products: []
}
1000.times do
data_1000[:products] << {
:external_index=>"product",
:url=>"/products/7",
:image=>"products/product.jpg"
}
data_1000_escaped[:products] << {
:external_index=>"This <product> should've been \"properly\" escaped.",
:url=>"/products/7",
:image=>"products/product.jpg"
}
end
view = Mustache.new
view.template = template
view.render # Call render once so the template will be compiled
iodine_view = Iodine::Mustache.new(template: template)
Benchmark.ips do |x|
x.report("Ruby Mustache render list of 1000") do |times|
view.render(data_1000)
end
x.report("Iodine::Mustache render list of 1000") do |times|
iodine_view.render(data_1000)
end
x.report("Ruby Mustache render list of 1000 with escaped data") do |times|
view.render(data_1000_escaped)
end
x.report("Iodine::Mustache render list of 1000 with escaped data") do |times|
iodine_view.render(data_1000_escaped)
end
x.report("Ruby Mustache - no caching - render list of 1000") do |times|
tmp = Mustache.new
tmp.template = template
tmp.render(data_1000)
end
x.report("Iodine::Mustache - no caching - render list of 1000") do |times|
Iodine::Mustache.render(nil, data_1000, template)
end
end
nil
end
benchmark_mustache
Class Method Summary collapse
-
.render(*args) ⇒ Object
Renders the mustache template found in
filename
, using the data provided in thedata
argument.
Instance Method Summary collapse
-
#initialize(*args) ⇒ Object
constructor
Loads the mustache template found in
:filename
. -
#render(data) ⇒ Object
Renders the mustache template using the data provided in the
data
argument.
Constructor Details
#initialize(*args) ⇒ Object
Loads the mustache template found in :filename
. If :template
is provided it
will be used instead of reading the file's content.
Iodine::Mustache.new(filename, template = nil)
When template data is provided, filename (if any) will only be used for partial template path resolution and the template data will be used for the template's content. This allows, for example, for front matter to be extracted before parsing the template.
Once a template was loaded, it could be rendered using render.
Accepts named arguments as well:
Iodine::Mustache.new(filename: "foo.mustache", template: "{{ bar }}")
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# File 'ext/iodine_ext/iodine_mustache.c', line 283
static VALUE iodine_mustache_new(int argc, VALUE *argv, VALUE self) {
VALUE filename = Qnil, template = Qnil;
if (argc == 1 && RB_TYPE_P(argv[0], T_HASH)) {
/* named arguments */
filename = rb_hash_aref(argv[0], filename_id);
template = rb_hash_aref(argv[0], template_id);
} else {
/* regular arguments */
if (argc == 0 || argc > 2)
rb_raise(rb_eArgError, "expecting 1..2 arguments or named arguments.");
filename = argv[0];
if (argc > 1) {
template = argv[1];
}
}
if (filename == Qnil && template == Qnil)
rb_raise(rb_eArgError, "need either template contents or file name.");
if (template != Qnil)
Check_Type(template, T_STRING);
if (filename != Qnil)
Check_Type(filename, T_STRING);
mustache_s **m = NULL;
TypedData_Get_Struct(self, mustache_s *, &iodine_mustache_data_type, m);
if (!m) {
rb_raise(rb_eRuntimeError, "Iodine::Mustache allocation error.");
}
mustache_error_en err;
*m = mustache_load(.filename =
(filename == Qnil ? NULL : RSTRING_PTR(filename)),
.filename_len =
(filename == Qnil ? 0 : RSTRING_LEN(filename)),
.data = (template == Qnil ? NULL : RSTRING_PTR(template)),
.data_len = (template == Qnil ? 0 : RSTRING_LEN(template)),
.err = &err);
if (!*m)
goto error;
FIO_LOG_DEBUG("allocated / loaded mustache data at: %p", (void *)*m);
return self;
error:
switch (err) {
case MUSTACHE_OK:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template ok, unknown error.");
break;
case MUSTACHE_ERR_TOO_DEEP:
rb_raise(rb_eRuntimeError, "Iodine::Mustache element nesting too deep.");
break;
case MUSTACHE_ERR_CLOSURE_MISMATCH:
rb_raise(rb_eRuntimeError,
"Iodine::Mustache template error, closure mismatch.");
break;
case MUSTACHE_ERR_FILE_NOT_FOUND:
rb_raise(rb_eLoadError, "Iodine::Mustache template not found.");
break;
case MUSTACHE_ERR_FILE_TOO_BIG:
rb_raise(rb_eLoadError, "Iodine::Mustache template too big.");
break;
case MUSTACHE_ERR_FILE_NAME_TOO_LONG:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template name too long.");
break;
case MUSTACHE_ERR_EMPTY_TEMPLATE:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template is empty.");
break;
case MUSTACHE_ERR_UNKNOWN:
rb_raise(rb_eRuntimeError, "Iodine::Mustache unknown error.");
break;
case MUSTACHE_ERR_USER_ERROR:
rb_raise(rb_eRuntimeError, "Iodine::Mustache internal error.");
break;
case MUSTACHE_ERR_FILE_NAME_TOO_SHORT:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template file name too long.");
break;
case MUSTACHE_ERR_DELIMITER_TOO_LONG:
rb_raise(rb_eRuntimeError, "Iodine::Mustache new delimiter is too long.");
break;
case MUSTACHE_ERR_NAME_TOO_LONG:
rb_raise(rb_eRuntimeError,
"Iodine::Mustache section name in template is too long.");
default:
break;
}
return self;
}
|
Class Method Details
.render(*args) ⇒ Object
Renders the mustache template found in filename
, using the data provided in
the data
argument. If template
is provided it will be used instead of
reading the file's content.
Iodine::Mustache.render(filename, data, template = nil)
Returns a String with the rendered template.
Raises an exception on error.
template = "<h1>{{title}}</h1>"
filename = "templates/index"
data = {title: "Home"}
result = Iodine::Mustache.render(filename, data)
# filename will be used to resolve the path to any partials:
result = Iodine::Mustache.render(filename, data, template)
# OR, if we don't need partial template path resolution
result = Iodine::Mustache.render(template: template, data: data)
NOTE 1:
This function doesn't cache the template data.
The more complext the template the higher the cost of the template parsing stage.
Consider creating a persistent template object using a new object and using the instance #render method.
NOTE 2:
As one might notice, no binding is provided. Instead, a data
Hash is assumed.
Iodine will search the Hash for any data while protecting against code
execution.
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 |
# File 'ext/iodine_ext/iodine_mustache.c', line 447
static VALUE iodine_mustache_render_klass(int argc, VALUE *argv, VALUE self) {
VALUE filename = Qnil, data = Qnil, template = Qnil;
if (argc == 1) {
/* named arguments */
Check_Type(argv[0], T_HASH);
filename = rb_hash_aref(argv[0], filename_id);
data = rb_hash_aref(argv[0], data_id);
template = rb_hash_aref(argv[0], template_id);
} else {
/* regular arguments */
if (argc < 2 || argc > 3)
rb_raise(rb_eArgError, "expecting 2..3 arguments or named arguments.");
filename = argv[0];
data = argv[1];
if (argc > 2) {
template = argv[2];
}
}
if (filename == Qnil && template == Qnil)
rb_raise(rb_eArgError, "need either template contents or file name.");
if (template != Qnil)
Check_Type(template, T_STRING);
if (filename != Qnil)
Check_Type(filename, T_STRING);
fio_str_s str = FIO_STR_INIT;
mustache_s *m = NULL;
mustache_error_en err;
m = mustache_load(.filename =
(filename == Qnil ? NULL : RSTRING_PTR(filename)),
.filename_len =
(filename == Qnil ? 0 : RSTRING_LEN(filename)),
.data = (template == Qnil ? NULL : RSTRING_PTR(template)),
.data_len = (template == Qnil ? 0 : RSTRING_LEN(template)),
.err = &err);
if (!m)
goto error;
int e = mustache_build(m, .udata1 = &str, .udata2 = (void *)data);
mustache_free(m);
if (e)
goto render_error;
fio_str_info_s i = fio_str_info(&str);
VALUE ret = rb_str_new(i.data, i.len);
fio_str_free(&str);
return ret;
error:
switch (err) {
case MUSTACHE_OK:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template ok, unknown error.");
break;
case MUSTACHE_ERR_TOO_DEEP:
rb_raise(rb_eRuntimeError, "Iodine::Mustache element nesting too deep.");
break;
case MUSTACHE_ERR_CLOSURE_MISMATCH:
rb_raise(rb_eRuntimeError,
"Iodine::Mustache template error, closure mismatch.");
break;
case MUSTACHE_ERR_FILE_NOT_FOUND:
rb_raise(rb_eLoadError, "Iodine::Mustache template not found.");
break;
case MUSTACHE_ERR_FILE_TOO_BIG:
rb_raise(rb_eLoadError, "Iodine::Mustache template too big.");
break;
case MUSTACHE_ERR_FILE_NAME_TOO_LONG:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template name too long.");
break;
case MUSTACHE_ERR_EMPTY_TEMPLATE:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template is empty.");
break;
case MUSTACHE_ERR_UNKNOWN:
rb_raise(rb_eRuntimeError, "Iodine::Mustache unknown error.");
break;
case MUSTACHE_ERR_USER_ERROR:
rb_raise(rb_eRuntimeError,
"Iodine::Mustache internal error or unexpected data structure.");
break;
case MUSTACHE_ERR_FILE_NAME_TOO_SHORT:
rb_raise(rb_eRuntimeError, "Iodine::Mustache template file name too long.");
break;
case MUSTACHE_ERR_DELIMITER_TOO_LONG:
rb_raise(rb_eRuntimeError, "Iodine::Mustache new delimiter is too long.");
break;
case MUSTACHE_ERR_NAME_TOO_LONG:
rb_raise(rb_eRuntimeError,
"Iodine::Mustache section name in template is too long.");
break;
default:
break;
}
return Qnil;
render_error:
fio_str_free(&str);
rb_raise(rb_eRuntimeError, "Couldn't build template frome data.");
}
|
Instance Method Details
#render(data) ⇒ Object
Renders the mustache template using the data provided in the data
argument.
Returns a String with the rendered template.
Raises an exception on error.
NOTE:
As one might notice, no binding is provided. Instead, a data
Hash is assumed.
Iodine will search the Hash for any data while protecting against code
execution.
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'ext/iodine_ext/iodine_mustache.c', line 390
static VALUE iodine_mustache_render(VALUE self, VALUE data) {
fio_str_s str = FIO_STR_INIT;
mustache_s **m = NULL;
TypedData_Get_Struct(self, mustache_s *, &iodine_mustache_data_type, m);
if (!m) {
rb_raise(rb_eRuntimeError, "Iodine::Mustache allocation error.");
}
if (mustache_build(*m, .udata1 = &str, .udata2 = (void *)data))
goto error;
fio_str_info_s i = fio_str_info(&str);
VALUE ret = rb_str_new(i.data, i.len);
fio_str_free(&str);
return ret;
error:
fio_str_free(&str);
rb_raise(rb_eRuntimeError, "Couldn't build template frome data.");
}
|