Sinful is not a fugly language

June 13th 2011 by Guillaume Malette

I’m working on a Rails project at Mentel Inc, which requires some templates made. Last week I was faced with a problem. The application is built with HAML which even our designer has come to adopt. However, we deal with freelancers, whom are using PHP. This meant that whatever API or DSL I was to create needs to:

  • Be designer-oriented
  • Be written to support both Ruby and PHP
  • Be HAML compatible
  • Be designer-oriented

The HAML compatibility is easily solved by asking for HTML code and using a tool like html2haml, so that’s a non-issue.

The real problem remains: a designer-friendly DSL with intercompatible code.

Solution 1: pure compatible code

The first solution is obviously to use code that would be 100% compatible between the two. For example, for the menu, one could use a function call like

menu("page1", "page2", "page3");

This would mean using parenthesis everywhere, with strings and semicolons at the end of the line. It also means axing argument hashes, because PHP requires specifying the array(). It wouldn’t have been easily extensible. Also, it’s ugly.

Solution 2: PHP Ruby Interpreter

Well, we can’t just ask the designer to install a bunch of PCRE extensions, or even Node.js, and it’s not like PHP has any gems that you can bundle install. I wrote a PHP Lexer and Parser that took Ruby-style code, but I stopped before getting to the actual code evaluation. The node tree looks right and I may finish it (I won’t) some day. The fact that I couldn’t use YACC or existing grammar made it tedious and tiresome.

Solution 3: Meet Sinful

The name Sinful came from the method I used to translate Ruby code to PHP eval()‘able code. The idea behind it comes from Unholy. It’s also a Recursive Backronym of “Sinful Is Not a FUgly Language”.

All about syntax

Thomas and I agreed that the DSL should be very straight forward and be far closer to Ruby than to PHP. Instead of the previous example, we wanted something like:

menu :for => [ :page1, :page2, :page3 ]

This syntax is both clear and extensible, by using indexed arrays / hashes for the arguments.

Easy conversion

The conversion from the ruby code above can be done with 3 replacement regular expressions:

  • Add parenthesis to the method call and wrap all the arguments into a PHP array()
  • Replace all Ruby lists by PHP arrays ( [] => array() )
  • Replace the symbols by strings.

Prefixing the function name with internal_ allowed us to use reserved function names like:

link :for => :page1

Final touches: Markup

We’re going to be using haml2html anyway, so just as well use ERB’s markup

<%= menu :for => [ :page1, :page2, :page3 ] %>

Sinful:

Here’s the code for the whole thing. From here you only need to write your helper methods.

    global $REGEX;
    $REGEX = array(
        "methodize"     => array("pattern" => "/\A([a-z]\w*)(\s?\(?.*\)?)/", "replace" => "internal_$1(array($2));"),
        "arrayfy"       => array("pattern" => "/\[(.*)\]/", "replace" => "array($1)"),
        "unsymbolize"   => array("pattern" => "/(:\w+)/", "replace" => "\"$1\"")
    );

    function sinful ($string)
    {
        global $REGEX;

        foreach($REGEX as $cute_name => $r)
        {
            $string = preg_replace ($r['pattern'], $r['replace'] , trim($string));
        }
        eval('$a = ' . $string);
        return $a;
    }

    function load_and_eval ($filename)
    {
        $contents = file($filename);
        $outputs = array();
        foreach ($contents as $line)
        {
            $outputs[] = (preg_match("/<%=(.*)%>$/", $line, $matches)) ? sinful($matches[1]) : $line;
        }
        return implode("", $outputs);
    }

    echo load_and_eval("main.html");
blog comments powered by Disqus