some other minor changes from milpeer
[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)) || !insertion.insertsNew) {
17             nodeParent = insertion.ofNode.parent();
18         }
19         if(!insertion.insertsNew && insertion.ofNode.isSurroundedByTextNodes()) {
20             var prev = insertion.ofNode.prev(),
21                 next = insertion.ofNode.next();
22             prev.setText(prev.getText()+next.getText());
23             next.detach();
24         }
25         returned = implementation.call(this, insertion.ofNode);
26         if(!options.silent && returned && returned.sameNode(insertion.ofNode)) {
27             if(!insertion.insertsNew) {
28                 this.triggerChangeEvent('nodeDetached', {node: insertion.ofNode, parent: nodeParent, move: true});
29             }
30             this.triggerChangeEvent('nodeAdded', {node: insertion.ofNode, move: !insertion.insertsNew}, nodeParent, nodeWasContained);
31         }
32         return returned;
33     };
34     return toret;
35 };
36
37 var documentNodeTransformations = {
38     detach: function() {
39         var parent = this.parent(),
40             existed = this.document.containsNode(this);
41         this._$.detach();
42         if(existed) {
43             this.triggerChangeEvent('nodeDetached', {parent: parent});
44             if(!parent) {
45                 // This was the root of the document
46                 this.document._defineDocumentProperties(null);
47             }
48         }
49         return this;
50     },
51
52     replaceWith: function(node) {
53         var toret;
54         if(this.isRoot()) {
55             return this.document.replaceRoot(node);
56         }
57         if(this.parent()) {
58             toret = this.after(node);
59             this.detach();
60             return toret;
61         }
62         throw new Error('Cannot replace node without a parent.');
63     },
64
65     after: INSERTION(function(node) {
66         if(this.isRoot()) {
67             return;
68         }
69         var next = this.next();
70
71         if(next && next.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
72             next.setText(node.getText() + next.getText());
73             node.detach();
74             return next;
75         }
76         this._$.after(node.nativeNode);
77         return node;
78     }),
79
80     before: INSERTION(function(node) {
81         if(this.isRoot()) {
82             return;
83         }
84         var prev = this.prev();
85         if(prev && prev.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
86             prev.setText(prev.getText() + node.getText());
87             node.detach();
88             return prev;
89         }
90         this._$.before(node.nativeNode);
91         return node;
92     }),
93
94     wrapWith: function(node) {
95         var insertion = this.getNodeInsertion(node);
96
97         if(this.parent() || this.isRoot()) {
98             this.replaceWith(insertion.ofNode);
99         }
100         insertion.ofNode.append(this);
101         return insertion.ofNode;
102     },
103
104     /**
105     * Removes parent of a node if node has no siblings.
106     */
107     unwrap: function() {
108         if(this.isRoot()) {
109             return;
110         }
111         var parent = this.parent(),
112             grandParent;
113         if(parent.contents().length === 1) {
114             grandParent = parent.parent();
115             parent.unwrapContent();
116             return grandParent;
117         }
118     }
119 };
120
121 var elementNodeTransformations = {
122
123     detach: function(params) {
124         var next, prev;
125
126         params = params || {};
127
128         if(!params.normalizeStrategy) {
129             params.normalizeStrategy = 'merge';
130         }
131
132         if(this.parent() && this.isSurroundedByTextNodes()) {
133             if(params.normalizeStrategy === 'detach-left') {
134                 this.prev().detach();
135             } else if(params.normalizeStrategy === 'detach-right') {
136                 this.next().detach();
137             } else if(params.normalizeStrategy === 'merge') {
138                 next = this.next();
139                 prev = this.prev();
140                 params.ret = {
141                     mergedTo: prev,
142                     previousLen: prev.getText().length
143                 };
144                 prev.appendText(next.getText());
145                 next.detach();
146             } else {
147                 throw new Error('unknown normalize strategy for detach');
148             }
149         }
150         return this.__super__.detach();
151     },
152
153     setTag: function(tagName) {
154         var node = this.document.createDocumentNode({tagName: tagName});
155
156         this.getAttrs().forEach(function(attribute) {
157             node.setAttr(attribute.name, attribute.value);
158         });
159
160         this.contents().forEach(function(child) {
161             node.append(child);
162         });
163
164         node.setData(this.getData());
165
166         this.replaceWith(node);
167         return node;
168     },
169
170     setAttr: function(name, value, silent) {
171         var oldVal = this.getAttr(name);
172         if(_.isUndefined(value)) {
173             this._$.removeAttr(name);
174         } else {
175             this._$.attr(name, value);
176         }
177         if(!silent) {
178             this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value});
179         }
180     },
181
182     append: INSERTION(function(node) {
183         var last = _.last(this.contents());
184         if(last && last.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
185             last.setText(last.getText() + node.getText());
186             node.detach();
187             return last;
188         } else {
189             this._$.append(node.nativeNode);
190             return node;
191         }
192     }),
193
194     prepend: INSERTION(function(node) {
195         var first = this.contents()[0];
196         if(first && first.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
197             first.setText(node.getText() + first.getText());
198             node.detach();
199             return first;
200         } else {
201             this._$.prepend(node.nativeNode);
202             return node;
203         }
204     }),
205
206     insertAtIndex: function(nativeNode, index) {
207         var contents = this.contents();
208         if(index < contents.length) {
209             return contents[index].before(nativeNode);
210         } else if(index === contents.length) {
211             return this.append(nativeNode);
212         }
213     },
214
215     unwrapContent: function() {
216         var parent = this.parent();
217         if(!parent) {
218             return;
219         }
220
221         this.contents()
222             .filter(function(child) {
223                 return child.getProperty('describesParent');
224             }.bind(this))
225             .forEach(function(child) {
226                 child.detach();
227             });
228
229         var myContents = this.contents(),
230             myIdx = parent.indexOf(this);
231
232         if(myContents.length === 0) {
233             return this.detach();
234         }
235
236         var childrenLength = this.contents().length,
237             first = true,
238             shiftRange = false;
239         this.contents().forEach(function(child) {
240             var returned = this.before(child);
241             if(first && !(returned.sameNode(child))) {
242                 shiftRange = true;
243                 first = false;
244             }
245         }.bind(this));
246
247         this.detach();
248
249         return {
250             element1: parent.contents()[myIdx + (shiftRange ? -1 : 0)],
251             element2: parent.contents()[myIdx + childrenLength-1 + (shiftRange ? -1 : 0)]
252         };
253     },
254
255     wrapText: function(params) {
256         return this.document._wrapText(_.extend({inside: this}, params));
257     }
258 };
259
260 var textNodeTransformations = {
261     setText: {
262         impl: function(t, text) {
263             t.oldText = this.getText();
264             this.nativeNode.data = text;
265             this.triggerTextChangeEvent();
266         },
267         undo: function(t) {
268             this.setText(t.oldText);
269         }
270     },
271
272     before: INSERTION(function(node) {
273         if(node.nodeType === Node.TEXT_NODE) {
274             this.prependText(node.getText());
275             node.detach();
276             return this;
277         } else {
278             return this.__super__.before(node, {silent:true});
279         }
280     }),
281
282     after: INSERTION(function(node) {
283         if(node.nodeType === Node.TEXT_NODE) {
284             this.appendText(node.getText());
285             node.detach();
286             return this;
287         } else {
288             return this.__super__.after(node, {silent:true});
289         }
290     }),
291
292     append: function(node) {
293         if(node.nodeType === Node.TEXT_NODE) {
294             this.appendText(node.getText());
295             node.detach();
296             return this;
297         }
298     },
299     prepend: function(node) {
300         if(node.nodeType === Node.TEXT_NODE) {
301             this.prependText(node.getText());
302             node.detach();
303             return this;
304         }
305     },
306
307     appendText: function(text) {
308         this.nativeNode.data = this.nativeNode.data + text;
309         this.triggerTextChangeEvent();
310     },
311
312     prependText: function(text) {
313         this.nativeNode.data = text + this.nativeNode.data;
314         this.triggerTextChangeEvent();
315     },
316
317     wrapWith: function(desc) {
318         if(typeof desc.start === 'number' && typeof desc.end === 'number') {
319             return this.document._wrapText({
320                 inside: this.parent(),
321                 textNodeIdx: this.parent().indexOf(this),
322                 offsetStart: Math.min(desc.start, desc.end),
323                 offsetEnd: Math.max(desc.start, desc.end),
324                 _with: {tagName: desc.tagName, attrs: desc.attrs}
325             });
326         } else {
327             return this.__super__.wrapWith.call(this, desc);
328         }
329     },
330
331     split: function(params) {
332         var parentElement = this.parent(),
333             passed = false,
334             succeedingChildren = [],
335             prefix = this.getText().substr(0, params.offset),
336             suffix = this.getText().substr(params.offset);
337
338         parentElement.contents().forEach(function(child) {
339             if(passed) {
340                 succeedingChildren.push(child);
341             }
342             if(child.sameNode(this)) {
343                 passed = true;
344             }
345         }.bind(this));
346
347         if(prefix.length > 0) {
348             this.setText(prefix);
349         }
350         else {
351             this.detach();
352         }
353
354         var attrs = {};
355         parentElement.getAttrs().forEach(function(attr) {attrs[attr.name] = attr.value; });
356         var newElement = this.document.createDocumentNode({tagName: parentElement.getTagName(), attrs: attrs});
357         parentElement.after(newElement);
358
359         succeedingChildren.reverse().forEach(function(child) {
360             newElement.prepend(child);
361         });
362         if(suffix.length > 0) {
363             newElement.prepend({text: suffix});
364         }
365
366         return {first: parentElement, second: newElement, created: newElement};
367     },
368
369     divideWithElementNode: function(node, params) {
370         var insertion = this.getNodeInsertion(node),
371             myText = this.getText();
372
373         if(params.offset === myText.length) {
374             return this.after(node);
375         }
376         if(params.offset === 0) {
377             return this.before(node);
378         }
379
380         var lhsText = myText.substr(0, params.offset),
381             rhsText = myText.substr(params.offset),
382             rhsTextNode = this.document.createDocumentNode({text: rhsText});
383
384         this.setText(lhsText);
385         this.after(insertion.ofNode);
386         insertion.ofNode.after(rhsTextNode);
387         return insertion.ofNode;
388     }
389 };
390
391 var documentTransformations = {
392     wrapNodes: function(params) {
393         if(!(params.node1.parent().sameNode(params.node2.parent()))) {
394             throw new Error('Wrapping non-sibling nodes not supported.');
395         }
396
397         var parent = params.node1.parent(),
398             parentContents = parent.contents(),
399             wrapper = this.createDocumentNode({
400                 tagName: params._with.tagName,
401                 attrs: params._with.attrs}),
402             idx1 = parent.indexOf(params.node1),
403             idx2 = parent.indexOf(params.node2);
404
405         if(idx1 > idx2) {
406             var tmp = idx1;
407             idx1 = idx2;
408             idx2 = tmp;
409         }
410
411         var insertingMethod, insertingTarget;
412         if(idx1 === 0) {
413             insertingMethod = 'prepend';
414             insertingTarget = parent;
415         } else {
416             insertingMethod = 'after';
417             insertingTarget = parentContents[idx1-1];
418         }
419
420         for(var i = idx1; i <= idx2; i++) {
421             if(!parentContents[i].getProperty('describesParent')) {
422                 wrapper.append(parentContents[i].detach());
423             }
424         }
425
426         insertingTarget[insertingMethod](wrapper);
427         return wrapper;
428     },
429
430     _wrapText: function(params) {
431         params = _.extend({textNodeIdx: 0}, params);
432         if(typeof params.textNodeIdx === 'number') {
433             params.textNodeIdx = [params.textNodeIdx];
434         }
435         
436         var contentsInside = params.inside.contents(),
437             idx1 = Math.min.apply(Math, params.textNodeIdx),
438             idx2 = Math.max.apply(Math, params.textNodeIdx),
439             textNode1 = contentsInside[idx1],
440             textNode2 = contentsInside[idx2],
441             sameNode = textNode1.sameNode(textNode2),
442             prefixOutside = textNode1.getText().substr(0, params.offsetStart),
443             prefixInside = textNode1.getText().substr(params.offsetStart),
444             suffixInside = textNode2.getText().substr(0, params.offsetEnd),
445             suffixOutside = textNode2.getText().substr(params.offsetEnd)
446         ;
447
448         if(!(textNode1.parent().sameNode(textNode2.parent()))) {
449             throw new Error('Wrapping text in non-sibling text nodes not supported.');
450         }
451         
452         var wrapperElement = this.createDocumentNode({tagName: params._with.tagName, attrs: params._with.attrs});
453         textNode1.after(wrapperElement);
454         textNode1.detach();
455         
456         if(prefixOutside.length > 0) {
457             wrapperElement.before({text:prefixOutside});
458         }
459         if(sameNode) {
460             var core = textNode1.getText().substr(params.offsetStart, params.offsetEnd - params.offsetStart);
461             wrapperElement.append({text: core});
462         } else {
463             textNode2.detach();
464             if(prefixInside.length > 0) {
465                 wrapperElement.append({text: prefixInside});
466             }
467             for(var i = idx1 + 1; i < idx2; i++) {
468                 if(!contentsInside[i].getProperty('describesParent')) {
469                     wrapperElement.append(contentsInside[i]);
470                 }
471             }
472             if(suffixInside.length > 0) {
473                 wrapperElement.append({text: suffixInside});
474             }
475         }
476         if(suffixOutside.length > 0) {
477             wrapperElement.after({text: suffixOutside});
478         }
479         return wrapperElement;
480     },
481     replaceRoot: function(node) {
482         var insertion = this.getNodeInsertion(node);
483         this.root.detach();
484         this._defineDocumentProperties(insertion.ofNode._$);
485         insertion.ofNode.triggerChangeEvent('nodeAdded');
486         return insertion.ofNode;
487     },
488     deleteText: function(params) {
489         var ptr, next, nextNext, toDetach, middle, text;
490
491         if(params.from.node.sameNode(params.to.node)) {
492             ptr = params.from.node;
493             text = ptr.getText();
494             ptr.setText(text.substr(0, params.from.offset) + text.substr(params.to.offset));
495             return;
496         }
497
498         // Both edge text nodes need to be edited before anything else happen in case that
499         // they get merged when detaching content between them.
500         params.from.node.setText(params.from.node.getText().substr(0, params.from.offset));
501         params.to.node.setText(params.to.node.getText().substr(params.to.offset));
502
503         ptr = params.from.node;
504         next = ptr.next();
505
506         while(next || ptr.parent()) {
507             if(next) {
508                 if(next.sameNode(params.to.node)) {
509                     return;
510                 }
511                 else if(next.nodeType === Node.ELEMENT_NODE && next.containsNode(params.to.node)) {
512                     middle = next;
513                     break;
514                 } else {
515                     toDetach = next;
516                     next = next.next();
517                     nextNext = next ? next.next() : null;
518                     toDetach.detach({normalizeStrategy: (next && next.sameNode(params.to.node)) ? 'merge' : 'detach-right'});
519                     if(next && !next.isInDocument()) {
520                         if(next.sameNode(params.to.node)) {
521                             return;
522                         }
523                         next = nextNext;
524                     }
525                 }
526             } else {
527                 ptr = ptr.parent();
528                 next = ptr.next();
529             }
530         }
531
532         if(!this.containsNode(params.to.node)) {
533             // The end node was merged during detaching nodes above - there is nothing more left to do.
534             return;
535         }
536
537         ptr = middle.contents()[0];
538         while(ptr && !ptr.sameNode(params.to.node)) {
539             if(ptr.nodeType === Node.ELEMENT_NODE && ptr.containsNode(params.to.node)) {
540                 ptr = ptr.contents()[0];
541                 continue;
542             } else {
543                 ptr = ptr.next();
544                 ptr.prev().detach();
545             }
546         }
547     }
548 };
549
550 return {
551     document: {
552         transformations: documentTransformations
553     },
554     documentNode: {
555         transformations: documentNodeTransformations
556     },
557     elementNode: {
558         transformations: elementNodeTransformations
559     },
560     textNode: {
561         transformations: textNodeTransformations
562     }
563 };
564
565 });