Class: Iodine::Mustache

Inherits:
Object
  • Object
show all
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 as String) 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

Instance Method Summary collapse

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.");
}