/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import Matcher from '../view/matcher'; import ModelRange from '../model/range'; import ModelPosition from '../model/position'; import { cloneDeep } from 'lodash-es'; /** * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}. * * @module engine/conversion/upcast-converters */ /** * View element to model element conversion helper. * * This conversion results in creating a model element. For example, view `
Foo
` becomes `Foo
-->` element, `bold="true"` was added to the text. See
* {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute} for comparison.
*
* Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
*
* upcastElementToAttribute( { view: 'strong', model: 'bold' } );
*
* upcastElementToAttribute( { view: 'strong', model: 'bold', converterPriority: 'high' } );
*
* upcastElementToAttribute( {
* view: {
* name: 'span',
* classes: 'bold'
* },
* model: 'bold'
* } );
*
* upcastElementToAttribute( {
* view: {
* name: 'span',
* classes: [ 'styled', 'styled-dark' ]
* },
* model: {
* key: 'styled',
* value: 'dark'
* }
* } );
*
* upcastElementToAttribute( {
* view: {
* name: 'span',
* styles: {
* 'font-size': /[\s\S]+/
* }
* },
* model: {
* key: 'fontSize',
* value: viewElement => {
* const fontSize = viewElement.getStyle( 'font-size' );
* const value = fontSize.substr( 0, fontSize.length - 2 );
*
* if ( value <= 10 ) {
* return 'small';
* } else if ( value > 12 ) {
* return 'big';
* }
*
* return null;
* }
* }
* } );
*
* See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
*
* @param {Object} config Conversion configuration.
* @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
* @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
* the model attribute. `value` property may be set as a function that takes a view element and returns the value.
* If `String` is given, the model attribute value will be set to `true`.
* @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
* @returns {Function} Conversion helper.
*/
export function upcastElementToAttribute( config ) {
config = cloneDeep( config );
_normalizeModelAttributeConfig( config );
const converter = _prepareToAttributeConverter( config, false );
const elementName = _getViewElementNameFromConfig( config );
const eventName = elementName ? 'element:' + elementName : 'element';
return dispatcher => {
dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } );
};
}
/**
* View attribute to model attribute conversion helper.
*
* This conversion results in setting an attribute on a model node. For example, view `
` becomes
* `
Foo
-->` element, `bold="true"` was added to the text. * * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration. * * upcastAttributeToAttribute( { view: 'src', model: 'source' } ); * * upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } ); * * upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', converterPriority: 'normal' } ); * * upcastAttributeToAttribute( { * view: { * key: 'data-style', * value: /[\s\S]+/ * }, * model: 'styled' * } ); * * upcastAttributeToAttribute( { * view: { * name: 'img', * key: 'class', * value: 'styled-dark' * }, * model: { * key: 'styled', * value: 'dark' * } * } ); * * upcastAttributeToAttribute( { * view: { * key: 'class', * value: /styled-[\S]+/ * }, * model: { * key: 'styled' * value: viewElement => { * const regexp = /styled-([\S]+)/; * const match = viewElement.getAttribute( 'class' ).match( regexp ); * * return match[ 1 ]; * } * } * } ); * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {Object} config Conversion configuration. * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed, * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property, * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name` * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`, * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`. * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing * the model attribute. `value` property may be set as a function that takes a view element and returns the value. * If `String` is given, the model attribute value will be same as view attribute value. * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority. * @returns {Function} Conversion helper. */ export function upcastAttributeToAttribute( config ) { config = cloneDeep( config ); let viewKey = null; if ( typeof config.view == 'string' || config.view.key ) { viewKey = _normalizeViewAttributeKeyValueConfig( config ); } _normalizeModelAttributeConfig( config, viewKey ); const converter = _prepareToAttributeConverter( config, true ); return dispatcher => { dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } ); }; } /** * View element to model marker conversion helper. * * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element: * `
Foo
Bar
`, * after the conversion is done, the marker will be available in * {@link module:engine/model/model~Model#markers model document markers}. * * upcastElementToMarker( { view: 'marker-search', model: 'search' } ); * * upcastElementToMarker( { view: 'marker-search', model: 'search', converterPriority: 'high' } ); * * upcastElementToMarker( { * view: 'marker-search', * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) * } ); * * upcastElementToMarker( { * view: { * name: 'span', * attributes: { * 'data-marker': 'search' * } * }, * model: 'search' * } ); * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {Object} config Conversion configuration. * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted. * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns * a model marker name. * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. * @returns {Function} Conversion helper. */ export function upcastElementToMarker( config ) { config = cloneDeep( config ); _normalizeToMarkerConfig( config ); return upcastElementToElement( config ); } // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name // and if so, returns it. // // @param {Object} config Conversion config. // @returns {String|null} View element name or `null` if name is not directly set. function _getViewElementNameFromConfig( config ) { if ( typeof config.view == 'string' ) { return config.view; } if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) { return config.view.name; } return null; } // Helper for to-model-element conversion. Takes a config object and returns a proper converter function. // // @param {Object} config Conversion configuration. // @returns {Function} View to model converter. function _prepareToElementConverter( config ) { const matcher = new Matcher( config.view ); return ( evt, data, conversionApi ) => { // This will be usually just one pattern but we support matchers with many patterns too. const match = matcher.match( data.viewItem ); // If there is no match, this callback should not do anything. if ( !match ) { return; } // Force consuming element's name. match.match.name = true; // Create model element basing on config. const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer ); // Do not convert if element building function returned falsy value. if ( !modelElement ) { return; } // When element was already consumed then skip it. if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) { return; } // Find allowed parent for element that we are going to insert. // If current parent does not allow to insert element but one of the ancestors does // then split nodes to allowed parent. const splitResult = conversionApi.splitToAllowedParent( modelElement, data.modelCursor ); // When there is no split result it means that we can't insert element to model tree, so let's skip it. if ( !splitResult ) { return; } // Insert element on allowed position. conversionApi.writer.insert( modelElement, splitResult.position ); // Convert children and insert to element. const childrenResult = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelElement ) ); // Consume appropriate value from consumable values list. conversionApi.consumable.consume( data.viewItem, match.match ); // Set conversion result range. data.modelRange = new ModelRange( // Range should start before inserted element ModelPosition.createBefore( modelElement ), // Should end after but we need to take into consideration that children could split our // element, so we need to move range after parent of the last converted child. // before: