stylesmap.js 25 KB

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