|
1 /** |
|
2 * Serializer.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /** |
|
12 * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for |
|
13 * more details and examples on how to use this class. |
|
14 * |
|
15 * @class tinymce.dom.Serializer |
|
16 */ |
|
17 define("tinymce/dom/Serializer", [ |
|
18 "tinymce/dom/DOMUtils", |
|
19 "tinymce/html/DomParser", |
|
20 "tinymce/html/Entities", |
|
21 "tinymce/html/Serializer", |
|
22 "tinymce/html/Node", |
|
23 "tinymce/html/Schema", |
|
24 "tinymce/Env", |
|
25 "tinymce/util/Tools" |
|
26 ], function(DOMUtils, DomParser, Entities, Serializer, Node, Schema, Env, Tools) { |
|
27 var each = Tools.each, trim = Tools.trim; |
|
28 var DOM = DOMUtils.DOM; |
|
29 |
|
30 /** |
|
31 * Constructs a new DOM serializer class. |
|
32 * |
|
33 * @constructor |
|
34 * @method Serializer |
|
35 * @param {Object} settings Serializer settings object. |
|
36 * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from. |
|
37 */ |
|
38 return function(settings, editor) { |
|
39 var dom, schema, htmlParser; |
|
40 |
|
41 if (editor) { |
|
42 dom = editor.dom; |
|
43 schema = editor.schema; |
|
44 } |
|
45 |
|
46 // Default DOM and Schema if they are undefined |
|
47 dom = dom || DOM; |
|
48 schema = schema || new Schema(settings); |
|
49 settings.entity_encoding = settings.entity_encoding || 'named'; |
|
50 settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; |
|
51 |
|
52 htmlParser = new DomParser(settings, schema); |
|
53 |
|
54 // Convert tabindex back to elements when serializing contents |
|
55 htmlParser.addAttributeFilter('data-mce-tabindex', function(nodes, name) { |
|
56 var i = nodes.length, node; |
|
57 |
|
58 while (i--) { |
|
59 node = nodes[i]; |
|
60 node.attr('tabindex', node.attributes.map['data-mce-tabindex']); |
|
61 node.attr(name, null); |
|
62 } |
|
63 }); |
|
64 |
|
65 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed |
|
66 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) { |
|
67 var i = nodes.length, node, value, internalName = 'data-mce-' + name; |
|
68 var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; |
|
69 |
|
70 while (i--) { |
|
71 node = nodes[i]; |
|
72 |
|
73 value = node.attributes.map[internalName]; |
|
74 if (value !== undef) { |
|
75 // Set external name to internal value and remove internal |
|
76 node.attr(name, value.length > 0 ? value : null); |
|
77 node.attr(internalName, null); |
|
78 } else { |
|
79 // No internal attribute found then convert the value we have in the DOM |
|
80 value = node.attributes.map[name]; |
|
81 |
|
82 if (name === "style") { |
|
83 value = dom.serializeStyle(dom.parseStyle(value), node.name); |
|
84 } else if (urlConverter) { |
|
85 value = urlConverter.call(urlConverterScope, value, name, node.name); |
|
86 } |
|
87 |
|
88 node.attr(name, value.length > 0 ? value : null); |
|
89 } |
|
90 } |
|
91 }); |
|
92 |
|
93 // Remove internal classes mceItem<..> or mceSelected |
|
94 htmlParser.addAttributeFilter('class', function(nodes) { |
|
95 var i = nodes.length, node, value; |
|
96 |
|
97 while (i--) { |
|
98 node = nodes[i]; |
|
99 value = node.attr('class'); |
|
100 |
|
101 if (value) { |
|
102 value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); |
|
103 node.attr('class', value.length > 0 ? value : null); |
|
104 } |
|
105 } |
|
106 }); |
|
107 |
|
108 // Remove bookmark elements |
|
109 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) { |
|
110 var i = nodes.length, node; |
|
111 |
|
112 while (i--) { |
|
113 node = nodes[i]; |
|
114 |
|
115 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) { |
|
116 node.remove(); |
|
117 } |
|
118 } |
|
119 }); |
|
120 |
|
121 htmlParser.addNodeFilter('noscript', function(nodes) { |
|
122 var i = nodes.length, node; |
|
123 |
|
124 while (i--) { |
|
125 node = nodes[i].firstChild; |
|
126 |
|
127 if (node) { |
|
128 node.value = Entities.decode(node.value); |
|
129 } |
|
130 } |
|
131 }); |
|
132 |
|
133 // Force script into CDATA sections and remove the mce- prefix also add comments around styles |
|
134 htmlParser.addNodeFilter('script,style', function(nodes, name) { |
|
135 var i = nodes.length, node, value, type; |
|
136 |
|
137 function trim(value) { |
|
138 /*jshint maxlen:255 */ |
|
139 /*eslint max-len:0 */ |
|
140 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') |
|
141 .replace(/^[\r\n]*|[\r\n]*$/g, '') |
|
142 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '') |
|
143 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); |
|
144 } |
|
145 |
|
146 while (i--) { |
|
147 node = nodes[i]; |
|
148 value = node.firstChild ? node.firstChild.value : ''; |
|
149 |
|
150 if (name === "script") { |
|
151 // Remove mce- prefix from script elements and remove default type since the user specified |
|
152 // a script element without type attribute |
|
153 type = node.attr('type'); |
|
154 if (type) { |
|
155 node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, '')); |
|
156 } |
|
157 |
|
158 if (value.length > 0) { |
|
159 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; |
|
160 } |
|
161 } else { |
|
162 if (value.length > 0) { |
|
163 node.firstChild.value = '<!--\n' + trim(value) + '\n-->'; |
|
164 } |
|
165 } |
|
166 } |
|
167 }); |
|
168 |
|
169 // Convert comments to cdata and handle protected comments |
|
170 htmlParser.addNodeFilter('#comment', function(nodes) { |
|
171 var i = nodes.length, node; |
|
172 |
|
173 while (i--) { |
|
174 node = nodes[i]; |
|
175 |
|
176 if (node.value.indexOf('[CDATA[') === 0) { |
|
177 node.name = '#cdata'; |
|
178 node.type = 4; |
|
179 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); |
|
180 } else if (node.value.indexOf('mce:protected ') === 0) { |
|
181 node.name = "#text"; |
|
182 node.type = 3; |
|
183 node.raw = true; |
|
184 node.value = unescape(node.value).substr(14); |
|
185 } |
|
186 } |
|
187 }); |
|
188 |
|
189 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) { |
|
190 var i = nodes.length, node; |
|
191 |
|
192 while (i--) { |
|
193 node = nodes[i]; |
|
194 if (node.type === 7) { |
|
195 node.remove(); |
|
196 } else if (node.type === 1) { |
|
197 if (name === "input" && !("type" in node.attributes.map)) { |
|
198 node.attr('type', 'text'); |
|
199 } |
|
200 } |
|
201 } |
|
202 }); |
|
203 |
|
204 // Fix list elements, TODO: Replace this later |
|
205 if (settings.fix_list_elements) { |
|
206 htmlParser.addNodeFilter('ul,ol', function(nodes) { |
|
207 var i = nodes.length, node, parentNode; |
|
208 |
|
209 while (i--) { |
|
210 node = nodes[i]; |
|
211 parentNode = node.parent; |
|
212 |
|
213 if (parentNode.name === 'ul' || parentNode.name === 'ol') { |
|
214 if (node.prev && node.prev.name === 'li') { |
|
215 node.prev.append(node); |
|
216 } |
|
217 } |
|
218 } |
|
219 }); |
|
220 } |
|
221 |
|
222 // Remove internal data attributes |
|
223 htmlParser.addAttributeFilter( |
|
224 'data-mce-src,data-mce-href,data-mce-style,' + |
|
225 'data-mce-selected,data-mce-expando,' + |
|
226 'data-mce-type,data-mce-resize', |
|
227 |
|
228 function(nodes, name) { |
|
229 var i = nodes.length; |
|
230 |
|
231 while (i--) { |
|
232 nodes[i].attr(name, null); |
|
233 } |
|
234 } |
|
235 ); |
|
236 |
|
237 // Return public methods |
|
238 return { |
|
239 /** |
|
240 * Schema instance that was used to when the Serializer was constructed. |
|
241 * |
|
242 * @field {tinymce.html.Schema} schema |
|
243 */ |
|
244 schema: schema, |
|
245 |
|
246 /** |
|
247 * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name |
|
248 * and then execute the callback ones it has finished parsing the document. |
|
249 * |
|
250 * @example |
|
251 * parser.addNodeFilter('p,h1', function(nodes, name) { |
|
252 * for (var i = 0; i < nodes.length; i++) { |
|
253 * console.log(nodes[i].name); |
|
254 * } |
|
255 * }); |
|
256 * @method addNodeFilter |
|
257 * @method {String} name Comma separated list of nodes to collect. |
|
258 * @param {function} callback Callback function to execute once it has collected nodes. |
|
259 */ |
|
260 addNodeFilter: htmlParser.addNodeFilter, |
|
261 |
|
262 /** |
|
263 * Adds a attribute filter function to the parser used by the serializer, the parser will |
|
264 * collect nodes that has the specified attributes |
|
265 * and then execute the callback ones it has finished parsing the document. |
|
266 * |
|
267 * @example |
|
268 * parser.addAttributeFilter('src,href', function(nodes, name) { |
|
269 * for (var i = 0; i < nodes.length; i++) { |
|
270 * console.log(nodes[i].name); |
|
271 * } |
|
272 * }); |
|
273 * @method addAttributeFilter |
|
274 * @method {String} name Comma separated list of nodes to collect. |
|
275 * @param {function} callback Callback function to execute once it has collected nodes. |
|
276 */ |
|
277 addAttributeFilter: htmlParser.addAttributeFilter, |
|
278 |
|
279 /** |
|
280 * Serializes the specified browser DOM node into a HTML string. |
|
281 * |
|
282 * @method serialize |
|
283 * @param {DOMNode} node DOM node to serialize. |
|
284 * @param {Object} args Arguments option that gets passed to event handlers. |
|
285 */ |
|
286 serialize: function(node, args) { |
|
287 var self = this, impl, doc, oldDoc, htmlSerializer, content; |
|
288 |
|
289 // Explorer won't clone contents of script and style and the |
|
290 // selected index of select elements are cleared on a clone operation. |
|
291 if (Env.ie && dom.select('script,style,select,map').length > 0) { |
|
292 content = node.innerHTML; |
|
293 node = node.cloneNode(false); |
|
294 dom.setHTML(node, content); |
|
295 } else { |
|
296 node = node.cloneNode(true); |
|
297 } |
|
298 |
|
299 // Nodes needs to be attached to something in WebKit/Opera |
|
300 // This fix will make DOM ranges and make Sizzle happy! |
|
301 impl = node.ownerDocument.implementation; |
|
302 if (impl.createHTMLDocument) { |
|
303 // Create an empty HTML document |
|
304 doc = impl.createHTMLDocument(""); |
|
305 |
|
306 // Add the element or it's children if it's a body element to the new document |
|
307 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) { |
|
308 doc.body.appendChild(doc.importNode(node, true)); |
|
309 }); |
|
310 |
|
311 // Grab first child or body element for serialization |
|
312 if (node.nodeName != 'BODY') { |
|
313 node = doc.body.firstChild; |
|
314 } else { |
|
315 node = doc.body; |
|
316 } |
|
317 |
|
318 // set the new document in DOMUtils so createElement etc works |
|
319 oldDoc = dom.doc; |
|
320 dom.doc = doc; |
|
321 } |
|
322 |
|
323 args = args || {}; |
|
324 args.format = args.format || 'html'; |
|
325 |
|
326 // Don't wrap content if we want selected html |
|
327 if (args.selection) { |
|
328 args.forced_root_block = ''; |
|
329 } |
|
330 |
|
331 // Pre process |
|
332 if (!args.no_events) { |
|
333 args.node = node; |
|
334 self.onPreProcess(args); |
|
335 } |
|
336 |
|
337 // Setup serializer |
|
338 htmlSerializer = new Serializer(settings, schema); |
|
339 |
|
340 // Parse and serialize HTML |
|
341 args.content = htmlSerializer.serialize( |
|
342 htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args) |
|
343 ); |
|
344 |
|
345 // Replace all BOM characters for now until we can find a better solution |
|
346 if (!args.cleanup) { |
|
347 args.content = args.content.replace(/\uFEFF/g, ''); |
|
348 } |
|
349 |
|
350 // Post process |
|
351 if (!args.no_events) { |
|
352 self.onPostProcess(args); |
|
353 } |
|
354 |
|
355 // Restore the old document if it was changed |
|
356 if (oldDoc) { |
|
357 dom.doc = oldDoc; |
|
358 } |
|
359 |
|
360 args.node = null; |
|
361 |
|
362 return args.content; |
|
363 }, |
|
364 |
|
365 /** |
|
366 * Adds valid elements rules to the serializers schema instance this enables you to specify things |
|
367 * like what elements should be outputted and what attributes specific elements might have. |
|
368 * Consult the Wiki for more details on this format. |
|
369 * |
|
370 * @method addRules |
|
371 * @param {String} rules Valid elements rules string to add to schema. |
|
372 */ |
|
373 addRules: function(rules) { |
|
374 schema.addValidElements(rules); |
|
375 }, |
|
376 |
|
377 /** |
|
378 * Sets the valid elements rules to the serializers schema instance this enables you to specify things |
|
379 * like what elements should be outputted and what attributes specific elements might have. |
|
380 * Consult the Wiki for more details on this format. |
|
381 * |
|
382 * @method setRules |
|
383 * @param {String} rules Valid elements rules string. |
|
384 */ |
|
385 setRules: function(rules) { |
|
386 schema.setValidElements(rules); |
|
387 }, |
|
388 |
|
389 onPreProcess: function(args) { |
|
390 if (editor) { |
|
391 editor.fire('PreProcess', args); |
|
392 } |
|
393 }, |
|
394 |
|
395 onPostProcess: function(args) { |
|
396 if (editor) { |
|
397 editor.fire('PostProcess', args); |
|
398 } |
|
399 } |
|
400 }; |
|
401 }; |
|
402 }); |