@ckeditor/ckeditor5-engine
Version:
The editing engine of CKEditor 5 – the best browser-based rich text editor.
942 lines (941 loc) • 32.1 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module engine/view/stylesmap
*/
import { get, isObject, merge, set } from 'es-toolkit/compat';
import { toArray } from '@ckeditor/ckeditor5-utils';
import { isPatternMatched } from './matcher.js';
/**
* Styles map. Allows handling (adding, removing, retrieving) a set of style rules (usually, of an element).
*/
export class StylesMap {
/**
* Keeps an internal representation of styles map. Normalized styles are kept as object tree to allow unified modification and
* value access model using lodash's get, set, unset, etc methods.
*
* When no style processor rules are defined it acts as simple key-value storage.
*/
_styles;
/**
* Cached list of style names for faster access.
*/
_cachedStyleNames = null;
/**
* Cached list of expanded style names for faster access.
*/
_cachedExpandedStyleNames = null;
/**
* An instance of the {@link module:engine/view/stylesmap~StylesProcessor}.
*/
_styleProcessor;
/**
* Creates Styles instance.
*/
constructor(styleProcessor) {
this._styles = {};
this._styleProcessor = styleProcessor;
}
/**
* Returns true if style map has no styles set.
*/
get isEmpty() {
const entries = Object.entries(this._styles);
return !entries.length;
}
/**
* Number of styles defined.
*/
get size() {
if (this.isEmpty) {
return 0;
}
return this.getStyleNames().length;
}
/**
* Set styles map to a new value.
*
* ```ts
* styles.setTo( 'border:1px solid blue;margin-top:1px;' );
* ```
*/
setTo(inlineStyle) {
this.clear();
const parsedStyles = parseInlineStyles(inlineStyle);
for (const [key, value] of parsedStyles) {
this._styleProcessor.toNormalizedForm(key, value, this._styles);
}
return this;
}
/**
* Checks if a given style is set.
*
* ```ts
* styles.setTo( 'margin-left:1px;' );
*
* styles.has( 'margin-left' ); // -> true
* styles.has( 'padding' ); // -> false
* ```
*
* **Note**: This check supports normalized style names.
*
* ```ts
* // Enable 'margin' shorthand processing:
* editor.data.addStyleProcessorRules( addMarginStylesRules );
*
* styles.setTo( 'margin:2px;' );
*
* styles.has( 'margin' ); // -> true
* styles.has( 'margin-top' ); // -> true
* styles.has( 'margin-left' ); // -> true
*
* styles.remove( 'margin-top' );
*
* styles.has( 'margin' ); // -> false
* styles.has( 'margin-top' ); // -> false
* styles.has( 'margin-left' ); // -> true
* ```
*
* @param name Style name.
*/
has(name) {
if (this.isEmpty) {
return false;
}
const styles = this._styleProcessor.getReducedForm(name, this._styles);
const propertyDescriptor = styles.find(([property]) => property === name);
// Only return a value if it is set;
return Array.isArray(propertyDescriptor);
}
set(nameOrObject, valueOrObject) {
this._cachedStyleNames = null;
this._cachedExpandedStyleNames = null;
if (isObject(nameOrObject)) {
for (const [key, value] of Object.entries(nameOrObject)) {
this._styleProcessor.toNormalizedForm(key, value, this._styles);
}
}
else {
this._styleProcessor.toNormalizedForm(nameOrObject, valueOrObject, this._styles);
}
}
/**
* Removes given style.
*
* ```ts
* styles.setTo( 'background:#f00;margin-right:2px;' );
*
* styles.remove( 'background' );
*
* styles.toString(); // -> 'margin-right:2px;'
* ```
*
* ***Note**:* This method uses {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules
* enabled style processor rules} to normalize passed values.
*
* ```ts
* // Enable 'margin' shorthand processing:
* editor.data.addStyleProcessorRules( addMarginStylesRules );
*
* styles.setTo( 'margin:1px' );
*
* styles.remove( 'margin-top' );
* styles.remove( 'margin-right' );
*
* styles.toString(); // -> 'margin-bottom:1px;margin-left:1px;'
* ```
*
* @param names Style name or an array of names.
*/
remove(names) {
const normalizedStylesToRemove = {};
for (const name of toArray(names)) {
// First, try the easy path, when the path reflects normalized styles structure.
const path = toPath(name);
const pathValue = get(this._styles, path);
if (pathValue) {
appendStyleValue(normalizedStylesToRemove, path, pathValue);
}
else {
// Easy path did not work, so try to get the value from the styles map.
const value = this.getAsString(name);
if (value !== undefined) {
this._styleProcessor.toNormalizedForm(name, value, normalizedStylesToRemove);
}
}
}
if (Object.keys(normalizedStylesToRemove).length) {
removeStyles(this._styles, normalizedStylesToRemove);
this._cachedStyleNames = null;
this._cachedExpandedStyleNames = null;
}
}
/**
* Returns a normalized style object or a single value.
*
* ```ts
* // Enable 'margin' shorthand processing:
* editor.data.addStyleProcessorRules( addMarginStylesRules );
*
* const styles = new Styles();
* styles.setTo( 'margin:1px 2px 3em;' );
*
* styles.getNormalized( 'margin' );
* // will log:
* // {
* // top: '1px',
* // right: '2px',
* // bottom: '3em',
* // left: '2px' // normalized value from margin shorthand
* // }
*
* styles.getNormalized( 'margin-left' ); // -> '2px'
* ```
*
* **Note**: This method will only return normalized styles if a style processor was defined.
*
* @param name Style name.
*/
getNormalized(name) {
return this._styleProcessor.getNormalized(name, this._styles);
}
/**
* Returns a normalized style string. Styles are sorted by name.
*
* ```ts
* styles.set( 'margin' , '1px' );
* styles.set( 'background', '#f00' );
*
* styles.toString(); // -> 'background:#f00;margin:1px;'
* ```
*
* **Note**: This method supports normalized styles if defined.
*
* ```ts
* // Enable 'margin' shorthand processing:
* editor.data.addStyleProcessorRules( addMarginStylesRules );
*
* styles.set( 'margin' , '1px' );
* styles.set( 'background', '#f00' );
* styles.remove( 'margin-top' );
* styles.remove( 'margin-right' );
*
* styles.toString(); // -> 'background:#f00;margin-bottom:1px;margin-left:1px;'
* ```
*/
toString() {
if (this.isEmpty) {
return '';
}
return this.getStylesEntries()
.map(arr => arr.join(':'))
.sort()
.join(';') + ';';
}
/**
* Returns property as a value string or undefined if property is not set.
*
* ```ts
* // Enable 'margin' shorthand processing:
* editor.data.addStyleProcessorRules( addMarginStylesRules );
*
* const styles = new Styles();
* styles.setTo( 'margin:1px;' );
* styles.set( 'margin-bottom', '3em' );
*
* styles.getAsString( 'margin' ); // -> 'margin: 1px 1px 3em;'
* ```
*
* Note, however, that all sub-values must be set for the longhand property name to return a value:
*
* ```ts
* const styles = new Styles();
* styles.setTo( 'margin:1px;' );
* styles.remove( 'margin-bottom' );
*
* styles.getAsString( 'margin' ); // -> undefined
* ```
*
* In the above scenario, it is not possible to return a `margin` value, so `undefined` is returned.
* Instead, you should use:
*
* ```ts
* const styles = new Styles();
* styles.setTo( 'margin:1px;' );
* styles.remove( 'margin-bottom' );
*
* for ( const styleName of styles.getStyleNames() ) {
* console.log( styleName, styles.getAsString( styleName ) );
* }
* // 'margin-top', '1px'
* // 'margin-right', '1px'
* // 'margin-left', '1px'
* ```
*
* In general, it is recommend to iterate over style names like in the example above. This way, you will always get all
* the currently set style values. So, if all the 4 margin values would be set
* the for-of loop above would yield only `'margin'`, `'1px'`:
*
* ```ts
* const styles = new Styles();
* styles.setTo( 'margin:1px;' );
*
* for ( const styleName of styles.getStyleNames() ) {
* console.log( styleName, styles.getAsString( styleName ) );
* }
* // 'margin', '1px'
* ```
*
* **Note**: To get a normalized version of a longhand property use the {@link #getNormalized `#getNormalized()`} method.
*/
getAsString(propertyName) {
if (this.isEmpty) {
return;
}
if (this._styles[propertyName] && !isObject(this._styles[propertyName])) {
// Try return styles set directly - values that are not parsed.
return this._styles[propertyName];
}
const styles = this._styleProcessor.getReducedForm(propertyName, this._styles);
const propertyDescriptor = styles.find(([property]) => property === propertyName);
// Only return a value if it is set;
if (Array.isArray(propertyDescriptor)) {
return propertyDescriptor[1];
}
}
/**
* Returns all style properties names as they would appear when using {@link #toString `#toString()`}.
*
* When `expand` is set to true and there's a shorthand style property set, it will also return all equivalent styles:
*
* ```ts
* stylesMap.setTo( 'margin: 1em' )
* ```
*
* will be expanded to:
*
* ```ts
* [ 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ]
* ```
*
* @param expand Expand shorthand style properties and all return equivalent style representations.
*/
getStyleNames(expand = false) {
if (this.isEmpty) {
return [];
}
if (expand) {
this._cachedExpandedStyleNames ||= this._styleProcessor.getStyleNames(this._styles);
return this._cachedExpandedStyleNames;
}
this._cachedStyleNames ||= this.getStylesEntries().map(([key]) => key);
return this._cachedStyleNames;
}
/**
* Alias for {@link #getStyleNames}.
*/
keys() {
return this.getStyleNames();
}
/**
* Removes all styles.
*/
clear() {
this._styles = {};
this._cachedStyleNames = null;
this._cachedExpandedStyleNames = null;
}
/**
* Returns `true` if both attributes have the same styles.
*/
isSimilar(other) {
if (this.size !== other.size) {
return false;
}
for (const property of this.getStyleNames()) {
if (!other.has(property) || other.getAsString(property) !== this.getAsString(property)) {
return false;
}
}
return true;
}
/**
* Returns normalized styles entries for further processing.
*/
getStylesEntries() {
const parsed = [];
const keys = Object.keys(this._styles);
for (const key of keys) {
parsed.push(...this._styleProcessor.getReducedForm(key, this._styles));
}
return parsed;
}
/**
* Clones the attribute value.
*
* @internal
*/
_clone() {
const clone = new this.constructor(this._styleProcessor);
clone.set(this.getNormalized());
return clone;
}
/**
* Used by the {@link module:engine/view/matcher~Matcher Matcher} to collect matching styles.
*
* @internal
* @param tokenPattern The matched style name pattern.
* @param valuePattern The matched style value pattern.
* @returns An array of matching tokens (style names).
*/
_getTokensMatch(tokenPattern, valuePattern) {
const match = [];
for (const styleName of this.getStyleNames(true)) {
if (isPatternMatched(tokenPattern, styleName)) {
if (valuePattern === true) {
match.push(styleName);
continue;
}
// For now, the reducers are not returning the full tree of properties.
// Casting to string preserves the old behavior until the root cause is fixed.
// More can be found in https://github.com/ckeditor/ckeditor5/issues/10399.
const value = this.getAsString(styleName);
if (isPatternMatched(valuePattern, value)) {
match.push(styleName);
}
}
}
return match.length ? match : undefined;
}
/**
* Returns a list of consumables for the attribute. This includes related styles.
*
* Could be filtered by the given style name.
*
* @internal
*/
_getConsumables(name) {
const result = [];
if (name) {
result.push(name);
for (const relatedName of this._styleProcessor.getRelatedStyles(name)) {
result.push(relatedName);
}
}
else {
for (const name of this.getStyleNames()) {
for (const relatedName of this._styleProcessor.getRelatedStyles(name)) {
result.push(relatedName);
}
result.push(name);
}
}
return result;
}
/**
* Used by {@link module:engine/view/element~ViewElement#_canMergeAttributesFrom} to verify if the given attribute can be merged without
* conflicts into the attribute.
*
* This method is indirectly used by the {@link module:engine/view/downcastwriter~ViewDowncastWriter} while down-casting
* an {@link module:engine/view/attributeelement~ViewAttributeElement} to merge it with other ViewAttributeElement.
*
* @internal
*/
_canMergeFrom(other) {
for (const key of other.getStyleNames()) {
if (this.has(key) && this.getAsString(key) !== other.getAsString(key)) {
return false;
}
}
return true;
}
/**
* Used by {@link module:engine/view/element~ViewElement#_mergeAttributesFrom} to merge a given attribute into the attribute.
*
* This method is indirectly used by the {@link module:engine/view/downcastwriter~ViewDowncastWriter} while down-casting
* an {@link module:engine/view/attributeelement~ViewAttributeElement} to merge it with other ViewAttributeElement.
*
* @internal
*/
_mergeFrom(other) {
for (const prop of other.getStyleNames()) {
if (!this.has(prop)) {
this.set(prop, other.getAsString(prop));
}
}
}
/**
* Used by {@link module:engine/view/element~ViewElement#_canSubtractAttributesOf} to verify if the given attribute can be fully
* subtracted from the attribute.
*
* This method is indirectly used by the {@link module:engine/view/downcastwriter~ViewDowncastWriter} while down-casting
* an {@link module:engine/view/attributeelement~ViewAttributeElement} to unwrap the ViewAttributeElement.
*
* @internal
*/
_isMatching(other) {
for (const key of other.getStyleNames()) {
if (!this.has(key) || this.getAsString(key) !== other.getAsString(key)) {
return false;
}
}
return true;
}
}
/**
* Style processor is responsible for writing and reading a normalized styles object.
*/
export class StylesProcessor {
_normalizers;
_extractors;
_reducers;
_consumables;
/**
* Creates StylesProcessor instance.
*
* @internal
*/
constructor() {
this._normalizers = new Map();
this._extractors = new Map();
this._reducers = new Map();
this._consumables = new Map();
}
/**
* Parse style string value to a normalized object and appends it to styles object.
*
* ```ts
* const styles = {};
*
* stylesProcessor.toNormalizedForm( 'margin', '1px', styles );
*
* // styles will consist: { margin: { top: '1px', right: '1px', bottom: '1px', left: '1px; } }
* ```
*
* **Note**: To define normalizer callbacks use {@link #setNormalizer}.
*
* @param name Name of style property.
* @param propertyValue Value of style property.
* @param styles Object holding normalized styles.
*/
toNormalizedForm(name, propertyValue, styles) {
if (isObject(propertyValue)) {
appendStyleValue(styles, toPath(name), propertyValue);
return;
}
if (this._normalizers.has(name)) {
const normalizer = this._normalizers.get(name);
const { path, value } = normalizer(propertyValue);
appendStyleValue(styles, path, value);
}
else {
appendStyleValue(styles, name, propertyValue);
}
}
/**
* Returns a normalized version of a style property.
*
* ```ts
* const styles = {
* margin: { top: '1px', right: '1px', bottom: '1px', left: '1px; },
* background: { color: '#f00' }
* };
*
* stylesProcessor.getNormalized( 'background' );
* // will return: { color: '#f00' }
*
* stylesProcessor.getNormalized( 'margin-top' );
* // will return: '1px'
* ```
*
* **Note**: In some cases extracting single value requires defining an extractor callback {@link #setExtractor}.
*
* @param name Name of style property.
* @param styles Object holding normalized styles.
*/
getNormalized(name, styles) {
if (!name) {
return merge({}, styles);
}
// Might be empty string.
if (styles[name] !== undefined) {
return styles[name];
}
if (this._extractors.has(name)) {
const extractor = this._extractors.get(name);
if (typeof extractor === 'string') {
return get(styles, extractor);
}
const value = extractor(name, styles);
if (value) {
return value;
}
}
return get(styles, toPath(name));
}
/**
* Returns a reduced form of style property form normalized object.
*
* For default margin reducer, the below code:
*
* ```ts
* stylesProcessor.getReducedForm( 'margin', {
* margin: { top: '1px', right: '1px', bottom: '2px', left: '1px; }
* } );
* ```
*
* will return:
*
* ```ts
* [
* [ 'margin', '1px 1px 2px' ]
* ]
* ```
*
* because it might be represented as a shorthand 'margin' value. However if one of margin long hand values is missing it should return:
*
* ```ts
* [
* [ 'margin-top', '1px' ],
* [ 'margin-right', '1px' ],
* [ 'margin-bottom', '2px' ]
* // the 'left' value is missing - cannot use 'margin' shorthand.
* ]
* ```
*
* **Note**: To define reducer callbacks use {@link #setReducer}.
*
* @param name Name of style property.
*/
getReducedForm(name, styles) {
const normalizedValue = this.getNormalized(name, styles);
// Might be empty string.
if (normalizedValue === undefined) {
return [];
}
if (this._reducers.has(name)) {
const reducer = this._reducers.get(name);
return reducer(normalizedValue);
}
return [[name, normalizedValue]];
}
/**
* Return all style properties. Also expand shorthand properties (e.g. `margin`, `background`) if respective extractor is available.
*
* @param styles Object holding normalized styles.
*/
getStyleNames(styles) {
const styleNamesKeysSet = new Set();
// Find all extractable styles that have a value.
for (const name of this._consumables.keys()) {
const style = this.getNormalized(name, styles);
if (style && (typeof style != 'object' || Object.keys(style).length)) {
styleNamesKeysSet.add(name);
}
}
// For simple styles (for example `color`) we don't have a map of those styles
// but they are 1 to 1 with normalized object keys.
for (const name of Object.keys(styles)) {
styleNamesKeysSet.add(name);
}
return Array.from(styleNamesKeysSet);
}
/**
* Returns related style names.
*
* ```ts
* stylesProcessor.getRelatedStyles( 'margin' );
* // will return: [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ];
*
* stylesProcessor.getRelatedStyles( 'margin-top' );
* // will return: [ 'margin' ];
* ```
*
* **Note**: To define new style relations load an existing style processor or use
* {@link module:engine/view/stylesmap~StylesProcessor#setStyleRelation `StylesProcessor.setStyleRelation()`}.
*/
getRelatedStyles(name) {
return this._consumables.get(name) || [];
}
/**
* Adds a normalizer method for a style property.
*
* A normalizer returns describing how the value should be normalized.
*
* For instance 'margin' style is a shorthand for four margin values:
*
* - 'margin-top'
* - 'margin-right'
* - 'margin-bottom'
* - 'margin-left'
*
* and can be written in various ways if some values are equal to others. For instance `'margin: 1px 2em;'` is a shorthand for
* `'margin-top: 1px;margin-right: 2em;margin-bottom: 1px;margin-left: 2em'`.
*
* A normalizer should parse various margin notations as a single object:
*
* ```ts
* const styles = {
* margin: {
* top: '1px',
* right: '2em',
* bottom: '1px',
* left: '2em'
* }
* };
* ```
*
* Thus a normalizer for 'margin' style should return an object defining style path and value to store:
*
* ```ts
* const returnValue = {
* path: 'margin',
* value: {
* top: '1px',
* right: '2em',
* bottom: '1px',
* left: '2em'
* }
* };
* ```
*
* Additionally to fully support all margin notations there should be also defined 4 normalizers for longhand margin notations. Below
* is an example for 'margin-top' style property normalizer:
*
* ```ts
* stylesProcessor.setNormalizer( 'margin-top', valueString => {
* return {
* path: 'margin.top',
* value: valueString
* }
* } );
* ```
*/
setNormalizer(name, callback) {
this._normalizers.set(name, callback);
}
/**
* Adds a extractor callback for a style property.
*
* Most normalized style values are stored as one level objects. It is assumed that `'margin-top'` style will be stored as:
*
* ```ts
* const styles = {
* margin: {
* top: 'value'
* }
* }
* ```
*
* However, some styles can have conflicting notations and thus it might be harder to extract a style value from shorthand. For instance
* the 'border-top-style' can be defined using `'border-top:solid'`, `'border-style:solid none none none'` or by `'border:solid'`
* shorthands. The default border styles processors stores styles as:
*
* ```ts
* const styles = {
* border: {
* style: {
* top: 'solid'
* }
* }
* }
* ```
*
* as it is better to modify border style independently from other values. On the other part the output of the border might be
* desired as `border-top`, `border-left`, etc notation.
*
* In the above example an extractor should return a side border value that combines style, color and width:
*
* ```ts
* styleProcessor.setExtractor( 'border-top', styles => {
* return {
* color: styles.border.color.top,
* style: styles.border.style.top,
* width: styles.border.width.top
* }
* } );
* ```
*
* @param callbackOrPath Callback that return a requested value or path string for single values.
*/
setExtractor(name, callbackOrPath) {
this._extractors.set(name, callbackOrPath);
}
/**
* Adds a reducer callback for a style property.
*
* Reducer returns a minimal notation for given style name. For longhand properties it is not required to write a reducer as
* by default the direct value from style path is taken.
*
* For shorthand styles a reducer should return minimal style notation either by returning single name-value tuple or multiple tuples
* if a shorthand cannot be used. For instance for a margin shorthand a reducer might return:
*
* ```ts
* const marginShortHandTuple = [
* [ 'margin', '1px 1px 2px' ]
* ];
* ```
*
* or a longhand tuples for defined values:
*
* ```ts
* // Considering margin.bottom and margin.left are undefined.
* const marginLonghandsTuples = [
* [ 'margin-top', '1px' ],
* [ 'margin-right', '1px' ]
* ];
* ```
*
* A reducer obtains a normalized style value:
*
* ```ts
* // Simplified reducer that always outputs 4 values which are always present:
* stylesProcessor.setReducer( 'margin', margin => {
* return [
* [ 'margin', `${ margin.top } ${ margin.right } ${ margin.bottom } ${ margin.left }` ]
* ]
* } );
* ```
*/
setReducer(name, callback) {
this._reducers.set(name, callback);
}
/**
* Defines a style shorthand relation to other style notations.
*
* ```ts
* stylesProcessor.setStyleRelation( 'margin', [
* 'margin-top',
* 'margin-right',
* 'margin-bottom',
* 'margin-left'
* ] );
* ```
*
* This enables expanding of style names for shorthands. For instance, if defined,
* {@link module:engine/conversion/viewconsumable~ViewConsumable view consumable} items are automatically created
* for long-hand margin style notation alongside the `'margin'` item.
*
* This means that when an element being converted has a style `margin`, a converter for `margin-left` will work just
* fine since the view consumable will contain a consumable `margin-left` item (thanks to the relation) and
* `element.getStyle( 'margin-left' )` will work as well assuming that the style processor was correctly configured.
* However, once `margin-left` is consumed, `margin` will not be consumable anymore.
*/
setStyleRelation(shorthandName, styleNames) {
this._mapStyleNames(shorthandName, styleNames);
for (const alsoName of styleNames) {
this._mapStyleNames(alsoName, [shorthandName]);
}
}
/**
* Set two-way binding of style names.
*/
_mapStyleNames(name, styleNames) {
if (!this._consumables.has(name)) {
this._consumables.set(name, []);
}
this._consumables.get(name).push(...styleNames);
}
}
/**
* Parses inline styles and puts property - value pairs into styles map.
*
* @param stylesString Styles to parse.
* @returns Map of parsed properties and values.
*/
function parseInlineStyles(stylesString) {
// `null` if no quote was found in input string or last found quote was a closing quote. See below.
let quoteType = null;
let propertyNameStart = 0;
let propertyValueStart = 0;
let propertyName = null;
const stylesMap = new Map();
// Do not set anything if input string is empty.
if (stylesString === '') {
return stylesMap;
}
// Fix inline styles that do not end with `;` so they are compatible with algorithm below.
if (stylesString.charAt(stylesString.length - 1) != ';') {
stylesString = stylesString + ';';
}
// Seek the whole string for "special characters".
for (let i = 0; i < stylesString.length; i++) {
const char = stylesString.charAt(i);
if (quoteType === null) {
// No quote found yet or last found quote was a closing quote.
switch (char) {
case ':':
// Most of time colon means that property name just ended.
// Sometimes however `:` is found inside property value (for example in background image url).
if (!propertyName) {
// Treat this as end of property only if property name is not already saved.
// Save property name.
propertyName = stylesString.substr(propertyNameStart, i - propertyNameStart);
// Save this point as the start of property value.
propertyValueStart = i + 1;
}
break;
case '"':
case '\'':
// Opening quote found (this is an opening quote, because `quoteType` is `null`).
quoteType = char;
break;
case ';': {
// Property value just ended.
// Use previously stored property value start to obtain property value.
const propertyValue = stylesString.substr(propertyValueStart, i - propertyValueStart);
if (propertyName) {
// Save parsed part.
stylesMap.set(propertyName.trim(), propertyValue.trim());
}
propertyName = null;
// Save this point as property name start. Property name starts immediately after previous property value ends.
propertyNameStart = i + 1;
break;
}
}
}
else if (char === quoteType) {
// If a quote char is found and it is a closing quote, mark this fact by `null`-ing `quoteType`.
quoteType = null;
}
}
return stylesMap;
}
/**
* Return lodash compatible path from style name.
*/
function toPath(name) {
return name.replace('-', '.');
}
/**
* Appends style definition to the styles object.
*/
function appendStyleValue(stylesObject, nameOrPath, valueOrObject) {
let valueToSet = valueOrObject;
if (isObject(valueOrObject)) {
valueToSet = merge({}, get(stylesObject, nameOrPath), valueOrObject);
}
set(stylesObject, nameOrPath, valueToSet);
}
/**
* Modifies the `styles` deeply nested object by removing properties defined in `toRemove`.
*/
function removeStyles(styles, toRemove) {
for (const key of Object.keys(toRemove)) {
if (styles[key] !== null &&
!Array.isArray(styles[key]) &&
typeof styles[key] == 'object' &&
typeof toRemove[key] == 'object') {
removeStyles(styles[key], toRemove[key]);
if (!Object.keys(styles[key]).length) {
delete styles[key];
}
}
else {
delete styles[key];
}
}
}