lightncandy icon indicating copy to clipboard operation
lightncandy copied to clipboard

Template Parsing Error — PHP Fatal error: Uncaught Exception: Unexcepted ')'

Open jasonh-brimar opened this issue 7 years ago • 5 comments

The PHP Code:

require('./vendor/autoload.php');

// Define the template string.
// $templateString = '{{> MyPartial (newObject name="John Doe") message=(echo message="Hello World!")}}'; // This template works.
// $templateString = '{{> MyPartial (newObject name="JohnDoe") message=(echo message=(echo message="Hello World!"))}}'; // This template works.
$templateString = '{{> MyPartial (newObject name="John Doe") message=(echo message=(echo message="Hello World!"))}}'; // This template does NOT work.

// Define the partial template string.
$partialTemplateString = '{{name}} says: “{{message}}”';

// Define helpers...
$helpers = [
    'newObject' => (
        function ($options): stdClass
        {
            return (object)$options['hash'];
        }
    ),
    'echo' => (
        function ($options)
        {
            return $options['hash']['message'];
        }
    )
];

// Define the LightnCandy compile options.
$compileOptions = [
    'flags' => LightnCandy\LightnCandy::FLAG_HANDLEBARSJS_FULL | LightnCandy\LightnCandy::FLAG_ERROR_SKIPPARTIAL | LightnCandy\LightnCandy::FLAG_EXTHELPER | LightnCandy\LightnCandy::FLAG_ERROR_EXCEPTION,
    'helperresolver' => (
        function ($context, $name) use ($helpers): bool
        {
            return array_key_exists($name, $helpers);
        }
    )
];

// Create the template using LightnCandy.
$template = eval(LightnCandy\LightnCandy::compile($templateString, $compileOptions));

// Define the LightnCandy render options.
$renderOptions = [
    'partials' => [
        'MyPartial' => eval('use \LightnCandy\Runtime as LR; use \LightnCandy\SafeString as SafeString; return ' . LightnCandy\LightnCandy::compilePartial($partialTemplateString, $compileOptions) . ';')
    ],
    'helpers' => $helpers
];

?>
<!doctype html>
<html lang="en">

    <head>
        <meta charset="utf-8">
        <title>Nested Helper Error Test</title>
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <style>
            #php, #js {
                border: 1px solid black;
                margin: 20px;
                padding: 20px;
            }
            #php {
                background: #ffe;
            }
            #js {
                background: #eff;
            }
        </style>
    </head>

    <body>

        <div id="php">
            <h1>Rendered by PHP</h1>
            <div id="render-php"><?= $template(new stdClass(), $renderOptions) ?></div>
        </div>

        <div id="js">
            <h1>Rendered by JS</h1>
            <div id="render-js"></div>
        </div>

        <script id="template" type="text/x-handlebars-template"><?= $templateString ?></script>
        <script id="partial-template" type="text/x-handlebars-template"><?= $partialTemplateString ?></script>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.js" integrity="sha256-JWyJjSicZs/EX4AJmuCHSYYARSvIkYeM79Dn1pJOSCE=" crossorigin="anonymous"></script>

        <script>

            // Register helpers.
            Handlebars.registerHelper({
                newObject: function (options) {
                    return Object.assign({}, options.hash);
                },
                echo: function (options) {
                    return options.hash.message;
                }
            });

            // Make the partial template available as a partial.
            Handlebars.registerPartial("MyPartial", Handlebars.compile(document.getElementById("partial-template").innerHTML));

            // Compile the the template and render.
            document.getElementById("render-js").innerHTML = Handlebars.compile(document.getElementById("template").innerHTML)({});

        </script>

    </body>

</html>

The Issue:

LightnCandy fails to parse/compile the template above. It seems to be related to the combination of both the space in "John Doe" and the double-nested usage of the echo helper. If either the space is removed or the helper is reduced to a single level of nesting, no error is thrown (as shown in the two alternative $templateString values shared above).

Below is the stack trace sent to my error log:

mod_fcgid: stderr: PHP Fatal error:  Uncaught Exception: Unexcepted ')' in expression ' MyPartial (newObject name="John Doe") message=(echo message=(echo message="Hello World!"))' !! in vendor/zordius/lightncandy/src/LightnCandy.php:110
mod_fcgid: stderr: Stack trace:
mod_fcgid: stderr: #0 vendor/zordius/lightncandy/src/LightnCandy.php(54): LightnCandy\\LightnCandy::handleError(Array)
mod_fcgid: stderr: #1 test.php(42): LightnCandy\\LightnCandy::compile('{{> MyPartial (...', Array)
mod_fcgid: stderr: #2 {main}
mod_fcgid: stderr:   thrown in vendor/zordius/lightncandy/src/LightnCandy.php on line 110

Note that even when using one of the working $templateString alternatives shared above, LightnCandy’s output won’t match that of Handlebars. Both the name (John Doe) and the message (Hello World!) will be missing. I created separate issues for the specific causes of both of those absent values (#293 and #294).

jasonh-brimar avatar Apr 26 '18 14:04 jasonh-brimar

I'm having the exact same issue with the newest version.

dennis-hh avatar Jun 29 '18 08:06 dennis-hh

I can confirm that this issue is still present in LightnCandy 1.2.5. The line numbers in the error are slightly different now, though:

PHP Fatal error:  Uncaught Exception: Unexcepted ')' in expression ' MyPartial (newObject name="John Doe") message=(echo message=(echo message="Hello World!"))' !! in vendor/zordius/lightncandy/src/LightnCandy.php:108
Stack trace:
#0 vendor/zordius/lightncandy/src/LightnCandy.php(50): LightnCandy\\LightnCandy::handleError()
#1 test.php(45): LightnCandy\\LightnCandy::compile()
#2 {main}
  thrown in vendor/zordius/lightncandy/src/LightnCandy.php on line 108

jasonh-brimar avatar Apr 25 '20 00:04 jasonh-brimar

I also had this issue, and was able to fix it by editing the source.

This isn't an issue in partials, this is an issue with the tokenizer for subqueries. The token parser breaks expressions by whitespace & then checks each broken string for an expected symbol. This issue stems from the parser not handling quotes correctly when there's no whitespace between the end quote and the end parens. There's an internal counter for the number of end parens & when there's an open quote detected it doesn't change this #. Any quoted text with white space inside that doesn't have white space directly after the end quote will cause this error.

The file "Parser.php" inside of the source folder can be modified (quick & probably dirty) to check for quotes after matching the expected symbol. The line below can be added after Parser.php:497 to correct this bug.

if ($quote !== 0 && strpos($t, $quote) !== false){ $quote = 0; }

My fix may break other things, but there's definitely a fundamental issue with the source that's causing this issue. Let me know if there's a better fix or if this breaks something.

LiamPierce avatar Jun 26 '21 02:06 LiamPierce

@LiamPierce your fix didn't work for me. However, I did come to the same conclusion as your interpretation of the problem. My solution can be found in #358.

tunnela avatar Nov 04 '21 13:11 tunnela