Class: Iodine::Mustache

Inherits:
Data
  • Object
show all
Defined in:
lib/iodine/mustache.rb,
ext/iodine/iodine_mustache.c

Overview

Iodine includes a strict and extra safe Mustache templating engine.

The Iodine Mustache templating engine provides increased XSS protection through agressive HTML escaping. It’s also faster than the original (Ruby based) Mustache templating engine.

Another difference is that the Iodine Mustache templating engine always loads the templates from the disk, allowing for patial template path resolution.

There’s no monkey-patch for ‘mustache` Ruby gem since the API is incompatible.

You can benchmark the Iodine Mustache performance and decide if you wish to switch from the Ruby implementation.

require 'benchmark/ips'
require 'mustache'
require 'iodine'

def benchmark_mustache
  # benchmark code was copied, in part, from:
  #   https://github.com/mustache/mustache/blob/master/benchmarks/render_collection_benchmark.rb
  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_1 = {
    products: [ {
      :external_index=>"This <product> should've been \"properly\" escaped.",
      :url=>"/products/7",
      :image=>"products/product.jpg"
    } ]
  }
  data_1000 = {
    products: []
  }

  1000.times do
    data_1000[:products] << {
      :external_index=>"product",
      :url=>"/products/7",
      :image=>"products/product.jpg"
    }
  end

  data_1000_escaped = {
    products: []
  }

  1000.times do
    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(filename)

  puts "Ruby Mustache rendering (and HTML escaping) results in:",
       view.render(data_1), "",
       "Notice that Iodine::Mustache rendering (and HTML escaping) results in agressive escaping:",
       iodine_view.render(data_1), "", "----"

  # return;

  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 chaching - render list of 1000") do |times|
      tmp = Mustache.new
      tmp.template = template
      tmp.render(data_1000)
    end
    x.report("Iodine::Mustache - no chaching - render list of 1000") do |times|
      Iodine::Mustache.render(nil, data_1000, template)
    end

  end
end

benchmark_mustache

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename) ⇒ Object

Loads a mustache template (and any partials).

Once a template was loaded, it could be rendered using render.



281
282
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
# File 'ext/iodine/iodine_mustache.c', line 281

static VALUE iodine_mustache_new(VALUE self, VALUE filename) {
  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.");
  }
  Check_Type(filename, T_STRING);
  mustache_error_en err;
  *m = mustache_load(.filename = RSTRING_PTR(filename),
                     .filename_len = RSTRING_LEN(filename), .err = &err);
  if (!*m)
    goto error;
  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_eRuntimeError, "Iodine::Mustache template not found.");
    break;
  case MUSTACHE_ERR_FILE_TOO_BIG:
    rb_raise(rb_eRuntimeError, "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;
  }
  return self;
}

Class Method Details

.render(*args) ⇒ Object

Renders the mustache template found in ‘filename`, using the data provided in the `data` argument.

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.



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
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
# File 'ext/iodine/iodine_mustache.c', line 401

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, "missing both template contents and 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_eRuntimeError, "Iodine::Mustache template not found.");
    break;
  case MUSTACHE_ERR_FILE_TOO_BIG:
    rb_raise(rb_eRuntimeError, "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;
  }
  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.



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'ext/iodine/iodine_mustache.c', line 345

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