Add support for if/elseif/else control sections in templates.
authorJames Burke <jrburke@mozillamessaging.com>
Thu, 24 Jun 2010 12:02:09 -0700
changeset 1786 1543c4ae88308d6cc9da5abb98cd44285d181caf
parent 1785 74d179ddd5a33762d7ea66f203becc9d790eec59
child 1787 42b1a25c30619f6627b1d545bde75200ab064ab3
push id989
push userjrburke@gmail.com
push dateThu, 24 Jun 2010 19:02:14 +0000
Add support for if/elseif/else control sections in templates.
client/api/scripts/blade/jig.js
--- a/client/api/scripts/blade/jig.js
+++ b/client/api/scripts/blade/jig.js
@@ -49,17 +49,18 @@ require.def("blade/jig", ["blade/object"
             or: function (a, b) {
                 return !!(a || b);
             },
             and: function (a, b) {
                 return !!(a && b);
             }
         },
         attachData = true,
-        dataIdCounter = 0,
+        dataIdCounter = 1,
+        controlIdCounter = 1,
         dataRegistry = {};
 
     function isArray(it) {
         return ostring.call(it) === "[object Array]";
     }
 
     /**
      * Gets a property from a context object. Allows for an alternative topContext
@@ -179,17 +180,20 @@ require.def("blade/jig", ["blade/object"
                 //and need to compute if the values compare.
                 if (args[1]) {
                     comparison = value === comparison;
                     value = data;
                 } else {
                     //Just use the value, so the value is used in the comparison.
                     comparison = value;
                 }
-                if (comparison === null || comparison === undefined || (isArray(comparison) && !comparison.length)) {
+                //Want to allow returning 0 for values, so this next check is
+                //a bit verbose.
+                if (comparison === false || comparison === null ||
+                    comparison === undefined || (isArray(comparison) && !comparison.length)) {
                     return '';
                 } else if (children) {
                     if (isArray(value)) {
                         for (i = 0; i < value.length; i++) {
                             text += render(children, value[i], options);
                         }
                     } else {
                         text = render(children, value, options);
@@ -236,16 +240,25 @@ require.def("blade/jig", ["blade/object"
         '.': {
             doc: 'Variable declaration',
             action: function (args, data, options, children, render) {
                 options.context[args[0]] = getObject(args[1], data, options);
                 //TODO: allow definining a variable then doing a block with
                 //that variable.
                 return '';
             }
+        },
+        '>': {
+            doc: 'Else',
+            action: function (args, data, options, children, render) {
+                if (children) {
+                    return render(children, data, options);
+                }
+                return '';
+            }
         }
     };
 
     jig = function (text, data, options) {
         if (typeof text === 'string') {
             text = jig.compile(text, options);
         }
         return jig.render(text, data, options);
@@ -254,18 +267,19 @@ require.def("blade/jig", ["blade/object"
     jig.htmlEscape = function (text) {
         return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
     };
 
     function compile(text, options) {
         var compiled = [],
             start = 0,
             useRawHtml = false,
+            controlId = 0,
             segment, index, match, tag, command, args, lastArg, lastChar,
-            children, i;
+            children, i, tempTag;
 
         while ((index = text.indexOf(options.startToken, start)) !== -1) {
             //Output any string that is before the template tag start
             if (index !== start) {
                 compiled.push(text.substring(start, index));
             }
 
             //Find the end of the token
@@ -290,16 +304,29 @@ require.def("blade/jig", ["blade/object"
                 //if the command is commented out end block call, that messes with stuff,
                 //just throw to let the user know, otherwise browser can lock up.
                 if (badCommentRegExp.test(tag)) {
                     throw new Error('blade/jig: end block tags should not be commented: ' + tag);
                 }
 
                 command = tag.charAt(0);
 
+                if (command === ']' && controlId) {
+                    //In a control block, previous block was a related control block,
+                    //so parse it without the starting ] character.
+                    tempTag = tag.substring(1).trim();
+                    if (tempTag === '[') {
+                        command = '>';
+                    } else {
+                        command = tempTag.charAt(0);
+                        //Remove the starting ] so it is seen as a regular tag
+                        tag = tempTag;
+                    }
+                }
+
                 if (command && !options.propertyRegExp.test(command)) {
                     //Have a template command
                     tag = tag.substring(1).trim();
                 } else {
                     command = '_default_';
                     //Command could contain just the raw HTML indicator.
                     useRawHtml = (command === options.rawHtmlToken);
                 }
@@ -309,31 +336,51 @@ require.def("blade/jig", ["blade/object"
                     tag = tag.substring(options.rawHtmlToken.length, tag.length);
                 }
 
                 args = tag.split(options.argSeparator);
                 lastArg = args[args.length - 1];
                 lastChar = lastArg.charAt(lastArg.length - 1);
                 children = null;
 
-                //If last arg ends with a [ it means a block element.
-                if (lastChar === '[') {
+                if (command === ']') {
+                    //If there are no other args, this is an end tag, to close
+                    //out a block and possibly a set of control blocks.
+                    if (lastChar !== '[') {
+                        //End of a block. End the recursion, let the parent know
+                        //the place where parsing stopped.
+                        compiled.templateEnd = start;
+
+                        //Also end of a control section, indicate it as such.
+                        compiled.endControl = true;
+                    } else {
+                        //End of a block. End the recursion, let the parent know
+                        //the place where parsing stopped, before this end tag,
+                        //so it can process it and match it to a control flow
+                        //from previous control tag.
+                        compiled.templateEnd = start - match[0].length;
+                    }
+
+                    return compiled;
+                } else if (lastChar === '[') {
+                    //If last arg ends with a [ it means a block element.
+
+                    //Assign a new control section ID if one is not in play already
+                    if (!controlId) {
+                        controlId = controlIdCounter++;
+                    }
+
                     //Adjust the last arg to not have the block character.
                     args[args.length - 1] = lastArg.substring(0, lastArg.length - 1);
 
                     //Process the block
                     children = compile(text.substring(start), options);
 
                     //Skip the part of the string that is part of the child compile.
                     start += children.templateEnd;
-                } else if (command === ']') {
-                    //End of a block. End this recursion, let the parent know
-                    //the place where parsing stopped.
-                    compiled.templateEnd = start;
-                    return compiled;
                 }
 
                 //If this defines a template, save it off,
                 //if a comment (starts with /), then ignore it.
                 if (command === '+') {
                     options.templates[args[0]] = children;
                 } else if (command !== '/') {
                     //Adjust args if some end in commas, it means they are function
@@ -346,19 +393,25 @@ require.def("blade/jig", ["blade/object"
                             }
                         }
                     }
 
                     compiled.push({
                         action: options.commands[command].action,
                         useRawHtml: useRawHtml,
                         args: args,
+                        controlId: controlId,
                         children: children
                     });
                 }
+
+                //If the end of a block, clear the control ID
+                if (children && children.endControl) {
+                    controlId = 0;
+                }
             }
         }
 
         if (start !== text.length - 1) {
             compiled.push(text.substring(start, text.length));
         }
 
         return compiled;
@@ -374,26 +427,40 @@ require.def("blade/jig", ["blade/object"
             propertyRegExp: propertyRegExp,
             commands: commands,
             argSeparator: argSeparator,
             templates: templateCache
         });
 
         options.endRegExp = new RegExp('[^\\r\\n]*?' + endToken);
 
+        //Do some reset to avoid a number from getting too big.
+        controlIdCounter = 1;
+
         return compile(text, options);
     };
 
     function render(compiled, data, options) {
-        var text = '', i, dataId;
+        var text = '', i, dataId, controlId, currentControlId, currentValue, lastValue;
         if (typeof compiled === 'string') {
             text = compiled;
         } else if (isArray(compiled)) {
             for (i = 0; i < compiled.length; i++) {
-                text += render(compiled[i], data, options);
+                //Account for control blocks (if/elseif/else)
+                //control blocks all have the same control ID, so only call the next
+                //control block if the first one did not return a value.
+                currentControlId = compiled[i].controlId;
+                if (!currentControlId || currentControlId !== controlId || !lastValue) {
+                    currentValue = render(compiled[i], data, options);
+                    text += currentValue;
+                    if (currentControlId) {
+                        controlId = currentControlId;
+                        lastValue = currentValue;
+                    }
+                }
             }
         } else {
             //A template command to run.
             text = compiled.action(compiled.args, data, options, compiled.children, render);
             if (!text) {
                 text = '';
             } else if (!compiled.useRawHtml && !compiled.children) {
                 //Only html escape commands that are not block actions.