editor: Setting descriptions for transactions performed in actions
[fnpeditor.git] / src / wlxml / wlxml.js
1 define([
2     'libs/jquery',
3     'libs/underscore',
4     'smartxml/smartxml',
5     'smartxml/transformations',
6     'wlxml/extensions/metadata/metadata'
7 ], function($, _, smartxml, transformations, metadataExtension) {
8     
9 'use strict';
10
11 /* globals Node */
12
13
14 var WLXMLDocumentNodeMethods =  {
15     isInside: function(query) {
16         var parent = this.getParent(query);
17         return !!parent;
18     },
19     getParent: function(query) {
20         /* globals Node */
21         var me = this.nodeType === Node.ELEMENT_NODE ? [this] : [],
22             toret;
23         me.concat(this.parents()).some(function(node) {
24             if(node.is(query)) {
25                 toret = node;
26                 return true;
27             }
28         });
29         return toret;
30     },
31 };
32
33 var AttributesList = function() {};
34 AttributesList.prototype = Object.create({});
35 AttributesList.prototype.keys = function() {
36     return _.keys(this);
37 };
38
39 var getClassLists = function(klassName) {
40     var toret = [],
41         classParts = [''].concat(klassName.split('.')),
42         classCurrent;
43
44     classParts.forEach(function(part) {
45         classCurrent = classCurrent ? classCurrent + '.' + part : part;
46         toret.push(classCurrent);
47     });
48     return toret;
49 };
50
51 var installObject = function(instance, klass) {
52     var methods = {},
53         transformations = {};
54
55     getClassLists(klass).forEach(function(klassName) {
56         _.extend(methods, instance.document.classMethods[klassName] || {});
57         _.extend(methods, instance.document.classTransformations[klassName] || {});
58     });
59     instance.object = Object.create(_.extend({}, methods, transformations));
60     _.keys(methods).concat(_.keys(transformations)).forEach(function(key) {
61         instance.object[key] = _.bind(instance.object[key], instance);
62     });
63 };
64
65 var WLXMLElementNode = function(nativeNode, document) {
66     smartxml.ElementNode.call(this, nativeNode, document);
67     installObject(this, this.getClass());
68 };
69 WLXMLElementNode.prototype = Object.create(smartxml.ElementNode.prototype);
70
71 $.extend(WLXMLElementNode.prototype, WLXMLDocumentNodeMethods, smartxml.ElementNode.prototype, {
72     getClass: function() {
73         return this.getAttr('class') || '';
74     },
75     setClass: function(klass) {
76         if(klass !== this.klass) {
77             installObject(this, klass);
78             return this.setAttr('class', klass);
79         }
80     },
81     is: function(query) {
82         if(typeof query === 'string') {
83             query = {klass: query};
84         }
85         return (_.isUndefined(query.klass) || this.getClass().substr(0, query.klass.length) === query.klass) &&
86                (_.isUndefined(query.tagName) || this.getTagName() === query.tagName);
87     },
88     getMetaAttributes: function() {
89         var toret = new AttributesList(),
90             classParts = [''].concat(this.getClass().split('.')),
91             classCurrent, classDesc;
92
93         classParts.forEach(function(part) {
94             classCurrent = classCurrent ? classCurrent + '.' + part : part;
95             classDesc = this.document.options.wlxmlClasses[classCurrent];
96             if(classDesc) {
97                 _.keys(classDesc.attrs).forEach(function(attrName) {
98                     toret[attrName] = _.extend({value: this.getAttr(attrName)}, classDesc.attrs[attrName]);
99                 }.bind(this));
100             }
101         }.bind(this));
102         return toret;
103     },
104     setMetaAttribute: function(key, value) {
105         this.setAttr(key, value);
106     },
107     getOtherAttributes: function() {
108         var toret = {},
109             node = this;
110         this.getAttrs().forEach(function(attr) {
111             if(attr.name !== 'class' && !node.isMetaAttribute(attr.name)) {
112                 toret[attr.name] = {value: attr.value};
113             }
114         });
115         return toret;
116     },
117     isMetaAttribute: function(attrName) {
118         return attrName !== 'class' &&_.contains(_.keys(this.getMetaAttributes()), attrName);
119     },
120
121     _getXMLDOMToDump: function() {
122         var DOM = this._$.clone(true, true),
123             doc = this.document;
124
125         DOM.find('*').addBack().each(function() {
126             var el = $(this),
127                 parent = el.parent(),
128                 contents = parent.contents(),
129                 idx = contents.index(el),
130                 data = el.data();
131
132
133             var txt, documentNode, metaNode;
134
135             if(data[formatter_prefix+ 'orig_before']) {
136                 txt = idx > 0 && contents[idx-1].nodeType === Node.TEXT_NODE ? contents[idx-1] : null;
137                 if(txt && txt.data === data[formatter_prefix + 'orig_before_transformed']) {
138                     txt.data = data[formatter_prefix+ 'orig_before_original'];
139                 } else {
140                     el.before(data[formatter_prefix+ 'orig_before']);
141                 }
142             }
143             if(data[formatter_prefix+ 'orig_after']) {
144                 txt = idx < contents.length-1 && contents[idx+1].nodeType === Node.TEXT_NODE ? contents[idx+1] : null;
145                 if(txt && txt.data === data[formatter_prefix + 'orig_after_transformed']) {
146                     txt.data = data[formatter_prefix+ 'orig_after_original'];
147                 } else {
148                     el.after(data[formatter_prefix+ 'orig_after']);
149                 }
150             }
151             if(data[formatter_prefix+ 'orig_begin']) {
152                 el.prepend(data[formatter_prefix+ 'orig_begin']);
153             }
154             if(data[formatter_prefix+ 'orig_end']) {
155                 contents = el.contents();
156                 txt = (contents.length && contents[contents.length-1].nodeType === Node.TEXT_NODE) ? contents[contents.length-1] : null;
157                 if(txt && txt.data === data[formatter_prefix + 'orig_end_transformed']) {
158                     txt.data = data[formatter_prefix+ 'orig_end_original'];
159                 } else {
160                     el.append(data[formatter_prefix+ 'orig_end']);
161                 }
162             }
163
164
165             if(this.nodeType === Node.ELEMENT_NODE) {
166                 documentNode = doc.createDocumentNode(this);
167                 metaNode = $('<metadata>');
168                 documentNode.getMetadata().forEach(function(row) {
169                     metaNode.append('<dc:'+ row.key + '>' + row.value + '</dc:' + row.key + '>');
170                 });
171                 if(metaNode.children().length) {
172                     $(this).prepend(metaNode);
173                 }
174             }
175
176         });
177
178         
179
180         return DOM;
181     }
182 });
183
184 // WLXMLElementNode.prototype.transformations.register(transformations.createContextTransformation({
185 //     name: 'wlxml.setMetaAttribute',
186 //     impl: function(args) {
187 //         this.setMetaAttribute(args.name, args.value);
188 //     },
189 //     getChangeRoot: function() {
190 //         return this.context;
191 //     }
192 // }));
193
194
195
196 var WLXMLDocumentNode = function() {
197     smartxml.DocumentNode.apply(this, arguments);
198 };
199 WLXMLDocumentNode.prototype = Object.create(smartxml.DocumentNode.prototype);
200
201
202 var WLXMLTextNode = function() {
203     smartxml.TextNode.apply(this, arguments);
204 };
205 WLXMLTextNode.prototype = Object.create(smartxml.TextNode.prototype);
206 $.extend(WLXMLTextNode.prototype, WLXMLDocumentNodeMethods);
207
208 var WLXMLDocument = function(xml, options) {
209     this.classMethods = {};
210     this.classTransformations = {};
211     smartxml.Document.call(this, xml, [metadataExtension]);
212     this.options = options;
213 };
214
215 var formatter_prefix = '_wlxml_formatter_';
216
217
218 WLXMLDocument.prototype = Object.create(smartxml.Document.prototype);
219 $.extend(WLXMLDocument.prototype, {
220     ElementNodeFactory: WLXMLElementNode,
221     TextNodeFactory: WLXMLTextNode,
222     loadXML: function(xml) {
223         smartxml.Document.prototype.loadXML.call(this, xml, {silent: true});
224         this.trigger('contentSet');
225     },
226
227     normalizeXML: function(nativeNode) {
228         var doc = this,
229             prefixLength = 'dc:'.length;
230
231         $(nativeNode).find('metadata').each(function() {
232             var metadataNode = $(this),
233                 owner = doc.createDocumentNode(metadataNode.parent()[0]),
234                 metadata = owner.getMetadata();
235                 
236             metadataNode.children().each(function() {
237                 metadata.add({key: (this.tagName).toLowerCase().substr(prefixLength), value: $(this).text()}, {undoable: false});
238             });
239             metadataNode.remove();
240         });
241         nativeNode.normalize();
242
243         $(nativeNode).find(':not(iframe)').addBack().contents()
244             .filter(function() {return this.nodeType === Node.TEXT_NODE;})
245             .each(function() {
246                 var el = $(this),
247                     text = {original: el.text(), trimmed: $.trim(el.text())},
248                     elParent = el.parent(),
249                     hasSpanParent = elParent.prop('tagName') === 'SPAN',
250                     hasSpanBefore = el.prev().length && $(el.prev()).prop('tagName') === 'SPAN',
251                     hasSpanAfter = el.next().length && $(el.next()).prop('tagName') === 'SPAN';
252
253
254                 var addInfo = function(toAdd, where, transformed, original) {
255                     var parentContents = elParent.contents(),
256                         idx = parentContents.index(el[0]),
257                         prev = idx > 0 ? parentContents[idx-1] : null,
258                         next = idx < parentContents.length - 1 ? parentContents[idx+1] : null,
259                         target, key;
260
261                     if(where === 'above') {
262                         target = prev ? $(prev) : elParent;
263                         key = prev ? 'orig_after' : 'orig_begin';
264                     } else if(where === 'below') {
265                         target = next ? $(next) : elParent;
266                         key = next ? 'orig_before' : 'orig_end';
267                     } else { throw new Error();}
268
269                     target.data(formatter_prefix + key, toAdd);
270                     if(transformed !== undefined) {
271                         target.data(formatter_prefix + key + '_transformed', transformed);
272                     }
273                     if(original !== undefined) {
274                         target.data(formatter_prefix + key + '_original', original);
275                     }
276                 };
277
278                 text.transformed = text.trimmed;
279
280                 if(hasSpanParent || hasSpanBefore || hasSpanAfter) {
281                     var startSpace = /\s/g.test(text.original.substr(0,1)),
282                         endSpace = /\s/g.test(text.original.substr(-1)) && text.original.length > 1;
283                     text.transformed = (startSpace && (hasSpanParent || hasSpanBefore) ? ' ' : '');
284                     text.transformed += text.trimmed;
285                     text.transformed += (endSpace && (hasSpanParent || hasSpanAfter) ? ' ' : '');
286                 } else {
287                     if(text.trimmed.length === 0 && text.original.length > 0 && elParent.contents().length === 1) {
288                         text.transformed = ' ';
289                     }
290                 }
291
292                 if(!text.transformed) {
293                     addInfo(text.original, 'below');
294                     el.remove();
295                     return true; // continue
296                 }
297
298                 if(text.transformed !== text.original) {
299                     // if(!text.trimmed) {
300                     //     addInfo(text.original, 'below');
301                     // } else {
302                         var startingMatch = text.original.match(/^\s+/g),
303                             endingMatch = text.original.match(/\s+$/g),
304                             startingWhiteSpace = startingMatch ? startingMatch[0] : null,
305                             endingWhiteSpace = endingMatch ? endingMatch[0] : null;
306
307                         if(endingWhiteSpace) {
308                             if(text.transformed[text.transformed.length - 1] === ' ' && endingWhiteSpace[0] === ' ') {
309                                 endingWhiteSpace = endingWhiteSpace.substr(1);
310                             }
311                             addInfo(endingWhiteSpace, 'below', !text.trimmed ? text.transformed : undefined, !text.trimmed ? text.original : undefined);
312                         }
313
314                         if(startingWhiteSpace && text.trimmed) {
315                             if(text.transformed[0] === ' ' && startingWhiteSpace[startingWhiteSpace.length-1] === ' ') {
316                                 startingWhiteSpace = startingWhiteSpace.substr(0, startingWhiteSpace.length -1);
317                             }
318                             addInfo(startingWhiteSpace, 'above', !text.trimmed ? text.transformed : undefined, !text.trimmed ? text.original : undefined);
319                         }
320                     //}
321                 }
322                 /* globals document */
323                 el.replaceWith(document.createTextNode(text.transformed));
324             });
325         
326
327     },
328
329     registerClassTransformation: function(Transformation, className) {
330         var thisClassTransformations = (this.classTransformations[className] = this.classTransformations[className] || {});
331         thisClassTransformations[Transformation.prototype.name] = function() {
332             var nodeInstance = this;
333             var args = Array.prototype.slice.call(arguments, 0);
334             return nodeInstance.transform(Transformation, args);
335         };
336     },
337
338     registerClassMethod: function(methodName, method, className) {
339         var thisClassMethods = (this.classMethods[className] = this.classMethods[className] || {});
340         thisClassMethods[methodName] = method;
341     },
342
343     registerExtension: function(extension) {
344         //debugger;
345         smartxml.Document.prototype.registerExtension.call(this, extension);
346         var doc = this;
347
348         _.pairs(extension.wlxmlClass).forEach(function(pair) {
349             var className = pair[0],
350                 classExtension = pair[1];
351
352             _.pairs(classExtension.methods || {}).forEach(function(pair) {
353                 var name = pair[0],
354                     method = pair[1];
355                 doc.registerClassMethod(name, method, className);
356             });
357
358             _.pairs(classExtension.transformations || {}).forEach(function(pair) {
359                 var name = pair[0],
360                     desc = pair[1];
361                 doc.registerClassTransformation(transformations.createContextTransformation(desc, name), className);
362             });
363         });
364
365     }
366
367 });
368
369 var wlxmlClasses = {
370     'link': {
371         attrs: {href: {type: 'string'}}
372     }
373 };
374
375
376 return {
377     WLXMLDocumentFromXML: function(xml, options, Factory) {
378         options = _.extend({wlxmlClasses: wlxmlClasses}, options);
379         Factory = Factory || WLXMLDocument;
380         return new Factory(xml, options);
381     },
382
383     WLXMLElementNodeFromXML: function(xml) {
384         return this.WLXMLDocumentFromXML(xml).root;
385     },
386
387     WLXMLDocument: WLXMLDocument
388 };
389
390 });