stylesmap.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  1. /**
  2. * @license Copyright (c) 2003-2020, 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/stylesmap
  7. */
  8. import { get, isObject, merge, set, unset } from 'lodash-es';
  9. /**
  10. * Styles map. Allows handling (adding, removing, retrieving) a set of style rules (usually, of an element).
  11. *
  12. * The styles map is capable of normalizing style names so e.g. the following operations are possible:
  13. */
  14. export default class StylesMap {
  15. /**
  16. * Creates Styles instance.
  17. *
  18. * @param {module:engine/view/stylesmap~StylesProcessor} styleProcessor
  19. */
  20. constructor( styleProcessor ) {
  21. /**
  22. * Keeps an internal representation of styles map. Normalized styles are kept as object tree to allow unified modification and
  23. * value access model using lodash's get, set, unset, etc methods.
  24. *
  25. * When no style processor rules are defined the it acts as simple key-value storage.
  26. *
  27. * @private
  28. * @type {Object}
  29. */
  30. this._styles = {};
  31. /**
  32. * An instance of the {@link module:engine/view/stylesmap~StylesProcessor}.
  33. *
  34. * @private
  35. * @member {module:engine/view/stylesmap~StylesProcessor}
  36. */
  37. this._styleProcessor = styleProcessor;
  38. }
  39. /**
  40. * Returns true if style map has no styles set.
  41. *
  42. * @returns {Boolean}
  43. */
  44. get isEmpty() {
  45. const entries = Object.entries( this._styles );
  46. const from = Array.from( entries );
  47. return !from.length;
  48. }
  49. /**
  50. * Number of styles defined.
  51. *
  52. * @type {Number}
  53. */
  54. get size() {
  55. if ( this.isEmpty ) {
  56. return 0;
  57. }
  58. return this.getStyleNames().length;
  59. }
  60. /**
  61. * Set styles map to a new value.
  62. *
  63. * styles.setTo( 'border:1px solid blue;margin-top:1px;' );
  64. *
  65. * @param {String} inlineStyle
  66. */
  67. setTo( inlineStyle ) {
  68. this.clear();
  69. const parsedStyles = Array.from( parseInlineStyles( inlineStyle ).entries() );
  70. for ( const [ key, value ] of parsedStyles ) {
  71. this._styleProcessor.toNormalizedForm( key, value, this._styles );
  72. }
  73. }
  74. /**
  75. * Checks if a given style is set.
  76. *
  77. * styles.setTo( 'margin-left:1px;' );
  78. *
  79. * styles.has( 'margin-left' ); // -> true
  80. * styles.has( 'padding' ); // -> false
  81. *
  82. * **Note**: This check supports normalized style names.
  83. *
  84. * // Enable 'margin' shorthand processing:
  85. * editor.data.addStyleProcessorRules( addMarginRules );
  86. *
  87. * styles.setTo( 'margin:2px;' );
  88. *
  89. * styles.has( 'margin' ); // -> true
  90. * styles.has( 'margin-top' ); // -> true
  91. * styles.has( 'margin-left' ); // -> true
  92. *
  93. * styles.remove( 'margin-top' );
  94. *
  95. * styles.has( 'margin' ); // -> false
  96. * styles.has( 'margin-top' ); // -> false
  97. * styles.has( 'margin-left' ); // -> true
  98. *
  99. * @param {String} name Style name.
  100. * @returns {Boolean}
  101. */
  102. has( name ) {
  103. if ( this.isEmpty ) {
  104. return false;
  105. }
  106. const styles = this._styleProcessor.getReducedForm( name, this._styles );
  107. const propertyDescriptor = styles.find( ( [ property ] ) => property === name );
  108. // Only return a value if it is set;
  109. return Array.isArray( propertyDescriptor );
  110. }
  111. /**
  112. * Sets a given style.
  113. *
  114. * Can insert one by one:
  115. *
  116. * styles.set( 'color', 'blue' );
  117. * styles.set( 'margin-right', '1em' );
  118. *
  119. * or many styles at once:
  120. *
  121. * styles.set( {
  122. * color: 'blue',
  123. * 'margin-right': '1em'
  124. * } );
  125. *
  126. * ***Note**:* This method uses {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules
  127. * enabled style processor rules} to normalize passed values.
  128. *
  129. * // Enable 'margin' shorthand processing:
  130. * editor.data.addStyleProcessorRules( addMarginRules );
  131. *
  132. * styles.set( 'margin', '2px' );
  133. *
  134. * The above code will set margin to:
  135. *
  136. * styles.getNormalized( 'margin' );
  137. * // -> { top: '2px', right: '2px', bottom: '2px', left: '2px' }
  138. *
  139. * Which makes it possible to retrieve a "sub-value":
  140. *
  141. * styles.get( 'margin-left' ); // -> '2px'
  142. *
  143. * Or modify it:
  144. *
  145. * styles.remove( 'margin-left' );
  146. *
  147. * styles.getNormalized( 'margin' ); // -> { top: '1px', bottom: '1px', right: '1px' }
  148. * styles.toString(); // -> 'margin-bottom:1px;margin-right:1px;margin-top:1px;'
  149. *
  150. * This method also allows to set normalized values directly (if a particular styles processor rule was enabled):
  151. *
  152. * styles.set( 'border-color', { top: 'blue' } );
  153. * styles.set( 'margin', { right: '2em' } );
  154. *
  155. * styles.toString(); // -> 'border-color-top:blue;margin-right:2em;'
  156. *
  157. * @param {String|Object} nameOrObject Style property name or object with multiple properties.
  158. * @param {String|Object} valueOrObject Value to set.
  159. */
  160. set( nameOrObject, valueOrObject ) {
  161. if ( isObject( nameOrObject ) ) {
  162. for ( const [ key, value ] of Object.entries( nameOrObject ) ) {
  163. this._styleProcessor.toNormalizedForm( key, value, this._styles );
  164. }
  165. } else {
  166. this._styleProcessor.toNormalizedForm( nameOrObject, valueOrObject, this._styles );
  167. }
  168. }
  169. /**
  170. * Removes given style.
  171. *
  172. * styles.setTo( 'background:#f00;margin-right:2px;' );
  173. *
  174. * styles.remove( 'background' );
  175. *
  176. * styles.toString(); // -> 'margin-right:2px;'
  177. *
  178. * ***Note**:* This method uses {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules
  179. * enabled style processor rules} to normalize passed values.
  180. *
  181. * // Enable 'margin' shorthand processing:
  182. * editor.data.addStyleProcessorRules( addMarginRules );
  183. *
  184. * styles.setTo( 'margin:1px' );
  185. *
  186. * styles.remove( 'margin-top' );
  187. * styles.remove( 'margin-right' );
  188. *
  189. * styles.toString(); // -> 'margin-bottom:1px;margin-left:1px;'
  190. *
  191. * @param {String} name Style name.
  192. */
  193. remove( name ) {
  194. const path = toPath( name );
  195. unset( this._styles, path );
  196. delete this._styles[ name ];
  197. this._cleanEmptyObjectsOnPath( path );
  198. }
  199. /**
  200. * Returns a normalized style object or a single value.
  201. *
  202. * // Enable 'margin' shorthand processing:
  203. * editor.data.addStyleProcessorRules( addMarginRules );
  204. *
  205. * const styles = new Styles();
  206. * styles.setTo( 'margin:1px 2px 3em;' );
  207. *
  208. * styles.getNormalized( 'margin' );
  209. * // will log:
  210. * // {
  211. * // top: '1px',
  212. * // right: '2px',
  213. * // bottom: '3em',
  214. * // left: '2px' // normalized value from margin shorthand
  215. * // }
  216. *
  217. * styles.getNormalized( 'margin-left' ); // -> '2px'
  218. *
  219. * **Note**: This method will only return normalized styles if a style processor was defined.
  220. *
  221. * @param {String} name Style name.
  222. * @returns {Object|String|undefined}
  223. */
  224. getNormalized( name ) {
  225. return this._styleProcessor.getNormalized( name, this._styles );
  226. }
  227. /**
  228. * Returns a normalized style string. Styles are sorted by name.
  229. *
  230. * styles.set( 'margin' , '1px' );
  231. * styles.set( 'background', '#f00' );
  232. *
  233. * styles.toString(); // -> 'background:#f00;margin:1px;'
  234. *
  235. * **Note**: This method supports normalized styles if defined.
  236. *
  237. * // Enable 'margin' shorthand processing:
  238. * editor.data.addStyleProcessorRules( addMarginRules );
  239. *
  240. * styles.set( 'margin' , '1px' );
  241. * styles.set( 'background', '#f00' );
  242. * styles.remove( 'margin-top' );
  243. * styles.remove( 'margin-right' );
  244. *
  245. * styles.toString(); // -> 'background:#f00;margin-bottom:1px;margin-left:1px;'
  246. *
  247. * @returns {String}
  248. */
  249. toString() {
  250. if ( this.isEmpty ) {
  251. return '';
  252. }
  253. return this._getStylesEntries()
  254. .map( arr => arr.join( ':' ) )
  255. .sort()
  256. .join( ';' ) + ';';
  257. }
  258. /**
  259. * Returns property as a value string or undefined if property is not set.
  260. *
  261. * // Enable 'margin' shorthand processing:
  262. * editor.data.addStyleProcessorRules( addMarginRules );
  263. *
  264. * const styles = new Styles();
  265. * styles.setTo( 'margin:1px;' );
  266. * styles.set( 'margin-bottom', '3em' );
  267. *
  268. * styles.getAsString( 'margin' ); // -> 'margin: 1px 1px 3em;'
  269. *
  270. * Note, however, that all sub-values must be set for the longhand property name to return a value:
  271. *
  272. * const styles = new Styles();
  273. * styles.setTo( 'margin:1px;' );
  274. * styles.remove( 'margin-bottom' );
  275. *
  276. * styles.getAsString( 'margin' ); // -> undefined
  277. *
  278. * In the above scenario, it is not possible to return a `margin` value, so `undefined` is returned.
  279. * Instead, you should use:
  280. *
  281. * const styles = new Styles();
  282. * styles.setTo( 'margin:1px;' );
  283. * styles.remove( 'margin-bottom' );
  284. *
  285. * for ( const styleName of styles.getStyleNames() ) {
  286. * console.log( styleName, styles.getAsString( styleName ) );
  287. * }
  288. * // 'margin-top', '1px'
  289. * // 'margin-right', '1px'
  290. * // 'margin-left', '1px'
  291. *
  292. * In general, it is recommend to iterate over style names like in the example above. This way, you will always get all
  293. * the currently set style values. So, if all the 4 margin values would be set
  294. * the for-of loop above would yield only `'margin'`, `'1px'`:
  295. *
  296. * const styles = new Styles();
  297. * styles.setTo( 'margin:1px;' );
  298. *
  299. * for ( const styleName of styles.getStyleNames() ) {
  300. * console.log( styleName, styles.getAsString( styleName ) );
  301. * }
  302. * // 'margin', '1px'
  303. *
  304. * **Note**: To get a normalized version of a longhand property use the {@link #getNormalized `#getNormalized()`} method.
  305. *
  306. * @param {String} propertyName
  307. * @returns {String|undefined}
  308. */
  309. getAsString( propertyName ) {
  310. if ( this.isEmpty ) {
  311. return;
  312. }
  313. if ( this._styles[ propertyName ] && !isObject( this._styles[ propertyName ] ) ) {
  314. // Try return styles set directly - values that are not parsed.
  315. return this._styles[ propertyName ];
  316. }
  317. const styles = this._styleProcessor.getReducedForm( propertyName, this._styles );
  318. const propertyDescriptor = styles.find( ( [ property ] ) => property === propertyName );
  319. // Only return a value if it is set;
  320. if ( Array.isArray( propertyDescriptor ) ) {
  321. return propertyDescriptor[ 1 ];
  322. }
  323. }
  324. /**
  325. * Returns style property names as they would appear when using {@link #toString `#toString()`}.
  326. *
  327. * @returns {Array.<String>}
  328. */
  329. getStyleNames() {
  330. if ( this.isEmpty ) {
  331. return [];
  332. }
  333. const entries = this._getStylesEntries();
  334. return entries.map( ( [ key ] ) => key );
  335. }
  336. /**
  337. * Removes all styles.
  338. */
  339. clear() {
  340. this._styles = {};
  341. }
  342. /**
  343. * Returns normalized styles entries for further processing.
  344. *
  345. * @private
  346. * @returns {Array.<module:engine/view/stylesmap~PropertyDescriptor>}
  347. */
  348. _getStylesEntries() {
  349. const parsed = [];
  350. const keys = Object.keys( this._styles );
  351. for ( const key of keys ) {
  352. parsed.push( ...this._styleProcessor.getReducedForm( key, this._styles ) );
  353. }
  354. return parsed;
  355. }
  356. /**
  357. * Removes empty objects upon removing an entry from internal object.
  358. *
  359. * @param {String} path
  360. * @private
  361. */
  362. _cleanEmptyObjectsOnPath( path ) {
  363. const pathParts = path.split( '.' );
  364. const isChildPath = pathParts.length > 1;
  365. if ( !isChildPath ) {
  366. return;
  367. }
  368. const parentPath = pathParts.splice( 0, pathParts.length - 1 ).join( '.' );
  369. const parentObject = get( this._styles, parentPath );
  370. if ( !parentObject ) {
  371. return;
  372. }
  373. const isParentEmpty = !Array.from( Object.keys( parentObject ) ).length;
  374. if ( isParentEmpty ) {
  375. this.remove( parentPath );
  376. }
  377. }
  378. }
  379. /**
  380. * Style processor is responsible for writing and reading a normalized styles object.
  381. */
  382. export class StylesProcessor {
  383. /**
  384. * Creates StylesProcessor instance.
  385. *
  386. * @private
  387. */
  388. constructor() {
  389. this._normalizers = new Map();
  390. this._extractors = new Map();
  391. this._reducers = new Map();
  392. this._consumables = new Map();
  393. }
  394. /**
  395. * Parse style string value to a normalized object and appends it to styles object.
  396. *
  397. * const styles = {};
  398. *
  399. * stylesProcessor.toNormalizedForm( 'margin', '1px', styles );
  400. *
  401. * // styles will consist: { margin: { top: '1px', right: '1px', bottom: '1px', left: '1px; } }
  402. *
  403. * **Note**: To define normalizer callbacks use {@link #setNormalizer}.
  404. *
  405. * @param {String} name Name of style property.
  406. * @param {String} propertyValue Value of style property.
  407. * @param {Object} styles Object holding normalized styles.
  408. */
  409. toNormalizedForm( name, propertyValue, styles ) {
  410. if ( isObject( propertyValue ) ) {
  411. appendStyleValue( styles, toPath( name ), propertyValue );
  412. return;
  413. }
  414. if ( this._normalizers.has( name ) ) {
  415. const normalizer = this._normalizers.get( name );
  416. const { path, value } = normalizer( propertyValue );
  417. appendStyleValue( styles, path, value );
  418. } else {
  419. appendStyleValue( styles, name, propertyValue );
  420. }
  421. }
  422. /**
  423. * Returns a normalized version of a style property.
  424. * const styles = {
  425. * margin: { top: '1px', right: '1px', bottom: '1px', left: '1px; },
  426. * background: { color: '#f00' }
  427. * };
  428. *
  429. * stylesProcessor.getNormalized( 'background' );
  430. * // will return: { color: '#f00' }
  431. *
  432. * stylesProcessor.getNormalized( 'margin-top' );
  433. * // will return: '1px'
  434. *
  435. * **Note**: In some cases extracting single value requires defining an extractor callback {@link #setExtractor}.
  436. *
  437. * @param {String} name Name of style property.
  438. * @param {Object} styles Object holding normalized styles.
  439. * @returns {*}
  440. */
  441. getNormalized( name, styles ) {
  442. if ( !name ) {
  443. return merge( {}, styles );
  444. }
  445. // Might be empty string.
  446. if ( styles[ name ] !== undefined ) {
  447. return styles[ name ];
  448. }
  449. if ( this._extractors.has( name ) ) {
  450. const extractor = this._extractors.get( name );
  451. if ( typeof extractor === 'string' ) {
  452. return get( styles, extractor );
  453. }
  454. const value = extractor( name, styles );
  455. if ( value ) {
  456. return value;
  457. }
  458. }
  459. return get( styles, toPath( name ) );
  460. }
  461. /**
  462. * Returns a reduced form of style property form normalized object.
  463. *
  464. * For default margin reducer, the below code:
  465. *
  466. * stylesProcessor.getReducedForm( 'margin', {
  467. * margin: { top: '1px', right: '1px', bottom: '2px', left: '1px; }
  468. * } );
  469. *
  470. * will return:
  471. *
  472. * [
  473. * [ 'margin', '1px 1px 2px' ]
  474. * ]
  475. *
  476. * because it might be represented as a shorthand 'margin' value. However if one of margin long hand values is missing it should return:
  477. *
  478. * [
  479. * [ 'margin-top', '1px' ],
  480. * [ 'margin-right', '1px' ],
  481. * [ 'margin-bottom', '2px' ]
  482. * // the 'left' value is missing - cannot use 'margin' shorthand.
  483. * ]
  484. *
  485. * **Note**: To define reducer callbacks use {@link #setReducer}.
  486. *
  487. * @param {String} name
  488. * @param {String} name Name of style property.
  489. * @param {Object} styles Object holding normalized styles.
  490. * @returns {Array.<module:engine/view/stylesmap~PropertyDescriptor>}
  491. */
  492. getReducedForm( name, styles ) {
  493. const normalizedValue = this.getNormalized( name, styles );
  494. // Might be empty string.
  495. if ( normalizedValue === undefined ) {
  496. return [];
  497. }
  498. if ( this._reducers.has( name ) ) {
  499. const reducer = this._reducers.get( name );
  500. return reducer( normalizedValue );
  501. }
  502. return [ [ name, normalizedValue ] ];
  503. }
  504. /**
  505. * Returns related style names.
  506. *
  507. * stylesProcessor.getRelatedStyles( 'margin' );
  508. * // will return: [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ];
  509. *
  510. * stylesProcessor.getRelatedStyles( 'margin-top' );
  511. * // will return: [ 'margin' ];
  512. *
  513. * **Note**: To define new style relations load an existing style processor or use
  514. * {@link module:engine/view/stylesmap~StylesProcessor#setStyleRelation `StylesProcessor.setStyleRelation()`}.
  515. *
  516. * @param {String} name
  517. * @returns {Array.<String>}
  518. */
  519. getRelatedStyles( name ) {
  520. return this._consumables.get( name ) || [];
  521. }
  522. /**
  523. * Adds a normalizer method for a style property.
  524. *
  525. * A normalizer returns describing how the value should be normalized.
  526. *
  527. * For instance 'margin' style is a shorthand for four margin values:
  528. *
  529. * - 'margin-top'
  530. * - 'margin-right'
  531. * - 'margin-bottom'
  532. * - 'margin-left'
  533. *
  534. * and can be written in various ways if some values are equal to others. For instance `'margin: 1px 2em;'` is a shorthand for
  535. * `'margin-top: 1px;margin-right: 2em;margin-bottom: 1px;margin-left: 2em'`.
  536. *
  537. * A normalizer should parse various margin notations as a single object:
  538. *
  539. * const styles = {
  540. * margin: {
  541. * top: '1px',
  542. * right: '2em',
  543. * bottom: '1px',
  544. * left: '2em'
  545. * }
  546. * };
  547. *
  548. * Thus a normalizer for 'margin' style should return an object defining style path and value to store:
  549. *
  550. * const returnValue = {
  551. * path: 'margin',
  552. * value: {
  553. * top: '1px',
  554. * right: '2em',
  555. * bottom: '1px',
  556. * left: '2em'
  557. * }
  558. * };
  559. *
  560. * Additionally to fully support all margin notations there should be also defined 4 normalizers for longhand margin notations. Below
  561. * is an example for 'margin-top' style property normalizer:
  562. *
  563. * stylesProcessor.setNormalizer( 'margin-top', valueString => {
  564. * return {
  565. * path: 'margin.top',
  566. * value: valueString
  567. * }
  568. * } );
  569. *
  570. * @param {String} name
  571. * @param {Function} callback
  572. */
  573. setNormalizer( name, callback ) {
  574. this._normalizers.set( name, callback );
  575. }
  576. /**
  577. * Adds a extractor callback for a style property.
  578. *
  579. * Most normalized style values are stored as one level objects. It is assumed that `'margin-top'` style will be stored as:
  580. *
  581. * const styles = {
  582. * margin: {
  583. * top: 'value'
  584. * }
  585. * }
  586. *
  587. * However, some styles can have conflicting notations and thus it might be harder to extract a style value from shorthand. For instance
  588. * the 'border-top-style' can be defined using `'border-top:solid'`, `'border-style:solid none none none'` or by `'border:solid'`
  589. * shorthands. The default border styles processors stores styles as:
  590. *
  591. * const styles = {
  592. * border: {
  593. * style: {
  594. * top: 'solid'
  595. * }
  596. * }
  597. * }
  598. *
  599. * as it is better to modify border style independently from other values. On the other part the output of the border might be
  600. * desired as `border-top`, `border-left`, etc notation.
  601. *
  602. * In the above example a reducer should return a side border value that combines style, color and width:
  603. *
  604. * styleProcessor.setExtractor( 'border-top', styles => {
  605. * return {
  606. * color: styles.border.color.top,
  607. * style: styles.border.style.top,
  608. * width: styles.border.width.top
  609. * }
  610. * } );
  611. *
  612. * @param {String} name
  613. * @param {Function|String} callbackOrPath Callback that return a requested value or path string for single values.
  614. */
  615. setExtractor( name, callbackOrPath ) {
  616. this._extractors.set( name, callbackOrPath );
  617. }
  618. /**
  619. * Adds a reducer callback for a style property.
  620. *
  621. * Reducer returns a minimal notation for given style name. For longhand properties it is not required to write a reducer as
  622. * by default the direct value from style path is taken.
  623. *
  624. * For shorthand styles a reducer should return minimal style notation either by returning single name-value tuple or multiple tuples
  625. * if a shorthand cannot be used. For instance for a margin shorthand a reducer might return:
  626. *
  627. * const marginShortHandTuple = [
  628. * [ 'margin', '1px 1px 2px' ]
  629. * ];
  630. *
  631. * or a longhand tuples for defined values:
  632. *
  633. * // Considering margin.bottom and margin.left are undefined.
  634. * const marginLonghandsTuples = [
  635. * [ 'margin-top', '1px' ],
  636. * [ 'margin-right', '1px' ]
  637. * ];
  638. *
  639. * A reducer obtains a normalized style value:
  640. *
  641. * // Simplified reducer that always outputs 4 values which are always present:
  642. * stylesProcessor.setReducer( 'margin', margin => {
  643. * return [
  644. * [ 'margin', `${ margin.top } ${ margin.right } ${ margin.bottom } ${ margin.left }` ]
  645. * ]
  646. * } );
  647. *
  648. * @param {String} name
  649. * @param {Function} callback
  650. */
  651. setReducer( name, callback ) {
  652. this._reducers.set( name, callback );
  653. }
  654. /**
  655. * Defines a style shorthand relation to other style notations.
  656. *
  657. * stylesProcessor.setStyleRelation( 'margin', [
  658. * 'margin-top',
  659. * 'margin-right',
  660. * 'margin-bottom',
  661. * 'margin-left'
  662. * ] );
  663. *
  664. * This enables expanding of style names for shorthands. For instance, if defined,
  665. * {@link module:engine/conversion/viewconsumable~ViewConsumable view consumable} items are automatically created
  666. * for long-hand margin style notation alongside the `'margin'` item.
  667. *
  668. * This means that when an element being converted has a style `margin`, a converter for `margin-left` will work just
  669. * fine since the view consumable will contain a consumable `margin-left` item (thanks to the relation) and
  670. * `element.getStyle( 'margin-left' )` will work as well assuming that the style processor was correctly configured.
  671. * However, once `margin-left` is consumed, `margin` will not be consumable anymore.
  672. *
  673. * @param {String} shorthandName
  674. * @param {Array.<String>} styleNames
  675. */
  676. setStyleRelation( shorthandName, styleNames ) {
  677. this._mapStyleNames( shorthandName, styleNames );
  678. for ( const alsoName of styleNames ) {
  679. this._mapStyleNames( alsoName, [ shorthandName ] );
  680. }
  681. }
  682. /**
  683. * Set two-way binding of style names.
  684. *
  685. * @param {String} name
  686. * @param {Array.<String>} styleNames
  687. * @private
  688. */
  689. _mapStyleNames( name, styleNames ) {
  690. if ( !this._consumables.has( name ) ) {
  691. this._consumables.set( name, [] );
  692. }
  693. this._consumables.get( name ).push( ...styleNames );
  694. }
  695. }
  696. // Parses inline styles and puts property - value pairs into styles map.
  697. //
  698. // @param {String} stylesString Styles to parse.
  699. // @returns {Map.<String, String>} stylesMap Map of parsed properties and values.
  700. function parseInlineStyles( stylesString ) {
  701. // `null` if no quote was found in input string or last found quote was a closing quote. See below.
  702. let quoteType = null;
  703. let propertyNameStart = 0;
  704. let propertyValueStart = 0;
  705. let propertyName = null;
  706. const stylesMap = new Map();
  707. // Do not set anything if input string is empty.
  708. if ( stylesString === '' ) {
  709. return stylesMap;
  710. }
  711. // Fix inline styles that do not end with `;` so they are compatible with algorithm below.
  712. if ( stylesString.charAt( stylesString.length - 1 ) != ';' ) {
  713. stylesString = stylesString + ';';
  714. }
  715. // Seek the whole string for "special characters".
  716. for ( let i = 0; i < stylesString.length; i++ ) {
  717. const char = stylesString.charAt( i );
  718. if ( quoteType === null ) {
  719. // No quote found yet or last found quote was a closing quote.
  720. switch ( char ) {
  721. case ':':
  722. // Most of time colon means that property name just ended.
  723. // Sometimes however `:` is found inside property value (for example in background image url).
  724. if ( !propertyName ) {
  725. // Treat this as end of property only if property name is not already saved.
  726. // Save property name.
  727. propertyName = stylesString.substr( propertyNameStart, i - propertyNameStart );
  728. // Save this point as the start of property value.
  729. propertyValueStart = i + 1;
  730. }
  731. break;
  732. case '"':
  733. case '\'':
  734. // Opening quote found (this is an opening quote, because `quoteType` is `null`).
  735. quoteType = char;
  736. break;
  737. case ';': {
  738. // Property value just ended.
  739. // Use previously stored property value start to obtain property value.
  740. const propertyValue = stylesString.substr( propertyValueStart, i - propertyValueStart );
  741. if ( propertyName ) {
  742. // Save parsed part.
  743. stylesMap.set( propertyName.trim(), propertyValue.trim() );
  744. }
  745. propertyName = null;
  746. // Save this point as property name start. Property name starts immediately after previous property value ends.
  747. propertyNameStart = i + 1;
  748. break;
  749. }
  750. }
  751. } else if ( char === quoteType ) {
  752. // If a quote char is found and it is a closing quote, mark this fact by `null`-ing `quoteType`.
  753. quoteType = null;
  754. }
  755. }
  756. return stylesMap;
  757. }
  758. // Return lodash compatible path from style name.
  759. function toPath( name ) {
  760. return name.replace( '-', '.' );
  761. }
  762. // Appends style definition to the styles object.
  763. //
  764. // @param {String} nameOrPath
  765. // @param {String|Object} valueOrObject
  766. // @private
  767. function appendStyleValue( stylesObject, nameOrPath, valueOrObject ) {
  768. let valueToSet = valueOrObject;
  769. if ( isObject( valueOrObject ) ) {
  770. valueToSet = merge( {}, get( stylesObject, nameOrPath ), valueOrObject );
  771. }
  772. set( stylesObject, nameOrPath, valueToSet );
  773. }
  774. /**
  775. * A CSS style property descriptor that contains tuplet of two strings:
  776. *
  777. * - first string describes property name
  778. * - second string describes property value
  779. *
  780. * const marginDescriptor = [ 'margin', '2px 3em' ];
  781. * const marginTopDescriptor = [ 'margin-top', '2px' ];
  782. *
  783. * @typedef {Array.<String, String>} module:engine/view/stylesmap~PropertyDescriptor
  784. */
  785. /**
  786. * An object describing values associated with the sides of a box, for instance margins, paddings,
  787. * border widths, border colors, etc.
  788. *
  789. * const margin = {
  790. * top: '1px',
  791. * right: '3px',
  792. * bottom: '3px',
  793. * left: '7px'
  794. * };
  795. *
  796. * const borderColor = {
  797. * top: 'red',
  798. * right: 'blue',
  799. * bottom: 'blue',
  800. * left: 'red'
  801. * };
  802. *
  803. * @typedef {Object} module:engine/view/stylesmap~BoxSides
  804. *
  805. * @property {String} top Top side value.
  806. * @property {String} right Right side value.
  807. * @property {String} bottom Bottom side value.
  808. * @property {String} left Left side value.
  809. */