|
1 /** |
|
2 * Styles.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 parse CSS styles it also compresses styles to reduce the output size. |
|
13 * |
|
14 * @example |
|
15 * var Styles = new tinymce.html.Styles({ |
|
16 * url_converter: function(url) { |
|
17 * return url; |
|
18 * } |
|
19 * }); |
|
20 * |
|
21 * styles = Styles.parse('border: 1px solid red'); |
|
22 * styles.color = 'red'; |
|
23 * |
|
24 * console.log(new tinymce.html.StyleSerializer().serialize(styles)); |
|
25 * |
|
26 * @class tinymce.html.Styles |
|
27 * @version 3.4 |
|
28 */ |
|
29 define("tinymce/html/Styles", [], function() { |
|
30 return function(settings, schema) { |
|
31 /*jshint maxlen:255 */ |
|
32 /*eslint max-len:0 */ |
|
33 var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, |
|
34 urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, |
|
35 styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, |
|
36 trimRightRegExp = /\s+$/, |
|
37 undef, i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF'; |
|
38 |
|
39 settings = settings || {}; |
|
40 |
|
41 if (schema) { |
|
42 validStyles = schema.getValidStyles(); |
|
43 invalidStyles = schema.getInvalidStyles(); |
|
44 } |
|
45 |
|
46 encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); |
|
47 for (i = 0; i < encodingItems.length; i++) { |
|
48 encodingLookup[encodingItems[i]] = invisibleChar + i; |
|
49 encodingLookup[invisibleChar + i] = encodingItems[i]; |
|
50 } |
|
51 |
|
52 function toHex(match, r, g, b) { |
|
53 function hex(val) { |
|
54 val = parseInt(val, 10).toString(16); |
|
55 |
|
56 return val.length > 1 ? val : '0' + val; // 0 -> 00 |
|
57 } |
|
58 |
|
59 return '#' + hex(r) + hex(g) + hex(b); |
|
60 } |
|
61 |
|
62 return { |
|
63 /** |
|
64 * Parses the specified RGB color value and returns a hex version of that color. |
|
65 * |
|
66 * @method toHex |
|
67 * @param {String} color RGB string value like rgb(1,2,3) |
|
68 * @return {String} Hex version of that RGB value like #FF00FF. |
|
69 */ |
|
70 toHex: function(color) { |
|
71 return color.replace(rgbRegExp, toHex); |
|
72 }, |
|
73 |
|
74 /** |
|
75 * Parses the specified style value into an object collection. This parser will also |
|
76 * merge and remove any redundant items that browsers might have added. It will also convert non hex |
|
77 * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. |
|
78 * |
|
79 * @method parse |
|
80 * @param {String} css Style value to parse for example: border:1px solid red;. |
|
81 * @return {Object} Object representation of that style like {border: '1px solid red'} |
|
82 */ |
|
83 parse: function(css) { |
|
84 var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter; |
|
85 var urlConverterScope = settings.url_converter_scope || this; |
|
86 |
|
87 function compress(prefix, suffix, noJoin) { |
|
88 var top, right, bottom, left; |
|
89 |
|
90 top = styles[prefix + '-top' + suffix]; |
|
91 if (!top) { |
|
92 return; |
|
93 } |
|
94 |
|
95 right = styles[prefix + '-right' + suffix]; |
|
96 if (!right) { |
|
97 return; |
|
98 } |
|
99 |
|
100 bottom = styles[prefix + '-bottom' + suffix]; |
|
101 if (!bottom) { |
|
102 return; |
|
103 } |
|
104 |
|
105 left = styles[prefix + '-left' + suffix]; |
|
106 if (!left) { |
|
107 return; |
|
108 } |
|
109 |
|
110 var box = [top, right, bottom, left]; |
|
111 i = box.length - 1; |
|
112 while (i--) { |
|
113 if (box[i] !== box[i + 1]) { |
|
114 break; |
|
115 } |
|
116 } |
|
117 |
|
118 if (i > -1 && noJoin) { |
|
119 return; |
|
120 } |
|
121 |
|
122 styles[prefix + suffix] = i == -1 ? box[0] : box.join(' '); |
|
123 delete styles[prefix + '-top' + suffix]; |
|
124 delete styles[prefix + '-right' + suffix]; |
|
125 delete styles[prefix + '-bottom' + suffix]; |
|
126 delete styles[prefix + '-left' + suffix]; |
|
127 } |
|
128 |
|
129 /** |
|
130 * Checks if the specific style can be compressed in other words if all border-width are equal. |
|
131 */ |
|
132 function canCompress(key) { |
|
133 var value = styles[key], i; |
|
134 |
|
135 if (!value) { |
|
136 return; |
|
137 } |
|
138 |
|
139 value = value.split(' '); |
|
140 i = value.length; |
|
141 while (i--) { |
|
142 if (value[i] !== value[0]) { |
|
143 return false; |
|
144 } |
|
145 } |
|
146 |
|
147 styles[key] = value[0]; |
|
148 |
|
149 return true; |
|
150 } |
|
151 |
|
152 /** |
|
153 * Compresses multiple styles into one style. |
|
154 */ |
|
155 function compress2(target, a, b, c) { |
|
156 if (!canCompress(a)) { |
|
157 return; |
|
158 } |
|
159 |
|
160 if (!canCompress(b)) { |
|
161 return; |
|
162 } |
|
163 |
|
164 if (!canCompress(c)) { |
|
165 return; |
|
166 } |
|
167 |
|
168 // Compress |
|
169 styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; |
|
170 delete styles[a]; |
|
171 delete styles[b]; |
|
172 delete styles[c]; |
|
173 } |
|
174 |
|
175 // Encodes the specified string by replacing all \" \' ; : with _<num> |
|
176 function encode(str) { |
|
177 isEncoded = true; |
|
178 |
|
179 return encodingLookup[str]; |
|
180 } |
|
181 |
|
182 // Decodes the specified string by replacing all _<num> with it's original value \" \' etc |
|
183 // It will also decode the \" \' if keep_slashes is set to fale or omitted |
|
184 function decode(str, keep_slashes) { |
|
185 if (isEncoded) { |
|
186 str = str.replace(/\uFEFF[0-9]/g, function(str) { |
|
187 return encodingLookup[str]; |
|
188 }); |
|
189 } |
|
190 |
|
191 if (!keep_slashes) { |
|
192 str = str.replace(/\\([\'\";:])/g, "$1"); |
|
193 } |
|
194 |
|
195 return str; |
|
196 } |
|
197 |
|
198 function processUrl(match, url, url2, url3, str, str2) { |
|
199 str = str || str2; |
|
200 |
|
201 if (str) { |
|
202 str = decode(str); |
|
203 |
|
204 // Force strings into single quote format |
|
205 return "'" + str.replace(/\'/g, "\\'") + "'"; |
|
206 } |
|
207 |
|
208 url = decode(url || url2 || url3); |
|
209 |
|
210 if (!settings.allow_script_urls) { |
|
211 var scriptUrl = url.replace(/[\s\r\n]+/, ''); |
|
212 |
|
213 if (/(java|vb)script:/i.test(scriptUrl)) { |
|
214 return ""; |
|
215 } |
|
216 |
|
217 if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { |
|
218 return ""; |
|
219 } |
|
220 } |
|
221 |
|
222 // Convert the URL to relative/absolute depending on config |
|
223 if (urlConverter) { |
|
224 url = urlConverter.call(urlConverterScope, url, 'style'); |
|
225 } |
|
226 |
|
227 // Output new URL format |
|
228 return "url('" + url.replace(/\'/g, "\\'") + "')"; |
|
229 } |
|
230 |
|
231 if (css) { |
|
232 css = css.replace(/[\u0000-\u001F]/g, ''); |
|
233 |
|
234 // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing |
|
235 css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) { |
|
236 return str.replace(/[;:]/g, encode); |
|
237 }); |
|
238 |
|
239 // Parse styles |
|
240 while ((matches = styleRegExp.exec(css))) { |
|
241 name = matches[1].replace(trimRightRegExp, '').toLowerCase(); |
|
242 value = matches[2].replace(trimRightRegExp, ''); |
|
243 |
|
244 // Decode escaped sequences like \65 -> e |
|
245 /*jshint loopfunc:true*/ |
|
246 /*eslint no-loop-func:0 */ |
|
247 value = value.replace(/\\[0-9a-f]+/g, function(e) { |
|
248 return String.fromCharCode(parseInt(e.substr(1), 16)); |
|
249 }); |
|
250 |
|
251 if (name && value.length > 0) { |
|
252 // Don't allow behavior name or expression/comments within the values |
|
253 if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) { |
|
254 continue; |
|
255 } |
|
256 |
|
257 // Opera will produce 700 instead of bold in their style values |
|
258 if (name === 'font-weight' && value === '700') { |
|
259 value = 'bold'; |
|
260 } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED |
|
261 value = value.toLowerCase(); |
|
262 } |
|
263 |
|
264 // Convert RGB colors to HEX |
|
265 value = value.replace(rgbRegExp, toHex); |
|
266 |
|
267 // Convert URLs and force them into url('value') format |
|
268 value = value.replace(urlOrStrRegExp, processUrl); |
|
269 styles[name] = isEncoded ? decode(value, true) : value; |
|
270 } |
|
271 |
|
272 styleRegExp.lastIndex = matches.index + matches[0].length; |
|
273 } |
|
274 // Compress the styles to reduce it's size for example IE will expand styles |
|
275 compress("border", "", true); |
|
276 compress("border", "-width"); |
|
277 compress("border", "-color"); |
|
278 compress("border", "-style"); |
|
279 compress("padding", ""); |
|
280 compress("margin", ""); |
|
281 compress2('border', 'border-width', 'border-style', 'border-color'); |
|
282 |
|
283 // Remove pointless border, IE produces these |
|
284 if (styles.border === 'medium none') { |
|
285 delete styles.border; |
|
286 } |
|
287 |
|
288 // IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p> |
|
289 // So lets asume it shouldn't be there |
|
290 if (styles['border-image'] === 'none') { |
|
291 delete styles['border-image']; |
|
292 } |
|
293 } |
|
294 |
|
295 return styles; |
|
296 }, |
|
297 |
|
298 /** |
|
299 * Serializes the specified style object into a string. |
|
300 * |
|
301 * @method serialize |
|
302 * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'} |
|
303 * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized. |
|
304 * @return {String} String representation of the style object for example: border: 1px solid red. |
|
305 */ |
|
306 serialize: function(styles, elementName) { |
|
307 var css = '', name, value; |
|
308 |
|
309 function serializeStyles(name) { |
|
310 var styleList, i, l, value; |
|
311 |
|
312 styleList = validStyles[name]; |
|
313 if (styleList) { |
|
314 for (i = 0, l = styleList.length; i < l; i++) { |
|
315 name = styleList[i]; |
|
316 value = styles[name]; |
|
317 |
|
318 if (value !== undef && value.length > 0) { |
|
319 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; |
|
320 } |
|
321 } |
|
322 } |
|
323 } |
|
324 |
|
325 function isValid(name, elementName) { |
|
326 var styleMap; |
|
327 |
|
328 styleMap = invalidStyles['*']; |
|
329 if (styleMap && styleMap[name]) { |
|
330 return false; |
|
331 } |
|
332 |
|
333 styleMap = invalidStyles[elementName]; |
|
334 if (styleMap && styleMap[name]) { |
|
335 return false; |
|
336 } |
|
337 |
|
338 return true; |
|
339 } |
|
340 |
|
341 // Serialize styles according to schema |
|
342 if (elementName && validStyles) { |
|
343 // Serialize global styles and element specific styles |
|
344 serializeStyles('*'); |
|
345 serializeStyles(elementName); |
|
346 } else { |
|
347 // Output the styles in the order they are inside the object |
|
348 for (name in styles) { |
|
349 value = styles[name]; |
|
350 |
|
351 if (value !== undef && value.length > 0) { |
|
352 if (!invalidStyles || isValid(name, elementName)) { |
|
353 css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; |
|
354 } |
|
355 } |
|
356 } |
|
357 } |
|
358 |
|
359 return css; |
|
360 } |
|
361 }; |
|
362 }; |
|
363 }); |