smartxml: append/prepend shortcuts for text nodes
[fnpeditor.git] / src / smartxml / core.js
1 define(function(require) {
2     
3 'use strict';
4 /* globals Node */
5
6 var _ = require('libs/underscore');
7
8
9 var INSERTION = function(implementation) {
10     var toret = function(node, options) {
11         var insertion = this.getNodeInsertion(node),
12             nodeWasContained = this.document.containsNode(insertion.ofNode),
13             nodeParent,
14             returned;
15         options = options || {};
16         if(!(this.document.containsNode(this))) {
17             nodeParent = insertion.ofNode.parent();
18         }
19         returned = implementation.call(this, insertion.ofNode);
20         if(!options.silent && returned.sameNode(insertion.ofNode)) {
21             this.triggerChangeEvent(insertion.insertsNew ? 'nodeAdded' : 'nodeMoved', {node: insertion.ofNode}, nodeParent, nodeWasContained);
22         }
23         return returned;
24     };
25     return toret;
26 };
27
28 var documentNodeTransformations = {
29     detach: function() {
30         var parent = this.parent(),
31             existed = this.document.containsNode(this);
32         this._$.detach();
33         if(existed) {
34             this.triggerChangeEvent('nodeDetached', {parent: parent});
35         }
36         return this;
37     },
38
39     replaceWith: function(node) {
40         var toret;
41         if(this.isRoot()) {
42             return this.document.replaceRoot(node);
43         }
44         toret = this.after(node);
45         this.detach();
46         return toret;
47     },
48
49     after: INSERTION(function(node) {
50         var next = this.next();
51         if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
52             next.setText(node.getText() + next.getText());
53             node.detach();
54             return next;
55         }
56         this._$.after(node.nativeNode);
57         return node;
58     }),
59
60     before: INSERTION(function(node) {
61         var prev = this.prev();
62         if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
63             prev.setText(prev.getText() + node.getText());
64             node.detach();
65             return prev;
66         }
67         this._$.before(node.nativeNode);
68         return node;
69     }),
70
71     wrapWith: function(node) {
72         var insertion = this.getNodeInsertion(node);
73         if(this.parent()) {
74             this.before(insertion.ofNode);
75         }
76         insertion.ofNode.append(this);
77         return insertion.ofNode;
78     },
79
80     /**
81     * Removes parent of a node if node has no siblings.
82     */
83     unwrap: function() {
84         if(this.isRoot()) {
85             return;
86         }
87         var parent = this.parent(),
88             grandParent;
89         if(parent.contents().length === 1) {
90             grandParent = parent.parent();
91             parent.unwrapContent();
92             return grandParent;
93         }
94     }
95 };
96
97 var elementNodeTransformations = {
98
99     detach: function() {
100         var next;
101         if(this.parent() && this.isSurroundedByTextElements()) {
102             next = this.next();
103             this.prev().appendText(next.getText());
104             next.detach();
105         }
106         return this.__super__.detach();
107     },
108
109     setTag: function(tagName) {
110         var node = this.document.createDocumentNode({tagName: tagName}),
111             oldTagName = this.getTagName(),
112             myContents = this._$.contents();
113
114         this.getAttrs().forEach(function(attribute) {
115             node.setAttr(attribute.name, attribute.value, true);
116         });
117         node.setData(this.getData());
118
119         if(this.sameNode(this.document.root)) {
120             this.document._defineDocumentProperties(node._$);
121         }
122
123         /* TODO: This invalidates old references to this node. Caching instances on nodes would fix this. */
124         this._$.replaceWith(node._$);
125         this._setNativeNode(node._$[0]);
126         this._$.append(myContents);
127         this.triggerChangeEvent('nodeTagChange', {oldTagName: oldTagName, newTagName: this.getTagName()});
128     },
129
130
131     setAttr: function(name, value, silent) {
132         var oldVal = this.getAttr(name);
133         this._$.attr(name, value);
134         if(!silent) {
135             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
136         }
137     },
138
139     append: INSERTION(function(node) {
140         var last = _.last(this.contents());
141         if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
142             last.setText(last.getText() + node.getText());
143             node.detach();
144             return last;
145         } else {
146             this._$.append(node.nativeNode);
147             return node;
148         }
149     }),
150
151     prepend: INSERTION(function(node) {
152         var first = this.contents()[0];
153         if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
154             first.setText(node.getText() + first.getText());
155             node.detach();
156             return first;
157         } else {
158             this._$.prepend(node.nativeNode);
159             return node;
160         }
161     }),
162
163     insertAtIndex: function(nativeNode, index) {
164         var contents = this.contents();
165         if(index < contents.length) {
166             return contents[index].before(nativeNode);
167         } else if(index === contents.length) {
168             return this.append(nativeNode);
169         }
170     },
171
172     unwrapContent: function() {
173         var parent = this.parent();
174         if(!parent) {
175             return;
176         }
177
178         var myContents = this.contents(),
179             myIdx = parent.indexOf(this);
180
181
182         if(myContents.length === 0) {
183             return this.detach();
184         }
185
186
187         var childrenLength = this.contents().length,
188             first = true,
189             shiftRange = false;
190         this.contents().forEach(function(child) {
191             var returned = this.before(child);
192             if(first && !(returned.sameNode(child))) {
193                 shiftRange = true;
194                 first = false;
195             }
196         }.bind(this));
197
198         this.detach();
199
200         return {
201             element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
202             element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
203         };
204     },
205
206     wrapText: function(params) {
207         return this.document._wrapText(_.extend({inside: this}, params));
208     }
209 };
210
211 var textNodeTransformations = {
212     setText: {
213         impl: function(t, text) {
214             t.oldText = this.getText();
215             this.nativeNode.data = text;
216             this.triggerTextChangeEvent();
217         },
218         undo: function(t) {
219             this.setText(t.oldText);
220         }
221     },
222
223     before: INSERTION(function(node) {
224         if(node.nodeType === Node.TEXT_NODE) {
225             this.prependText(node.getText());
226             node.detach();
227             return this;
228         } else {
229             return this.__super__.before(node, {silent:true});
230         }
231     }),
232
233     after: INSERTION(function(node) {
234         if(node.nodeType === Node.TEXT_NODE) {
235             this.appendText(node.getText());
236             node.detach();
237             return this;
238         } else {
239             return this.__super__.after(node, {silent:true});
240         }
241     }),
242
243     append: function(node) {
244         if(node.nodeType === Node.TEXT_NODE) {
245             this.appendText(node.getText());
246             node.detach();
247             return this;
248         }
249     },
250     prepend: function(node) {
251         if(node.nodeType === Node.TEXT_NODE) {
252             this.prependText(node.getText());
253             node.detach();
254             return this;
255         }
256     },
257
258     appendText: function(text) {
259         this.nativeNode.data = this.nativeNode.data + text;
260         this.triggerTextChangeEvent();
261     },
262
263     prependText: function(text) {
264         this.nativeNode.data = text + this.nativeNode.data;
265         this.triggerTextChangeEvent();
266     },
267
268     wrapWith: function(desc) {
269         if(typeof desc.start === 'number' && typeof desc.end === 'number') {
270             return this.document._wrapText({
271                 inside: this.parent(),
272                 textNodeIdx: this.parent().indexOf(this),
273                 offsetStart: Math.min(desc.start, desc.end),
274                 offsetEnd: Math.max(desc.start, desc.end),
275                 _with: {tagName: desc.tagName, attrs: desc.attrs}
276             });
277         } else {
278             return this.__super__.wrapWith.call(this, desc);
279         }
280     },
281
282     split: function(params) {
283         var parentElement = this.parent(),
284             passed = false,
285             succeedingChildren = [],
286             prefix = this.getText().substr(0, params.offset),
287             suffix = this.getText().substr(params.offset);
288
289         parentElement.contents().forEach(function(child) {
290             if(passed) {
291                 succeedingChildren.push(child);
292             }
293             if(child.sameNode(this)) {
294                 passed = true;
295             }
296         }.bind(this));
297
298         if(prefix.length > 0) {
299             this.setText(prefix);
300         }
301         else {
302             this.detach();
303         }
304
305         var attrs = {};
306         parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
307         var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
308         parentElement.after(newElement);
309
310         if(suffix.length > 0) {
311             newElement.append({text: suffix});
312         }
313         succeedingChildren.forEach(function(child) {
314             newElement.append(child);
315         });
316
317         return {first: parentElement, second: newElement};
318     },
319
320     divideWithElementNode: function(node, params) {
321         var insertion = this.getNodeInsertion(node),
322             myText = this.getText();
323
324         if(params.offset === myText.length) {
325             return this.after(node);
326         }
327         if(params.offset === 0) {
328             return this.before(node);
329         }
330
331         var lhsText = myText.substr(0, params.offset),
332             rhsText = myText.substr(params.offset),
333             rhsTextNode = this.document.createDocumentNode({text: rhsText});
334
335         this.setText(lhsText);
336         this.after(insertion.ofNode);
337         insertion.ofNode.after(rhsTextNode);
338         return insertion.ofNode;
339     }
340 };
341
342 var documentTransformations = {
343     wrapNodes: function(params) {
344         if(!(params.node1.parent().sameNode(params.node2.parent()))) {
345             throw new Error('Wrapping non-sibling nodes not supported.');
346         }
347
348         var parent = params.node1.parent(),
349             parentContents = parent.contents(),
350             wrapper = this.createDocumentNode({
351                 tagName: params._with.tagName,
352                 attrs: params._with.attrs}),
353             idx1 = parent.indexOf(params.node1),
354             idx2 = parent.indexOf(params.node2);
355
356         if(idx1 > idx2) {
357             var tmp = idx1;
358             idx1 = idx2;
359             idx2 = tmp;
360         }
361
362         var insertingMethod, insertingTarget;
363         if(idx1 === 0) {
364             insertingMethod = 'prepend';
365             insertingTarget = parent;
366         } else {
367             insertingMethod = 'after';
368             insertingTarget = parentContents[idx1-1];
369         }
370
371         for(var i = idx1; i <= idx2; i++) {
372             wrapper.append(parentContents[i].detach());
373         }
374
375         insertingTarget[insertingMethod](wrapper);
376         return wrapper;
377     },
378
379     _wrapText: function(params) {
380         params = _.extend({textNodeIdx: 0}, params);
381         if(typeof params.textNodeIdx === 'number') {
382             params.textNodeIdx = [params.textNodeIdx];
383         }
384         
385         var contentsInside = params.inside.contents(),
386             idx1 = Math.min.apply(Math, params.textNodeIdx),
387             idx2 = Math.max.apply(Math, params.textNodeIdx),
388             textNode1 = contentsInside[idx1],
389             textNode2 = contentsInside[idx2],
390             sameNode = textNode1.sameNode(textNode2),
391             prefixOutside = textNode1.getText().substr(0, params.offsetStart),
392             prefixInside = textNode1.getText().substr(params.offsetStart),
393             suffixInside = textNode2.getText().substr(0, params.offsetEnd),
394             suffixOutside = textNode2.getText().substr(params.offsetEnd)
395         ;
396
397         if(!(textNode1.parent().sameNode(textNode2.parent()))) {
398             throw new Error('Wrapping text in non-sibling text nodes not supported.');
399         }
400         
401         var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
402         textNode1.after(wrapperElement);
403         textNode1.detach();
404         
405         if(prefixOutside.length > 0) {
406             wrapperElement.before({text:prefixOutside});
407         }
408         if(sameNode) {
409             var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
410             wrapperElement.append({text: core});
411         } else {
412             textNode2.detach();
413             if(prefixInside.length > 0) {
414                 wrapperElement.append({text: prefixInside});
415             }
416             for(var i = idx1 + 1; i < idx2; i++) {
417                 wrapperElement.append(contentsInside[i]);
418             }
419             if(suffixInside.length > 0) {
420                 wrapperElement.append({text: suffixInside});
421             }
422         }
423         if(suffixOutside.length > 0) {
424             wrapperElement.after({text: suffixOutside});
425         }
426         return wrapperElement;
427     },
428     replaceRoot: function(node) {
429         var insertion = this.getNodeInsertion(node);
430         this.root.detach();
431         this._defineDocumentProperties(insertion.ofNode._$);
432         insertion.ofNode.triggerChangeEvent('nodeAdded');
433         return insertion.ofNode;
434     }
435 };
436
437 return {
438     document: {
439         transformations: documentTransformations
440     },
441     documentNode: {
442         transformations: documentNodeTransformations
443     },
444     elementNode: {
445         transformations: elementNodeTransformations
446     },
447     textNode: {
448         transformations: textNodeTransformations
449     }
450 };
451
452 });