| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module engine/conversion/conversion
- */
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
- import {
- downcastElementToElement,
- downcastAttributeToElement,
- downcastAttributeToAttribute
- } from './downcast-converters';
- import {
- upcastElementToElement,
- upcastElementToAttribute,
- upcastAttributeToAttribute
- } from './upcast-converters';
- /**
- * An utility class that helps adding converters to upcast and downcast dispatchers.
- *
- * We recommend reading first the {@glink framework/guides/architecture/editing-engine} guide to understand the
- * core concepts of the conversion mechanisms.
- *
- * The instance of the conversion manager is available in the
- * {@link module:core/editor/editor~Editor#conversion `editor.conversion`} property
- * and by default has the following groups of dispatchers (i.e. directions of conversion):
- *
- * * `downcast` (editing and data downcasts)
- * * `editingDowncast`
- * * `dataDowncast`
- * * `upcast`
- *
- * To add a converter to a specific group use the {@link module:engine/conversion/conversion~Conversion#for `for()`}
- * method:
- *
- * // Add a converter to editing downcast and data downcast.
- * editor.conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
- *
- * // Add a converter to the data pipepline only:
- * editor.conversion.for( 'dataDowncast' ).add( downcastElementToElement( config ) );
- * // And a slightly different one for the editing pipeline:
- * editor.conversion.for( 'editingDowncast' ).add( downcastElementToWidget( config ) );
- *
- * The functions used in `add()` calls are one-way converters (i.e. you need to remember yourself to add
- * a converter in the other direction, if you feature requires that). They are also called "conversion helpers".
- * You can find a set of them in the {@link module:engine/conversion/downcast-converters} and
- * {@link module:engine/conversion/upcast-converters} modules
- *
- * Besides allowing to register converters to specific dispatchers, you can also use methods available in this
- * class to add two-way converters (upcast and downcast):
- *
- * * {@link module:engine/conversion/conversion~Conversion#elementToElement `elementToElement()`} –
- * model element to view element and vice versa
- * * {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement()`} –
- * model attribute to view element and vice versa
- * * {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement()`} –
- * model attribute to view element and vice versa
- */
- export default class Conversion {
- /**
- * Creates new Conversion instance.
- */
- constructor() {
- /**
- * @private
- * @member {Map}
- */
- this._dispatchersGroups = new Map();
- }
- /**
- * Registers one or more converters under given group name. Then, group name can be used to assign a converter
- * to multiple dispatchers at once.
- *
- * If given group name is used for a second time,
- * {@link module:utils/ckeditorerror~CKEditorError conversion-register-group-exists} error is thrown.
- *
- * @param {String} groupName A name for dispatchers group.
- * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
- * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers Dispatchers to register
- * under given name.
- */
- register( groupName, dispatchers ) {
- if ( this._dispatchersGroups.has( groupName ) ) {
- /**
- * Trying to register a group name that was already registered.
- *
- * @error conversion-register-group-exists
- */
- throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' );
- }
- this._dispatchersGroups.set( groupName, dispatchers );
- }
- /**
- * Provides chainable API to assign converters to dispatchers registered under given group name. Converters are added
- * by calling `.add()` method of an object returned by this function.
- *
- * conversion.for( 'downcast' )
- * .add( conversionHelperA )
- * .add( conversionHelperB );
- *
- * In above example, `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from `'model'` group.
- *
- * `.add()` takes exactly one parameter, which is a function. That function should accept one parameter, which
- * is a dispatcher instance. The function should add an actual converter to passed dispatcher instance.
- *
- * Conversion helpers for most common cases are already provided. They are flexible enough to cover most use cases.
- * See documentation to learn how they can be configured.
- *
- * For downcast (model to view conversion), these are:
- *
- * * {@link module:engine/conversion/downcast-converters~downcastElementToElement downcast element to element converter},
- * * {@link module:engine/conversion/downcast-converters~downcastAttributeToElement downcast attribute to element converter},
- * * {@link module:engine/conversion/downcast-converters~downcastAttributeToAttribute downcast attribute to attribute converter}.
- *
- * For upcast (view to model conversion), these are:
- *
- * * {@link module:engine/conversion/upcast-converters~upcastElementToElement upcast element to element converter},
- * * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute upcast attribute to element converter},
- * * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute upcast attribute to attribute converter}.
- *
- * An example of using conversion helpers to convert `paragraph` model element to `p` view element (and back):
- *
- * // Define conversion configuration - model element 'paragraph' should be converted to view element 'p'.
- * const config = { model: 'paragraph', view: 'p' };
- *
- * // Add converters to proper dispatchers using conversion helpers.
- * conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
- * conversion.for( 'upcast' ).add( upcastElementToElement( config ) );
- *
- * An example of providing custom conversion helper that uses custom converter function:
- *
- * // Adding custom `myConverter` converter for 'paragraph' element insertion, with default priority ('normal').
- * conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) );
- *
- * @param {String} groupName Name of dispatchers group to add converters to.
- * @returns {Object} Object with `.add()` method, providing a way to add converters.
- */
- for( groupName ) {
- const dispatchers = this._getDispatchers( groupName );
- return {
- add( conversionHelper ) {
- _addToDispatchers( dispatchers, conversionHelper );
- return this;
- }
- };
- }
- /**
- * Sets up converters between the model and the view which convert a model element to a view element (and vice versa).
- * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
- *
- * // Simple conversion from `paragraph` model element to `<p>` view element (and vice versa).
- * conversion.elementToElement( { model: 'paragraph', view: 'p' } );
- *
- * // Override other converters by specifying converter definition with higher priority.
- * conversion.elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } );
- *
- * // View specified as an object instead of a string.
- * conversion.elementToElement( {
- * model: 'fancyParagraph',
- * view: {
- * name: 'p',
- * classes: 'fancy'
- * }
- * } );
- *
- * // Use `upcastAlso` to define other view elements that should be also converted to `paragraph` element.
- * conversion.elementToElement( {
- * model: 'paragraph',
- * view: 'p',
- * upcastAlso: [
- * 'div',
- * {
- * // Any element with `display: block` style.
- * styles: {
- * display: 'block'
- * }
- * }
- * ]
- * } );
- *
- * // `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
- * conversion.elementToElement( {
- * model: 'heading',
- * view: 'h2',
- * // Convert "headling-like" paragraphs to headings.
- * upcastAlso: viewElement => {
- * const fontSize = viewElement.getStyle( 'font-size' );
- *
- * if ( !fontSize ) {
- * return null;
- * }
- *
- * const match = fontSize.match( /(\d+)\s*px/ );
- *
- * if ( !match ) {
- * return null;
- * }
- *
- * const size = Number( match[ 1 ] );
- *
- * if ( size > 26 ) {
- * // Returned value be an object with the matched properties.
- * // Those properties will be "consumed" during conversion.
- * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
- *
- * return { name: true, styles: [ 'font-size' ] };
- * }
- *
- * return null;
- * }
- * } );
- *
- * `definition.model` is a `String` with a model element name to converter from/to.
- * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
- *
- * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
- */
- elementToElement( definition ) {
- // Set up downcast converter.
- this.for( 'downcast' ).add( downcastElementToElement( definition ) );
- // Set up upcast converter.
- for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
- this.for( 'upcast' ).add(
- upcastElementToElement( {
- model,
- view,
- converterPriority: definition.converterPriority
- } )
- );
- }
- }
- /**
- * Sets up converters between the model and the view which convert a model attribute to a view element (and vice versa).
- * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
- *
- * // Simple conversion from `bold=true` attribute to `<strong>` view element (and vice versa).
- * conversion.attributeToElement( { model: 'bold', view: 'strong' } );
- *
- * // Override other converters by specifying converter definition with higher priority.
- * conversion.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
- *
- * // View specified as an object instead of a string.
- * conversion.attributeToElement( {
- * model: 'bold',
- * view: {
- * name: 'span',
- * classes: 'bold'
- * }
- * } );
- *
- * // Use `config.model.name` to define conversion only from given node type, `$text` in this case.
- * // The same attribute on different elements may be then handled by a different converter.
- * conversion.attributeToElement( {
- * model: {
- * key: 'textDecoration',
- * values: [ 'underline', 'lineThrough' ],
- * name: '$text'
- * },
- * view: {
- * underline: {
- * name: 'span',
- * styles: {
- * 'text-decoration': 'underline'
- * }
- * },
- * lineThrough: {
- * name: 'span',
- * styles: {
- * 'text-decoration': 'line-through'
- * }
- * }
- * }
- * } );
- *
- * // Use `upcastAlso` to define other view elements that should be also converted to `bold` attribute.
- * conversion.attributeToElement( {
- * model: 'bold',
- * view: 'strong',
- * upcastAlso: [
- * 'b',
- * {
- * name: 'span',
- * classes: 'bold'
- * },
- * {
- * name: 'span',
- * styles: {
- * 'font-weight': 'bold'
- * }
- * },
- * viewElement => {
- * const fontWeight = viewElement.getStyle( 'font-weight' );
- *
- * if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
- * // Returned value be an object with the matched properties.
- * // Those properties will be "consumed" during conversion.
- * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
- *
- * return {
- * name: true,
- * styles: [ 'font-weight' ]
- * };
- * }
- * }
- * ]
- * } );
- *
- * // Conversion from/to a model attribute key which value is an enum (`fontSize=big|small`).
- * // `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
- * conversion.attributeToElement( {
- * model: {
- * key: 'fontSize',
- * values: [ 'big', 'small' ]
- * },
- * view: {
- * big: {
- * name: 'span',
- * styles: {
- * 'font-size': '1.2em'
- * }
- * },
- * small: {
- * name: 'span',
- * styles: {
- * 'font-size': '0.8em'
- * }
- * }
- * },
- * upcastAlso: {
- * big: viewElement => {
- * const fontSize = viewElement.getStyle( 'font-size' );
- *
- * if ( !fontSize ) {
- * return null;
- * }
- *
- * const match = fontSize.match( /(\d+)\s*px/ );
- *
- * if ( !match ) {
- * return null;
- * }
- *
- * const size = Number( match[ 1 ] );
- *
- * if ( viewElement.is( 'span' ) && size > 10 ) {
- * // Returned value be an object with the matched properties.
- * // Those properties will be "consumed" during conversion.
- * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
- *
- * return { name: true, styles: [ 'font-size' ] };
- * }
- *
- * return null;
- * },
- * small: viewElement => {
- * const fontSize = viewElement.getStyle( 'font-size' );
- *
- * if ( !fontSize ) {
- * return null;
- * }
- *
- * const match = fontSize.match( /(\d+)\s*px/ );
- *
- * if ( !match ) {
- * return null;
- * }
- *
- * const size = Number( match[ 1 ] );
- *
- * if ( viewElement.is( 'span' ) && size < 10 ) {
- * // Returned value be an object with the matched properties.
- * // Those properties will be "consumed" during conversion.
- * // See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
- *
- * return { name: true, styles: [ 'font-size' ] };
- * }
- *
- * return null;
- * }
- * }
- * } );
- *
- * `definition.model` parameter specifies what model attribute should be converted from/to. It can be a `{ key, value }` object
- * describing attribute key and value to convert or a `String` specifying just attribute key (then `value` is set to `true`).
- * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
- *
- * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
- */
- attributeToElement( definition ) {
- // Set up downcast converter.
- this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
- // Set up upcast converter.
- for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
- this.for( 'upcast' ).add(
- upcastElementToAttribute( {
- view,
- model,
- priority: definition.priority
- } )
- );
- }
- }
- /**
- * Sets up converters between the model and the view which convert a model attribute to a view attribute (and vice versa).
- * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute key and value).
- *
- * // Simple conversion from `source` model attribute to `src` view attribute (and vice versa).
- * conversion.attributeToAttribute( { model: 'source', view: 'src' } );
- *
- * // Attributes values are strictly specified.
- * conversion.attributeToAttribute( {
- * model: {
- * name: 'image',
- * key: 'aside',
- * values: [ 'aside' ]
- * },
- * view: {
- * aside: {
- * name: 'img',
- * key: 'class',
- * value: [ 'aside', 'half-size' ]
- * }
- * }
- * } );
- *
- * // Set style attribute.
- * conversion.attributeToAttribute( {
- * model: {
- * name: 'image',
- * key: 'aside',
- * values: [ 'aside' ]
- * },
- * view: {
- * aside: {
- * name: 'img',
- * key: 'style',
- * value: {
- * float: 'right',
- * width: '50%',
- * margin: '5px'
- * }
- * }
- * }
- * } );
- *
- * // Conversion from/to a model attribute key which value is an enum (`align=right|center`).
- * // Use `upcastAlso` to define other view elements that should be also converted to `align=right` attribute.
- * conversion.attributeToAttribute( {
- * model: {
- * key: 'align',
- * values: [ 'right', 'center' ]
- * },
- * view: {
- * right: {
- * key: 'class',
- * value: 'align-right'
- * },
- * center: {
- * key: 'class',
- * value: 'align-center'
- * }
- * },
- * upcastAlso: {
- * right: {
- * styles: {
- * 'text-align': 'right'
- * }
- * },
- * center: {
- * styles: {
- * 'text-align': 'center'
- * }
- * }
- * }
- * } );
- *
- * `definition.model` parameter specifies what model attribute should be converted from/to.
- * It can be a `{ key, [ values ], [ name ] }` object or a `String`, which will be treated like `{ key: definition.model }`.
- * `key` property is the model attribute key to convert from/to.
- * `values` are the possible model attribute values. If `values` is not set, model attribute value will be the same as the
- * view attribute value.
- * If `name` is set, conversion will be set up only for model elements with the given name.
- *
- * `definition.view` parameter specifies what view attribute should be converted from/to.
- * It can be a `{ key, value, [ name ] }` object or a `String`, which will be treated like `{ key: definition.view }`.
- * `key` property is the view attribute key to convert from/to.
- * `value` is the view attribute value to convert from/to. If `definition.value` is not set, view attribute value will be
- * the same as the model attribute value.
- * If `key` is `'class'`, `value` can be a `String` or an array of `String`s.
- * If `key` is `'style'`, `value` is an object with key-value pairs.
- * In other cases, `value` is a `String`.
- * If `name` is set, conversion will be set up only for model elements with the given name.
- * If `definition.model.values` is set, `definition.view` is an object which assigns values from `definition.model.values`
- * to `{ key, value, [ name ] }` objects.
- *
- * `definition.upcastAlso` specifies which other matching view elements should be also upcast to given model configuration.
- * If `definition.model.values` is set, `definition.upcastAlso` should be an object assigning values from `definition.model.values`
- * to {@link module:engine/view/matcher~MatcherPattern}s or arrays of {@link module:engine/view/matcher~MatcherPattern}s.
- *
- * **Note:** `definition.model` and `definition.view` form should be mirrored, that is the same type of parameters should
- * be given in both parameters.
- *
- * @param {Object} definition Converter definition.
- * @param {String|Object} definition.model Model attribute to convert from/to.
- * @param {String|Object} definition.view View attribute to convert from/to.
- * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
- * Any view element matching `definition.upcastAlso` will also be converted to the given model attribute. `definition.upcastAlso`
- * is used only if `config.model.values` is specified.
- */
- attributeToAttribute( definition ) {
- // Set up downcast converter.
- this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
- // Set up upcast converter.
- for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
- this.for( 'upcast' ).add(
- upcastAttributeToAttribute( {
- view,
- model
- } )
- );
- }
- }
- /**
- * Returns dispatchers registered under given group name.
- *
- * If given group name has not been registered,
- * {@link module:utils/ckeditorerror~CKEditorError conversion-for-unknown-group} error is thrown.
- *
- * @private
- * @param {String} groupName
- * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
- * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
- */
- _getDispatchers( groupName ) {
- const dispatchers = this._dispatchersGroups.get( groupName );
- if ( !dispatchers ) {
- /**
- * Trying to add a converter to an unknown dispatchers group.
- *
- * @error conversion-for-unknown-group
- */
- throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
- }
- return dispatchers;
- }
- }
- /**
- * Defines how the model should be converted from/to the view.
- *
- * @typedef {Object} module:engine/conversion/conversion~ConverterDefinition
- *
- * @property {*} [model] Model conversion definition. Describes model element or model attribute to convert. This parameter differs
- * for different functions that accepts `ConverterDefinition`. See the description of a function to learn how to set it.
- * @property {module:engine/view/elementdefinition~ElementDefinition|Object} view Definition of a view element to convert from/to.
- * If `model` describes multiple values, `view` is an object that assigns those values (`view` object keys) to view element definitions
- * (`view` object values).
- * @property {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [upcastAlso]
- * Any view element matching `upcastAlso` will also be converted to model. If `model` describes multiple values, `upcastAlso`
- * is an object that assigns those values (`upcastAlso` object keys) to {@link module:engine/view/matcher~MatcherPattern}s
- * (`upcastAlso` object values).
- * @property {module:utils/priorities~PriorityString} [converterPriority] Converter priority.
- */
- // Helper function for `Conversion` `.add()` method.
- //
- // Calls `conversionHelper` on each dispatcher from the group specified earlier in `.for()` call, effectively
- // adding converters to all specified dispatchers.
- //
- // @private
- // @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
- // module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
- // @param {Function} conversionHelper
- function _addToDispatchers( dispatchers, conversionHelper ) {
- for ( const dispatcher of dispatchers ) {
- conversionHelper( dispatcher );
- }
- }
- // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
- // `definition.upcastAlso`.
- //
- // @param {module:engine/conversion/conversion~ConverterDefinition} definition
- // @returns {Array} Array containing view definitions.
- function* _getAllUpcastDefinitions( definition ) {
- if ( definition.model.values ) {
- for ( const value of definition.model.values ) {
- const model = { key: definition.model.key, value };
- const view = definition.view[ value ];
- const upcastAlso = definition.upcastAlso ? definition.upcastAlso[ value ] : undefined;
- yield* _getUpcastDefinition( model, view, upcastAlso );
- }
- } else {
- yield* _getUpcastDefinition( definition.model, definition.view, definition.upcastAlso );
- }
- }
- function* _getUpcastDefinition( model, view, upcastAlso ) {
- yield { model, view };
- if ( upcastAlso ) {
- upcastAlso = Array.isArray( upcastAlso ) ? upcastAlso : [ upcastAlso ];
- for ( const upcastAlsoItem of upcastAlso ) {
- yield { model, view: upcastAlsoItem };
- }
- }
- }
|