mustache / spec

The Mustache spec.
MIT License
364 stars 71 forks source link

Mutual lambda interpolation expansion and escaping problem. #100

Open perchingeagle opened 8 years ago

perchingeagle commented 8 years ago

There is a rule for each of the first two codes, but if you trying using them mutually, like in the third example, it creates a problem.

Lambda Escaping (code 1) lambda rule 5

var data = {
  'lambda' : function(){
    return '>';  // it is escaped by default (Lambda Expansion)
  }
};

var template = "<{{lambda}}{{{lambda}}}";

// Result = "<&gt;>"

Lambda Interpolation Expansion (code 2) lambda rule 2

var data = {
  'planet' : 'world',
  'lambda' : function(){
    return {{planet}};  // Interpolation Expansion
  }
};

var template = "Hello, {{lambda}}";

// Result = "Hello, world"

Now simply change the value of data['planet'] from 'world' to '<' and there is the problem:

var data = {
  'planet' : '<',
  'lambda': function(){
    return "{{planet}} "  // the planet tag is escaped twice 
  }
};

// the first escaping of  '<' becomes '&lt;'  (Interpolation Expansion)
// and the second escaping becomes  '&amp;lt;' ( Lambda Escaping)

var template = "Hello, {{lambda}}";

// Result = "Hello &amp;lt;"  // makes no sense

And this is what the rule currently specifies. Is there a clause in the rules or specification that prevents this?

groue commented 8 years ago

GRMustache.swift is able to do it properly:

do {
    let data = [
        "lambda": Box(Lambda { ">" })
    ]
    let template = try! Template(string: "<{{lambda}}{{{lambda}}}")
    // <&gt;>
    let rendering = try! template.render(Box(data))
}

do {
    let data = [
        "planet": Box("world"),
        "lambda": Box(Lambda { "{{planet}}" })
    ]
    let template = try! Template(string: "Hello, {{lambda}}")
    // Hello, world
    let rendering = try! template.render(Box(data))
}

do {
    let data = [
        "planet": Box("<"),
        "lambda": Box(Lambda { "{{planet}}" })
    ]
    let template = try! Template(string: "Hello, {{lambda}}")
    // Hello, &lt;
    let rendering = try! template.render(Box(data))
}

It is able to do so by processing the string returned by the lambda as a text template (a template that does not escape double-mustache tags), and then escaping the full rendering of the text lambda.

I think this avoids total brain-fucked rendering of the majority of lambdas (those with double-mustache tags). But it requires introducing the concept of a text template in the rendering implementation. And it breaks when one wants to use triple-mustache tags in lambdas (note how both {{planet}} and {{{planet}}} have the same rendering below):

do {
    let data = [
        "planet": Box("<"),
        "lambda": Box(Lambda { "{{planet}} {{{planet}}}" })
    ]
    let template = try! Template(string: "Hello, {{lambda}}")
    // Hello, &lt; &lt;
    let rendering = try! template.render(Box(data))
}

Well, what is the conclusion? The lambda spec is broken. A proper lambda shouldn't return a template string, but allow user to perform rendering (and interact with the rendering engine HTML escaping). This is the way chosen by GRMustache and Handlebars. GRMustache implements lambdas on top of this more general solution, but with the caveats implied by the specification.

groue commented 8 years ago

FYI, the GRMustache way of dealing properly with a lambda that renders "{{planet}} {{{planet}}}" is the following.

First with the general rendering solution:

let data = [
    "planet": Box("<"),
    // RenderingInfo contains all necessary input, and the Rendering result
    // is a string tagged with a content type (HTML or text).
    "lambda": Box({ (info: RenderingInfo) -> Rendering in
        try! Template(string: "{{planet}} {{{planet}}}").render(info.context)
    })
]
let template = try! Template(string: "Hello, {{lambda}}")
// Hello, &lt; <
let rendering = try! template.render(Box(data))

Or with a template instead of a lambda (this technique is also called dynamic partial:

let lambdaTemplate = try! Template(string: "{{planet}} {{{planet}}}")
let data = [
    "planet": Box("<"),
    "lambda": Box(lambdaTemplate)
]
let template = try! Template(string: "Hello, {{lambda}}")
// Hello, &lt; <
let rendering = try! template.render(Box(data))
perchingeagle commented 8 years ago

First of all, thank you for taking some time out of your schedule to read and write a response to my problem / question. I thought as much that the specification is broken. If those two rules weren't implemented together, there would be no problem, or if interpolations didn't have lambdas as an option, then there wouldn't have been this issue. While the triple lambda solves this problem, the triple lambda also has it own problem with delimiters for example:

{{regular-tag}} {{{triple-mustache}}} {{=[ ]=}} [regular-tag] [{triple-mustache}]
groue commented 8 years ago

You're welcome, @perchingeagle. But don't expect the spec to change: it's stuck (it hasn't changed in years despite the work done by many implementors).

The way to avoid HTML escaping with custom delimiters is & tags: {{{foo}}} is equivalent to {{&foo}}, and to {{=[ ]=}} [&foo]. & was introduced specifically for custom delimiters. Quoting https://mustache.github.io/mustache.5.html:

You can also use & to unescape a variable: {{& name}}. This may be useful when changing delimiters (see "Set Delimiter" below).

perchingeagle commented 8 years ago

I guess the current solution would be the choice of which tag to use for each situation:

{{& tag }} // for delimiters
{{{ tag }}} // for mutual lambda interpolation expansion and escaping

Thanks :+1:

perchingeagle commented 8 years ago

I guess it is safe to say that this issue is now closed again @groue thank you

groue commented 8 years ago

Why did you close this issue? The lambda spec has an issue - there's no point hiding it under the carpet, is it?

perchingeagle commented 8 years ago

I have reopened it.