2

I have an XML document that defines a task, which is a list of actions to be performed on certain data. I need to convert this "task list" to a Javascript method which can be called at some later time, which in turn calls a series of pre-defined methods, with the appropriate data. How would you achieve this?

Important Clarification:
I'm not worried about the XML parsing. I'm more interested in how to actually build the Task Method, including binding the essential data to the pre-defined action methods. That's the part I'm struggling with.

Edit: I've revised my example to make it a bit more interesting, and hopefully a bit clearer.

XML:

<task id="enter-castle">
    <if holding="castle-key">
        <print message="You unlock the castle door and enter." />
        <destroy item="castle-key" />
        <goto location="castle" />

        <else>
            <print message="The castle door is locked." />
        </else>
    </if>
</task>

Javascript:

Game = {

    print: function(message) {
        // display message
    },

    destroy: function(item) {
        // destroy the object
    },

    goto: function(location) {
        // change player location
    },

    ifHolding: function(item) {
        // return true if player has item
    }
};

parseTask(taskNode) {

    var taskId = taskNode.getAttribute('id');

    // What goes here??

    Game.tasks[taskId] = /* ??? */;
}

When I call parseTask() on the <task id="enter-castle"> node, this should create a function that, in effect, does the following when called:

Game.tasks.enterCastle = function() {
    if (Game.ifHolding('castle-key')) {
        Game.print("You unlock the castle door and enter.");
        Game.destroy('castle-key');
        Game.goto('castle');
    } else {
        Game.print("The castle door is locked.");
    }
}
Brian Lacy
  • 18,785
  • 10
  • 55
  • 73
  • Despite of your understandable XML-to-"compiled" example, it's not clear what you want. At which part are you stuck? Your current output will not do anything meaningful, unless `this` is a global object, and your functions are defined in the global namespace. Could you edit your question, to describe more clearly where you're stuck? – Rob W Mar 01 '12 at 20:17
  • My example is of course not a working or complete example. I'll try to make the question clearer. – Brian Lacy Mar 01 '12 at 20:27
  • At this point, every approach has given me considerable trouble. I'm now considering just storing the hierarchy for later interpretation, so that it can be interpreted on the fly when the "task" is executed. – Brian Lacy Mar 15 '12 at 16:58

3 Answers3

3

What you want are closures.

function createMethod(arguments) {
    var task = doSomethingWithYour(arguments);
    return function(xmlData) { // <- this is the fundamental part
        // do the task with your data
        // the "task" vars are still available
        // even if the returned function is executed in a different context
    }
}

This allows you to create an own method for each task. Don't use the Function constructor or eval.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • This sounds closer to what I was looking for, and is similar to the solution I've been working on. Actually though, I'm now leaning more toward eval ;) it's vastly simpler, aside from the need for some careful input sanitization. – Brian Lacy Mar 05 '12 at 17:33
  • Decided this was my favorite answer, even though I went a different direction. – Brian Lacy Mar 29 '12 at 19:17
2

This is a situation where JavaScript's eval() function will make your life much easier. You can easily build a JavaScript source string matching your desired one and evaluate it to assign the function to the desired property of your Game object.

Of course, there are drawbacks to using "eval", which I won't explore in this answer since you can find countless justifications for why not to use it on the web. However, building and evaluating a simple JS source string will be much easier in the short term than say, a closure based solution, despite any potential drawbacks of performance and security. Moreover, the "eval" based solution will be easy to test since you can simply inspect the source string before it is evaluated.

So try something like this:

function buildTaskFunction(taskXml) {
  var source='', depth=0, node /*a visitor to each DOM node*/;
  // foreach (node in traverseInOrder(taskXml)) {
    switch (node.nodeName) {
      case 'TASK':
        source += 'Game.tasks.' + makeFunctionName(node.id) + '= function(){';
        depth++;
        break;
      case 'IF':
        source += 'if(' + getConditionalAttribute(node) + '){'
        depth++;
        break;
      case 'ELSE':
        source += '}else{';
        break;
      case 'DESTROY':
        source += 'Game.destroy("' + node.getAttribute('item') + '");'
        break;
      case 'PRINT':
        source += 'Game.print("' + node.getAttribute('message') + '");'
        break;
      // case etc...
      default: throw new Error('unhandled node type "' + node.nodeName + '"');
    }
  // end "foreach node".
  while (depth-- > 0) { // You'll need to account for nested "if"s somehow...
    source += '}';
  }
  eval(source);
}

And again, there are many potential problems (not definitive ones) with using "eval", so please do read about and try to understand them in the context of your solution. Use your own judgement when deciding if the drawbacks are worth the benefits in your own program -- just because a tool can be dangerous doesn't mean you should not use it.

maerics
  • 151,642
  • 46
  • 269
  • 291
  • I've gone back and forth on using eval(). It is by far the easiest solution and would solve the problem nicely. I worry some about performance, though. Granted, this all takes place during the "compilation" phase, so it may not be so bad, especially if I can figure out a way to cache/store a "compiled" function. Security-wise, I would just need to filter every raw-data input from the XML file.. – Brian Lacy Mar 05 '12 at 17:31
  • 1
    @BrianLacy: sounds like you're taking a level-headed approach. A closure-based solution is definitely more interesting (IMHO) if you like things like functional programming, first-class functions, currying, etc., but if you don't then it will quickly get frustrating. – maerics Mar 05 '12 at 17:57
  • If eval is ever appropriate, it is appropriate in this use case. – RushPL Sep 13 '13 at 09:05
0

Example using dojo:

dojo.require("dojox.xml.parser");

dojo.ready(function(){
  // Parse text and generate an XML DOM
  var xml = "<tnode><node>Some Text</node><node>Some Other Text</node></tnode>";
  var dom = dojox.xml.parser.parse(xml);

  var docNode = dom.documentElement();
  // ...
}

The remainder of the function is non-trivial, but would largely just consist of attribute lookup using ['<attribute-name>'] and child node iteration using dojo.forEach(<node>.childNodes, function(childNode) { /* do stuff */ });

Ryan Ransford
  • 3,224
  • 28
  • 35
  • Sorry, as I clarified just a moment ago, I'm not too concerned about the XML itself. I'm more interested in how to setup the Task Methods. The source data could just as easily be JSON or some other format. Thanks for contributing though! – Brian Lacy Mar 01 '12 at 21:04
  • are you wanting to execute it as it is parsed, or create an object with that holds that functionality? – Ryan Ransford Mar 01 '12 at 21:14
  • I do NOT want to execute it as it is parsed, I want to create an object that has that functionality, so that it can be executed later. – Brian Lacy Mar 01 '12 at 21:50
  • okay, then you can use the `dojo.hitch` method to create function variables which are bound to a context (`Game` in your example) with bound values. – Ryan Ransford Mar 01 '12 at 22:03
  • 1
    but, I'm not sure that I can figure out the rest without resorting to `eval`s – Ryan Ransford Mar 01 '12 at 22:09
  • Also, I'm not using dojo, or any other JS utility library at present. But thanks again for your help. – Brian Lacy Mar 02 '12 at 00:19