4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
13 * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for more details and examples on how to use this class.
15 * @class tinymce.dom.Serializer
19 * Constucts a new DOM serializer class.
23 * @param {Object} settings Serializer settings object.
24 * @param {tinymce.dom.DOMUtils} dom DOMUtils instance reference.
25 * @param {tinymce.html.Schema} schema Optional schema reference.
27 tinymce.dom.Serializer = function(settings, dom, schema) {
28 var onPreProcess, onPostProcess, isIE = tinymce.isIE, each = tinymce.each, htmlParser;
30 // Support the old apply_source_formatting option
31 if (!settings.apply_source_formatting)
32 settings.indent = false;
34 settings.remove_trailing_brs = true;
36 // Default DOM and Schema if they are undefined
37 dom = dom || tinymce.DOM;
38 schema = schema || new tinymce.html.Schema(settings);
39 settings.entity_encoding = settings.entity_encoding || 'named';
42 * This event gets executed before a HTML fragment gets serialized into a HTML string. This event enables you to do modifications to the DOM before the serialization occurs. It's important to know that the element that is getting serialized is cloned so it's not inside a document.
45 * @param {tinymce.dom.Serializer} sender object/Serializer instance that is serializing an element.
46 * @param {Object} args Object containing things like the current node.
48 * // Adds an observer to the onPreProcess event
49 * serializer.onPreProcess.add(function(se, o) {
50 * // Add a class to each paragraph
51 * se.dom.addClass(se.dom.select('p', o.node), 'myclass');
54 onPreProcess = new tinymce.util.Dispatcher(self);
57 * This event gets executed after a HTML fragment has been serialized into a HTML string. This event enables you to do modifications to the HTML string like regexp replaces etc.
60 * @param {tinymce.dom.Serializer} sender object/Serializer instance that is serializing an element.
61 * @param {Object} args Object containing things like the current contents.
63 * // Adds an observer to the onPostProcess event
64 * serializer.onPostProcess.add(function(se, o) {
65 * // Remove all paragraphs and replace with BR
66 * o.content = o.content.replace(/<p[^>]+>|<p>/g, '');
67 * o.content = o.content.replace(/<\/p>/g, '<br />');
70 onPostProcess = new tinymce.util.Dispatcher(self);
72 htmlParser = new tinymce.html.DomParser(settings, schema);
74 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
75 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
76 var i = nodes.length, node, value, internalName = 'data-mce-' + name, urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
81 value = node.attributes.map[internalName];
82 if (value !== undef) {
83 // Set external name to internal value and remove internal
84 node.attr(name, value.length > 0 ? value : null);
85 node.attr(internalName, null);
87 // No internal attribute found then convert the value we have in the DOM
88 value = node.attributes.map[name];
91 value = dom.serializeStyle(dom.parseStyle(value), node.name);
92 else if (urlConverter)
93 value = urlConverter.call(urlConverterScope, value, name, node.name);
95 node.attr(name, value.length > 0 ? value : null);
100 // Remove internal classes mceItem<..>
101 htmlParser.addAttributeFilter('class', function(nodes, name) {
102 var i = nodes.length, node, value;
106 value = node.attr('class').replace(/\s*mce(Item\w+|Selected)\s*/g, '');
107 node.attr('class', value.length > 0 ? value : null);
111 // Remove bookmark elements
112 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
113 var i = nodes.length, node;
118 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup)
123 // Force script into CDATA sections and remove the mce- prefix also add comments around styles
124 htmlParser.addNodeFilter('script,style', function(nodes, name) {
125 var i = nodes.length, node, value;
127 function trim(value) {
128 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
129 .replace(/^[\r\n]*|[\r\n]*$/g, '')
130 .replace(/^\s*(\/\/\s*<!--|\/\/\s*<!\[CDATA\[|<!--|<!\[CDATA\[)[\r\n]*/g, '')
131 .replace(/\s*(\/\/\s*\]\]>|\/\/\s*-->|\]\]>|-->|\]\]-->)\s*$/g, '');
136 value = node.firstChild ? node.firstChild.value : '';
138 if (name === "script") {
139 // Remove mce- prefix from script elements
140 node.attr('type', (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''));
142 if (value.length > 0)
143 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
145 if (value.length > 0)
146 node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
151 // Convert comments to cdata and handle protected comments
152 htmlParser.addNodeFilter('#comment', function(nodes, name) {
153 var i = nodes.length, node;
158 if (node.value.indexOf('[CDATA[') === 0) {
159 node.name = '#cdata';
161 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
162 } else if (node.value.indexOf('mce:protected ') === 0) {
166 node.value = unescape(node.value).substr(14);
171 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
172 var i = nodes.length, node;
178 else if (node.type === 1) {
179 if (name === "input" && !("type" in node.attributes.map))
180 node.attr('type', 'text');
185 // Fix list elements, TODO: Replace this later
186 if (settings.fix_list_elements) {
187 htmlParser.addNodeFilter('ul,ol', function(nodes, name) {
188 var i = nodes.length, node, parentNode;
192 parentNode = node.parent;
194 if (parentNode.name === 'ul' || parentNode.name === 'ol') {
195 if (node.prev && node.prev.name === 'li') {
196 node.prev.append(node);
203 // Remove internal data attributes
204 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', function(nodes, name) {
205 var i = nodes.length;
208 nodes[i].attr(name, null);
212 // Return public methods
215 * Schema instance that was used to when the Serializer was constructed.
217 * @field {tinymce.html.Schema} schema
222 * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name
223 * and then execute the callback ones it has finished parsing the document.
226 * parser.addNodeFilter('p,h1', function(nodes, name) {
227 * for (var i = 0; i < nodes.length; i++) {
228 * console.log(nodes[i].name);
231 * @method addNodeFilter
232 * @method {String} name Comma separated list of nodes to collect.
233 * @param {function} callback Callback function to execute once it has collected nodes.
235 addNodeFilter : htmlParser.addNodeFilter,
238 * Adds a attribute filter function to the parser used by the serializer, the parser will collect nodes that has the specified attributes
239 * and then execute the callback ones it has finished parsing the document.
242 * parser.addAttributeFilter('src,href', function(nodes, name) {
243 * for (var i = 0; i < nodes.length; i++) {
244 * console.log(nodes[i].name);
247 * @method addAttributeFilter
248 * @method {String} name Comma separated list of nodes to collect.
249 * @param {function} callback Callback function to execute once it has collected nodes.
251 addAttributeFilter : htmlParser.addAttributeFilter,
254 * Fires when the Serializer does a preProcess on the contents.
256 * @event onPreProcess
257 * @param {tinymce.Editor} sender Editor instance.
258 * @param {Object} obj PreProcess object.
259 * @option {Node} node DOM node for the item being serialized.
260 * @option {String} format The specified output format normally "html".
261 * @option {Boolean} get Is true if the process is on a getContent operation.
262 * @option {Boolean} set Is true if the process is on a setContent operation.
263 * @option {Boolean} cleanup Is true if the process is on a cleanup operation.
265 onPreProcess : onPreProcess,
268 * Fires when the Serializer does a postProcess on the contents.
270 * @event onPostProcess
271 * @param {tinymce.Editor} sender Editor instance.
272 * @param {Object} obj PreProcess object.
274 onPostProcess : onPostProcess,
277 * Serializes the specified browser DOM node into a HTML string.
280 * @param {DOMNode} node DOM node to serialize.
281 * @param {Object} args Arguments option that gets passed to event handlers.
283 serialize : function(node, args) {
284 var impl, doc, oldDoc, htmlSerializer, content;
286 // Explorer won't clone contents of script and style and the
287 // selected index of select elements are cleared on a clone operation.
288 if (isIE && dom.select('script,style,select').length > 0) {
289 content = node.innerHTML;
290 node = node.cloneNode(false);
291 dom.setHTML(node, content);
293 node = node.cloneNode(true);
295 // Nodes needs to be attached to something in WebKit/Opera
296 // Older builds of Opera crashes if you attach the node to an document created dynamically
297 // and since we can't feature detect a crash we need to sniff the acutal build number
298 // This fix will make DOM ranges and make Sizzle happy!
299 impl = node.ownerDocument.implementation;
300 if (impl.createHTMLDocument) {
301 // Create an empty HTML document
302 doc = impl.createHTMLDocument("");
304 // Add the element or it's children if it's a body element to the new document
305 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
306 doc.body.appendChild(doc.importNode(node, true));
309 // Grab first child or body element for serialization
310 if (node.nodeName != 'BODY')
311 node = doc.body.firstChild;
315 // set the new document in DOMUtils so createElement etc works
321 args.format = args.format || 'html';
324 if (!args.no_events) {
326 onPreProcess.dispatch(self, args);
330 htmlSerializer = new tinymce.html.Serializer(settings, schema);
332 // Parse and serialize HTML
333 args.content = htmlSerializer.serialize(
334 htmlParser.parse(args.getInner ? node.innerHTML : tinymce.trim(dom.getOuterHTML(node), args), args)
337 // Replace all BOM characters for now until we can find a better solution
339 args.content = args.content.replace(/\uFEFF/g, '');
343 onPostProcess.dispatch(self, args);
345 // Restore the old document if it was changed
355 * Adds valid elements rules to the serializers schema instance this enables you to specify things
356 * like what elements should be outputted and what attributes specific elements might have.
357 * Consult the Wiki for more details on this format.
360 * @param {String} rules Valid elements rules string to add to schema.
362 addRules : function(rules) {
363 schema.addValidElements(rules);
367 * Sets the valid elements rules to the serializers schema instance this enables you to specify things
368 * like what elements should be outputted and what attributes specific elements might have.
369 * Consult the Wiki for more details on this format.
372 * @param {String} rules Valid elements rules string.
374 setRules : function(rules) {
375 schema.setValidElements(rules);