/** @namespace */ (function(module, ns) { var Template = ns.require('template'); /** * Builds a node of a calendar-data or filter xml element. * * @param {QueryBuilder} builder instance. * @param {String} name component/prop name (like RRULE/VEVENT). * @param {Boolean} isProp is this node a property tag? */ function Node(builder, name, isProp) { this.name = name; this.builder = builder; this.isProp = !!isProp; this.comps = Object.create(null); this.props = Object.create(null); } Node.prototype = { /** * Hook for adding custom node content. * (for unsupported or custom filters) * * Usually you never want to use this. * * @type {Null|Array} */ content: null, /** * Appends custom string content into node. * * @param {String} string content. */ appendString: function(string) { if (!this.content) { this.content = []; } if (typeof(string) !== 'string') { string = string.toString(); } this.content.push(string); }, _timeRange: null, /** * Adds a time range element to the node. * * Example: * * var node; * * // key/values not validated or converted * // but directly piped into the time-range element. * node.setTimeRange({ * start: '20060104T000000Z', * end: '20060105T000000Z' * }); * * // when null removes element * node.setTimeRange(null); * * @param {Object|Null} range time range or null to remove. */ setTimeRange: function(range) { this._timeRange = range; }, /** * Removes a property from the output. * @param {String} name prop. */ removeProp: function(name) { delete this.props[name]; }, /** * Removes a component from the output. * * @param {String} name comp. */ removeComp: function(name) { delete this.comps[name]; }, _addNodes: function(type, nodes) { // return value when is array var result = this; if (!Array.isArray(nodes)) { // clear out the return value as we will // now use the first node. result = null; } nodes = (Array.isArray(nodes)) ? nodes : [nodes]; var idx = 0; var len = nodes.length; var name; var node; for (; idx < len; idx++) { name = nodes[idx]; node = new Node(this.builder, name, type === 'props'); this[type][name] = node; } // when we where not given an array of nodes // assume we want one specific one so set that // as the return value. if (!result) result = node; return result; }, /** * Adds one or more props. * If property already exists will not add * duplicates but return the existing property. * * @param {String|Array[String]} prop one or more properties to add. * @return {Node|Self} returns a node or self when given an array. */ prop: function(prop) { return this._addNodes('props', prop); }, /** * Adds one or more comp. * If comp already exists will not add * duplicates but return the existing comp. * * @param {String|Array[String]} comp one or more components to add. * @return {Node|Self} returns a node or self when given an array. */ comp: function(comp) { return this._addNodes('comps', comp); }, xmlAttributes: function() { return { name: this.name }; }, /** * Transform tree into a string. * * NOTE: order is not preserved at all here. * It is highly unlikely that order is a * factor for calendar-data or filter * but this is fair warning for other uses. */ toString: function() { var content = ''; var key; var template = this.builder.template; if (this._timeRange) { content += template.tag( ['caldav', 'time-range'], this._timeRange ); } // render out children for (key in this.props) { content += this.props[key].toString(); } for (key in this.comps) { content += this.comps[key].toString(); } if (this.content) { content += this.content.join(''); } // determine the tag name var tag; if (this.isProp) { tag = this.builder.propTag; } else { tag = this.builder.compTag; } // build the xml element and return it. return template.tag( tag, this.xmlAttributes(), content ); } }; /** * Query builder can be used to build xml document fragments * for calendar-data & calendar-filter. * (and any other xml document with a similar structure) * * Options: * - template: (Caldav.Template instance) * - tag: container tag (like 'calendar-data') * - attributes: attributes for root * - compTag: name of comp[onent] tag name (like 'comp') * - propTag: name of property tag (like 'prop') * * @param {Object} options query builder options. */ function QueryBuilder(options) { if (!options) options = {}; if (!(options.template instanceof Template)) { throw new TypeError( '.template must be an instance' + ' of Caldav.Template given "' + options.template + '"' ); } for (var key in options) { if (options.hasOwnProperty(key)) { this[key] = options[key]; } } } QueryBuilder.prototype = { tag: ['caldav', 'calendar-data'], compTag: ['caldav', 'comp'], propTag: ['caldav', 'prop'], attributes: null, _limitRecurrenceSet: null, /** * Adds the recurrence set limit child to the query. * Directly maps to the caldav 'limit-recurrence-set' element. * * Examples: * * var builder; * * // no validation or formatting is done. * builder.setRecurrenceSetLimit({ * start: '20060103T000000Z', * end: '20060103T000000Z' * }); * * // use null to clear value * builder.setRecurrenceSetLimit(null); * * @param {Object|Null} limit see above. */ setRecurrenceSetLimit: function(limit) { this._limitRecurrenceSet = limit; }, /** * @param {String} name component name (like VCALENDAR). * @return {QueryBuilder.Node} node instance. */ setComp: function(name) { return this._compRoot = new Node(this, name); }, /** * Returns the root node of the document fragment. */ getComp: function() { return this._compRoot; }, toString: function() { var content = ''; var comp = this.getComp(); if (this._limitRecurrenceSet) { content += this.template.tag( ['caldav', 'limit-recurrence-set'], this._limitRecurrenceSet ); } if (comp) { content += comp.toString(); } return this.template.tag( this.tag, this.attributes, content ); } }; QueryBuilder.Node = Node; module.exports = QueryBuilder; }.apply( this, (this.Caldav) ? [Caldav('query_builder'), Caldav] : [module, require('./caldav')] ));