First the simple change. Shaper.match(template, node) now accepts the template in string format and will in that case perform Shaper.parseExpression on it. You can of course still provide a template node. Same applies to Shaper.replace(node, ...). There's no caching in place yet so watch out for bad big-O behavior. I don't want to add a cache without putting invalidation in place from the beginning, to avoid bad surprises later on. Adding a cache with invalidation should be fairly straightforward, should a contributor be up for it.
Then I made it more capable, with configurable wild-cards and conditionals. You can still use $ in the template as a wild-card matching any node, and $$ as a wild-card matching the rest of the nodes in a sequence. But you can now do much more as well. This is done by providing the optional conditionals argument to match. It's an object from identifier keys to objects or functions.
Here's how it works.
Assert(match("[$, $$]", "[1, 2, [3]]")); // $ matches 1 and $$ matches 2, [3]
Assert(match("[$, $$]", "[1, 2, [3]]", {$: {}, $$: {rest: true}})); // explicit conditionals (same as above)
Assert(match("[_, __]", "[1, 2, [3]]", {_: {}, __: {rest: true}})); // you can name them how you wish
So {} means matches any object and {rest: true} means matches any object and the rest. And we can name the wild-cards however we wish, which should solve any issue of name clashes. Let's do more with the conditionals.
Assert(match("$NUM", "1", {$NUM: {type: tkn.NUMBER}}));
Assert(match("$NUM", "1", {$NUM: {type: function(t) { return t === tkn.NUMBER; }}}));
Assert(match("$NUM", "1", {$NUM: function(n) { return n.type === tkn.NUMBER; }}));
Here are examples of more advanced usage, with three different ways of creating a conditional that matches numbers. Simplest is to provide {type: tkn.NUMBER} instead of {}. Here type is just a property of the node. Any property you add to the conditional will be required to match so you could just as well check value or something else. The second form is providing a function for the property instead of a value. Finally, the third form let's you provide a function for the entire node. The two first forms can be mixed freely. This allows you to tune how high you want to turn the declarative knob.
Here are some even more advanced examples (all taken from the tests). I picked ugly conditional names just to show that you can use any valid identifier, doesn't have to be dollars or underscores in them.
Assert(match("[ONE_STR, REST_NUMBERS]", "['one', 2, 3]", {
ONE_STR: {type: tkn.STRING},
REST_NUMBERS: {rest: true, type: tkn.NUMBER}
}));
var conds = {
ONE_STR: {type: tkn.STRING},
REST_NUMBERS_OR_STRINGS: {rest: true, type: function(t) {
return t === tkn.NUMBER || t === tkn.STRING;
}}
};
Assert(match("[ONE_STR, REST_NUMBERS_OR_STRINGS]", "['one', 2, 3]", conds));
Assert(!match("[ONE_STR, REST_NUMBERS_OR_STRINGS]", "['one', 2, null]", conds));
I'm quite happy with how the API turned out and its expressiveness. I'm keen to hear your thoughts on it.
Check out the commit for more details and examples: <https://github.com/olov/jsshaper/commit/72ee7e7b1460a49d3ac20010b92256d7ba7f851e>
Next step will be to add support for optionally collecting the matched nodes either via callbacks or a return value, but that's almost orthogonal to what we have here. After that I'll cook up something for Shaper.replace as well.
/Olov
> Assert(match("[$, $$]", "[1, 2, [3]]", {$: {}, $$: {rest: true}})); // explicit conditionals (same as above)
I should clarify that the match function in the examples is just a minor wrapper that parses the strings before calling Shaper.match to make the tests shorter and easier to read/write. While Shaper.match does accept a string for the template it will bail out if you provide a string instead of a node for the second argument.
/Olov