4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
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.
15 * @class tinymce.dom.Serializer
17 define("tinymce/dom/Serializer", [
18 "tinymce/dom/DOMUtils",
19 "tinymce/html/DomParser",
20 "tinymce/html/Entities",
21 "tinymce/html/Serializer",
23 "tinymce/html/Schema",
26 ], function(DOMUtils, DomParser, Entities, Serializer, Node, Schema, Env, Tools) {
27 var each = Tools.each, trim = Tools.trim;
28 var DOM = DOMUtils.DOM;
31 * Constructs a new DOM serializer class.
35 * @param {Object} settings Serializer settings object.
36 * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from.
38 return function(settings, editor) {
39 var dom, schema, htmlParser;
43 schema = editor.schema;
46 // Default DOM and Schema if they are undefined
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;
52 htmlParser = new DomParser(settings, schema);
54 // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
55 htmlParser.addAttributeFilter('src,href,style', function(nodes, name) {
56 var i = nodes.length, node, value, internalName = 'data-mce-' + name;
57 var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
62 value = node.attributes.map[internalName];
63 if (value !== undef) {
64 // Set external name to internal value and remove internal
65 node.attr(name, value.length > 0 ? value : null);
66 node.attr(internalName, null);
68 // No internal attribute found then convert the value we have in the DOM
69 value = node.attributes.map[name];
71 if (name === "style") {
72 value = dom.serializeStyle(dom.parseStyle(value), node.name);
73 } else if (urlConverter) {
74 value = urlConverter.call(urlConverterScope, value, name, node.name);
77 node.attr(name, value.length > 0 ? value : null);
82 // Remove internal classes mceItem<..> or mceSelected
83 htmlParser.addAttributeFilter('class', function(nodes) {
84 var i = nodes.length, node, value;
88 value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, '');
89 node.attr('class', value.length > 0 ? value : null);
93 // Remove bookmark elements
94 htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
95 var i = nodes.length, node;
100 if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) {
106 // Remove expando attributes
107 htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name) {
108 var i = nodes.length;
111 nodes[i].attr(name, null);
115 htmlParser.addNodeFilter('noscript', function(nodes) {
116 var i = nodes.length, node;
119 node = nodes[i].firstChild;
122 node.value = Entities.decode(node.value);
127 // Force script into CDATA sections and remove the mce- prefix also add comments around styles
128 htmlParser.addNodeFilter('script,style', function(nodes, name) {
129 var i = nodes.length, node, value;
131 function trim(value) {
132 /*jshint maxlen:255 */
133 return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
134 .replace(/^[\r\n]*|[\r\n]*$/g, '')
135 .replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
136 .replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
141 value = node.firstChild ? node.firstChild.value : '';
143 if (name === "script") {
144 // Remove mce- prefix from script elements and remove default text/javascript mime type (HTML5)
145 var type = (node.attr('type') || 'text/javascript').replace(/^mce\-/, '');
146 node.attr('type', type === 'text/javascript' ? null : type);
148 if (value.length > 0) {
149 node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
152 if (value.length > 0) {
153 node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
159 // Convert comments to cdata and handle protected comments
160 htmlParser.addNodeFilter('#comment', function(nodes) {
161 var i = nodes.length, node;
166 if (node.value.indexOf('[CDATA[') === 0) {
167 node.name = '#cdata';
169 node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
170 } else if (node.value.indexOf('mce:protected ') === 0) {
174 node.value = unescape(node.value).substr(14);
179 htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
180 var i = nodes.length, node;
184 if (node.type === 7) {
186 } else if (node.type === 1) {
187 if (name === "input" && !("type" in node.attributes.map)) {
188 node.attr('type', 'text');
194 // Fix list elements, TODO: Replace this later
195 if (settings.fix_list_elements) {
196 htmlParser.addNodeFilter('ul,ol', function(nodes) {
197 var i = nodes.length, node, parentNode;
201 parentNode = node.parent;
203 if (parentNode.name === 'ul' || parentNode.name === 'ol') {
204 if (node.prev && node.prev.name === 'li') {
205 node.prev.append(node);
212 // Remove internal data attributes
213 htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style,data-mce-selected', function(nodes, name) {
214 var i = nodes.length;
217 nodes[i].attr(name, null);
221 // Return public methods
224 * Schema instance that was used to when the Serializer was constructed.
226 * @field {tinymce.html.Schema} schema
231 * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name
232 * and then execute the callback ones it has finished parsing the document.
235 * parser.addNodeFilter('p,h1', function(nodes, name) {
236 * for (var i = 0; i < nodes.length; i++) {
237 * console.log(nodes[i].name);
240 * @method addNodeFilter
241 * @method {String} name Comma separated list of nodes to collect.
242 * @param {function} callback Callback function to execute once it has collected nodes.
244 addNodeFilter: htmlParser.addNodeFilter,
247 * Adds a attribute filter function to the parser used by the serializer, the parser will
248 * collect nodes that has the specified attributes
249 * and then execute the callback ones it has finished parsing the document.
252 * parser.addAttributeFilter('src,href', function(nodes, name) {
253 * for (var i = 0; i < nodes.length; i++) {
254 * console.log(nodes[i].name);
257 * @method addAttributeFilter
258 * @method {String} name Comma separated list of nodes to collect.
259 * @param {function} callback Callback function to execute once it has collected nodes.
261 addAttributeFilter: htmlParser.addAttributeFilter,
264 * Serializes the specified browser DOM node into a HTML string.
267 * @param {DOMNode} node DOM node to serialize.
268 * @param {Object} args Arguments option that gets passed to event handlers.
270 serialize: function(node, args) {
271 var self = this, impl, doc, oldDoc, htmlSerializer, content;
273 // Explorer won't clone contents of script and style and the
274 // selected index of select elements are cleared on a clone operation.
275 if (Env.ie && dom.select('script,style,select,map').length > 0) {
276 content = node.innerHTML;
277 node = node.cloneNode(false);
278 dom.setHTML(node, content);
280 node = node.cloneNode(true);
283 // Nodes needs to be attached to something in WebKit/Opera
284 // This fix will make DOM ranges and make Sizzle happy!
285 impl = node.ownerDocument.implementation;
286 if (impl.createHTMLDocument) {
287 // Create an empty HTML document
288 doc = impl.createHTMLDocument("");
290 // Add the element or it's children if it's a body element to the new document
291 each(node.nodeName == 'BODY' ? node.childNodes : [node], function(node) {
292 doc.body.appendChild(doc.importNode(node, true));
295 // Grab first child or body element for serialization
296 if (node.nodeName != 'BODY') {
297 node = doc.body.firstChild;
302 // set the new document in DOMUtils so createElement etc works
308 args.format = args.format || 'html';
310 // Don't wrap content if we want selected html
311 if (args.selection) {
312 args.forced_root_block = '';
316 if (!args.no_events) {
318 self.onPreProcess(args);
322 htmlSerializer = new Serializer(settings, schema);
324 // Parse and serialize HTML
325 args.content = htmlSerializer.serialize(
326 htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args)
329 // Replace all BOM characters for now until we can find a better solution
331 args.content = args.content.replace(/\uFEFF/g, '');
335 if (!args.no_events) {
336 self.onPostProcess(args);
339 // Restore the old document if it was changed
350 * Adds valid elements rules to the serializers schema instance this enables you to specify things
351 * like what elements should be outputted and what attributes specific elements might have.
352 * Consult the Wiki for more details on this format.
355 * @param {String} rules Valid elements rules string to add to schema.
357 addRules: function(rules) {
358 schema.addValidElements(rules);
362 * Sets the valid elements rules to the serializers schema instance this enables you to specify things
363 * like what elements should be outputted and what attributes specific elements might have.
364 * Consult the Wiki for more details on this format.
367 * @param {String} rules Valid elements rules string.
369 setRules: function(rules) {
370 schema.setValidElements(rules);
373 onPreProcess: function(args) {
375 editor.fire('PreProcess', args);
379 onPostProcess: function(args) {
381 editor.fire('PostProcess', args);