/** * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import Matcher from '../view/matcher'; import ModelRange from '../model/range'; import ConversionHelpers from './conversionhelpers'; import { cloneDeep } from 'lodash-es'; import ModelSelection from '../model/selection'; /** * 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/upcasthelpers */ /** * Upcast conversion helper functions. * * @extends module:engine/conversion/conversionhelpers~ConversionHelpers */ export default class UpcastHelpers extends ConversionHelpers { /** * 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/upcasthelpers~UpcastHelpers#attributeToAttribute} 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.
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: 'strong',
* model: 'bold'
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: 'strong',
* model: 'bold',
* converterPriority: 'high'
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: {
* name: 'span',
* classes: 'bold'
* },
* model: 'bold'
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: {
* name: 'span',
* classes: [ 'styled', 'styled-dark' ]
* },
* model: {
* key: 'styled',
* value: 'dark'
* }
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* 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 `conversion.for()`} to learn how to add a converter
* to the conversion process.
*
* @method #elementToAttribute
* @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 {module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
elementToAttribute( config ) {
return this.add( upcastElementToAttribute( config ) );
}
/**
* 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. * * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: 'src', * model: 'source' * } ); * * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: { key: 'src' }, * model: 'source' * } ); * * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: { key: 'src' }, * model: 'source', * converterPriority: 'normal' * } ); * * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: { * key: 'data-style', * value: /[\s\S]+/ * }, * model: 'styled' * } ); * * editor.conversion.for( 'upcast' ).attributeToAttribute( { * view: { * name: 'img', * key: 'class', * value: 'styled-dark' * }, * model: { * key: 'styled', * value: 'dark' * } * } ); * * editor.conversion.for( 'upcast' ).attributeToAttribute( { * 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 `conversion.for()`} to learn how to add a converter * to the conversion process. * * @method #attributeToAttribute * @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 {module:engine/conversion/upcasthelpers~UpcastHelpers} */ attributeToAttribute( config ) { return this.add( upcastAttributeToAttribute( config ) ); } /** * 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}. * * editor.conversion.for( 'upcast' ).elementToMarker( { * view: 'marker-search', * model: 'search' * } ); * * editor.conversion.for( 'upcast' ).elementToMarker( { * view: 'marker-search', * model: 'search', * converterPriority: 'high' * } ); * * editor.conversion.for( 'upcast' ).elementToMarker( { * view: 'marker-search', * model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' ) * } ); * * editor.conversion.for( 'upcast' ).elementToMarker( { * view: { * name: 'span', * attributes: { * 'data-marker': 'search' * } * }, * model: 'search' * } ); * * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter * to the conversion process. * * @method #elementToMarker * @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 {module:engine/conversion/upcasthelpers~UpcastHelpers} */ elementToMarker( config ) { return this.add( upcastElementToMarker( config ) ); } } /** * Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment} * or all children of {@link module:engine/view/element~Element} into * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}. * This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters. * * This also a "default", last resort converter for all view elements that has not been converted by other converters. * When a view element is being converted to the model but it does not have converter specified, that view element * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned. * * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and * {@link module:engine/view/element~Element elements} that returns * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item. */ export function convertToModelFragment() { return ( evt, data, conversionApi ) => { // Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement. if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) { const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor ); data.modelRange = modelRange; data.modelCursor = modelCursor; } }; } /** * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}. * * @returns {Function} {@link module:engine/view/text~Text View text} converter. */ export function convertText() { return ( evt, data, conversionApi ) => { if ( conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) { if ( conversionApi.consumable.consume( data.viewItem ) ) { const text = conversionApi.writer.createText( data.viewItem.data ); conversionApi.writer.insert( text, data.modelCursor ); data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize ); data.modelCursor = data.modelRange.end; } } }; } /** * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event * and sets in on the {@link module:engine/model/document~Document#selection model}. * * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model * conversion mechanism, the callback should be set directly on view document. * * view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) ); * * @param {module:engine/model/model~Model} model Data model. * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper. * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function. */ export function convertSelectionChange( model, mapper ) { return ( evt, data ) => { const viewSelection = data.newSelection; const modelSelection = new ModelSelection(); const ranges = []; for ( const viewRange of viewSelection.getRanges() ) { ranges.push( mapper.toModelRange( viewRange ) ); } modelSelection.setTo( ranges, { backward: viewSelection.isBackward } ); if ( !modelSelection.isEqual( model.document.selection ) ) { model.change( writer => { writer.setSelection( modelSelection ); } ); } }; } // View element to model element conversion helper. // // See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples. // // @param {Object} config Conversion configuration. // @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not // set, the converter will fire for every view element. // @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element // instance or a function that takes a view element and returns a model element. The model element will be inserted in the model. // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority. // @returns {Function} Conversion helper. function upcastElementToElement( config ) { config = cloneDeep( config ); const converter = prepareToElementConverter( config ); const elementName = getViewElementNameFromConfig( config ); const eventName = elementName ? 'element:' + elementName : 'element'; return dispatcher => { dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } ); }; } // View element to model attribute conversion helper. // // See {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper} for examples. // // @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. 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 || 'low' } ); }; } // View attribute to model attribute conversion helper. // // See {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper} for examples. // // @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. 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. // // See {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper} for examples. // // @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. 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 = config.view ? new Matcher( config.view ) : null; return ( evt, data, conversionApi ) => { let match = {}; // If `config.view` has not been passed do not try matching. In this case, the converter should fire for all elements. if ( matcher ) { // This will be usually just one pattern but we support matchers with many patterns too. const matcherResult = matcher.match( data.viewItem ); // If there is no match, this callback should not do anything. if ( !matcherResult ) { return; } match = matcherResult.match; } // Force consuming element's name. 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 ) ) { 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. conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) ); // Consume appropriate value from consumable values list. conversionApi.consumable.consume( data.viewItem, match ); const parts = conversionApi.getSplitParts( modelElement ); // Set conversion result range. data.modelRange = new ModelRange( conversionApi.writer.createPositionBefore( modelElement ), conversionApi.writer.createPositionAfter( parts[ parts.length - 1 ] ) ); // Now we need to check where the `modelCursor` should be. if ( splitResult.cursorParent ) { // If we split parent to insert our element then we want to continue conversion in the new part of the split parent. // // before: