CRUD for nodes in XML files based on XPath queries. Based on grunt-xmlpoke / xmlpoke in NAnt.
JavaScript
Pull request Compare This branch is 28 commits ahead, 96 commits behind bdukes:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
tasks
test
.gitignore
.jshintrc
Gruntfile.js
LICENSE-MIT
README.md
package.json

README.md

grunt-xmlstoke

An extended version of grunt-xmlpoke by Brian Dukes - a gruntjs port of the xmlpoke NAnt task.

In addition to updating the values of existing nodes, as provided by grunt-xmlpoke, grunt-xmlstoke can perform all basic CRUD operations on XML Files: creating/inserting new nodes, deleting existing nodes, and reading the values of existing nodes (to then save them in grunt.option).

The current API should be completely backward-compatible with grunt-xmlpoke


Getting Started

This plugin requires Grunt ~0.4.2

If you haven't used Grunt before, be sure to check out the Getting Started guide, as it explains how to create a Gruntfile as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command:

npm install grunt-xmlstoke --save-dev

Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript:

grunt.loadNpmTasks('grunt-xmlstoke');

The "xmlstoke" task

Overview

In your project's Gruntfile, add a section named xmlstoke to the data object passed into grunt.initConfig().

grunt.initConfig({
  xmlstoke: {
    updateTitle: {
      options: {
        actions: [{
          xpath: '//title',
          value: 'The Good Parts'
        }],
      }
      files: { 'dest.xml': 'src.xml' },
    },
  },
})

Action Options

grunt-xmlstoke is configured with a series of actions (an array of objects), each of which is a single Read, Create/Insert, Update, or Delete operation.


action.type (all actions)

Type: String, Default value: undefined (Default operation is Update)
Denotes the type of operation. Only the first character is evaluated (case-insensitive) so you can pretty much name it whatever for readability in your gruntfile. i, Ins, INSERT and even indian food all denote an insertion.

First character should be:

C or I - Create/Insert
R - Read
U - Update (this is the default, type can be left blank for Update as well)
D - Delete


action.xpath (all actions)

Type: String or Array of Strings of valid XPath selectors.
Note that for Read operations only the first selector is evaluated.

For Insertion operations, this points to the parentNode/ownerNode to have the new elements/attributes inserted into.


action.value (only Insert and Update)

Type: String or Function(node?) returning a String

For Updates, this function is passed the selected node.
For Insertions, this function is passed the selected parentNode/ownerNode.


action.node (only Insert)

Type: String

The name of the node to be inserted, e.g. "my-node" to insert <my-node/> into, or "@myattr" to add the myattr attribute to the node(s) selected with action.xpath. Can also contain an XPath index (NOTE: unlike JavaScript Arrays ,XPath is 1-index) which is stripped from the name upon node creation, in order to create more than one of a kind:

actions: [
  { type: 'I', xpath: '/cds', node: 'cd', value: 'Slayer' },
  { type: 'I', xpath: '/cds', node: 'cd', value: 'More Slayer' }
]

Would find that /cds/cd already exists after the first insertion, and instead update its value (compare: mysql insert on duplicate key update).

actions: [
  { type: 'I', xpath: '/cds', node: 'cd[1]', value: 'Slayer' },
  { type: 'I', xpath: '/cds', node: 'cd[2]', value: 'More Slayer' }
]

Would correctly insert two different CDs, the index being optional in the first line.


action.saveAs (only Read)

Type: String
The key with which to save the retrieved value(s) in grunt.option. If set to myFoo, the value can later be retrieved by calling grunt.option('myFoo'). Note that the value saved can be a string if only one node was found, or an array of strings if more than one were found. This behaviour can be overridden using action.returnArray.


action.returnArray (only Read)

Type: String
Always saves an array in grunt.option, even if only a single node was matched. I.e. turns "result" into ["result"]


action.callback (only Read)

Type: Function
A post-processor for the extracted value(s) if you will. Applied the the extracted values before saving them in grunt.option.

Examples:

callback: function (readValues) { // readValues := "100"
    // Typecast to int before storing
    return parseInt(readValues, 10);
}

callback: function (readValues) { // readValues := ["A", "B"]
    if (readValues.length < 3) {
        // Return null to throw a grunt.error and abort the task
        // grunt.log.verbose("Aborting because there were only 2 nodes, expecting: 3+");
        return null;
    }
    return readValues;
}

callback: function (readValues) { // readValues := null
    if (readValues === null) {
        // Prevent grunt from throwing an Error because no nodes matched
        // grunt.log.verbose("Config elem not found, using default");
        return grunt.option('myDefaultValue');;
    }
    return readValues;
}

Task Options

options.actions

Type: Array, Default value: undefined

An array of Action objects, see Actions Options.
Examples:

actions: [
  // Delete all <foo> nodes
  {type: 'D', xpath: '//foo'},

  // Insert <baz> into all <baz> under /myroot
  {type: 'I', xpath: '/rootelem/bar', node: 'baz', },

  // Add an athe foobar="100" attribute to them
  {type: 'I', xpath: '/rootelem/bar/baz', node: '@foobar', value: '100'},

  // Change it to 200 for the second of those <baz>, type=update assumed as default
  {xpath: '/rootelem/bar/baz[2]/@foobar', value: '200'},

  // Read the value of the foobar attr from the 5th overall occurence of <baz>,
  // Save it in grunt.option as "myData"
  {type: 'R', xpath: '//baz[5]/@foobar', saveAs: 'myData' }
}]

Alternatively, the reads, deletions, insertions and updates/replacements shorthands can be used instead of actions. That way you don't have to specify the action type manually. The config arrays are processed in the order reads, deletions, insertions, replacements || updates, actions.

options.updates (alias: options.replacements)

Type: Array, Default value: undefined
An Array of Update Actions. action.type is set automatically.

options.deletions

Type: Array, Default value: undefined
An Array of Deletion Actions. action.type is set automatically.

options.reads

Type: Array, Default value: undefined
An Array of Read Actions. action.type is set automatically.

options.insertions

Type: Array, Default value: undefined
An Array of Insertion Actions. action.type is set automatically.


Usage Examples

Example - Basic Updates

In this example, the text content of an element is set to a static value. So if the test.xml file has the content <abc attr="1"></abc>, the generated result would be <abc attr="2">123</abc>.

grunt.initConfig({
  xmlstoke: {
    setTheNumber: {
      options: {
        actions: [{
          xpath: '/abc',
          value: '123'
        }, {
          xpath: '/abc/@attr',
          value: '2'
        }],
      }
      files: { 'dest/output.xml': 'src/test.xml' }
    },
  },
})

Example - Update with function as value

In this example, the value of an attribute is modified. So if the test.xml file has the content <x y="abc" />, the generated result in this case would be <x y="ABC" />.

grunt.initConfig({
  xmlstoke: {
    upperCaseAttr: {
      options: {
        actions: [{
          xpath: '/x/@y',
          value: function (node) { return node.value.toUpperCase(); }
        }]
      },
      files: {
        'dest/output.xml': 'src/test.xml',
      },
    },
  },
})

Example - Multiple XPath Selectors

In this example, the same value is updated in multiple locations. So if the testing.xml file has the content <x y="999" />, the generated result in this case would be <x y="111">111</x>.

grunt.initConfig({
  xmlstoke: {
    updateAllTheThings: {
      options: {
        actions: [{
          xpath: ['/x/@y','/x'],
          value: '111'
        ]}
      },
      files: {
        'dest/output.xml': 'src/test.xml',
      },
    },
  },
})

Example - Deleting Nodes

Given <x><a /><a /><b /><c /></x>, will delete all matched nodes and return <x></x> (<x />)

grunt.initConfig({
  xmlstoke: {
    deleteStuff: {
      options: {
        actions: [{
          type: 'del'
          xpath: ['/x/a', '//b'],
        }]
      },
      files: {
        'dest/output.xml': 'src/test.xml',
      },
    },
  },
})

Example - Inserting Elements and Attributes

Given <x a="1"></x>, returns <x a="2"><foo bar="baz"/></a>. Notice how an Insertion just performs an update if the node exists already as seen with the @a attribute

grunt.initConfig({
  xmlstoke: {
    updateAllTheThings: {
      options: {
        actions: [{
          type: 'ins'
          xpath: '//x',
          node: '@a',
          value: '2'
        }, {
          type: 'ins'
          xpath: '//x',
          node: 'foo'
        }, {
          type: 'ins'
          xpath: '//x/foo',
          node: '@bar'
          value: 'baz'
        }]
      },
      files: {
        'dest/output.xml': 'src/test.xml',
      },
    },
  },
})

Example - Namespaces and Reads

This example covers both basic Reads and namespaces. options.namespaces must be specified whenever operations inolve namespaces. After that, simply use the usual ns:elemName or ns:attrName syntax everywhere.

Given <RDF><foo>A</foo><em:bar>B</em:bar></RDF>, persists the values read from the tags in grunt.option, then retrieves them in value callbacks in order to swap them in the resulting XML: <RDF><foo>B</foo><em:bar>A</em:bar></RDF>)

grunt.initConfig({
  xmlstoke: {
    rebuildScreensTag: {
      options: {
       namespaces: { 'em': 'http://www.mozilla.org/2004/em-rdf#' },
        actions: [{
          type: 'R'
          xpath: '//foo',
          saveAs: 'myFoo'
        }, {
          type: 'R'
          xpath: '//em:bar',
          saveAs: 'myBarAsArray',
          returnArray: true
        }, {
          xpath: '//em:bar',
          value: function (node) { return grunt.option('myFoo'); }
        }, , {
          xpath: '//foo',
          value: function (node) { return grunt.option('myBarAsArray')[0]; }
        }]
      },
      files: {
        'dest/output.xml': 'src/test.xml',
      },
    },
  },
})

Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using Grunt.

Release History

  • 0.1.0
    — Initial release
  • 0.2.0
    — Multiple replacements at once
  • 0.2.1
    — Color filename when logged
  • 0.3.0
    — Allow specifying replacement value as a function
  • 0.4.0
    — Allow specifying namespaces
  • 0.5.0 (point of fork from grunt-xmlpoke)
    — Allow adding elements or attributes via insertions option
  • 0.5.1
    — Bugfixes
  • 0.5.2
    — Allow [index] selection for insertion xpath (stripped from name for actual element creation)
    — Allow removing elements via deletions option (barely tested)
  • 0.6.0
    — Code cleanup
  • 0.7.0
    — Fixed deleting attribute nodes
    — Added updates option as an alias for replacements
    — Added reads option to extract node values by xpath and save them to grunt.option
    — Added actions option as a series of CRUD actions in arbitrary order
    — Added tests for deletions, reads and new alias parameters
    TODO: Add tests for expected error scenarios