8
0

styles.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/view/styles
  7. */
  8. import { get, has, isObject, merge, set, unset } from 'lodash-es';
  9. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  10. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  11. class StylesConverter {
  12. /**
  13. * Holds shorthand properties normalizers.
  14. *
  15. * Shorthand properties must be normalized as they can be written in various ways.
  16. * Normalizer must return object describing given shorthand.
  17. *
  18. * Example:
  19. * The `border-color` style is a shorthand property for `border-top-color`, `border-right-color`, `border-bottom-color`
  20. * and `border-left-color`. Similarly there are shorthand for border width (`border-width`) and style (`border-style`).
  21. *
  22. * For `border-color` the given shorthand:
  23. *
  24. * border-color: #f00 #ba7;
  25. *
  26. * might be written as:
  27. *
  28. * border-color-top: #f00;
  29. * border-color-right: #ba7;
  30. * border-color-bottom: #f00;
  31. * border-color-left: #ba7;
  32. *
  33. * Normalizers produces coherent object representation for both shorthand and longhand forms:
  34. *
  35. * stylesConverter.on( 'normalize:border-color', ( evt, data ) => {
  36. * data.path = 'border.color';
  37. * data.value = {
  38. * top: '#f00',
  39. * right: '#ba7',
  40. * bottom: '#f00',
  41. * left: '#ba7'
  42. * }
  43. * } );
  44. *
  45. * @event normalize
  46. */
  47. /**
  48. * An style reducer takes normalized object of style property and outputs array of normalized property-value pairs that can
  49. * be later used to inline a style.
  50. *
  51. * Those work in opposite direction to {@link #normalizers} and always outputs style in the same way.
  52. *
  53. * If normalized style is represented as:
  54. *
  55. * const style = {
  56. * border: {
  57. * color: {
  58. * top: '#f00',
  59. * right: '#ba7',
  60. * bottom: '#f00',
  61. * left: '#ba7'
  62. * }
  63. * }
  64. * }
  65. *
  66. * The border reducer will output:
  67. *
  68. * const reduced = [
  69. * [ 'border-color', '#f00 #ba7' ]
  70. * ];
  71. *
  72. * which can be used to return the inline style string:
  73. *
  74. * style="border-color:#f00 #ba7;"
  75. *
  76. * @event reduce
  77. */
  78. /**
  79. * Returns reduced form of style property form normalized object.
  80. *
  81. * @private
  82. * @param {String} styleName
  83. * @param {Object|String} normalizedValue
  84. * @returns {Array.<Array.<String, String>>}
  85. */
  86. _getReduceForm( styleName, normalizedValue ) {
  87. const data = {
  88. value: normalizedValue
  89. };
  90. this.fire( 'reduce:' + styleName, data );
  91. return data.reduced || [ [ styleName, normalizedValue ] ];
  92. }
  93. getNormalized( name, styles ) {
  94. if ( !name ) {
  95. return merge( {}, styles );
  96. }
  97. if ( styles[ name ] ) {
  98. return styles[ name ];
  99. }
  100. const data = {
  101. name,
  102. styles
  103. };
  104. this.fire( 'extract:' + name, data );
  105. if ( data.path ) {
  106. return get( styles, data.path );
  107. }
  108. if ( data.value ) {
  109. return data.value;
  110. }
  111. // if ( this.extractors.has( name ) ) {
  112. // const extractor = this.extractors.get( name );
  113. //
  114. // if ( typeof extractor === 'string' ) {
  115. // return this.getNormalized( extractor, styles );
  116. // }
  117. //
  118. // return extractor( styles, this );
  119. // }
  120. return get( styles, toPath( name ) );
  121. }
  122. /**
  123. * Parse style property value to a normalized form.
  124. *
  125. * @param {String} propertyName Name of style property.
  126. * @param {String} value Value of style property.
  127. * @param {Object} styles
  128. * @private
  129. */
  130. _toNormalizedForm( propertyName, value, styles ) {
  131. if ( isObject( value ) ) {
  132. appendStyleValue( styles, toPath( propertyName ), value );
  133. return;
  134. }
  135. const data = {
  136. path: propertyName,
  137. value
  138. };
  139. this.fire( 'normalize:' + propertyName, data );
  140. appendStyleValue( styles, data.path, data.value );
  141. }
  142. }
  143. mix( StylesConverter, EmitterMixin );
  144. const stylesConverter = new StylesConverter();
  145. class BorderStyles {
  146. static attach( stylesConverter ) {
  147. stylesConverter.on( 'normalize:border', normalizeBorder );
  148. // Border-position shorthands.
  149. stylesConverter.on( 'normalize:border-top', getBorderPositionNormalizer( 'top' ) );
  150. stylesConverter.on( 'normalize:border-right', getBorderPositionNormalizer( 'right' ) );
  151. stylesConverter.on( 'normalize:border-bottom', getBorderPositionNormalizer( 'bottom' ) );
  152. stylesConverter.on( 'normalize:border-left', getBorderPositionNormalizer( 'left' ) );
  153. // Border-property shorthands.
  154. stylesConverter.on( 'normalize:border-color', getBorderPropertyNormalizer( 'color' ) );
  155. stylesConverter.on( 'normalize:border-width', getBorderPropertyNormalizer( 'width' ) );
  156. stylesConverter.on( 'normalize:border-style', getBorderPropertyNormalizer( 'style' ) );
  157. // Border longhands.
  158. stylesConverter.on( 'normalize:border-top-color', getBorderPropertyPositionNormalizer( 'color', 'top' ) );
  159. stylesConverter.on( 'normalize:border-top-style', getBorderPropertyPositionNormalizer( 'style', 'top' ) );
  160. stylesConverter.on( 'normalize:border-top-width', getBorderPropertyPositionNormalizer( 'width', 'top' ) );
  161. stylesConverter.on( 'normalize:border-right-color', getBorderPropertyPositionNormalizer( 'color', 'right' ) );
  162. stylesConverter.on( 'normalize:border-right-style', getBorderPropertyPositionNormalizer( 'style', 'right' ) );
  163. stylesConverter.on( 'normalize:border-right-width', getBorderPropertyPositionNormalizer( 'width', 'right' ) );
  164. stylesConverter.on( 'normalize:border-bottom-color', getBorderPropertyPositionNormalizer( 'color', 'bottom' ) );
  165. stylesConverter.on( 'normalize:border-bottom-style', getBorderPropertyPositionNormalizer( 'style', 'bottom' ) );
  166. stylesConverter.on( 'normalize:border-bottom-width', getBorderPropertyPositionNormalizer( 'width', 'bottom' ) );
  167. stylesConverter.on( 'normalize:border-left-color', getBorderPropertyPositionNormalizer( 'color', 'left' ) );
  168. stylesConverter.on( 'normalize:border-left-style', getBorderPropertyPositionNormalizer( 'style', 'left' ) );
  169. stylesConverter.on( 'normalize:border-left-width', getBorderPropertyPositionNormalizer( 'width', 'left' ) );
  170. stylesConverter.on( 'extract:border-top', borderPositionExtractor( 'top' ) );
  171. stylesConverter.on( 'extract:border-right', borderPositionExtractor( 'right' ) );
  172. stylesConverter.on( 'extract:border-bottom', borderPositionExtractor( 'bottom' ) );
  173. stylesConverter.on( 'extract:border-left', borderPositionExtractor( 'left' ) );
  174. stylesConverter.on( 'extract:border-top-color', ( evt, data ) => ( data.path = 'border.color.top' ) );
  175. stylesConverter.on( 'extract:border-right-color', ( evt, data ) => ( data.path = 'border.color.right' ) );
  176. stylesConverter.on( 'extract:border-bottom-color', ( evt, data ) => ( data.path = 'border.color.bottom' ) );
  177. stylesConverter.on( 'extract:border-left-color', ( evt, data ) => ( data.path = 'border.color.left' ) );
  178. stylesConverter.on( 'extract:border-top-width', ( evt, data ) => ( data.path = 'border.width.top' ) );
  179. stylesConverter.on( 'extract:border-right-width', ( evt, data ) => ( data.path = 'border.width.right' ) );
  180. stylesConverter.on( 'extract:border-bottom-width', ( evt, data ) => ( data.path = 'border.width.bottom' ) );
  181. stylesConverter.on( 'extract:border-left-width', ( evt, data ) => ( data.path = 'border.width.left' ) );
  182. stylesConverter.on( 'extract:border-top-style', ( evt, data ) => ( data.path = 'border.style.top' ) );
  183. stylesConverter.on( 'extract:border-right-style', ( evt, data ) => ( data.path = 'border.style.right' ) );
  184. stylesConverter.on( 'extract:border-bottom-style', ( evt, data ) => ( data.path = 'border.style.bottom' ) );
  185. stylesConverter.on( 'extract:border-left-style', ( evt, data ) => ( data.path = 'border.style.left' ) );
  186. stylesConverter.on( 'reduce:border-color', getTopRightBottomLeftValueReducer( 'border-color' ) );
  187. stylesConverter.on( 'reduce:border-style', getTopRightBottomLeftValueReducer( 'border-style' ) );
  188. stylesConverter.on( 'reduce:border-width', getTopRightBottomLeftValueReducer( 'border-width' ) );
  189. stylesConverter.on( 'reduce:border-top',
  190. ( evt, data ) => ( data.reduced = getBorderPositionReducer( 'top' )( data.value ) ) );
  191. stylesConverter.on( 'reduce:border-right',
  192. ( evt, data ) => ( data.reduced = getBorderPositionReducer( 'right' )( data.value ) ) );
  193. stylesConverter.on( 'reduce:border-bottom',
  194. ( evt, data ) => ( data.reduced = getBorderPositionReducer( 'bottom' )( data.value ) ) );
  195. stylesConverter.on( 'reduce:border-left',
  196. ( evt, data ) => ( data.reduced = getBorderPositionReducer( 'left' )( data.value ) ) );
  197. stylesConverter.on( 'reduce:border', getBorderReducer );
  198. }
  199. }
  200. class MarginStyles {
  201. static attach( stylesConverter ) {
  202. stylesConverter.on( 'normalize:margin', getPositionShorthandNormalizer( 'margin' ) );
  203. stylesConverter.on( 'normalize:margin-top', ( evt, data ) => ( data.path = 'margin.top' ) );
  204. stylesConverter.on( 'normalize:margin-right', ( evt, data ) => ( data.path = 'margin.right' ) );
  205. stylesConverter.on( 'normalize:margin-bottom', ( evt, data ) => ( data.path = 'margin.bottom' ) );
  206. stylesConverter.on( 'normalize:margin-left', ( evt, data ) => ( data.path = 'margin.left' ) );
  207. stylesConverter.on( 'reduce:margin', getTopRightBottomLeftValueReducer( 'margin' ) );
  208. }
  209. }
  210. class PaddingStyles {
  211. static attach( stylesConverter ) {
  212. stylesConverter.on( 'normalize:padding', getPositionShorthandNormalizer( 'padding' ) );
  213. stylesConverter.on( 'normalize:padding-top', ( evt, data ) => ( data.path = 'padding.top' ) );
  214. stylesConverter.on( 'normalize:padding-right', ( evt, data ) => ( data.path = 'padding.right' ) );
  215. stylesConverter.on( 'normalize:padding-bottom', ( evt, data ) => ( data.path = 'padding.bottom' ) );
  216. stylesConverter.on( 'normalize:padding-left', ( evt, data ) => ( data.path = 'padding.left' ) );
  217. stylesConverter.on( 'reduce:padding', getTopRightBottomLeftValueReducer( 'padding' ) );
  218. }
  219. }
  220. class BackgroundStyles {
  221. static attach( stylesConverter ) {
  222. stylesConverter.on( 'normalize:background', normalizeBackground );
  223. stylesConverter.on( 'normalize:background-color', ( evt, data ) => ( data.path = 'background.color' ) );
  224. stylesConverter.on( 'reduce:background', ( evt, data ) => {
  225. const ret = [];
  226. ret.push( [ 'background-color', data.value.color ] );
  227. data.reduced = ret;
  228. } );
  229. }
  230. }
  231. BorderStyles.attach( stylesConverter );
  232. MarginStyles.attach( stylesConverter );
  233. PaddingStyles.attach( stylesConverter );
  234. BackgroundStyles.attach( stylesConverter );
  235. /**
  236. * Styles class.
  237. *
  238. * Handles styles normalization.
  239. */
  240. export default class Styles {
  241. /**
  242. * Creates Styles instance.
  243. */
  244. constructor() {
  245. /**
  246. * @type {{}}
  247. * @private
  248. */
  249. this._styles = {};
  250. }
  251. /**
  252. * Number of styles defined.
  253. *
  254. * @type {Number}
  255. */
  256. get size() {
  257. return this.getStyleNames().length;
  258. }
  259. /**
  260. * Re-sets internal styles definition.
  261. *
  262. * @param {String} styleString
  263. */
  264. setStyle( styleString ) {
  265. this.clear();
  266. const map = parseInlineStyles( styleString );
  267. for ( const key of map.keys() ) {
  268. const value = map.get( key );
  269. stylesConverter._toNormalizedForm( key, value, this._styles );
  270. }
  271. }
  272. /**
  273. * Checks if single style rule is set.
  274. *
  275. * Supports shorthands.
  276. *
  277. * @param {String} name
  278. * @returns {Boolean}
  279. */
  280. hasProperty( name ) {
  281. const nameNorm = toPath( name );
  282. return has( this._styles, nameNorm ) || !!this._styles[ name ];
  283. }
  284. /**
  285. * Inserts single style property.
  286. *
  287. * Can insert one by one
  288. *
  289. * styles.insertProperty( 'color', 'blue' );
  290. * styles.insertProperty( 'margin-right', '1em' );
  291. *
  292. * or many styles at once:
  293. *
  294. * styles.insertProperty( {
  295. * color: 'blue',
  296. * 'margin-right': '1em'
  297. * } );
  298. *
  299. * Supports shorthands.
  300. *
  301. * @param {String|Object} nameOrObject
  302. * @param {String|Object} value
  303. * @returns {Boolean}
  304. */
  305. insertProperty( nameOrObject, value ) {
  306. if ( isObject( nameOrObject ) ) {
  307. for ( const key of Object.keys( nameOrObject ) ) {
  308. this.insertProperty( key, nameOrObject[ key ] );
  309. }
  310. } else {
  311. stylesConverter._toNormalizedForm( nameOrObject, value, this._styles );
  312. }
  313. }
  314. /**
  315. * Removes styles property.
  316. *
  317. * @param name
  318. */
  319. removeProperty( name ) {
  320. unset( this._styles, toPath( name ) );
  321. delete this._styles[ name ];
  322. }
  323. /**
  324. * Return normalized style object;
  325. *
  326. * const styles = new Styles();
  327. * styles.setStyle( 'margin:1px 2px 3em;' );
  328. *
  329. * console.log( styles.getNormalized( 'margin' ) );
  330. * // will log:
  331. * // {
  332. * // top: '1px',
  333. * // right: '2px',
  334. * // bottom: '3em',
  335. * // left: '2px'
  336. * // }
  337. *
  338. * @param {String} name
  339. * @returns {Object|undefined}
  340. */
  341. getNormalized( name ) {
  342. return stylesConverter.getNormalized( name, this._styles );
  343. }
  344. /**
  345. * Returns a string containing normalized styles string or undefined if no style properties are set.
  346. *
  347. * @returns {String|undefined}
  348. */
  349. getInlineStyle() {
  350. const entries = this._getStylesEntries();
  351. // Return undefined for empty styles map.
  352. if ( !entries.length ) {
  353. return;
  354. }
  355. return entries.map( arr => arr.join( ':' ) ).join( ';' ) + ';';
  356. }
  357. /**
  358. * Returns property value string.
  359. *
  360. * @param {String} propertyName
  361. * @returns {String|undefined}
  362. */
  363. getInlineProperty( propertyName ) {
  364. const normalized = stylesConverter.getNormalized( propertyName, this._styles );
  365. if ( !normalized ) {
  366. // Try return styles set directly - values that are not parsed.
  367. return this._styles[ propertyName ];
  368. }
  369. if ( isObject( normalized ) ) {
  370. const styles = stylesConverter._getReduceForm( propertyName, normalized );
  371. const propertyDescriptor = styles.find( ( [ property ] ) => property === propertyName );
  372. // Only return a value if it is set;
  373. if ( Array.isArray( propertyDescriptor ) ) {
  374. return propertyDescriptor[ 1 ];
  375. }
  376. } else {
  377. return normalized;
  378. }
  379. }
  380. /**
  381. * Returns style properties names as the would appear when using {@link #getInlineStyle()}
  382. *
  383. * @returns {Array.<String>}
  384. */
  385. getStyleNames() {
  386. const entries = this._getStylesEntries();
  387. return entries.map( ( [ key ] ) => key );
  388. }
  389. /**
  390. * Removes all styles.
  391. */
  392. clear() {
  393. this._styles = {};
  394. }
  395. /**
  396. * Returns normalized styles entries for further processing.
  397. *
  398. * @private
  399. * @returns {Array.<Array.<String, String>> ]}
  400. */
  401. _getStylesEntries() {
  402. const parsed = [];
  403. const keys = Object.keys( this._styles ).sort();
  404. for ( const key of keys ) {
  405. const normalized = stylesConverter.getNormalized( key, this._styles );
  406. parsed.push( ...stylesConverter._getReduceForm( key, normalized ) );
  407. }
  408. return parsed;
  409. }
  410. }
  411. function getTopRightBottomLeftValues( value = '' ) {
  412. if ( value === '' ) {
  413. return { top: undefined, right: undefined, bottom: undefined, left: undefined };
  414. }
  415. const values = value.split( ' ' );
  416. const top = values[ 0 ];
  417. const bottom = values[ 2 ] || top;
  418. const right = values[ 1 ] || top;
  419. const left = values[ 3 ] || right;
  420. return { top, bottom, right, left };
  421. }
  422. function toBorderPropertyShorthand( value, property ) {
  423. return {
  424. [ property ]: getTopRightBottomLeftValues( value )
  425. };
  426. }
  427. function getPositionShorthandNormalizer( longhand ) {
  428. return ( evt, data ) => {
  429. data.path = longhand;
  430. data.value = getTopRightBottomLeftValues( data.value );
  431. };
  432. }
  433. function normalizeBorder( evt, data ) {
  434. const { color, style, width } = normalizeBorderShorthand( data.value );
  435. data.path = 'border';
  436. data.value = {
  437. color: getTopRightBottomLeftValues( color ),
  438. style: getTopRightBottomLeftValues( style ),
  439. width: getTopRightBottomLeftValues( width )
  440. };
  441. }
  442. function getBorderPositionNormalizer( side ) {
  443. return ( evt, data ) => {
  444. const { color, style, width } = normalizeBorderShorthand( data.value );
  445. const border = {};
  446. if ( color !== undefined ) {
  447. border.color = { [ side ]: color };
  448. }
  449. if ( style !== undefined ) {
  450. border.style = { [ side ]: style };
  451. }
  452. if ( width !== undefined ) {
  453. border.width = { [ side ]: width };
  454. }
  455. data.path = 'border';
  456. data.value = border;
  457. };
  458. }
  459. function getBorderPropertyNormalizer( propertyName ) {
  460. return ( evt, data ) => {
  461. data.path = 'border';
  462. data.value = toBorderPropertyShorthand( data.value, propertyName );
  463. };
  464. }
  465. function getBorderPropertyPositionNormalizer( property, side ) {
  466. return ( evt, data ) => {
  467. data.path = 'border';
  468. data.value = {
  469. [ property ]: {
  470. [ side ]: data.value
  471. }
  472. };
  473. };
  474. }
  475. function borderPositionExtractor( which ) {
  476. return ( evt, data ) => {
  477. const border = data.styles.border;
  478. const value = [];
  479. if ( border.width && border.width[ which ] ) {
  480. value.push( border.width[ which ] );
  481. }
  482. if ( border.style && border.style[ which ] ) {
  483. value.push( border.style[ which ] );
  484. }
  485. if ( border.color && border.color[ which ] ) {
  486. value.push( border.color[ which ] );
  487. }
  488. data.value = value.join( ' ' );
  489. };
  490. }
  491. function normalizeBorderShorthand( string ) {
  492. const result = {};
  493. for ( const part of string.split( ' ' ) ) {
  494. if ( isLength( part ) ) {
  495. result.width = part;
  496. }
  497. if ( isLineStyle( part ) ) {
  498. result.style = part;
  499. }
  500. if ( isColor( part ) ) {
  501. result.color = part;
  502. }
  503. }
  504. return result;
  505. }
  506. function normalizeBackground( evt, data ) {
  507. const background = {};
  508. const parts = data.value.split( ' ' );
  509. for ( const part of parts ) {
  510. if ( isRepeat( part ) ) {
  511. background.repeat = background.repeat || [];
  512. background.repeat.push( part );
  513. } else if ( isPosition( part ) ) {
  514. background.position = background.position || [];
  515. background.position.push( part );
  516. } else if ( isAttachment( part ) ) {
  517. background.attachment = part;
  518. } else if ( isColor( part ) ) {
  519. background.color = part;
  520. } else if ( isURL( part ) ) {
  521. background.image = part;
  522. }
  523. }
  524. data.path = 'background';
  525. data.value = background;
  526. }
  527. function isColor( string ) {
  528. return /^([#0-9A-Fa-f]{3,8}|[a-zA-Z]+)$/.test( string ) && !isLineStyle( string );
  529. }
  530. function isLineStyle( string ) {
  531. return /^(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)$/.test( string );
  532. }
  533. function isLength( string ) {
  534. return /^[+-]?[0-9]?[.]?[0-9]+([a-z]+|%)$/.test( string );
  535. }
  536. function isRepeat( string ) {
  537. return /^(repeat-x|repeat-y|repeat|space|round|no-repeat)$/.test( string );
  538. }
  539. function isPosition( string ) {
  540. return /^(center|top|bottom|left|right)$/.test( string );
  541. }
  542. function isAttachment( string ) {
  543. return /^(fixed|scroll|local)$/.test( string );
  544. }
  545. function isURL( string ) {
  546. return /^url\(/.test( string );
  547. }
  548. function getBorderReducer( evt, data ) {
  549. const ret = [];
  550. ret.push( ...getBorderPositionReducer( 'top' )( data.value ) );
  551. ret.push( ...getBorderPositionReducer( 'right' )( data.value ) );
  552. ret.push( ...getBorderPositionReducer( 'bottom' )( data.value ) );
  553. ret.push( ...getBorderPositionReducer( 'left' )( data.value ) );
  554. data.reduced = ret;
  555. }
  556. function getTopRightBottomLeftValueReducer( styleShorthand ) {
  557. return ( evt, data ) => {
  558. const { top, right, bottom, left } = ( data.value || {} );
  559. const reduced = [];
  560. if ( ![ top, right, left, bottom ].every( value => !!value ) ) {
  561. if ( top ) {
  562. reduced.push( [ styleShorthand + '-top', top ] );
  563. }
  564. if ( right ) {
  565. reduced.push( [ styleShorthand + '-right', right ] );
  566. }
  567. if ( bottom ) {
  568. reduced.push( [ styleShorthand + '-bottom', bottom ] );
  569. }
  570. if ( left ) {
  571. reduced.push( [ styleShorthand + '-left', left ] );
  572. }
  573. } else {
  574. reduced.push( [ styleShorthand, getTopRightBottomLeftShorthandValue( data.value ) ] );
  575. }
  576. data.reduced = reduced;
  577. };
  578. }
  579. function getBorderPositionReducer( which ) {
  580. return value => {
  581. const reduced = [];
  582. if ( value && value.width && value.width[ which ] !== undefined ) {
  583. reduced.push( value.width[ which ] );
  584. }
  585. if ( value && value.style && value.style[ which ] !== undefined ) {
  586. reduced.push( value.style[ which ] );
  587. }
  588. if ( value && value.color && value.color[ which ] !== undefined ) {
  589. reduced.push( value.color[ which ] );
  590. }
  591. if ( reduced.length ) {
  592. return [ [ 'border-' + which, reduced.join( ' ' ) ] ];
  593. }
  594. return [];
  595. };
  596. }
  597. function getTopRightBottomLeftShorthandValue( { left, right, top, bottom } ) {
  598. const out = [];
  599. if ( left !== right ) {
  600. out.push( top, right, bottom, left );
  601. } else if ( bottom !== top ) {
  602. out.push( top, right, bottom );
  603. } else if ( right !== top ) {
  604. out.push( top, right );
  605. } else {
  606. out.push( top );
  607. }
  608. return out.join( ' ' );
  609. }
  610. // Parses inline styles and puts property - value pairs into styles map.
  611. //
  612. // @param {String} stylesString Styles to parse.
  613. // @returns {Map.<String, String>} stylesMap Map of parsed properties and values.
  614. function parseInlineStyles( stylesString ) {
  615. // `null` if no quote was found in input string or last found quote was a closing quote. See below.
  616. let quoteType = null;
  617. let propertyNameStart = 0;
  618. let propertyValueStart = 0;
  619. let propertyName = null;
  620. const stylesMap = new Map();
  621. // Do not set anything if input string is empty.
  622. if ( stylesString === '' ) {
  623. return stylesMap;
  624. }
  625. // Fix inline styles that do not end with `;` so they are compatible with algorithm below.
  626. if ( stylesString.charAt( stylesString.length - 1 ) != ';' ) {
  627. stylesString = stylesString + ';';
  628. }
  629. // Seek the whole string for "special characters".
  630. for ( let i = 0; i < stylesString.length; i++ ) {
  631. const char = stylesString.charAt( i );
  632. if ( quoteType === null ) {
  633. // No quote found yet or last found quote was a closing quote.
  634. switch ( char ) {
  635. case ':':
  636. // Most of time colon means that property name just ended.
  637. // Sometimes however `:` is found inside property value (for example in background image url).
  638. if ( !propertyName ) {
  639. // Treat this as end of property only if property name is not already saved.
  640. // Save property name.
  641. propertyName = stylesString.substr( propertyNameStart, i - propertyNameStart );
  642. // Save this point as the start of property value.
  643. propertyValueStart = i + 1;
  644. }
  645. break;
  646. case '"':
  647. case '\'':
  648. // Opening quote found (this is an opening quote, because `quoteType` is `null`).
  649. quoteType = char;
  650. break;
  651. case ';': {
  652. // Property value just ended.
  653. // Use previously stored property value start to obtain property value.
  654. const propertyValue = stylesString.substr( propertyValueStart, i - propertyValueStart );
  655. if ( propertyName ) {
  656. // Save parsed part.
  657. stylesMap.set( propertyName.trim(), propertyValue.trim() );
  658. }
  659. propertyName = null;
  660. // Save this point as property name start. Property name starts immediately after previous property value ends.
  661. propertyNameStart = i + 1;
  662. break;
  663. }
  664. }
  665. } else if ( char === quoteType ) {
  666. // If a quote char is found and it is a closing quote, mark this fact by `null`-ing `quoteType`.
  667. quoteType = null;
  668. }
  669. }
  670. return stylesMap;
  671. }
  672. function toPath( name ) {
  673. return name.replace( '-', '.' );
  674. }
  675. // Appends style definition to the styles object.
  676. //
  677. // @param {String} nameOrPath
  678. // @param {String|Object} valueOrObject
  679. // @private
  680. function appendStyleValue( stylesObject, nameOrPath, valueOrObject ) {
  681. let valueToSet = valueOrObject;
  682. if ( isObject( valueOrObject ) ) {
  683. valueToSet = merge( {}, get( stylesObject, nameOrPath ), valueOrObject );
  684. }
  685. set( stylesObject, nameOrPath, valueToSet );
  686. }