TYPO3 / Fluid

Fluid template rendering engine - Standalone version
GNU Lesser General Public License v3.0
152 stars 93 forks source link

Support short array syntax matching short object notation of ES2015 #336

Open albe opened 7 years ago

albe commented 7 years ago

There are a lot of use-cases where one must pass on a number of template variables to a partial, in which case one often ends up with something like the following:

   <f:render partial="Foo" arguments="{ something: something, somethingElse: somethingElse, somethingWholeDifferent: somethingWholeDifferent, andAlsoThis: WhichIsSomethingElse }">

It would be cool if Fluid supported a short array notation like the short object notation of ES2015 (https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Object_Initializer#New_notations_in_ECMAScript_2015).

The above would then become:

   <f:render partial="Foo" arguments="{ something, somethingElse, somethingWholeDifferent, andAlsoThis: WhichIsSomethingElse }">

Edit: There is currently one big issue with this feature: It makes the expression { foo } ambiguous to mean "the value of the variable 'foo'" or "an array with key 'foo' and value of the variable 'foo'". Any ideas on how to approach this are welcome.

albe commented 7 years ago

Something like this should do for parser regexes:

$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS = '/^
    (?P<Recursion>                                             # Start the recursive part of the regular expression - describing the array syntax
        {                                                      # Each array needs to start with {
            (?P<Array>                                         # Start sub-match
                (?:
                    \s*(?:
                        (
                            [a-zA-Z0-9\\-_]+                       # Unquoted key
                            |"(?:\\\"|[^"])+"                      # Double quoted key, supporting more characters like dots and square brackets
                            |\'(?:\\\\\'|[^\'])+\'                 # Single quoted key, supporting more characters like dots and square brackets
                        )                                          # END possible key options
                        \s*:\s*                                    # Key|Value delimiter :
                        (?:                                        # Possible value options:
                            "(?:\\\"|[^"])*"                       # Double quoted string
                            |\'(?:\\\\\'|[^\'])*\'                 # Single quoted string
                            |[a-zA-Z0-9\-_.]+                      # variable identifiers
                            |(?P>Recursion)                        # Another sub-array
                        )                                          # END possible value options
                    )                                          # END key-value syntax
                    |[a-zA-Z0-9\\-_]+                          # Single unquoted key matching short array syntax
                    \s*,?                                      # There might be a , to separate different parts of the array
                )*                                             # The above cycle is repeated for all array elements
            )                                                  # End array sub-match
        }                                                      # Each array ends with }
    )$/x';

$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS = '/
    (?P<ArrayPart>
        (?:                                                                 # Start sub-match of one key and value pair
            (?P<Key>                                                        # The arry key
                 [a-zA-Z0-9_-]+                                             # Unquoted
                |"(?:\\\\"|[^"])+"                                          # Double quoted
                |\'(?:\\\\\'|[^\'])+\'                                      # Single quoted
            )
            \\s*:\\s*                                                       # Key|Value delimiter :
            (?:                                                             # BEGIN Possible value options
                (?P<QuotedString>                                           # Quoted string
                     "(?:\\\\"|[^"])*"
                    |\'(?:\\\\\'|[^\'])*\'
                )
                |(?P<VariableIdentifier>
                    (?:(?=[^,{}\.]*[a-zA-Z])[a-zA-Z0-9_-]*)                 # variable identifiers must contain letters (otherwise they are hardcoded numbers)
                    (?:\\.[a-zA-Z0-9_-]+)*                                  # but in sub key access only numbers are fine (foo.55)
                )
                |(?P<Number>[0-9]+(?:\\.[0-9]+)?)                           # A hardcoded Number (also possibly with decimals)
                |\\{\\s*(?P<Subarray>(?:(?P>ArrayPart)\\s*,?\\s*)+)\\s*\\}  # Another sub-array
            )                                                               # END possible value options
        )
        |(?P<KeyValue>[a-zA-Z0-9_-]+)                                       # The arry key and value in short syntax
    )\\s*(?=\\z|,|\\})                                                  # An array part sub-match ends with either a comma, a closing curly bracket or end of string
/x';

The recursiveArrayHandler would then just add a check early in the loop like this:

    if (!empty($singleMatch['KeyValue'])) {
        $arrayToBuild[$singleMatch['KeyValue']] = new ObjectAccessorNode($singleMatch['KeyValue']);
        continue;
    }

If this agreed on, I would take care to provide the PR with the necessary changes and tests.

kdambekalns commented 7 years ago

In the first example, how is the matching to argument names done? Not at all, so order must be correct? The beauty of the current way of passing arguments is the support for named arguments, something not used in that example. Or is it merely a bad example?!

For short array syntax as such: 👍 For use instead of named arguments: 👎

bwaidelich commented 7 years ago

If I get it right, the argument name would be equal to the variable name, so these two would be equivalent:

{ foo: foo, bar: bar }
{ foo, bar }

One possible caveat might be the ambiguity between the regular object accessor syntax and a "shorthand array" with one item ({foo})

albe commented 7 years ago

Yes, exactly that @bwaidelich and good point with the object accessor syntax, that could be an issue. Not sure how to best deal with that in fact. At least for cases where { foo } evaluates to a non-array, that could/would be solved via type checks, but that still leaves the case when 'foo' is an array variable. Maybe just document it clearly that short array syntax only works with > 1 elements and cross fingers?

@kdambekalns not removing/replacing named arguments (or anything else). Just providing an additional shorter notation for 1:1 mapping of variable name to array key. It should be totally free to mix with the current notation per attribute.

Edit: To be more clear, this should be totally fine:

{ foo, bar: bar, baz: bazinga, quux: { hido, hidi: '{f:translate(...)}`, whoop: '{say.what}' } }
kdambekalns commented 7 years ago

Ok, so the variable name is used as key. I wasn't sure the name of the passed variable is known, but a bit of thinking would have cleared that up (we are looking at parsing the code here, doh!)

So, well, great. I can imagine shooting myself in the foot with this, but it'll be a pleasure, most of the time… ;)

sascha-egerer commented 7 years ago

@albe You can also do something like this:

    <f:alias map="{andAlsoThis: WhichIsSomethingElse}">
       <f:render partial="Foo" arguments="{_all}">
    </f:alias>

That will pass all variables to the partial but you can also define your own variables via the wrapping alias.

bwaidelich commented 7 years ago

I try to avoid using _all in my Fluid Templates (it's prefixed for a reason) ;) While it's very convenient in times it makes the partials less reusable if you pass them the whole world as context.

sascha-egerer commented 7 years ago

Yes but if you need to pass all variables to the partial anyway but do also need some custom one you can do it like that.

NamelessCoder commented 7 years ago

Couple of things come to my mind:

Just some things to be careful about :)

NamelessCoder commented 7 years ago

Btw, thinking out loud regarding _all we could replace that with *:

...and make it exclusively a different way of detecting arrays. This would also potentially improve performance because the special _all variable name no longer needs to be checked every time an ObjectAccessor is asked for a value, which also improves the compiled templates. The array argument's value can then be extracted using $renderingContext->getVariableProvider()->toArray() which also fits nicely into a compiled template.

albe commented 7 years ago

@sascha-egerer Yes, but the use case is really where you only want to pass a sub set of the variables, which happens quite often (at least for me). Also I prefer to be explicit about what I do and passing "_all" is very implicit in the sense that you need to know what "_all" is in the current context. So if I for example need to change some partial code and want to know which variables are available (esp. when I didn't write the template/partial in the first place), I prefer to only have to search one level up to the caller to see what I have at hand, rather than go n levels up and eventually land in the controller.

@NamelessCoder I would totally not allow { 1, 2, 3 } or { 'a', 'b', "c" } - that's also not allowed in ES2015. For something like defining a non-associative array I'd rather introduce an specific syntax for that [ 1, 2, 3 ] instead. But that's beyond the scope of this. Thanks for the hint at the " ExpressionNode implementation that detects CSV", I'll have a thought about that :) The last point with the ambiguity is the most concerning to me actually, and I have no good idea how to solve that :/

albe commented 7 years ago

Ok, so after thinking a bit about the ExpressionNode, I think this would not work easily with mixed notation, e.g. { a, b, c: d, e: { f, g: 'h' } }. At least not unless the ExpressionNode would copy a lot of the Array Syntax Regex (and logic). So I'm in favor of just adding the shorthand pattern as suggested above.

masi commented 1 year ago

It's time that Fluid becomes more user oriented. Vue templates are much more fun to write.