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