1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
|
/**
@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')]
));
|