Styles.js 9.5 KB

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