3

I'm trying to write a Ruby/Parslet parser for Handlebars but I'm getting stuck with the {{ else }} keyword. To explain brieflt for those who do not use Handlebars, an if/else is written this way:

{{#if my_condition}}
  show something
{{else}}
  show something else
{{/if}}

but it becomes tricky as the inlining and the helpers can use the same syntax, for example:

Name: {{ name }}
Address: {{ address }}

So I first made a rule to recognise the replacements:

rule(:identifier)  { match['a-zA-Z0-9_'].repeat(1) }
rule(:path)        { identifier >> (dot >> identifier).repeat }

rule(:replacement) { docurly >> space? >> path.as(:item) >> space? >> dccurly}

Which match anything like {{name}} or {{people.name}}. The problem of course is that is also matches the {{ else }} block. Here is how I've written the rule to match an if/else block:

rule(:else_kw) {str('else')}
rule(:if_block) {
  docurly >>
  str('#if') >>
  space >>
  path.as(:condition) >>
  space? >>
  dccurly >>
  block.as(:if_body) >>
  (
    docurly >>
    else_kw >>
    dccurly >>
    block.as(:else_body)
  ).maybe >>
  docurly >>
  str('/if') >>
  dccurly
}

(note: docurly is {{, dccurly is }} and block can be more or less anything)

So my need now is to rewrite the `identifier``rule so it matches any word but not "else".

Thanks in advance, Vincent

Vincent
  • 620
  • 7
  • 19
  • OMG! Let me escape first.. :( – Arup Rakshit Mar 04 '15 at 10:03
  • Any of these answers useful? – Nigel Thorne Oct 28 '15 at 01:15
  • Sadly I can't yet say, we've had some priority changes for the project and I did not get much time to work on this part of the project, so I had to keep the quick&dirty solution I found first (keeping the {{ else }} as any other identifier and treat it in the code (each block is splitted in two part if an {{ else }} identifier is found). I hope I can try one of those solutions one day when I'll have time to work on this part of the project again ... – Vincent Oct 28 '15 at 10:51

2 Answers2

0

One way to do this is to use the absent? lookahead modifier. foo.absent? will match if the atom or rule foo does not match at this point, and does so without consuming any input.

With this in hand, you could write the identifier rule as

rule(:identifier)
    { (else_kw >> dccurly).absent? >> match['a-zA-Z0-9_'].repeat(1) }
halfflat
  • 1,584
  • 8
  • 11
  • 1
    took a long time to get back to the parsley project, but your solution worked like a charm :) – Vincent Jul 15 '19 at 12:28
0

It depend on the syntax you are trying to match. If you were not inside an {{if}} {{/if}} pair should {{else}} be treated as a valid identifier or a syntax error? If you had a path with a.else.b should that be valid?

If a.else.b isn't valid you could do the following:

rule(:identifier)
    { (else_kw).absent? >> match['a-zA-Z0-9_'].repeat(1) | else_kw >> match['a-zA-Z0-9_'].repeat(1) }

which accepts all strings except "else", by saying "any string not starting with else, OR strings starting with else that have at least one more character".

Note: This makes me think "Why is else so special?" should we be excluding all keywords here?

If a.else.b is valid you can't exclude it at the identifier level. It's then more accurate to say your path can't be "else".

If you said:

rule(:path)        { else_kw.absent? >> (identifier >> (dot >> identifier).repeat) }

This would rule out any identifier that started with 'else', e.g. "elsewise.option"

So.. the absent? needs to also match something to show your block has ended.

rule(:path)        { (else_kw >> dccurly).absent? >> (identifier >> (dot >> identifier).repeat) }

The problem here is that we are now coupling path to the idea that it ends with a dccurly which isn't strictly correct (and doesn't deal with whitespace). So "path" isn't the right place to put this stuff.

If we were trying to stop replacement from matching else, that would be easier.

rule(:replacement) { docurly >> space? >> (else_kw >> space? >> dccurly).absent? >> path.as(:item) >> space? >> dccurly}

That would prevent replacement matching else, but would allow elsewise.something, or else.something.

If you don't want "else.something" then you need something like this:

rule(:replacement) { docurly >> space? >> (else_kw >> (space | dccurly | dot)).absent? >> path.as(:item) >> space? >> dccurly}

so that "else " "else." and "else}}" are all prevented.

Nigel Thorne
  • 21,158
  • 3
  • 35
  • 51