Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

T/1198: ViewElementConfig with helper converters. #1205

Merged
merged 28 commits into from
Dec 21, 2017
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2b4352a
Other: Initial implementation of unified converters from ViewElementD…
jodator Dec 5, 2017
ea95029
Other: Unify ViewElement converters for attribute and element.
jodator Dec 11, 2017
1e28cba
Other: Rename ViewElementDefinition attributes to plural names.
jodator Dec 11, 2017
61ea426
Tests: Add tests for Element.fromViewDefinition() method.
jodator Dec 11, 2017
b2f1044
Tests: Add tests for new conversion helpers.
jodator Dec 12, 2017
a54727d
Other: Rename AttributeElement conversion helpers.
jodator Dec 12, 2017
d7141cc
Other: Rename ContainerElement conversion helpers.
jodator Dec 12, 2017
7381a09
Other: Merge configuration defined converters into one file.
jodator Dec 14, 2017
291d889
Docs: Update module:engine/conversion/configurationdefinedconverters …
jodator Dec 14, 2017
1328204
Merge branch 'master' into t/1198
jodator Dec 14, 2017
f80abc2
Docs: Update configuration defined converters docs and method names.
jodator Dec 14, 2017
2ac20cc
Other: Revert changes in matcher.
jodator Dec 14, 2017
4604f04
Tests: Update tests description in configurationdefinedconverters.js.
jodator Dec 14, 2017
cda98ba
Merge branch 'master' into t/1198
jodator Dec 17, 2017
16cae1d
Changed: configuration defined converts should not alter definition a…
jodator Dec 19, 2017
67de247
Other: Rename configurationdefinedconverters to definition-based-conv…
jodator Dec 19, 2017
7d30fa1
Docs: Refine definition-based-converters documentation.
jodator Dec 19, 2017
86be1e7
Refactor: Rename `view.Element.fromViewDefinition()` to `view.Element…
jodator Dec 19, 2017
e2ce318
Docs: Update ViewElementDefinition description.
jodator Dec 19, 2017
a48f922
Refactor: Use singular class and styles in ViewElementDefinition.
jodator Dec 19, 2017
f6b5676
Other: Rename internal variables of definition-based-converters to av…
jodator Dec 21, 2017
b77b76f
Other: Move static method `createFromDefinition` from `view:Element` …
jodator Dec 21, 2017
42848f7
Docs: Fix documentation links.
jodator Dec 21, 2017
55f7555
Merge branch 'master' into t/1198
jodator Dec 21, 2017
5e4ebda
Changed: Pass all `ConverterDefinition`s to modelAttributeToViewAttri…
jodator Dec 21, 2017
211d748
Merge branch 'master' into t/1198
Reinmar Dec 21, 2017
548734e
API docs fixes.
Reinmar Dec 21, 2017
4760cce
Typo fixes.
jodator Dec 21, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 310 additions & 0 deletions src/conversion/configurationdefinedconverters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is named incorrectly. If it doesn't export a variable called configurationDefinedConverters then these are 3 separate words and hence the file should be called configuration-defined-converters.js.

I'm also unsure about naming this file after "configuration". What if I just want to use these helpers in my feature (which has no configuration)? Can we think about a different name?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definition-based-converters.js? :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we need to find some proper name for these definitions. Something distinctive that we could use all around the code. Conversion specs, conversion maps, conversion rules, conversion... dunno. None of these is better than conversion definitions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... I go with that definition-based-converts and other cleanups. We can review it once again after I make all the requested changes.

* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module engine/conversion/configurationdefinedconverters
*/

import AttributeElement from '../view/attributeelement';
import ViewContainerElement from '../view/containerelement';

import buildModelConverter from './buildmodelconverter';
import buildViewConverter from './buildviewconverter';

/**
* Helper for creating model to view converter from model's element
* to {@link module:engine/view/containerelement~ContainerElement ViewContainerElement}. The `acceptAlso` property is ignored.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the last sentence do here? You're introducing the function, so don't go into details like what kind of property is ignored because you haven't introduced this property yet.

*
* You can define conversion as simple model element to view element conversion using simplified definition:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's always "by using". "using" means that the definition is an unrelated condition. https://english.stackexchange.com/questions/217815/what-is-difference-between-using-and-by-using

The same applies below – "by defining".

*
* modelElementToViewContainerElement( {
* model: 'heading1',
* view: 'h1',
* }, [ dispatcher ] );
*
* Or defining full-flavored view object:
*
* modelElementToViewContainerElement( {
* model: 'heading1',
* view: {
* name: 'h1',
* class: [ 'header', 'article-header' ],
* attributes: {
* data-header: 'level-1',
* }
* },
* }, [ dispatcher ] );
*
* Above will generate an HTML tag:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the following DOM element:"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, isn't it a view element?

*
* <h1 class="header article-header" data-header="level-1">...</h1>
*
* @param {module:engine/conversion/configurationdefinedconverters~ConverterDefinition} definition A conversion configuration.
* @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
*/
export function modelElementToViewContainerElement( definition, dispatchers ) {
const { model: modelElement, viewDefinition } = parseConverterDefinition( definition );

buildModelConverter()
.for( ...dispatchers )
.fromElement( modelElement )
.toElement( () => ViewContainerElement.fromViewDefinition( viewDefinition ) );
}

/**
* Helper for creating view to model converter from view to model element. It will convert also all matched view elements defined in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first sentence makes no sense. "view to model converter" and then "from view to model"?

* `acceptAlso` property. The `model` property is used as model element name.
*
* Conversion from model to view might be defined as simple one to one conversion:
*
* viewToModelElement( { model: 'heading1', view: 'h1' }, [ dispatcher ] );
*
* As a full-flavored definition:
*
* viewToModelElement( {
* model: 'heading1',
* view: {
* name: 'p',
* attributes: {
* 'data-heading': 'true'
* },
* // it might require to define higher priority for elements matched by other features
Copy link
Member

@Reinmar Reinmar Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upper case and period.

You may need to use a high-priority listener to catch elements which are handled by other (usually – more generic) converters too.

* priority: 'high'
* }
* }, [ dispatcher ] );
*
* or with `acceptAlso` property to match many elements:
*
* viewToModelElement( {
* model: 'heading1',
* view: {
* name: 'h1'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why using the object syntax in this case?

* },
* acceptAlso: [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe view could be an array in case of multiple definitions?

Copy link
Contributor Author

@jodator jodator Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view option defines default model->view conversion and it should always be one as it would be impossible to convert one model value to many (random?) view definitions ;).

The acceptAlso is used as a way to convert existing markup to model in view->model conversion. As in this example you always want to convert to h1 element but when loading content you want to map other elements to heading1 also.

So TL;DR: I don't think so ;)

Copy link
Contributor

@oskarwrobel oskarwrobel Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean view as array only for view->model conversion. I wouldn't be afraid that someone will try to use it for converting to multiple or random element in case of model->view conversion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or I don't understand something. Maybe exactly the same definition will be used for both conversions.

Copy link
Contributor Author

@jodator jodator Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the configuration (like in heading) would be one:

return VirtualTestEditor
	.create( {
		plugins: [ HeadingEngine ],
		heading: {
			options: [
				{ model: 'paragraph', title: 'paragraph' },
				{
					model: 'heading1',
					view: 'h1',
					acceptsAlso: [
						{
							name: 'p',
							attributes: { 'data-heading': 'h1' },
							priority: 'high'
						}
					],
					title: 'User H1'
				}
			]
		}
	} )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes sense now :)

Copy link
Contributor Author

@jodator jodator Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* { name: 'p', attributes: { 'data-heading': 'level1' }, priority: 'high' },
* { name: 'h2', class: 'heading-main' },
* { name: 'div', style: { 'font-weight': 'bold', font-size: '24px' } }
* ]
* }, [ dispatcher ] );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a real dispatchers here – I mean, something like editor.editing.fromView. Otherwise it will be super confusing what this dispatcher is and where to take it from. In general, code samples should be as realistic as possible.

*
* Above example will convert such existing HTML content:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above
an existing view elements

*
* <h1>A heading</h1>
* <h2 class="heading-main">Another heading</h2>
* <p data-heading="level1">Paragraph-like heading</p>
* <div style="font-size:24px; font-weigh:bold;">Another non-semantic header</div>
*
* into `heading1` model element so after rendering it the output HTML will be cleaned up:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again - avoid using "HTML".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops. I haven't noticed that you mention here retrieving the data back. So it makes sense to call it HTML. But at the same time, maybe it's better to only mention here what ends up in the model. It will be clearer where and when the unification happen.

*
* <h1>A heading</h1>
* <h1>Another heading</h1>
* <h1>Paragraph-like heading</h1>
* <h1>Another non-semantic header</h1>
*
* @param {module:engine/conversion/configurationdefinedconverters~ConverterDefinition} definition A conversion configuration.
* @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
*/
export function viewToModelElement( definition, dispatchers ) {
const { model: modelElement, viewDefinitions } = parseConverterDefinition( definition );

const converter = prepareViewConverter( dispatchers, viewDefinitions );

converter.toElement( modelElement );
}

/**
* Helper for creating model to view converter from model's attribute
* to {@link module:engine/view/attributeelement~AttributeElement AttributeElement}. The `acceptAlso` property is ignored.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't specify the second param of {@link} because what you did here is actually breaking the link. It should look like: AttributeElement but you made it AttributeElement. If you won't specify the second param it will be rendered correctly. Specify the second param only if you want to write something custom like config.foo.bar (which would be better than the default bar). Also, make sure to always wrap code names in backticks.

*
* You can define conversion as simple model element to view element conversion using simplified definition:
*
* modelAttributeToViewAttributeElement( 'bold', {
* model: 'true',
* view: 'strong',
* }, [ dispatcher ] );
*
* Or defining full-flavored view object:
*
* modelAttributeToViewAttributeElement( 'fontSize' {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing comma.

* model: 'big',
* view: {
* name: 'span',
* styles: {
* 'font-size': '1.2em'
* }
* },
* }, [ dispatcher ] );
*
* Above will generate an HTML tag for model's attribute `fontSize` with a `big` value set:
*
* <span style="font-size:1.2em;">...</span>
*
* @param {String} attributeName Attribute name from which convert.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"to convert" or something even more understandable "The name of the model attribute which should be converted."

* @param {module:engine/conversion/configurationdefinedconverters~ConverterDefinition} definition A conversion configuration.
* @param {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>} dispatchers
*/
export function modelAttributeToViewAttributeElement( attributeName, definition, dispatchers ) {
const { model: attributeValue, viewDefinition } = parseConverterDefinition( definition );

buildModelConverter()
.for( ...dispatchers )
.fromAttribute( attributeName )
.toElement( value => {
if ( value != attributeValue ) {
return;
}

return AttributeElement.fromViewDefinition( viewDefinition );
} );
}

/**
* Helper for creating view to model converter from view to model attribute. It will convert also all matched view elements defined in
* `acceptAlso` property. The `model` property is used as model's attribute value to match.
*
* Conversion from model to view might be defined as simple one to one conversion:
*
* viewToModelAttribute( 'bold', { model: true, view: 'strong' }, [ dispatcher ] );
*
* As a full-flavored definition:
*
* viewToModelAttribute( 'fontSize', {
* model: 'big',
* view: {
* name: 'span',
* style: {
* 'font-size': '1.2em'
* }
* }
* }, [ dispatcher ] );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something's wrong with indentation here.

*
* or with `acceptAlso` property to match many elements:
*
* viewToModelAttribute( 'fontSize', {
* model: 'big',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the fact that attirbute name is passed as a separate parameter (what also @jodator notice in the PR description). It is also strange that in one case model is the model element name and in the other it is attibute value.

At, the same time I undrestand that for feature configuration it is the best, because feature knows what is the name of the attribute.

However, I would go here with:

model: {
   attributes: { 'fontSize': 'big' }
}

This is configuration of the conversion building tool. The plugin configuration does not need to be the same. Plugin can do some transformation adding obvious data to it, to make its configuration as simple as possible. However, this config will not fit all features need and we should not try to make it 1-1 with the config.

* view: {
* name: 'span',
* class: 'text-big'
* },
* acceptAlso: [
* { name: 'span', attributes: { 'data-size': 'big' } },
* { name: 'span', class: [ 'font', 'font-huge' ] },
* { name: 'span', style: { font-size: '18px' } }
* ]
* }, [ dispatcher ] );
*
* Above example will convert such existing HTML content:
*
* <p>An example text with some big elements:
* <span class="text-big>one</span>,
* <span data-size="big>two</span>,
* <span class="font font-huge>three</span>,
* <span style="font-size:18px>four</span>
* <p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing / (and below too).

*
* into `fontSize` model attribute with 'big' value set so after rendering it the output HTML will be cleaned up:
*
* <p>An example text with some big elements:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention, "An example..." should be in a new line.

* <span class="text-big>one</span>,
* <span class="text-big>two</span>,
* <span class="text-big>three</span>,
* <span class="text-big>four</span>
* <p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing /.

*
* @param {String} attributeName Attribute name to which convert.
* @param {module:engine/conversion/configurationdefinedconverters~ConverterDefinition} definition A conversion configuration.
* @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
*/
export function viewToModelAttribute( attributeName, definition, dispatchers ) {
const { model: attributeValue, viewDefinitions } = parseConverterDefinition( definition );

const converter = prepareViewConverter( dispatchers, viewDefinitions );

converter.toAttribute( () => ( {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we fine with this notation or we prefer a return statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might saw that in code so any advice on team-preferred code style is welcome :)

key: attributeName,
value: attributeValue
} ) );
}

// Prepares a {@link module:engine/conversion/configurationdefinedconverters~ConverterDefinition definition object} for building converters.
//
// @param {module:engine/conversion/configurationdefinedconverters~ConverterDefinition} definition An object that defines view to model
// and model to view conversion.
// @returns {Object}
function parseConverterDefinition( definition ) {
const model = definition.model;
const view = definition.view;

const viewDefinition = typeof view == 'string' ? { name: view } : view;

const viewDefinitions = definition.acceptsAlso ? definition.acceptsAlso : [];

viewDefinitions.push( viewDefinition );

return { model, viewDefinition, viewDefinitions };
}

// Helper method for preparing a view converter from passed view definitions.
//
// @param {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>} dispatchers
// @param {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} viewDefinitions
// @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
function prepareViewConverter( dispatchers, viewDefinitions ) {
const converter = buildViewConverter().for( ...dispatchers );

for ( const viewDefinition of viewDefinitions ) {
converter.from( definitionToPattern( viewDefinition ) );

if ( viewDefinition.priority ) {
converter.withPriority( viewDefinition.priority );
}
}

return converter;
}

// Converts viewDefinition to a matcher pattern.
//
// @param {module:engine/view/viewelementdefinition~ViewElementDefinition} viewDefinition
// @returns {module:engine/view/matcher~Pattern}
function definitionToPattern( viewDefinition ) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead if translating syntaxes, we should make them the same (change matcher pattern to fit definition pattern).

const name = viewDefinition.name;
const classes = viewDefinition.classes;
const styles = viewDefinition.styles;
const attributes = viewDefinition.attributes;

const pattern = { name };

if ( classes ) {
pattern.class = classes;
}

if ( styles ) {
pattern.style = styles;
}

if ( attributes ) {
pattern.attribute = attributes;
}

return pattern;
}

/**
* Defines conversion details.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing links to where is all this used.

*
* @typedef {Object} ConverterDefinition
* @property {String} model Defines to model conversion. When using to element conversion
* ({@link module:engine/conversion/configurationdefinedconverters~viewToModelElement}
* and {@link module:engine/conversion/configurationdefinedconverters~modelElementToViewContainerElement})
* it defines element name. When using to attribute conversion
* ({@link module:engine/conversion/configurationdefinedconverters~viewToModelAttribute}
* and {@link module:engine/conversion/configurationdefinedconverters~modelAttributeToViewAttributeElement})
* it defines attribute value to which it is converted.
* @property {String|module:engine/view/viewelementdefinition~ViewElementDefinition} view Defines model to view conversion and is also used
* in view to model conversion pipeline.
* @property {Array.<module:engine/view/viewelementdefinition~ViewElementDefinition>} acceptAlso An array with all matched elements that
* view to model conversion should also accepts.
*/
42 changes: 42 additions & 0 deletions src/view/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,48 @@ export default class Element extends Node {
( attributes == '' ? '' : ` ${ attributes }` );
}

/**
* Creates element instance from provided viewElementDefinition.
*
* @param {module:engine/view/viewelementdefinition~ViewElementDefinition} viewElementDefinition
* @returns {Element}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope

*/
static fromViewDefinition( viewElementDefinition ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misses a verb and repeating "view" isn't necessary IMO. ViewElement.createFromDefinition(). should be fine.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that soon view writer will be the only proper way of changing model, so such methods will have to be moved.

const attributes = {};

const classes = viewElementDefinition.classes;

if ( classes ) {
attributes.class = Array.isArray( classes ) ? classes.join( ' ' ) : classes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with styles below.

}

const stylesObject = viewElementDefinition.styles;

if ( stylesObject ) {
attributes.style = toStylesString( stylesObject );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you guys think that we could avoid this stringification? E.g. by setting styles after creating this element (then, view Element should then have setStyles() method)?

Uncareful stringification is unsafe.

}

const attributesObject = viewElementDefinition.attributes;

if ( attributesObject ) {
for ( const key in attributesObject ) {
attributes[ key ] = attributesObject[ key ];
}
}

return new this( viewElementDefinition.name, attributes );

function toStylesString( stylesObject ) {
const styles = [];

for ( const key in stylesObject ) {
styles.push( key + ':' + stylesObject[ key ] );
}

return styles.join( ';' );
}
}

/**
* Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
*
Expand Down
22 changes: 22 additions & 0 deletions src/view/viewelementdefinition.jsdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module engine/view/viewelementdefinition
*/

/**
* An object defining view element used for defining elements for conversion.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some examples and links to places where this is used will help.

*
* @typedef {Object} module:engine/view/viewelementdefinition~ViewElementDefinition
Copy link
Member

@Reinmar Reinmar Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it wouldn't be easier to define here that an element may be defined by a single string. Otherwise, everywhere this is used you'd need to write:

@property {String|module:engine/view/viewelementdefinition~ViewElementDefinition}

And you'll need to check the type of this property before deciding whether to use ViewElement.createFromDefinition() or the constructor (if a string was passed). It's usually easier if you have a handful method which can do that.

*
* @property {String} name View element name.
* @property {String|Array.<String>} [classes] Class name or array of class names to match. Each name can be
* provided in a form of string.
* @property {Object} [styles] Object with key-value pairs representing styles to match. Each object key
* represents style name. Value under that key must be a string.
* @property {Object} [attributes] Object with key-value pairs representing attributes to match. Each object key
* represents attribute name. Value under that key must be a string.
*/
Loading