292631bdefe39b68cba6190c9ab8abe95c1db851
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / HTMLArea / DOM / Walker.js
1 /*
2 * This file is part of the TYPO3 CMS project.
3 *
4 * It is free software; you can redistribute it and/or modify it under
5 * the terms of the GNU General Public License, either version 2
6 * of the License, or any later version.
7 *
8 * For the full copyright and license information, please read the
9 * LICENSE.txt file that was distributed with this source code.
10 *
11 * The TYPO3 project - inspiring people to share!
12 */
13 /***************************************************
14 * HTMLArea.DOM.Walker: DOM tree walk
15 ***************************************************/
16 define('TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Walker',
17 ['TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
18 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM'],
20 function (UserAgent, Util, Dom) {
21
22 /**
23 * Constructor method
24 *
25 * @param object config: an object with property "editor" giving reference to the parent object
26 *
27 * @return void
28 */
29 var Walker = function (config) {
30 // Configuration defaults
31 var configDefaults = {
32 keepComments: false,
33 keepCDATASections: false,
34 removeTags: /none/i,
35 removeTagsAndContents: /none/i,
36 keepTags: /.*/i,
37 removeAttributes: /none/i,
38 removeTrailingBR: true,
39 baseUrl: ''
40 };
41 Util.apply(this, config, configDefaults);
42 };
43
44 /**
45 * Walk the DOM tree
46 *
47 * @param object node: the root node of the tree
48 * @param boolean includeNode: if set, apply callback to the node
49 * @param string startCallback: a function call to be evaluated on each node, before walking the children
50 * @param string endCallback: a function call to be evaluated on each node, after walking the children
51 * @param array args: array of arguments
52 * @return void
53 */
54 Walker.prototype.walk = function (node, includeNode, startCallback, endCallback, args) {
55 if (!this.removeTagsAndContents.test(node.nodeName)) {
56 if (includeNode) {
57 eval(startCallback);
58 }
59 // Walk the children
60 var child = node.firstChild;
61 while (child) {
62 this.walk(child, true, startCallback, endCallback, args);
63 child = child.nextSibling;
64 }
65 if (includeNode) {
66 eval(endCallback);
67 }
68 }
69 };
70
71 /**
72 * Generate html string from DOM tree
73 *
74 * @param object node: the root node of the tree
75 * @param boolean includeNode: if set, apply callback to root element
76 * @return string rendered html code
77 */
78 Walker.prototype.render = function (node, includeNode) {
79 this.html = '';
80 this.walk(node, includeNode, 'args[0].renderNodeStart(node)', 'args[0].renderNodeEnd(node)', [this]);
81 return this.html;
82 };
83
84 /**
85 * Generate html string for the start of a node
86 *
87 * @param object node: the root node of the tree
88 * @return string rendered html code (accumulated in this.html)
89 */
90 Walker.prototype.renderNodeStart = function (node) {
91 var html = '';
92 switch (node.nodeType) {
93 case Dom.ELEMENT_NODE:
94 if (this.keepTags.test(node.nodeName) && !this.removeTags.test(node.nodeName)) {
95 html += this.setOpeningTag(node);
96 }
97 break;
98 case Dom.TEXT_NODE:
99 html += /^(script|style)$/i.test(node.parentNode.nodeName) ? node.data : Util.htmlEncode(node.data);
100 break;
101 case Dom.ENTITY_NODE:
102 html += node.nodeValue;
103 break;
104 case Dom.ENTITY_REFERENCE_NODE:
105 html += '&' + node.nodeValue + ';';
106 break;
107 case Dom.COMMENT_NODE:
108 if (this.keepComments) {
109 html += '<!--' + node.data + '-->';
110 }
111 break;
112 case Dom.CDATA_SECTION_NODE:
113 if (this.keepCDATASections) {
114 html += '<![CDATA[' + node.data + ']]>';
115 }
116 break;
117 default:
118 // Ignore all other node types
119 break;
120 }
121 this.html += html;
122 };
123
124 /**
125 * Generate html string for the end of a node
126 *
127 * @param object node: the root node of the tree
128 * @return string rendered html code (accumulated in this.html)
129 */
130 Walker.prototype.renderNodeEnd = function (node) {
131 var html = '';
132 if (node.nodeType === Dom.ELEMENT_NODE) {
133 if (this.keepTags.test(node.nodeName) && !this.removeTags.test(node.nodeName)) {
134 html += this.setClosingTag(node);
135 }
136 }
137 this.html += html;
138 };
139
140 /**
141 * Get the attributes of the node, filtered and cleaned-up
142 *
143 * @param object node: the node
144 * @return object an object with attribute name as key and attribute value as value
145 */
146 Walker.prototype.getAttributes = function (node) {
147 var attributes = node.attributes;
148 var filterededAttributes = [];
149 var attribute, attributeName, attributeValue;
150 for (var i = attributes.length; --i >= 0;) {
151 attribute = attributes.item(i);
152 attributeName = attribute.nodeName.toLowerCase();
153 attributeValue = attribute.nodeValue;
154 // Ignore some attributes and those configured to be removed
155 if (/_moz|contenteditable|complete/.test(attributeName) || this.removeAttributes.test(attributeName)) {
156 continue;
157 }
158 // Ignore default values except for the value attribute
159 if (!attribute.specified && attributeName !== 'value') {
160 continue;
161 }
162 if (UserAgent.isIE) {
163 // IE before I9 fails to put style in attributes list.
164 if (attributeName === 'style') {
165 if (UserAgent.isIEBeforeIE9) {
166 attributeValue = node.style.cssText;
167 }
168 // May need to strip the base url
169 } else if (attributeName === 'href' || attributeName === 'src') {
170 attributeValue = this.stripBaseURL(attributeValue);
171 // Ignore value="0" reported by IE on all li elements
172 } else if (attributeName === 'value' && /^li$/i.test(node.nodeName) && attributeValue == 0) {
173 continue;
174 }
175 } else if (UserAgent.isGecko) {
176 // Ignore special values reported by Mozilla
177 if (/(_moz|^$)/.test(attributeValue)) {
178 continue;
179 // Pasted internal url's are made relative by Mozilla: https://bugzilla.mozilla.org/show_bug.cgi?id=613517
180 } else if (attributeName === 'href' || attributeName === 'src') {
181 attributeValue = Dom.addBaseUrl(attributeValue, this.baseUrl);
182 }
183 }
184 // Ignore id attributes generated by ExtJS
185 if (attributeName === 'id' && /^ext-gen/.test(attributeValue)) {
186 continue;
187 }
188 filterededAttributes.push({
189 attributeName: attributeName,
190 attributeValue: attributeValue
191 });
192 }
193 return (UserAgent.isWebKit || UserAgent.isOpera) ? filterededAttributes.reverse() : filterededAttributes;
194 };
195
196 /**
197 * Set opening tag for a node
198 *
199 * @param object node: the node
200 * @return object opening tag
201 */
202 Walker.prototype.setOpeningTag = function (node) {
203 var html = '';
204 // Handle br oddities
205 if (/^br$/i.test(node.nodeName)) {
206 // Remove Mozilla special br node
207 if (UserAgent.isGecko && node.hasAttribute('_moz_editor_bogus_node')) {
208 return html;
209 // In Gecko, whenever some text is entered in an empty block, a trailing br tag is added by the browser.
210 // If the br element is a trailing br in a block element with no other content or with content other than a br, it may be configured to be removed
211 } else if (this.removeTrailingBR && !node.nextSibling && Dom.isBlockElement(node.parentNode) && (!node.previousSibling || !/^br$/i.test(node.previousSibling.nodeName))) {
212 // If an empty paragraph with a class attribute, insert a non-breaking space so that RTE transform does not clean it away
213 if (!node.previousSibling && node.parentNode && /^p$/i.test(node.parentNode.nodeName) && node.parentNode.className) {
214 html += "&nbsp;";
215 }
216 return html;
217 }
218 }
219 // Normal node
220 var attributes = this.getAttributes(node);
221 for (var i = 0, n = attributes.length; i < n; i++) {
222 html += ' ' + attributes[i]['attributeName'] + '="' + Util.htmlEncode(attributes[i]['attributeValue']) + '"';
223 }
224 html = '<' + node.nodeName.toLowerCase() + html + (Dom.RE_noClosingTag.test(node.nodeName) ? ' />' : '>');
225 // Fix orphan list elements
226 if (/^li$/i.test(node.nodeName) && !/^[ou]l$/i.test(node.parentNode.nodeName)) {
227 html = '<ul>' + html;
228 }
229 return html;
230 };
231
232 /**
233 * Set closing tag for a node
234 *
235 * @param object node: the node
236 * @return object closing tag, if required
237 */
238 Walker.prototype.setClosingTag = function (node) {
239 var html = Dom.RE_noClosingTag.test(node.nodeName) ? '' : '</' + node.nodeName.toLowerCase() + '>';
240 // Fix orphan list elements
241 if (/^li$/i.test(node.nodeName) && !/^[ou]l$/i.test(node.parentNode.nodeName)) {
242 html += '</ul>';
243 }
244 return html;
245 };
246
247 /**
248 * Strip base url
249 * May be overridden by link handling plugin
250 *
251 * @param string value: value of a href or src attribute
252 * @return tring stripped value
253 */
254 Walker.prototype.stripBaseURL = function (value) {
255 return value;
256 };
257
258 return Walker;
259
260 });