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