From 09172271502d1e2e39a1bb26f574002738aa7f9a Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 18 Jan 2021 12:23:39 +0100 Subject: [PATCH 001/217] Initial POC of general HTML support. --- .../src/dataschema.js | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 packages/ckeditor5-content-compatibility/src/dataschema.js diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js new file mode 100644 index 00000000000..dce78f4e9a7 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -0,0 +1,193 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module content-compatibility/dataschema + */ + +import { capitalize, escapeRegExp } from 'lodash-es'; + +const DATA_SCHEMA_PREFIX = 'ghs'; + +const dtd = { + 'div': { + inheritAllFrom: '$block' + } +}; + +export default class DataSchema { + constructor( editor ) { + this.editor = editor; + this.attributeFilter = new AttributeFilter(); + } + + allowElement( { name: viewName, attributes = [] } ) { + this._defineSchema( viewName ); + this._defineConverters( viewName ); + + if ( !Array.isArray( attributes ) ) { + attributes = [ attributes ]; + } + + for ( const attribute of attributes ) { + this.allowAttribute( { + name: attribute, + elements: [ viewName ] + } ); + } + } + + allowAttribute( { name: attributeName, elements = [] } ) { + if ( !Array.isArray( elements ) ) { + elements = [ elements ]; + } + + elements.forEach( element => this.attributeFilter.allow( element, attributeName ) ); + } + + disallowAttribute( { name: attributeName, elements = [] } ) { + if ( !Array.isArray( elements ) ) { + elements = [ elements ]; + } + + elements.forEach( element => this.attributeFilter.disallow( element, attributeName ) ); + } + + * _filterAttributes( elementName, attributes ) { + for ( const attribute of attributes ) { + const attributeName = attribute[ 0 ]; + + if ( this.attributeFilter.isAllowed( elementName, attributeName ) ) { + yield attribute; + } + } + } + + _defineSchema( viewName ) { + const schema = this.editor.model.schema; + + schema.register( encodeView( viewName ), dtd[ viewName ] ); + } + + _defineConverters( viewName ) { + const conversion = this.editor.conversion; + const filterAttributes = this._filterAttributes.bind( this ); + const modelName = encodeView( viewName ); + + conversion.for( 'upcast' ).elementToElement( { + view: viewName, + model: ( viewElement, { writer: modelWriter } ) => { + const attributes = filterAttributes( viewName, viewElement.getAttributes() ); + + const encodedAttributes = []; + for ( const attribute of attributes ) { + encodedAttributes.push( [ + encodeAttributeKey( attribute[ 0 ] ), + attribute[ 1 ] + ] ); + } + + return modelWriter.createElement( modelName, encodedAttributes ); + } + } ); + + conversion.for( 'downcast' ).elementToElement( { + model: modelName, + view: viewName + } ); + + conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( 'attribute', ( evt, data, conversionApi ) => { + if ( data.item.name != modelName ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewElement = conversionApi.mapper.toViewElement( data.item ); + const attributeKey = decodeAttributeKey( data.attributeKey ); + + if ( data.attributeNewValue !== null ) { + viewWriter.setAttribute( attributeKey, data.attributeNewValue, viewElement ); + } else { + viewWriter.removeAttribute( attributeKey, viewElement ); + } + } ); + } ); + } +} + +class AttributeFilter { + constructor() { + this._allowedRules = {}; + this._disallowedRules = {}; + } + + allow( elementRule, attributeRule ) { + this._register( this._allowedRules, elementRule, attributeRule ); + } + + disallow( elementRule, attributeRule ) { + this._register( this._disallowedRules, elementRule, attributeRule ); + } + + isAllowed( elementName, attributeName ) { + const isDisallowed = this._match( this._disallowedRules, elementName, attributeName ); + + if ( isDisallowed ) { + return false; + } + + return this._match( this._allowedRules, elementName, attributeName ); + } + + _register( rules, elementRule, attributeRule ) { + if ( !rules[ elementRule ] ) { + rules[ elementRule ] = []; + } + + if ( typeof attributeRule === 'string' ) { + attributeRule = createWildcardMatcher( attributeRule ); + } + + rules[ elementRule ].push( attributeRule ); + } + + _match( rules, elementName, attributeName ) { + for ( const rule of this._getMatchingRules( rules, elementName ) ) { + if ( rule( attributeName ) ) { + return true; + } + } + + return false; + } + + * _getMatchingRules( rules, elementName ) { + for ( const ruleName in rules ) { + const matcher = createWildcardMatcher( ruleName ); + + if ( matcher( elementName ) ) { + yield* rules[ ruleName ]; + } + } + } +} + +function createWildcardMatcher( rule ) { + const matcher = new RegExp( '^' + rule.split( '*' ).map( escapeRegExp ).join( '.*' ) + '$' ); + return value => matcher.test( value ); +} + +function encodeView( name ) { + return DATA_SCHEMA_PREFIX + capitalize( name ); +} + +function encodeAttributeKey( name ) { + return DATA_SCHEMA_PREFIX + '-' + name; +} + +function decodeAttributeKey( name ) { + return name.replace( DATA_SCHEMA_PREFIX + '-', '' ); +} From f30d3295031c939f4ed7f7470c00e3635dcc49d6 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 18 Jan 2021 12:43:50 +0100 Subject: [PATCH 002/217] Make sure to consume attribute on downcast. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index dce78f4e9a7..a594cacca9a 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -104,6 +104,10 @@ export default class DataSchema { return; } + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement( data.item ); const attributeKey = decodeAttributeKey( data.attributeKey ); From a204ba3e2f562059b51b09cd545aba44855b307d Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 18 Jan 2021 12:56:52 +0100 Subject: [PATCH 003/217] Refactoring. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index a594cacca9a..b39a116af45 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -34,7 +34,7 @@ export default class DataSchema { for ( const attribute of attributes ) { this.allowAttribute( { name: attribute, - elements: [ viewName ] + elements: viewName } ); } } From 059edf5cb6033a83e449d836b708666566656880 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 4 Feb 2021 11:14:49 +0100 Subject: [PATCH 004/217] Changed dataschema implementation to use view matcher. --- .../src/dataschema.js | 191 +++++++----------- 1 file changed, 71 insertions(+), 120 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index b39a116af45..880ce931717 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ @@ -8,88 +8,96 @@ */ import { capitalize, escapeRegExp } from 'lodash-es'; +import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher'; const DATA_SCHEMA_PREFIX = 'ghs'; +const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; + +// "h1", "h2", "h3", "h4", "h5", "h6", "legend", "pre", "rp", "rt", "summary", "p" +// legend, rp, rt, summary + +// Phrasing elements. +const P = encodeView( [ 'a', 'em', 'strong', 'small', 'abbr', 'dfn', 'i', 'b', 's', 'u', 'code', + 'var', 'samp', 'kbd', 'sup', 'sub', 'q', 'cite', 'span', 'bdo', 'bdi', 'br', + 'wbr', 'ins', 'del', 'img', 'embed', 'object', 'iframe', 'map', 'area', 'script', + 'noscript', 'ruby', 'video', 'audio', 'input', 'textarea', 'select', 'button', + 'label', 'output', 'keygen', 'progress', 'command', 'canvas', 'time', 'meter', 'detalist' ] ); + +// Flow elements. +const F = encodeView( [ 'a', 'p', 'hr', 'pre', 'ul', 'ol', 'dl', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'hgroup', 'address', 'blockquote', 'ins', 'del', 'object', 'map', 'noscript', 'section', + 'nav', 'article', 'aside', 'header', 'footer', 'video', 'audio', 'figure', 'table', + 'form', 'fieldset', 'menu', 'canvas', 'details' ] ); const dtd = { - 'div': { - inheritAllFrom: '$block' - } + section: { inheritAllFrom: '$block', allowContentOf: P, allowIn: F, allowAttributes: [ DATA_SCHEMA_ATTRIBUTE_KEY ] }, + article: { inheritAllFrom: '$block', allowContentOf: P, allowIn: F, allowAttributes: [ DATA_SCHEMA_ATTRIBUTE_KEY ] } }; +function remove( source, toRemove ) { + return source.filter( item => !toRemove.includes( item ) ); +} + export default class DataSchema { constructor( editor ) { this.editor = editor; - this.attributeFilter = new AttributeFilter(); - } - allowElement( { name: viewName, attributes = [] } ) { - this._defineSchema( viewName ); - this._defineConverters( viewName ); - - if ( !Array.isArray( attributes ) ) { - attributes = [ attributes ]; - } - - for ( const attribute of attributes ) { - this.allowAttribute( { - name: attribute, - elements: viewName - } ); - } + this.allowedContent = {}; } - allowAttribute( { name: attributeName, elements = [] } ) { - if ( !Array.isArray( elements ) ) { - elements = [ elements ]; + allow( config ) { + if ( !config.name ) { + return; } - elements.forEach( element => this.attributeFilter.allow( element, attributeName ) ); - } + const matchElement = getElementNameMatchingRegExp( config.name ); - disallowAttribute( { name: attributeName, elements = [] } ) { - if ( !Array.isArray( elements ) ) { - elements = [ elements ]; - } + for ( const elementName in dtd ) { + if ( !matchElement.test( elementName ) ) { + continue; + } - elements.forEach( element => this.attributeFilter.disallow( element, attributeName ) ); + this._defineSchema( elementName ); + this._defineConverters( elementName ); + this._getOrCreateMatcher( elementName ).add( config ); + } } - * _filterAttributes( elementName, attributes ) { - for ( const attribute of attributes ) { - const attributeName = attribute[ 0 ]; - - if ( this.attributeFilter.isAllowed( elementName, attributeName ) ) { - yield attribute; - } + _getOrCreateMatcher( elementName ) { + if ( !this.allowedContent[ elementName ] ) { + this.allowedContent[ elementName ] = new Matcher(); } + + return this.allowedContent[ elementName ]; } _defineSchema( viewName ) { const schema = this.editor.model.schema; + const modelName = encodeView( viewName ); - schema.register( encodeView( viewName ), dtd[ viewName ] ); + schema.register( modelName, dtd[ viewName ] ); } _defineConverters( viewName ) { const conversion = this.editor.conversion; - const filterAttributes = this._filterAttributes.bind( this ); const modelName = encodeView( viewName ); conversion.for( 'upcast' ).elementToElement( { view: viewName, - model: ( viewElement, { writer: modelWriter } ) => { - const attributes = filterAttributes( viewName, viewElement.getAttributes() ); - - const encodedAttributes = []; - for ( const attribute of attributes ) { - encodedAttributes.push( [ - encodeAttributeKey( attribute[ 0 ] ), - attribute[ 1 ] - ] ); + model: ( viewElement, conversionApi ) => { + const match = this._getOrCreateMatcher( viewName ).match( viewElement ); + const attributeMatch = match.match.attributes || []; + + const originalAttributes = attributeMatch.map( attributeName => { + return [ attributeName, viewElement.getAttribute( attributeName ) ]; + } ); + + let attributesToAdd; + if ( originalAttributes.length ) { + attributesToAdd = [ [ DATA_SCHEMA_ATTRIBUTE_KEY, originalAttributes ] ]; } - return modelWriter.createElement( modelName, encodedAttributes ); + return conversionApi.writer.createElement( modelName, attributesToAdd ); } } ); @@ -99,7 +107,7 @@ export default class DataSchema { } ); conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( 'attribute', ( evt, data, conversionApi ) => { + dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }`, ( evt, data, conversionApi ) => { if ( data.item.name != modelName ) { return; } @@ -110,88 +118,31 @@ export default class DataSchema { const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement( data.item ); - const attributeKey = decodeAttributeKey( data.attributeKey ); if ( data.attributeNewValue !== null ) { - viewWriter.setAttribute( attributeKey, data.attributeNewValue, viewElement ); - } else { - viewWriter.removeAttribute( attributeKey, viewElement ); + data.attributeNewValue.forEach( ( [ key, value ] ) => { + viewWriter.setAttribute( key, value, viewElement ); + } ); } + + viewWriter.removeAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewElement ); } ); } ); } } -class AttributeFilter { - constructor() { - this._allowedRules = {}; - this._disallowedRules = {}; - } - - allow( elementRule, attributeRule ) { - this._register( this._allowedRules, elementRule, attributeRule ); - } - - disallow( elementRule, attributeRule ) { - this._register( this._disallowedRules, elementRule, attributeRule ); - } - - isAllowed( elementName, attributeName ) { - const isDisallowed = this._match( this._disallowedRules, elementName, attributeName ); - - if ( isDisallowed ) { - return false; - } - - return this._match( this._allowedRules, elementName, attributeName ); - } - - _register( rules, elementRule, attributeRule ) { - if ( !rules[ elementRule ] ) { - rules[ elementRule ] = []; - } - - if ( typeof attributeRule === 'string' ) { - attributeRule = createWildcardMatcher( attributeRule ); - } - - rules[ elementRule ].push( attributeRule ); - } - - _match( rules, elementName, attributeName ) { - for ( const rule of this._getMatchingRules( rules, elementName ) ) { - if ( rule( attributeName ) ) { - return true; - } - } - - return false; +function encodeView( name ) { + if ( Array.isArray( name ) ) { + return name.map( encodeView ); } - * _getMatchingRules( rules, elementName ) { - for ( const ruleName in rules ) { - const matcher = createWildcardMatcher( ruleName ); - - if ( matcher( elementName ) ) { - yield* rules[ ruleName ]; - } - } - } -} - -function createWildcardMatcher( rule ) { - const matcher = new RegExp( '^' + rule.split( '*' ).map( escapeRegExp ).join( '.*' ) + '$' ); - return value => matcher.test( value ); -} - -function encodeView( name ) { return DATA_SCHEMA_PREFIX + capitalize( name ); } -function encodeAttributeKey( name ) { - return DATA_SCHEMA_PREFIX + '-' + name; -} +function getElementNameMatchingRegExp( value ) { + if ( value instanceof RegExp ) { + return value; + } -function decodeAttributeKey( name ) { - return name.replace( DATA_SCHEMA_PREFIX + '-', '' ); + return new RegExp( escapeRegExp( value ) ); } From 8b1112b00ff6ee7a8fc449cd36342394c62df6ad Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 4 Feb 2021 13:04:57 +0100 Subject: [PATCH 005/217] Refactoring. --- .../src/dataschema.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 880ce931717..026d3ef7710 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -52,18 +52,19 @@ export default class DataSchema { const matchElement = getElementNameMatchingRegExp( config.name ); - for ( const elementName in dtd ) { - if ( !matchElement.test( elementName ) ) { + for ( const viewName in dtd ) { + if ( !matchElement.test( viewName ) ) { continue; } - this._defineSchema( elementName ); - this._defineConverters( elementName ); - this._getOrCreateMatcher( elementName ).add( config ); + this._defineModel( viewName ); + + const matcher = this._getMatcher( viewName ); + matcher.add( config ); } } - _getOrCreateMatcher( elementName ) { + _getMatcher( elementName ) { if ( !this.allowedContent[ elementName ] ) { this.allowedContent[ elementName ] = new Matcher(); } @@ -71,21 +72,26 @@ export default class DataSchema { return this.allowedContent[ elementName ]; } - _defineSchema( viewName ) { + _defineModel( viewName ) { const schema = this.editor.model.schema; const modelName = encodeView( viewName ); + if ( schema.isRegistered( modelName ) ) { + return; + } + schema.register( modelName, dtd[ viewName ] ); + + this._defineConverters( viewName, modelName ); } - _defineConverters( viewName ) { + _defineConverters( viewName, modelName ) { const conversion = this.editor.conversion; - const modelName = encodeView( viewName ); conversion.for( 'upcast' ).elementToElement( { view: viewName, model: ( viewElement, conversionApi ) => { - const match = this._getOrCreateMatcher( viewName ).match( viewElement ); + const match = this._getMatcher( viewName ).match( viewElement ); const attributeMatch = match.match.attributes || []; const originalAttributes = attributeMatch.map( attributeName => { From da9f31d5d58cb8aef2f1e718ef95c6d2a6701ed6 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 10 Feb 2021 09:21:13 +0100 Subject: [PATCH 006/217] Refactored data schema definitions. --- .../src/dataschema.js | 367 ++++++++++++++---- .../src/generalhtmlsupport.js | 58 +++ .../tests/dataschema.js | 262 +++++++++++++ .../tests/manual/generalhtmlsupport.html | 11 + .../tests/manual/generalhtmlsupport.js | 46 +++ .../tests/manual/generalhtmlsupport.md | 1 + 6 files changed, 675 insertions(+), 70 deletions(-) create mode 100644 packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js create mode 100644 packages/ckeditor5-content-compatibility/tests/dataschema.js create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 026d3ef7710..8082fce08c8 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -7,100 +7,206 @@ * @module content-compatibility/dataschema */ -import { capitalize, escapeRegExp } from 'lodash-es'; +import { escapeRegExp, cloneDeep, uniq } from 'lodash-es'; import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher'; +import toArray from '@ckeditor/ckeditor5-utils/src/toarray'; -const DATA_SCHEMA_PREFIX = 'ghs'; const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; -// "h1", "h2", "h3", "h4", "h5", "h6", "legend", "pre", "rp", "rt", "summary", "p" -// legend, rp, rt, summary - -// Phrasing elements. -const P = encodeView( [ 'a', 'em', 'strong', 'small', 'abbr', 'dfn', 'i', 'b', 's', 'u', 'code', - 'var', 'samp', 'kbd', 'sup', 'sub', 'q', 'cite', 'span', 'bdo', 'bdi', 'br', - 'wbr', 'ins', 'del', 'img', 'embed', 'object', 'iframe', 'map', 'area', 'script', - 'noscript', 'ruby', 'video', 'audio', 'input', 'textarea', 'select', 'button', - 'label', 'output', 'keygen', 'progress', 'command', 'canvas', 'time', 'meter', 'detalist' ] ); - -// Flow elements. -const F = encodeView( [ 'a', 'p', 'hr', 'pre', 'ul', 'ol', 'dl', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'hgroup', 'address', 'blockquote', 'ins', 'del', 'object', 'map', 'noscript', 'section', - 'nav', 'article', 'aside', 'header', 'footer', 'video', 'audio', 'figure', 'table', - 'form', 'fieldset', 'menu', 'canvas', 'details' ] ); - -const dtd = { - section: { inheritAllFrom: '$block', allowContentOf: P, allowIn: F, allowAttributes: [ DATA_SCHEMA_ATTRIBUTE_KEY ] }, - article: { inheritAllFrom: '$block', allowContentOf: P, allowIn: F, allowAttributes: [ DATA_SCHEMA_ATTRIBUTE_KEY ] } -}; - -function remove( source, toRemove ) { - return source.filter( item => !toRemove.includes( item ) ); -} - +/** + * Holds representation of the extended HTML document type definitions to be used by the + * editor in content compatibility support. + * + * Allows to validate additional elements and element attributes using declarative data schema API. + * + * Data schema is represented by registered definitions. To register new definition, use {@link #register} method: + * + * dataSchema.register( { + * view: 'section', + * model: 'my-section', + * schema: '$block' + * } ); + * + * Note that the above code will only register new data schema definition describing document structure + * for the given element. + * + * To enable registered element in the editor, use {@link #allowElement} + * method: + * + * dataSchema.allowElement( { + * name: 'section' + * } ); + * + * Additionaly, you can allow or disallow specific element attributes: + * + * // Allow `data-foo` attribute on `section` element. + * dataSchema.allowedAttributes( { + * name: 'section', + * attributes: { + * 'data-foo': true + * } + * } ); + * + * // Disallow `color` style attribute on 'section' element. + * dataSchema.disallowedAttributes( { + * name: 'section', + * styles: { + * color: /[^]/ + * } + * } ); + */ export default class DataSchema { constructor( editor ) { this.editor = editor; - this.allowedContent = {}; + this._definitions = {}; + + this._viewMatchers = { + allowedAttributes: {}, + disallowedAttributes: {} + }; } - allow( config ) { - if ( !config.name ) { - return; + /** + * Allow the given element registered by {@link #register} method. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements which should be allowed. + */ + allowElement( config ) { + for ( const definition of this._getDefinitions( config.name ) ) { + this._defineSchema( definition ); + this._defineConverters( definition ); + + this.allowAttributes( config ); } + } - const matchElement = getElementNameMatchingRegExp( config.name ); + /** + * Allow the given attributes for view element allowed by {@link #allowElement} method. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be allowed. + */ + allowAttributes( config ) { + this._addAttributeMatcher( config, this._viewMatchers.allowedAttributes ); + } - for ( const viewName in dtd ) { - if ( !matchElement.test( viewName ) ) { - continue; - } + /** + * Disallowe the given attributes for view element allowed by {@link #allowElement} method. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be disallowed. + */ + disallowAttributes( config ) { + this._addAttributeMatcher( config, this._viewMatchers.disallowedAttributes ); + } - this._defineModel( viewName ); + /** + * Register new data schema definition. + * + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + */ + register( definition ) { + this._definitions[ definition.view ] = definition; + } - const matcher = this._getMatcher( viewName ); - matcher.add( config ); + /** + * Adds attribute matcher for every registered data schema definition for the given `config.name`. + * + * @private + * @param {module:engine/view/matcher~MatcherPattern} pattern + * @param {Object} cache Cache object holding matchers. + */ + _addAttributeMatcher( config, cache ) { + const elementName = config.name; + + config = cloneDeep( config ); + // We don't want match by name when matching attributes. + // Matcher will be already attached to specific definition. + delete config.name; + + // 1 to 1 relation between matcher and definition. Single matcher + // can be extended by adding additional configs. + for ( const definition of this._getDefinitions( elementName ) ) { + getOrCreateMatcher( definition.view, cache ).add( config ); } } - _getMatcher( elementName ) { - if ( !this.allowedContent[ elementName ] ) { - this.allowedContent[ elementName ] = new Matcher(); + /** + * Returns registered data schema definitions for the given view name. + * + * @private + * @param {String|RegExp} name View name registered by {@link #register} method. + * @returns {Iterable.} + */ + * _getDefinitions( name ) { + // Match everything if name not given. + if ( !name ) { + name = /[^]/; } - return this.allowedContent[ elementName ]; + if ( !( name instanceof RegExp ) ) { + name = new RegExp( escapeRegExp( name ) ); + } + + for ( const viewName in this._definitions ) { + if ( name.test( viewName ) ) { + yield this._definitions[ viewName ]; + } + } } - _defineModel( viewName ) { + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} + */ + _defineSchema( definition ) { const schema = this.editor.model.schema; - const modelName = encodeView( viewName ); - if ( schema.isRegistered( modelName ) ) { - return; + if ( typeof definition.schema === 'string' ) { + schema.register( definition.model, { + inheritAllFrom: definition.schema, + allowAttributes: DATA_SCHEMA_ATTRIBUTE_KEY + } ); + } else { + schema.register( definition.model, definition.schema ); } - - schema.register( modelName, dtd[ viewName ] ); - - this._defineConverters( viewName, modelName ); } - _defineConverters( viewName, modelName ) { + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} + */ + _defineConverters( definition ) { const conversion = this.editor.conversion; + const viewName = definition.view; + const modelName = definition.model; conversion.for( 'upcast' ).elementToElement( { view: viewName, model: ( viewElement, conversionApi ) => { - const match = this._getMatcher( viewName ).match( viewElement ); - const attributeMatch = match.match.attributes || []; + const viewAttributes = []; + const allowedAttributes = this._getAllowedAttributes( viewElement ); - const originalAttributes = attributeMatch.map( attributeName => { - return [ attributeName, viewElement.getAttribute( attributeName ) ]; - } ); + for ( const key of allowedAttributes.attributes ) { + viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); + } + + if ( allowedAttributes.classes.length ) { + viewAttributes.push( [ 'class', allowedAttributes.classes ] ); + } + + if ( allowedAttributes.styles.length ) { + const stylesObj = {}; + + for ( const styleName of allowedAttributes.styles ) { + stylesObj[ styleName ] = viewElement.getStyle( styleName ); + } + + viewAttributes.push( [ 'style', stylesObj ] ); + } let attributesToAdd; - if ( originalAttributes.length ) { - attributesToAdd = [ [ DATA_SCHEMA_ATTRIBUTE_KEY, originalAttributes ] ]; + if ( viewAttributes.length ) { + attributesToAdd = [ [ DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes ] ]; } return conversionApi.writer.createElement( modelName, attributesToAdd ); @@ -125,30 +231,151 @@ export default class DataSchema { const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement( data.item ); + // Remove old values. + if ( data.attributeOldValue !== null ) { + data.attributeOldValue.forEach( ( [ key, value ] ) => { + if ( key === 'class' ) { + const classes = toArray( value ); + + for ( const className of classes ) { + viewWriter.removeClass( className, viewElement ); + } + } else if ( key === 'style' ) { + for ( const key in value ) { + viewWriter.removeStyle( key, viewElement ); + } + } else { + viewWriter.removeAttribute( key, viewElement ); + } + } ); + } + + // Apply new values. if ( data.attributeNewValue !== null ) { data.attributeNewValue.forEach( ( [ key, value ] ) => { - viewWriter.setAttribute( key, value, viewElement ); + if ( key === 'class' ) { + const classes = toArray( value ); + + for ( const className of classes ) { + viewWriter.addClass( className, viewElement ); + } + } else if ( key === 'style' ) { + for ( const key in value ) { + viewWriter.setStyle( key, value[ key ], viewElement ); + } + } else { + viewWriter.setAttribute( key, value, viewElement ); + } } ); } - - viewWriter.removeAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewElement ); } ); } ); } + + /** + * Returns all allowed view element attributes based on attribute matchers registered via {@link #allowedAttributes} + * and {@link #disallowedAttributes} methods. + * + * @private + * @param {module:engine/view/element~Element} viewElement + * @returns {Object} result + * @returns {Array} result.attributes Array with matched attribute names. + * @returns {Array} result.classes Array with matched class names. + * @returns {Array} result.styles Array with matched style names. + */ + _getAllowedAttributes( viewElement ) { + const allowedAttributes = matchAll( viewElement, this._viewMatchers.allowedAttributes ); + const disallowedAttributes = matchAll( viewElement, this._viewMatchers.disallowedAttributes ); + + // Drop disallowed content. + for ( const key in allowedAttributes ) { + allowedAttributes[ key ] = removeArray( allowedAttributes[ key ], disallowedAttributes[ key ] ); + } + + return allowedAttributes; + } } -function encodeView( name ) { - if ( Array.isArray( name ) ) { - return name.map( encodeView ); +/** + * Helper function restoring matcher for the given key from cache object. + * + * If matcher for the given key does not exist, this function will create a new one + * inside cache object under the given key. + * + * @private + * @param {String} key + * @param {Object} cache + */ +function getOrCreateMatcher( key, cache ) { + if ( !cache[ key ] ) { + cache[ key ] = new Matcher(); } - return DATA_SCHEMA_PREFIX + capitalize( name ); + return cache[ key ]; +} + +/** + * Helper function matching all attributes for the given element. + * + * @private + * @param {module:engine/view/element~Element} viewElement + * @param {Object} cache Cache object holding matchers. + * @returns {Object} result + * @returns {Array} result.attributes Array with matched attribute names. + * @returns {Array} result.classes Array with matched class names. + * @returns {Array} result.styles Array with matched style names. + */ +function matchAll( viewElement, cache ) { + const matcher = getOrCreateMatcher( viewElement.name, cache ); + const matches = matcher.matchAll( viewElement ); + + return mergeMatchAllResult( matches || [] ); } -function getElementNameMatchingRegExp( value ) { - if ( value instanceof RegExp ) { - return value; +/** + * Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. + * + * @private + * @param {Array} matches + * @returns {Object} result + * @returns {Array} result.attributes Array with matched attribute names. + * @returns {Array} result.classes Array with matched class names. + * @returns {Array} result.styles Array with matched style names. + */ +function mergeMatchAllResult( matches ) { + const matchResult = { attributes: [], classes: [], styles: [] }; + + for ( const match of matches ) { + for ( const key in matchResult ) { + const values = match.match[ key ] || []; + matchResult[ key ].push( ...values ); + } } - return new RegExp( escapeRegExp( value ) ); + for ( const key in matchResult ) { + matchResult[ key ] = uniq( matchResult[ key ] ); + } + + return matchResult; } + +/** + * Removes array items included in the second array parameter. + * + * @param {Array} array + * @param {Array} toRemove + * @returns {Array} Filtered array items. + */ +function removeArray( array, toRemove ) { + return array.filter( item => !toRemove.includes( item ) ); +} + +/** + * A definition of {@link module:content-compatibility/dataschema data schema}. + * + * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition + * @property {String} view Name of the view element. + * @property {String} model Name of the model element. + * @property {String|module:engine/model/schema~SchemaItemDefinition} schema Name of the schema to inherit + * or custom schema item definition. + */ diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js new file mode 100644 index 00000000000..4cfd3d7b730 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -0,0 +1,58 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module content-compatibility/generalhtmlsupport + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import DataSchema from './dataschema'; + +/** + * @extends module:core/plugin~Plugin + */ +export default class GeneralHTMLSupport extends Plugin { + constructor( editor ) { + super( editor ); + + this.dataSchema = new DataSchema( editor ); + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'GeneralHTMLSupport'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const dataSchema = this.dataSchema; + + // Register block elements. + editor.model.schema.register( '$ghsBlock', { + inheritAllFrom: '$block', + allowIn: '$ghsBlock' + } ); + + dataSchema.register( { view: 'article', model: 'ghsArticle', schema: '$ghsBlock' } ); + dataSchema.register( { view: 'section', model: 'ghsSection', schema: '$ghsBlock' } ); + dataSchema.register( { view: 'dl', model: 'ghsDl', schema: '$ghsBlock' } ); + + // Register data list elements. + editor.model.schema.register( '$ghsDatalist', { + allowIn: 'ghsDl', + isBlock: true, + allowContentOf: '$ghsBlock' + } ); + editor.model.schema.extend( '$text', { allowIn: '$ghsDatalist' } ); + + dataSchema.register( { view: 'dt', model: 'ghsDt', schema: '$ghsDatalist' } ); + dataSchema.register( { view: 'dd', model: 'ghsDd', schema: '$ghsDatalist' } ); + } +} diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js new file mode 100644 index 00000000000..e91dfcb1e4f --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -0,0 +1,262 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import DataSchema from '../src/dataschema'; + +import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'DataSchema', () => { + let editor, model, dataSchema; + + beforeEach( () => { + return VirtualTestEditor + .create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + + dataSchema = new DataSchema( editor ); + + // Define some made up type definitions for testing purposes. + dataSchema.register( { view: 'div', model: 'ghsDiv', schema: { + inheritAllFrom: '$block', + allowIn: 'ghsDiv' + } } ); + dataSchema.register( { view: 'article', model: 'ghsArticle', schema: '$block' } ); + dataSchema.register( { view: 'section', model: 'ghsSection', schema: { + inheritAllFrom: '$block', + allowIn: 'ghsArticle' + } } ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should allow element', () => { + dataSchema.allowElement( { name: 'article' } ); + + editor.setData( '
section1
section2
' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.eq( + 'section1section2' + ); + + expect( editor.getData() ).to.eq( + '
section1section2
' + ); + + dataSchema.allowElement( { name: 'section' } ); + + editor.setData( '
section1
section2
' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.eq( + 'section1section2' + ); + + expect( editor.getData() ).to.eq( + '
section1
section2
' + ); + } ); + + it( 'should allow deeply nested structure', () => { + dataSchema.allowElement( { name: 'div' } ); + + editor.setData( '
1
2
3
' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.eq( + '123' + ); + + expect( editor.getData() ).to.eq( + '
1
2
3
' + ); + } ); + + it( 'should allow attributes', () => { + dataSchema.allowElement( { name: 'div' } ); + dataSchema.allowAttributes( { name: 'div', attributes: { + 'data-foo': 'foobar' + } } ); + + editor.setData( '
foobar
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: 'foobar', + attributes: { + 1: [ [ 'data-foo', 'foobar' ] ] + } + } ); + + expect( editor.getData() ).to.eq( + '
foobar
' + ); + } ); + + it( 'should allow attributes (styles)', () => { + dataSchema.allowElement( { name: 'div' } ); + dataSchema.allowAttributes( { name: 'div', styles: { + 'color': 'red' + } } ); + + editor.setData( '
foobar
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: 'foobar', + attributes: { + 1: [ [ 'style', { color: 'red' } ] ] + } + } ); + + expect( editor.getData() ).to.eq( + '
foobar
' + ); + } ); + + it( 'should allow attributes (classes)', () => { + dataSchema.allowElement( { name: 'div' } ); + dataSchema.allowAttributes( { name: 'div', classes: [ 'foo', 'bar' ] } ); + + editor.setData( '
foobar
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: 'foobar', + attributes: { + 1: [ [ 'class', [ 'foo', 'bar' ] ] ] + } + } ); + + expect( editor.getData() ).to.eq( + '
foobar
' + ); + } ); + + it( 'should allow nested attributes', () => { + dataSchema.allowElement( { name: /article|section/ } ); + dataSchema.allowAttributes( { attributes: { 'data-foo': /foo|bar/ } } ); + + editor.setData( '
' + + '
section1' + + '
section2
' + + '
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: '' + + 'section1' + + 'section2' + + '', + attributes: { + 1: [ [ 'data-foo', 'foo' ] ], + 2: [ [ 'data-foo', 'bar' ] ], + 3: [ [ 'data-foo', 'foo' ] ] + } + } ); + } ); + + it( 'should allow attributes for all allowed definitions', () => { + dataSchema.allowElement( { name: 'div' } ); + dataSchema.allowElement( { name: 'article' } ); + // We skip name purposely to allow attribute on every data schema element. + dataSchema.allowAttributes( { attributes: { 'data-foo': 'foo' } } ); + dataSchema.allowAttributes( { attributes: { 'data-bar': 'bar' } } ); + + editor.setData( '
foo
bar
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: 'foobar', + attributes: { + 1: [ [ 'data-foo', 'foo' ] ], + 2: [ [ 'data-bar', 'bar' ] ] + } + } ); + + expect( editor.getData() ).to.eq( + '
foo
bar
' + ); + } ); + + it( 'should disallow attributes', () => { + dataSchema.allowElement( { name: 'div' } ); + dataSchema.allowAttributes( { name: 'div', attributes: { 'data-foo': /[^]/ } } ); + dataSchema.disallowAttributes( { name: 'div', attributes: { 'data-foo': 'bar' } } ); + + editor.setData( '
foo
bar
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: 'foobar', + attributes: { + 1: [ [ 'data-foo', 'foo' ] ] + } + } ); + + expect( editor.getData() ).to.eq( + '
foo
bar
' + ); + } ); + + it( 'should disallow attributes (styles)', () => { + dataSchema.allowElement( { name: 'div' } ); + dataSchema.allowAttributes( { name: 'div', styles: { color: /[^]/ } } ); + dataSchema.disallowAttributes( { name: 'div', styles: { color: 'red' } } ); + + editor.setData( '
foo
bar
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: 'foobar', + attributes: { + 1: [ [ 'style', { color: 'blue' } ] ] + } + } ); + + expect( editor.getData() ).to.eq( + '
foo
bar
' + ); + } ); + + it( 'should disallow attributes (classes)', () => { + dataSchema.allowElement( { name: 'div' } ); + dataSchema.allowAttributes( { name: 'div', classes: [ 'foo', 'bar' ] } ); + dataSchema.disallowAttributes( { name: 'div', classes: [ 'bar' ] } ); + + editor.setData( '
foo
bar
' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + data: 'foobar', + attributes: { + 1: [ [ 'class', [ 'foo' ] ] ] + } + } ); + + expect( editor.getData() ).to.eq( + '
foo
bar
' + ); + } ); +} ); + +function getModelDataWithAttributes( model, options ) { + // Simplify GHS attributes as they are not readable at this point due to complex structure. + let counter = 1; + const data = getModelData( model, options ).replace( /ghsAttributes="(.*?)"/g, () => { + return `ghsAttributes="(${ counter++ })"`; + } ); + + const range = model.createRangeIn( model.document.getRoot() ); + + let attributes = []; + for ( const item of range.getItems() ) { + if ( item.hasAttribute && item.hasAttribute( 'ghsAttributes' ) ) { + attributes.push( item.getAttribute( 'ghsAttributes' ) ); + } + } + + attributes = attributes.reduce( ( prev, cur, index ) => { + prev[ index + 1 ] = cur; + return prev; + }, {} ); + + return { data, attributes }; +} diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html new file mode 100644 index 00000000000..230a06bc528 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -0,0 +1,11 @@ + + + + +
+
+
Section #1
+
Section #2
+
Section #3
+
+
diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js new file mode 100644 index 00000000000..8987048db4e --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -0,0 +1,46 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console:false, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +import GeneralHTMLSupport from '../../src/generalhtmlsupport'; + +class ExtendHTMLSupport extends Plugin { + static get requires() { + return [ GeneralHTMLSupport ]; + } + + init() { + const dataSchema = this.editor.plugins.get( 'GeneralHTMLSupport' ).dataSchema; + + dataSchema.allowElement( { name: 'article' } ); + dataSchema.allowElement( { name: 'section' } ); + + dataSchema.allowAttributes( { name: 'section', attributes: { id: /[^]/ } } ); + dataSchema.allowAttributes( { name: 'section', classes: /[^]/ } ); + dataSchema.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); + } +} + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Essentials, Paragraph, Bold, Italic, Strikethrough, GeneralHTMLSupport, ExtendHTMLSupport ], + toolbar: [ 'bold', 'italic', 'strikethrough', 'contentTags' ] + } ) + .then( editor => { + window.editor = editor; + window.dataSchema = editor.plugins.get( 'GeneralHTMLSupport' ).dataSchema; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md new file mode 100644 index 00000000000..4f09838d7e4 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md @@ -0,0 +1 @@ +# Content compatibility From 1d74150263b164e85d663c59050986735e7445e7 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 10 Feb 2021 10:20:51 +0100 Subject: [PATCH 007/217] Extended support for details. --- .../ckeditor5-content-compatibility/src/dataschema.js | 7 ++++++- .../src/generalhtmlsupport.js | 9 +++++++++ .../tests/manual/generalhtmlsupport.html | 4 ++++ .../tests/manual/generalhtmlsupport.js | 5 +++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 8082fce08c8..707218ea94c 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -167,7 +167,12 @@ export default class DataSchema { allowAttributes: DATA_SCHEMA_ATTRIBUTE_KEY } ); } else { - schema.register( definition.model, definition.schema ); + const schemaDefinition = cloneDeep( definition.schema ); + + schemaDefinition.allowAttributes = toArray( schemaDefinition.allowAttributes || [] ); + schemaDefinition.allowAttributes.push( DATA_SCHEMA_ATTRIBUTE_KEY ); + + schema.register( definition.model, schemaDefinition ); } } diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index 4cfd3d7b730..80dcd5091ed 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -54,5 +54,14 @@ export default class GeneralHTMLSupport extends Plugin { dataSchema.register( { view: 'dt', model: 'ghsDt', schema: '$ghsDatalist' } ); dataSchema.register( { view: 'dd', model: 'ghsDd', schema: '$ghsDatalist' } ); + + // Register details elements. + dataSchema.register( { view: 'details', model: 'ghsDetails', schema: '$ghsBlock' } ); + + dataSchema.register( { view: 'summary', model: 'ghsSummary', schema: { + allowIn: 'ghsDetails' + } } ); + + editor.model.schema.extend( '$text', { allowIn: 'ghsSummary' } ); } } diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index 230a06bc528..e009bc8cc3e 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -8,4 +8,8 @@
Section #2
Section #3
+
+ Summary + Hello world +
diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 8987048db4e..fa0bc6119c8 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -23,12 +23,13 @@ class ExtendHTMLSupport extends Plugin { init() { const dataSchema = this.editor.plugins.get( 'GeneralHTMLSupport' ).dataSchema; - dataSchema.allowElement( { name: 'article' } ); - dataSchema.allowElement( { name: 'section' } ); + dataSchema.allowElement( { name: /article|section/ } ); dataSchema.allowAttributes( { name: 'section', attributes: { id: /[^]/ } } ); dataSchema.allowAttributes( { name: 'section', classes: /[^]/ } ); dataSchema.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); + + dataSchema.allowElement( { name: /details|summary/ } ); } } From 17826af804b544e390e579114ad0bfff06ae9936 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 11 Feb 2021 11:58:47 +0100 Subject: [PATCH 008/217] Fixed dl schema. --- .../src/generalhtmlsupport.js | 6 +++++- .../tests/manual/generalhtmlsupport.html | 6 ++++++ .../tests/manual/generalhtmlsupport.js | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index 80dcd5091ed..31ebec34dae 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -42,9 +42,13 @@ export default class GeneralHTMLSupport extends Plugin { dataSchema.register( { view: 'article', model: 'ghsArticle', schema: '$ghsBlock' } ); dataSchema.register( { view: 'section', model: 'ghsSection', schema: '$ghsBlock' } ); - dataSchema.register( { view: 'dl', model: 'ghsDl', schema: '$ghsBlock' } ); // Register data list elements. + dataSchema.register( { view: 'dl', model: 'ghsDl', schema: { + allowIn: [ '$ghsBlock', '$root' ], + isBlock: true + } } ); + editor.model.schema.register( '$ghsDatalist', { allowIn: 'ghsDl', isBlock: true, diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index e009bc8cc3e..6a0960f8c3e 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -12,4 +12,10 @@ Summary Hello world +
+
dt1
+
dt2
+
dd1
+
dd2
+
diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index fa0bc6119c8..abf7d1f8e3d 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -30,6 +30,8 @@ class ExtendHTMLSupport extends Plugin { dataSchema.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); dataSchema.allowElement( { name: /details|summary/ } ); + + dataSchema.allowElement( { name: /dl|dt|dd/ } ); } } From f54b0706da670d840dea5c16d914a05748685b37 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 16 Feb 2021 10:14:14 +0100 Subject: [PATCH 009/217] Break model into data schema and data filter. --- .../src/datafilter.js | 325 +++++++++++++++ .../src/dataschema.js | 386 ++++-------------- .../src/generalhtmlsupport.js | 71 ---- .../tests/{dataschema.js => datafilter.js} | 117 +++--- .../tests/manual/generalhtmlsupport.js | 25 +- 5 files changed, 461 insertions(+), 463 deletions(-) create mode 100644 packages/ckeditor5-content-compatibility/src/datafilter.js delete mode 100644 packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js rename packages/ckeditor5-content-compatibility/tests/{dataschema.js => datafilter.js} (57%) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js new file mode 100644 index 00000000000..ee4711eec10 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -0,0 +1,325 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module content-compatibility/datafilter + */ + +import { escapeRegExp, cloneDeep, uniq } from 'lodash-es'; +import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher'; +import toArray from '@ckeditor/ckeditor5-utils/src/toarray'; + +import DataSchema from './dataschema'; + +const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; + +/** + * Allows to validate elements and element attributes registered by {@link @module content-compatibility/dataschema~DataSchema}. + * + * To enable registered element in the editor, use {@link #allowElement} method: + * + * dataFilter.allowElement( { + * name: 'section' + * } ); + * + * You can also allow or disallow specific element attributes: + * + * // Allow `data-foo` attribute on `section` element. + * dataFilter.allowedAttributes( { + * name: 'section', + * attributes: { + * 'data-foo': true + * } + * } ); + * + * // Disallow `color` style attribute on 'section' element. + * dataFilter.disallowedAttributes( { + * name: 'section', + * styles: { + * color: /[^]/ + * } + * } ); + */ +export default class DataFilter { + constructor( editor ) { + this.editor = editor; + + this.dataSchema = new DataSchema( editor ); + + this._viewMatchers = { + allowedAttributes: {}, + disallowedAttributes: {} + }; + } + + /** + * Allow the given element registered by {@link #register} method. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements which should be allowed. + */ + allowElement( config ) { + const nameRegExp = toRegExp( config.name ); + + for ( const mapping of this.dataSchema.getModelViewMapping() ) { + if ( nameRegExp.test( mapping.view ) ) { + this.dataSchema.enable( mapping.model ); + this._defineConverters( mapping ); + } + } + + this.allowAttributes( config ); + } + + /** + * Allow the given attributes for view element allowed by {@link #allowElement} method. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be allowed. + */ + allowAttributes( config ) { + this._addAttributeMatcher( config, this._viewMatchers.allowedAttributes ); + } + + /** + * Disallowe the given attributes for view element allowed by {@link #allowElement} method. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be disallowed. + */ + disallowAttributes( config ) { + this._addAttributeMatcher( config, this._viewMatchers.disallowedAttributes ); + } + + /** + * Adds attribute matcher for every registered data schema definition for the given `config.name`. + * + * @private + * @param {module:engine/view/matcher~MatcherPattern} pattern + * @param {Object} cache Cache object holding matchers. + */ + _addAttributeMatcher( config, cache ) { + const nameRegExp = toRegExp( config.name ); + + config = cloneDeep( config ); + // We don't want match by name when matching attributes. + // Matcher will be already attached to specific definition. + delete config.name; + + for ( const { view } of this.dataSchema.getModelViewMapping() ) { + if ( nameRegExp.test( view ) ) { + getOrCreateMatcher( view, cache ).add( config ); + } + } + } + + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} + */ + _defineConverters( definition ) { + const conversion = this.editor.conversion; + const viewName = definition.view; + const modelName = definition.model; + + conversion.for( 'upcast' ).elementToElement( { + view: viewName, + model: ( viewElement, conversionApi ) => { + const viewAttributes = []; + const allowedAttributes = this._getAllowedAttributes( viewElement ); + + for ( const key of allowedAttributes.attributes ) { + viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); + } + + if ( allowedAttributes.classes.length ) { + viewAttributes.push( [ 'class', allowedAttributes.classes ] ); + } + + if ( allowedAttributes.styles.length ) { + const stylesObj = {}; + + for ( const styleName of allowedAttributes.styles ) { + stylesObj[ styleName ] = viewElement.getStyle( styleName ); + } + + viewAttributes.push( [ 'style', stylesObj ] ); + } + + let attributesToAdd; + if ( viewAttributes.length ) { + attributesToAdd = [ [ DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes ] ]; + } + + return conversionApi.writer.createElement( modelName, attributesToAdd ); + } + } ); + + conversion.for( 'downcast' ).elementToElement( { + model: modelName, + view: viewName + } ); + + conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }`, ( evt, data, conversionApi ) => { + if ( data.item.name != modelName ) { + return; + } + + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewElement = conversionApi.mapper.toViewElement( data.item ); + + // Remove old values. + if ( data.attributeOldValue !== null ) { + data.attributeOldValue.forEach( ( [ key, value ] ) => { + if ( key === 'class' ) { + const classes = toArray( value ); + + for ( const className of classes ) { + viewWriter.removeClass( className, viewElement ); + } + } else if ( key === 'style' ) { + for ( const key in value ) { + viewWriter.removeStyle( key, viewElement ); + } + } else { + viewWriter.removeAttribute( key, viewElement ); + } + } ); + } + + // Apply new values. + if ( data.attributeNewValue !== null ) { + data.attributeNewValue.forEach( ( [ key, value ] ) => { + if ( key === 'class' ) { + const classes = toArray( value ); + + for ( const className of classes ) { + viewWriter.addClass( className, viewElement ); + } + } else if ( key === 'style' ) { + for ( const key in value ) { + viewWriter.setStyle( key, value[ key ], viewElement ); + } + } else { + viewWriter.setAttribute( key, value, viewElement ); + } + } ); + } + } ); + } ); + } + + /** + * Returns all allowed view element attributes based on attribute matchers registered via {@link #allowedAttributes} + * and {@link #disallowedAttributes} methods. + * + * @private + * @param {module:engine/view/element~Element} viewElement + * @returns {Object} result + * @returns {Array} result.attributes Array with matched attribute names. + * @returns {Array} result.classes Array with matched class names. + * @returns {Array} result.styles Array with matched style names. + */ + _getAllowedAttributes( viewElement ) { + const allowedAttributes = matchAll( viewElement, this._viewMatchers.allowedAttributes ); + const disallowedAttributes = matchAll( viewElement, this._viewMatchers.disallowedAttributes ); + + // Drop disallowed content. + for ( const key in allowedAttributes ) { + allowedAttributes[ key ] = removeArray( allowedAttributes[ key ], disallowedAttributes[ key ] ); + } + + return allowedAttributes; + } +} + +/** + * Helper function restoring matcher for the given key from cache object. + * + * If matcher for the given key does not exist, this function will create a new one + * inside cache object under the given key. + * + * @private + * @param {String} key + * @param {Object} cache + */ +function getOrCreateMatcher( key, cache ) { + if ( !cache[ key ] ) { + cache[ key ] = new Matcher(); + } + + return cache[ key ]; +} + +/** + * Helper function matching all attributes for the given element. + * + * @private + * @param {module:engine/view/element~Element} viewElement + * @param {Object} cache Cache object holding matchers. + * @returns {Object} result + * @returns {Array} result.attributes Array with matched attribute names. + * @returns {Array} result.classes Array with matched class names. + * @returns {Array} result.styles Array with matched style names. + */ +function matchAll( viewElement, cache ) { + const matcher = getOrCreateMatcher( viewElement.name, cache ); + const matches = matcher.matchAll( viewElement ); + + return mergeMatchAllResult( matches || [] ); +} + +/** + * Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. + * + * @private + * @param {Array} matches + * @returns {Object} result + * @returns {Array} result.attributes Array with matched attribute names. + * @returns {Array} result.classes Array with matched class names. + * @returns {Array} result.styles Array with matched style names. + */ +function mergeMatchAllResult( matches ) { + const matchResult = { attributes: [], classes: [], styles: [] }; + + for ( const match of matches ) { + for ( const key in matchResult ) { + const values = match.match[ key ] || []; + matchResult[ key ].push( ...values ); + } + } + + for ( const key in matchResult ) { + matchResult[ key ] = uniq( matchResult[ key ] ); + } + + return matchResult; +} + +/** + * Removes array items included in the second array parameter. + * + * @param {Array} array + * @param {Array} toRemove + * @returns {Array} Filtered array items. + */ +function removeArray( array, toRemove ) { + return array.filter( item => !toRemove.includes( item ) ); +} + +function toRegExp( value ) { + // Match everything if name not given. + if ( !value ) { + value = /[^]/; + } + + if ( !( value instanceof RegExp ) ) { + value = new RegExp( escapeRegExp( value ) ); + } + + return value; +} diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 707218ea94c..6cb7e4dd786 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -7,53 +7,22 @@ * @module content-compatibility/dataschema */ -import { escapeRegExp, cloneDeep, uniq } from 'lodash-es'; -import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher'; +import { cloneDeep } from 'lodash-es'; import toArray from '@ckeditor/ckeditor5-utils/src/toarray'; -const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; - /** * Holds representation of the extended HTML document type definitions to be used by the * editor in content compatibility support. * - * Allows to validate additional elements and element attributes using declarative data schema API. - * - * Data schema is represented by registered definitions. To register new definition, use {@link #register} method: + * Data schema is represented by data schema definitions. To add new definition, use {@link #register} method: * - * dataSchema.register( { - * view: 'section', - * model: 'my-section', - * schema: '$block' + * dataSchema.register( { view: 'section', model: 'my-section' }, { + * inheritAllFrom: '$block' * } ); * - * Note that the above code will only register new data schema definition describing document structure - * for the given element. + * Once registered, definition can be enabled in editor's model: * - * To enable registered element in the editor, use {@link #allowElement} - * method: - * - * dataSchema.allowElement( { - * name: 'section' - * } ); - * - * Additionaly, you can allow or disallow specific element attributes: - * - * // Allow `data-foo` attribute on `section` element. - * dataSchema.allowedAttributes( { - * name: 'section', - * attributes: { - * 'data-foo': true - * } - * } ); - * - * // Disallow `color` style attribute on 'section' element. - * dataSchema.disallowedAttributes( { - * name: 'section', - * styles: { - * color: /[^]/ - * } - * } ); + * dataSchema.enable( 'my-section' ); */ export default class DataSchema { constructor( editor ) { @@ -61,325 +30,114 @@ export default class DataSchema { this._definitions = {}; - this._viewMatchers = { - allowedAttributes: {}, - disallowedAttributes: {} - }; - } + // Add block elements. + this.register( { model: '$ghsBlock' }, { + inheritAllFrom: '$block', + allowIn: '$ghsBlock' + } ); - /** - * Allow the given element registered by {@link #register} method. - * - * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements which should be allowed. - */ - allowElement( config ) { - for ( const definition of this._getDefinitions( config.name ) ) { - this._defineSchema( definition ); - this._defineConverters( definition ); + this.register( { view: 'article', model: 'ghsArticle' }, { inheritAllFrom: '$ghsBlock' } ); + this.register( { view: 'section', model: 'ghsSection' }, { inheritAllFrom: '$ghsBlock' } ); - this.allowAttributes( config ); - } - } + // Add data list elements. + this.register( { view: 'dl', model: 'ghsDl' }, { + allowIn: [ '$ghsBlock', '$root' ], + isBlock: true + } ); - /** - * Allow the given attributes for view element allowed by {@link #allowElement} method. - * - * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be allowed. - */ - allowAttributes( config ) { - this._addAttributeMatcher( config, this._viewMatchers.allowedAttributes ); - } + this.register( { model: '$ghsDatalist' }, { + allowIn: 'ghsDl', + isBlock: true, + allowContentOf: '$ghsBlock', + allowText: true + } ); - /** - * Disallowe the given attributes for view element allowed by {@link #allowElement} method. - * - * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be disallowed. - */ - disallowAttributes( config ) { - this._addAttributeMatcher( config, this._viewMatchers.disallowedAttributes ); + this.register( { view: 'dt', model: 'ghsDt' }, { inheritAllFrom: '$ghsDatalist' } ); + this.register( { view: 'dd', model: 'ghsDd' }, { inheritAllFrom: '$ghsDatalist' } ); + + // Add details elements. + this.register( { view: 'details', model: 'ghsDetails' }, { inheritAllFrom: '$ghsBlock' } ); + + this.register( { view: 'summary', model: 'ghsSummary' }, { + allowIn: 'ghsDetails', + allowText: true + } ); } /** - * Register new data schema definition. + * Add new data schema definition. * * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ - register( definition ) { - this._definitions[ definition.view ] = definition; + register( { view, model }, schema ) { + this._definitions[ model ] = { view, model, schema }; } /** - * Adds attribute matcher for every registered data schema definition for the given `config.name`. + * Returns model-view-pairs for the added data schema definitions. * - * @private - * @param {module:engine/view/matcher~MatcherPattern} pattern - * @param {Object} cache Cache object holding matchers. - */ - _addAttributeMatcher( config, cache ) { - const elementName = config.name; - - config = cloneDeep( config ); - // We don't want match by name when matching attributes. - // Matcher will be already attached to specific definition. - delete config.name; - - // 1 to 1 relation between matcher and definition. Single matcher - // can be extended by adding additional configs. - for ( const definition of this._getDefinitions( elementName ) ) { - getOrCreateMatcher( definition.view, cache ).add( config ); - } - } - - /** - * Returns registered data schema definitions for the given view name. + * This method will only return pairs when if + * {@link module:content-compatibility/dataschema~DataSchemaDefinition#view view} has been set. * - * @private - * @param {String|RegExp} name View name registered by {@link #register} method. - * @returns {Iterable.} + * @returns {Object[]} result */ - * _getDefinitions( name ) { - // Match everything if name not given. - if ( !name ) { - name = /[^]/; - } - - if ( !( name instanceof RegExp ) ) { - name = new RegExp( escapeRegExp( name ) ); - } - - for ( const viewName in this._definitions ) { - if ( name.test( viewName ) ) { - yield this._definitions[ viewName ]; - } - } + getModelViewMapping() { + return Object.values( this._definitions ) + .filter( def => def.view && def.model ) + .map( def => ( { model: def.model, view: def.view } ) ); } /** - * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} + * Registers model schema item for the given + * {@link module:content-compatibility/dataschema~DataSchemaDefinition data schema definition} name. + * + * @param {String} name */ - _defineSchema( definition ) { + enable( name ) { const schema = this.editor.model.schema; - if ( typeof definition.schema === 'string' ) { - schema.register( definition.model, { - inheritAllFrom: definition.schema, - allowAttributes: DATA_SCHEMA_ATTRIBUTE_KEY - } ); - } else { - const schemaDefinition = cloneDeep( definition.schema ); - - schemaDefinition.allowAttributes = toArray( schemaDefinition.allowAttributes || [] ); - schemaDefinition.allowAttributes.push( DATA_SCHEMA_ATTRIBUTE_KEY ); - - schema.register( definition.model, schemaDefinition ); + if ( schema.isRegistered( name ) ) { + return; } - } - - /** - * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} - */ - _defineConverters( definition ) { - const conversion = this.editor.conversion; - const viewName = definition.view; - const modelName = definition.model; - - conversion.for( 'upcast' ).elementToElement( { - view: viewName, - model: ( viewElement, conversionApi ) => { - const viewAttributes = []; - const allowedAttributes = this._getAllowedAttributes( viewElement ); - - for ( const key of allowedAttributes.attributes ) { - viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); - } - - if ( allowedAttributes.classes.length ) { - viewAttributes.push( [ 'class', allowedAttributes.classes ] ); - } - - if ( allowedAttributes.styles.length ) { - const stylesObj = {}; - - for ( const styleName of allowedAttributes.styles ) { - stylesObj[ styleName ] = viewElement.getStyle( styleName ); - } - - viewAttributes.push( [ 'style', stylesObj ] ); - } - - let attributesToAdd; - if ( viewAttributes.length ) { - attributesToAdd = [ [ DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes ] ]; - } - - return conversionApi.writer.createElement( modelName, attributesToAdd ); - } - } ); - - conversion.for( 'downcast' ).elementToElement( { - model: modelName, - view: viewName - } ); - conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }`, ( evt, data, conversionApi ) => { - if ( data.item.name != modelName ) { - return; - } + const definition = this._definitions[ name ]; + const schemaDefinition = cloneDeep( definition.schema ); - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { - return; - } - - const viewWriter = conversionApi.writer; - const viewElement = conversionApi.mapper.toViewElement( data.item ); - - // Remove old values. - if ( data.attributeOldValue !== null ) { - data.attributeOldValue.forEach( ( [ key, value ] ) => { - if ( key === 'class' ) { - const classes = toArray( value ); - - for ( const className of classes ) { - viewWriter.removeClass( className, viewElement ); - } - } else if ( key === 'style' ) { - for ( const key in value ) { - viewWriter.removeStyle( key, viewElement ); - } - } else { - viewWriter.removeAttribute( key, viewElement ); - } - } ); - } + for ( const reference of this._getReferences( name ) ) { + this.enable( reference ); + } - // Apply new values. - if ( data.attributeNewValue !== null ) { - data.attributeNewValue.forEach( ( [ key, value ] ) => { - if ( key === 'class' ) { - const classes = toArray( value ); + schema.register( name, schemaDefinition ); - for ( const className of classes ) { - viewWriter.addClass( className, viewElement ); - } - } else if ( key === 'style' ) { - for ( const key in value ) { - viewWriter.setStyle( key, value[ key ], viewElement ); - } - } else { - viewWriter.setAttribute( key, value, viewElement ); - } - } ); - } - } ); - } ); + if ( schema.allowText ) { + schema.extend( '$text', { allowIn: name } ); + } } /** - * Returns all allowed view element attributes based on attribute matchers registered via {@link #allowedAttributes} - * and {@link #disallowedAttributes} methods. + * Resolves all model references registered for the given data schema definition. * * @private - * @param {module:engine/view/element~Element} viewElement - * @returns {Object} result - * @returns {Array} result.attributes Array with matched attribute names. - * @returns {Array} result.classes Array with matched class names. - * @returns {Array} result.styles Array with matched style names. + * @param {String} name Data schema model name. + * @returns {Iterable} */ - _getAllowedAttributes( viewElement ) { - const allowedAttributes = matchAll( viewElement, this._viewMatchers.allowedAttributes ); - const disallowedAttributes = matchAll( viewElement, this._viewMatchers.disallowedAttributes ); + * _getReferences( name ) { + // TODO extend with the rest of schema properties based on other model types. + const { schema } = this._definitions[ name ]; - // Drop disallowed content. - for ( const key in allowedAttributes ) { - allowedAttributes[ key ] = removeArray( allowedAttributes[ key ], disallowedAttributes[ key ] ); - } - - return allowedAttributes; - } -} - -/** - * Helper function restoring matcher for the given key from cache object. - * - * If matcher for the given key does not exist, this function will create a new one - * inside cache object under the given key. - * - * @private - * @param {String} key - * @param {Object} cache - */ -function getOrCreateMatcher( key, cache ) { - if ( !cache[ key ] ) { - cache[ key ] = new Matcher(); - } - - return cache[ key ]; -} - -/** - * Helper function matching all attributes for the given element. - * - * @private - * @param {module:engine/view/element~Element} viewElement - * @param {Object} cache Cache object holding matchers. - * @returns {Object} result - * @returns {Array} result.attributes Array with matched attribute names. - * @returns {Array} result.classes Array with matched class names. - * @returns {Array} result.styles Array with matched style names. - */ -function matchAll( viewElement, cache ) { - const matcher = getOrCreateMatcher( viewElement.name, cache ); - const matches = matcher.matchAll( viewElement ); - - return mergeMatchAllResult( matches || [] ); -} - -/** - * Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. - * - * @private - * @param {Array} matches - * @returns {Object} result - * @returns {Array} result.attributes Array with matched attribute names. - * @returns {Array} result.classes Array with matched class names. - * @returns {Array} result.styles Array with matched style names. - */ -function mergeMatchAllResult( matches ) { - const matchResult = { attributes: [], classes: [], styles: [] }; - - for ( const match of matches ) { - for ( const key in matchResult ) { - const values = match.match[ key ] || []; - matchResult[ key ].push( ...values ); + for ( const model of toArray( schema.inheritAllFrom || [] ) ) { + if ( this._definitions[ model ] ) { + yield model; + } } } - - for ( const key in matchResult ) { - matchResult[ key ] = uniq( matchResult[ key ] ); - } - - return matchResult; -} - -/** - * Removes array items included in the second array parameter. - * - * @param {Array} array - * @param {Array} toRemove - * @returns {Array} Filtered array items. - */ -function removeArray( array, toRemove ) { - return array.filter( item => !toRemove.includes( item ) ); } /** * A definition of {@link module:content-compatibility/dataschema data schema}. * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition - * @property {String} view Name of the view element. + * @property {String} [view] Name of the view element. * @property {String} model Name of the model element. * @property {String|module:engine/model/schema~SchemaItemDefinition} schema Name of the schema to inherit * or custom schema item definition. diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js deleted file mode 100644 index 31ebec34dae..00000000000 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module content-compatibility/generalhtmlsupport - */ - -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import DataSchema from './dataschema'; - -/** - * @extends module:core/plugin~Plugin - */ -export default class GeneralHTMLSupport extends Plugin { - constructor( editor ) { - super( editor ); - - this.dataSchema = new DataSchema( editor ); - } - - /** - * @inheritDoc - */ - static get pluginName() { - return 'GeneralHTMLSupport'; - } - - /** - * @inheritDoc - */ - init() { - const editor = this.editor; - const dataSchema = this.dataSchema; - - // Register block elements. - editor.model.schema.register( '$ghsBlock', { - inheritAllFrom: '$block', - allowIn: '$ghsBlock' - } ); - - dataSchema.register( { view: 'article', model: 'ghsArticle', schema: '$ghsBlock' } ); - dataSchema.register( { view: 'section', model: 'ghsSection', schema: '$ghsBlock' } ); - - // Register data list elements. - dataSchema.register( { view: 'dl', model: 'ghsDl', schema: { - allowIn: [ '$ghsBlock', '$root' ], - isBlock: true - } } ); - - editor.model.schema.register( '$ghsDatalist', { - allowIn: 'ghsDl', - isBlock: true, - allowContentOf: '$ghsBlock' - } ); - editor.model.schema.extend( '$text', { allowIn: '$ghsDatalist' } ); - - dataSchema.register( { view: 'dt', model: 'ghsDt', schema: '$ghsDatalist' } ); - dataSchema.register( { view: 'dd', model: 'ghsDd', schema: '$ghsDatalist' } ); - - // Register details elements. - dataSchema.register( { view: 'details', model: 'ghsDetails', schema: '$ghsBlock' } ); - - dataSchema.register( { view: 'summary', model: 'ghsSummary', schema: { - allowIn: 'ghsDetails' - } } ); - - editor.model.schema.extend( '$text', { allowIn: 'ghsSummary' } ); - } -} diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js similarity index 57% rename from packages/ckeditor5-content-compatibility/tests/dataschema.js rename to packages/ckeditor5-content-compatibility/tests/datafilter.js index e91dfcb1e4f..8a74ffe3b76 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -4,12 +4,12 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import DataSchema from '../src/dataschema'; +import DataFilter from '../src/datafilter'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -describe( 'DataSchema', () => { - let editor, model, dataSchema; +describe( 'DataFilter', () => { + let editor, model, dataFilter; beforeEach( () => { return VirtualTestEditor @@ -18,18 +18,7 @@ describe( 'DataSchema', () => { editor = newEditor; model = editor.model; - dataSchema = new DataSchema( editor ); - - // Define some made up type definitions for testing purposes. - dataSchema.register( { view: 'div', model: 'ghsDiv', schema: { - inheritAllFrom: '$block', - allowIn: 'ghsDiv' - } } ); - dataSchema.register( { view: 'article', model: 'ghsArticle', schema: '$block' } ); - dataSchema.register( { view: 'section', model: 'ghsSection', schema: { - inheritAllFrom: '$block', - allowIn: 'ghsArticle' - } } ); + dataFilter = new DataFilter( editor ); } ); } ); @@ -38,7 +27,7 @@ describe( 'DataSchema', () => { } ); it( 'should allow element', () => { - dataSchema.allowElement( { name: 'article' } ); + dataFilter.allowElement( { name: 'article' } ); editor.setData( '
section1
section2
' ); @@ -50,7 +39,7 @@ describe( 'DataSchema', () => { '
section1section2
' ); - dataSchema.allowElement( { name: 'section' } ); + dataFilter.allowElement( { name: 'section' } ); editor.setData( '
section1
section2
' ); @@ -64,83 +53,83 @@ describe( 'DataSchema', () => { } ); it( 'should allow deeply nested structure', () => { - dataSchema.allowElement( { name: 'div' } ); + dataFilter.allowElement( { name: 'section' } ); - editor.setData( '
1
2
3
' ); + editor.setData( '
1
2
3
' ); expect( getModelData( model, { withoutSelection: true } ) ).to.eq( - '123' + '123' ); expect( editor.getData() ).to.eq( - '
1
2
3
' + '
1
2
3
' ); } ); it( 'should allow attributes', () => { - dataSchema.allowElement( { name: 'div' } ); - dataSchema.allowAttributes( { name: 'div', attributes: { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': 'foobar' } } ); - editor.setData( '
foobar
' ); + editor.setData( '
foobar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + data: 'foobar', attributes: { 1: [ [ 'data-foo', 'foobar' ] ] } } ); expect( editor.getData() ).to.eq( - '
foobar
' + '
foobar
' ); } ); it( 'should allow attributes (styles)', () => { - dataSchema.allowElement( { name: 'div' } ); - dataSchema.allowAttributes( { name: 'div', styles: { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', styles: { 'color': 'red' } } ); - editor.setData( '
foobar
' ); + editor.setData( '
foobar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + data: 'foobar', attributes: { 1: [ [ 'style', { color: 'red' } ] ] } } ); expect( editor.getData() ).to.eq( - '
foobar
' + '
foobar
' ); } ); it( 'should allow attributes (classes)', () => { - dataSchema.allowElement( { name: 'div' } ); - dataSchema.allowAttributes( { name: 'div', classes: [ 'foo', 'bar' ] } ); + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); - editor.setData( '
foobar
' ); + editor.setData( '
foobar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + data: 'foobar', attributes: { 1: [ [ 'class', [ 'foo', 'bar' ] ] ] } } ); expect( editor.getData() ).to.eq( - '
foobar
' + '
foobar
' ); } ); it( 'should allow nested attributes', () => { - dataSchema.allowElement( { name: /article|section/ } ); - dataSchema.allowAttributes( { attributes: { 'data-foo': /foo|bar/ } } ); + dataFilter.allowElement( { name: /article|section/ } ); + dataFilter.allowAttributes( { attributes: { 'data-foo': /foo|bar/ } } ); editor.setData( '
' + - '
section1' + + '
section1
' + '
section2
' + '
' ); @@ -158,16 +147,16 @@ describe( 'DataSchema', () => { } ); it( 'should allow attributes for all allowed definitions', () => { - dataSchema.allowElement( { name: 'div' } ); - dataSchema.allowElement( { name: 'article' } ); + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( { name: 'article' } ); // We skip name purposely to allow attribute on every data schema element. - dataSchema.allowAttributes( { attributes: { 'data-foo': 'foo' } } ); - dataSchema.allowAttributes( { attributes: { 'data-bar': 'bar' } } ); + dataFilter.allowAttributes( { attributes: { 'data-foo': 'foo' } } ); + dataFilter.allowAttributes( { attributes: { 'data-bar': 'bar' } } ); - editor.setData( '
foo
bar
' ); + editor.setData( '
foo
bar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + data: 'foobar', attributes: { 1: [ [ 'data-foo', 'foo' ] ], 2: [ [ 'data-bar', 'bar' ] ] @@ -175,64 +164,64 @@ describe( 'DataSchema', () => { } ); expect( editor.getData() ).to.eq( - '
foo
bar
' + '
foo
bar
' ); } ); it( 'should disallow attributes', () => { - dataSchema.allowElement( { name: 'div' } ); - dataSchema.allowAttributes( { name: 'div', attributes: { 'data-foo': /[^]/ } } ); - dataSchema.disallowAttributes( { name: 'div', attributes: { 'data-foo': 'bar' } } ); + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': /[^]/ } } ); + dataFilter.disallowAttributes( { name: 'section', attributes: { 'data-foo': 'bar' } } ); - editor.setData( '
foo
bar
' ); + editor.setData( '
foo
bar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + data: 'foobar', attributes: { 1: [ [ 'data-foo', 'foo' ] ] } } ); expect( editor.getData() ).to.eq( - '
foo
bar
' + '
foo
bar
' ); } ); it( 'should disallow attributes (styles)', () => { - dataSchema.allowElement( { name: 'div' } ); - dataSchema.allowAttributes( { name: 'div', styles: { color: /[^]/ } } ); - dataSchema.disallowAttributes( { name: 'div', styles: { color: 'red' } } ); + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); + dataFilter.disallowAttributes( { name: 'section', styles: { color: 'red' } } ); - editor.setData( '
foo
bar
' ); + editor.setData( '
foo
bar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + data: 'foobar', attributes: { 1: [ [ 'style', { color: 'blue' } ] ] } } ); expect( editor.getData() ).to.eq( - '
foo
bar
' + '
foo
bar
' ); } ); it( 'should disallow attributes (classes)', () => { - dataSchema.allowElement( { name: 'div' } ); - dataSchema.allowAttributes( { name: 'div', classes: [ 'foo', 'bar' ] } ); - dataSchema.disallowAttributes( { name: 'div', classes: [ 'bar' ] } ); + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); + dataFilter.disallowAttributes( { name: 'section', classes: [ 'bar' ] } ); - editor.setData( '
foo
bar
' ); + editor.setData( '
foo
bar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + data: 'foobar', attributes: { 1: [ [ 'class', [ 'foo' ] ] ] } } ); expect( editor.getData() ).to.eq( - '
foo
bar
' + '
foo
bar
' ); } ); } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index abf7d1f8e3d..7fcc915ecc3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -13,36 +13,33 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import GeneralHTMLSupport from '../../src/generalhtmlsupport'; +import DataFilter from '../../src/datafilter'; class ExtendHTMLSupport extends Plugin { - static get requires() { - return [ GeneralHTMLSupport ]; - } - init() { - const dataSchema = this.editor.plugins.get( 'GeneralHTMLSupport' ).dataSchema; + const dataFilter = new DataFilter( this.editor ); + + dataFilter.allowElement( { name: /article|section/ } ); - dataSchema.allowElement( { name: /article|section/ } ); + dataFilter.allowAttributes( { name: 'section', attributes: { id: /[^]/ } } ); + dataFilter.allowAttributes( { name: 'section', classes: /[^]/ } ); + dataFilter.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); - dataSchema.allowAttributes( { name: 'section', attributes: { id: /[^]/ } } ); - dataSchema.allowAttributes( { name: 'section', classes: /[^]/ } ); - dataSchema.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); + dataFilter.allowElement( { name: /details|summary/ } ); - dataSchema.allowElement( { name: /details|summary/ } ); + dataFilter.allowElement( { name: /dl|dt|dd/ } ); - dataSchema.allowElement( { name: /dl|dt|dd/ } ); + window.dataFilter = dataFilter; } } ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Essentials, Paragraph, Bold, Italic, Strikethrough, GeneralHTMLSupport, ExtendHTMLSupport ], + plugins: [ Essentials, Paragraph, Bold, Italic, Strikethrough, ExtendHTMLSupport ], toolbar: [ 'bold', 'italic', 'strikethrough', 'contentTags' ] } ) .then( editor => { window.editor = editor; - window.dataSchema = editor.plugins.get( 'GeneralHTMLSupport' ).dataSchema; } ) .catch( err => { console.error( err.stack ); From eaa0ec5dcb3dcbc7f28fbc968a066e1f958dd4cc Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 16 Feb 2021 18:34:50 +0100 Subject: [PATCH 010/217] Updated schema inheritance. --- .../ckeditor5-content-compatibility/src/dataschema.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 6cb7e4dd786..afdab2b0fff 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -122,12 +122,14 @@ export default class DataSchema { * @returns {Iterable} */ * _getReferences( name ) { - // TODO extend with the rest of schema properties based on other model types. const { schema } = this._definitions[ name ]; + const inheritProperties = [ 'inheritAllFrom', 'inheritTypesFrom', 'allowWhere', 'allowContentOf', 'allowAttributesOf' ]; - for ( const model of toArray( schema.inheritAllFrom || [] ) ) { - if ( this._definitions[ model ] ) { - yield model; + for ( const property of inheritProperties ) { + for ( const model of toArray( schema[ property ] || [] ) ) { + if ( model !== name && this._definitions[ model ] ) { + yield model; + } } } } From f448f5c5c498082aa27b8bcccea31e979c2f8076 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Feb 2021 08:42:12 +0100 Subject: [PATCH 011/217] Fixed allowText. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index afdab2b0fff..e77977cc492 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -109,7 +109,7 @@ export default class DataSchema { schema.register( name, schemaDefinition ); - if ( schema.allowText ) { + if ( schemaDefinition.allowText ) { schema.extend( '$text', { allowIn: name } ); } } From 4e1271dbc547bcd04ea3e1bb7044ea331387fc18 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Feb 2021 08:42:47 +0100 Subject: [PATCH 012/217] Ensure that attribute is not used by other features. --- .../src/datafilter.js | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index ee4711eec10..042d9953338 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -124,27 +124,45 @@ export default class DataFilter { conversion.for( 'upcast' ).elementToElement( { view: viewName, model: ( viewElement, conversionApi ) => { + // We will stash only attributes which are not processed by any other features + // and should be left unchaged. + const viewConsumable = conversionApi.consumable; + const viewAttributes = []; const allowedAttributes = this._getAllowedAttributes( viewElement ); + // Stash attributes. for ( const key of allowedAttributes.attributes ) { - viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); + if ( viewConsumable.test( viewElement, { attributes: key } ) ) { + viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); + } } - if ( allowedAttributes.classes.length ) { + // Stash classes. + const classes = allowedAttributes.classes.filter( className => { + return viewConsumable.test( viewElement, { classes: className } ); + } ); + + if ( classes.length ) { viewAttributes.push( [ 'class', allowedAttributes.classes ] ); } - if ( allowedAttributes.styles.length ) { + // Stash styles. + const styles = allowedAttributes.styles.filter( styleName => { + return viewConsumable.test( viewElement, { styles: styleName } ); + } ); + + if ( styles.length ) { const stylesObj = {}; - for ( const styleName of allowedAttributes.styles ) { + for ( const styleName of styles ) { stylesObj[ styleName ] = viewElement.getStyle( styleName ); } viewAttributes.push( [ 'style', stylesObj ] ); } + // Keep compatibility attributes inside a single model attribute. let attributesToAdd; if ( viewAttributes.length ) { attributesToAdd = [ [ DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes ] ]; From bf3d979511816d2cd074a093709308d49d780d25 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 09:47:45 +0100 Subject: [PATCH 013/217] Refactoring. --- .../src/datafilter.js | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 042d9953338..e76762bb51a 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -48,10 +48,8 @@ export default class DataFilter { this.dataSchema = new DataSchema( editor ); - this._viewMatchers = { - allowedAttributes: {}, - disallowedAttributes: {} - }; + this._allowedAttributes = {}; + this._disallowedAttributes = {}; } /** @@ -78,7 +76,7 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be allowed. */ allowAttributes( config ) { - this._addAttributeMatcher( config, this._viewMatchers.allowedAttributes ); + this._addAttributeMatcher( config, this._allowedAttributes ); } /** @@ -87,7 +85,7 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be disallowed. */ disallowAttributes( config ) { - this._addAttributeMatcher( config, this._viewMatchers.disallowedAttributes ); + this._addAttributeMatcher( config, this._disallowedAttributes ); } /** @@ -95,9 +93,9 @@ export default class DataFilter { * * @private * @param {module:engine/view/matcher~MatcherPattern} pattern - * @param {Object} cache Cache object holding matchers. + * @param {Object} rules Rules object holding matchers. */ - _addAttributeMatcher( config, cache ) { + _addAttributeMatcher( config, rules ) { const nameRegExp = toRegExp( config.name ); config = cloneDeep( config ); @@ -107,7 +105,7 @@ export default class DataFilter { for ( const { view } of this.dataSchema.getModelViewMapping() ) { if ( nameRegExp.test( view ) ) { - getOrCreateMatcher( view, cache ).add( config ); + getOrCreateMatcher( view, rules ).add( config ); } } } @@ -243,8 +241,8 @@ export default class DataFilter { * @returns {Array} result.styles Array with matched style names. */ _getAllowedAttributes( viewElement ) { - const allowedAttributes = matchAll( viewElement, this._viewMatchers.allowedAttributes ); - const disallowedAttributes = matchAll( viewElement, this._viewMatchers.disallowedAttributes ); + const allowedAttributes = matchAll( viewElement, this._allowedAttributes ); + const disallowedAttributes = matchAll( viewElement, this._disallowedAttributes ); // Drop disallowed content. for ( const key in allowedAttributes ) { @@ -256,21 +254,21 @@ export default class DataFilter { } /** - * Helper function restoring matcher for the given key from cache object. + * Helper function restoring matcher for the given key from `rules` object. * * If matcher for the given key does not exist, this function will create a new one - * inside cache object under the given key. + * inside `rules` object under the given key. * * @private * @param {String} key - * @param {Object} cache + * @param {Object} rules */ -function getOrCreateMatcher( key, cache ) { - if ( !cache[ key ] ) { - cache[ key ] = new Matcher(); +function getOrCreateMatcher( key, rules ) { + if ( !rules[ key ] ) { + rules[ key ] = new Matcher(); } - return cache[ key ]; + return rules[ key ]; } /** @@ -278,14 +276,14 @@ function getOrCreateMatcher( key, cache ) { * * @private * @param {module:engine/view/element~Element} viewElement - * @param {Object} cache Cache object holding matchers. + * @param {Object} rules Rules object holding matchers. * @returns {Object} result * @returns {Array} result.attributes Array with matched attribute names. * @returns {Array} result.classes Array with matched class names. * @returns {Array} result.styles Array with matched style names. */ -function matchAll( viewElement, cache ) { - const matcher = getOrCreateMatcher( viewElement.name, cache ); +function matchAll( viewElement, rules ) { + const matcher = getOrCreateMatcher( viewElement.name, rules ); const matches = matcher.matchAll( viewElement ); return mergeMatchAllResult( matches || [] ); From 067ac0348f38538a22a47d6c32b008b6c78c8336 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 09:48:36 +0100 Subject: [PATCH 014/217] Removed unnecessary code. --- .../src/datafilter.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e76762bb51a..a1f25a5de11 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -188,25 +188,6 @@ export default class DataFilter { const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement( data.item ); - // Remove old values. - if ( data.attributeOldValue !== null ) { - data.attributeOldValue.forEach( ( [ key, value ] ) => { - if ( key === 'class' ) { - const classes = toArray( value ); - - for ( const className of classes ) { - viewWriter.removeClass( className, viewElement ); - } - } else if ( key === 'style' ) { - for ( const key in value ) { - viewWriter.removeStyle( key, viewElement ); - } - } else { - viewWriter.removeAttribute( key, viewElement ); - } - } ); - } - // Apply new values. if ( data.attributeNewValue !== null ) { data.attributeNewValue.forEach( ( [ key, value ] ) => { From 1dea09f321693efad51a7246f81b6bba46f2db15 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 12:54:22 +0100 Subject: [PATCH 015/217] Use additional upcast converter to disallow attributes. --- .../src/datafilter.js | 88 ++++++------------- .../tests/datafilter.js | 8 +- 2 files changed, 31 insertions(+), 65 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index a1f25a5de11..bd92c7eda52 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -119,54 +119,57 @@ export default class DataFilter { const viewName = definition.view; const modelName = definition.model; + // Consumes disallowed element attributes to prevent them of being processed by other converters. + conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { + for ( const match of matchAll( data.viewItem, this._disallowedAttributes ) ) { + conversionApi.consumable.consume( data.viewItem, match.match ); + } + }, { priority: 'high' } ); + } ); + + // Stash unused, allowed element attributes, so they can be reapplied later in data conversion. conversion.for( 'upcast' ).elementToElement( { view: viewName, model: ( viewElement, conversionApi ) => { - // We will stash only attributes which are not processed by any other features - // and should be left unchaged. - const viewConsumable = conversionApi.consumable; + const matches = []; + for ( const match of matchAll( viewElement, this._allowedAttributes ) ) { + if ( conversionApi.consumable.consume( viewElement, match.match ) ) { + matches.push( match ); + } + } + const allowedAttributes = mergeMatchResults( matches ); const viewAttributes = []; - const allowedAttributes = this._getAllowedAttributes( viewElement ); // Stash attributes. for ( const key of allowedAttributes.attributes ) { - if ( viewConsumable.test( viewElement, { attributes: key } ) ) { - viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); - } + viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); } // Stash classes. - const classes = allowedAttributes.classes.filter( className => { - return viewConsumable.test( viewElement, { classes: className } ); - } ); - - if ( classes.length ) { + if ( allowedAttributes.classes.length ) { viewAttributes.push( [ 'class', allowedAttributes.classes ] ); } // Stash styles. - const styles = allowedAttributes.styles.filter( styleName => { - return viewConsumable.test( viewElement, { styles: styleName } ); - } ); - - if ( styles.length ) { + if ( allowedAttributes.styles.length ) { const stylesObj = {}; - for ( const styleName of styles ) { + for ( const styleName of allowedAttributes.styles ) { stylesObj[ styleName ] = viewElement.getStyle( styleName ); } viewAttributes.push( [ 'style', stylesObj ] ); } - // Keep compatibility attributes inside a single model attribute. - let attributesToAdd; + const element = conversionApi.writer.createElement( modelName ); + if ( viewAttributes.length ) { - attributesToAdd = [ [ DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes ] ]; + conversionApi.writer.setAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes, element ); } - return conversionApi.writer.createElement( modelName, attributesToAdd ); + return element; } } ); @@ -209,29 +212,6 @@ export default class DataFilter { } ); } ); } - - /** - * Returns all allowed view element attributes based on attribute matchers registered via {@link #allowedAttributes} - * and {@link #disallowedAttributes} methods. - * - * @private - * @param {module:engine/view/element~Element} viewElement - * @returns {Object} result - * @returns {Array} result.attributes Array with matched attribute names. - * @returns {Array} result.classes Array with matched class names. - * @returns {Array} result.styles Array with matched style names. - */ - _getAllowedAttributes( viewElement ) { - const allowedAttributes = matchAll( viewElement, this._allowedAttributes ); - const disallowedAttributes = matchAll( viewElement, this._disallowedAttributes ); - - // Drop disallowed content. - for ( const key in allowedAttributes ) { - allowedAttributes[ key ] = removeArray( allowedAttributes[ key ], disallowedAttributes[ key ] ); - } - - return allowedAttributes; - } } /** @@ -253,7 +233,7 @@ function getOrCreateMatcher( key, rules ) { } /** - * Helper function matching all attributes for the given element. + * Alias for {@link module:engine/view/matcher~Matcher#matchAll matchAll}. * * @private * @param {module:engine/view/element~Element} viewElement @@ -265,9 +245,8 @@ function getOrCreateMatcher( key, rules ) { */ function matchAll( viewElement, rules ) { const matcher = getOrCreateMatcher( viewElement.name, rules ); - const matches = matcher.matchAll( viewElement ); - return mergeMatchAllResult( matches || [] ); + return matcher.matchAll( viewElement ) || []; } /** @@ -280,7 +259,7 @@ function matchAll( viewElement, rules ) { * @returns {Array} result.classes Array with matched class names. * @returns {Array} result.styles Array with matched style names. */ -function mergeMatchAllResult( matches ) { +function mergeMatchResults( matches ) { const matchResult = { attributes: [], classes: [], styles: [] }; for ( const match of matches ) { @@ -297,17 +276,6 @@ function mergeMatchAllResult( matches ) { return matchResult; } -/** - * Removes array items included in the second array parameter. - * - * @param {Array} array - * @param {Array} toRemove - * @returns {Array} Filtered array items. - */ -function removeArray( array, toRemove ) { - return array.filter( item => !toRemove.includes( item ) ); -} - function toRegExp( value ) { // Match everything if name not given. if ( !value ) { diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 8a74ffe3b76..134042bdd03 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -214,14 +214,12 @@ describe( 'DataFilter', () => { editor.setData( '
foo
bar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', - attributes: { - 1: [ [ 'class', [ 'foo' ] ] ] - } + data: 'foobar', + attributes: {} } ); expect( editor.getData() ).to.eq( - '
foo
bar
' + '
foo
bar
' ); } ); } ); From db8df3fb9646419c003c3d195b33265ad97363e3 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 13:21:14 +0100 Subject: [PATCH 016/217] Changed conversion priority. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index bd92c7eda52..440ed2d1505 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -170,7 +170,8 @@ export default class DataFilter { } return element; - } + }, + converterPriority: 'low' } ); conversion.for( 'downcast' ).elementToElement( { From e84a96f575ccd41bf66370780aba986e6a85fae9 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 13:29:33 +0100 Subject: [PATCH 017/217] Fixed autoparagraphing for some elements. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 440ed2d1505..d002797b721 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -11,6 +11,8 @@ import { escapeRegExp, cloneDeep, uniq } from 'lodash-es'; import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher'; import toArray from '@ckeditor/ckeditor5-utils/src/toarray'; +import { priorities } from 'ckeditor5/src/utils'; + import DataSchema from './dataschema'; const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; @@ -171,7 +173,9 @@ export default class DataFilter { return element; }, - converterPriority: 'low' + // With a `low` priority, `paragraph` plugin autoparagraphing mechanism is executed. Make sure + // this listener is called before it. If not, some elements will be transformed into a paragraph. + converterPriority: priorities.get( 'low' ) + 1 } ); conversion.for( 'downcast' ).elementToElement( { From 665b13cdf88590db0164e4a0f10d5b69a807fd5e Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 13:33:57 +0100 Subject: [PATCH 018/217] Fixed dependencies. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 5 ++--- packages/ckeditor5-content-compatibility/src/dataschema.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index d002797b721..cc9ee394469 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -8,10 +8,9 @@ */ import { escapeRegExp, cloneDeep, uniq } from 'lodash-es'; -import Matcher from '@ckeditor/ckeditor5-engine/src/view/matcher'; -import toArray from '@ckeditor/ckeditor5-utils/src/toarray'; -import { priorities } from 'ckeditor5/src/utils'; +import { Matcher } from 'ckeditor5/src/engine'; +import { priorities, toArray } from 'ckeditor5/src/utils'; import DataSchema from './dataschema'; diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index e77977cc492..ded87e0f825 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -8,7 +8,7 @@ */ import { cloneDeep } from 'lodash-es'; -import toArray from '@ckeditor/ckeditor5-utils/src/toarray'; +import { toArray } from 'ckeditor5/src/utils'; /** * Holds representation of the extended HTML document type definitions to be used by the From 3628142493489076a12a6a921f3af29fa8574c2f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 14:08:22 +0100 Subject: [PATCH 019/217] Refactored element name matching. --- .../src/datafilter.js | 21 ++++++++----------- .../tests/datafilter.js | 11 +++++----- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index cc9ee394469..7e3618eecc7 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -59,10 +59,8 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements which should be allowed. */ allowElement( config ) { - const nameRegExp = toRegExp( config.name ); - for ( const mapping of this.dataSchema.getModelViewMapping() ) { - if ( nameRegExp.test( mapping.view ) ) { + if ( matchViewName( config.name, mapping.view ) ) { this.dataSchema.enable( mapping.model ); this._defineConverters( mapping ); } @@ -97,7 +95,7 @@ export default class DataFilter { * @param {Object} rules Rules object holding matchers. */ _addAttributeMatcher( config, rules ) { - const nameRegExp = toRegExp( config.name ); + const name = config.name; config = cloneDeep( config ); // We don't want match by name when matching attributes. @@ -105,7 +103,7 @@ export default class DataFilter { delete config.name; for ( const { view } of this.dataSchema.getModelViewMapping() ) { - if ( nameRegExp.test( view ) ) { + if ( matchViewName( name, view ) ) { getOrCreateMatcher( view, rules ).add( config ); } } @@ -280,15 +278,14 @@ function mergeMatchResults( matches ) { return matchResult; } -function toRegExp( value ) { - // Match everything if name not given. - if ( !value ) { - value = /[^]/; +function matchViewName( pattern, viewName ) { + if ( typeof pattern === 'string' ) { + return pattern === viewName; } - if ( !( value instanceof RegExp ) ) { - value = new RegExp( escapeRegExp( value ) ); + if ( pattern instanceof RegExp ) { + return pattern.test( viewName ); } - return value; + return false; } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 134042bdd03..8e98e6c20db 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -126,7 +126,7 @@ describe( 'DataFilter', () => { it( 'should allow nested attributes', () => { dataFilter.allowElement( { name: /article|section/ } ); - dataFilter.allowAttributes( { attributes: { 'data-foo': /foo|bar/ } } ); + dataFilter.allowAttributes( { name: /[^]/, attributes: { 'data-foo': /foo|bar/ } } ); editor.setData( '
' + '
section1
' + @@ -147,11 +147,10 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes for all allowed definitions', () => { - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowElement( { name: 'article' } ); - // We skip name purposely to allow attribute on every data schema element. - dataFilter.allowAttributes( { attributes: { 'data-foo': 'foo' } } ); - dataFilter.allowAttributes( { attributes: { 'data-bar': 'bar' } } ); + dataFilter.allowElement( { name: /section|article/ } ); + + dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-foo': 'foo' } } ); + dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-bar': 'bar' } } ); editor.setData( '
foo
bar
' ); From 2b110eb0a03828e2062464f3c24d79d3f36a605d Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 23 Feb 2021 15:44:31 +0100 Subject: [PATCH 020/217] Better dataSchema and dataFilter separation. --- .../src/datafilter.js | 64 +++++++++------ .../src/dataschema.js | 77 ++++++++++++++----- 2 files changed, 99 insertions(+), 42 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 7e3618eecc7..a9bee7bfdec 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -7,7 +7,7 @@ * @module content-compatibility/datafilter */ -import { escapeRegExp, cloneDeep, uniq } from 'lodash-es'; +import { cloneDeep, uniq } from 'lodash-es'; import { Matcher } from 'ckeditor5/src/engine'; import { priorities, toArray } from 'ckeditor5/src/utils'; @@ -59,11 +59,12 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements which should be allowed. */ allowElement( config ) { - for ( const mapping of this.dataSchema.getModelViewMapping() ) { - if ( matchViewName( config.name, mapping.view ) ) { - this.dataSchema.enable( mapping.model ); - this._defineConverters( mapping ); + for ( const definition of this.dataSchema.getDefinitionsForView( config.name ) ) { + for ( const reference of definition.references ) { + this._registerElement( reference ); } + + this._registerElement( definition ); } this.allowAttributes( config ); @@ -95,17 +96,44 @@ export default class DataFilter { * @param {Object} rules Rules object holding matchers. */ _addAttributeMatcher( config, rules ) { - const name = config.name; + const viewName = config.name; config = cloneDeep( config ); - // We don't want match by name when matching attributes. - // Matcher will be already attached to specific definition. + // We don't want match by name when matching attributes. Matcher will be already attached to specific definition. delete config.name; - for ( const { view } of this.dataSchema.getModelViewMapping() ) { - if ( matchViewName( name, view ) ) { - getOrCreateMatcher( view, rules ).add( config ); - } + for ( const definition of this.dataSchema.getDefinitionsForView( viewName ) ) { + getOrCreateMatcher( definition.view, rules ).add( config ); + } + } + + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + */ + _registerElement( definition ) { + if ( this.editor.model.schema.isRegistered( definition.model ) ) { + return; + } + + this._defineSchema( definition ); + + if ( definition.view ) { + this._defineConverters( definition ); + } + } + + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + */ + _defineSchema( definition ) { + const schema = this.editor.model.schema; + + schema.register( definition.model, definition.schema ); + + if ( definition.schema.allowText ) { + schema.extend( '$text', { allowIn: definition.model } ); } } @@ -277,15 +305,3 @@ function mergeMatchResults( matches ) { return matchResult; } - -function matchViewName( pattern, viewName ) { - if ( typeof pattern === 'string' ) { - return pattern === viewName; - } - - if ( pattern instanceof RegExp ) { - return pattern.test( viewName ); - } - - return false; -} diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index ded87e0f825..5610e88ac56 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -73,20 +73,6 @@ export default class DataSchema { this._definitions[ model ] = { view, model, schema }; } - /** - * Returns model-view-pairs for the added data schema definitions. - * - * This method will only return pairs when if - * {@link module:content-compatibility/dataschema~DataSchemaDefinition#view view} has been set. - * - * @returns {Object[]} result - */ - getModelViewMapping() { - return Object.values( this._definitions ) - .filter( def => def.view && def.model ) - .map( def => ( { model: def.model, view: def.view } ) ); - } - /** * Registers model schema item for the given * {@link module:content-compatibility/dataschema~DataSchemaDefinition data schema definition} name. @@ -115,7 +101,39 @@ export default class DataSchema { } /** - * Resolves all model references registered for the given data schema definition. + * Returns all definitions matching the given view name. + * + * @param {String} viewName + * @returns {Iterable<*>} + */ + * getDefinitionsForView( viewName ) { + const definitions = Object.values( this._definitions ) + .filter( def => def.view && testViewName( viewName, def.view ) ); + + for ( const definition of definitions ) { + yield this.getDefinition( definition.model ); + } + } + + /** + * Returns definition for the given model name. + * + * Definition will also include `references` property including all definitions + * referenced by this definition. + * + * @param {String} modelName + * @returns {module:content-compatibility/dataschema~DataSchemaDefinition} + */ + getDefinition( modelName ) { + const definition = cloneDeep( this._definitions[ modelName ] ); + + definition.references = this._getReferences( modelName ); + + return definition; + } + + /** + * Resolves all definition references registered for the given data schema definition. * * @private * @param {String} name Data schema model name. @@ -126,15 +144,38 @@ export default class DataSchema { const inheritProperties = [ 'inheritAllFrom', 'inheritTypesFrom', 'allowWhere', 'allowContentOf', 'allowAttributesOf' ]; for ( const property of inheritProperties ) { - for ( const model of toArray( schema[ property ] || [] ) ) { - if ( model !== name && this._definitions[ model ] ) { - yield model; + for ( const modelName of toArray( schema[ property ] || [] ) ) { + const definition = this._definitions[ modelName ]; + + if ( modelName !== name && definition ) { + yield* this._getReferences( definition.model ); + yield definition; } } } } } +/** + * Test view name against the given pattern. + * + * @private + * @param {String|RegExp} pattern + * @param {String} viewName + * @returns {Boolean} + */ +function testViewName( pattern, viewName ) { + if ( typeof pattern === 'string' ) { + return pattern === viewName; + } + + if ( pattern instanceof RegExp ) { + return pattern.test( viewName ); + } + + return false; +} + /** * A definition of {@link module:content-compatibility/dataschema data schema}. * From 2f5f5a0d6fc6af4ed1aefb36f2849e804576e610 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 24 Feb 2021 09:33:06 +0100 Subject: [PATCH 021/217] Flatten styles and classes structure to simplify code. --- .../src/datafilter.js | 44 ++++++++++--------- .../tests/datafilter.js | 13 +++--- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index a9bee7bfdec..b262834810b 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -10,7 +10,9 @@ import { cloneDeep, uniq } from 'lodash-es'; import { Matcher } from 'ckeditor5/src/engine'; -import { priorities, toArray } from 'ckeditor5/src/utils'; +import { priorities } from 'ckeditor5/src/utils'; + +import StylesMap, { StylesProcessor } from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; import DataSchema from './dataschema'; @@ -176,7 +178,7 @@ export default class DataFilter { // Stash classes. if ( allowedAttributes.classes.length ) { - viewAttributes.push( [ 'class', allowedAttributes.classes ] ); + viewAttributes.push( [ 'class', allowedAttributes.classes.join( ' ' ) ] ); } // Stash styles. @@ -187,7 +189,7 @@ export default class DataFilter { stylesObj[ styleName ] = viewElement.getStyle( styleName ); } - viewAttributes.push( [ 'style', stylesObj ] ); + viewAttributes.push( [ 'style', inlineStyles( stylesObj ) ] ); } const element = conversionApi.writer.createElement( modelName ); @@ -214,6 +216,10 @@ export default class DataFilter { return; } + if ( data.attributeNewValue === null ) { + return; + } + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { return; } @@ -222,23 +228,9 @@ export default class DataFilter { const viewElement = conversionApi.mapper.toViewElement( data.item ); // Apply new values. - if ( data.attributeNewValue !== null ) { - data.attributeNewValue.forEach( ( [ key, value ] ) => { - if ( key === 'class' ) { - const classes = toArray( value ); - - for ( const className of classes ) { - viewWriter.addClass( className, viewElement ); - } - } else if ( key === 'style' ) { - for ( const key in value ) { - viewWriter.setStyle( key, value[ key ], viewElement ); - } - } else { - viewWriter.setAttribute( key, value, viewElement ); - } - } ); - } + data.attributeNewValue.forEach( ( [ key, value ] ) => { + viewWriter.setAttribute( key, value, viewElement ); + } ); } ); } ); } @@ -305,3 +297,15 @@ function mergeMatchResults( matches ) { return matchResult; } + +/** + * Inlines styles object into normalized style string. + * + * @param {Object} stylesObject + * @returns {String} + */ +function inlineStyles( stylesObj ) { + const stylesMap = new StylesMap( new StylesProcessor() ); + stylesMap.set( stylesObj ); + return stylesMap.toString(); +} diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 8e98e6c20db..3295698e7b3 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -89,20 +89,21 @@ describe( 'DataFilter', () => { it( 'should allow attributes (styles)', () => { dataFilter.allowElement( { name: 'section' } ); dataFilter.allowAttributes( { name: 'section', styles: { - 'color': 'red' + 'color': 'red', + 'background-color': 'blue' } } ); - editor.setData( '
foobar
' ); + editor.setData( '
foobar
' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'style', { color: 'red' } ] ] + 1: [ [ 'style', 'background-color:blue;color:red;' ] ] } } ); expect( editor.getData() ).to.eq( - '
foobar
' + '
foobar
' ); } ); @@ -115,7 +116,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'class', [ 'foo', 'bar' ] ] ] + 1: [ [ 'class', 'foo bar' ] ] } } ); @@ -196,7 +197,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'style', { color: 'blue' } ] ] + 1: [ [ 'style', 'color:blue;' ] ] } } ); From af048b7be274bc4cc6d94b9442651ac43605759d Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 24 Feb 2021 10:12:15 +0100 Subject: [PATCH 022/217] Refactoring, removed unused code. --- .../src/datafilter.js | 16 +++---- .../src/dataschema.js | 47 ++++--------------- 2 files changed, 18 insertions(+), 45 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index b262834810b..d09bad1874a 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -51,8 +51,8 @@ export default class DataFilter { this.dataSchema = new DataSchema( editor ); - this._allowedAttributes = {}; - this._disallowedAttributes = {}; + this._allowedAttributes = new Map(); + this._disallowedAttributes = new Map(); } /** @@ -95,7 +95,7 @@ export default class DataFilter { * * @private * @param {module:engine/view/matcher~MatcherPattern} pattern - * @param {Object} rules Rules object holding matchers. + * @param {Map} rules Rules map holding matchers. */ _addAttributeMatcher( config, rules ) { const viewName = config.name; @@ -244,14 +244,14 @@ export default class DataFilter { * * @private * @param {String} key - * @param {Object} rules + * @param {Map} rules */ function getOrCreateMatcher( key, rules ) { - if ( !rules[ key ] ) { - rules[ key ] = new Matcher(); + if ( !rules.has( key ) ) { + rules.set( key, new Matcher() ); } - return rules[ key ]; + return rules.get( key ); } /** @@ -259,7 +259,7 @@ function getOrCreateMatcher( key, rules ) { * * @private * @param {module:engine/view/element~Element} viewElement - * @param {Object} rules Rules object holding matchers. + * @param {Map} rules Rules map holding matchers. * @returns {Object} result * @returns {Array} result.attributes Array with matched attribute names. * @returns {Array} result.classes Array with matched class names. diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 5610e88ac56..5ec7f1fd547 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -28,7 +28,7 @@ export default class DataSchema { constructor( editor ) { this.editor = editor; - this._definitions = {}; + this._definitions = new Map(); // Add block elements. this.register( { model: '$ghsBlock' }, { @@ -70,34 +70,7 @@ export default class DataSchema { * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ register( { view, model }, schema ) { - this._definitions[ model ] = { view, model, schema }; - } - - /** - * Registers model schema item for the given - * {@link module:content-compatibility/dataschema~DataSchemaDefinition data schema definition} name. - * - * @param {String} name - */ - enable( name ) { - const schema = this.editor.model.schema; - - if ( schema.isRegistered( name ) ) { - return; - } - - const definition = this._definitions[ name ]; - const schemaDefinition = cloneDeep( definition.schema ); - - for ( const reference of this._getReferences( name ) ) { - this.enable( reference ); - } - - schema.register( name, schemaDefinition ); - - if ( schemaDefinition.allowText ) { - schema.extend( '$text', { allowIn: name } ); - } + this._definitions.set( model, { view, model, schema } ); } /** @@ -107,7 +80,7 @@ export default class DataSchema { * @returns {Iterable<*>} */ * getDefinitionsForView( viewName ) { - const definitions = Object.values( this._definitions ) + const definitions = Array.from( this._definitions.values() ) .filter( def => def.view && testViewName( viewName, def.view ) ); for ( const definition of definitions ) { @@ -125,7 +98,7 @@ export default class DataSchema { * @returns {module:content-compatibility/dataschema~DataSchemaDefinition} */ getDefinition( modelName ) { - const definition = cloneDeep( this._definitions[ modelName ] ); + const definition = cloneDeep( this._definitions.get( modelName ) ); definition.references = this._getReferences( modelName ); @@ -136,18 +109,18 @@ export default class DataSchema { * Resolves all definition references registered for the given data schema definition. * * @private - * @param {String} name Data schema model name. + * @param {String} modelName Data schema model name. * @returns {Iterable} */ - * _getReferences( name ) { - const { schema } = this._definitions[ name ]; + * _getReferences( modelName ) { + const { schema } = this._definitions.get( modelName ); const inheritProperties = [ 'inheritAllFrom', 'inheritTypesFrom', 'allowWhere', 'allowContentOf', 'allowAttributesOf' ]; for ( const property of inheritProperties ) { - for ( const modelName of toArray( schema[ property ] || [] ) ) { - const definition = this._definitions[ modelName ]; + for ( const referenceName of toArray( schema[ property ] || [] ) ) { + const definition = this._definitions.get( referenceName ); - if ( modelName !== name && definition ) { + if ( referenceName !== modelName && definition ) { yield* this._getReferences( definition.model ); yield definition; } From 79fd7da4a91ba954d99f7a8556421f4155465af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20Bogda=C5=84ski?= Date: Fri, 26 Feb 2021 09:19:33 +0100 Subject: [PATCH 023/217] Docs update. Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- packages/ckeditor5-content-compatibility/src/datafilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index d09bad1874a..b4911f56c1b 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -19,7 +19,7 @@ import DataSchema from './dataschema'; const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; /** - * Allows to validate elements and element attributes registered by {@link @module content-compatibility/dataschema~DataSchema}. + * Allows to validate elements and element attributes registered by {@link module:content-compatibility/dataschema~DataSchema}. * * To enable registered element in the editor, use {@link #allowElement} method: * From ef5e974baa7a315e38a8c512ca46b20d77bd2470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20Bogda=C5=84ski?= Date: Fri, 26 Feb 2021 09:19:56 +0100 Subject: [PATCH 024/217] Simplified downcast converter. Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- packages/ckeditor5-content-compatibility/src/datafilter.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index b4911f56c1b..4e9f7696208 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -211,10 +211,7 @@ export default class DataFilter { } ); conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }`, ( evt, data, conversionApi ) => { - if ( data.item.name != modelName ) { - return; - } + dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }:${ modelName }`, ( evt, data, conversionApi ) => { if ( data.attributeNewValue === null ) { return; From 36ab682d2db78b23f2a9dce4e7d55ced5937c551 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 26 Feb 2021 09:36:29 +0100 Subject: [PATCH 025/217] Used view document styles processor. --- .../src/datafilter.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 4e9f7696208..704dcc29f34 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -12,7 +12,7 @@ import { cloneDeep, uniq } from 'lodash-es'; import { Matcher } from 'ckeditor5/src/engine'; import { priorities } from 'ckeditor5/src/utils'; -import StylesMap, { StylesProcessor } from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; +import StylesMap from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; import DataSchema from './dataschema'; @@ -189,7 +189,7 @@ export default class DataFilter { stylesObj[ styleName ] = viewElement.getStyle( styleName ); } - viewAttributes.push( [ 'style', inlineStyles( stylesObj ) ] ); + viewAttributes.push( [ 'style', this.inlineStyles( stylesObj ) ] ); } const element = conversionApi.writer.createElement( modelName ); @@ -212,7 +212,6 @@ export default class DataFilter { conversion.for( 'downcast' ).add( dispatcher => { dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }:${ modelName }`, ( evt, data, conversionApi ) => { - if ( data.attributeNewValue === null ) { return; } @@ -231,6 +230,21 @@ export default class DataFilter { } ); } ); } + + /** + * Inlines styles object into normalized style string. + * + * @param {Object} stylesObject + * @returns {String} + */ + inlineStyles( stylesObj ) { + const stylesProcessor = this.editor.editing.view.document.stylesProcessor; + const stylesMap = new StylesMap( stylesProcessor ); + + stylesMap.set( stylesObj ); + + return stylesMap.toString(); + } } /** @@ -294,15 +308,3 @@ function mergeMatchResults( matches ) { return matchResult; } - -/** - * Inlines styles object into normalized style string. - * - * @param {Object} stylesObject - * @returns {String} - */ -function inlineStyles( stylesObj ) { - const stylesMap = new StylesMap( new StylesProcessor() ); - stylesMap.set( stylesObj ); - return stylesMap.toString(); -} From 47bd784bb51d693bb783513f010b4b438f3a125c Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 26 Feb 2021 10:11:47 +0100 Subject: [PATCH 026/217] Changed ghs attributes format to object. --- .../src/datafilter.js | 20 +++++++++------- .../tests/datafilter.js | 24 +++++++++---------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 704dcc29f34..4dc65e7ebdb 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -169,16 +169,16 @@ export default class DataFilter { } const allowedAttributes = mergeMatchResults( matches ); - const viewAttributes = []; + const viewAttributes = {}; // Stash attributes. for ( const key of allowedAttributes.attributes ) { - viewAttributes.push( [ key, viewElement.getAttribute( key ) ] ); + viewAttributes[ key ] = viewElement.getAttribute( key ); } // Stash classes. if ( allowedAttributes.classes.length ) { - viewAttributes.push( [ 'class', allowedAttributes.classes.join( ' ' ) ] ); + viewAttributes.class = allowedAttributes.classes.join( ' ' ); } // Stash styles. @@ -189,12 +189,12 @@ export default class DataFilter { stylesObj[ styleName ] = viewElement.getStyle( styleName ); } - viewAttributes.push( [ 'style', this.inlineStyles( stylesObj ) ] ); + viewAttributes.style = this.inlineStyles( stylesObj ); } const element = conversionApi.writer.createElement( modelName ); - if ( viewAttributes.length ) { + if ( Object.keys( viewAttributes ).length ) { conversionApi.writer.setAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes, element ); } @@ -212,7 +212,9 @@ export default class DataFilter { conversion.for( 'downcast' ).add( dispatcher => { dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }:${ modelName }`, ( evt, data, conversionApi ) => { - if ( data.attributeNewValue === null ) { + const viewAttributes = data.attributeNewValue; + + if ( viewAttributes === null ) { return; } @@ -224,9 +226,9 @@ export default class DataFilter { const viewElement = conversionApi.mapper.toViewElement( data.item ); // Apply new values. - data.attributeNewValue.forEach( ( [ key, value ] ) => { - viewWriter.setAttribute( key, value, viewElement ); - } ); + for ( const key in viewAttributes ) { + viewWriter.setAttribute( key, viewAttributes[ key ], viewElement ); + } } ); } ); } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 3295698e7b3..ec78b8ec49b 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -77,7 +77,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'data-foo', 'foobar' ] ] + 1: { 'data-foo': 'foobar' } } } ); @@ -98,7 +98,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'style', 'background-color:blue;color:red;' ] ] + 1: { 'style': 'background-color:blue;color:red;' } } } ); @@ -116,7 +116,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'class', 'foo bar' ] ] + 1: { 'class': 'foo bar' } } } ); @@ -140,9 +140,9 @@ describe( 'DataFilter', () => { 'section2' + '', attributes: { - 1: [ [ 'data-foo', 'foo' ] ], - 2: [ [ 'data-foo', 'bar' ] ], - 3: [ [ 'data-foo', 'foo' ] ] + 1: { 'data-foo': 'foo' }, + 2: { 'data-foo': 'bar' }, + 3: { 'data-foo': 'foo' } } } ); } ); @@ -158,8 +158,8 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'data-foo', 'foo' ] ], - 2: [ [ 'data-bar', 'bar' ] ] + 1: { 'data-foo': 'foo' }, + 2: { 'data-bar': 'bar' } } } ); @@ -178,7 +178,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'data-foo', 'foo' ] ] + 1: { 'data-foo': 'foo' } } } ); @@ -197,7 +197,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: [ [ 'style', 'color:blue;' ] ] + 1: { 'style': 'color:blue;' } } } ); @@ -225,9 +225,9 @@ describe( 'DataFilter', () => { } ); function getModelDataWithAttributes( model, options ) { - // Simplify GHS attributes as they are not readable at this point due to complex structure. + // Simplify GHS attributes as they are not very readable at this point due to object structure. let counter = 1; - const data = getModelData( model, options ).replace( /ghsAttributes="(.*?)"/g, () => { + const data = getModelData( model, options ).replace( /ghsAttributes="{(.*?)}"/g, () => { return `ghsAttributes="(${ counter++ })"`; } ); From 08cdbcf9c0a3f772b141bd1e7a8f8f3f8554e100 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 26 Feb 2021 10:33:06 +0100 Subject: [PATCH 027/217] Replaced uniq with Set. --- .../src/datafilter.js | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 4dc65e7ebdb..72b6ca4d874 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -7,7 +7,7 @@ * @module content-compatibility/datafilter */ -import { cloneDeep, uniq } from 'lodash-es'; +import { cloneDeep } from 'lodash-es'; import { Matcher } from 'ckeditor5/src/engine'; import { priorities } from 'ckeditor5/src/utils'; @@ -168,24 +168,24 @@ export default class DataFilter { } } - const allowedAttributes = mergeMatchResults( matches ); + const { attributes, classes, styles } = mergeMatchResults( matches ); const viewAttributes = {}; // Stash attributes. - for ( const key of allowedAttributes.attributes ) { + for ( const key of attributes.values() ) { viewAttributes[ key ] = viewElement.getAttribute( key ); } // Stash classes. - if ( allowedAttributes.classes.length ) { - viewAttributes.class = allowedAttributes.classes.join( ' ' ); + if ( classes.size ) { + viewAttributes.class = [ ...classes.values() ].join( ' ' ); } // Stash styles. - if ( allowedAttributes.styles.length ) { + if ( styles.size ) { const stylesObj = {}; - for ( const styleName of allowedAttributes.styles ) { + for ( const styleName of styles.values() ) { stylesObj[ styleName ] = viewElement.getStyle( styleName ); } @@ -295,18 +295,14 @@ function matchAll( viewElement, rules ) { * @returns {Array} result.styles Array with matched style names. */ function mergeMatchResults( matches ) { - const matchResult = { attributes: [], classes: [], styles: [] }; + const matchResult = { attributes: new Set(), classes: new Set(), styles: new Set() }; for ( const match of matches ) { for ( const key in matchResult ) { const values = match.match[ key ] || []; - matchResult[ key ].push( ...values ); + values.forEach( value => matchResult[ key ].add( value ) ); } } - for ( const key in matchResult ) { - matchResult[ key ] = uniq( matchResult[ key ] ); - } - return matchResult; } From 0c15a7a14d4aec6d91edee90ef10bbecbd9efa6f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 26 Feb 2021 11:08:21 +0100 Subject: [PATCH 028/217] Docs update. --- .../ckeditor5-content-compatibility/src/datafilter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 72b6ca4d874..703b00da596 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -58,7 +58,7 @@ export default class DataFilter { /** * Allow the given element registered by {@link #register} method. * - * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements which should be allowed. + * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all view elements which should be allowed. */ allowElement( config ) { for ( const definition of this.dataSchema.getDefinitionsForView( config.name ) ) { @@ -75,7 +75,7 @@ export default class DataFilter { /** * Allow the given attributes for view element allowed by {@link #allowElement} method. * - * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be allowed. + * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be allowed. */ allowAttributes( config ) { this._addAttributeMatcher( config, this._allowedAttributes ); @@ -84,7 +84,7 @@ export default class DataFilter { /** * Disallowe the given attributes for view element allowed by {@link #allowElement} method. * - * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all attributes which should be disallowed. + * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be disallowed. */ disallowAttributes( config ) { this._addAttributeMatcher( config, this._disallowedAttributes ); @@ -94,7 +94,7 @@ export default class DataFilter { * Adds attribute matcher for every registered data schema definition for the given `config.name`. * * @private - * @param {module:engine/view/matcher~MatcherPattern} pattern + * @param {module:engine/view/matcher~MatcherPattern} config * @param {Map} rules Rules map holding matchers. */ _addAttributeMatcher( config, rules ) { From 38d3f8ae14fea1aca7fe547cb6b319e5cb476a71 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 26 Feb 2021 11:16:29 +0100 Subject: [PATCH 029/217] Removed hard dependency on data schema. --- .../ckeditor5-content-compatibility/src/datafilter.js | 10 ++++------ .../ckeditor5-content-compatibility/src/dataschema.js | 4 +--- .../tests/datafilter.js | 3 ++- .../tests/manual/generalhtmlsupport.js | 6 +++--- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 703b00da596..3868ceed31a 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -14,8 +14,6 @@ import { priorities } from 'ckeditor5/src/utils'; import StylesMap from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; -import DataSchema from './dataschema'; - const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; /** @@ -46,10 +44,10 @@ const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; * } ); */ export default class DataFilter { - constructor( editor ) { + constructor( editor, dataSchema ) { this.editor = editor; - this.dataSchema = new DataSchema( editor ); + this._dataSchema = dataSchema; this._allowedAttributes = new Map(); this._disallowedAttributes = new Map(); @@ -61,7 +59,7 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all view elements which should be allowed. */ allowElement( config ) { - for ( const definition of this.dataSchema.getDefinitionsForView( config.name ) ) { + for ( const definition of this._dataSchema.getDefinitionsForView( config.name ) ) { for ( const reference of definition.references ) { this._registerElement( reference ); } @@ -104,7 +102,7 @@ export default class DataFilter { // We don't want match by name when matching attributes. Matcher will be already attached to specific definition. delete config.name; - for ( const definition of this.dataSchema.getDefinitionsForView( viewName ) ) { + for ( const definition of this._dataSchema.getDefinitionsForView( viewName ) ) { getOrCreateMatcher( definition.view, rules ).add( config ); } } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 5ec7f1fd547..04f250250af 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -25,9 +25,7 @@ import { toArray } from 'ckeditor5/src/utils'; * dataSchema.enable( 'my-section' ); */ export default class DataSchema { - constructor( editor ) { - this.editor = editor; - + constructor() { this._definitions = new Map(); // Add block elements. diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index ec78b8ec49b..5c010fb191e 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -4,6 +4,7 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import DataSchema from '../src/dataschema'; import DataFilter from '../src/datafilter'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -18,7 +19,7 @@ describe( 'DataFilter', () => { editor = newEditor; model = editor.model; - dataFilter = new DataFilter( editor ); + dataFilter = new DataFilter( editor, new DataSchema() ); } ); } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 7fcc915ecc3..468b38483e1 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -13,11 +13,13 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import DataSchema from '../../src/dataschema'; import DataFilter from '../../src/datafilter'; class ExtendHTMLSupport extends Plugin { init() { - const dataFilter = new DataFilter( this.editor ); + const dataSchema = new DataSchema(); + const dataFilter = new DataFilter( this.editor, dataSchema ); dataFilter.allowElement( { name: /article|section/ } ); @@ -28,8 +30,6 @@ class ExtendHTMLSupport extends Plugin { dataFilter.allowElement( { name: /details|summary/ } ); dataFilter.allowElement( { name: /dl|dt|dd/ } ); - - window.dataFilter = dataFilter; } } From 7d231ca101867d6d1f9121c61485e1229d19f3df Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 26 Feb 2021 14:08:13 +0100 Subject: [PATCH 030/217] Improved manual test with some additional comments. --- .../tests/manual/generalhtmlsupport.html | 3 +- .../tests/manual/generalhtmlsupport.js | 36 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index 6a0960f8c3e..140129ab86e 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -5,7 +5,7 @@
Section #1
-
Section #2
+
Section #2
Section #3
@@ -18,4 +18,5 @@
dd1
dd2
+ XYZ
diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 468b38483e1..0176e4efc3a 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -16,20 +16,44 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import DataSchema from '../../src/dataschema'; import DataFilter from '../../src/datafilter'; +/** + * Client custom plugin extending HTML support for compatibility. + */ class ExtendHTMLSupport extends Plugin { init() { + // Create data schema object including default configuration based on CKE4 + // DTD elements, missing dedicated feature in CKEditor 5. + // Data schema only behaves as container for DTD definitions, it doesn't change + // anything inside the editor itself. Registered elements are not extending editor + // model schema at this point. const dataSchema = new DataSchema(); + + // Extend schema with custom `xyz` element. + dataSchema.register( { view: 'xyz', model: 'ghsXyz' }, { + inheritAllFrom: '$ghsBlock', + allowText: true + } ); + + // Create data filter which will register editor model schema and converters required + // to allow elements and filter attributes. const dataFilter = new DataFilter( this.editor, dataSchema ); - dataFilter.allowElement( { name: /article|section/ } ); + // Allow some elements, at this point model schema will include information about view-model mapping + // e.g. article -> ghsArticle + dataFilter.allowElement( { name: 'article' } ); + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( { name: /^details|summary$/ } ); + dataFilter.allowElement( { name: /^dl|dd|dt$/ } ); + dataFilter.allowElement( { name: 'xyz' } ); - dataFilter.allowAttributes( { name: 'section', attributes: { id: /[^]/ } } ); + // Let's extend 'section' with some attributes. Data filter will take care of + // creating proper converters and attribute matchers: + dataFilter.allowAttributes( { name: 'section', attributes: { id: /[^]/, 'data-foo': /[^]/ } } ); dataFilter.allowAttributes( { name: 'section', classes: /[^]/ } ); - dataFilter.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); - - dataFilter.allowElement( { name: /details|summary/ } ); + dataFilter.allowAttributes( { name: 'section', styles: { color: 'red' } } ); - dataFilter.allowElement( { name: /dl|dt|dd/ } ); + // but disallow setting id attribute if it start with `_` prefix: + dataFilter.disallowAttributes( { name: 'section', attributes: { id: /^_.*/ } } ); } } From 19eece0133791931db5fdf44e4537b00684185cf Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 26 Feb 2021 14:24:36 +0100 Subject: [PATCH 031/217] Removed unused attribute group. --- .../tests/manual/generalhtmlsupport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 0176e4efc3a..818eec2dbf3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -48,7 +48,7 @@ class ExtendHTMLSupport extends Plugin { // Let's extend 'section' with some attributes. Data filter will take care of // creating proper converters and attribute matchers: - dataFilter.allowAttributes( { name: 'section', attributes: { id: /[^]/, 'data-foo': /[^]/ } } ); + dataFilter.allowAttributes( { name: 'section', attributes: { id: /[^]/ } } ); dataFilter.allowAttributes( { name: 'section', classes: /[^]/ } ); dataFilter.allowAttributes( { name: 'section', styles: { color: 'red' } } ); From 30b0c0b09c2dd078323c7f274bfde39a576c5bae Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 1 Mar 2021 09:20:57 +0100 Subject: [PATCH 032/217] Extended definition with allowChildren. --- .../src/datafilter.js | 12 ++- .../src/dataschema.js | 90 ++++++++++++++----- .../tests/manual/generalhtmlsupport.js | 34 +++++-- 3 files changed, 107 insertions(+), 29 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 3868ceed31a..e89445c88c2 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -10,7 +10,7 @@ import { cloneDeep } from 'lodash-es'; import { Matcher } from 'ckeditor5/src/engine'; -import { priorities } from 'ckeditor5/src/utils'; +import { priorities, toArray } from 'ckeditor5/src/utils'; import StylesMap from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; @@ -132,8 +132,14 @@ export default class DataFilter { schema.register( definition.model, definition.schema ); - if ( definition.schema.allowText ) { - schema.extend( '$text', { allowIn: definition.model } ); + const allowedChildren = toArray( definition.allowChildren || [] ); + + for ( const child of allowedChildren ) { + if ( schema.isRegistered( child ) ) { + schema.extend( child, { + allowIn: definition.model + } ); + } } } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 04f250250af..9766ccb2c9f 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -29,36 +29,83 @@ export default class DataSchema { this._definitions = new Map(); // Add block elements. - this.register( { model: '$ghsBlock' }, { - inheritAllFrom: '$block', - allowIn: '$ghsBlock' + this.register( { + model: '$ghsBlock', + allowChildren: '$block', + schema: { + inheritAllFrom: '$block', + allowIn: '$ghsBlock' + } + } ); + + this.register( { + view: 'article', + model: 'ghsArticle', + schema: { + inheritAllFrom: '$ghsBlock' + } } ); - this.register( { view: 'article', model: 'ghsArticle' }, { inheritAllFrom: '$ghsBlock' } ); - this.register( { view: 'section', model: 'ghsSection' }, { inheritAllFrom: '$ghsBlock' } ); + this.register( { + view: 'section', + model: 'ghsSection', + schema: { + inheritAllFrom: '$ghsBlock' + } + } ); // Add data list elements. - this.register( { view: 'dl', model: 'ghsDl' }, { - allowIn: [ '$ghsBlock', '$root' ], - isBlock: true + this.register( { + view: 'dl', + model: 'ghsDl', + schema: { + allowIn: [ '$ghsBlock', '$root' ], + isBlock: true + } + } ); + + this.register( { + model: '$ghsDatalist', + allowChildren: '$text', + schema: { + allowIn: 'ghsDl', + isBlock: true, + allowContentOf: '$ghsBlock' + } } ); - this.register( { model: '$ghsDatalist' }, { - allowIn: 'ghsDl', - isBlock: true, - allowContentOf: '$ghsBlock', - allowText: true + this.register( { + view: 'dt', + model: 'ghsDt', + schema: { + inheritAllFrom: '$ghsDatalist' + } } ); - this.register( { view: 'dt', model: 'ghsDt' }, { inheritAllFrom: '$ghsDatalist' } ); - this.register( { view: 'dd', model: 'ghsDd' }, { inheritAllFrom: '$ghsDatalist' } ); + this.register( { + view: 'dd', + model: 'ghsDd', + schema: { + inheritAllFrom: '$ghsDatalist' + } + } ); // Add details elements. - this.register( { view: 'details', model: 'ghsDetails' }, { inheritAllFrom: '$ghsBlock' } ); + this.register( { + view: 'details', + model: 'ghsDetails', + schema: { + inheritAllFrom: '$ghsBlock' + } + } ); - this.register( { view: 'summary', model: 'ghsSummary' }, { - allowIn: 'ghsDetails', - allowText: true + this.register( { + view: 'summary', + model: 'ghsSummary', + allowChildren: '$text', + schema: { + allowIn: 'ghsDetails' + } } ); } @@ -67,8 +114,8 @@ export default class DataSchema { * * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ - register( { view, model }, schema ) { - this._definitions.set( model, { view, model, schema } ); + register( definition ) { + this._definitions.set( definition.model, definition ); } /** @@ -154,5 +201,6 @@ function testViewName( pattern, viewName ) { * @property {String} [view] Name of the view element. * @property {String} model Name of the model element. * @property {String|module:engine/model/schema~SchemaItemDefinition} schema Name of the schema to inherit + * @property {String|Array} allowChildren Extends the given children list to allow definition model. * or custom schema item definition. */ diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 818eec2dbf3..0bd8fb2d7b2 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -12,6 +12,8 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import List from '@ckeditor/ckeditor5-list/src/list'; import DataSchema from '../../src/dataschema'; import DataFilter from '../../src/datafilter'; @@ -29,9 +31,13 @@ class ExtendHTMLSupport extends Plugin { const dataSchema = new DataSchema(); // Extend schema with custom `xyz` element. - dataSchema.register( { view: 'xyz', model: 'ghsXyz' }, { - inheritAllFrom: '$ghsBlock', - allowText: true + dataSchema.register( { + view: 'xyz', + model: 'ghsXyz', + schema: { + inheritAllFrom: '$ghsBlock', + allowText: true + } } ); // Create data filter which will register editor model schema and converters required @@ -59,8 +65,26 @@ class ExtendHTMLSupport extends Plugin { ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Essentials, Paragraph, Bold, Italic, Strikethrough, ExtendHTMLSupport ], - toolbar: [ 'bold', 'italic', 'strikethrough', 'contentTags' ] + plugins: [ + BlockQuote, + Bold, + Essentials, + ExtendHTMLSupport, + Italic, + List, + Paragraph, + Strikethrough + ], + toolbar: [ + 'bold', + 'italic', + 'strikethrough', + '|', + 'numberedList', + 'bulletedList', + '|', + 'blockquote' + ] } ) .then( editor => { window.editor = editor; From 161fe88abfce4db23c0426a65e33bacc6d615a77 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 1 Mar 2021 10:00:53 +0100 Subject: [PATCH 033/217] Changed attributes structure. --- .../src/datafilter.js | 48 ++++++++-------- .../tests/datafilter.js | 57 +++++++++++++++---- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e89445c88c2..2a7557df0af 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -12,8 +12,6 @@ import { cloneDeep } from 'lodash-es'; import { Matcher } from 'ckeditor5/src/engine'; import { priorities, toArray } from 'ckeditor5/src/utils'; -import StylesMap from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; - const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; /** @@ -176,24 +174,30 @@ export default class DataFilter { const viewAttributes = {}; // Stash attributes. - for ( const key of attributes.values() ) { - viewAttributes[ key ] = viewElement.getAttribute( key ); + if ( attributes.size ) { + const attributesObject = {}; + + for ( const attributeName of attributes ) { + attributesObject[ attributeName ] = viewElement.getAttribute( attributeName ); + } + + viewAttributes.attributes = attributesObject; } // Stash classes. if ( classes.size ) { - viewAttributes.class = [ ...classes.values() ].join( ' ' ); + viewAttributes.classes = Array.from( classes ); } // Stash styles. if ( styles.size ) { const stylesObj = {}; - for ( const styleName of styles.values() ) { + for ( const styleName of styles ) { stylesObj[ styleName ] = viewElement.getStyle( styleName ); } - viewAttributes.style = this.inlineStyles( stylesObj ); + viewAttributes.styles = stylesObj; } const element = conversionApi.writer.createElement( modelName ); @@ -229,27 +233,21 @@ export default class DataFilter { const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement( data.item ); - // Apply new values. - for ( const key in viewAttributes ) { - viewWriter.setAttribute( key, viewAttributes[ key ], viewElement ); + if ( viewAttributes.attributes ) { + for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) { + viewWriter.setAttribute( key, value, viewElement ); + } } - } ); - } ); - } - /** - * Inlines styles object into normalized style string. - * - * @param {Object} stylesObject - * @returns {String} - */ - inlineStyles( stylesObj ) { - const stylesProcessor = this.editor.editing.view.document.stylesProcessor; - const stylesMap = new StylesMap( stylesProcessor ); - - stylesMap.set( stylesObj ); + if ( viewAttributes.styles ) { + viewWriter.setStyle( viewAttributes.styles, viewElement ); + } - return stylesMap.toString(); + if ( viewAttributes.classes ) { + viewWriter.addClass( viewAttributes.classes, viewElement ); + } + } ); + } ); } } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 5c010fb191e..71abda893c6 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -78,7 +78,11 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: { 'data-foo': 'foobar' } + 1: { + attributes: { + 'data-foo': 'foobar' + } + } } } ); @@ -99,7 +103,12 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: { 'style': 'background-color:blue;color:red;' } + 1: { + styles: { + 'background-color': 'blue', + color: 'red' + } + } } } ); @@ -117,7 +126,7 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: { 'class': 'foo bar' } + 1: { classes: [ 'foo', 'bar' ] } } } ); @@ -141,9 +150,21 @@ describe( 'DataFilter', () => { 'section2' + '', attributes: { - 1: { 'data-foo': 'foo' }, - 2: { 'data-foo': 'bar' }, - 3: { 'data-foo': 'foo' } + 1: { + attributes: { + 'data-foo': 'foo' + } + }, + 2: { + attributes: { + 'data-foo': 'bar' + } + }, + 3: { + attributes: { + 'data-foo': 'foo' + } + } } } ); } ); @@ -159,8 +180,16 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: { 'data-foo': 'foo' }, - 2: { 'data-bar': 'bar' } + 1: { + attributes: { + 'data-foo': 'foo' + } + }, + 2: { + attributes: { + 'data-bar': 'bar' + } + } } } ); @@ -179,7 +208,11 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: { 'data-foo': 'foo' } + 1: { + attributes: { + 'data-foo': 'foo' + } + } } } ); @@ -198,7 +231,11 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { data: 'foobar', attributes: { - 1: { 'style': 'color:blue;' } + 1: { + styles: { + color: 'blue' + } + } } } ); From ca56a068f5ad2cce260e132ae04c8483e3f9461f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 1 Mar 2021 10:09:28 +0100 Subject: [PATCH 034/217] Minor refactoring. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2a7557df0af..112b0fd8f41 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -297,7 +297,11 @@ function matchAll( viewElement, rules ) { * @returns {Array} result.styles Array with matched style names. */ function mergeMatchResults( matches ) { - const matchResult = { attributes: new Set(), classes: new Set(), styles: new Set() }; + const matchResult = { + attributes: new Set(), + classes: new Set(), + styles: new Set() + }; for ( const match of matches ) { for ( const key in matchResult ) { From 8143ac355062e9ba431fe1349202ae3d0e84e13f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 1 Mar 2021 11:10:50 +0100 Subject: [PATCH 035/217] Flatten definitions for getDefinitionsForView method. --- .../src/datafilter.js | 6 +-- .../src/dataschema.js | 40 ++++++++++--------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 112b0fd8f41..2e9fa13036c 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -57,11 +57,7 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all view elements which should be allowed. */ allowElement( config ) { - for ( const definition of this._dataSchema.getDefinitionsForView( config.name ) ) { - for ( const reference of definition.references ) { - this._registerElement( reference ); - } - + for ( const definition of this._dataSchema.getDefinitionsForView( config.name, true ) ) { this._registerElement( definition ); } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 9766ccb2c9f..c402bac0534 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -7,7 +7,6 @@ * @module content-compatibility/dataschema */ -import { cloneDeep } from 'lodash-es'; import { toArray } from 'ckeditor5/src/utils'; /** @@ -122,32 +121,35 @@ export default class DataSchema { * Returns all definitions matching the given view name. * * @param {String} viewName - * @returns {Iterable<*>} + * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. + * @returns {Set<*>} */ - * getDefinitionsForView( viewName ) { - const definitions = Array.from( this._definitions.values() ) - .filter( def => def.view && testViewName( viewName, def.view ) ); + getDefinitionsForView( viewName, includeReferences ) { + const definitions = new Set(); + + for ( const definition of this._filterViewDefinitions( viewName ) ) { + if ( includeReferences ) { + for ( const reference of this._getReferences( definition.model ) ) { + definitions.add( reference ); + } + } - for ( const definition of definitions ) { - yield this.getDefinition( definition.model ); + definitions.add( definition ); } + + return definitions; } /** - * Returns definition for the given model name. - * - * Definition will also include `references` property including all definitions - * referenced by this definition. + * Filters definitions matching the given view name. * - * @param {String} modelName - * @returns {module:content-compatibility/dataschema~DataSchemaDefinition} + * @private + * @param {String} viewName + * @returns {Array} */ - getDefinition( modelName ) { - const definition = cloneDeep( this._definitions.get( modelName ) ); - - definition.references = this._getReferences( modelName ); - - return definition; + _filterViewDefinitions( viewName ) { + return Array.from( this._definitions.values() ) + .filter( def => def.view && testViewName( viewName, def.view ) ); } /** From e3f4b61099d46f5710aaeab5cddea70539cabbff Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 1 Mar 2021 11:22:21 +0100 Subject: [PATCH 036/217] Refactoring. --- .../src/datafilter.js | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2e9fa13036c..2ba7074f19d 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -166,18 +166,17 @@ export default class DataFilter { } } - const { attributes, classes, styles } = mergeMatchResults( matches ); + const { attributes, styles, classes } = mergeMatchResults( matches ); const viewAttributes = {}; // Stash attributes. if ( attributes.size ) { - const attributesObject = {}; - - for ( const attributeName of attributes ) { - attributesObject[ attributeName ] = viewElement.getAttribute( attributeName ); - } + viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); + } - viewAttributes.attributes = attributesObject; + // Stash styles. + if ( styles.size ) { + viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); } // Stash classes. @@ -185,17 +184,6 @@ export default class DataFilter { viewAttributes.classes = Array.from( classes ); } - // Stash styles. - if ( styles.size ) { - const stylesObj = {}; - - for ( const styleName of styles ) { - stylesObj[ styleName ] = viewElement.getStyle( styleName ); - } - - viewAttributes.styles = stylesObj; - } - const element = conversionApi.writer.createElement( modelName ); if ( Object.keys( viewAttributes ).length ) { @@ -308,3 +296,21 @@ function mergeMatchResults( matches ) { return matchResult; } + +/** + * Convertes the given iterable object into an object. + * + * @private + * @param {Iterable} iterable + * @param {Function} getValue Shoud result with value for the given object key. + * @returns {Object} + */ +function iterableToObject( iterable, getValue ) { + const attributesObject = {}; + + for ( const prop of iterable ) { + attributesObject[ prop ] = getValue( prop ); + } + + return attributesObject; +} From a0749849a34e67f23c564c4b8cbbdffadaed857c Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 1 Mar 2021 12:24:56 +0100 Subject: [PATCH 037/217] Removed leftover. --- .../tests/manual/generalhtmlsupport.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 0bd8fb2d7b2..8e1bba81c11 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -35,8 +35,7 @@ class ExtendHTMLSupport extends Plugin { view: 'xyz', model: 'ghsXyz', schema: { - inheritAllFrom: '$ghsBlock', - allowText: true + inheritAllFrom: '$ghsBlock' } } ); From ebf1f94c7471cd6384cce8a3d736e68c01e99a64 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 2 Mar 2021 16:25:05 +0100 Subject: [PATCH 038/217] Docs. --- .../src/datafilter.js | 29 ++++++++++++++- .../src/dataschema.js | 36 +++++++++++-------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2ba7074f19d..325e7a5b6f7 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -45,9 +45,36 @@ export default class DataFilter { constructor( editor, dataSchema ) { this.editor = editor; + /** + * An instance of the {@link module:content-compatibility/dataschema~DataSchema}. + * + * @readonly + * @private + * @member {module:content-compatibility/dataschema~DataSchema} module:content-compatibility/datafilter~DataFilter#_dataSchema + */ this._dataSchema = dataSchema; + /** + * A map of registered {@link module:engine/view/matcher~Matcher Matcher} instances. + * + * Describes rules upon which content attributes should be allowed. + * + * @readonly + * @private + * @member {Map} module:content-compatibility/datafilter~DataFilter#_allowedAttributes + */ this._allowedAttributes = new Map(); + + /** + * A map of registered {@link module:engine/view/matcher~Matcher Matcher} instances. + * + * Describes rules upon which content attributes should be disallowed. + * + * @readonly + * @private + * @member {Map} + * module:content-compatibility/datafilter~DataFilter#_disallowedAttributes + */ this._disallowedAttributes = new Map(); } @@ -87,7 +114,7 @@ export default class DataFilter { * * @private * @param {module:engine/view/matcher~MatcherPattern} config - * @param {Map} rules Rules map holding matchers. + * @param {Map} rules Rules map holding matchers. */ _addAttributeMatcher( config, rules ) { const viewName = config.name; diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index c402bac0534..919fcfd21a3 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -15,16 +15,24 @@ import { toArray } from 'ckeditor5/src/utils'; * * Data schema is represented by data schema definitions. To add new definition, use {@link #register} method: * - * dataSchema.register( { view: 'section', model: 'my-section' }, { - * inheritAllFrom: '$block' + * dataSchema.register( { + * view: 'section', + * model: 'my-section', + * schema: { + * inheritAllFrom: '$block' + * } * } ); - * - * Once registered, definition can be enabled in editor's model: - * - * dataSchema.enable( 'my-section' ); */ export default class DataSchema { constructor() { + /** + * A map of registered data schema definitions via {@link #register} method. + * + * @readonly + * @private + * @member {Map} + * module:content-compatibility/dataschema~DataSchema#_definitions + */ this._definitions = new Map(); // Add block elements. @@ -120,14 +128,14 @@ export default class DataSchema { /** * Returns all definitions matching the given view name. * - * @param {String} viewName + * @param {String|RegExp} viewName * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. - * @returns {Set<*>} + * @returns {Set} */ getDefinitionsForView( viewName, includeReferences ) { const definitions = new Set(); - for ( const definition of this._filterViewDefinitions( viewName ) ) { + for ( const definition of this._getMatchingViewDefinitions( viewName ) ) { if ( includeReferences ) { for ( const reference of this._getReferences( definition.model ) ) { definitions.add( reference ); @@ -141,13 +149,13 @@ export default class DataSchema { } /** - * Filters definitions matching the given view name. + * Returns definitions matching the given view name. * * @private - * @param {String} viewName + * @param {String|RegExp} viewName * @returns {Array} */ - _filterViewDefinitions( viewName ) { + _getMatchingViewDefinitions( viewName ) { return Array.from( this._definitions.values() ) .filter( def => def.view && testViewName( viewName, def.view ) ); } @@ -157,7 +165,7 @@ export default class DataSchema { * * @private * @param {String} modelName Data schema model name. - * @returns {Iterable} + * @returns {Iterable} */ * _getReferences( modelName ) { const { schema } = this._definitions.get( modelName ); @@ -202,7 +210,7 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} [view] Name of the view element. * @property {String} model Name of the model element. - * @property {String|module:engine/model/schema~SchemaItemDefinition} schema Name of the schema to inherit + * @property {module:engine/model/schema~SchemaItemDefinition} Model schema item definition describing registered model. * @property {String|Array} allowChildren Extends the given children list to allow definition model. * or custom schema item definition. */ From baea36afc39bfd59043eb0f6fa59c915ece8d431 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 3 Mar 2021 09:52:28 +0100 Subject: [PATCH 039/217] Updated block elements to allow only flow content. --- .../src/dataschema.js | 9 +- .../tests/datafilter.js | 161 +++++++++++------- .../tests/manual/generalhtmlsupport.html | 16 +- 3 files changed, 115 insertions(+), 71 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 919fcfd21a3..19ad2ed308b 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -40,8 +40,8 @@ export default class DataSchema { model: '$ghsBlock', allowChildren: '$block', schema: { - inheritAllFrom: '$block', - allowIn: '$ghsBlock' + allowIn: [ '$root', '$ghsBlock' ], + isBlock: true } } ); @@ -73,11 +73,10 @@ export default class DataSchema { this.register( { model: '$ghsDatalist', - allowChildren: '$text', + allowChildren: '$block', schema: { allowIn: 'ghsDl', - isBlock: true, - allowContentOf: '$ghsBlock' + isBlock: true } } ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 71abda893c6..49215bea8cb 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -4,6 +4,7 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import DataSchema from '../src/dataschema'; import DataFilter from '../src/datafilter'; @@ -14,7 +15,9 @@ describe( 'DataFilter', () => { beforeEach( () => { return VirtualTestEditor - .create() + .create( { + plugins: [ Paragraph ] + } ) .then( newEditor => { editor = newEditor; model = editor.model; @@ -30,40 +33,61 @@ describe( 'DataFilter', () => { it( 'should allow element', () => { dataFilter.allowElement( { name: 'article' } ); - editor.setData( '
section1
section2
' ); + editor.setData( '
' + + '
section1
' + + '
section2
' + ); - expect( getModelData( model, { withoutSelection: true } ) ).to.eq( - 'section1section2' + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'section1section2' ); - expect( editor.getData() ).to.eq( - '
section1section2
' + expect( editor.getData() ).to.equal( + '

section1section2

' ); dataFilter.allowElement( { name: 'section' } ); - editor.setData( '
section1
section2
' ); + editor.setData( '
' + + '
section1
' + + '
section2
' + ); - expect( getModelData( model, { withoutSelection: true } ) ).to.eq( - 'section1section2' + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '' + + 'section1' + + 'section2' ); - expect( editor.getData() ).to.eq( - '
section1
section2
' + expect( editor.getData() ).to.equal( + '
' + + '

section1

' + + '

section2

' ); } ); it( 'should allow deeply nested structure', () => { dataFilter.allowElement( { name: 'section' } ); - editor.setData( '
1
2
3
' ); + editor.setData( + '

1

' + + '

2

' + + '

3

' + + '
' + ); - expect( getModelData( model, { withoutSelection: true } ) ).to.eq( - '123' + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '1' + + '2' + + '3' + + '' ); - expect( editor.getData() ).to.eq( - '
1
2
3
' + expect( editor.getData() ).to.equal( + '

1

' + + '

2

' + + '

3

' + + '
' ); } ); @@ -73,10 +97,10 @@ describe( 'DataFilter', () => { 'data-foo': 'foobar' } } ); - editor.setData( '
foobar
' ); + editor.setData( '

foobar

' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', attributes: { 1: { attributes: { @@ -86,8 +110,8 @@ describe( 'DataFilter', () => { } } ); - expect( editor.getData() ).to.eq( - '
foobar
' + expect( editor.getData() ).to.equal( + '

foobar

' ); } ); @@ -98,10 +122,10 @@ describe( 'DataFilter', () => { 'background-color': 'blue' } } ); - editor.setData( '
foobar
' ); + editor.setData( '

foobar

' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', attributes: { 1: { styles: { @@ -112,8 +136,8 @@ describe( 'DataFilter', () => { } } ); - expect( editor.getData() ).to.eq( - '
foobar
' + expect( editor.getData() ).to.equal( + '

foobar

' ); } ); @@ -121,17 +145,17 @@ describe( 'DataFilter', () => { dataFilter.allowElement( { name: 'section' } ); dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); - editor.setData( '
foobar
' ); + editor.setData( '

foobar

' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', attributes: { 1: { classes: [ 'foo', 'bar' ] } } } ); - expect( editor.getData() ).to.eq( - '
foobar
' + expect( editor.getData() ).to.equal( + '

foobar

' ); } ); @@ -140,15 +164,16 @@ describe( 'DataFilter', () => { dataFilter.allowAttributes( { name: /[^]/, attributes: { 'data-foo': /foo|bar/ } } ); editor.setData( '
' + - '
section1
' + - '
section2
' + - '
' ); + '

section1

' + + '

section2

' + + '
' + ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - 'section1' + - 'section2' + - '', + 'section1' + + 'section2' + + '', attributes: { 1: { attributes: { @@ -175,10 +200,14 @@ describe( 'DataFilter', () => { dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-foo': 'foo' } } ); dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-bar': 'bar' } } ); - editor.setData( '
foo
bar
' ); + editor.setData( + '

foo

' + + '

bar

' + ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', attributes: { 1: { attributes: { @@ -193,8 +222,9 @@ describe( 'DataFilter', () => { } } ); - expect( editor.getData() ).to.eq( - '
foo
bar
' + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' ); } ); @@ -203,10 +233,14 @@ describe( 'DataFilter', () => { dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': /[^]/ } } ); dataFilter.disallowAttributes( { name: 'section', attributes: { 'data-foo': 'bar' } } ); - editor.setData( '
foo
bar
' ); + editor.setData( + '

foo

' + + '

bar

' + ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', attributes: { 1: { attributes: { @@ -216,8 +250,9 @@ describe( 'DataFilter', () => { } } ); - expect( editor.getData() ).to.eq( - '
foo
bar
' + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' ); } ); @@ -226,10 +261,14 @@ describe( 'DataFilter', () => { dataFilter.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); dataFilter.disallowAttributes( { name: 'section', styles: { color: 'red' } } ); - editor.setData( '
foo
bar
' ); + editor.setData( + '

foo

' + + '

bar

' + ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', attributes: { 1: { styles: { @@ -239,8 +278,9 @@ describe( 'DataFilter', () => { } } ); - expect( editor.getData() ).to.eq( - '
foo
bar
' + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' ); } ); @@ -249,15 +289,20 @@ describe( 'DataFilter', () => { dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); dataFilter.disallowAttributes( { name: 'section', classes: [ 'bar' ] } ); - editor.setData( '
foo
bar
' ); + editor.setData( + '

foo

' + + '

bar

' + ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.eq( { - data: 'foobar', + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', attributes: {} } ); - expect( editor.getData() ).to.eq( - '
foo
bar
' + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' ); } ); } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index 140129ab86e..3d5c4a5ccb3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -4,19 +4,19 @@
-
Section #1
-
Section #2
-
Section #3
+

Section #1

+

Section #2

+

Section #3

Summary Hello world
-
dt1
-
dt2
-
dd1
-
dd2
+

dt1

+

dt2

+

dd1

+

dd2

- XYZ +

XYZ

From 78b30d0a83fdfd7b8f11626d69785cc8967344ec Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 3 Mar 2021 12:14:55 +0100 Subject: [PATCH 040/217] Added test coverage for DataSchema. --- .../tests/dataschema.js | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 packages/ckeditor5-content-compatibility/tests/dataschema.js diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js new file mode 100644 index 00000000000..ace286e6117 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -0,0 +1,153 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import DataSchema from '../src/dataschema'; + +const fakeDefinitions = [ + { + view: 'def1', + model: 'ghsDef1', + allowChildren: [ 'ghsDef2', 'ghsDef3' ], + schema: { + inheritAllFrom: '$block' + } + }, + { + view: 'def2', + model: 'ghsDef2', + schema: { + inheritAllFrom: 'ghsDef1' + } + }, + { + view: 'def3', + model: 'ghsDef3', + schema: { + inheritTypesFrom: 'ghsDef2' + } + }, + { + view: 'def4', + model: 'ghsDef4', + schema: { + allowWhere: 'ghsDef3' + } + }, + { + view: 'def5', + model: 'ghsDef5', + schema: { + allowContentOf: 'ghsDef4' + } + }, + { + view: 'def6', + model: 'ghsDef6', + schema: { + allowAttributesOf: 'ghsDef5' + } + } +]; + +describe( 'DataSchema', () => { + let editor, dataSchema; + + beforeEach( () => { + return VirtualTestEditor + .create() + .then( newEditor => { + editor = newEditor; + + dataSchema = new DataSchema(); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should allow registering schema with proper definition', () => { + const definitions = getFakeDefinitions( 'def1' ); + + dataSchema.register( definitions[ 0 ] ); + + const result = dataSchema.getDefinitionsForView( 'def1' ); + + expect( Array.from( result ) ).to.deep.equal( definitions ); + } ); + + it( 'should allow resolving definitions by view name (string)', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( 'def2' ); + + expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def2' ) ); + } ); + + it( 'should allow resolving definitions by view name (RegExp)', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( /^def1|def2$/ ); + + expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2' ) ); + } ); + + it( 'should allow resolving definitions by view name including references (inheritAllFrom)', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( 'def2', true ); + + expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2' ) ); + } ); + + it( 'should allow resolving definitions by view name including references (inheritTypes)', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( 'def3', true ); + + expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3' ) ); + } ); + + it( 'should allow resolving definitions by view name including references (allowWhere)', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( 'def4', true ); + + expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3', 'def4' ) ); + } ); + + it( 'should allow resolving definitions by view name including references (allowContentOf)', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( 'def5', true ); + + expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3', 'def4', 'def5' ) ); + } ); + + it( 'should allow resolving definitions by view name including references (allowAttributesOf)', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( 'def6', true ); + + expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3', 'def4', 'def5', 'def6' ) ); + } ); + + it( 'should return nothing for invalid view name', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( null ); + + expect( result.size ).to.equal( 0 ); + } ); +} ); + +function registerMany( dataSchema, definitions ) { + definitions.forEach( def => dataSchema.register( def ) ); +} + +function getFakeDefinitions( ...viewNames ) { + return fakeDefinitions.filter( def => viewNames.includes( def.view ) ); +} From 878d544154cfda6eda08af527fce557706a39ef4 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 3 Mar 2021 16:11:25 +0100 Subject: [PATCH 041/217] Removed unreachable code. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 325e7a5b6f7..e1423ae5a17 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -233,10 +233,6 @@ export default class DataFilter { dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }:${ modelName }`, ( evt, data, conversionApi ) => { const viewAttributes = data.attributeNewValue; - if ( viewAttributes === null ) { - return; - } - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { return; } From 274500b55308b84f8dc9d3ad520aa3c7b7df28a0 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 3 Mar 2021 16:13:24 +0100 Subject: [PATCH 042/217] Added test coverage for DataFilter. --- .../tests/datafilter.js | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 49215bea8cb..c800b0e11c1 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -11,7 +11,7 @@ import DataFilter from '../src/datafilter'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'DataFilter', () => { - let editor, model, dataFilter; + let editor, model, dataFilter, dataSchema; beforeEach( () => { return VirtualTestEditor @@ -22,7 +22,8 @@ describe( 'DataFilter', () => { editor = newEditor; model = editor.model; - dataFilter = new DataFilter( editor, new DataSchema() ); + dataSchema = new DataSchema(); + dataFilter = new DataFilter( editor, dataSchema ); } ); } ); @@ -305,6 +306,68 @@ describe( 'DataFilter', () => { '

bar

' ); } ); + + it( 'should extend allowed children only if specified model schema exists', () => { + dataSchema.register( { + view: 'xyz', + model: 'ghsXyz', + allowChildren: 'not-exists', + schema: { + inheritAllFrom: '$ghsBlock' + } + } ); + + expect( () => { + dataFilter.allowElement( { name: 'xyz' } ); + } ).to.not.throw(); + } ); + + it( 'should not consume attribute already consumed (upcast)', () => { + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:section', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.viewItem, { attributes: [ 'data-foo' ] } ); + } ); + } ); + + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + attributes: {} + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should not consume attribute already consumed (downcast)', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( 'attribute:ghsAttributes:ghsSection', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, evt.name ); + }, { priority: 'high' } ); + } ); + + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + // At this point, attribute should still be in the model, as we are testing downcast conversion. + attributes: { + 1: { + attributes: { + 'data-foo': '' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); } ); function getModelDataWithAttributes( model, options ) { From 9fffed12e8a2d9d5f43918f77380433091e36e3a Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 3 Mar 2021 17:07:01 +0100 Subject: [PATCH 043/217] Added package.json file. --- .../package.json | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 packages/ckeditor5-content-compatibility/package.json diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json new file mode 100644 index 00000000000..89bb1f11e13 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/package.json @@ -0,0 +1,50 @@ +{ + "name": "@ckeditor/ckeditor5-content-compatibility", + "version": "25.0.0", + "description": "Content compatibility feature", + "private": true, + "keywords": [ + "ckeditor", + "ckeditor4", + "ckeditor 4", + "ckeditor5", + "ckeditor 5", + "ckeditor5-feature", + "ckeditor5-plugin", + "WYSIWYG", + "WYSIWYM", + "text", + "rich-text", + "richtext", + "ckeditor", + "editor", + "editing", + "html", + "contentEditable", + "compatibility", + "content compatibility" + ], + "dependencies": { + "ckeditor5": "^25.0.0", + "lodash-es": "^4.17.15" + }, + "devDependencies": { + "@ckeditor/ckeditor5-basic-styles": "^25.0.0", + "@ckeditor/ckeditor5-block-quote": "^25.0.0", + "@ckeditor/ckeditor5-core": ">=19.0.0", + "@ckeditor/ckeditor5-list": "^25.0.0", + "@ckeditor/ckeditor5-paragraph": "^25.0.0", + "@ckeditor/ckeditor5-utils": "^25.0.0" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.7.1" + }, + "license": "SEE LICENSE IN LICENSE.md", + "author": "CKSource (http://cksource.com/)", + "homepage": "https://ckeditor.com", + "bugs": "https://github.com/ckeditor/ckeditor5/issues", + "files": [ + "src" + ] +} From 88f0659c1ff1f1815287f0db166c0c69c70838d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20Bogda=C5=84ski?= Date: Fri, 5 Mar 2021 09:02:16 +0100 Subject: [PATCH 044/217] Corrected docs. Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- .../src/datafilter.js | 11 +++++------ .../src/dataschema.js | 13 ++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e1423ae5a17..e0d1682e30d 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -50,7 +50,7 @@ export default class DataFilter { * * @readonly * @private - * @member {module:content-compatibility/dataschema~DataSchema} module:content-compatibility/datafilter~DataFilter#_dataSchema + * @member {module:content-compatibility/dataschema~DataSchema} #_dataSchema */ this._dataSchema = dataSchema; @@ -61,7 +61,7 @@ export default class DataFilter { * * @readonly * @private - * @member {Map} module:content-compatibility/datafilter~DataFilter#_allowedAttributes + * @member {Map.} #_allowedAttributes */ this._allowedAttributes = new Map(); @@ -72,8 +72,7 @@ export default class DataFilter { * * @readonly * @private - * @member {Map} - * module:content-compatibility/datafilter~DataFilter#_disallowedAttributes + * @member {Map.} #_disallowedAttributes */ this._disallowedAttributes = new Map(); } @@ -114,7 +113,7 @@ export default class DataFilter { * * @private * @param {module:engine/view/matcher~MatcherPattern} config - * @param {Map} rules Rules map holding matchers. + * @param {Map.} rules Rules map holding matchers. */ _addAttributeMatcher( config, rules ) { const viewName = config.name; @@ -324,7 +323,7 @@ function mergeMatchResults( matches ) { * Convertes the given iterable object into an object. * * @private - * @param {Iterable} iterable + * @param {Iterable.} iterable * @param {Function} getValue Shoud result with value for the given object key. * @returns {Object} */ diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 19ad2ed308b..344c4f3bce0 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -30,8 +30,7 @@ export default class DataSchema { * * @readonly * @private - * @member {Map} - * module:content-compatibility/dataschema~DataSchema#_definitions + * @member {Map.} #_definitions */ this._definitions = new Map(); @@ -129,7 +128,7 @@ export default class DataSchema { * * @param {String|RegExp} viewName * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. - * @returns {Set} + * @returns {Set.} */ getDefinitionsForView( viewName, includeReferences ) { const definitions = new Set(); @@ -152,7 +151,7 @@ export default class DataSchema { * * @private * @param {String|RegExp} viewName - * @returns {Array} + * @returns {Array.} */ _getMatchingViewDefinitions( viewName ) { return Array.from( this._definitions.values() ) @@ -164,7 +163,7 @@ export default class DataSchema { * * @private * @param {String} modelName Data schema model name. - * @returns {Iterable} + * @returns {Iterable.} */ * _getReferences( modelName ) { const { schema } = this._definitions.get( modelName ); @@ -209,7 +208,7 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} [view] Name of the view element. * @property {String} model Name of the model element. - * @property {module:engine/model/schema~SchemaItemDefinition} Model schema item definition describing registered model. - * @property {String|Array} allowChildren Extends the given children list to allow definition model. + * @property {module:engine/model/schema~SchemaItemDefinition} schema The model schema item definition describing registered model. + * @property {String|Array.} allowChildren Extends the given children list to allow definition model. * or custom schema item definition. */ From 5b7e58aa1d1b36c761b977fb2c39a4f91ee441ae Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 5 Mar 2021 09:20:23 +0100 Subject: [PATCH 045/217] Updated package.json. --- .../package.json | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index 89bb1f11e13..ef59dc79cd2 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -1,40 +1,30 @@ { "name": "@ckeditor/ckeditor5-content-compatibility", - "version": "25.0.0", + "version": "26.0.0", "description": "Content compatibility feature", "private": true, "keywords": [ "ckeditor", - "ckeditor4", - "ckeditor 4", "ckeditor5", "ckeditor 5", "ckeditor5-feature", "ckeditor5-plugin", - "WYSIWYG", - "WYSIWYM", - "text", - "rich-text", - "richtext", - "ckeditor", - "editor", - "editing", - "html", - "contentEditable", "compatibility", - "content compatibility" + "content compatibility", + "generic html support" ], + "main": "src/index.js", "dependencies": { - "ckeditor5": "^25.0.0", + "ckeditor5": "^26.0.0", "lodash-es": "^4.17.15" }, "devDependencies": { - "@ckeditor/ckeditor5-basic-styles": "^25.0.0", - "@ckeditor/ckeditor5-block-quote": "^25.0.0", - "@ckeditor/ckeditor5-core": ">=19.0.0", - "@ckeditor/ckeditor5-list": "^25.0.0", - "@ckeditor/ckeditor5-paragraph": "^25.0.0", - "@ckeditor/ckeditor5-utils": "^25.0.0" + "@ckeditor/ckeditor5-basic-styles": "^26.0.0", + "@ckeditor/ckeditor5-block-quote": "^26.0.0", + "@ckeditor/ckeditor5-core": "^26.0.0", + "@ckeditor/ckeditor5-list": "^26.0.0", + "@ckeditor/ckeditor5-paragraph": "^26.0.0", + "@ckeditor/ckeditor5-utils": "^26.0.0" }, "engines": { "node": ">=12.0.0", From f88cdf5f64f9bcd6ffd166d75f5524ef4809440d Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 5 Mar 2021 10:04:21 +0100 Subject: [PATCH 046/217] Refactoring. --- .../src/datafilter.js | 74 +++++++++---------- .../src/dataschema.js | 14 ++-- 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e0d1682e30d..0faa030223c 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -7,11 +7,11 @@ * @module content-compatibility/datafilter */ -import { cloneDeep } from 'lodash-es'; - import { Matcher } from 'ckeditor5/src/engine'; import { priorities, toArray } from 'ckeditor5/src/utils'; +import { cloneDeep } from 'lodash-es'; + const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; /** @@ -257,16 +257,14 @@ export default class DataFilter { } } -/** - * Helper function restoring matcher for the given key from `rules` object. - * - * If matcher for the given key does not exist, this function will create a new one - * inside `rules` object under the given key. - * - * @private - * @param {String} key - * @param {Map} rules - */ +// Helper function restoring matcher for the given key from `rules` object. + +// If matcher for the given key does not exist, this function will create a new one +// inside `rules` object under the given key. + +// @private +// @param {String} key +// @param {Map.} rules function getOrCreateMatcher( key, rules ) { if ( !rules.has( key ) ) { rules.set( key, new Matcher() ); @@ -275,33 +273,29 @@ function getOrCreateMatcher( key, rules ) { return rules.get( key ); } -/** - * Alias for {@link module:engine/view/matcher~Matcher#matchAll matchAll}. - * - * @private - * @param {module:engine/view/element~Element} viewElement - * @param {Map} rules Rules map holding matchers. - * @returns {Object} result - * @returns {Array} result.attributes Array with matched attribute names. - * @returns {Array} result.classes Array with matched class names. - * @returns {Array} result.styles Array with matched style names. - */ +// Alias for {@link module:engine/view/matcher~Matcher#matchAll matchAll}. + +// @private +// @param {module:engine/view/element~Element} viewElement +// @param {Map} rules Rules map holding matchers. +// @returns {Object} result +// @returns {Array.} result.attributes Array with matched attribute names. +// @returns {Array.} result.classes Array with matched class names. +// @returns {Array.} result.styles Array with matched style names. function matchAll( viewElement, rules ) { const matcher = getOrCreateMatcher( viewElement.name, rules ); return matcher.matchAll( viewElement ) || []; } -/** - * Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. - * - * @private - * @param {Array} matches - * @returns {Object} result - * @returns {Array} result.attributes Array with matched attribute names. - * @returns {Array} result.classes Array with matched class names. - * @returns {Array} result.styles Array with matched style names. - */ +// Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. + +// @private +// @param {Array} matches +// @returns {Object} result +// @returns {Array.} result.attributes Array with matched attribute names. +// @returns {Array.} result.classes Array with matched class names. +// @returns {Array.} result.styles Array with matched style names. function mergeMatchResults( matches ) { const matchResult = { attributes: new Set(), @@ -319,14 +313,12 @@ function mergeMatchResults( matches ) { return matchResult; } -/** - * Convertes the given iterable object into an object. - * - * @private - * @param {Iterable.} iterable - * @param {Function} getValue Shoud result with value for the given object key. - * @returns {Object} - */ +// Convertes the given iterable object into an object. + +// @private +// @param {Iterable.} iterable +// @param {Function} getValue Shoud result with value for the given object key. +// @returns {Object} function iterableToObject( iterable, getValue ) { const attributesObject = {}; diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 344c4f3bce0..9c3a813acb9 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -182,14 +182,12 @@ export default class DataSchema { } } -/** - * Test view name against the given pattern. - * - * @private - * @param {String|RegExp} pattern - * @param {String} viewName - * @returns {Boolean} - */ +// Test view name against the given pattern. + +// @private +// @param {String|RegExp} pattern +// @param {String} viewName +// @returns {Boolean} function testViewName( pattern, viewName ) { if ( typeof pattern === 'string' ) { return pattern === viewName; From 89d8e4b90fd2b822dac72e925fee36740443e4df Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 5 Mar 2021 10:11:59 +0100 Subject: [PATCH 047/217] Corrected docs indentation. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 9c3a813acb9..35e1a30d88b 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -30,7 +30,7 @@ export default class DataSchema { * * @readonly * @private - * @member {Map.} #_definitions + * @member {Map.} #_definitions */ this._definitions = new Map(); From af7781dfa1fd31d9858aca5d9d14b67cb9b78a9f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 10 Mar 2021 10:29:13 +0100 Subject: [PATCH 048/217] Renaming from ghs to html. --- .../src/datafilter.js | 2 +- .../src/dataschema.js | 36 +++++------ .../tests/datafilter.js | 64 +++++++++---------- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 0faa030223c..435c4828181 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -12,7 +12,7 @@ import { priorities, toArray } from 'ckeditor5/src/utils'; import { cloneDeep } from 'lodash-es'; -const DATA_SCHEMA_ATTRIBUTE_KEY = 'ghsAttributes'; +const DATA_SCHEMA_ATTRIBUTE_KEY = 'htmlAttributes'; /** * Allows to validate elements and element attributes registered by {@link module:content-compatibility/dataschema~DataSchema}. diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 35e1a30d88b..5252b00d8e0 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -36,80 +36,80 @@ export default class DataSchema { // Add block elements. this.register( { - model: '$ghsBlock', + model: '$htmlBlock', allowChildren: '$block', schema: { - allowIn: [ '$root', '$ghsBlock' ], + allowIn: [ '$root', '$htmlBlock' ], isBlock: true } } ); this.register( { view: 'article', - model: 'ghsArticle', + model: 'htmlArticle', schema: { - inheritAllFrom: '$ghsBlock' + inheritAllFrom: '$htmlBlock' } } ); this.register( { view: 'section', - model: 'ghsSection', + model: 'htmlSection', schema: { - inheritAllFrom: '$ghsBlock' + inheritAllFrom: '$htmlBlock' } } ); // Add data list elements. this.register( { view: 'dl', - model: 'ghsDl', + model: 'htmlDl', schema: { - allowIn: [ '$ghsBlock', '$root' ], + allowIn: [ '$htmlBlock', '$root' ], isBlock: true } } ); this.register( { - model: '$ghsDatalist', + model: '$htmlDatalist', allowChildren: '$block', schema: { - allowIn: 'ghsDl', + allowIn: 'htmlDl', isBlock: true } } ); this.register( { view: 'dt', - model: 'ghsDt', + model: 'htmlDt', schema: { - inheritAllFrom: '$ghsDatalist' + inheritAllFrom: '$htmlDatalist' } } ); this.register( { view: 'dd', - model: 'ghsDd', + model: 'htmlDd', schema: { - inheritAllFrom: '$ghsDatalist' + inheritAllFrom: '$htmlDatalist' } } ); // Add details elements. this.register( { view: 'details', - model: 'ghsDetails', + model: 'htmlDetails', schema: { - inheritAllFrom: '$ghsBlock' + inheritAllFrom: '$htmlBlock' } } ); this.register( { view: 'summary', - model: 'ghsSummary', + model: 'htmlSummary', allowChildren: '$text', schema: { - allowIn: 'ghsDetails' + allowIn: 'htmlDetails' } } ); } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index c800b0e11c1..1caccd91d84 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -40,7 +40,7 @@ describe( 'DataFilter', () => { ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'section1section2' + 'section1section2' ); expect( editor.getData() ).to.equal( @@ -55,9 +55,9 @@ describe( 'DataFilter', () => { ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - 'section1' + - 'section2' + '' + + 'section1' + + 'section2' ); expect( editor.getData() ).to.equal( @@ -78,10 +78,10 @@ describe( 'DataFilter', () => { ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '1' + - '2' + - '3' + - '' + '1' + + '2' + + '3' + + '' ); expect( editor.getData() ).to.equal( @@ -101,7 +101,7 @@ describe( 'DataFilter', () => { editor.setData( '

foobar

' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foobar', + data: 'foobar', attributes: { 1: { attributes: { @@ -126,7 +126,7 @@ describe( 'DataFilter', () => { editor.setData( '

foobar

' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foobar', + data: 'foobar', attributes: { 1: { styles: { @@ -149,7 +149,7 @@ describe( 'DataFilter', () => { editor.setData( '

foobar

' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foobar', + data: 'foobar', attributes: { 1: { classes: [ 'foo', 'bar' ] } } @@ -171,10 +171,10 @@ describe( 'DataFilter', () => { ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '' + - 'section1' + - 'section2' + - '', + data: '' + + 'section1' + + 'section2' + + '', attributes: { 1: { attributes: { @@ -207,8 +207,8 @@ describe( 'DataFilter', () => { ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', + data: 'foo' + + 'bar', attributes: { 1: { attributes: { @@ -240,8 +240,8 @@ describe( 'DataFilter', () => { ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', + data: 'foo' + + 'bar', attributes: { 1: { attributes: { @@ -268,8 +268,8 @@ describe( 'DataFilter', () => { ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', + data: 'foo' + + 'bar', attributes: { 1: { styles: { @@ -296,8 +296,8 @@ describe( 'DataFilter', () => { ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', + data: 'foo' + + 'bar', attributes: {} } ); @@ -310,10 +310,10 @@ describe( 'DataFilter', () => { it( 'should extend allowed children only if specified model schema exists', () => { dataSchema.register( { view: 'xyz', - model: 'ghsXyz', + model: 'htmlXyz', allowChildren: 'not-exists', schema: { - inheritAllFrom: '$ghsBlock' + inheritAllFrom: '$htmlBlock' } } ); @@ -335,7 +335,7 @@ describe( 'DataFilter', () => { editor.setData( '

foo

' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo', + data: 'foo', attributes: {} } ); @@ -344,7 +344,7 @@ describe( 'DataFilter', () => { it( 'should not consume attribute already consumed (downcast)', () => { editor.conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( 'attribute:ghsAttributes:ghsSection', ( evt, data, conversionApi ) => { + dispatcher.on( 'attribute:htmlAttributes:htmlSection', ( evt, data, conversionApi ) => { conversionApi.consumable.consume( data.item, evt.name ); }, { priority: 'high' } ); } ); @@ -355,7 +355,7 @@ describe( 'DataFilter', () => { editor.setData( '

foo

' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo', + data: 'foo', // At this point, attribute should still be in the model, as we are testing downcast conversion. attributes: { 1: { @@ -373,16 +373,16 @@ describe( 'DataFilter', () => { function getModelDataWithAttributes( model, options ) { // Simplify GHS attributes as they are not very readable at this point due to object structure. let counter = 1; - const data = getModelData( model, options ).replace( /ghsAttributes="{(.*?)}"/g, () => { - return `ghsAttributes="(${ counter++ })"`; + const data = getModelData( model, options ).replace( /htmlAttributes="{(.*?)}"/g, () => { + return `htmlAttributes="(${ counter++ })"`; } ); const range = model.createRangeIn( model.document.getRoot() ); let attributes = []; for ( const item of range.getItems() ) { - if ( item.hasAttribute && item.hasAttribute( 'ghsAttributes' ) ) { - attributes.push( item.getAttribute( 'ghsAttributes' ) ); + if ( item.hasAttribute && item.hasAttribute( 'htmlAttributes' ) ) { + attributes.push( item.getAttribute( 'htmlAttributes' ) ); } } From c90ebf07643b78c7a1eb2dabb0f1bc6759aabfb4 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 10 Mar 2021 10:31:57 +0100 Subject: [PATCH 049/217] Renaming from ghs to html (part2). --- .../tests/dataschema.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index ace286e6117..84fe698780d 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -9,45 +9,45 @@ import DataSchema from '../src/dataschema'; const fakeDefinitions = [ { view: 'def1', - model: 'ghsDef1', - allowChildren: [ 'ghsDef2', 'ghsDef3' ], + model: 'htmlDef1', + allowChildren: [ 'htmlDef2', 'htmlDef3' ], schema: { inheritAllFrom: '$block' } }, { view: 'def2', - model: 'ghsDef2', + model: 'htmlDef2', schema: { - inheritAllFrom: 'ghsDef1' + inheritAllFrom: 'htmlDef1' } }, { view: 'def3', - model: 'ghsDef3', + model: 'htmlDef3', schema: { - inheritTypesFrom: 'ghsDef2' + inheritTypesFrom: 'htmlDef2' } }, { view: 'def4', - model: 'ghsDef4', + model: 'htmlDef4', schema: { - allowWhere: 'ghsDef3' + allowWhere: 'htmlDef3' } }, { view: 'def5', - model: 'ghsDef5', + model: 'htmlDef5', schema: { - allowContentOf: 'ghsDef4' + allowContentOf: 'htmlDef4' } }, { view: 'def6', - model: 'ghsDef6', + model: 'htmlDef6', schema: { - allowAttributesOf: 'ghsDef5' + allowAttributesOf: 'htmlDef5' } } ]; From 97c4ccb7dfe7f170b05ce6fdd870081ce03e56a2 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 11 Mar 2021 13:53:25 +0100 Subject: [PATCH 050/217] Minor docs correction. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 5252b00d8e0..a2ca75b3745 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -207,6 +207,5 @@ function testViewName( pattern, viewName ) { * @property {String} [view] Name of the view element. * @property {String} model Name of the model element. * @property {module:engine/model/schema~SchemaItemDefinition} schema The model schema item definition describing registered model. - * @property {String|Array.} allowChildren Extends the given children list to allow definition model. - * or custom schema item definition. + * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. */ From fdb4d6c59a9e3e268bc20671dc83e14d988dd4f6 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:11:51 +0100 Subject: [PATCH 051/217] Typo fix. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 435c4828181..080df5782c9 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -100,7 +100,7 @@ export default class DataFilter { } /** - * Disallowe the given attributes for view element allowed by {@link #allowElement} method. + * Disallow the given attributes for view element allowed by {@link #allowElement} method. * * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be disallowed. */ From eb4bd1a5e5d290abdbf15b6b28f4808cb756687c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:13:54 +0100 Subject: [PATCH 052/217] Added missing empty line. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 080df5782c9..079752c97ec 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -119,6 +119,7 @@ export default class DataFilter { const viewName = config.name; config = cloneDeep( config ); + // We don't want match by name when matching attributes. Matcher will be already attached to specific definition. delete config.name; From bd1920f598fd4c3511cd16cef6d6067964075b7a Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:20:25 +0100 Subject: [PATCH 053/217] Added missing empty line. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 079752c97ec..df8e462be9d 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -187,6 +187,7 @@ export default class DataFilter { view: viewName, model: ( viewElement, conversionApi ) => { const matches = []; + for ( const match of matchAll( viewElement, this._allowedAttributes ) ) { if ( conversionApi.consumable.consume( viewElement, match.match ) ) { matches.push( match ); From c4c2f6821426c2d825adb7e4c5029d48774bb581 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:38:56 +0100 Subject: [PATCH 054/217] Apply suggestions from code review --- packages/ckeditor5-content-compatibility/src/datafilter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index df8e462be9d..bb12c1d3212 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -260,10 +260,10 @@ export default class DataFilter { } // Helper function restoring matcher for the given key from `rules` object. - +// // If matcher for the given key does not exist, this function will create a new one // inside `rules` object under the given key. - +// // @private // @param {String} key // @param {Map.} rules From 681fead7cbbfdedce66662e8c424ddc576538a62 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:41:47 +0100 Subject: [PATCH 055/217] Apply suggestions from code review --- packages/ckeditor5-content-compatibility/src/datafilter.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index bb12c1d3212..a9ee0cbe81d 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -267,6 +267,7 @@ export default class DataFilter { // @private // @param {String} key // @param {Map.} rules +// @returns {module:engine/view/matcher~Matcher} function getOrCreateMatcher( key, rules ) { if ( !rules.has( key ) ) { rules.set( key, new Matcher() ); @@ -276,10 +277,10 @@ function getOrCreateMatcher( key, rules ) { } // Alias for {@link module:engine/view/matcher~Matcher#matchAll matchAll}. - +// // @private // @param {module:engine/view/element~Element} viewElement -// @param {Map} rules Rules map holding matchers. +// @param {Map.} rules Rules map holding matchers. // @returns {Object} result // @returns {Array.} result.attributes Array with matched attribute names. // @returns {Array.} result.classes Array with matched class names. From 819ccfc90faf8e02f80b31852d0147013f8f51ee Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:44:29 +0100 Subject: [PATCH 056/217] Apply suggestions from code review --- .../ckeditor5-content-compatibility/src/datafilter.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index a9ee0cbe81d..912f7bfa4b1 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -292,13 +292,13 @@ function matchAll( viewElement, rules ) { } // Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. - +// // @private // @param {Array} matches // @returns {Object} result -// @returns {Array.} result.attributes Array with matched attribute names. -// @returns {Array.} result.classes Array with matched class names. -// @returns {Array.} result.styles Array with matched style names. +// @returns {Set.} result.attributes Set with matched attribute names. +// @returns {Set.} result.classes Set with matched class names. +// @returns {Set.} result.styles Set with matched style names. function mergeMatchResults( matches ) { const matchResult = { attributes: new Set(), @@ -309,6 +309,7 @@ function mergeMatchResults( matches ) { for ( const match of matches ) { for ( const key in matchResult ) { const values = match.match[ key ] || []; + values.forEach( value => matchResult[ key ].add( value ) ); } } @@ -317,7 +318,7 @@ function mergeMatchResults( matches ) { } // Convertes the given iterable object into an object. - +// // @private // @param {Iterable.} iterable // @param {Function} getValue Shoud result with value for the given object key. From 838b68e4f6053af17cdbddb82a2c8f2a68953adb Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:47:26 +0100 Subject: [PATCH 057/217] Apply suggestions from code review --- packages/ckeditor5-content-compatibility/src/datafilter.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 912f7bfa4b1..b80583faea2 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -281,10 +281,7 @@ function getOrCreateMatcher( key, rules ) { // @private // @param {module:engine/view/element~Element} viewElement // @param {Map.} rules Rules map holding matchers. -// @returns {Object} result -// @returns {Array.} result.attributes Array with matched attribute names. -// @returns {Array.} result.classes Array with matched class names. -// @returns {Array.} result.styles Array with matched style names. +// @returns {Array.} Array with match information about found elements. function matchAll( viewElement, rules ) { const matcher = getOrCreateMatcher( viewElement.name, rules ); From 9b447fb3b69c5767c49cac67924d83a98fe3603c Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:49:26 +0100 Subject: [PATCH 058/217] Apply suggestions from code review --- packages/ckeditor5-content-compatibility/src/dataschema.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index a2ca75b3745..320d7ebdc35 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -183,7 +183,7 @@ export default class DataSchema { } // Test view name against the given pattern. - +// // @private // @param {String|RegExp} pattern // @param {String} viewName @@ -201,7 +201,7 @@ function testViewName( pattern, viewName ) { } /** - * A definition of {@link module:content-compatibility/dataschema data schema}. + * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema}. * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} [view] Name of the view element. From 84a7492c9f63a3455ee7cd86221f51a3f584aa12 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> Date: Thu, 11 Mar 2021 18:52:50 +0100 Subject: [PATCH 059/217] Apply suggestions from code review --- packages/ckeditor5-content-compatibility/tests/datafilter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 1caccd91d84..27215a2590a 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -5,11 +5,11 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + import DataSchema from '../src/dataschema'; import DataFilter from '../src/datafilter'; -import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - describe( 'DataFilter', () => { let editor, model, dataFilter, dataSchema; From fa1c77c4c39701226eac7eea5d819a0148d6fc06 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 11 Mar 2021 18:58:39 +0100 Subject: [PATCH 060/217] Code review fixes. --- .../src/datafilter.js | 8 +- .../tests/datafilter.js | 56 ++++----- .../tests/dataschema.js | 106 +++++++++--------- 3 files changed, 87 insertions(+), 83 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index b80583faea2..980329d29b2 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -166,7 +166,7 @@ export default class DataFilter { /** * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ _defineConverters( definition ) { const conversion = this.editor.conversion; @@ -220,7 +220,7 @@ export default class DataFilter { return element; }, - // With a `low` priority, `paragraph` plugin autoparagraphing mechanism is executed. Make sure + // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure // this listener is called before it. If not, some elements will be transformed into a paragraph. converterPriority: priorities.get( 'low' ) + 1 } ); @@ -314,11 +314,11 @@ function mergeMatchResults( matches ) { return matchResult; } -// Convertes the given iterable object into an object. +// Converts the given iterable object into an object. // // @private // @param {Iterable.} iterable -// @param {Function} getValue Shoud result with value for the given object key. +// @param {Function} getValue Should result with value for the given object key. // @returns {Object} function iterableToObject( iterable, getValue ) { const attributesObject = {}; diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 27215a2590a..0f7ba803014 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -94,9 +94,11 @@ describe( 'DataFilter', () => { it( 'should allow attributes', () => { dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', attributes: { - 'data-foo': 'foobar' - } } ); + dataFilter.allowAttributes( { + name: 'section', attributes: { + 'data-foo': 'foobar' + } + } ); editor.setData( '

foobar

' ); @@ -118,10 +120,12 @@ describe( 'DataFilter', () => { it( 'should allow attributes (styles)', () => { dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', styles: { - 'color': 'red', - 'background-color': 'blue' - } } ); + dataFilter.allowAttributes( { + name: 'section', styles: { + 'color': 'red', + 'background-color': 'blue' + } + } ); editor.setData( '

foobar

' ); @@ -368,28 +372,28 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

foo

' ); } ); -} ); -function getModelDataWithAttributes( model, options ) { - // Simplify GHS attributes as they are not very readable at this point due to object structure. - let counter = 1; - const data = getModelData( model, options ).replace( /htmlAttributes="{(.*?)}"/g, () => { - return `htmlAttributes="(${ counter++ })"`; - } ); + function getModelDataWithAttributes( model, options ) { + // Simplify GHS attributes as they are not very readable at this point due to object structure. + let counter = 1; + const data = getModelData( model, options ).replace( /htmlAttributes="{(.*?)}"/g, () => { + return `htmlAttributes="(${ counter++ })"`; + } ); - const range = model.createRangeIn( model.document.getRoot() ); + const range = model.createRangeIn( model.document.getRoot() ); - let attributes = []; - for ( const item of range.getItems() ) { - if ( item.hasAttribute && item.hasAttribute( 'htmlAttributes' ) ) { - attributes.push( item.getAttribute( 'htmlAttributes' ) ); + let attributes = []; + for ( const item of range.getItems() ) { + if ( item.hasAttribute && item.hasAttribute( 'htmlAttributes' ) ) { + attributes.push( item.getAttribute( 'htmlAttributes' ) ); + } } - } - attributes = attributes.reduce( ( prev, cur, index ) => { - prev[ index + 1 ] = cur; - return prev; - }, {} ); + attributes = attributes.reduce( ( prev, cur, index ) => { + prev[ index + 1 ] = cur; + return prev; + }, {} ); - return { data, attributes }; -} + return { data, attributes }; + } +} ); diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 84fe698780d..60ce0843d24 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -6,55 +6,55 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import DataSchema from '../src/dataschema'; -const fakeDefinitions = [ - { - view: 'def1', - model: 'htmlDef1', - allowChildren: [ 'htmlDef2', 'htmlDef3' ], - schema: { - inheritAllFrom: '$block' - } - }, - { - view: 'def2', - model: 'htmlDef2', - schema: { - inheritAllFrom: 'htmlDef1' - } - }, - { - view: 'def3', - model: 'htmlDef3', - schema: { - inheritTypesFrom: 'htmlDef2' - } - }, - { - view: 'def4', - model: 'htmlDef4', - schema: { - allowWhere: 'htmlDef3' - } - }, - { - view: 'def5', - model: 'htmlDef5', - schema: { - allowContentOf: 'htmlDef4' - } - }, - { - view: 'def6', - model: 'htmlDef6', - schema: { - allowAttributesOf: 'htmlDef5' - } - } -]; - describe( 'DataSchema', () => { let editor, dataSchema; + const fakeDefinitions = [ + { + view: 'def1', + model: 'htmlDef1', + allowChildren: [ 'htmlDef2', 'htmlDef3' ], + schema: { + inheritAllFrom: '$block' + } + }, + { + view: 'def2', + model: 'htmlDef2', + schema: { + inheritAllFrom: 'htmlDef1' + } + }, + { + view: 'def3', + model: 'htmlDef3', + schema: { + inheritTypesFrom: 'htmlDef2' + } + }, + { + view: 'def4', + model: 'htmlDef4', + schema: { + allowWhere: 'htmlDef3' + } + }, + { + view: 'def5', + model: 'htmlDef5', + schema: { + allowContentOf: 'htmlDef4' + } + }, + { + view: 'def6', + model: 'htmlDef6', + schema: { + allowAttributesOf: 'htmlDef5' + } + } + ]; + beforeEach( () => { return VirtualTestEditor .create() @@ -142,12 +142,12 @@ describe( 'DataSchema', () => { expect( result.size ).to.equal( 0 ); } ); -} ); -function registerMany( dataSchema, definitions ) { - definitions.forEach( def => dataSchema.register( def ) ); -} + function registerMany( dataSchema, definitions ) { + definitions.forEach( def => dataSchema.register( def ) ); + } -function getFakeDefinitions( ...viewNames ) { - return fakeDefinitions.filter( def => viewNames.includes( def.view ) ); -} + function getFakeDefinitions( ...viewNames ) { + return fakeDefinitions.filter( def => viewNames.includes( def.view ) ); + } +} ); From 9fc6ab1e66e52574cad3a8403085a1e063f48632 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 11 Mar 2021 19:02:58 +0100 Subject: [PATCH 061/217] Code review fixes. --- .../ckeditor5-content-compatibility/tests/datafilter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 0f7ba803014..bd192f1d088 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -95,7 +95,8 @@ describe( 'DataFilter', () => { it( 'should allow attributes', () => { dataFilter.allowElement( { name: 'section' } ); dataFilter.allowAttributes( { - name: 'section', attributes: { + name: 'section', + attributes: { 'data-foo': 'foobar' } } ); @@ -121,7 +122,8 @@ describe( 'DataFilter', () => { it( 'should allow attributes (styles)', () => { dataFilter.allowElement( { name: 'section' } ); dataFilter.allowAttributes( { - name: 'section', styles: { + name: 'section', + styles: { 'color': 'red', 'background-color': 'blue' } From 840a9245e8273c28258ebffd8354075083e57300 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 29 Mar 2021 12:50:11 +0200 Subject: [PATCH 062/217] Updated package.json to match the latest release. --- .../package.json | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index ef59dc79cd2..b175869e6b6 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-content-compatibility", - "version": "26.0.0", + "version": "27.0.0", "description": "Content compatibility feature", "private": true, "keywords": [ @@ -15,16 +15,20 @@ ], "main": "src/index.js", "dependencies": { - "ckeditor5": "^26.0.0", + "ckeditor5": "^27.0.0", "lodash-es": "^4.17.15" }, "devDependencies": { - "@ckeditor/ckeditor5-basic-styles": "^26.0.0", - "@ckeditor/ckeditor5-block-quote": "^26.0.0", - "@ckeditor/ckeditor5-core": "^26.0.0", - "@ckeditor/ckeditor5-list": "^26.0.0", - "@ckeditor/ckeditor5-paragraph": "^26.0.0", - "@ckeditor/ckeditor5-utils": "^26.0.0" + "@ckeditor/ckeditor5-basic-styles": "^27.0.0", + "@ckeditor/ckeditor5-block-quote": "^27.0.0", + "@ckeditor/ckeditor5-core": "^27.0.0", + "@ckeditor/ckeditor5-editor-classic": "^27.0.0", + "@ckeditor/ckeditor5-engine": "^27.0.0", + "@ckeditor/ckeditor5-essentials": "^27.0.0", + "@ckeditor/ckeditor5-list": "^27.0.0", + "@ckeditor/ckeditor5-paragraph": "^27.0.0", + "webpack": "^4.43.0", + "webpack-cli": "^3.3.11" }, "engines": { "node": ">=12.0.0", From beecd7ab1920480a2ccd7b2b0c85146969937d7c Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 10 Mar 2021 13:30:12 +0100 Subject: [PATCH 063/217] Added initial support for inline elements. --- .../src/datafilter.js | 200 ++++++++++++++---- .../src/dataschema.js | 12 ++ .../tests/datafilter.js | 6 +- .../tests/manual/generalhtmlsupport.html | 10 + .../tests/manual/generalhtmlsupport.js | 10 +- 5 files changed, 187 insertions(+), 51 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 980329d29b2..25f6f408cea 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -37,7 +37,7 @@ const DATA_SCHEMA_ATTRIBUTE_KEY = 'htmlAttributes'; * dataFilter.disallowedAttributes( { * name: 'section', * styles: { - * color: /[^]/ + * color: /[\s\S]+/ * } * } ); */ @@ -133,15 +133,47 @@ export default class DataFilter { * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ _registerElement( definition ) { + if ( definition.isInline ) { + this._defineInlineElement( definition ); + } else { + this._defineBlockElement( definition ); + } + } + + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + */ + _defineBlockElement( definition ) { if ( this.editor.model.schema.isRegistered( definition.model ) ) { return; } this._defineSchema( definition ); - if ( definition.view ) { - this._defineConverters( definition ); + if ( !definition.view ) { + return; } + + this._defineBlockElementConverters( definition ); + } + + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + */ + _defineInlineElement( definition ) { + const schema = this.editor.model.schema; + + schema.extend( '$text', { + allowAttributes: definition.model + } ); + + schema.setAttributeProperties( definition.model, { + copyOnEnter: true + } ); + + this._defineInlineElementConverters( definition ); } /** @@ -168,53 +200,58 @@ export default class DataFilter { * @private * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ - _defineConverters( definition ) { + _defineInlineElementConverters( definition ) { const conversion = this.editor.conversion; const viewName = definition.view; const modelName = definition.model; - // Consumes disallowed element attributes to prevent them of being processed by other converters. - conversion.for( 'upcast' ).add( dispatcher => { - dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - for ( const match of matchAll( data.viewItem, this._disallowedAttributes ) ) { - conversionApi.consumable.consume( data.viewItem, match.match ); - } - }, { priority: 'high' } ); - } ); + this._addDisallowedAttributesConverter( viewName ); - // Stash unused, allowed element attributes, so they can be reapplied later in data conversion. - conversion.for( 'upcast' ).elementToElement( { + conversion.for( 'upcast' ).elementToAttribute( { view: viewName, - model: ( viewElement, conversionApi ) => { - const matches = []; + model: { + key: modelName, + value: this._matchAndConsumeAttributes.bind( this ) + }, + converterPriority: 'low' + } ); - for ( const match of matchAll( viewElement, this._allowedAttributes ) ) { - if ( conversionApi.consumable.consume( viewElement, match.match ) ) { - matches.push( match ); - } + conversion.for( 'downcast' ).attributeToElement( { + model: modelName, + view: ( attributeValue, conversionApi ) => { + if ( !attributeValue ) { + return; } - const { attributes, styles, classes } = mergeMatchResults( matches ); - const viewAttributes = {}; + const { writer } = conversionApi; + const viewElement = writer.createAttributeElement( viewName ); - // Stash attributes. - if ( attributes.size ) { - viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); - } + setAttributesOn( writer, attributeValue, viewElement ); - // Stash styles. - if ( styles.size ) { - viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); - } + return viewElement; + } + } ) + } - // Stash classes. - if ( classes.size ) { - viewAttributes.classes = Array.from( classes ); - } + /** + * @private + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} + */ + _defineBlockElementConverters( definition ) { + const conversion = this.editor.conversion; + const viewName = definition.view; + const modelName = definition.model; + + this._addDisallowedAttributesConverter( viewName ); + // Stash unused, allowed element attributes, so they can be reapplied later in data conversion. + conversion.for( 'upcast' ).elementToElement( { + view: viewName, + model: ( viewElement, conversionApi ) => { const element = conversionApi.writer.createElement( modelName ); + const viewAttributes = this._matchAndConsumeAttributes( viewElement, conversionApi ); - if ( Object.keys( viewAttributes ).length ) { + if ( viewAttributes ) { conversionApi.writer.setAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes, element ); } @@ -241,22 +278,93 @@ export default class DataFilter { const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement( data.item ); - if ( viewAttributes.attributes ) { - for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) { - viewWriter.setAttribute( key, value, viewElement ); - } - } + setAttributesOn( viewWriter, viewAttributes, viewElement ); + } ); + } ); + } - if ( viewAttributes.styles ) { - viewWriter.setStyle( viewAttributes.styles, viewElement ); - } + /** + * Adds converter responsible for consuming disallowed view attributes. + * + * @private + * @param {String} viewName + */ + _addDisallowedAttributesConverter( viewName ) { + const conversion = this.editor.conversion; - if ( viewAttributes.classes ) { - viewWriter.addClass( viewAttributes.classes, viewElement ); + // Consumes disallowed element attributes to prevent them of being processed by other converters. + conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { + for ( const match of matchAll( data.viewItem, this._disallowedAttributes ) ) { + conversionApi.consumable.consume( data.viewItem, match.match ); } - } ); + }, { priority: 'high' } ); } ); } + + /** + * Matches and consumes allowed view attributes. + * + * @private + * @param {module:engine/view/element~Element} viewElement + * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi + * @returns {Object} [result] + * @returns {Array.} result.attributes Array with matched attribute names. + * @returns {Array.} result.classes Array with matched class names. + * @returns {Array.} result.styles Array with matched style names. + */ + _matchAndConsumeAttributes( viewElement, { consumable } ) { + const matches = []; + for ( const match of matchAll( viewElement, this._allowedAttributes ) ) { + if ( consumable.consume( viewElement, match.match ) ) { + matches.push( match ); + } + } + + const { attributes, styles, classes } = mergeMatchResults( matches ); + const viewAttributes = {}; + + if ( attributes.size ) { + viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); + } + + if ( styles.size ) { + viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); + } + + if ( classes.size ) { + viewAttributes.classes = Array.from( classes ); + } + + if ( !Object.keys( viewAttributes ).length ) { + return null; + } + + return viewAttributes; + } +} + +// Helper function for downcast converter. Sets attributes on the given view element. +// +// @private +// @param {module:engine/view/downcastwriter~DowncastWriter} writer +// @param {Object} viewAttributes +// @param {module:engine/view/element~Element} viewElement +// on all elements in the range. +function setAttributesOn( writer, viewAttributes, viewElement ) { + if ( viewAttributes.attributes ) { + for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) { + writer.setAttribute( key, value, viewElement ); + } + } + + if ( viewAttributes.styles ) { + writer.setStyle( viewAttributes.styles, viewElement ); + } + + if ( viewAttributes.classes ) { + writer.addClass( viewAttributes.classes, viewElement ); + } } // Helper function restoring matcher for the given key from `rules` object. diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 320d7ebdc35..df519a0290c 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -112,6 +112,13 @@ export default class DataSchema { allowIn: 'htmlDetails' } } ); + + // Inline elements. + this.register( { + view: 'span', + model: 'htmlSpan', + isInline: true + } ); } /** @@ -167,6 +174,11 @@ export default class DataSchema { */ * _getReferences( modelName ) { const { schema } = this._definitions.get( modelName ); + + if ( !schema ) { + return; + } + const inheritProperties = [ 'inheritAllFrom', 'inheritTypesFrom', 'allowWhere', 'allowContentOf', 'allowAttributesOf' ]; for ( const property of inheritProperties ) { diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index bd192f1d088..243cb96ff40 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -168,7 +168,7 @@ describe( 'DataFilter', () => { it( 'should allow nested attributes', () => { dataFilter.allowElement( { name: /article|section/ } ); - dataFilter.allowAttributes( { name: /[^]/, attributes: { 'data-foo': /foo|bar/ } } ); + dataFilter.allowAttributes( { name: /[\s\S]+/, attributes: { 'data-foo': /foo|bar/ } } ); editor.setData( '
' + '

section1

' + @@ -237,7 +237,7 @@ describe( 'DataFilter', () => { it( 'should disallow attributes', () => { dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': /[^]/ } } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'section', attributes: { 'data-foo': 'bar' } } ); editor.setData( @@ -265,7 +265,7 @@ describe( 'DataFilter', () => { it( 'should disallow attributes (styles)', () => { dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', styles: { color: /[^]/ } } ); + dataFilter.allowAttributes( { name: 'section', styles: { color: /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'section', styles: { color: 'red' } } ); editor.setData( diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index 3d5c4a5ccb3..bf9924f6fd3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -8,15 +8,25 @@

Section #2

Section #3

+
Summary Hello world
+

dt1

dt2

dd1

dd2

+

XYZ

+ +

+ Some span element with allowed attributes! +

+

+ Some span element with disallowed attributes! +

diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 8e1bba81c11..9a8e1e7f118 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -53,12 +53,18 @@ class ExtendHTMLSupport extends Plugin { // Let's extend 'section' with some attributes. Data filter will take care of // creating proper converters and attribute matchers: - dataFilter.allowAttributes( { name: 'section', attributes: { id: /[^]/ } } ); - dataFilter.allowAttributes( { name: 'section', classes: /[^]/ } ); + dataFilter.allowAttributes( { name: 'section', attributes: { id: /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: 'section', classes: /[\s\S]+/ } ); dataFilter.allowAttributes( { name: 'section', styles: { color: 'red' } } ); // but disallow setting id attribute if it start with `_` prefix: dataFilter.disallowAttributes( { name: 'section', attributes: { id: /^_.*/ } } ); + + // Let's also add some inline elements support: + dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowAttributes( { name: 'span', attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: 'span', styles: { color: /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: 'span', styles: { color: 'red' } } ); } } From 31accfe4818b435c739ddedc8556f1bbe8d10a71 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 11 Mar 2021 14:21:53 +0100 Subject: [PATCH 064/217] Nested inline elements support. --- .../src/datafilter.js | 54 +++++++++++++++---- .../src/dataschema.js | 9 +++- .../tests/manual/generalhtmlsupport.html | 4 +- .../tests/manual/generalhtmlsupport.js | 8 +-- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 25f6f408cea..507f4c58cc5 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -10,7 +10,7 @@ import { Matcher } from 'ckeditor5/src/engine'; import { priorities, toArray } from 'ckeditor5/src/utils'; -import { cloneDeep } from 'lodash-es'; +import { cloneDeep, uniq } from 'lodash-es'; const DATA_SCHEMA_ATTRIBUTE_KEY = 'htmlAttributes'; @@ -203,21 +203,33 @@ export default class DataFilter { _defineInlineElementConverters( definition ) { const conversion = this.editor.conversion; const viewName = definition.view; - const modelName = definition.model; + const attributeKey = definition.model; this._addDisallowedAttributesConverter( viewName ); - conversion.for( 'upcast' ).elementToAttribute( { - view: viewName, - model: { - key: modelName, - value: this._matchAndConsumeAttributes.bind( this ) - }, - converterPriority: 'low' + conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { + const viewAttributes = this._matchAndConsumeAttributes( data.viewItem, conversionApi ); + + // Convert children and set conversion result as a current data. + data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); + + // Set attribute on each item in range according to Schema. + for ( const node of data.modelRange.getItems() ) { + if ( conversionApi.schema.checkAttribute( node, attributeKey ) ) { + // Node's children are converter recursively, so node can already include model attribute. + // We want to extend it, not replace. + const attributesToAdd = mergeAttributes( node.getAttribute( attributeKey ), viewAttributes ); + conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); + } + } + }, { + priority: 'low' + } ); } ); conversion.for( 'downcast' ).attributeToElement( { - model: modelName, + model: attributeKey, view: ( attributeValue, conversionApi ) => { if ( !attributeValue ) { return; @@ -230,7 +242,7 @@ export default class DataFilter { return viewElement; } - } ) + } ); } /** @@ -437,3 +449,23 @@ function iterableToObject( iterable, getValue ) { return attributesObject; } + +// Merges attribute objects. +// +// @private +// @param {Object} oldValue +// @param {Object} newValue +// @returns {Object} +function mergeAttributes( oldValue, newValue ) { + const result = cloneDeep( newValue ); + + for ( const key in oldValue ) { + if ( Array.isArray( oldValue[ key ] ) ) { + result[ key ] = uniq( [ ...oldValue[ key ], ...newValue[ key ] ] ); + } else { + result[ key ] = Object.assign( {}, oldValue[ key ], newValue[ key ] ); + } + } + + return result; +} diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index df519a0290c..969bd7936eb 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -113,12 +113,18 @@ export default class DataSchema { } } ); - // Inline elements. + // Add inline elements. this.register( { view: 'span', model: 'htmlSpan', isInline: true } ); + + this.register( { + view: 'cite', + model: 'htmlCite', + isInline: true + } ); } /** @@ -220,4 +226,5 @@ function testViewName( pattern, viewName ) { * @property {String} model Name of the model element. * @property {module:engine/model/schema~SchemaItemDefinition} schema The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. + * @property {Boolean} [isInline] Indicates if the element decribed by data schema definition is inline. */ diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index bf9924f6fd3..53c0ddafed3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -24,9 +24,9 @@

XYZ

- Some span element with allowed attributes! + Nested cite: I'm blue!

- Some span element with disallowed attributes! + Nested span: I'm blue!

diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 9a8e1e7f118..3a1f9987860 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -61,10 +61,10 @@ class ExtendHTMLSupport extends Plugin { dataFilter.disallowAttributes( { name: 'section', attributes: { id: /^_.*/ } } ); // Let's also add some inline elements support: - dataFilter.allowElement( { name: 'span' } ); - dataFilter.allowAttributes( { name: 'span', attributes: { 'data-foo': /[\s\S]+/ } } ); - dataFilter.allowAttributes( { name: 'span', styles: { color: /[\s\S]+/ } } ); - dataFilter.disallowAttributes( { name: 'span', styles: { color: 'red' } } ); + dataFilter.allowElement( { name: /^span|cite$/ } ); + dataFilter.allowAttributes( { name: /^span|cite$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: /^span|cite$/, styles: { color: /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: /^span|cite$/, styles: { color: 'red' } } ); } } From c1c6c6226cbefd57d0e9e7b93ba4940d8a48e3af Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 12 Mar 2021 08:55:21 +0100 Subject: [PATCH 065/217] Added initial test for inline elements. --- .../tests/datafilter.js | 598 +++++++++--------- 1 file changed, 308 insertions(+), 290 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 243cb96ff40..b73188a74cd 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -31,363 +31,381 @@ describe( 'DataFilter', () => { return editor.destroy(); } ); - it( 'should allow element', () => { - dataFilter.allowElement( { name: 'article' } ); - - editor.setData( '
' + - '
section1
' + - '
section2
' - ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - 'section1section2' - ); - - expect( editor.getData() ).to.equal( - '

section1section2

' - ); - - dataFilter.allowElement( { name: 'section' } ); - - editor.setData( '
' + - '
section1
' + - '
section2
' - ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - 'section1' + - 'section2' - ); - - expect( editor.getData() ).to.equal( - '
' + - '

section1

' + - '

section2

' - ); - } ); - - it( 'should allow deeply nested structure', () => { - dataFilter.allowElement( { name: 'section' } ); - - editor.setData( - '

1

' + - '

2

' + - '

3

' + - '
' - ); - - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '1' + - '2' + - '3' + - '' - ); - - expect( editor.getData() ).to.equal( - '

1

' + - '

2

' + - '

3

' + - '
' - ); - } ); + describe( 'block', () => { + it( 'should allow element', () => { + dataFilter.allowElement( { name: 'article' } ); + + editor.setData( '
' + + '
section1
' + + '
section2
' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + 'section1section2' + ); + + expect( editor.getData() ).to.equal( + '

section1section2

' + ); + + dataFilter.allowElement( { name: 'section' } ); + + editor.setData( '
' + + '
section1
' + + '
section2
' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '' + + 'section1' + + 'section2' + ); + + expect( editor.getData() ).to.equal( + '
' + + '

section1

' + + '

section2

' + ); + } ); - it( 'should allow attributes', () => { - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { - name: 'section', - attributes: { - 'data-foo': 'foobar' - } + it( 'should allow deeply nested structure', () => { + dataFilter.allowElement( { name: 'section' } ); + + editor.setData( + '

1

' + + '

2

' + + '

3

' + + '
' + ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '1' + + '2' + + '3' + + '' + ); + + expect( editor.getData() ).to.equal( + '

1

' + + '

2

' + + '

3

' + + '
' + ); } ); - editor.setData( '

foobar

' ); + it( 'should allow attributes', () => { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { + name: 'section', + attributes: { + 'data-foo': 'foobar' + } + } ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foobar', - attributes: { - 1: { - attributes: { - 'data-foo': 'foobar' + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foobar' + } } } - } - } ); - - expect( editor.getData() ).to.equal( - '

foobar

' - ); - } ); + } ); - it( 'should allow attributes (styles)', () => { - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { - name: 'section', - styles: { - 'color': 'red', - 'background-color': 'blue' - } + expect( editor.getData() ).to.equal( + '

foobar

' + ); } ); - editor.setData( '

foobar

' ); + it( 'should allow attributes (styles)', () => { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { + name: 'section', + styles: { + 'color': 'red', + 'background-color': 'blue' + } + } ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foobar', - attributes: { - 1: { - styles: { - 'background-color': 'blue', - color: 'red' + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'background-color': 'blue', + color: 'red' + } } } - } + } ); + + expect( editor.getData() ).to.equal( + '

foobar

' + ); } ); - expect( editor.getData() ).to.equal( - '

foobar

' - ); - } ); + it( 'should allow attributes (classes)', () => { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); - it( 'should allow attributes (classes)', () => { - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); + editor.setData( '

foobar

' ); - editor.setData( '

foobar

' ); + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { classes: [ 'foo', 'bar' ] } + } + } ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foobar', - attributes: { - 1: { classes: [ 'foo', 'bar' ] } - } + expect( editor.getData() ).to.equal( + '

foobar

' + ); } ); - expect( editor.getData() ).to.equal( - '

foobar

' - ); - } ); - - it( 'should allow nested attributes', () => { - dataFilter.allowElement( { name: /article|section/ } ); - dataFilter.allowAttributes( { name: /[\s\S]+/, attributes: { 'data-foo': /foo|bar/ } } ); - - editor.setData( '
' + - '

section1

' + - '

section2

' + - '
' - ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '' + - 'section1' + - 'section2' + - '', - attributes: { - 1: { - attributes: { - 'data-foo': 'foo' - } - }, - 2: { - attributes: { - 'data-foo': 'bar' - } - }, - 3: { - attributes: { - 'data-foo': 'foo' + it( 'should allow nested attributes', () => { + dataFilter.allowElement( { name: /article|section/ } ); + dataFilter.allowAttributes( { name: /[\s\S]+/, attributes: { 'data-foo': /foo|bar/ } } ); + + editor.setData( '
' + + '

section1

' + + '

section2

' + + '
' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + 'section1' + + 'section2' + + '', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + }, + 2: { + attributes: { + 'data-foo': 'bar' + } + }, + 3: { + attributes: { + 'data-foo': 'foo' + } } } - } + } ); } ); - } ); - - it( 'should allow attributes for all allowed definitions', () => { - dataFilter.allowElement( { name: /section|article/ } ); - dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-foo': 'foo' } } ); - dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-bar': 'bar' } } ); - - editor.setData( - '

foo

' + - '

bar

' - ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', - attributes: { - 1: { - attributes: { - 'data-foo': 'foo' - } - }, - 2: { - attributes: { - 'data-bar': 'bar' + it( 'should allow attributes for all allowed definitions', () => { + dataFilter.allowElement( { name: /section|article/ } ); + + dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-foo': 'foo' } } ); + dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-bar': 'bar' } } ); + + editor.setData( + '

foo

' + + '

bar

' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + }, + 2: { + attributes: { + 'data-bar': 'bar' + } } } - } - } ); + } ); - expect( editor.getData() ).to.equal( - '

foo

' + - '

bar

' - ); - } ); + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' + ); + } ); - it( 'should disallow attributes', () => { - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': /[\s\S]+/ } } ); - dataFilter.disallowAttributes( { name: 'section', attributes: { 'data-foo': 'bar' } } ); - - editor.setData( - '

foo

' + - '

bar

' - ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', - attributes: { - 1: { - attributes: { - 'data-foo': 'foo' + it( 'should disallow attributes', () => { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: 'section', attributes: { 'data-foo': 'bar' } } ); + + editor.setData( + '

foo

' + + '

bar

' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } } } - } - } ); + } ); - expect( editor.getData() ).to.equal( - '

foo

' + - '

bar

' - ); - } ); + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' + ); + } ); - it( 'should disallow attributes (styles)', () => { - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', styles: { color: /[\s\S]+/ } } ); - dataFilter.disallowAttributes( { name: 'section', styles: { color: 'red' } } ); - - editor.setData( - '

foo

' + - '

bar

' - ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', - attributes: { - 1: { - styles: { - color: 'blue' + it( 'should disallow attributes (styles)', () => { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', styles: { color: /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: 'section', styles: { color: 'red' } } ); + + editor.setData( + '

foo

' + + '

bar

' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', + attributes: { + 1: { + styles: { + color: 'blue' + } } } - } + } ); + + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' + ); } ); - expect( editor.getData() ).to.equal( - '

foo

' + - '

bar

' - ); - } ); + it( 'should disallow attributes (classes)', () => { + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); + dataFilter.disallowAttributes( { name: 'section', classes: [ 'bar' ] } ); - it( 'should disallow attributes (classes)', () => { - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); - dataFilter.disallowAttributes( { name: 'section', classes: [ 'bar' ] } ); + editor.setData( + '

foo

' + + '

bar

' + ); - editor.setData( - '

foo

' + - '

bar

' - ); + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo' + + 'bar', + attributes: {} + } ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo' + - 'bar', - attributes: {} + expect( editor.getData() ).to.equal( + '

foo

' + + '

bar

' + ); } ); - expect( editor.getData() ).to.equal( - '

foo

' + - '

bar

' - ); - } ); + it( 'should extend allowed children only if specified model schema exists', () => { + dataSchema.register( { + view: 'xyz', + model: 'htmlXyz', + allowChildren: 'not-exists', + schema: { + inheritAllFrom: '$htmlBlock' + } + } ); - it( 'should extend allowed children only if specified model schema exists', () => { - dataSchema.register( { - view: 'xyz', - model: 'htmlXyz', - allowChildren: 'not-exists', - schema: { - inheritAllFrom: '$htmlBlock' - } + expect( () => { + dataFilter.allowElement( { name: 'xyz' } ); + } ).to.not.throw(); } ); - expect( () => { - dataFilter.allowElement( { name: 'xyz' } ); - } ).to.not.throw(); - } ); - - it( 'should not consume attribute already consumed (upcast)', () => { - editor.conversion.for( 'upcast' ).add( dispatcher => { - dispatcher.on( 'element:section', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.viewItem, { attributes: [ 'data-foo' ] } ); + it( 'should not consume attribute already consumed (upcast)', () => { + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:section', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.viewItem, { attributes: [ 'data-foo' ] } ); + } ); } ); - } ); - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); - editor.setData( '

foo

' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo', - attributes: {} - } ); + editor.setData( '

foo

' ); - expect( editor.getData() ).to.equal( '

foo

' ); - } ); + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + attributes: {} + } ); - it( 'should not consume attribute already consumed (downcast)', () => { - editor.conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( 'attribute:htmlAttributes:htmlSection', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, evt.name ); - }, { priority: 'high' } ); + expect( editor.getData() ).to.equal( '

foo

' ); } ); - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); + it( 'should not consume attribute already consumed (downcast)', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( 'attribute:htmlAttributes:htmlSection', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, evt.name ); + }, { priority: 'high' } ); + } ); + + dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); - editor.setData( '

foo

' ); + editor.setData( '

foo

' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo', - // At this point, attribute should still be in the model, as we are testing downcast conversion. - attributes: { - 1: { - attributes: { - 'data-foo': '' + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + // At this point, attribute should still be in the model, as we are testing downcast conversion. + attributes: { + 1: { + attributes: { + 'data-foo': '' + } } } - } + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); } ); + } ); + + describe( 'inline', () => { + it( 'should allow element', () => { + dataFilter.allowElement( { name: 'cite' } ); - expect( editor.getData() ).to.equal( '

foo

' ); + editor.setData( '

foobar

' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text htmlCite="(1)">foobar' + ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); } ); function getModelDataWithAttributes( model, options ) { // Simplify GHS attributes as they are not very readable at this point due to object structure. let counter = 1; - const data = getModelData( model, options ).replace( /htmlAttributes="{(.*?)}"/g, () => { - return `htmlAttributes="(${ counter++ })"`; + const data = getModelData( model, options ).replace( /(html.*?)="{.*?}"/g, ( fullMatch, attributeName ) => { + return `${ attributeName }="(${ counter++ })"`; } ); const range = model.createRangeIn( model.document.getRoot() ); let attributes = []; for ( const item of range.getItems() ) { - if ( item.hasAttribute && item.hasAttribute( 'htmlAttributes' ) ) { - attributes.push( item.getAttribute( 'htmlAttributes' ) ); + for ( const [ key, value ] of item.getAttributes() ) { + if ( key.startsWith( 'html' ) ) { + attributes.push( value ); + } } } From a7f17a8af595ea967a74b66c00540358a2b2123a Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 15 Mar 2021 13:06:53 +0100 Subject: [PATCH 066/217] Added tests, improved element nesting. --- .../src/datafilter.js | 18 +- .../tests/datafilter.js | 280 +++++++++++++++++- .../tests/manual/generalhtmlsupport.html | 4 + 3 files changed, 298 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 507f4c58cc5..3fc885377f3 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -75,6 +75,15 @@ export default class DataFilter { * @member {Map.} #_disallowedAttributes */ this._disallowedAttributes = new Map(); + + /** + * A set of inline elements which should not be preserved during conversion if + * they are missing attributes. + * + * @readonly + * @member {Set.} #transparentElements + */ + this.transparentElements = new Set( [ 'span' ] ); } /** @@ -211,6 +220,11 @@ export default class DataFilter { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { const viewAttributes = this._matchAndConsumeAttributes( data.viewItem, conversionApi ); + // Skip information about transparent element type if there is no attribute to convert. + if ( !viewAttributes && this.transparentElements.has( viewName ) ) { + return; + } + // Convert children and set conversion result as a current data. data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); @@ -219,7 +233,9 @@ export default class DataFilter { if ( conversionApi.schema.checkAttribute( node, attributeKey ) ) { // Node's children are converter recursively, so node can already include model attribute. // We want to extend it, not replace. - const attributesToAdd = mergeAttributes( node.getAttribute( attributeKey ), viewAttributes ); + const nodeAttributes = node.getAttribute( attributeKey ); + const attributesToAdd = mergeAttributes( nodeAttributes, viewAttributes || {} ); + conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); } } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index b73188a74cd..476cbe8e50c 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -383,12 +383,267 @@ describe( 'DataFilter', () => { editor.setData( '

foobar

' ); - expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '<$text htmlCite="(1)">foobar' + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foobar', + attributes: { + 1: {} + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should allow deeply nested structure', () => { + dataFilter.allowElement( { name: 'cite' } ); + + editor.setData( '

foobarbaz' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foobarbaz', + attributes: { + 1: {}, + 2: {}, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( '

foobarbaz

' ); + } ); + + it( 'should allow attributes', () => { + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { + name: 'cite', + attributes: { + 'data-foo': 'foobar' + } + } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foobar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foobar' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should allow attributes (styles)', () => { + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { + name: 'cite', + styles: { + 'color': 'red', + 'background-color': 'blue' + } + } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foobar', + attributes: { + 1: { + styles: { + 'background-color': 'blue', + color: 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( + '

foobar

' ); + } ); + + it( 'should allow attributes (classes)', () => { + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { name: 'cite', classes: [ 'foo', 'bar' ] } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foobar', + attributes: { + 1: { classes: [ 'foo', 'bar' ] } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should allow nested attributes', () => { + dataFilter.allowElement( { name: /span|cite/ } ); + dataFilter.allowAttributes( { name: /span|cite/, attributes: { 'data-foo': 'foo' } } ); + dataFilter.allowAttributes( { name: /span|cite/, attributes: { 'data-bar': 'bar' } } ); + + editor.setData( '

' + + 'cite' + + 'span' + + '

' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlCite="(1)">cite' + + '<$text htmlCite="(2)" htmlSpan="(3)">span' + + '', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo', + 'data-bar': 'bar' + } + }, + 2: { + attributes: { + 'data-foo': 'foo' + } + }, + 3: { + attributes: { + 'data-bar': 'bar' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

' + + 'cite' + + 'span' + + '

' ); + } ); + + it( 'should disallow attributes', () => { + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { name: 'cite', attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: 'cite', attributes: { 'data-foo': 'bar' } } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foo<$text htmlCite="(2)">bar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + }, + 2: {} + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should disallow attributes (styles)', () => { + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { name: 'cite', styles: { color: /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: 'cite', styles: { color: 'red' } } ); + + editor.setData( + '

' + + 'foo' + + 'bar' + + '

' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foo<$text htmlCite="(2)">bar', + attributes: { + 1: { + styles: { + color: 'blue' + } + }, + 2: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

foobar

' + ); + } ); + + it( 'should disallow attributes (classes)', () => { + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { name: 'cite', classes: [ 'foo', 'bar' ] } ); + dataFilter.disallowAttributes( { name: 'cite', classes: [ 'bar' ] } ); + + editor.setData( + '

' + + 'foo' + + 'bar' + + '

' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foobar', + attributes: { + 1: {}, + 2: {} + } + } ); expect( editor.getData() ).to.equal( '

foobar

' ); } ); + + it( 'should not consume attribute already consumed (upcast)', () => { + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:cite', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.viewItem, { attributes: [ 'data-foo' ] } ); + } ); + } ); + + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { name: 'cite', attributes: { 'data-foo': true } } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foo', + attributes: { + 1: {} + } + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should not consume attribute already consumed (downcast)', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( 'attribute:htmlCite:$text', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, evt.name ); + }, { priority: 'high' } ); + } ); + + dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowAttributes( { name: 'cite', attributes: { 'data-foo': true } } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCite="(1)">foo', + // At this point, attribute should still be in the model, as we are testing downcast conversion. + attributes: { + 1: { + attributes: { + 'data-foo': '' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); } ); function getModelDataWithAttributes( model, options ) { @@ -402,7 +657,7 @@ describe( 'DataFilter', () => { let attributes = []; for ( const item of range.getItems() ) { - for ( const [ key, value ] of item.getAttributes() ) { + for ( const [ key, value ] of sortAttributes( item.getAttributes() ) ) { if ( key.startsWith( 'html' ) ) { attributes.push( value ); } @@ -416,4 +671,23 @@ describe( 'DataFilter', () => { return { data, attributes }; } + + function sortAttributes( attributes ) { + attributes = Array.from( attributes ); + + return attributes.sort( ( attr1, attr2 ) => { + const key1 = attr1[ 0 ]; + const key2 = attr2[ 0 ]; + + if ( key1 > key2 ) { + return 1; + } + + if ( key1 < key2 ) { + return -1; + } + + return 0; + } ); + } } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index 53c0ddafed3..a0ef6682e19 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -29,4 +29,8 @@

Nested span: I'm blue!

+ +

Deeply nested cite with span!

+ +

Span with no attributes!

From 3d014e984af54e5dd17aeee4fe3dd862ac8c13be Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 15 Mar 2021 13:58:51 +0100 Subject: [PATCH 067/217] Added missing unit tests. --- .../tests/datafilter.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 476cbe8e50c..bf7a69ba14c 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -644,6 +644,45 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

foo

' ); } ); + + it( 'should not preserve span with no attributes', () => { + dataFilter.allowElement( { name: 'span' } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + attributes: {} + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should correctly merge class names', () => { + dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowAttributes( { name: 'span', classes: /[\s\S]+/ } ); + + editor.setData( '

foobarbaz

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlSpan="(1)">foo' + + '<$text htmlSpan="(2)">bar' + + '<$text htmlSpan="(3)">baz' + + '', + attributes: { + 1: { + classes: [ 'foo' ] + }, + 2: { + classes: [ 'bar', 'foo' ] + }, + 3: { + classes: [ 'baz', 'bar', 'foo' ] + } + } + } ); + } ); } ); function getModelDataWithAttributes( model, options ) { From d4ba2d0a0b8cc90b0e86dfa012c6319952bea814 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 15 Mar 2021 14:05:32 +0100 Subject: [PATCH 068/217] Docs corrections. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 3fc885377f3..12a1c4bf7d0 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -378,7 +378,6 @@ export default class DataFilter { // @param {module:engine/view/downcastwriter~DowncastWriter} writer // @param {Object} viewAttributes // @param {module:engine/view/element~Element} viewElement -// on all elements in the range. function setAttributesOn( writer, viewAttributes, viewElement ) { if ( viewAttributes.attributes ) { for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) { @@ -476,8 +475,10 @@ function mergeAttributes( oldValue, newValue ) { const result = cloneDeep( newValue ); for ( const key in oldValue ) { + // Merge classes. if ( Array.isArray( oldValue[ key ] ) ) { result[ key ] = uniq( [ ...oldValue[ key ], ...newValue[ key ] ] ); + // Merge attributes or styles. } else { result[ key ] = Object.assign( {}, oldValue[ key ], newValue[ key ] ); } From 142d013bb00292eee8354571eca0d4b7c4e388d6 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 15 Mar 2021 15:07:21 +0100 Subject: [PATCH 069/217] Corrected attributes merging order. --- .../src/datafilter.js | 8 +++---- .../tests/datafilter.js | 24 ++++++++++++++++--- .../tests/manual/generalhtmlsupport.js | 2 +- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 12a1c4bf7d0..52d05adf166 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -234,7 +234,7 @@ export default class DataFilter { // Node's children are converter recursively, so node can already include model attribute. // We want to extend it, not replace. const nodeAttributes = node.getAttribute( attributeKey ); - const attributesToAdd = mergeAttributes( nodeAttributes, viewAttributes || {} ); + const attributesToAdd = mergeAttributes( viewAttributes || {}, nodeAttributes || {} ); conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); } @@ -472,11 +472,11 @@ function iterableToObject( iterable, getValue ) { // @param {Object} newValue // @returns {Object} function mergeAttributes( oldValue, newValue ) { - const result = cloneDeep( newValue ); + const result = cloneDeep( oldValue ); - for ( const key in oldValue ) { + for ( const key in newValue ) { // Merge classes. - if ( Array.isArray( oldValue[ key ] ) ) { + if ( Array.isArray( newValue[ key ] ) ) { result[ key ] = uniq( [ ...oldValue[ key ], ...newValue[ key ] ] ); // Merge attributes or styles. } else { diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index bf7a69ba14c..5435403008a 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -517,7 +517,7 @@ describe( 'DataFilter', () => { } ); expect( editor.getData() ).to.equal( '

' + - 'cite' + + 'cite' + 'span' + '

' ); } ); @@ -675,16 +675,34 @@ describe( 'DataFilter', () => { classes: [ 'foo' ] }, 2: { - classes: [ 'bar', 'foo' ] + classes: [ 'foo', 'bar' ] }, 3: { - classes: [ 'baz', 'bar', 'foo' ] + classes: [ 'foo', 'bar', 'baz' ] } } } ); } ); } ); + it( 'should correctly resolve attributes nesting order', () => { + dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowAttributes( { name: 'span', styles: { color: /[\s\S]+/ } } ); + + editor.setData( '

foobar' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlSpan="(1)">foobar', + attributes: { + 1: { + styles: { color: 'blue' } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + function getModelDataWithAttributes( model, options ) { // Simplify GHS attributes as they are not very readable at this point due to object structure. let counter = 1; diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 3a1f9987860..97b637efad3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -64,7 +64,7 @@ class ExtendHTMLSupport extends Plugin { dataFilter.allowElement( { name: /^span|cite$/ } ); dataFilter.allowAttributes( { name: /^span|cite$/, attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.allowAttributes( { name: /^span|cite$/, styles: { color: /[\s\S]+/ } } ); - dataFilter.disallowAttributes( { name: /^span|cite$/, styles: { color: 'red' } } ); + // dataFilter.disallowAttributes( { name: /^span|cite$/, styles: { color: 'red' } } ); } } From 91fc98c2aab5fb0c2f6e19b49336283edc941a68 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 15 Mar 2021 15:12:04 +0100 Subject: [PATCH 070/217] Manual test fixes. --- .../tests/manual/generalhtmlsupport.html | 2 ++ .../tests/manual/generalhtmlsupport.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index a0ef6682e19..e64de28457f 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -33,4 +33,6 @@

Deeply nested cite with span!

Span with no attributes!

+ +

Span with nested styles!

diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 97b637efad3..3a1f9987860 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -64,7 +64,7 @@ class ExtendHTMLSupport extends Plugin { dataFilter.allowElement( { name: /^span|cite$/ } ); dataFilter.allowAttributes( { name: /^span|cite$/, attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.allowAttributes( { name: /^span|cite$/, styles: { color: /[\s\S]+/ } } ); - // dataFilter.disallowAttributes( { name: /^span|cite$/, styles: { color: 'red' } } ); + dataFilter.disallowAttributes( { name: /^span|cite$/, styles: { color: 'red' } } ); } } From 7e2ed81f4b083618cfee77770e6b6de155fb32a3 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 15 Mar 2021 15:17:54 +0100 Subject: [PATCH 071/217] Favor Set over uniq. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 52d05adf166..0ce68e4fd3c 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -10,7 +10,7 @@ import { Matcher } from 'ckeditor5/src/engine'; import { priorities, toArray } from 'ckeditor5/src/utils'; -import { cloneDeep, uniq } from 'lodash-es'; +import { cloneDeep } from 'lodash-es'; const DATA_SCHEMA_ATTRIBUTE_KEY = 'htmlAttributes'; @@ -477,7 +477,7 @@ function mergeAttributes( oldValue, newValue ) { for ( const key in newValue ) { // Merge classes. if ( Array.isArray( newValue[ key ] ) ) { - result[ key ] = uniq( [ ...oldValue[ key ], ...newValue[ key ] ] ); + result[ key ] = Array.from( new Set( [ ...oldValue[ key ], ...newValue[ key ] ] ) ); // Merge attributes or styles. } else { result[ key ] = Object.assign( {}, oldValue[ key ], newValue[ key ] ); From 0a7fe46ab305d52f8b3b4e845324fb6bd3b49716 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 16 Mar 2021 13:30:49 +0100 Subject: [PATCH 072/217] Removed 'transparent' elements. --- .../src/datafilter.js | 14 -------------- .../tests/datafilter.js | 13 ------------- 2 files changed, 27 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 0ce68e4fd3c..10aaf72be43 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -75,15 +75,6 @@ export default class DataFilter { * @member {Map.} #_disallowedAttributes */ this._disallowedAttributes = new Map(); - - /** - * A set of inline elements which should not be preserved during conversion if - * they are missing attributes. - * - * @readonly - * @member {Set.} #transparentElements - */ - this.transparentElements = new Set( [ 'span' ] ); } /** @@ -220,11 +211,6 @@ export default class DataFilter { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { const viewAttributes = this._matchAndConsumeAttributes( data.viewItem, conversionApi ); - // Skip information about transparent element type if there is no attribute to convert. - if ( !viewAttributes && this.transparentElements.has( viewName ) ) { - return; - } - // Convert children and set conversion result as a current data. data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 5435403008a..6a02616b2fb 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -645,19 +645,6 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

foo

' ); } ); - it( 'should not preserve span with no attributes', () => { - dataFilter.allowElement( { name: 'span' } ); - - editor.setData( '

foo

' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo', - attributes: {} - } ); - - expect( editor.getData() ).to.equal( '

foo

' ); - } ); - it( 'should correctly merge class names', () => { dataFilter.allowElement( { name: 'span' } ); dataFilter.allowAttributes( { name: 'span', classes: /[\s\S]+/ } ); From 87bea0954be466bffbbe8feb32ef3d1d96ba9d0f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 16 Mar 2021 14:02:52 +0100 Subject: [PATCH 073/217] Corrected regexpes. --- .../tests/datafilter.js | 14 +++++++------- .../tests/manual/generalhtmlsupport.js | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 6a02616b2fb..13e958098d5 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -168,7 +168,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow nested attributes', () => { - dataFilter.allowElement( { name: /article|section/ } ); + dataFilter.allowElement( { name: /^(article|section)$/ } ); dataFilter.allowAttributes( { name: /[\s\S]+/, attributes: { 'data-foo': /foo|bar/ } } ); editor.setData( '
' + @@ -203,10 +203,10 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes for all allowed definitions', () => { - dataFilter.allowElement( { name: /section|article/ } ); + dataFilter.allowElement( { name: /^(section|article)$/ } ); - dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-foo': 'foo' } } ); - dataFilter.allowAttributes( { name: /section|article/, attributes: { 'data-bar': 'bar' } } ); + dataFilter.allowAttributes( { name: /^(section|article)$/, attributes: { 'data-foo': 'foo' } } ); + dataFilter.allowAttributes( { name: /^(section|article)$/, attributes: { 'data-bar': 'bar' } } ); editor.setData( '

foo

' + @@ -481,9 +481,9 @@ describe( 'DataFilter', () => { } ); it( 'should allow nested attributes', () => { - dataFilter.allowElement( { name: /span|cite/ } ); - dataFilter.allowAttributes( { name: /span|cite/, attributes: { 'data-foo': 'foo' } } ); - dataFilter.allowAttributes( { name: /span|cite/, attributes: { 'data-bar': 'bar' } } ); + dataFilter.allowElement( { name: /^(span|cite)$/ } ); + dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-foo': 'foo' } } ); + dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-bar': 'bar' } } ); editor.setData( '

' + 'cite' + diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 3a1f9987860..2a97c2ddb78 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -47,8 +47,8 @@ class ExtendHTMLSupport extends Plugin { // e.g. article -> ghsArticle dataFilter.allowElement( { name: 'article' } ); dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowElement( { name: /^details|summary$/ } ); - dataFilter.allowElement( { name: /^dl|dd|dt$/ } ); + dataFilter.allowElement( { name: /^(details|summary)$/ } ); + dataFilter.allowElement( { name: /^(dl|dd|dt)$/ } ); dataFilter.allowElement( { name: 'xyz' } ); // Let's extend 'section' with some attributes. Data filter will take care of @@ -61,10 +61,10 @@ class ExtendHTMLSupport extends Plugin { dataFilter.disallowAttributes( { name: 'section', attributes: { id: /^_.*/ } } ); // Let's also add some inline elements support: - dataFilter.allowElement( { name: /^span|cite$/ } ); - dataFilter.allowAttributes( { name: /^span|cite$/, attributes: { 'data-foo': /[\s\S]+/ } } ); - dataFilter.allowAttributes( { name: /^span|cite$/, styles: { color: /[\s\S]+/ } } ); - dataFilter.disallowAttributes( { name: /^span|cite$/, styles: { color: 'red' } } ); + dataFilter.allowElement( { name: /^(span|cite)$/ } ); + dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: /^(span|cite)$/, styles: { color: 'red' } } ); } } From 7f1d7d8c79b2322eff6c087d3629d6cfb03bd172 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 16 Mar 2021 15:12:46 +0100 Subject: [PATCH 074/217] Fixed converter to include passed model range. --- .../package.json | 1 + .../src/datafilter.js | 7 ++- .../tests/datafilter.js | 63 ++++++++++++++++--- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index b175869e6b6..05c00a34771 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -25,6 +25,7 @@ "@ckeditor/ckeditor5-editor-classic": "^27.0.0", "@ckeditor/ckeditor5-engine": "^27.0.0", "@ckeditor/ckeditor5-essentials": "^27.0.0", + "@ckeditor/ckeditor5-font": "^27.0.0", "@ckeditor/ckeditor5-list": "^27.0.0", "@ckeditor/ckeditor5-paragraph": "^27.0.0", "webpack": "^4.43.0", diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 10aaf72be43..9ee2997ff15 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -211,8 +211,11 @@ export default class DataFilter { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { const viewAttributes = this._matchAndConsumeAttributes( data.viewItem, conversionApi ); - // Convert children and set conversion result as a current data. - data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); + // Since we are converting to attribute we need an range on which we will set the attribute. + // If the range is not created yet, we will create it. + if ( !data.modelRange ) { + data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); + } // Set attribute on each item in range according to Schema. for ( const node of data.modelRange.getItems() ) { diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 13e958098d5..95449d2a45c 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -3,20 +3,26 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +/* global document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Font from '@ckeditor/ckeditor5-font/src/font'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import DataSchema from '../src/dataschema'; import DataFilter from '../src/datafilter'; describe( 'DataFilter', () => { - let editor, model, dataFilter, dataSchema; + let editor, model, dataFilter, dataSchema, element; beforeEach( () => { - return VirtualTestEditor - .create( { - plugins: [ Paragraph ] + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ Paragraph, Font ] } ) .then( newEditor => { editor = newEditor; @@ -28,6 +34,8 @@ describe( 'DataFilter', () => { } ); afterEach( () => { + element.remove(); + return editor.destroy(); } ); @@ -674,20 +682,57 @@ describe( 'DataFilter', () => { it( 'should correctly resolve attributes nesting order', () => { dataFilter.allowElement( { name: 'span' } ); - dataFilter.allowAttributes( { name: 'span', styles: { color: /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: 'span', styles: { 'font-weight': /[\s\S]+/ } } ); - editor.setData( '

foobar' ); + editor.setData( '

foobar' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '<$text htmlSpan="(1)">foobar', attributes: { 1: { - styles: { color: 'blue' } + styles: { 'font-weight': '400' } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should allow using attributes by other features', () => { + dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowAttributes( { name: 'span', styles: { 'color': /[\s\S]+/ } } ); + + editor.setData( '

foobar

' ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text fontColor="blue" htmlSpan="(1)">foobar', + attributes: { + 1: {} + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should preserve attributes not used by other features', () => { + dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowAttributes( { name: 'span', styles: { 'color': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: 'span', classes: [ 'foo', 'bar' ] } ); + + editor.setData( '

foobar

' ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text fontColor="blue" htmlSpan="(1)">foobar', + attributes: { + 1: { + classes: [ 'foo', 'bar' ] } } } ); - expect( editor.getData() ).to.equal( '

foobar

' ); + expect( editor.getData() ).to.equal( '

foobar

' ); } ); function getModelDataWithAttributes( model, options ) { From 11e15e16bd1c614b81e3572a140c20dcd0640ef8 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 16 Mar 2021 15:52:53 +0100 Subject: [PATCH 075/217] Splitted schema register method into inline and block elements. --- .../src/datafilter.js | 33 ++- .../src/dataschema.js | 72 +++--- .../tests/datafilter.js | 2 +- .../tests/dataschema.js | 215 ++++++++++-------- 4 files changed, 187 insertions(+), 135 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 9ee2997ff15..c8ee6e726d6 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -8,7 +8,7 @@ */ import { Matcher } from 'ckeditor5/src/engine'; -import { priorities, toArray } from 'ckeditor5/src/utils'; +import { priorities, toArray, CKEditorError } from 'ckeditor5/src/utils'; import { cloneDeep } from 'lodash-es'; @@ -130,19 +130,32 @@ export default class DataFilter { /** * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition + * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _registerElement( definition ) { if ( definition.isInline ) { this._defineInlineElement( definition ); - } else { + } else if ( definition.isBlock ) { this._defineBlockElement( definition ); + } else { + /** + * Only a definition marked as inline or block can be allowed. + * + * @error data-filter-invalid-definition-type + */ + throw new CKEditorError( + 'data-filter-invalid-definition-type', + null, + { definition } + ); } } /** * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition + * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _defineBlockElement( definition ) { if ( this.editor.model.schema.isRegistered( definition.model ) ) { @@ -160,7 +173,8 @@ export default class DataFilter { /** * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition + * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _defineInlineElement( definition ) { const schema = this.editor.model.schema; @@ -178,7 +192,8 @@ export default class DataFilter { /** * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition + * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _defineSchema( definition ) { const schema = this.editor.model.schema; @@ -198,7 +213,8 @@ export default class DataFilter { /** * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition + * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _defineInlineElementConverters( definition ) { const conversion = this.editor.conversion; @@ -252,7 +268,8 @@ export default class DataFilter { /** * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition + * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _defineBlockElementConverters( definition ) { const conversion = this.editor.conversion; diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 969bd7936eb..d50bab59809 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -30,12 +30,12 @@ export default class DataSchema { * * @readonly * @private - * @member {Map.} #_definitions + * @member {Map.} #_definitions */ this._definitions = new Map(); - // Add block elements. - this.register( { + this.registerBlockElement( { model: '$htmlBlock', allowChildren: '$block', schema: { @@ -44,7 +44,7 @@ export default class DataSchema { } } ); - this.register( { + this.registerBlockElement( { view: 'article', model: 'htmlArticle', schema: { @@ -52,7 +52,7 @@ export default class DataSchema { } } ); - this.register( { + this.registerBlockElement( { view: 'section', model: 'htmlSection', schema: { @@ -61,7 +61,7 @@ export default class DataSchema { } ); // Add data list elements. - this.register( { + this.registerBlockElement( { view: 'dl', model: 'htmlDl', schema: { @@ -70,7 +70,7 @@ export default class DataSchema { } } ); - this.register( { + this.registerBlockElement( { model: '$htmlDatalist', allowChildren: '$block', schema: { @@ -79,7 +79,7 @@ export default class DataSchema { } } ); - this.register( { + this.registerBlockElement( { view: 'dt', model: 'htmlDt', schema: { @@ -87,7 +87,7 @@ export default class DataSchema { } } ); - this.register( { + this.registerBlockElement( { view: 'dd', model: 'htmlDd', schema: { @@ -96,7 +96,7 @@ export default class DataSchema { } ); // Add details elements. - this.register( { + this.registerBlockElement( { view: 'details', model: 'htmlDetails', schema: { @@ -104,7 +104,7 @@ export default class DataSchema { } } ); - this.register( { + this.registerBlockElement( { view: 'summary', model: 'htmlSummary', allowChildren: '$text', @@ -113,27 +113,33 @@ export default class DataSchema { } } ); - // Add inline elements. - this.register( { + this.registerInlineElement( { view: 'span', - model: 'htmlSpan', - isInline: true + model: 'htmlSpan' } ); - this.register( { + this.registerInlineElement( { view: 'cite', - model: 'htmlCite', - isInline: true + model: 'htmlCite' } ); } /** - * Add new data schema definition. + * Add new data schema definition for block element. * - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ - register( definition ) { - this._definitions.set( definition.model, definition ); + registerBlockElement( definition ) { + this._definitions.set( definition.model, { ...definition, isBlock: true } ); + } + + /** + * Add new data schema definition for inline element. + * + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + */ + registerInlineElement( definition ) { + this._definitions.set( definition.model, { ...definition, isInline: true } ); } /** @@ -141,7 +147,8 @@ export default class DataSchema { * * @param {String|RegExp} viewName * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. - * @returns {Set.} + * @returns {Set.} */ getDefinitionsForView( viewName, includeReferences ) { const definitions = new Set(); @@ -164,7 +171,8 @@ export default class DataSchema { * * @private * @param {String|RegExp} viewName - * @returns {Array.} + * @returns {Array.} */ _getMatchingViewDefinitions( viewName ) { return Array.from( this._definitions.values() ) @@ -176,7 +184,8 @@ export default class DataSchema { * * @private * @param {String} modelName Data schema model name. - * @returns {Iterable.} + * @returns {Iterable.} */ * _getReferences( modelName ) { const { schema } = this._definitions.get( modelName ); @@ -219,12 +228,19 @@ function testViewName( pattern, viewName ) { } /** - * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema}. + * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for block elements. * - * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition + * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. * @property {String} model Name of the model element. * @property {module:engine/model/schema~SchemaItemDefinition} schema The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. - * @property {Boolean} [isInline] Indicates if the element decribed by data schema definition is inline. + */ + +/** + * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for inline elements. + * + * @typedef {Object} module:content-compatibility/dataschema~DataSchemaInlineElementDefinition + * @property {String} view Name of the view element. + * @property {String} model Name of the model attribute key. */ diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 95449d2a45c..7e65ff45baf 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -323,7 +323,7 @@ describe( 'DataFilter', () => { } ); it( 'should extend allowed children only if specified model schema exists', () => { - dataSchema.register( { + dataSchema.registerBlockElement( { view: 'xyz', model: 'htmlXyz', allowChildren: 'not-exists', diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 60ce0843d24..adc0623ad57 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -9,52 +9,6 @@ import DataSchema from '../src/dataschema'; describe( 'DataSchema', () => { let editor, dataSchema; - const fakeDefinitions = [ - { - view: 'def1', - model: 'htmlDef1', - allowChildren: [ 'htmlDef2', 'htmlDef3' ], - schema: { - inheritAllFrom: '$block' - } - }, - { - view: 'def2', - model: 'htmlDef2', - schema: { - inheritAllFrom: 'htmlDef1' - } - }, - { - view: 'def3', - model: 'htmlDef3', - schema: { - inheritTypesFrom: 'htmlDef2' - } - }, - { - view: 'def4', - model: 'htmlDef4', - schema: { - allowWhere: 'htmlDef3' - } - }, - { - view: 'def5', - model: 'htmlDef5', - schema: { - allowContentOf: 'htmlDef4' - } - }, - { - view: 'def6', - model: 'htmlDef6', - schema: { - allowAttributesOf: 'htmlDef5' - } - } - ]; - beforeEach( () => { return VirtualTestEditor .create() @@ -69,85 +23,150 @@ describe( 'DataSchema', () => { return editor.destroy(); } ); - it( 'should allow registering schema with proper definition', () => { - const definitions = getFakeDefinitions( 'def1' ); - - dataSchema.register( definitions[ 0 ] ); + describe( 'registerInlineElement', () => { + it( 'should register proper definition', () => { + dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def' } ); - const result = dataSchema.getDefinitionsForView( 'def1' ); + const result = dataSchema.getDefinitionsForView( 'def' ); - expect( Array.from( result ) ).to.deep.equal( definitions ); + expect( Array.from( result ) ).to.deep.equal( [ { + model: 'htmlDef', + view: 'def', + isInline: true + } ] ); + } ); } ); - it( 'should allow resolving definitions by view name (string)', () => { - registerMany( dataSchema, fakeDefinitions ); + describe( 'registerBlockElement', () => { + const fakeDefinitions = [ + { + view: 'def1', + model: 'htmlDef1', + allowChildren: [ 'htmlDef2', 'htmlDef3' ], + schema: { + inheritAllFrom: '$block' + } + }, + { + view: 'def2', + model: 'htmlDef2', + schema: { + inheritAllFrom: 'htmlDef1' + } + }, + { + view: 'def3', + model: 'htmlDef3', + schema: { + inheritTypesFrom: 'htmlDef2' + } + }, + { + view: 'def4', + model: 'htmlDef4', + schema: { + allowWhere: 'htmlDef3' + } + }, + { + view: 'def5', + model: 'htmlDef5', + schema: { + allowContentOf: 'htmlDef4' + } + }, + { + view: 'def6', + model: 'htmlDef6', + schema: { + allowAttributesOf: 'htmlDef5' + } + } + ]; - const result = dataSchema.getDefinitionsForView( 'def2' ); + it( 'should allow registering schema with proper definition', () => { + dataSchema.registerBlockElement( getFakeDefinitions( 'def1' )[ 0 ] ); - expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def2' ) ); - } ); + const result = dataSchema.getDefinitionsForView( 'def1' ); - it( 'should allow resolving definitions by view name (RegExp)', () => { - registerMany( dataSchema, fakeDefinitions ); + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1' ) ); + } ); - const result = dataSchema.getDefinitionsForView( /^def1|def2$/ ); + it( 'should allow resolving definitions by view name (string)', () => { + registerMany( dataSchema, fakeDefinitions ); - expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2' ) ); - } ); + const result = dataSchema.getDefinitionsForView( 'def2' ); - it( 'should allow resolving definitions by view name including references (inheritAllFrom)', () => { - registerMany( dataSchema, fakeDefinitions ); + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def2' ) ); + } ); - const result = dataSchema.getDefinitionsForView( 'def2', true ); + it( 'should allow resolving definitions by view name (RegExp)', () => { + registerMany( dataSchema, fakeDefinitions ); - expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2' ) ); - } ); + const result = dataSchema.getDefinitionsForView( /^(def1|def2)$/ ); - it( 'should allow resolving definitions by view name including references (inheritTypes)', () => { - registerMany( dataSchema, fakeDefinitions ); + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1', 'def2' ) ); + } ); - const result = dataSchema.getDefinitionsForView( 'def3', true ); + it( 'should allow resolving definitions by view name including references (inheritAllFrom)', () => { + registerMany( dataSchema, fakeDefinitions ); - expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3' ) ); - } ); + const result = dataSchema.getDefinitionsForView( 'def2', true ); - it( 'should allow resolving definitions by view name including references (allowWhere)', () => { - registerMany( dataSchema, fakeDefinitions ); + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1', 'def2' ) ); + } ); - const result = dataSchema.getDefinitionsForView( 'def4', true ); + it( 'should allow resolving definitions by view name including references (inheritTypes)', () => { + registerMany( dataSchema, fakeDefinitions ); - expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3', 'def4' ) ); - } ); + const result = dataSchema.getDefinitionsForView( 'def3', true ); - it( 'should allow resolving definitions by view name including references (allowContentOf)', () => { - registerMany( dataSchema, fakeDefinitions ); + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1', 'def2', 'def3' ) ); + } ); - const result = dataSchema.getDefinitionsForView( 'def5', true ); + it( 'should allow resolving definitions by view name including references (allowWhere)', () => { + registerMany( dataSchema, fakeDefinitions ); - expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3', 'def4', 'def5' ) ); - } ); + const result = dataSchema.getDefinitionsForView( 'def4', true ); - it( 'should allow resolving definitions by view name including references (allowAttributesOf)', () => { - registerMany( dataSchema, fakeDefinitions ); + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1', 'def2', 'def3', 'def4' ) ); + } ); - const result = dataSchema.getDefinitionsForView( 'def6', true ); + it( 'should allow resolving definitions by view name including references (allowContentOf)', () => { + registerMany( dataSchema, fakeDefinitions ); - expect( Array.from( result ) ).to.deep.equal( getFakeDefinitions( 'def1', 'def2', 'def3', 'def4', 'def5', 'def6' ) ); - } ); + const result = dataSchema.getDefinitionsForView( 'def5', true ); - it( 'should return nothing for invalid view name', () => { - registerMany( dataSchema, fakeDefinitions ); + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1', 'def2', 'def3', 'def4', 'def5' ) ); + } ); - const result = dataSchema.getDefinitionsForView( null ); + it( 'should allow resolving definitions by view name including references (allowAttributesOf)', () => { + registerMany( dataSchema, fakeDefinitions ); - expect( result.size ).to.equal( 0 ); - } ); + const result = dataSchema.getDefinitionsForView( 'def6', true ); + + expect( Array.from( result ) ).to.deep.equal( getExpectedFakeDefinitions( 'def1', 'def2', 'def3', 'def4', 'def5', 'def6' ) ); + } ); + + it( 'should return nothing for invalid view name', () => { + registerMany( dataSchema, fakeDefinitions ); + + const result = dataSchema.getDefinitionsForView( null ); - function registerMany( dataSchema, definitions ) { - definitions.forEach( def => dataSchema.register( def ) ); - } + expect( result.size ).to.equal( 0 ); + } ); - function getFakeDefinitions( ...viewNames ) { - return fakeDefinitions.filter( def => viewNames.includes( def.view ) ); - } + function registerMany( dataSchema, definitions ) { + definitions.forEach( def => dataSchema.registerBlockElement( def ) ); + } + + function getFakeDefinitions( ...viewNames ) { + return fakeDefinitions.filter( def => viewNames.includes( def.view ) ); + } + + function getExpectedFakeDefinitions( ...viewNames ) { + // It's expected that definition will include `isBlock` property. + return getFakeDefinitions( ...viewNames ).map( def => ( { ...def, isBlock: true } ) ); + } + } ); } ); From 2f2a32055b2de6c86e894d6b3f035d4bcf155dc0 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 16 Mar 2021 15:56:59 +0100 Subject: [PATCH 076/217] Renaming schema to modelSchema. --- .../src/datafilter.js | 2 +- .../src/dataschema.js | 28 +++++++++---------- .../tests/dataschema.js | 12 ++++---- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index c8ee6e726d6..f96b68e6b2f 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -198,7 +198,7 @@ export default class DataFilter { _defineSchema( definition ) { const schema = this.editor.model.schema; - schema.register( definition.model, definition.schema ); + schema.register( definition.model, definition.modelSchema ); const allowedChildren = toArray( definition.allowChildren || [] ); diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index d50bab59809..c55a13ea4b7 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -18,7 +18,7 @@ import { toArray } from 'ckeditor5/src/utils'; * dataSchema.register( { * view: 'section', * model: 'my-section', - * schema: { + * modelSchema: { * inheritAllFrom: '$block' * } * } ); @@ -38,7 +38,7 @@ export default class DataSchema { this.registerBlockElement( { model: '$htmlBlock', allowChildren: '$block', - schema: { + modelSchema: { allowIn: [ '$root', '$htmlBlock' ], isBlock: true } @@ -47,7 +47,7 @@ export default class DataSchema { this.registerBlockElement( { view: 'article', model: 'htmlArticle', - schema: { + modelSchema: { inheritAllFrom: '$htmlBlock' } } ); @@ -55,7 +55,7 @@ export default class DataSchema { this.registerBlockElement( { view: 'section', model: 'htmlSection', - schema: { + modelSchema: { inheritAllFrom: '$htmlBlock' } } ); @@ -64,7 +64,7 @@ export default class DataSchema { this.registerBlockElement( { view: 'dl', model: 'htmlDl', - schema: { + modelSchema: { allowIn: [ '$htmlBlock', '$root' ], isBlock: true } @@ -73,7 +73,7 @@ export default class DataSchema { this.registerBlockElement( { model: '$htmlDatalist', allowChildren: '$block', - schema: { + modelSchema: { allowIn: 'htmlDl', isBlock: true } @@ -82,7 +82,7 @@ export default class DataSchema { this.registerBlockElement( { view: 'dt', model: 'htmlDt', - schema: { + modelSchema: { inheritAllFrom: '$htmlDatalist' } } ); @@ -90,7 +90,7 @@ export default class DataSchema { this.registerBlockElement( { view: 'dd', model: 'htmlDd', - schema: { + modelSchema: { inheritAllFrom: '$htmlDatalist' } } ); @@ -99,7 +99,7 @@ export default class DataSchema { this.registerBlockElement( { view: 'details', model: 'htmlDetails', - schema: { + modelSchema: { inheritAllFrom: '$htmlBlock' } } ); @@ -108,7 +108,7 @@ export default class DataSchema { view: 'summary', model: 'htmlSummary', allowChildren: '$text', - schema: { + modelSchema: { allowIn: 'htmlDetails' } } ); @@ -188,16 +188,16 @@ export default class DataSchema { * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition>} */ * _getReferences( modelName ) { - const { schema } = this._definitions.get( modelName ); + const { modelSchema } = this._definitions.get( modelName ); - if ( !schema ) { + if ( !modelSchema ) { return; } const inheritProperties = [ 'inheritAllFrom', 'inheritTypesFrom', 'allowWhere', 'allowContentOf', 'allowAttributesOf' ]; for ( const property of inheritProperties ) { - for ( const referenceName of toArray( schema[ property ] || [] ) ) { + for ( const referenceName of toArray( modelSchema[ property ] || [] ) ) { const definition = this._definitions.get( referenceName ); if ( referenceName !== modelName && definition ) { @@ -233,7 +233,7 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. * @property {String} model Name of the model element. - * @property {module:engine/model/schema~SchemaItemDefinition} schema The model schema item definition describing registered model. + * @property {module:engine/model/schema~SchemaItemDefinition} modelSchema The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. */ diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index adc0623ad57..a23d7eb2d0f 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -43,42 +43,42 @@ describe( 'DataSchema', () => { view: 'def1', model: 'htmlDef1', allowChildren: [ 'htmlDef2', 'htmlDef3' ], - schema: { + modelSchema: { inheritAllFrom: '$block' } }, { view: 'def2', model: 'htmlDef2', - schema: { + modelSchema: { inheritAllFrom: 'htmlDef1' } }, { view: 'def3', model: 'htmlDef3', - schema: { + modelSchema: { inheritTypesFrom: 'htmlDef2' } }, { view: 'def4', model: 'htmlDef4', - schema: { + modelSchema: { allowWhere: 'htmlDef3' } }, { view: 'def5', model: 'htmlDef5', - schema: { + modelSchema: { allowContentOf: 'htmlDef4' } }, { view: 'def6', model: 'htmlDef6', - schema: { + modelSchema: { allowAttributesOf: 'htmlDef5' } } From 65966543a86414c26d574611f23c258c432e0eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20Bogda=C5=84ski?= Date: Wed, 17 Mar 2021 09:43:17 +0100 Subject: [PATCH 077/217] Apply suggestions from code review Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- .../src/datafilter.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index f96b68e6b2f..81c2068a9f3 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -227,16 +227,16 @@ export default class DataFilter { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { const viewAttributes = this._matchAndConsumeAttributes( data.viewItem, conversionApi ); - // Since we are converting to attribute we need an range on which we will set the attribute. + // Since we are converting to attribute we need a range on which we will set the attribute. // If the range is not created yet, we will create it. if ( !data.modelRange ) { data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); } - // Set attribute on each item in range according to Schema. + // Set attribute on each item in range according to the schema. for ( const node of data.modelRange.getItems() ) { if ( conversionApi.schema.checkAttribute( node, attributeKey ) ) { - // Node's children are converter recursively, so node can already include model attribute. + // Node's children are converted recursively, so node can already include model attribute. // We want to extend it, not replace. const nodeAttributes = node.getAttribute( attributeKey ); const attributesToAdd = mergeAttributes( viewAttributes || {}, nodeAttributes || {} ); @@ -244,9 +244,7 @@ export default class DataFilter { conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); } } - }, { - priority: 'low' - } ); + }, { priority: 'low' } ); } ); conversion.for( 'downcast' ).attributeToElement( { @@ -484,9 +482,11 @@ function mergeAttributes( oldValue, newValue ) { // Merge classes. if ( Array.isArray( newValue[ key ] ) ) { result[ key ] = Array.from( new Set( [ ...oldValue[ key ], ...newValue[ key ] ] ) ); + } + // Merge attributes or styles. - } else { - result[ key ] = Object.assign( {}, oldValue[ key ], newValue[ key ] ); + else { + result[ key ] = { ...oldValue[ key ], ...newValue[ key ] }; } } From b2ca37193c05857e44c4413fbead710668c31ba6 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 08:51:56 +0100 Subject: [PATCH 078/217] Fixed manual test. --- .../tests/manual/generalhtmlsupport.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 2a97c2ddb78..54d8c2849b1 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -31,11 +31,11 @@ class ExtendHTMLSupport extends Plugin { const dataSchema = new DataSchema(); // Extend schema with custom `xyz` element. - dataSchema.register( { + dataSchema.registerBlockElement( { view: 'xyz', model: 'ghsXyz', - schema: { - inheritAllFrom: '$ghsBlock' + modelSchema: { + inheritAllFrom: '$htmlBlock' } } ); From 2f3626b806c0fa9cd23bea5067fd58420a8f7e3c Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 08:53:54 +0100 Subject: [PATCH 079/217] Renaming general to generic. --- .../manual/{generalhtmlsupport.html => generichtmlsupport.html} | 0 .../manual/{generalhtmlsupport.js => generichtmlsupport.js} | 2 +- .../manual/{generalhtmlsupport.md => generichtmlsupport.md} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/ckeditor5-content-compatibility/tests/manual/{generalhtmlsupport.html => generichtmlsupport.html} (100%) rename packages/ckeditor5-content-compatibility/tests/manual/{generalhtmlsupport.js => generichtmlsupport.js} (99%) rename packages/ckeditor5-content-compatibility/tests/manual/{generalhtmlsupport.md => generichtmlsupport.md} (100%) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html rename to packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js similarity index 99% rename from packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js rename to packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js index 54d8c2849b1..dd3604a16a6 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js @@ -33,7 +33,7 @@ class ExtendHTMLSupport extends Plugin { // Extend schema with custom `xyz` element. dataSchema.registerBlockElement( { view: 'xyz', - model: 'ghsXyz', + model: 'htmlXyz', modelSchema: { inheritAllFrom: '$htmlBlock' } diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.md similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md rename to packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.md From 834b4d6a6d1060f509a3c8237be5120fdbe5ab53 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 09:40:18 +0100 Subject: [PATCH 080/217] Refactoring. --- .../src/datafilter.js | 62 +++++++++++-------- .../src/dataschema.js | 11 +++- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 81c2068a9f3..35e9e5b69bb 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -164,11 +164,9 @@ export default class DataFilter { this._defineSchema( definition ); - if ( !definition.view ) { - return; + if ( definition.view ) { + this._defineBlockElementConverters( definition ); } - - this._defineBlockElementConverters( definition ); } /** @@ -183,9 +181,9 @@ export default class DataFilter { allowAttributes: definition.model } ); - schema.setAttributeProperties( definition.model, { - copyOnEnter: true - } ); + if ( definition.attributeProperties ) { + schema.setAttributeProperties( definition.model, definition.attributeProperties ); + } this._defineInlineElementConverters( definition ); } @@ -225,7 +223,7 @@ export default class DataFilter { conversion.for( 'upcast' ).add( dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - const viewAttributes = this._matchAndConsumeAttributes( data.viewItem, conversionApi ); + const viewAttributes = this._matchAndConsumeAllowedAttributes( data.viewItem, conversionApi ); // Since we are converting to attribute we need a range on which we will set the attribute. // If the range is not created yet, we will create it. @@ -281,7 +279,7 @@ export default class DataFilter { view: viewName, model: ( viewElement, conversionApi ) => { const element = conversionApi.writer.createElement( modelName ); - const viewAttributes = this._matchAndConsumeAttributes( viewElement, conversionApi ); + const viewAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); if ( viewAttributes ) { conversionApi.writer.setAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes, element ); @@ -327,9 +325,7 @@ export default class DataFilter { // Consumes disallowed element attributes to prevent them of being processed by other converters. conversion.for( 'upcast' ).add( dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - for ( const match of matchAll( data.viewItem, this._disallowedAttributes ) ) { - conversionApi.consumable.consume( data.viewItem, match.match ); - } + consumeAttributeMatches( data.viewItem, conversionApi, this._disallowedAttributes ); }, { priority: 'high' } ); } ); } @@ -341,18 +337,12 @@ export default class DataFilter { * @param {module:engine/view/element~Element} viewElement * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi * @returns {Object} [result] - * @returns {Array.} result.attributes Array with matched attribute names. - * @returns {Array.} result.classes Array with matched class names. - * @returns {Array.} result.styles Array with matched style names. + * @returns {Set.} result.attributes Set with matched attribute names. + * @returns {Set.} result.styles Set with matched style names. + * @returns {Set.} result.classes Set with matched class names. */ - _matchAndConsumeAttributes( viewElement, { consumable } ) { - const matches = []; - for ( const match of matchAll( viewElement, this._allowedAttributes ) ) { - if ( consumable.consume( viewElement, match.match ) ) { - matches.push( match ); - } - } - + _matchAndConsumeAllowedAttributes( viewElement, conversionApi ) { + const matches = consumeAttributeMatches( viewElement, conversionApi, this._allowedAttributes ); const { attributes, styles, classes } = mergeMatchResults( matches ); const viewAttributes = {}; @@ -376,6 +366,26 @@ export default class DataFilter { } } +// Consumes attributes matched for the given `rules`. +// +// Returns sucessfully consumed attribute matches. +// +// @private +// @param {module:engine/view/element~Element} viewElement +// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi +// @param {Map.} rules +// @returns {Array.} Array with match information about found attributes. +function consumeAttributeMatches( viewElement, { consumable }, rules ) { + const matches = []; + for ( const match of matchAll( viewElement, rules ) ) { + if ( consumable.consume( viewElement, match.match ) ) { + matches.push( match ); + } + } + + return matches; +} + // Helper function for downcast converter. Sets attributes on the given view element. // // @private @@ -430,11 +440,11 @@ function matchAll( viewElement, rules ) { // Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. // // @private -// @param {Array} matches +// @param {Array.} matches // @returns {Object} result -// @returns {Set.} result.attributes Set with matched attribute names. +// @returns {Set.} result.attributes Set with matched attribute names. +// @returns {Set.} result.styles Set with matched style names. // @returns {Set.} result.classes Set with matched class names. -// @returns {Set.} result.styles Set with matched style names. function mergeMatchResults( matches ) { const matchResult = { attributes: new Set(), diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index c55a13ea4b7..ea94c3a8245 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -115,12 +115,18 @@ export default class DataSchema { this.registerInlineElement( { view: 'span', - model: 'htmlSpan' + model: 'htmlSpan', + attributeProperties: { + copyOnEnter: true + } } ); this.registerInlineElement( { view: 'cite', - model: 'htmlCite' + model: 'htmlCite', + attributeProperties: { + copyOnEnter: true + } } ); } @@ -243,4 +249,5 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaInlineElementDefinition * @property {String} view Name of the view element. * @property {String} model Name of the model attribute key. + * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. */ From baa3f8b595a75593fabe2a5cf2248de184163db1 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 09:50:54 +0100 Subject: [PATCH 081/217] Renaming. --- .../src/datafilter.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 35e9e5b69bb..803466e6dbf 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -237,7 +237,7 @@ export default class DataFilter { // Node's children are converted recursively, so node can already include model attribute. // We want to extend it, not replace. const nodeAttributes = node.getAttribute( attributeKey ); - const attributesToAdd = mergeAttributes( viewAttributes || {}, nodeAttributes || {} ); + const attributesToAdd = mergeViewElementAttributes( viewAttributes || {}, nodeAttributes || {} ); conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); } @@ -255,7 +255,7 @@ export default class DataFilter { const { writer } = conversionApi; const viewElement = writer.createAttributeElement( viewName ); - setAttributesOn( writer, attributeValue, viewElement ); + setViewElementAttributes( writer, attributeValue, viewElement ); return viewElement; } @@ -308,7 +308,7 @@ export default class DataFilter { const viewWriter = conversionApi.writer; const viewElement = conversionApi.mapper.toViewElement( data.item ); - setAttributesOn( viewWriter, viewAttributes, viewElement ); + setViewElementAttributes( viewWriter, viewAttributes, viewElement ); } ); } ); } @@ -377,6 +377,7 @@ export default class DataFilter { // @returns {Array.} Array with match information about found attributes. function consumeAttributeMatches( viewElement, { consumable }, rules ) { const matches = []; + for ( const match of matchAll( viewElement, rules ) ) { if ( consumable.consume( viewElement, match.match ) ) { matches.push( match ); @@ -392,7 +393,7 @@ function consumeAttributeMatches( viewElement, { consumable }, rules ) { // @param {module:engine/view/downcastwriter~DowncastWriter} writer // @param {Object} viewAttributes // @param {module:engine/view/element~Element} viewElement -function setAttributesOn( writer, viewAttributes, viewElement ) { +function setViewElementAttributes( writer, viewAttributes, viewElement ) { if ( viewAttributes.attributes ) { for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) { writer.setAttribute( key, value, viewElement ); @@ -485,15 +486,15 @@ function iterableToObject( iterable, getValue ) { // @param {Object} oldValue // @param {Object} newValue // @returns {Object} -function mergeAttributes( oldValue, newValue ) { +function mergeViewElementAttributes( oldValue, newValue ) { const result = cloneDeep( oldValue ); for ( const key in newValue ) { // Merge classes. if ( Array.isArray( newValue[ key ] ) ) { result[ key ] = Array.from( new Set( [ ...oldValue[ key ], ...newValue[ key ] ] ) ); - } - + } + // Merge attributes or styles. else { result[ key ] = { ...oldValue[ key ], ...newValue[ key ] }; From b73e6c85babb681fb3e74d29415c7f3135635dc8 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 10:22:31 +0100 Subject: [PATCH 082/217] Docs. --- .../src/datafilter.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 803466e6dbf..a1fd77e2af1 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -129,6 +129,8 @@ export default class DataFilter { } /** + * Registers element and attribute converters for the given data schema definition. + * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition @@ -153,6 +155,10 @@ export default class DataFilter { } /** + * Registers block element and attribute converters for the given data schema definition. + * + * If the element model schema is already registered, this method will do nothing. + * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition @@ -162,7 +168,7 @@ export default class DataFilter { return; } - this._defineSchema( definition ); + this._defineBlockElementSchema( definition ); if ( definition.view ) { this._defineBlockElementConverters( definition ); @@ -170,6 +176,10 @@ export default class DataFilter { } /** + * Registers inline element and attribute converters for the given data schema definition. + * + * Extends `$text` model schema to allow the given definition model attribute and its properties. + * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition @@ -189,11 +199,13 @@ export default class DataFilter { } /** + * Registers model schema definition for the given block element definition. + * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ - _defineSchema( definition ) { + _defineBlockElementSchema( definition ) { const schema = this.editor.model.schema; schema.register( definition.model, definition.modelSchema ); @@ -210,6 +222,8 @@ export default class DataFilter { } /** + * Registers attribute converters for the given inline element definition. + * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition @@ -263,6 +277,8 @@ export default class DataFilter { } /** + * Registers attribute converters for the given block element definition. + * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition From 16e22f1817ebc6682d010679bf40bfd767736ec0 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 12:35:53 +0100 Subject: [PATCH 083/217] Added test coverage, updated error message. --- .../src/datafilter.js | 2 +- .../tests/datafilter.js | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index a1fd77e2af1..9c75872c2d0 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -149,7 +149,7 @@ export default class DataFilter { throw new CKEditorError( 'data-filter-invalid-definition-type', null, - { definition } + definition ); } } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 7e65ff45baf..b1543879d39 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -8,6 +8,8 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Font from '@ckeditor/ckeditor5-font/src/font'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import DataSchema from '../src/dataschema'; @@ -16,6 +18,8 @@ import DataFilter from '../src/datafilter'; describe( 'DataFilter', () => { let editor, model, dataFilter, dataSchema, element; + testUtils.createSinonSandbox(); + beforeEach( () => { element = document.createElement( 'div' ); document.body.appendChild( element ); @@ -678,6 +682,57 @@ describe( 'DataFilter', () => { } } ); } ); + + describe( 'attribute properties', () => { + it( 'should set if given', () => { + dataSchema.registerInlineElement( { + view: 'xyz', + model: 'htmlXyz', + attributeProperties: { + copyOnEnter: true + } + } ); + + dataFilter.allowElement( { name: 'xyz' } ); + + expect( editor.model.schema.getAttributeProperties( 'htmlXyz' ) ).to.deep.equal( { copyOnEnter: true } ); + } ); + + it( 'should not set if missing', () => { + dataSchema.registerInlineElement( { + view: 'xyz', + model: 'htmlXyz' + } ); + + dataFilter.allowElement( { name: 'xyz' } ); + + expect( editor.model.schema.getAttributeProperties( 'htmlXyz' ) ).to.deep.equal( {} ); + } ); + } ); + + it( 'should not set attribute if disallowed by schema', () => { + editor.model.schema.addAttributeCheck( ( context, attributeName ) => { + if ( context.endsWith( '$text' ) && attributeName === 'htmlXyz' ) { + return false; + } + } ); + + dataSchema.registerInlineElement( { + view: 'xyz', + model: 'htmlXyz', + attributeProperties: { + copyOnEnter: true + } + } ); + + dataFilter.allowElement( { name: 'xyz' } ); + + editor.setData( '

foobar

' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( 'foobar' ); + + editor.getData( '

foobar

' ); + } ); } ); it( 'should correctly resolve attributes nesting order', () => { @@ -735,6 +790,19 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

foobar

' ); } ); + it( 'should throw error if definition has no specified element type', () => { + const definition = { + view: 'xyz', + model: 'htmlXyz' + }; + + sinon.stub( dataSchema, 'getDefinitionsForView' ).returns( new Set( [ definition ] ) ); + + expectToThrowCKEditorError( () => { + dataFilter.allowElement( { name: 'xyz' } ); + }, /data-filter-invalid-definition-type/, null, definition ); + } ); + function getModelDataWithAttributes( model, options ) { // Simplify GHS attributes as they are not very readable at this point due to object structure. let counter = 1; From 6a9e3c89fb92878e907bbd9ae8693039e6521192 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 12:45:27 +0100 Subject: [PATCH 084/217] Updated data schema docs to new API. --- .../src/dataschema.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index ea94c3a8245..9d609906f84 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -13,15 +13,27 @@ import { toArray } from 'ckeditor5/src/utils'; * Holds representation of the extended HTML document type definitions to be used by the * editor in content compatibility support. * - * Data schema is represented by data schema definitions. To add new definition, use {@link #register} method: + * Data schema is represented by data schema definitions. * - * dataSchema.register( { + * To add new definition for block element, use {@link #registerBlockElement} method: + * + * dataSchema.registerBlockElement( { * view: 'section', * model: 'my-section', * modelSchema: { * inheritAllFrom: '$block' * } * } ); + * + * To add new definition for inline element, use {@link #registerInlineElement} method: + * + * dataSchema.registerInlineElement( { + * view: 'span', + * model: 'my-span', + * attributeProperties: { + * copyOnEnter: true + * } + * } ); */ export default class DataSchema { constructor() { @@ -35,6 +47,7 @@ export default class DataSchema { */ this._definitions = new Map(); + // Block elements. this.registerBlockElement( { model: '$htmlBlock', allowChildren: '$block', @@ -113,6 +126,7 @@ export default class DataSchema { } } ); + // Inline elements. this.registerInlineElement( { view: 'span', model: 'htmlSpan', From 15a5be3fb796521f02ec1074a0e6acc7449fde76 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 12:58:23 +0100 Subject: [PATCH 085/217] Extracted base data schema definition type. --- .../src/datafilter.js | 18 +++++---------- .../src/dataschema.js | 23 +++++++++++-------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 9c75872c2d0..bdddaca19ff 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -132,8 +132,7 @@ export default class DataFilter { * Registers element and attribute converters for the given data schema definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition - * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ _registerElement( definition ) { if ( definition.isInline ) { @@ -160,8 +159,7 @@ export default class DataFilter { * If the element model schema is already registered, this method will do nothing. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition - * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ _defineBlockElement( definition ) { if ( this.editor.model.schema.isRegistered( definition.model ) ) { @@ -181,8 +179,7 @@ export default class DataFilter { * Extends `$text` model schema to allow the given definition model attribute and its properties. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition - * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _defineInlineElement( definition ) { const schema = this.editor.model.schema; @@ -202,8 +199,7 @@ export default class DataFilter { * Registers model schema definition for the given block element definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition - * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ _defineBlockElementSchema( definition ) { const schema = this.editor.model.schema; @@ -225,8 +221,7 @@ export default class DataFilter { * Registers attribute converters for the given inline element definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition - * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ _defineInlineElementConverters( definition ) { const conversion = this.editor.conversion; @@ -280,8 +275,7 @@ export default class DataFilter { * Registers attribute converters for the given block element definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition - * |module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ _defineBlockElementConverters( definition ) { const conversion = this.editor.conversion; diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 9d609906f84..62dcd9e5414 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -42,8 +42,7 @@ export default class DataSchema { * * @readonly * @private - * @member {Map.} #_definitions + * @member {Map.} #_definitions */ this._definitions = new Map(); @@ -167,8 +166,7 @@ export default class DataSchema { * * @param {String|RegExp} viewName * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. - * @returns {Set.} + * @returns {Set.} */ getDefinitionsForView( viewName, includeReferences ) { const definitions = new Set(); @@ -191,8 +189,7 @@ export default class DataSchema { * * @private * @param {String|RegExp} viewName - * @returns {Array.} + * @returns {Array.} */ _getMatchingViewDefinitions( viewName ) { return Array.from( this._definitions.values() ) @@ -204,8 +201,7 @@ export default class DataSchema { * * @private * @param {String} modelName Data schema model name. - * @returns {Iterable.} + * @returns {Iterable.} */ * _getReferences( modelName ) { const { modelSchema } = this._definitions.get( modelName ); @@ -247,14 +243,21 @@ function testViewName( pattern, viewName ) { return false; } +/** + * A base definition of {@link module:content-compatibility/dataschema~DataSchema data schema}. + * + * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition + * @property {String} model Name of the model. + */ + /** * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for block elements. * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. - * @property {String} model Name of the model element. * @property {module:engine/model/schema~SchemaItemDefinition} modelSchema The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. + * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ /** @@ -262,6 +265,6 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaInlineElementDefinition * @property {String} view Name of the view element. - * @property {String} model Name of the model attribute key. * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. + * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ From fd542acc7167928a607b35328c403d441d5bfbae Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 13:01:44 +0100 Subject: [PATCH 086/217] Minor docs update. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index bdddaca19ff..4f133e46f94 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -490,7 +490,7 @@ function iterableToObject( iterable, getValue ) { return attributesObject; } -// Merges attribute objects. +// Merges view element attribute objects. // // @private // @param {Object} oldValue From c8b0e5cb37f806de0da13c504ac31596d808a1d7 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 13:03:47 +0100 Subject: [PATCH 087/217] Added additional test for attribute properties. --- .../tests/dataschema.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index a23d7eb2d0f..273f9e1d25d 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -35,6 +35,27 @@ describe( 'DataSchema', () => { isInline: true } ] ); } ); + + it( 'should include attribute properties', () => { + dataSchema.registerInlineElement( { + model: 'htmlDef', + view: 'def', + attributeProperties: { + copyOnEnter: true + } + } ); + + const result = dataSchema.getDefinitionsForView( 'def' ); + + expect( Array.from( result ) ).to.deep.equal( [ { + model: 'htmlDef', + view: 'def', + attributeProperties: { + copyOnEnter: true + }, + isInline: true + } ] ); + } ); } ); describe( 'registerBlockElement', () => { From 16d00e37853fae4338bd7b8ffcea558f4c873274 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 17 Mar 2021 13:11:02 +0100 Subject: [PATCH 088/217] Added information about definition specific properties. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 62dcd9e5414..c2bf8fd6c42 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -255,8 +255,10 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. - * @property {module:engine/model/schema~SchemaItemDefinition} modelSchema The model schema item definition describing registered model. + * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. + * @property {Boolean} isBlock Indicates that the definition describes block element. + * Set by {@link @link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ @@ -266,5 +268,7 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaInlineElementDefinition * @property {String} view Name of the view element. * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. + * @property {Boolean} isInline Indicates that the definition descibes inline element. + * Set by {@link @link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ From 8382a8dccb0a8c38d66851d4c7a4850868239a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20Bogda=C5=84ski?= Date: Mon, 29 Mar 2021 11:13:17 +0200 Subject: [PATCH 089/217] Update packages/ckeditor5-content-compatibility/src/datafilter.js Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- packages/ckeditor5-content-compatibility/src/datafilter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 4f133e46f94..1db78b8eeb3 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -347,9 +347,9 @@ export default class DataFilter { * @param {module:engine/view/element~Element} viewElement * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi * @returns {Object} [result] - * @returns {Set.} result.attributes Set with matched attribute names. - * @returns {Set.} result.styles Set with matched style names. - * @returns {Set.} result.classes Set with matched class names. + * @returns {Object} result.attributes Set with matched attribute names. + * @returns {Object} result.styles Set with matched style names. + * @returns {Array.} result.classes Set with matched class names. */ _matchAndConsumeAllowedAttributes( viewElement, conversionApi ) { const matches = consumeAttributeMatches( viewElement, conversionApi, this._allowedAttributes ); From f40afe00323c5bbd5e00f6842688c309cfa72498 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 29 Mar 2021 11:17:53 +0200 Subject: [PATCH 090/217] Docs update. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index c2bf8fd6c42..9df5bc1b817 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -246,7 +246,7 @@ function testViewName( pattern, viewName ) { /** * A base definition of {@link module:content-compatibility/dataschema~DataSchema data schema}. * - * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition + * @interface module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} model Name of the model. */ From 88b9fdb80b77fa374b6443d81f6a826808d3c099 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 30 Mar 2021 08:51:30 +0200 Subject: [PATCH 091/217] Updated tests to use VirtualEditor. --- .../package.json | 1 + .../tests/datafilter.js | 19 ++++++------------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index 05c00a34771..67b600df95c 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -28,6 +28,7 @@ "@ckeditor/ckeditor5-font": "^27.0.0", "@ckeditor/ckeditor5-list": "^27.0.0", "@ckeditor/ckeditor5-paragraph": "^27.0.0", + "@ckeditor/ckeditor5-utils": "^27.0.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11" }, diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index b1543879d39..f5c020507bf 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -3,11 +3,9 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* global document */ - -import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import Font from '@ckeditor/ckeditor5-font/src/font'; +import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -16,17 +14,14 @@ import DataSchema from '../src/dataschema'; import DataFilter from '../src/datafilter'; describe( 'DataFilter', () => { - let editor, model, dataFilter, dataSchema, element; + let editor, model, dataFilter, dataSchema; testUtils.createSinonSandbox(); beforeEach( () => { - element = document.createElement( 'div' ); - document.body.appendChild( element ); - - return ClassicTestEditor - .create( element, { - plugins: [ Paragraph, Font ] + return VirtualTestEditor + .create( { + plugins: [ Paragraph, FontColorEditing ] } ) .then( newEditor => { editor = newEditor; @@ -38,8 +33,6 @@ describe( 'DataFilter', () => { } ); afterEach( () => { - element.remove(); - return editor.destroy(); } ); From 0a10e2d4cea12c270397653c7f1ab812470855da Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 30 Mar 2021 09:22:06 +0200 Subject: [PATCH 092/217] Corrected docs based on build verification. --- .../src/datafilter.js | 8 ++++++-- .../src/dataschema.js | 13 ++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 1db78b8eeb3..dcf9f5d75fc 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -5,6 +5,7 @@ /** * @module content-compatibility/datafilter + * @publicApi */ import { Matcher } from 'ckeditor5/src/engine'; @@ -17,7 +18,7 @@ const DATA_SCHEMA_ATTRIBUTE_KEY = 'htmlAttributes'; /** * Allows to validate elements and element attributes registered by {@link module:content-compatibility/dataschema~DataSchema}. * - * To enable registered element in the editor, use {@link #allowElement} method: + * To enable registered element in the editor, use {@link module:content-compatibility/datafilter~DataFilter#allowElement} method: * * dataFilter.allowElement( { * name: 'section' @@ -78,7 +79,10 @@ export default class DataFilter { } /** - * Allow the given element registered by {@link #register} method. + * Allow the given element in the editor context. + * + * This method will only allow elements described by the {@link module:content-compatibility/dataschema~DataSchema} used + * to create data filter. * * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all view elements which should be allowed. */ diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 9df5bc1b817..b3103838277 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -5,6 +5,7 @@ /** * @module content-compatibility/dataschema + * @publicApi */ import { toArray } from 'ckeditor5/src/utils'; @@ -15,7 +16,8 @@ import { toArray } from 'ckeditor5/src/utils'; * * Data schema is represented by data schema definitions. * - * To add new definition for block element, use {@link #registerBlockElement} method: + * To add new definition for block element, + * use {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method: * * dataSchema.registerBlockElement( { * view: 'section', @@ -25,7 +27,8 @@ import { toArray } from 'ckeditor5/src/utils'; * } * } ); * - * To add new definition for inline element, use {@link #registerInlineElement} method: + * To add new definition for inline element, + * use {@link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method: * * dataSchema.registerInlineElement( { * view: 'span', @@ -246,7 +249,7 @@ function testViewName( pattern, viewName ) { /** * A base definition of {@link module:content-compatibility/dataschema~DataSchema data schema}. * - * @interface module:content-compatibility/dataschema~DataSchemaDefinition + * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} model Name of the model. */ @@ -258,7 +261,7 @@ function testViewName( pattern, viewName ) { * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. * @property {Boolean} isBlock Indicates that the definition describes block element. - * Set by {@link @link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. + * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ @@ -269,6 +272,6 @@ function testViewName( pattern, viewName ) { * @property {String} view Name of the view element. * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. * @property {Boolean} isInline Indicates that the definition descibes inline element. - * Set by {@link @link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method. + * Set by {@link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ From 6bdeed158f11b2c1f756d8f1ae48201cd686ae40 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 30 Mar 2021 10:11:42 +0200 Subject: [PATCH 093/217] Removed @publicApi tags. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 1 - packages/ckeditor5-content-compatibility/src/dataschema.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index dcf9f5d75fc..07ea67c04f5 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -5,7 +5,6 @@ /** * @module content-compatibility/datafilter - * @publicApi */ import { Matcher } from 'ckeditor5/src/engine'; diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index b3103838277..ebdd61537e1 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -5,7 +5,6 @@ /** * @module content-compatibility/dataschema - * @publicApi */ import { toArray } from 'ckeditor5/src/utils'; From 3f51d92020bfc396f7453d724339d745b1a9a691 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 31 Mar 2021 11:39:59 +0200 Subject: [PATCH 094/217] Simplified data filter attribute matchers. --- .../src/datafilter.js | 89 +++++-------------- 1 file changed, 21 insertions(+), 68 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 07ea67c04f5..e8385203948 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -55,26 +55,24 @@ export default class DataFilter { this._dataSchema = dataSchema; /** - * A map of registered {@link module:engine/view/matcher~Matcher Matcher} instances. - * - * Describes rules upon which content attributes should be allowed. + * {@link module:engine/view/matcher~Matcher Matcher} instance describing rules upon which + * content attributes should be allowed. * * @readonly * @private - * @member {Map.} #_allowedAttributes + * @member {module:engine/view/matcher~Matcher} #_allowedAttributes */ - this._allowedAttributes = new Map(); + this._allowedAttributes = new Matcher(); /** - * A map of registered {@link module:engine/view/matcher~Matcher Matcher} instances. - * - * Describes rules upon which content attributes should be disallowed. + * {@link module:engine/view/matcher~Matcher Matcher} instance describing rules upon which + * content attributes should be disallowed. * * @readonly * @private - * @member {Map.} #_disallowedAttributes + * @member {module:engine/view/matcher~Matcher} #_disallowedAttributes */ - this._disallowedAttributes = new Map(); + this._disallowedAttributes = new Matcher(); } /** @@ -99,7 +97,7 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be allowed. */ allowAttributes( config ) { - this._addAttributeMatcher( config, this._allowedAttributes ); + this._allowedAttributes.add( config ); } /** @@ -108,27 +106,7 @@ export default class DataFilter { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be disallowed. */ disallowAttributes( config ) { - this._addAttributeMatcher( config, this._disallowedAttributes ); - } - - /** - * Adds attribute matcher for every registered data schema definition for the given `config.name`. - * - * @private - * @param {module:engine/view/matcher~MatcherPattern} config - * @param {Map.} rules Rules map holding matchers. - */ - _addAttributeMatcher( config, rules ) { - const viewName = config.name; - - config = cloneDeep( config ); - - // We don't want match by name when matching attributes. Matcher will be already attached to specific definition. - delete config.name; - - for ( const definition of this._dataSchema.getDefinitionsForView( viewName ) ) { - getOrCreateMatcher( definition.view, rules ).add( config ); - } + this._disallowedAttributes.add( config ); } /** @@ -379,25 +357,29 @@ export default class DataFilter { } } -// Consumes attributes matched for the given `rules`. +// Consumes matched attributes. // // Returns sucessfully consumed attribute matches. // // @private // @param {module:engine/view/element~Element} viewElement // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi -// @param {Map.} rules +// @param {module:engine/view/matcher~Matcher Matcher} matcher // @returns {Array.} Array with match information about found attributes. -function consumeAttributeMatches( viewElement, { consumable }, rules ) { - const matches = []; +function consumeAttributeMatches( viewElement, { consumable }, matcher ) { + const matches = matcher.matchAll( viewElement ) || []; + const consumedMatches = []; + + for ( const match of matches ) { + // We only want to consume attributes, so element can be still processed by other converters. + delete match.match.name; - for ( const match of matchAll( viewElement, rules ) ) { if ( consumable.consume( viewElement, match.match ) ) { - matches.push( match ); + consumedMatches.push( match ); } } - return matches; + return consumedMatches; } // Helper function for downcast converter. Sets attributes on the given view element. @@ -422,35 +404,6 @@ function setViewElementAttributes( writer, viewAttributes, viewElement ) { } } -// Helper function restoring matcher for the given key from `rules` object. -// -// If matcher for the given key does not exist, this function will create a new one -// inside `rules` object under the given key. -// -// @private -// @param {String} key -// @param {Map.} rules -// @returns {module:engine/view/matcher~Matcher} -function getOrCreateMatcher( key, rules ) { - if ( !rules.has( key ) ) { - rules.set( key, new Matcher() ); - } - - return rules.get( key ); -} - -// Alias for {@link module:engine/view/matcher~Matcher#matchAll matchAll}. -// -// @private -// @param {module:engine/view/element~Element} viewElement -// @param {Map.} rules Rules map holding matchers. -// @returns {Array.} Array with match information about found elements. -function matchAll( viewElement, rules ) { - const matcher = getOrCreateMatcher( viewElement.name, rules ); - - return matcher.matchAll( viewElement ) || []; -} - // Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. // // @private From ebe79c3f3705d4cbe894f2155014254f86a79a72 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 6 Apr 2021 09:40:45 +0200 Subject: [PATCH 095/217] Added support to allow attributes on existing features. --- .../src/datafilter.js | 155 ++++++++++-------- .../src/dataschema.js | 29 +++- .../tests/datafilter.js | 134 +++++++++++++++ .../tests/dataschema.js | 30 +++- .../tests/manual/generichtmlsupport.html | 21 ++- .../tests/manual/generichtmlsupport.js | 11 ++ 6 files changed, 305 insertions(+), 75 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e8385203948..8cba235cd0a 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -143,15 +143,13 @@ export default class DataFilter { * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ _defineBlockElement( definition ) { - if ( this.editor.model.schema.isRegistered( definition.model ) ) { - return; + if ( !definition.extend ) { + this._defineBlockElementSchema( definition ); + this._addBlockElementToElementConversion( definition ); } - this._defineBlockElementSchema( definition ); - - if ( definition.view ) { - this._defineBlockElementConverters( definition ); - } + this._addDisallowedAttributeConversion( definition ); + this._addBlockElementAttributeConversion( definition ); } /** @@ -173,7 +171,8 @@ export default class DataFilter { schema.setAttributeProperties( definition.model, definition.attributeProperties ); } - this._defineInlineElementConverters( definition ); + this._addDisallowedAttributeConversion( definition ); + this._addInlineElementConversion( definition ); } /** @@ -185,6 +184,10 @@ export default class DataFilter { _defineBlockElementSchema( definition ) { const schema = this.editor.model.schema; + if ( schema.isRegistered( definition.model ) ) { + return; + } + schema.register( definition.model, definition.modelSchema ); const allowedChildren = toArray( definition.allowChildren || [] ); @@ -199,18 +202,86 @@ export default class DataFilter { } /** - * Registers attribute converters for the given inline element definition. + * Registers element to element converters for the given block element definition. + * + * @private + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + */ + _addBlockElementToElementConversion( { model: modelName, view: viewName } ) { + const conversion = this.editor.conversion; + + if ( !viewName ) { + return; + } + + conversion.for( 'upcast' ).elementToElement( { + model: modelName, + view: viewName, + // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure + // this listener is called before it. If not, some elements will be transformed into a paragraph. + converterPriority: priorities.get( 'low' ) + 1 + } ); + + conversion.for( 'downcast' ).elementToElement( { + model: modelName, + view: viewName + } ); + } + + /** + * Registers attribute converters for the given block element definition. + * + * @private + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + */ + _addBlockElementAttributeConversion( { model: modelName, view: viewName } ) { + const conversion = this.editor.conversion; + + if ( !viewName ) { + return; + } + + conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { + if ( !data.modelRange ) { + return; + } + + const viewAttributes = this._matchAndConsumeAllowedAttributes( data.viewItem, conversionApi ); + + if ( viewAttributes ) { + conversionApi.writer.setAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes, data.modelRange ); + } + }, { priority: 'low' } ); + } ); + + conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }:${ modelName }`, ( evt, data, conversionApi ) => { + const viewAttributes = data.attributeNewValue; + + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewElement = conversionApi.mapper.toViewElement( data.item ); + + setViewElementAttributes( viewWriter, viewAttributes, viewElement ); + } ); + } ); + } + + /** + * Registers converters for the given inline element definition. * * @private * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ - _defineInlineElementConverters( definition ) { + _addInlineElementConversion( definition ) { const conversion = this.editor.conversion; const viewName = definition.view; const attributeKey = definition.model; - this._addDisallowedAttributesConverter( viewName ); - conversion.for( 'upcast' ).add( dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { const viewAttributes = this._matchAndConsumeAllowedAttributes( data.viewItem, conversionApi ); @@ -253,65 +324,17 @@ export default class DataFilter { } /** - * Registers attribute converters for the given block element definition. + * Registers converters responsible for consuming disallowed view attributes. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ - _defineBlockElementConverters( definition ) { + _addDisallowedAttributeConversion( { view: viewName } ) { const conversion = this.editor.conversion; - const viewName = definition.view; - const modelName = definition.model; - - this._addDisallowedAttributesConverter( viewName ); - - // Stash unused, allowed element attributes, so they can be reapplied later in data conversion. - conversion.for( 'upcast' ).elementToElement( { - view: viewName, - model: ( viewElement, conversionApi ) => { - const element = conversionApi.writer.createElement( modelName ); - const viewAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); - - if ( viewAttributes ) { - conversionApi.writer.setAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes, element ); - } - - return element; - }, - // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure - // this listener is called before it. If not, some elements will be transformed into a paragraph. - converterPriority: priorities.get( 'low' ) + 1 - } ); - conversion.for( 'downcast' ).elementToElement( { - model: modelName, - view: viewName - } ); - - conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }:${ modelName }`, ( evt, data, conversionApi ) => { - const viewAttributes = data.attributeNewValue; - - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { - return; - } - - const viewWriter = conversionApi.writer; - const viewElement = conversionApi.mapper.toViewElement( data.item ); - - setViewElementAttributes( viewWriter, viewAttributes, viewElement ); - } ); - } ); - } - - /** - * Adds converter responsible for consuming disallowed view attributes. - * - * @private - * @param {String} viewName - */ - _addDisallowedAttributesConverter( viewName ) { - const conversion = this.editor.conversion; + if ( !viewName ) { + return; + } // Consumes disallowed element attributes to prevent them of being processed by other converters. conversion.for( 'upcast' ).add( dispatcher => { diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index ebdd61537e1..3d119f33578 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -49,6 +49,21 @@ export default class DataSchema { this._definitions = new Map(); // Block elements. + this.extendBlockElement( { + model: 'paragraph', + view: 'p' + } ); + + this.extendBlockElement( { + model: 'blockQuote', + view: 'blockquote' + } ); + + this.extendBlockElement( { + model: 'listItem', + view: 'li' + } ); + this.registerBlockElement( { model: '$htmlBlock', allowChildren: '$block', @@ -146,7 +161,7 @@ export default class DataSchema { } /** - * Add new data schema definition for block element. + * Add new data schema definition describing block element. * * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ @@ -155,7 +170,7 @@ export default class DataSchema { } /** - * Add new data schema definition for inline element. + * Add new data schema definition describing inline element. * * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ @@ -163,6 +178,15 @@ export default class DataSchema { this._definitions.set( definition.model, { ...definition, isInline: true } ); } + /** + * Add new data schema definition to extend existing editor's model block element. + * + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + */ + extendBlockElement( definition ) { + this.registerBlockElement( { ...definition, extend: true } ); + } + /** * Returns all definitions matching the given view name. * @@ -250,6 +274,7 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} model Name of the model. + * @property {Boolean} [extend=false] Indicates if data schema should extend existing model definition. */ /** diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index f5c020507bf..8cb456a4bc7 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -380,6 +380,24 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

foo

' ); } ); + + it( 'should only set attributes on existing model range', () => { + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': 'foo' } } ); + + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:p', ( evt, data ) => { + data.modelRange = null; + } ); + } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + attributes: {} + } ); + } ); } ); describe( 'inline', () => { @@ -763,6 +781,122 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

foobar

' ); } ); + describe( 'existing features', () => { + it( 'should allow additional attributes', () => { + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': 'foo' } } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + attributes: { + 1: { + attributes: { 'data-foo': 'foo' } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should allow additional attributes (classes)', () => { + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', classes: /[\s\S]+/ } ); + + editor.setData( '

foo

bar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + classes: [ 'foo' ] + }, + 2: { + classes: [ 'bar' ] + } + } + } ); + + expect( editor.getData() ).to.equal( '

foo

bar

' ); + } ); + + it( 'should allow additional attributes (styles)', () => { + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', styles: { 'color': /[\s\S]+/ } } ); + + editor.setData( '

foo

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + attributes: { + 1: { + styles: { color: 'red' } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foo

' ); + } ); + + it( 'should disallow attributes', () => { + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: 'p', attributes: { 'data-foo': 'bar' } } ); + + editor.setData( '

foo

bar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foo

bar

' ); + } ); + + it( 'should disallow attributes (styles)', () => { + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', styles: { color: /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: 'p', styles: { color: 'red' } } ); + + editor.setData( '

foo

bar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + color: 'blue' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foo

bar

' ); + } ); + + it( 'should disallow attributes (classes)', () => { + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', classes: [ 'foo', 'bar' ] } ); + dataFilter.disallowAttributes( { name: 'p', classes: [ 'bar' ] } ); + + editor.setData( '

foo

bar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: {} + } ); + + expect( editor.getData() ).to.equal( '

foo

bar

' ); + } ); + } ); + it( 'should preserve attributes not used by other features', () => { dataFilter.allowElement( { name: 'span' } ); dataFilter.allowAttributes( { name: 'span', styles: { 'color': /[\s\S]+/ } } ); diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 273f9e1d25d..45cdce0f7a0 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -4,11 +4,14 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import DataSchema from '../src/dataschema'; describe( 'DataSchema', () => { let editor, dataSchema; + testUtils.createSinonSandbox(); + beforeEach( () => { return VirtualTestEditor .create() @@ -23,7 +26,7 @@ describe( 'DataSchema', () => { return editor.destroy(); } ); - describe( 'registerInlineElement', () => { + describe( 'registerInlineElement()', () => { it( 'should register proper definition', () => { dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def' } ); @@ -58,7 +61,7 @@ describe( 'DataSchema', () => { } ); } ); - describe( 'registerBlockElement', () => { + describe( 'registerBlockElement()', () => { const fakeDefinitions = [ { view: 'def1', @@ -190,4 +193,27 @@ describe( 'DataSchema', () => { return getFakeDefinitions( ...viewNames ).map( def => ( { ...def, isBlock: true } ) ); } } ); + + describe( 'extendBlockElement()', () => { + it( 'should register proper definition', () => { + dataSchema.extendBlockElement( { model: 'paragraph', view: 'p' } ); + + const result = dataSchema.getDefinitionsForView( 'p' ); + + expect( Array.from( result ) ).to.deep.equal( [ { + model: 'paragraph', + view: 'p', + isBlock: true, + extend: true + } ] ); + } ); + + it( 'should use registerBlockElement()', () => { + const spy = sinon.spy( dataSchema, 'registerBlockElement' ); + + dataSchema.extendBlockElement( { model: 'paragraph', view: 'p' } ); + + expect( spy.calledOnce ).to.be.true; + } ); + } ); } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html index e64de28457f..544c1e8728d 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html @@ -4,8 +4,8 @@
-

Section #1

-

Section #2

+

Section #1

+

Section #2

Section #3

@@ -23,16 +23,27 @@

XYZ

-

+

Nested cite: I'm blue!

-

+

Nested span: I'm blue!

Deeply nested cite with span!

-

Span with no attributes!

+

Span with no attributes!

Span with nested styles!

+ +
Red quote!
+ +
    +
  • Blue!
  • +
  • Red!
  • +
  • Violet!
  • +
      +
    • Yellow!
    • +
    +
diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js index dd3604a16a6..37fb9e6ad95 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js @@ -65,6 +65,17 @@ class ExtendHTMLSupport extends Plugin { dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.allowAttributes( { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: /^(span|cite)$/, styles: { color: 'red' } } ); + + // Allow existing features. + dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: 'p', styles: { 'background-color': /[\s\S]+/ } } ); + + dataFilter.allowElement( { name: 'blockquote' } ); + dataFilter.allowAttributes( { name: 'blockquote', styles: { 'color': /[\s\S]+/ } } ); + + dataFilter.allowElement( { name: 'li' } ); + dataFilter.allowAttributes( { name: 'li', styles: { 'color': /[\s\S]+/ } } ); } } From 7f81ef038e7b211ac48ebf4bf976579dbba87bd8 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 6 Apr 2021 10:00:50 +0200 Subject: [PATCH 096/217] Rewording. --- packages/ckeditor5-content-compatibility/src/dataschema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 3d119f33578..078970fad30 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -274,7 +274,7 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} model Name of the model. - * @property {Boolean} [extend=false] Indicates if data schema should extend existing model definition. + * @property {Boolean} [extend=false] Indicates if data schema should extend existing editor element. */ /** From 7dd2494ceda484587d5c394d7e513c1dedb2751e Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 8 Apr 2021 08:46:37 +0200 Subject: [PATCH 097/217] Renaming. --- .../src/datafilter.js | 2 +- .../src/dataschema.js | 14 +++++++------- .../tests/dataschema.js | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 8cba235cd0a..a6c7472f31b 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -143,7 +143,7 @@ export default class DataFilter { * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ _defineBlockElement( definition ) { - if ( !definition.extend ) { + if ( !definition.isFeature ) { this._defineBlockElementSchema( definition ); this._addBlockElementToElementConversion( definition ); } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 078970fad30..bfadf2f56ce 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -49,17 +49,17 @@ export default class DataSchema { this._definitions = new Map(); // Block elements. - this.extendBlockElement( { + this.registerBlockElementFeature( { model: 'paragraph', view: 'p' } ); - this.extendBlockElement( { + this.registerBlockElementFeature( { model: 'blockQuote', view: 'blockquote' } ); - this.extendBlockElement( { + this.registerBlockElementFeature( { model: 'listItem', view: 'li' } ); @@ -179,12 +179,12 @@ export default class DataSchema { } /** - * Add new data schema definition to extend existing editor's model block element. + * Add new data schema definition to extend existing editor's model block element feature. * * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ - extendBlockElement( definition ) { - this.registerBlockElement( { ...definition, extend: true } ); + registerBlockElementFeature( definition ) { + this.registerBlockElement( { ...definition, isFeature: true } ); } /** @@ -274,7 +274,7 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} model Name of the model. - * @property {Boolean} [extend=false] Indicates if data schema should extend existing editor element. + * @property {Boolean} [isFeature=false] Indicates if data schema should extend existing editor feature. */ /** diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 45cdce0f7a0..eed56377e6f 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -194,9 +194,9 @@ describe( 'DataSchema', () => { } } ); - describe( 'extendBlockElement()', () => { + describe( 'registerBlockElementFeature()', () => { it( 'should register proper definition', () => { - dataSchema.extendBlockElement( { model: 'paragraph', view: 'p' } ); + dataSchema.registerBlockElementFeature( { model: 'paragraph', view: 'p' } ); const result = dataSchema.getDefinitionsForView( 'p' ); @@ -204,14 +204,14 @@ describe( 'DataSchema', () => { model: 'paragraph', view: 'p', isBlock: true, - extend: true + isFeature: true } ] ); } ); it( 'should use registerBlockElement()', () => { const spy = sinon.spy( dataSchema, 'registerBlockElement' ); - dataSchema.extendBlockElement( { model: 'paragraph', view: 'p' } ); + dataSchema.registerBlockElementFeature( { model: 'paragraph', view: 'p' } ); expect( spy.calledOnce ).to.be.true; } ); From 8b326e99bf1708590af74c33ec827e551d0a4c56 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 8 Apr 2021 08:57:48 +0200 Subject: [PATCH 098/217] Ensure that model is not registered early. --- .../src/datafilter.js | 8 +++----- .../tests/datafilter.js | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index a6c7472f31b..cc7887e872e 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -143,7 +143,9 @@ export default class DataFilter { * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ _defineBlockElement( definition ) { - if ( !definition.isFeature ) { + const schema = this.editor.model.schema; + + if ( !definition.isFeature && !schema.isRegistered( definition.model ) ) { this._defineBlockElementSchema( definition ); this._addBlockElementToElementConversion( definition ); } @@ -184,10 +186,6 @@ export default class DataFilter { _defineBlockElementSchema( definition ) { const schema = this.editor.model.schema; - if ( schema.isRegistered( definition.model ) ) { - return; - } - schema.register( definition.model, definition.modelSchema ); const allowedChildren = toArray( definition.allowChildren || [] ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 8cb456a4bc7..26fbd9cc4af 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -398,6 +398,22 @@ describe( 'DataFilter', () => { attributes: {} } ); } ); + + it( 'should not register converters if element definition was already registered', () => { + dataSchema.registerBlockElement( { + model: 'htmlXyz', + view: 'xyz', + modelSchema: { inheritAllFrom: '$block' } + } ); + + editor.model.schema.register( 'htmlXyz', { inheritAllFrom: '$block' } ); + + dataFilter.allowElement( { name: 'xyz' } ); + + editor.setData( 'foo' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( 'foo' ); + } ); } ); describe( 'inline', () => { From 5028c38929e915f9493a71ab1c3fd3e794159d80 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 8 Apr 2021 09:22:37 +0200 Subject: [PATCH 099/217] Return early for attribute filters also. --- .../ckeditor5-content-compatibility/src/datafilter.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index cc7887e872e..ff63c02a91d 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -145,7 +145,13 @@ export default class DataFilter { _defineBlockElement( definition ) { const schema = this.editor.model.schema; - if ( !definition.isFeature && !schema.isRegistered( definition.model ) ) { + if ( !definition.isFeature ) { + // Early return, so if definition has been already registered, + // attribute filters also will be skipped. + if ( schema.isRegistered( definition.model ) ) { + return; + } + this._defineBlockElementSchema( definition ); this._addBlockElementToElementConversion( definition ); } From d5219fbd41c84bf901d53d068f20dfdd22f806e7 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 8 Apr 2021 09:28:49 +0200 Subject: [PATCH 100/217] Reworded test to use real case scenario. --- .../tests/datafilter.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 26fbd9cc4af..a9e1a4469fa 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -6,6 +6,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; +import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -21,7 +22,7 @@ describe( 'DataFilter', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ Paragraph, FontColorEditing ] + plugins: [ Paragraph, FontColorEditing, ListEditing ] } ) .then( newEditor => { editor = newEditor; @@ -382,16 +383,12 @@ describe( 'DataFilter', () => { } ); it( 'should only set attributes on existing model range', () => { - dataFilter.allowElement( { name: 'p' } ); - dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': 'foo' } } ); + dataSchema.registerBlockElementFeature( { view: 'xyz', model: 'modelXyz' } ); - editor.conversion.for( 'upcast' ).add( dispatcher => { - dispatcher.on( 'element:p', ( evt, data ) => { - data.modelRange = null; - } ); - } ); + dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowAttributes( { name: 'xyz', attributes: { 'data-foo': 'foo' } } ); - editor.setData( '

foo

' ); + editor.setData( 'foo' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: 'foo', From 26869819178a0967be8dba0e526e1e7e3bf314bc Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 8 Apr 2021 09:31:39 +0200 Subject: [PATCH 101/217] Removed dead code. --- packages/ckeditor5-content-compatibility/tests/datafilter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index a9e1a4469fa..3ee12db8b3d 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -6,7 +6,6 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; -import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -22,7 +21,7 @@ describe( 'DataFilter', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ Paragraph, FontColorEditing, ListEditing ] + plugins: [ Paragraph, FontColorEditing ] } ) .then( newEditor => { editor = newEditor; From c1b734d0d1350a3f74b7a3368277031f5d1a1c7b Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 8 Apr 2021 10:51:04 +0200 Subject: [PATCH 102/217] Minor refactoring, commenting. --- .../src/datafilter.js | 22 ++++++++++--------- .../tests/datafilter.js | 6 ++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index ff63c02a91d..18b8a6355a7 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -117,9 +117,9 @@ export default class DataFilter { */ _registerElement( definition ) { if ( definition.isInline ) { - this._defineInlineElement( definition ); + this._registerInlineElement( definition ); } else if ( definition.isBlock ) { - this._defineBlockElement( definition ); + this._registerBlockElement( definition ); } else { /** * Only a definition marked as inline or block can be allowed. @@ -142,7 +142,7 @@ export default class DataFilter { * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ - _defineBlockElement( definition ) { + _registerBlockElement( definition ) { const schema = this.editor.model.schema; if ( !definition.isFeature ) { @@ -152,10 +152,12 @@ export default class DataFilter { return; } - this._defineBlockElementSchema( definition ); + this._registerBlockElementSchema( definition ); this._addBlockElementToElementConversion( definition ); } + // TODO So far we are not able to detect if feature converters has been already added, + // so this code may result in duplicated converters and additional conversion overheat. this._addDisallowedAttributeConversion( definition ); this._addBlockElementAttributeConversion( definition ); } @@ -168,7 +170,7 @@ export default class DataFilter { * @private * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ - _defineInlineElement( definition ) { + _registerInlineElement( definition ) { const schema = this.editor.model.schema; schema.extend( '$text', { @@ -189,7 +191,7 @@ export default class DataFilter { * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ - _defineBlockElementSchema( definition ) { + _registerBlockElementSchema( definition ) { const schema = this.editor.model.schema; schema.register( definition.model, definition.modelSchema ); @@ -206,7 +208,7 @@ export default class DataFilter { } /** - * Registers element to element converters for the given block element definition. + * Adds element to element converters for the given block element definition. * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition @@ -233,7 +235,7 @@ export default class DataFilter { } /** - * Registers attribute converters for the given block element definition. + * Adds attribute converters for the given block element definition. * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition @@ -276,7 +278,7 @@ export default class DataFilter { } /** - * Registers converters for the given inline element definition. + * Adds converters for the given inline element definition. * * @private * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition @@ -328,7 +330,7 @@ export default class DataFilter { } /** - * Registers converters responsible for consuming disallowed view attributes. + * Adds converters responsible for consuming disallowed view attributes. * * @private * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 3ee12db8b3d..93c3c013449 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -381,12 +381,16 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

foo

' ); } ); - it( 'should only set attributes on existing model range', () => { + it( 'should not convert attributes if the model schema item definition is not registered', () => { dataSchema.registerBlockElementFeature( { view: 'xyz', model: 'modelXyz' } ); dataFilter.allowElement( { name: 'xyz' } ); dataFilter.allowAttributes( { name: 'xyz', attributes: { 'data-foo': 'foo' } } ); + // We are not registering model schema anywhere, to check if upcast + // converter will be able to detect this case. + // editor.model.schema.register( 'modelXyz', { ... } ); + editor.setData( 'foo' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { From b4d1af9b404c3728c35125d613c51141fcaf622f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 12 Apr 2021 17:26:51 +0200 Subject: [PATCH 103/217] Added inline elements priority support, simplified code. --- .../package.json | 1 + .../src/datafilter.js | 2 +- .../src/dataschema.js | 55 +++++++++++++------ .../tests/datafilter.js | 28 +++++++++- .../tests/dataschema.js | 39 ++++++------- .../tests/manual/generichtmlsupport.html | 12 ++++ .../tests/manual/generichtmlsupport.js | 18 ++++++ 7 files changed, 112 insertions(+), 43 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index 67b600df95c..97cc03ad01a 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -26,6 +26,7 @@ "@ckeditor/ckeditor5-engine": "^27.0.0", "@ckeditor/ckeditor5-essentials": "^27.0.0", "@ckeditor/ckeditor5-font": "^27.0.0", + "@ckeditor/ckeditor5-link": "^27.0.0", "@ckeditor/ckeditor5-list": "^27.0.0", "@ckeditor/ckeditor5-paragraph": "^27.0.0", "@ckeditor/ckeditor5-utils": "^27.0.0", diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 18b8a6355a7..9d94489582a 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -320,7 +320,7 @@ export default class DataFilter { } const { writer } = conversionApi; - const viewElement = writer.createAttributeElement( viewName ); + const viewElement = writer.createAttributeElement( viewName, null, { priority: definition.priority } ); setViewElementAttributes( writer, attributeValue, viewElement ); diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index bfadf2f56ce..b80378b5921 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -49,19 +49,22 @@ export default class DataSchema { this._definitions = new Map(); // Block elements. - this.registerBlockElementFeature( { + this.registerBlockElement( { model: 'paragraph', - view: 'p' + view: 'p', + isFeature: true } ); - this.registerBlockElementFeature( { + this.registerBlockElement( { model: 'blockQuote', - view: 'blockquote' + view: 'blockquote', + isFeature: true } ); - this.registerBlockElementFeature( { + this.registerBlockElement( { model: 'listItem', - view: 'li' + view: 'li', + isFeature: true } ); this.registerBlockElement( { @@ -143,6 +146,27 @@ export default class DataSchema { } ); // Inline elements. + this.registerInlineElement( { + view: 'a', + model: 'htmlA', + priority: 5 + } ); + + this.registerInlineElement( { + view: 'strong', + model: 'htmlStrong' + } ); + + this.registerInlineElement( { + view: 'i', + model: 'htmlI' + } ); + + this.registerInlineElement( { + view: 's', + model: 'htmlS' + } ); + this.registerInlineElement( { view: 'span', model: 'htmlSpan', @@ -175,16 +199,11 @@ export default class DataSchema { * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ registerInlineElement( definition ) { - this._definitions.set( definition.model, { ...definition, isInline: true } ); - } - - /** - * Add new data schema definition to extend existing editor's model block element feature. - * - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition - */ - registerBlockElementFeature( definition ) { - this.registerBlockElement( { ...definition, isFeature: true } ); + this._definitions.set( definition.model, { + priority: 10, + ...definition, + isInline: true + } ); } /** @@ -274,7 +293,6 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} model Name of the model. - * @property {Boolean} [isFeature=false] Indicates if data schema should extend existing editor feature. */ /** @@ -285,6 +303,7 @@ function testViewName( pattern, viewName ) { * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. * @property {Boolean} isBlock Indicates that the definition describes block element. + * @property {Boolean} [isFeature=false] Indicates if data schema should extend existing editor feature. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ @@ -296,6 +315,8 @@ function testViewName( pattern, viewName ) { * @property {String} view Name of the view element. * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. * @property {Boolean} isInline Indicates that the definition descibes inline element. + * @property {Number} [priority=10] Element priority. Decides in what order elements are wrapped by + * {@link module:engine/view/downcastwriter~DowncastWriter}. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 93c3c013449..4037433cfe2 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -5,6 +5,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; @@ -21,7 +22,7 @@ describe( 'DataFilter', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ Paragraph, FontColorEditing ] + plugins: [ Paragraph, FontColorEditing, LinkEditing ] } ) .then( newEditor => { editor = newEditor; @@ -382,7 +383,7 @@ describe( 'DataFilter', () => { } ); it( 'should not convert attributes if the model schema item definition is not registered', () => { - dataSchema.registerBlockElementFeature( { view: 'xyz', model: 'modelXyz' } ); + dataSchema.registerBlockElement( { view: 'xyz', model: 'modelXyz', isFeature: true } ); dataFilter.allowElement( { name: 'xyz' } ); dataFilter.allowAttributes( { name: 'xyz', attributes: { 'data-foo': 'foo' } } ); @@ -760,6 +761,29 @@ describe( 'DataFilter', () => { editor.getData( '

foobar

' ); } ); + + it( 'should use correct priority level for existing features', () => { + // 'a' element is registered by data schema with priority 5. + // We are checking if this element will be correctly nested due to different + // AttributeElement priority than default. + dataFilter.allowElement( { name: 'a' } ); + dataFilter.allowAttributes( { name: 'a', attributes: { 'data-foo': 'foo' } } ); + + editor.setData( '

link

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlA="(1)" linkHref="example.com">link', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

link

' ); + } ); } ); it( 'should correctly resolve attributes nesting order', () => { diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index eed56377e6f..8c589a00cc0 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -35,6 +35,7 @@ describe( 'DataSchema', () => { expect( Array.from( result ) ).to.deep.equal( [ { model: 'htmlDef', view: 'def', + priority: 10, isInline: true } ] ); } ); @@ -53,12 +54,26 @@ describe( 'DataSchema', () => { expect( Array.from( result ) ).to.deep.equal( [ { model: 'htmlDef', view: 'def', + priority: 10, attributeProperties: { copyOnEnter: true }, isInline: true } ] ); } ); + + it( 'should preserve custom priority', () => { + dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def', priority: 7 } ); + + const result = dataSchema.getDefinitionsForView( 'def' ); + + expect( Array.from( result ) ).to.deep.equal( [ { + model: 'htmlDef', + view: 'def', + priority: 7, + isInline: true + } ] ); + } ); } ); describe( 'registerBlockElement()', () => { @@ -81,6 +96,7 @@ describe( 'DataSchema', () => { { view: 'def3', model: 'htmlDef3', + isFeature: true, modelSchema: { inheritTypesFrom: 'htmlDef2' } @@ -193,27 +209,4 @@ describe( 'DataSchema', () => { return getFakeDefinitions( ...viewNames ).map( def => ( { ...def, isBlock: true } ) ); } } ); - - describe( 'registerBlockElementFeature()', () => { - it( 'should register proper definition', () => { - dataSchema.registerBlockElementFeature( { model: 'paragraph', view: 'p' } ); - - const result = dataSchema.getDefinitionsForView( 'p' ); - - expect( Array.from( result ) ).to.deep.equal( [ { - model: 'paragraph', - view: 'p', - isBlock: true, - isFeature: true - } ] ); - } ); - - it( 'should use registerBlockElement()', () => { - const spy = sinon.spy( dataSchema, 'registerBlockElement' ); - - dataSchema.registerBlockElementFeature( { model: 'paragraph', view: 'p' } ); - - expect( spy.calledOnce ).to.be.true; - } ); - } ); } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html index 544c1e8728d..491f00db3be 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html @@ -46,4 +46,16 @@
  • Yellow!
  • +

    + Link with different background color. +

    +

    + Strong with font weight 400 +

    +

    + Italic with custom attributes +

    +

    + Red strike +

    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js index 37fb9e6ad95..5821e3c578a 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js @@ -14,6 +14,7 @@ import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; import List from '@ckeditor/ckeditor5-list/src/list'; +import Link from '@ckeditor/ckeditor5-link/src/link'; import DataSchema from '../../src/dataschema'; import DataFilter from '../../src/datafilter'; @@ -66,6 +67,9 @@ class ExtendHTMLSupport extends Plugin { dataFilter.allowAttributes( { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: /^(span|cite)$/, styles: { color: 'red' } } ); + dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-order-id': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-item-id': /[\s\S]+/ } } ); + // Allow existing features. dataFilter.allowElement( { name: 'p' } ); dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } } ); @@ -76,12 +80,26 @@ class ExtendHTMLSupport extends Plugin { dataFilter.allowElement( { name: 'li' } ); dataFilter.allowAttributes( { name: 'li', styles: { 'color': /[\s\S]+/ } } ); + + dataFilter.allowElement( { name: 'a' } ); + dataFilter.allowAttributes( { name: 'a', styles: { 'background-color': /[\s\S]+/ } } ); + + dataFilter.allowElement( { name: 'strong' } ); + dataFilter.allowAttributes( { name: 'strong', styles: { 'font-weight': /[\s\S]+/ } } ); + + dataFilter.allowElement( { name: 'i' } ); + dataFilter.allowAttributes( { name: 'i', styles: { 'color': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: 'i', attributes: { 'data-foo': /[\s\S]+/ } } ); + + dataFilter.allowElement( { name: 's' } ); + dataFilter.allowAttributes( { name: 's', styles: { 'color': /[\s\S]+/ } } ); } } ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ + Link, BlockQuote, Bold, Essentials, From 37bf728581f7e838bc559174f0f16a743d2fc9b1 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 14 Apr 2021 10:14:18 +0200 Subject: [PATCH 104/217] Used model schema instead of isFeature, ensured that GHS is initialized at the correct moment. --- .../src/datafilter.js | 59 +++++++++++++++---- .../src/dataschema.js | 10 +--- .../src/generalhtmlsupport.js | 49 +++++++++++++++ .../tests/datafilter.js | 13 ++-- .../tests/dataschema.js | 1 - .../tests/manual/generichtmlsupport.js | 18 ++---- 6 files changed, 114 insertions(+), 36 deletions(-) create mode 100644 packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 9d94489582a..7c8c3421f81 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -73,6 +73,45 @@ export default class DataFilter { * @member {module:engine/view/matcher~Matcher} #_disallowedAttributes */ this._disallowedAttributes = new Matcher(); + + /** + * Allowed element definitions by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method. + * + * @readonly + * @private + * @param {Set.} #_allowedElements + */ + this._allowedElements = new Set(); + + /** + * Indicates if {@link module:core/editor~Editor#data editor's data controller} data has been already initialized. + * + * @private + * @param {Boolean} #_dataInitialized + */ + this._dataInitialized = false; + + this._registerElementsAfterInit(); + } + + /** + * Registers elements allowed by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method + * once {@link module:core/editor~Editor#data editor's data controller} is initialized. + * + * @private + */ + _registerElementsAfterInit() { + this.editor.data.on( 'init', () => { + this._dataInitialized = true; + + for ( const definition of this._allowedElements ) { + this._registerElement( definition ); + } + }, { + // With high priority listener we are able to register elements right before + // running data conversion. + priority: 'high' + } ); } /** @@ -85,7 +124,15 @@ export default class DataFilter { */ allowElement( config ) { for ( const definition of this._dataSchema.getDefinitionsForView( config.name, true ) ) { - this._registerElement( definition ); + if ( this._allowedElements.has( definition ) ) { + continue; + } + + this._allowedElements.add( definition ); + + if ( this._dataInitialized ) { + this._registerElement( definition ); + } } this.allowAttributes( config ); @@ -145,19 +192,11 @@ export default class DataFilter { _registerBlockElement( definition ) { const schema = this.editor.model.schema; - if ( !definition.isFeature ) { - // Early return, so if definition has been already registered, - // attribute filters also will be skipped. - if ( schema.isRegistered( definition.model ) ) { - return; - } - + if ( !schema.isRegistered( definition.model ) ) { this._registerBlockElementSchema( definition ); this._addBlockElementToElementConversion( definition ); } - // TODO So far we are not able to detect if feature converters has been already added, - // so this code may result in duplicated converters and additional conversion overheat. this._addDisallowedAttributeConversion( definition ); this._addBlockElementAttributeConversion( definition ); } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index b80378b5921..cbcd6ea6bf0 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -51,20 +51,17 @@ export default class DataSchema { // Block elements. this.registerBlockElement( { model: 'paragraph', - view: 'p', - isFeature: true + view: 'p' } ); this.registerBlockElement( { model: 'blockQuote', - view: 'blockquote', - isFeature: true + view: 'blockquote' } ); this.registerBlockElement( { model: 'listItem', - view: 'li', - isFeature: true + view: 'li' } ); this.registerBlockElement( { @@ -303,7 +300,6 @@ function testViewName( pattern, viewName ) { * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. * @property {Boolean} isBlock Indicates that the definition describes block element. - * @property {Boolean} [isFeature=false] Indicates if data schema should extend existing editor feature. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js new file mode 100644 index 00000000000..bae367309b0 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -0,0 +1,49 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module content-compatibility/generalhtmlsupport + */ + +import { Plugin } from 'ckeditor5/src/core'; + +import DataSchema from './dataschema'; +import DataFilter from './datafilter'; + +/** + * The General HTML Support feature. + * + * This is a "glue" plugin which initializes the {@link module:content-compatibility/dataschema~DataSchema data schema} + * and {@link module:content-compatibility/datafilter~DataFilter data filter} features. + * + * @extends module:core/plugin~Plugin + */ +export default class GeneralHtmlSupport extends Plugin { + constructor( editor ) { + super( editor ); + + /** + * An instance of the {@link module:content-compatibility/dataschema~DataSchema}. + * + * @readonly + * @member {module:content-compatibility/dataschema~DataSchema} #dataSchema + */ + this.dataSchema = new DataSchema(); + + /** + * An instance of the {@link module:content-compatibility/datafilter~DataFilter}. + * + * @readonly + * @member {module:content-compatibility/datafilter~DataFilter} #dataFilter + */ + this.dataFilter = new DataFilter( editor, this.dataSchema ); + } + /** + * @inheritDoc + */ + static get pluginName() { + return 'GeneralHtmlSupport'; + } +} diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 4037433cfe2..742f27b8769 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -11,8 +11,7 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import DataSchema from '../src/dataschema'; -import DataFilter from '../src/datafilter'; +import GeneralHtmlSupport from '../src/generalhtmlsupport'; describe( 'DataFilter', () => { let editor, model, dataFilter, dataSchema; @@ -22,14 +21,16 @@ describe( 'DataFilter', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ Paragraph, FontColorEditing, LinkEditing ] + plugins: [ Paragraph, FontColorEditing, LinkEditing, GeneralHtmlSupport ] } ) .then( newEditor => { editor = newEditor; model = editor.model; - dataSchema = new DataSchema(); - dataFilter = new DataFilter( editor, dataSchema ); + const plugin = editor.plugins.get( GeneralHtmlSupport ); + + dataFilter = plugin.dataFilter; + dataSchema = plugin.dataSchema; } ); } ); @@ -383,7 +384,7 @@ describe( 'DataFilter', () => { } ); it( 'should not convert attributes if the model schema item definition is not registered', () => { - dataSchema.registerBlockElement( { view: 'xyz', model: 'modelXyz', isFeature: true } ); + dataSchema.registerBlockElement( { view: 'xyz', model: 'modelXyz' } ); dataFilter.allowElement( { name: 'xyz' } ); dataFilter.allowAttributes( { name: 'xyz', attributes: { 'data-foo': 'foo' } } ); diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 8c589a00cc0..2e701974db5 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -96,7 +96,6 @@ describe( 'DataSchema', () => { { view: 'def3', model: 'htmlDef3', - isFeature: true, modelSchema: { inheritTypesFrom: 'htmlDef2' } diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js index 5821e3c578a..7603b63a6a8 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js @@ -16,20 +16,18 @@ import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; import List from '@ckeditor/ckeditor5-list/src/list'; import Link from '@ckeditor/ckeditor5-link/src/link'; -import DataSchema from '../../src/dataschema'; -import DataFilter from '../../src/datafilter'; +import GeneralHtmlSupport from '../../src/generalhtmlsupport'; /** * Client custom plugin extending HTML support for compatibility. */ class ExtendHTMLSupport extends Plugin { + static get requires() { + return [ GeneralHtmlSupport ]; + } + init() { - // Create data schema object including default configuration based on CKE4 - // DTD elements, missing dedicated feature in CKEditor 5. - // Data schema only behaves as container for DTD definitions, it doesn't change - // anything inside the editor itself. Registered elements are not extending editor - // model schema at this point. - const dataSchema = new DataSchema(); + const { dataSchema, dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); // Extend schema with custom `xyz` element. dataSchema.registerBlockElement( { @@ -40,10 +38,6 @@ class ExtendHTMLSupport extends Plugin { } } ); - // Create data filter which will register editor model schema and converters required - // to allow elements and filter attributes. - const dataFilter = new DataFilter( this.editor, dataSchema ); - // Allow some elements, at this point model schema will include information about view-model mapping // e.g. article -> ghsArticle dataFilter.allowElement( { name: 'article' } ); From e90852e3d707ecef36fb82c450f140885e069a8e Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 14 Apr 2021 10:16:43 +0200 Subject: [PATCH 105/217] Renaming generic to general. --- .../ckeditor5-content-compatibility/src/generalhtmlsupport.js | 1 + .../manual/{generichtmlsupport.html => generalhtmlsupport.html} | 0 .../manual/{generichtmlsupport.js => generalhtmlsupport.js} | 0 .../manual/{generichtmlsupport.md => generalhtmlsupport.md} | 0 4 files changed, 1 insertion(+) rename packages/ckeditor5-content-compatibility/tests/manual/{generichtmlsupport.html => generalhtmlsupport.html} (100%) rename packages/ckeditor5-content-compatibility/tests/manual/{generichtmlsupport.js => generalhtmlsupport.js} (100%) rename packages/ckeditor5-content-compatibility/tests/manual/{generichtmlsupport.md => generalhtmlsupport.md} (100%) diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index bae367309b0..a5e11bc0bbb 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -40,6 +40,7 @@ export default class GeneralHtmlSupport extends Plugin { */ this.dataFilter = new DataFilter( editor, this.dataSchema ); } + /** * @inheritDoc */ diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.html rename to packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.js rename to packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.md b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generichtmlsupport.md rename to packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md From c7a592fed7443ed6c5d05c7e7b14204d62fbc9f1 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 14 Apr 2021 12:09:40 +0200 Subject: [PATCH 106/217] Added test coverage for initializing ghs from plugin. --- .../tests/datafilter.js | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 742f27b8769..ebc2c078db0 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -4,6 +4,7 @@ */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; @@ -38,6 +39,78 @@ describe( 'DataFilter', () => { return editor.destroy(); } ); + describe( 'initialization', () => { + let initEditor, initModel; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, FakeExtentedHtmlPlugin, GeneralHtmlSupport ] + } ) + .then( newEditor => { + initEditor = newEditor; + initModel = newEditor.model; + } ); + } ); + + afterEach( () => { + return initEditor.destroy(); + } ); + + it( 'should allow element registered in init() method', () => { + initEditor.setData( '

    foobar

    ' ); + + expect( getModelData( initModel, { withoutSelection: true } ) ).to.equal( + 'foobar' + ); + + expect( initEditor.getData() ).to.equal( '

    foobar

    ' ); + } ); + + it( 'should allow element registered in afterInit() method', () => { + initEditor.setData( '

    foobar

    ' ); + + expect( getModelData( initModel, { withoutSelection: true } ) ).to.equal( + 'foobar' + ); + + expect( initEditor.getData() ).to.equal( '

    foobar

    ' ); + } ); + + it( 'should allow element registered after editor initialization', () => { + const { dataFilter } = initEditor.plugins.get( GeneralHtmlSupport ); + + dataFilter.allowElement( { name: 'span' } ); + + initEditor.setData( '

    foobar

    ' ); + + expect( getModelDataWithAttributes( initModel, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlSpan="(1)">foobar', + attributes: { + 1: {} + } + } ); + + expect( initEditor.getData() ).to.equal( '

    foobar

    ' ); + } ); + + class FakeExtentedHtmlPlugin extends Plugin { + static get requires() { + return [ GeneralHtmlSupport ]; + } + + init() { + const { dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); + dataFilter.allowElement( { name: 'article' } ); + } + + afterInit() { + const { dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); + dataFilter.allowElement( { name: 'section' } ); + } + } + } ); + describe( 'block', () => { it( 'should allow element', () => { dataFilter.allowElement( { name: 'article' } ); From 723ad211c60137425faebd3c01f0ec7a619b1c4d Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 14 Apr 2021 12:24:02 +0200 Subject: [PATCH 107/217] Updated defaults. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 7c8c3421f81..2fe4edd0d41 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -87,7 +87,7 @@ export default class DataFilter { * Indicates if {@link module:core/editor~Editor#data editor's data controller} data has been already initialized. * * @private - * @param {Boolean} #_dataInitialized + * @param {Boolean} [#_dataInitialized=false] */ this._dataInitialized = false; From 76be19c1ae001de4129d8bf26bf63ac888b2cd24 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 15 Apr 2021 11:53:16 +0200 Subject: [PATCH 108/217] Refactoring. --- .../src/datafilter.js | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2fe4edd0d41..d5c831e666e 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -79,7 +79,7 @@ export default class DataFilter { * * @readonly * @private - * @param {Set.} #_allowedElements + * @member {Set.} #_allowedElements */ this._allowedElements = new Set(); @@ -87,33 +87,13 @@ export default class DataFilter { * Indicates if {@link module:core/editor~Editor#data editor's data controller} data has been already initialized. * * @private - * @param {Boolean} [#_dataInitialized=false] + * @member {Boolean} [#_dataInitialized=false] */ this._dataInitialized = false; this._registerElementsAfterInit(); } - /** - * Registers elements allowed by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method - * once {@link module:core/editor~Editor#data editor's data controller} is initialized. - * - * @private - */ - _registerElementsAfterInit() { - this.editor.data.on( 'init', () => { - this._dataInitialized = true; - - for ( const definition of this._allowedElements ) { - this._registerElement( definition ); - } - }, { - // With high priority listener we are able to register elements right before - // running data conversion. - priority: 'high' - } ); - } - /** * Allow the given element in the editor context. * @@ -130,6 +110,10 @@ export default class DataFilter { this._allowedElements.add( definition ); + // We need to wait for all features to be initialized before we can register + // element, so we can access existing features model schemas. + // If the data has not been initialized yet, _registerElementsAfterInit() method will take care of + // registering elements. if ( this._dataInitialized ) { this._registerElement( definition ); } @@ -156,6 +140,26 @@ export default class DataFilter { this._disallowedAttributes.add( config ); } + /** + * Registers elements allowed by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method + * once {@link module:core/editor~Editor#data editor's data controller} is initialized. + * + * @private + */ + _registerElementsAfterInit() { + this.editor.data.on( 'init', () => { + this._dataInitialized = true; + + for ( const definition of this._allowedElements ) { + this._registerElement( definition ); + } + }, { + // With high priority listener we are able to register elements right before + // running data conversion. + priority: 'high' + } ); + } + /** * Registers element and attribute converters for the given data schema definition. * From 26033e1904bc9253952d1605ee94ce70ce24c4a5 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 15 Apr 2021 11:54:44 +0200 Subject: [PATCH 109/217] Avoid specifying default priority. --- .../ckeditor5-content-compatibility/src/dataschema.js | 8 ++------ .../ckeditor5-content-compatibility/tests/dataschema.js | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index cbcd6ea6bf0..11a3062679b 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -196,11 +196,7 @@ export default class DataSchema { * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition */ registerInlineElement( definition ) { - this._definitions.set( definition.model, { - priority: 10, - ...definition, - isInline: true - } ); + this._definitions.set( definition.model, { ...definition, isInline: true } ); } /** @@ -311,7 +307,7 @@ function testViewName( pattern, viewName ) { * @property {String} view Name of the view element. * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. * @property {Boolean} isInline Indicates that the definition descibes inline element. - * @property {Number} [priority=10] Element priority. Decides in what order elements are wrapped by + * @property {Number} [priority] Element priority. Decides in what order elements are wrapped by * {@link module:engine/view/downcastwriter~DowncastWriter}. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 2e701974db5..49523214515 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -35,7 +35,6 @@ describe( 'DataSchema', () => { expect( Array.from( result ) ).to.deep.equal( [ { model: 'htmlDef', view: 'def', - priority: 10, isInline: true } ] ); } ); @@ -54,7 +53,6 @@ describe( 'DataSchema', () => { expect( Array.from( result ) ).to.deep.equal( [ { model: 'htmlDef', view: 'def', - priority: 10, attributeProperties: { copyOnEnter: true }, From 3e12ab7b55f72f754e8ff70a6f94f6489dcaeb98 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 15 Apr 2021 11:57:38 +0200 Subject: [PATCH 110/217] Added missing jsdoc. --- .../ckeditor5-content-compatibility/src/generalhtmlsupport.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index a5e11bc0bbb..0954f2789aa 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -21,6 +21,9 @@ import DataFilter from './datafilter'; * @extends module:core/plugin~Plugin */ export default class GeneralHtmlSupport extends Plugin { + /** + * @param {module:core/editor/editor~Editor} editor + */ constructor( editor ) { super( editor ); From 397258d1331eba7d2aa766cccb119d0849d87194 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 15 Apr 2021 13:42:13 +0200 Subject: [PATCH 111/217] Listen on priority higher than RTC. --- .../src/datafilter.js | 5 +++-- .../tests/datafilter.js | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index d5c831e666e..373bcd85f10 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -155,8 +155,9 @@ export default class DataFilter { } }, { // With high priority listener we are able to register elements right before - // running data conversion. - priority: 'high' + // running data conversion. Make also sure that priority is higher than the one + // used by `RealTimeCollaborationClient`, as RTC is stopping event propagation. + priority: priorities.get( 'high' ) + 1 } ); } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index ebc2c078db0..d391eecd298 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -45,7 +45,8 @@ describe( 'DataFilter', () => { beforeEach( () => { return VirtualTestEditor .create( { - plugins: [ Paragraph, FakeExtentedHtmlPlugin, GeneralHtmlSupport ] + // Keep FakeRTCPlugin before FakeExtentedHtmlPlugin, so it's registered first. + plugins: [ Paragraph, FakeRTCPlugin, FakeExtentedHtmlPlugin ] } ) .then( newEditor => { initEditor = newEditor; @@ -94,6 +95,18 @@ describe( 'DataFilter', () => { expect( initEditor.getData() ).to.equal( '

    foobar

    ' ); } ); + class FakeRTCPlugin extends Plugin { + constructor( editor ) { + super( editor ); + + // Fake listener to simulate RTC one. Registering in constructor to + // register it before DataFilter listener. + this.editor.data.on( 'init', evt => { + evt.stop(); + }, { priority: 'high' } ); + } + } + class FakeExtentedHtmlPlugin extends Plugin { static get requires() { return [ GeneralHtmlSupport ]; From df22ed43a203d38b39957f750eea2fe684ae4145 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 6 May 2021 10:23:36 +0200 Subject: [PATCH 112/217] Updated package.json to the latest CKE5 release. --- .../package.json | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index 97cc03ad01a..93340377001 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-content-compatibility", - "version": "27.0.0", + "version": "27.1.0", "description": "Content compatibility feature", "private": true, "keywords": [ @@ -15,21 +15,21 @@ ], "main": "src/index.js", "dependencies": { - "ckeditor5": "^27.0.0", + "ckeditor5": "^27.1.0", "lodash-es": "^4.17.15" }, "devDependencies": { - "@ckeditor/ckeditor5-basic-styles": "^27.0.0", - "@ckeditor/ckeditor5-block-quote": "^27.0.0", - "@ckeditor/ckeditor5-core": "^27.0.0", - "@ckeditor/ckeditor5-editor-classic": "^27.0.0", - "@ckeditor/ckeditor5-engine": "^27.0.0", - "@ckeditor/ckeditor5-essentials": "^27.0.0", - "@ckeditor/ckeditor5-font": "^27.0.0", - "@ckeditor/ckeditor5-link": "^27.0.0", - "@ckeditor/ckeditor5-list": "^27.0.0", - "@ckeditor/ckeditor5-paragraph": "^27.0.0", - "@ckeditor/ckeditor5-utils": "^27.0.0", + "@ckeditor/ckeditor5-basic-styles": "^27.1.0", + "@ckeditor/ckeditor5-block-quote": "^27.1.0", + "@ckeditor/ckeditor5-core": "^27.1.0", + "@ckeditor/ckeditor5-editor-classic": "^27.1.0", + "@ckeditor/ckeditor5-engine": "^27.1.0", + "@ckeditor/ckeditor5-essentials": "^27.1.0", + "@ckeditor/ckeditor5-font": "^27.1.0", + "@ckeditor/ckeditor5-link": "^27.1.0", + "@ckeditor/ckeditor5-list": "^27.1.0", + "@ckeditor/ckeditor5-paragraph": "^27.1.0", + "@ckeditor/ckeditor5-utils": "^27.1.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11" }, From a0700a4789a126d3cbf791b8a5f7af5b32b34670 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 6 May 2021 09:33:39 +0200 Subject: [PATCH 113/217] Initial implementation for object elements. --- .../package.json | 1 + .../src/datafilter.js | 138 +++++++++++++----- .../src/dataschema.js | 127 ++++++++++++++++ .../tests/manual/objects.html | 72 +++++++++ .../tests/manual/objects.js | 91 ++++++++++++ .../tests/manual/objects.md | 1 + .../theme/datafilter.css | 48 ++++++ 7 files changed, 444 insertions(+), 34 deletions(-) create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/objects.html create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/objects.js create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/objects.md create mode 100644 packages/ckeditor5-content-compatibility/theme/datafilter.css diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index 93340377001..352c286da55 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -29,6 +29,7 @@ "@ckeditor/ckeditor5-link": "^27.1.0", "@ckeditor/ckeditor5-list": "^27.1.0", "@ckeditor/ckeditor5-paragraph": "^27.1.0", + "@ckeditor/ckeditor5-table": "^27.1.0", "@ckeditor/ckeditor5-utils": "^27.1.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11" diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 373bcd85f10..b2b64b69eb6 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -7,12 +7,13 @@ * @module content-compatibility/datafilter */ -import { Matcher } from 'ckeditor5/src/engine'; -import { priorities, toArray, CKEditorError } from 'ckeditor5/src/utils'; +import { Matcher, enablePlaceholder } from 'ckeditor5/src/engine'; +import { priorities, CKEditorError } from 'ckeditor5/src/utils'; +import { toWidget } from 'ckeditor5/src/widget'; +import { Template } from 'ckeditor5/src/ui'; +import { cloneDeep, capitalize } from 'lodash-es'; -import { cloneDeep } from 'lodash-es'; - -const DATA_SCHEMA_ATTRIBUTE_KEY = 'htmlAttributes'; +import '../theme/datafilter.css'; /** * Allows to validate elements and element attributes registered by {@link module:content-compatibility/dataschema~DataSchema}. @@ -172,6 +173,8 @@ export default class DataFilter { this._registerInlineElement( definition ); } else if ( definition.isBlock ) { this._registerBlockElement( definition ); + } else if ( definition.isObject ) { + this._registerObjectElement( definition ); } else { /** * Only a definition marked as inline or block can be allowed. @@ -186,6 +189,30 @@ export default class DataFilter { } } + _registerObjectElement( definition ) { + const schema = this.editor.model.schema; + + schema.register( definition.model, definition.modelSchema ); + + this._addObjectElementToElementConversion( definition ); + this._addDisallowedAttributeConversion( definition ); + this._addAllowedAttributeConversion( definition ); + } + + _createObjectElementView( modelName, writer ) { + if ( this.editor.model.schema.isInline( modelName ) ) { + return writer.createContainerElement( 'span', { + class: 'ck-widget__compatibility ck-widget__compatibility-inline' + }, { + isAllowedInsideAttributeElement: true + } ); + } + + return writer.createContainerElement( 'div', { + class: 'ck-widget__compatibility ck-widget__compatibility-block' + } ); + } + /** * Registers block element and attribute converters for the given data schema definition. * @@ -198,12 +225,12 @@ export default class DataFilter { const schema = this.editor.model.schema; if ( !schema.isRegistered( definition.model ) ) { - this._registerBlockElementSchema( definition ); + this.editor.model.schema.register( definition.model, definition.modelSchema ); this._addBlockElementToElementConversion( definition ); } this._addDisallowedAttributeConversion( definition ); - this._addBlockElementAttributeConversion( definition ); + this._addAllowedAttributeConversion( definition ); } /** @@ -229,28 +256,6 @@ export default class DataFilter { this._addInlineElementConversion( definition ); } - /** - * Registers model schema definition for the given block element definition. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition - */ - _registerBlockElementSchema( definition ) { - const schema = this.editor.model.schema; - - schema.register( definition.model, definition.modelSchema ); - - const allowedChildren = toArray( definition.allowChildren || [] ); - - for ( const child of allowedChildren ) { - if ( schema.isRegistered( child ) ) { - schema.extend( child, { - allowIn: definition.model - } ); - } - } - } - /** * Adds element to element converters for the given block element definition. * @@ -279,12 +284,12 @@ export default class DataFilter { } /** - * Adds attribute converters for the given block element definition. + * Adds attribute converters for the given element definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ - _addBlockElementAttributeConversion( { model: modelName, view: viewName } ) { + _addAllowedAttributeConversion( { model: modelName, view: viewName } ) { const conversion = this.editor.conversion; if ( !viewName ) { @@ -300,13 +305,13 @@ export default class DataFilter { const viewAttributes = this._matchAndConsumeAllowedAttributes( data.viewItem, conversionApi ); if ( viewAttributes ) { - conversionApi.writer.setAttribute( DATA_SCHEMA_ATTRIBUTE_KEY, viewAttributes, data.modelRange ); + conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange ); } }, { priority: 'low' } ); } ); conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( `attribute:${ DATA_SCHEMA_ATTRIBUTE_KEY }:${ modelName }`, ( evt, data, conversionApi ) => { + dispatcher.on( `attribute:htmlAttributes:${ modelName }`, ( evt, data, conversionApi ) => { const viewAttributes = data.attributeNewValue; if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { @@ -394,6 +399,46 @@ export default class DataFilter { } ); } + _addObjectElementToElementConversion( definition ) { + const conversion = this.editor.conversion; + const { view: viewName, model: modelName } = definition; + + conversion.for( 'upcast' ).elementToElement( { + model: modelName, + view: viewName + } ); + + conversion.for( 'editingDowncast' ).elementToElement( { + model: modelName, + view: ( modelElement, { writer } ) => { + const widgetWrapper = this._createObjectElementView( modelName, writer ); + const title = createHtmlObjectTitle( viewName ); + + enablePlaceholder( { + view: this.editor.editing.view, + element: widgetWrapper, + text: title + } ); + + const widgetLabel = createObjectElementWidgetUILabel( title, writer ); + + writer.insert( writer.createPositionAt( widgetWrapper, 'end' ), widgetLabel ); + + return toWidget( widgetWrapper, writer ); + } + + } ); + + conversion.for( 'dataDowncast' ).elementToElement( { + model: modelName, + view: ( modelItem, { writer } ) => { + return writer.createContainerElement( viewName, null, { + isAllowedInsideAttributeElement: this.editor.model.schema.isInline( modelName ) + } ); + } + } ); + } + /** * Matches and consumes allowed view attributes. * @@ -542,3 +587,28 @@ function mergeViewElementAttributes( oldValue, newValue ) { return result; } + +function createHtmlObjectTitle( viewName ) { + return 'HTML ' + capitalize( viewName ); +} + +function createObjectElementWidgetUILabel( title, writer ) { + return writer.createUIElement( 'div', { + class: 'ck ck-reset_all ck-widget__compatibility-type' + }, function( domDocument ) { + const wrapperDomElement = this.toDomElement( domDocument ); + + const labelTemplate = new Template( { + attributes: { + class: [ + 'ck', + 'ck-widget__compatibility-type__label' + ] + }, + text: title + } ); + wrapperDomElement.appendChild( labelTemplate.render() ); + + return wrapperDomElement; + } ); +} diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 11a3062679b..bfeb75317b7 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -142,6 +142,129 @@ export default class DataSchema { } } ); + this.registerObjectElement( { + view: 'object', + model: 'htmlObject', + modelSchema: { + isObject: true, + isInline: true, + allowWhere: '$text', + allowAttributesOf: '$text' + } + } ); + + this.registerObjectElement( { + view: 'param', + model: 'htmlParam', + modelSchema: { + isObject: true, + isInline: true, + allowIn: 'htmlObject' + } + } ); + + this.registerObjectElement( { + view: 'embed', + model: 'htmlEmbed', + modelSchema: { + isObject: true, + isInline: true, + allowIn: 'htmlObject' + } + } ); + + this.registerObjectElement( { + view: 'iframe', + model: 'htmlIframe', + modelSchema: { + isObject: true, + isInline: true, + allowWhere: '$text', + allowAttributesOf: '$text' + } + } ); + + this.registerObjectElement( { + view: 'form', + model: 'htmlForm', + modelSchema: { + inheritAllFrom: '$htmlBlock', + allowContentOf: '$root', + isObject: true + } + } ); + + this.registerObjectElement( { + view: 'input', + model: 'htmlInput', + modelSchema: { + allowIn: [ '$block', '$form' ], + isObject: true, + isInline: true + } + } ); + + this.registerObjectElement( { + view: 'textarea', + model: 'htmlTextarea', + modelSchema: { + allowIn: '$block', + allowChildren: '$text', + isObject: true, + isInline: true + } + } ); + + this.registerObjectElement( { + view: 'select', + model: 'htmlSelect', + modelSchema: { + allowIn: '$block', + isObject: true, + isInline: true + } + } ); + + this.registerObjectElement( { + view: 'option', + model: 'htmlOption', + modelSchema: { + allowIn: 'htmlSelect', + isObject: true, + isInline: true + } + } ); + + this.registerObjectElement( { + view: 'video', + model: 'htmlVideo', + modelSchema: { + allowIn: '$block', + isObject: true, + isInline: true + } + } ); + + this.registerObjectElement( { + view: 'audio', + model: 'htmlAudio', + modelSchema: { + allowIn: '$block', + isObject: true, + isInline: true + } + } ); + + this.registerObjectElement( { + view: 'source', + model: 'htmlSource', + modelSchema: { + allowIn: [ 'htmlVideo', 'htmlAudio' ], + isObject: true, + isInline: true + } + } ); + // Inline elements. this.registerInlineElement( { view: 'a', @@ -199,6 +322,10 @@ export default class DataSchema { this._definitions.set( definition.model, { ...definition, isInline: true } ); } + registerObjectElement( definition ) { + this._definitions.set( definition.model, { ...definition, isObject: true } ); + } + /** * Returns all definitions matching the given view name. * diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.html b/packages/ckeditor5-content-compatibility/tests/manual/objects.html new file mode 100644 index 00000000000..f53be64d54e --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.html @@ -0,0 +1,72 @@ + + + + +
    +

    + + + + + + +

    +

    +
    +

    + +

    + +

    + +

    + +

    + +

    + +

    + +

    + +

    some custom text

    + + + + + + + + + + + + + + + + +
      
      
      
    + +

    looks like form can include anything

    +
    +

    + +

    + +

    + +

    + +

    + +

    + +

    + +

    + +

    + +

    +
    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js new file mode 100644 index 00000000000..f80f4f24919 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -0,0 +1,91 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console:false, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import List from '@ckeditor/ckeditor5-list/src/list'; +import Link from '@ckeditor/ckeditor5-link/src/link'; +import Table from '@ckeditor/ckeditor5-table/src/table'; + +import GeneralHtmlSupport from '../../src/generalhtmlsupport'; + +/** + * Client custom plugin extending HTML support for compatibility. + */ +class ExtendHTMLSupport extends Plugin { + static get requires() { + return [ GeneralHtmlSupport ]; + } + + init() { + const { dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); + + const definitions = [ + { name: 'object', attributes: [ 'classid', 'codebase' ] }, + { name: 'param', attributes: [ 'name', 'value' ] }, + { name: 'embed', attributes: [ 'allowfullscreen', 'pluginspage', 'quality', 'src', 'type' ] }, + { name: 'iframe', attributes: [ 'frameborder', 'scrolling', 'src' ] }, + { name: 'form', attributes: [ 'action', 'method', 'name' ] }, + { name: 'input', attributes: [ 'name', 'type', 'value', 'alt', 'src' ] }, + { name: 'textarea', attributes: [ 'name' ] }, + { name: 'select', attributes: [ 'name' ] }, + { name: 'option', attributes: [ 'value', 'selected' ] }, + { name: 'video', attributes: [ 'height', 'width', 'controls' ] }, + { name: 'audio', attributes: [ 'controls' ] }, + { name: 'source', attributes: [ 'src', 'type' ] } + ]; + + for ( const definition of definitions ) { + dataFilter.allowElement( { name: definition.name } ); + + for ( const key of definition.attributes ) { + const attributes = {}; + attributes[ key ] = /[\s\S]+/; + + dataFilter.allowAttributes( { name: definition.name, attributes } ); + } + } + } +} + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + Table, + Link, + BlockQuote, + Bold, + Essentials, + ExtendHTMLSupport, + Italic, + List, + Paragraph, + Strikethrough + ], + toolbar: [ + 'bold', + 'italic', + 'strikethrough', + '|', + 'numberedList', + 'bulletedList', + '|', + 'blockquote' + ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.md b/packages/ckeditor5-content-compatibility/tests/manual/objects.md new file mode 100644 index 00000000000..4b123939219 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.md @@ -0,0 +1 @@ +# Object elements support diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css new file mode 100644 index 00000000000..42ddf3ce9e1 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -0,0 +1,48 @@ +:root { + --ck-widget__compatibility-base-background: var(--ck-color-widget-blurred-border) +} + +.ck .ck-widget { + &.ck-widget__compatibility { + padding: 20px 0 15px 0; + min-height: 20px; + + & .ck-widget__compatibility-type { + opacity: 0; + font-size: var(--ck-font-size-tiny); + padding: 0 5px; + background: var(--ck-widget-compatibility-base-background); + display: block; + position: absolute; + overflow: hidden; + z-index: var(--ck-z-default); + /* Place it in the middle of the outline */ + top: calc(-0.5 * var(--ck-widget-outline-thickness)); + right: min(10%, 30px); + transform: translateY(-50%); + } + + &.ck-widget_selected > .ck-widget__compatibility-type { + opacity: 1; + color: var(--ck-color-base-background); + background-color: var(--ck-color-focus-border); + } + + &:not(.ck-widget_selected):hover > .ck-widget__compatibility-type { + opacity: 1; + color: var(--ck-color-base-background); + background: var(--ck-color-widget-hover-border); + } + } + + &.ck-widget__compatibility-inline { + display: inline-block; + min-width: 100px; + } +} + +.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget__compatibility.ck-widget_selected > +.ck-widget__compatibility-type { + color: hsl(0,0%,60%); + background: var(--ck-widget-compatibility-base-background); +} From 09d528bbb8bf6ae77c1416eb9e6cdf45b5aff9e2 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 6 May 2021 10:09:55 +0200 Subject: [PATCH 114/217] Styling, small bug fix. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 4 ++++ packages/ckeditor5-content-compatibility/theme/datafilter.css | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index b2b64b69eb6..2f7c8c15cf3 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -314,6 +314,10 @@ export default class DataFilter { dispatcher.on( `attribute:htmlAttributes:${ modelName }`, ( evt, data, conversionApi ) => { const viewAttributes = data.attributeNewValue; + if ( !viewAttributes ) { + return; + } + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { return; } diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index 42ddf3ce9e1..7cd9d5b5d2e 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -6,6 +6,7 @@ &.ck-widget__compatibility { padding: 20px 0 15px 0; min-height: 20px; + border: 1px dotted hsl(15, 100%, 43%); & .ck-widget__compatibility-type { opacity: 0; From 0bcb80845c9bdf85b1ef10ab862b67c50164b4fb Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 6 May 2021 10:50:54 +0200 Subject: [PATCH 115/217] Fixed allowChildren API, docs. --- .../src/datafilter.js | 13 +++++++++++ .../src/dataschema.js | 23 +++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2f7c8c15cf3..3884b604957 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -189,6 +189,14 @@ export default class DataFilter { } } + /** + * Registers object element and attribute converters for the given data schema definition. + * + * If the element model schema is already registered, this method will do nothing. + * + * @private + * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition + */ _registerObjectElement( definition ) { const schema = this.editor.model.schema; @@ -199,6 +207,9 @@ export default class DataFilter { this._addAllowedAttributeConversion( definition ); } + /** + * TODO + */ _createObjectElementView( modelName, writer ) { if ( this.editor.model.schema.isInline( modelName ) ) { return writer.createContainerElement( 'span', { @@ -592,10 +603,12 @@ function mergeViewElementAttributes( oldValue, newValue ) { return result; } +// TODO function createHtmlObjectTitle( viewName ) { return 'HTML ' + capitalize( viewName ); } +// TODO function createObjectElementWidgetUILabel( title, writer ) { return writer.createUIElement( 'div', { class: 'ck ck-reset_all ck-widget__compatibility-type' diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index bfeb75317b7..96f10699b10 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -66,8 +66,8 @@ export default class DataSchema { this.registerBlockElement( { model: '$htmlBlock', - allowChildren: '$block', modelSchema: { + allowChildren: '$block', allowIn: [ '$root', '$htmlBlock' ], isBlock: true } @@ -101,8 +101,8 @@ export default class DataSchema { this.registerBlockElement( { model: '$htmlDatalist', - allowChildren: '$block', modelSchema: { + allowChildren: '$block', allowIn: 'htmlDl', isBlock: true } @@ -136,8 +136,8 @@ export default class DataSchema { this.registerBlockElement( { view: 'summary', model: 'htmlSummary', - allowChildren: '$text', modelSchema: { + allowChildren: '$text', allowIn: 'htmlDetails' } } ); @@ -322,6 +322,11 @@ export default class DataSchema { this._definitions.set( definition.model, { ...definition, isInline: true } ); } + /** + * Add new data schema definition describing inline element. + * + * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition + */ registerObjectElement( definition ) { this._definitions.set( definition.model, { ...definition, isObject: true } ); } @@ -421,12 +426,22 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. - * @property {String|Array.} [allowChildren] Extends the given children list to allow definition model. * @property {Boolean} isBlock Indicates that the definition describes block element. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ +/** + * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for object elements. + * + * @typedef {Object} module:content-compatibility/dataschema~DataSchemaObjectElementDefinition + * @property {String} view Name of the view element. + * @property {module:engine/model/schema~SchemaItemDefinition} modelSchema The model schema item definition describing registered model. + * @property {Boolean} isObject Indicates that the definition describes object element. + * Set by {@link module:content-compatibility/dataschema~DataSchema#registerObjectElement} method. + * @extends module:content-compatibility/dataschema~DataSchemaDefinition + */ + /** * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for inline elements. * From 832195132050426fca1ae095a0518804010486ad Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 6 May 2021 11:13:33 +0200 Subject: [PATCH 116/217] Refactoring, css fix. --- .../src/datafilter.js | 34 +++++++++---------- .../theme/datafilter.css | 7 ++-- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 3884b604957..7bdbc226b02 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -207,23 +207,6 @@ export default class DataFilter { this._addAllowedAttributeConversion( definition ); } - /** - * TODO - */ - _createObjectElementView( modelName, writer ) { - if ( this.editor.model.schema.isInline( modelName ) ) { - return writer.createContainerElement( 'span', { - class: 'ck-widget__compatibility ck-widget__compatibility-inline' - }, { - isAllowedInsideAttributeElement: true - } ); - } - - return writer.createContainerElement( 'div', { - class: 'ck-widget__compatibility ck-widget__compatibility-block' - } ); - } - /** * Registers block element and attribute converters for the given data schema definition. * @@ -454,6 +437,23 @@ export default class DataFilter { } ); } + /** + * TODO + */ + _createObjectElementView( modelName, writer ) { + if ( this.editor.model.schema.isInline( modelName ) ) { + return writer.createContainerElement( 'span', { + class: 'ck-widget__compatibility ck-widget__compatibility-inline' + }, { + isAllowedInsideAttributeElement: true + } ); + } + + return writer.createContainerElement( 'div', { + class: 'ck-widget__compatibility ck-widget__compatibility-block' + } ); + } + /** * Matches and consumes allowed view attributes. * diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index 7cd9d5b5d2e..c773f619028 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -1,5 +1,5 @@ :root { - --ck-widget__compatibility-base-background: var(--ck-color-widget-blurred-border) + --ck-widget-compatibility-base-background: var(--ck-color-widget-blurred-border) } .ck .ck-widget { @@ -26,7 +26,7 @@ &.ck-widget_selected > .ck-widget__compatibility-type { opacity: 1; color: var(--ck-color-base-background); - background-color: var(--ck-color-focus-border); + background: var(--ck-color-focus-border); } &:not(.ck-widget_selected):hover > .ck-widget__compatibility-type { @@ -42,8 +42,7 @@ } } -.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget__compatibility.ck-widget_selected > -.ck-widget__compatibility-type { +.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget__compatibility.ck-widget_selected > .ck-widget__compatibility-type { color: hsl(0,0%,60%); background: var(--ck-widget-compatibility-base-background); } From 94ded58bb2e5cded0ccca59a8f86e08e3052e2d0 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Sun, 9 May 2021 11:44:27 +0200 Subject: [PATCH 117/217] Rewritten implementation to embed whole objects instead. --- .../src/datafilter.js | 176 +++++++++--------- .../src/dataschema.js | 120 +++--------- .../tests/manual/objects.html | 10 +- .../tests/manual/objects.js | 6 +- .../theme/datafilter.css | 66 +++---- 5 files changed, 150 insertions(+), 228 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 7bdbc226b02..1fff7576ae4 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -7,11 +7,10 @@ * @module content-compatibility/datafilter */ -import { Matcher, enablePlaceholder } from 'ckeditor5/src/engine'; +import { Matcher } from 'ckeditor5/src/engine'; import { priorities, CKEditorError } from 'ckeditor5/src/utils'; import { toWidget } from 'ckeditor5/src/widget'; -import { Template } from 'ckeditor5/src/ui'; -import { cloneDeep, capitalize } from 'lodash-es'; +import { cloneDeep } from 'lodash-es'; import '../theme/datafilter.css'; @@ -192,19 +191,27 @@ export default class DataFilter { /** * Registers object element and attribute converters for the given data schema definition. * - * If the element model schema is already registered, this method will do nothing. - * * @private * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition */ _registerObjectElement( definition ) { const schema = this.editor.model.schema; - schema.register( definition.model, definition.modelSchema ); + // All object elements are represented by a single widget. + if ( !schema.isRegistered( 'htmlObjectEmbed' ) ) { + schema.register( 'htmlObjectEmbed', { + isObject: true, + isInline: true, + allowWhere: '$text', + allowAttributesOf: '$text', + allowAttributes: [ 'value', 'view', 'htmlAttributes' ] + } ); + + this._addHtmlObjectEmbedConversion(); + } - this._addObjectElementToElementConversion( definition ); this._addDisallowedAttributeConversion( definition ); - this._addAllowedAttributeConversion( definition ); + this._addObjectElementToElementConversion( definition ); } /** @@ -224,7 +231,7 @@ export default class DataFilter { } this._addDisallowedAttributeConversion( definition ); - this._addAllowedAttributeConversion( definition ); + this._addBlockElementAttributeConversion( definition ); } /** @@ -250,6 +257,40 @@ export default class DataFilter { this._addInlineElementConversion( definition ); } + _addHtmlObjectEmbedConversion() { + const editor = this.editor; + const conversion = editor.conversion; + const t = editor.t; + + conversion.for( 'dataDowncast' ).elementToElement( { + model: 'htmlObjectEmbed', + view: createObjectViewElement + } ); + + conversion.for( 'editingDowncast' ).elementToElement( { + model: 'htmlObjectEmbed', + view: ( modelElement, conversionApi ) => { + const { writer } = conversionApi; + + const viewContainer = writer.createContainerElement( 'span', { + class: 'html-object-embed', + 'data-html-object-embed-label': t( 'HTML object' ) + }, { + isAllowedInsideAttributeElement: true + } ); + + const viewElement = createObjectViewElement( modelElement, conversionApi ); + writer.addClass( 'html-object-embed__content', viewElement ); + + writer.insert( writer.createPositionAt( viewContainer, 0 ), viewElement ); + + return toWidget( viewContainer, writer, { + widgetLabel: t( 'HTML object' ) + } ); + } + } ); + } + /** * Adds element to element converters for the given block element definition. * @@ -283,7 +324,7 @@ export default class DataFilter { * @private * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ - _addAllowedAttributeConversion( { model: modelName, view: viewName } ) { + _addBlockElementAttributeConversion( { model: modelName, view: viewName } ) { const conversion = this.editor.conversion; if ( !viewName ) { @@ -308,10 +349,6 @@ export default class DataFilter { dispatcher.on( `attribute:htmlAttributes:${ modelName }`, ( evt, data, conversionApi ) => { const viewAttributes = data.attributeNewValue; - if ( !viewAttributes ) { - return; - } - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { return; } @@ -397,63 +434,31 @@ export default class DataFilter { } ); } - _addObjectElementToElementConversion( definition ) { + _addObjectElementToElementConversion( { view: viewName } ) { const conversion = this.editor.conversion; - const { view: viewName, model: modelName } = definition; - conversion.for( 'upcast' ).elementToElement( { - model: modelName, - view: viewName + // Object element content will be hidden under `value` property and restored by + // `htmlObjectEmbed` downcast converters. + this.editor.data.registerRawContentMatcher( { + name: viewName } ); - conversion.for( 'editingDowncast' ).elementToElement( { - model: modelName, - view: ( modelElement, { writer } ) => { - const widgetWrapper = this._createObjectElementView( modelName, writer ); - const title = createHtmlObjectTitle( viewName ); - - enablePlaceholder( { - view: this.editor.editing.view, - element: widgetWrapper, - text: title - } ); - - const widgetLabel = createObjectElementWidgetUILabel( title, writer ); - - writer.insert( writer.createPositionAt( widgetWrapper, 'end' ), widgetLabel ); - - return toWidget( widgetWrapper, writer ); - } - - } ); + // We just need to add upcast conversion for specific view. Downcast is handled by + // `htmlObjectEmbed` itself. + conversion.for( 'upcast' ).elementToElement( { + view: viewName, + model: ( viewElement, conversionApi ) => { + const htmlAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); - conversion.for( 'dataDowncast' ).elementToElement( { - model: modelName, - view: ( modelItem, { writer } ) => { - return writer.createContainerElement( viewName, null, { - isAllowedInsideAttributeElement: this.editor.model.schema.isInline( modelName ) + return conversionApi.writer.createElement( 'htmlObjectEmbed', { + value: viewElement.getCustomProperty( '$rawContent' ), + view: viewName, + ...( htmlAttributes && { htmlAttributes } ) } ); } } ); } - /** - * TODO - */ - _createObjectElementView( modelName, writer ) { - if ( this.editor.model.schema.isInline( modelName ) ) { - return writer.createContainerElement( 'span', { - class: 'ck-widget__compatibility ck-widget__compatibility-inline' - }, { - isAllowedInsideAttributeElement: true - } ); - } - - return writer.createContainerElement( 'div', { - class: 'ck-widget__compatibility ck-widget__compatibility-block' - } ); - } - /** * Matches and consumes allowed view attributes. * @@ -537,6 +542,28 @@ function setViewElementAttributes( writer, viewAttributes, viewElement ) { } } +// Creates object view element from the given model element. +// +// @private +// @param {module:engine/model/element~Element} modelElement +// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi +// @returns {module:engine/view/element~Element} viewElement +function createObjectViewElement( modelElement, { writer } ) { + const viewName = modelElement.getAttribute( 'view' ); + const viewAttributes = modelElement.getAttribute( 'htmlAttributes' ); + const viewContent = modelElement.getAttribute( 'value' ); + + const viewElement = writer.createRawElement( viewName, null, function( domElement ) { + domElement.innerHTML = viewContent; + } ); + + if ( viewAttributes ) { + setViewElementAttributes( writer, viewAttributes, viewElement ); + } + + return viewElement; +} + // Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. // // @private @@ -602,30 +629,3 @@ function mergeViewElementAttributes( oldValue, newValue ) { return result; } - -// TODO -function createHtmlObjectTitle( viewName ) { - return 'HTML ' + capitalize( viewName ); -} - -// TODO -function createObjectElementWidgetUILabel( title, writer ) { - return writer.createUIElement( 'div', { - class: 'ck ck-reset_all ck-widget__compatibility-type' - }, function( domDocument ) { - const wrapperDomElement = this.toDomElement( domDocument ); - - const labelTemplate = new Template( { - attributes: { - class: [ - 'ck', - 'ck-widget__compatibility-type__label' - ] - }, - text: title - } ); - wrapperDomElement.appendChild( labelTemplate.render() ); - - return wrapperDomElement; - } ); -} diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 96f10699b10..aeeedb027a1 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -89,6 +89,14 @@ export default class DataSchema { } } ); + this.registerBlockElement( { + view: 'form', + model: 'htmlForm', + modelSchema: { + inheritAllFrom: '$htmlBlock' + } + } ); + // Add data list elements. this.registerBlockElement( { view: 'dl', @@ -144,125 +152,42 @@ export default class DataSchema { this.registerObjectElement( { view: 'object', - model: 'htmlObject', - modelSchema: { - isObject: true, - isInline: true, - allowWhere: '$text', - allowAttributesOf: '$text' - } - } ); - - this.registerObjectElement( { - view: 'param', - model: 'htmlParam', - modelSchema: { - isObject: true, - isInline: true, - allowIn: 'htmlObject' - } - } ); - - this.registerObjectElement( { - view: 'embed', - model: 'htmlEmbed', - modelSchema: { - isObject: true, - isInline: true, - allowIn: 'htmlObject' - } + model: 'htmlObject' } ); this.registerObjectElement( { view: 'iframe', - model: 'htmlIframe', - modelSchema: { - isObject: true, - isInline: true, - allowWhere: '$text', - allowAttributesOf: '$text' - } + model: 'htmlIframe' } ); this.registerObjectElement( { - view: 'form', - model: 'htmlForm', - modelSchema: { - inheritAllFrom: '$htmlBlock', - allowContentOf: '$root', - isObject: true - } + view: 'input', + model: 'htmlInput' } ); this.registerObjectElement( { - view: 'input', - model: 'htmlInput', - modelSchema: { - allowIn: [ '$block', '$form' ], - isObject: true, - isInline: true - } + view: 'button', + model: 'htmlButton' } ); this.registerObjectElement( { view: 'textarea', - model: 'htmlTextarea', - modelSchema: { - allowIn: '$block', - allowChildren: '$text', - isObject: true, - isInline: true - } + model: 'htmlTextarea' } ); this.registerObjectElement( { view: 'select', - model: 'htmlSelect', - modelSchema: { - allowIn: '$block', - isObject: true, - isInline: true - } - } ); - - this.registerObjectElement( { - view: 'option', - model: 'htmlOption', - modelSchema: { - allowIn: 'htmlSelect', - isObject: true, - isInline: true - } + model: 'htmlSelect' } ); this.registerObjectElement( { view: 'video', - model: 'htmlVideo', - modelSchema: { - allowIn: '$block', - isObject: true, - isInline: true - } + model: 'htmlVideo' } ); this.registerObjectElement( { view: 'audio', - model: 'htmlAudio', - modelSchema: { - allowIn: '$block', - isObject: true, - isInline: true - } - } ); - - this.registerObjectElement( { - view: 'source', - model: 'htmlSource', - modelSchema: { - allowIn: [ 'htmlVideo', 'htmlAudio' ], - isObject: true, - isInline: true - } + model: 'htmlAudio' } ); // Inline elements. @@ -302,6 +227,14 @@ export default class DataSchema { copyOnEnter: true } } ); + + this.registerInlineElement( { + view: 'label', + model: 'htmlLabel', + attributeProperties: { + copyOnEnter: true + } + } ); } /** @@ -436,7 +369,6 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaObjectElementDefinition * @property {String} view Name of the view element. - * @property {module:engine/model/schema~SchemaItemDefinition} modelSchema The model schema item definition describing registered model. * @property {Boolean} isObject Indicates that the definition describes object element. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerObjectElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.html b/packages/ckeditor5-content-compatibility/tests/manual/objects.html index f53be64d54e..d69e187ab83 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.html @@ -25,9 +25,9 @@

    -

    +

    -

    +

    some custom text

    @@ -60,13 +60,13 @@

    -

    -

    -

    +

    + +

    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js index f80f4f24919..e8e88c41eb3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -42,13 +42,15 @@ class ExtendHTMLSupport extends Plugin { { name: 'option', attributes: [ 'value', 'selected' ] }, { name: 'video', attributes: [ 'height', 'width', 'controls' ] }, { name: 'audio', attributes: [ 'controls' ] }, - { name: 'source', attributes: [ 'src', 'type' ] } + { name: 'source', attributes: [ 'src', 'type' ] }, + { name: 'button' }, + { name: 'label' } ]; for ( const definition of definitions ) { dataFilter.allowElement( { name: definition.name } ); - for ( const key of definition.attributes ) { + for ( const key of ( definition.attributes || [] ) ) { const attributes = {}; attributes[ key ] = /[\s\S]+/; diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index c773f619028..806402e6e77 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -1,48 +1,36 @@ :root { - --ck-widget-compatibility-base-background: var(--ck-color-widget-blurred-border) + --ck-html-object-embed-unfocused-outline-width: 1px; } -.ck .ck-widget { - &.ck-widget__compatibility { - padding: 20px 0 15px 0; - min-height: 20px; - border: 1px dotted hsl(15, 100%, 43%); +.ck-widget.html-object-embed { + display: inline-block; + font-size: var(--ck-font-size-base); + background-color: var(--ck-color-base-foreground); + padding: var(--ck-spacing-small); + /* Leave space for label */ + padding-top: calc(var(--ck-font-size-tiny) + var(--ck-spacing-large)); + min-width: calc(76px + var(--ck-spacing-standard)); - & .ck-widget__compatibility-type { - opacity: 0; - font-size: var(--ck-font-size-tiny); - padding: 0 5px; - background: var(--ck-widget-compatibility-base-background); - display: block; - position: absolute; - overflow: hidden; - z-index: var(--ck-z-default); - /* Place it in the middle of the outline */ - top: calc(-0.5 * var(--ck-widget-outline-thickness)); - right: min(10%, 30px); - transform: translateY(-50%); - } - - &.ck-widget_selected > .ck-widget__compatibility-type { - opacity: 1; - color: var(--ck-color-base-background); - background: var(--ck-color-focus-border); - } - - &:not(.ck-widget_selected):hover > .ck-widget__compatibility-type { - opacity: 1; - color: var(--ck-color-base-background); - background: var(--ck-color-widget-hover-border); - } + &:not(.ck-widget_selected):not(:hover) { + outline: var(--ck-html-object-embed-unfocused-outline-width) dashed var(--ck-color-widget-blurred-border); } - &.ck-widget__compatibility-inline { - display: inline-block; - min-width: 100px; + &::before { + position: absolute; + content: attr(data-html-object-embed-label); + top: 0; + left: var(--ck-spacing-standard); + background: hsl(0deg 0% 60%); + transition: background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve); + padding: calc(var(--ck-spacing-tiny) + var(--ck-html-object-embed-unfocused-outline-width)) var(--ck-spacing-small) var(--ck-spacing-tiny); + border-radius: 0 0 var(--ck-border-radius) var(--ck-border-radius); + color: var(--ck-color-base-background); + font-size: var(--ck-font-size-tiny); + font-family: var(--ck-font-face); } -} -.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget__compatibility.ck-widget_selected > .ck-widget__compatibility-type { - color: hsl(0,0%,60%); - background: var(--ck-widget-compatibility-base-background); + & .html-object-embed__content { + /* Disable user interation with embed content */ + pointer-events: none; + } } From 0af201057bd87705c35ab89bd81da8ed7c83be49 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Sun, 9 May 2021 11:53:25 +0200 Subject: [PATCH 118/217] Corrected audio example. --- .../ckeditor5-content-compatibility/tests/manual/objects.html | 2 +- .../ckeditor5-content-compatibility/tests/manual/objects.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.html b/packages/ckeditor5-content-compatibility/tests/manual/objects.html index d69e187ab83..c849f8a401b 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.html @@ -66,7 +66,7 @@

    -

    +

    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js index e8e88c41eb3..0926be61f06 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -52,7 +52,7 @@ class ExtendHTMLSupport extends Plugin { for ( const key of ( definition.attributes || [] ) ) { const attributes = {}; - attributes[ key ] = /[\s\S]+/; + attributes[ key ] = true; dataFilter.allowAttributes( { name: definition.name, attributes } ); } From 9f41cfb98ab319c896ca24fe21cde9f16a236b6d Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 09:44:00 +0200 Subject: [PATCH 119/217] Added support for object block elements. --- .../src/datafilter.js | 141 ++++++++++++------ .../src/dataschema.js | 5 +- .../theme/datafilter.css | 14 +- 3 files changed, 108 insertions(+), 52 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 1fff7576ae4..979af765d09 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -168,15 +168,15 @@ export default class DataFilter { * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ _registerElement( definition ) { - if ( definition.isInline ) { + if ( definition.isObject ) { + this._registerObjectElement( definition ); + } else if ( definition.isInline ) { this._registerInlineElement( definition ); } else if ( definition.isBlock ) { this._registerBlockElement( definition ); - } else if ( definition.isObject ) { - this._registerObjectElement( definition ); } else { /** - * Only a definition marked as inline or block can be allowed. + * Only a definition marked as inline, block or object can be allowed. * * @error data-filter-invalid-definition-type */ @@ -191,27 +191,48 @@ export default class DataFilter { /** * Registers object element and attribute converters for the given data schema definition. * + * This method will embed element inside a `htmlObjectEmbedBlock` or `htmlObjectEmbedInline` widget + * depending on {@link module:content-compatibility/dataschema~DataSchemaObjectElementDefinition#isBlock} + * property. + * * @private * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition */ _registerObjectElement( definition ) { - const schema = this.editor.model.schema; + const t = this.editor.t; + + if ( definition.isBlock ) { + this._registerObjectElementWidget( 'htmlObjectEmbedBlock', { + isObject: true, + isBlock: true, + allowWhere: '$block', + allowAttributes: [ 'value', 'view', 'htmlAttributes' ] + }, writer => { + return writer.createContainerElement( 'div', { + class: 'html-object-embed html-object-embed-block', + 'data-html-object-embed-label': t( 'HTML object' ) + } ); + } ); - // All object elements are represented by a single widget. - if ( !schema.isRegistered( 'htmlObjectEmbed' ) ) { - schema.register( 'htmlObjectEmbed', { + this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedBlock' ); + } else { + this._registerObjectElementWidget( 'htmlObjectEmbedInline', { isObject: true, isInline: true, allowWhere: '$text', allowAttributesOf: '$text', allowAttributes: [ 'value', 'view', 'htmlAttributes' ] + }, writer => { + return writer.createContainerElement( 'span', { + class: 'html-object-embed html-object-embed-inline', + 'data-html-object-embed-label': t( 'HTML object' ) + }, { + isAllowedInsideAttributeElement: true + } ); } ); - this._addHtmlObjectEmbedConversion(); + this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedInline' ); } - - this._addDisallowedAttributeConversion( definition ); - this._addObjectElementToElementConversion( definition ); } /** @@ -257,27 +278,42 @@ export default class DataFilter { this._addInlineElementConversion( definition ); } - _addHtmlObjectEmbedConversion() { - const editor = this.editor; - const conversion = editor.conversion; - const t = editor.t; + /** + * Registers widget used to embed object elements. + * + * You should provide `createViewContainer` function used to create widget wrapper element. + * + * this._registerObjectElementWidget( 'customWidgetModel', widgetSchema, writer => { + * return writer.createContainerElement( 'div', { class: 'widget' } ); + * } ); + * + * @private + * @param {String} widgetName + * @param {module:engine/model/schema~SchemaItemDefinition} widgetSchema + * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition + */ + _registerObjectElementWidget( widgetName, widgetSchema, createViewContainer ) { + const t = this.editor.t; + const schema = this.editor.model.schema; + const conversion = this.editor.conversion; + + if ( schema.isRegistered( widgetName ) ) { + return; + } + + schema.register( widgetName, widgetSchema ); conversion.for( 'dataDowncast' ).elementToElement( { - model: 'htmlObjectEmbed', + model: widgetName, view: createObjectViewElement } ); conversion.for( 'editingDowncast' ).elementToElement( { - model: 'htmlObjectEmbed', + model: widgetName, view: ( modelElement, conversionApi ) => { const { writer } = conversionApi; - const viewContainer = writer.createContainerElement( 'span', { - class: 'html-object-embed', - 'data-html-object-embed-label': t( 'HTML object' ) - }, { - isAllowedInsideAttributeElement: true - } ); + const viewContainer = createViewContainer( writer ); const viewElement = createObjectViewElement( modelElement, conversionApi ); writer.addClass( 'html-object-embed__content', viewElement ); @@ -291,6 +327,38 @@ export default class DataFilter { } ); } + /** + * Registers object element to element upcast conversion for the given data schema definition. + * + * This function will embed view element inside widget element provided as a second argument. + * + * @private + * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition + * @param {String} widgetName + */ + _addObjectElementToElementUpcastConversion( { view: viewName }, widgetName ) { + const conversion = this.editor.conversion; + + // Object element content will be hidden under `value` property and restored by + // `htmlObjectEmbedInline` downcast converters. + this.editor.data.registerRawContentMatcher( { + name: viewName + } ); + + conversion.for( 'upcast' ).elementToElement( { + view: viewName, + model: ( viewElement, conversionApi ) => { + const htmlAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); + + return conversionApi.writer.createElement( widgetName, { + value: viewElement.getCustomProperty( '$rawContent' ), + view: viewName, + ...( htmlAttributes && { htmlAttributes } ) + } ); + } + } ); + } + /** * Adds element to element converters for the given block element definition. * @@ -434,31 +502,6 @@ export default class DataFilter { } ); } - _addObjectElementToElementConversion( { view: viewName } ) { - const conversion = this.editor.conversion; - - // Object element content will be hidden under `value` property and restored by - // `htmlObjectEmbed` downcast converters. - this.editor.data.registerRawContentMatcher( { - name: viewName - } ); - - // We just need to add upcast conversion for specific view. Downcast is handled by - // `htmlObjectEmbed` itself. - conversion.for( 'upcast' ).elementToElement( { - view: viewName, - model: ( viewElement, conversionApi ) => { - const htmlAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); - - return conversionApi.writer.createElement( 'htmlObjectEmbed', { - value: viewElement.getCustomProperty( '$rawContent' ), - view: viewName, - ...( htmlAttributes && { htmlAttributes } ) - } ); - } - } ); - } - /** * Matches and consumes allowed view attributes. * diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index aeeedb027a1..a0806479495 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -256,7 +256,7 @@ export default class DataSchema { } /** - * Add new data schema definition describing inline element. + * Add new data schema definition describing object element. * * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition */ @@ -359,7 +359,7 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. - * @property {Boolean} isBlock Indicates that the definition describes block element. + * @property {Boolean} isInline Indicates that the definition descibes inline element. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ @@ -370,6 +370,7 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaObjectElementDefinition * @property {String} view Name of the view element. * @property {Boolean} isObject Indicates that the definition describes object element. + * @property {Boolean} [isBlock] Indicates that the definition describes block element. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerObjectElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index 806402e6e77..f242c70d7bf 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -3,7 +3,6 @@ } .ck-widget.html-object-embed { - display: inline-block; font-size: var(--ck-font-size-base); background-color: var(--ck-color-base-foreground); padding: var(--ck-spacing-small); @@ -11,6 +10,19 @@ padding-top: calc(var(--ck-font-size-tiny) + var(--ck-spacing-large)); min-width: calc(76px + var(--ck-spacing-standard)); + &.html-object-embed-block { + margin: 1em auto; + } + + /* Make space for label but it only collides in LTR languages */ + & .ck-widget__type-around .ck-widget__type-around__button.ck-widget__type-around__button_before { + margin-left: 50px; + } + + &.html-object-embed-inline { + display: inline-block; + } + &:not(.ck-widget_selected):not(:hover) { outline: var(--ck-html-object-embed-unfocused-outline-width) dashed var(--ck-color-widget-blurred-border); } From 4e11c308d1267fe30fdd210593afbe904a00b948 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 13:12:28 +0200 Subject: [PATCH 120/217] Small fixes, test coverage. --- .../src/datafilter.js | 7 +- .../tests/datafilter.js | 196 ++++++++++++++++++ 2 files changed, 201 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 979af765d09..5b2eca072b1 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -200,6 +200,7 @@ export default class DataFilter { */ _registerObjectElement( definition ) { const t = this.editor.t; + const label = t( 'HTML object' ); if ( definition.isBlock ) { this._registerObjectElementWidget( 'htmlObjectEmbedBlock', { @@ -210,7 +211,7 @@ export default class DataFilter { }, writer => { return writer.createContainerElement( 'div', { class: 'html-object-embed html-object-embed-block', - 'data-html-object-embed-label': t( 'HTML object' ) + 'data-html-object-embed-label': label } ); } ); @@ -225,7 +226,7 @@ export default class DataFilter { }, writer => { return writer.createContainerElement( 'span', { class: 'html-object-embed html-object-embed-inline', - 'data-html-object-embed-label': t( 'HTML object' ) + 'data-html-object-embed-label': label }, { isAllowedInsideAttributeElement: true } ); @@ -233,6 +234,8 @@ export default class DataFilter { this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedInline' ); } + + this._addDisallowedAttributeConversion( definition ); } /** diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index d391eecd298..8d786f65867 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -124,6 +124,202 @@ describe( 'DataFilter', () => { } } ); + describe( 'object', () => { + it( 'should allow element', () => { + dataFilter.allowElement( { name: 'input' } ); + + editor.setData( '

    ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '' + + '' + + '' + + '' + ); + + expect( editor.getData() ).to.equal( '

    ' ); + } ); + + it( 'should allow element content', () => { + dataFilter.allowElement( { name: 'video' } ); + + editor.setData( '

    ' + + '

    ' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '' + + ' Your browser does not support the video tag."' + + ' view="video">' + + '' + + '' + ); + + expect( editor.getData() ).to.equal( '

    ' + + '

    ' + ); + } ); + + it( 'should recognize block elements', () => { + dataSchema.registerObjectElement( { + model: 'htmlXyz', + view: 'xyz', + isBlock: true + } ); + + dataFilter.allowElement( { name: 'xyz' } ); + + editor.setData( 'foobar' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( + '' + + '' + ); + + expect( editor.getData() ).to.equal( 'foobar' ); + } ); + + it( 'should allow attributes', () => { + dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowAttributes( { name: 'input', attributes: { type: true } } ); + + editor.setData( '

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '', + attributes: { + 1: { + attributes: { + type: 'text' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

    ' ); + } ); + + it( 'should allow attributes (styles)', () => { + dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowAttributes( { name: 'input', styles: { color: 'red' } } ); + + editor.setData( '

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '', + attributes: { + 1: { + styles: { + color: 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

    ' ); + } ); + + it( 'should allow attributes (classes)', () => { + dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowAttributes( { name: 'input', classes: [ 'foobar' ] } ); + + editor.setData( '

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '', + attributes: { + 1: { + classes: [ 'foobar' ] + } + } + } ); + + expect( editor.getData() ).to.equal( '

    ' ); + } ); + + it( 'should disallow attributes', () => { + dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowAttributes( { name: 'input', attributes: { type: true } } ); + dataFilter.disallowAttributes( { name: 'input', attributes: { type: 'hidden' } } ); + + editor.setData( '

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '' + + '' + + '', + attributes: { + 1: { + attributes: { + type: 'text' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

    ' ); + } ); + + it( 'should disallow attributes (styles)', () => { + dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowAttributes( { name: 'input', styles: { color: /^(red|blue)$/ } } ); + dataFilter.disallowAttributes( { name: 'input', styles: { color: 'red' } } ); + + editor.setData( '

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '' + + '' + + '', + attributes: { + 1: { + styles: { + color: 'blue' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

    ' ); + } ); + + it( 'should disallow attributes (classes)', () => { + dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowAttributes( { name: 'input', classes: [ 'foo', 'bar' ] } ); + dataFilter.disallowAttributes( { name: 'input', classes: [ 'bar' ] } ); + + editor.setData( '

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '' + + '' + + '', + attributes: {} + } ); + + expect( editor.getData() ).to.equal( '

    ' ); + } ); + + it( 'should register embed widget only once', () => { + dataFilter.allowElement( { name: 'video' } ); + + expect( () => { + dataFilter.allowElement( { name: 'audio' } ); + } ).to.not.throw(); + } ); + } ); + describe( 'block', () => { it( 'should allow element', () => { dataFilter.allowElement( { name: 'article' } ); From 35942349633cc7b172ea265b792d86e7eae6e158 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 14:12:37 +0200 Subject: [PATCH 121/217] Added block object manual test. --- .../tests/manual/objects.html | 2 ++ .../tests/manual/objects.js | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.html b/packages/ckeditor5-content-compatibility/tests/manual/objects.html index c849f8a401b..7567ff7607f 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.html @@ -69,4 +69,6 @@

    + +

    Hello, world!

    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js index 0926be61f06..9989ef3653f 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -28,7 +28,9 @@ class ExtendHTMLSupport extends Plugin { } init() { - const { dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); + const { dataFilter, dataSchema } = this.editor.plugins.get( GeneralHtmlSupport ); + + dataSchema.registerObjectElement( { model: 'htmlXyz', view: 'xyz', isBlock: true } ); const definitions = [ { name: 'object', attributes: [ 'classid', 'codebase' ] }, @@ -44,7 +46,8 @@ class ExtendHTMLSupport extends Plugin { { name: 'audio', attributes: [ 'controls' ] }, { name: 'source', attributes: [ 'src', 'type' ] }, { name: 'button' }, - { name: 'label' } + { name: 'label' }, + { name: 'xyz', attributes: [ 'data-foo' ] } ]; for ( const definition of definitions ) { From db8824f3f85f15b8e1388db827b4a05be86f368f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 14:24:52 +0200 Subject: [PATCH 122/217] Respect UI language. --- .../src/datafilter.js | 6 ++++-- .../theme/datafilter.css | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 5b2eca072b1..7de492110fe 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -211,7 +211,8 @@ export default class DataFilter { }, writer => { return writer.createContainerElement( 'div', { class: 'html-object-embed html-object-embed-block', - 'data-html-object-embed-label': label + 'data-html-object-embed-label': label, + dir: this.editor.locale.uiLanguageDirection } ); } ); @@ -226,7 +227,8 @@ export default class DataFilter { }, writer => { return writer.createContainerElement( 'span', { class: 'html-object-embed html-object-embed-inline', - 'data-html-object-embed-label': label + 'data-html-object-embed-label': label, + dir: this.editor.locale.uiLanguageDirection }, { isAllowedInsideAttributeElement: true } ); diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index f242c70d7bf..e251a60fbc6 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -14,11 +14,6 @@ margin: 1em auto; } - /* Make space for label but it only collides in LTR languages */ - & .ck-widget__type-around .ck-widget__type-around__button.ck-widget__type-around__button_before { - margin-left: 50px; - } - &.html-object-embed-inline { display: inline-block; } @@ -41,6 +36,16 @@ font-family: var(--ck-font-face); } + &[dir="rtl"]::before { + left: auto; + right: var(--ck-spacing-standard); + } + + /* Make space for label but it only collides in LTR languages */ + &[dir="ltr"] .ck-widget__type-around .ck-widget__type-around__button.ck-widget__type-around__button_before { + margin-left: 50px; + } + & .html-object-embed__content { /* Disable user interation with embed content */ pointer-events: none; From bc48865c243b868f929da69acc3a79f172978f35 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 14:50:06 +0200 Subject: [PATCH 123/217] Refactoring. --- .../src/datafilter.js | 89 +++++++++++-------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 7de492110fe..ababc393e24 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -199,42 +199,10 @@ export default class DataFilter { * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition */ _registerObjectElement( definition ) { - const t = this.editor.t; - const label = t( 'HTML object' ); - if ( definition.isBlock ) { - this._registerObjectElementWidget( 'htmlObjectEmbedBlock', { - isObject: true, - isBlock: true, - allowWhere: '$block', - allowAttributes: [ 'value', 'view', 'htmlAttributes' ] - }, writer => { - return writer.createContainerElement( 'div', { - class: 'html-object-embed html-object-embed-block', - 'data-html-object-embed-label': label, - dir: this.editor.locale.uiLanguageDirection - } ); - } ); - - this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedBlock' ); + this._registerBlockObjectElement( definition ); } else { - this._registerObjectElementWidget( 'htmlObjectEmbedInline', { - isObject: true, - isInline: true, - allowWhere: '$text', - allowAttributesOf: '$text', - allowAttributes: [ 'value', 'view', 'htmlAttributes' ] - }, writer => { - return writer.createContainerElement( 'span', { - class: 'html-object-embed html-object-embed-inline', - 'data-html-object-embed-label': label, - dir: this.editor.locale.uiLanguageDirection - }, { - isAllowedInsideAttributeElement: true - } ); - } ); - - this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedInline' ); + this._registerInlineObjectElement( definition ); } this._addDisallowedAttributeConversion( definition ); @@ -283,6 +251,55 @@ export default class DataFilter { this._addInlineElementConversion( definition ); } + /** + * Registers block object element and attribute converters for the given data schema definition. + * + * @private + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + */ + _registerBlockObjectElement( definition ) { + this._registerObjectElementWidget( 'htmlObjectEmbedBlock', { + isObject: true, + isBlock: true, + allowWhere: '$block', + allowAttributes: [ 'value', 'view', 'htmlAttributes' ] + }, writer => { + return writer.createContainerElement( 'div', { + class: 'html-object-embed html-object-embed-block', + 'data-html-object-embed-label': this.editor.t( 'HTML object' ), + dir: this.editor.locale.uiLanguageDirection + } ); + } ); + + this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedBlock' ); + } + + /** + * Registers inline object element and attribute converters for the given data schema definition. + * + * @private + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + */ + _registerInlineObjectElement( definition ) { + this._registerObjectElementWidget( 'htmlObjectEmbedInline', { + isObject: true, + isInline: true, + allowWhere: '$text', + allowAttributesOf: '$text', + allowAttributes: [ 'value', 'view', 'htmlAttributes' ] + }, writer => { + return writer.createContainerElement( 'span', { + class: 'html-object-embed html-object-embed-inline', + 'data-html-object-embed-label': this.editor.t( 'HTML object' ), + dir: this.editor.locale.uiLanguageDirection + }, { + isAllowedInsideAttributeElement: true + } ); + } ); + + this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedInline' ); + } + /** * Registers widget used to embed object elements. * @@ -335,8 +352,6 @@ export default class DataFilter { /** * Registers object element to element upcast conversion for the given data schema definition. * - * This function will embed view element inside widget element provided as a second argument. - * * @private * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition * @param {String} widgetName @@ -344,8 +359,6 @@ export default class DataFilter { _addObjectElementToElementUpcastConversion( { view: viewName }, widgetName ) { const conversion = this.editor.conversion; - // Object element content will be hidden under `value` property and restored by - // `htmlObjectEmbedInline` downcast converters. this.editor.data.registerRawContentMatcher( { name: viewName } ); From 4ac3c04a2024b72c768e019e259b06cca9711803 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 14:52:11 +0200 Subject: [PATCH 124/217] Sanity check for dataschema API. --- .../tests/dataschema.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 49523214515..a31c01b039d 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -26,6 +26,20 @@ describe( 'DataSchema', () => { return editor.destroy(); } ); + describe( 'registerObjectElement()', () => { + it( 'should register proper definition', () => { + dataSchema.registerObjectElement( { model: 'htmlDef', view: 'def' } ); + + const result = dataSchema.getDefinitionsForView( 'def' ); + + expect( Array.from( result ) ).to.deep.equal( [ { + model: 'htmlDef', + view: 'def', + isObject: true + } ] ); + } ); + } ); + describe( 'registerInlineElement()', () => { it( 'should register proper definition', () => { dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def' } ); From bb7ed346ce0dc4d29425723c1d1787c1fadac383 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 15:08:00 +0200 Subject: [PATCH 125/217] Docs update. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 7 ++++--- packages/ckeditor5-content-compatibility/src/dataschema.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index ababc393e24..d2aa258f237 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -255,7 +255,7 @@ export default class DataFilter { * Registers block object element and attribute converters for the given data schema definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition */ _registerBlockObjectElement( definition ) { this._registerObjectElementWidget( 'htmlObjectEmbedBlock', { @@ -278,7 +278,7 @@ export default class DataFilter { * Registers inline object element and attribute converters for the given data schema definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition */ _registerInlineObjectElement( definition ) { this._registerObjectElementWidget( 'htmlObjectEmbedInline', { @@ -312,13 +312,14 @@ export default class DataFilter { * @private * @param {String} widgetName * @param {module:engine/model/schema~SchemaItemDefinition} widgetSchema - * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition + * @param {Function} createViewContainer */ _registerObjectElementWidget( widgetName, widgetSchema, createViewContainer ) { const t = this.editor.t; const schema = this.editor.model.schema; const conversion = this.editor.conversion; + // Object element widget should be registered only once, as it's used to embed different views. if ( schema.isRegistered( widgetName ) ) { return; } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index a0806479495..20af84cea2f 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -359,7 +359,7 @@ function testViewName( pattern, viewName ) { * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. - * @property {Boolean} isInline Indicates that the definition descibes inline element. + * @property {Boolean} isBlock Indicates that the definition describes block element. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ From d715d777b7a3bef904b634e097a407a0375ecefc Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 15:38:21 +0200 Subject: [PATCH 126/217] Refactoring. --- .../src/datafilter.js | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index d2aa258f237..7960c1915a0 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -261,15 +261,10 @@ export default class DataFilter { this._registerObjectElementWidget( 'htmlObjectEmbedBlock', { isObject: true, isBlock: true, - allowWhere: '$block', - allowAttributes: [ 'value', 'view', 'htmlAttributes' ] - }, writer => { - return writer.createContainerElement( 'div', { - class: 'html-object-embed html-object-embed-block', - 'data-html-object-embed-label': this.editor.t( 'HTML object' ), - dir: this.editor.locale.uiLanguageDirection - } ); - } ); + allowWhere: '$block' + }, writer => writer.createContainerElement( 'div', + { class: 'html-object-embed-block' } ) + ); this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedBlock' ); } @@ -285,17 +280,11 @@ export default class DataFilter { isObject: true, isInline: true, allowWhere: '$text', - allowAttributesOf: '$text', - allowAttributes: [ 'value', 'view', 'htmlAttributes' ] - }, writer => { - return writer.createContainerElement( 'span', { - class: 'html-object-embed html-object-embed-inline', - 'data-html-object-embed-label': this.editor.t( 'HTML object' ), - dir: this.editor.locale.uiLanguageDirection - }, { - isAllowedInsideAttributeElement: true - } ); - } ); + allowAttributesOf: '$text' + }, writer => writer.createContainerElement( 'span', + { class: 'html-object-embed-inline' }, + { isAllowedInsideAttributeElement: true } ) + ); this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedInline' ); } @@ -315,7 +304,6 @@ export default class DataFilter { * @param {Function} createViewContainer */ _registerObjectElementWidget( widgetName, widgetSchema, createViewContainer ) { - const t = this.editor.t; const schema = this.editor.model.schema; const conversion = this.editor.conversion; @@ -324,7 +312,11 @@ export default class DataFilter { return; } - schema.register( widgetName, widgetSchema ); + schema.register( widgetName, { + // Extend with attributes required by conversion. + allowAttributes: [ 'value', 'view', 'htmlAttributes' ], + ...widgetSchema + } ); conversion.for( 'dataDowncast' ).elementToElement( { model: widgetName, @@ -335,17 +327,24 @@ export default class DataFilter { model: widgetName, view: ( modelElement, conversionApi ) => { const { writer } = conversionApi; + const widgetLabel = this.editor.t( 'HTML object' ); + // Widget cannot be a raw element because the widget system would not be able + // to add its UI to it. Thus, we need separate view container. const viewContainer = createViewContainer( writer ); + // Add required attributes here, so we don't have to duplicate this logic between + // #_registerInlineObjectElement() and #_registerBlockObjectElement() methods. + writer.addClass( 'html-object-embed', viewContainer ); + writer.setAttribute( 'data-html-object-embed-label', widgetLabel, viewContainer ); + writer.setAttribute( 'dir', this.editor.locale.uiLanguageDirection, viewContainer ); + const viewElement = createObjectViewElement( modelElement, conversionApi ); writer.addClass( 'html-object-embed__content', viewElement ); writer.insert( writer.createPositionAt( viewContainer, 0 ), viewElement ); - return toWidget( viewContainer, writer, { - widgetLabel: t( 'HTML object' ) - } ); + return toWidget( viewContainer, writer, { widgetLabel } ); } } ); } From 6510ebab1ec855c5b1fb43691b938f0d3bc03f1b Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 10 May 2021 15:41:27 +0200 Subject: [PATCH 127/217] Reverted unnecessary docs change. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 7960c1915a0..94df33ba53e 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -405,10 +405,10 @@ export default class DataFilter { } /** - * Adds attribute converters for the given element definition. + * Adds attribute converters for the given block element definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition */ _addBlockElementAttributeConversion( { model: modelName, view: viewName } ) { const conversion = this.editor.conversion; From a5b013dbb0ad80054ab195be307431b5e30afb12 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 11 May 2021 09:05:56 +0200 Subject: [PATCH 128/217] Fixed manual test. --- .../tests/manual/objects.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.html b/packages/ckeditor5-content-compatibility/tests/manual/objects.html index 7567ff7607f..fdef8f3855a 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.html @@ -7,11 +7,11 @@ - - + +

    -

    +

    From c019ad93e8e93f8ce932d170ab783e2ec77e96bc Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 11 May 2021 10:54:57 +0200 Subject: [PATCH 129/217] Ensure that widget label is not changing by basic styles. --- .../package.json | 1 + .../tests/manual/objects.js | 21 ++++++++++++++++--- .../theme/datafilter.css | 2 ++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index 352c286da55..396610351f9 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -26,6 +26,7 @@ "@ckeditor/ckeditor5-engine": "^27.1.0", "@ckeditor/ckeditor5-essentials": "^27.1.0", "@ckeditor/ckeditor5-font": "^27.1.0", + "@ckeditor/ckeditor5-highlight": "^27.1.0", "@ckeditor/ckeditor5-link": "^27.1.0", "@ckeditor/ckeditor5-list": "^27.1.0", "@ckeditor/ckeditor5-paragraph": "^27.1.0", diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js index 9989ef3653f..8effa30f8b3 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -11,11 +11,16 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; +import Subscript from '@ckeditor/ckeditor5-basic-styles/src/subscript'; +import Superscript from '@ckeditor/ckeditor5-basic-styles/src/superscript'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; import List from '@ckeditor/ckeditor5-list/src/list'; import Link from '@ckeditor/ckeditor5-link/src/link'; import Table from '@ckeditor/ckeditor5-table/src/table'; +import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; import GeneralHtmlSupport from '../../src/generalhtmlsupport'; @@ -66,21 +71,31 @@ class ExtendHTMLSupport extends Plugin { ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ - Table, - Link, BlockQuote, Bold, + Code, Essentials, ExtendHTMLSupport, + Highlight, Italic, + Link, List, Paragraph, - Strikethrough + Strikethrough, + Subscript, + Superscript, + Table, + Underline ], toolbar: [ 'bold', 'italic', 'strikethrough', + 'underline', + 'code', + 'subscript', + 'superscript', + 'highlight', '|', 'numberedList', 'bulletedList', diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index e251a60fbc6..5a2a0b913f8 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -23,6 +23,8 @@ } &::before { + font-weight: normal; + font-style: normal; position: absolute; content: attr(data-html-object-embed-label); top: 0; From 637ccb8fdff0370d8f4c3ffa40740bb91a2499af Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 12 May 2021 09:44:11 +0200 Subject: [PATCH 130/217] Simplified implementation to use original definition model. --- .../src/datafilter.js | 152 +++++----------- .../src/dataschema.js | 172 +++++++++++------- .../tests/datafilter.js | 43 ++--- .../tests/dataschema.js | 14 -- .../tests/manual/objects.js | 8 +- .../theme/datafilter.css | 17 +- 6 files changed, 182 insertions(+), 224 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 94df33ba53e..e0319b05d93 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -191,20 +191,17 @@ export default class DataFilter { /** * Registers object element and attribute converters for the given data schema definition. * - * This method will embed element inside a `htmlObjectEmbedBlock` or `htmlObjectEmbedInline` widget - * depending on {@link module:content-compatibility/dataschema~DataSchemaObjectElementDefinition#isBlock} - * property. - * * @private - * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ _registerObjectElement( definition ) { - if ( definition.isBlock ) { - this._registerBlockObjectElement( definition ); - } else { - this._registerInlineObjectElement( definition ); + const schema = this.editor.model.schema; + + if ( !schema.isRegistered( definition.model ) ) { + schema.register( definition.model, definition.modelSchema ); } + this._addObjectElementConversion( definition ); this._addDisallowedAttributeConversion( definition ); } @@ -252,94 +249,62 @@ export default class DataFilter { } /** - * Registers block object element and attribute converters for the given data schema definition. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition - */ - _registerBlockObjectElement( definition ) { - this._registerObjectElementWidget( 'htmlObjectEmbedBlock', { - isObject: true, - isBlock: true, - allowWhere: '$block' - }, writer => writer.createContainerElement( 'div', - { class: 'html-object-embed-block' } ) - ); - - this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedBlock' ); - } - - /** - * Registers inline object element and attribute converters for the given data schema definition. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition - */ - _registerInlineObjectElement( definition ) { - this._registerObjectElementWidget( 'htmlObjectEmbedInline', { - isObject: true, - isInline: true, - allowWhere: '$text', - allowAttributesOf: '$text' - }, writer => writer.createContainerElement( 'span', - { class: 'html-object-embed-inline' }, - { isAllowedInsideAttributeElement: true } ) - ); - - this._addObjectElementToElementUpcastConversion( definition, 'htmlObjectEmbedInline' ); - } - - /** - * Registers widget used to embed object elements. - * - * You should provide `createViewContainer` function used to create widget wrapper element. - * - * this._registerObjectElementWidget( 'customWidgetModel', widgetSchema, writer => { - * return writer.createContainerElement( 'div', { class: 'widget' } ); - * } ); + * Adds converters for the given data schema definition marked as + * {@link module:content-compatibility/dataschema~DataSchemaDefinition#isObject isObject}. * * @private - * @param {String} widgetName - * @param {module:engine/model/schema~SchemaItemDefinition} widgetSchema - * @param {Function} createViewContainer + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ - _registerObjectElementWidget( widgetName, widgetSchema, createViewContainer ) { - const schema = this.editor.model.schema; + _addObjectElementConversion( definition ) { + const { view: viewName, model: modelName } = definition; const conversion = this.editor.conversion; - // Object element widget should be registered only once, as it's used to embed different views. - if ( schema.isRegistered( widgetName ) ) { + if ( !viewName ) { return; } - schema.register( widgetName, { - // Extend with attributes required by conversion. - allowAttributes: [ 'value', 'view', 'htmlAttributes' ], - ...widgetSchema + // Store element content in special `$rawContent` custom property to + // avoid editor's data filtering mechanism. + this.editor.data.registerRawContentMatcher( { + name: viewName + } ); + + conversion.for( 'upcast' ).elementToElement( { + view: viewName, + model: ( viewElement, conversionApi ) => { + const htmlAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); + + // Let's keep element HTML and its attributes, so we can rebuild element in downcast conversions. + return conversionApi.writer.createElement( modelName, { + value: viewElement.getCustomProperty( '$rawContent' ), + ...( htmlAttributes && { htmlAttributes } ) + } ); + } } ); conversion.for( 'dataDowncast' ).elementToElement( { - model: widgetName, - view: createObjectViewElement + model: modelName, + view: ( modelElement, { writer } ) => { + return createObjectViewElement( viewName, modelElement, writer ); + } } ); conversion.for( 'editingDowncast' ).elementToElement( { - model: widgetName, - view: ( modelElement, conversionApi ) => { - const { writer } = conversionApi; + model: modelName, + view: ( modelElement, { writer } ) => { const widgetLabel = this.editor.t( 'HTML object' ); // Widget cannot be a raw element because the widget system would not be able // to add its UI to it. Thus, we need separate view container. - const viewContainer = createViewContainer( writer ); - - // Add required attributes here, so we don't have to duplicate this logic between - // #_registerInlineObjectElement() and #_registerBlockObjectElement() methods. - writer.addClass( 'html-object-embed', viewContainer ); - writer.setAttribute( 'data-html-object-embed-label', widgetLabel, viewContainer ); - writer.setAttribute( 'dir', this.editor.locale.uiLanguageDirection, viewContainer ); + const viewContainer = writer.createContainerElement( definition.isInline ? 'span' : 'div', { + class: 'html-object-embed', + 'data-html-object-embed-label': widgetLabel, + dir: this.editor.locale.uiLanguageDirection + }, { + isAllowedInsideAttributeElement: definition.isInline + } ); - const viewElement = createObjectViewElement( modelElement, conversionApi ); + const viewElement = createObjectViewElement( viewName, modelElement, writer ); writer.addClass( 'html-object-embed__content', viewElement ); writer.insert( writer.createPositionAt( viewContainer, 0 ), viewElement ); @@ -349,34 +314,6 @@ export default class DataFilter { } ); } - /** - * Registers object element to element upcast conversion for the given data schema definition. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition - * @param {String} widgetName - */ - _addObjectElementToElementUpcastConversion( { view: viewName }, widgetName ) { - const conversion = this.editor.conversion; - - this.editor.data.registerRawContentMatcher( { - name: viewName - } ); - - conversion.for( 'upcast' ).elementToElement( { - view: viewName, - model: ( viewElement, conversionApi ) => { - const htmlAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); - - return conversionApi.writer.createElement( widgetName, { - value: viewElement.getCustomProperty( '$rawContent' ), - view: viewName, - ...( htmlAttributes && { htmlAttributes } ) - } ); - } - } ); - } - /** * Adds element to element converters for the given block element definition. * @@ -609,8 +546,7 @@ function setViewElementAttributes( writer, viewAttributes, viewElement ) { // @param {module:engine/model/element~Element} modelElement // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi // @returns {module:engine/view/element~Element} viewElement -function createObjectViewElement( modelElement, { writer } ) { - const viewName = modelElement.getAttribute( 'view' ); +function createObjectViewElement( viewName, modelElement, writer ) { const viewAttributes = modelElement.getAttribute( 'htmlAttributes' ); const viewContent = modelElement.getAttribute( 'value' ); diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 20af84cea2f..eb179b35b4f 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -49,6 +49,15 @@ export default class DataSchema { this._definitions = new Map(); // Block elements. + this.registerBlockElement( { + model: '$htmlBlock', + modelSchema: { + allowChildren: '$block', + allowIn: [ '$root', '$htmlBlock' ], + isBlock: true + } + } ); + this.registerBlockElement( { model: 'paragraph', view: 'p' @@ -64,15 +73,6 @@ export default class DataSchema { view: 'li' } ); - this.registerBlockElement( { - model: '$htmlBlock', - modelSchema: { - allowChildren: '$block', - allowIn: [ '$root', '$htmlBlock' ], - isBlock: true - } - } ); - this.registerBlockElement( { view: 'article', model: 'htmlArticle', @@ -150,44 +150,15 @@ export default class DataSchema { } } ); - this.registerObjectElement( { - view: 'object', - model: 'htmlObject' - } ); - - this.registerObjectElement( { - view: 'iframe', - model: 'htmlIframe' - } ); - - this.registerObjectElement( { - view: 'input', - model: 'htmlInput' - } ); - - this.registerObjectElement( { - view: 'button', - model: 'htmlButton' - } ); - - this.registerObjectElement( { - view: 'textarea', - model: 'htmlTextarea' - } ); - - this.registerObjectElement( { - view: 'select', - model: 'htmlSelect' - } ); - - this.registerObjectElement( { - view: 'video', - model: 'htmlVideo' - } ); - - this.registerObjectElement( { - view: 'audio', - model: 'htmlAudio' + // Block objects. + this.registerBlockElement( { + model: '$htmlObjectBlock', + isObject: true, + modelSchema: { + isObject: true, + isBlock: true, + allowWhere: '$block' + } } ); // Inline elements. @@ -235,6 +206,90 @@ export default class DataSchema { copyOnEnter: true } } ); + + // Inline objects + this.registerInlineElement( { + model: '$htmlObjectInline', + isObject: true, + modelSchema: { + isObject: true, + isInline: true, + allowWhere: '$text', + allowAttributesOf: '$text' + } + } ); + + this.registerInlineElement( { + view: 'object', + model: 'htmlObject', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); + + this.registerInlineElement( { + view: 'iframe', + model: 'htmlIframe', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); + + this.registerInlineElement( { + view: 'input', + model: 'htmlInput', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); + + this.registerInlineElement( { + view: 'button', + model: 'htmlButton', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); + + this.registerInlineElement( { + view: 'textarea', + model: 'htmlTextarea', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); + + this.registerInlineElement( { + view: 'select', + model: 'htmlSelect', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); + + this.registerInlineElement( { + view: 'video', + model: 'htmlVideo', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); + + this.registerInlineElement( { + view: 'audio', + model: 'htmlAudio', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectInline' + } + } ); } /** @@ -255,15 +310,6 @@ export default class DataSchema { this._definitions.set( definition.model, { ...definition, isInline: true } ); } - /** - * Add new data schema definition describing object element. - * - * @param {module:content-compatibility/dataschema~DataSchemaObjectElementDefinition} definition - */ - registerObjectElement( definition ) { - this._definitions.set( definition.model, { ...definition, isObject: true } ); - } - /** * Returns all definitions matching the given view name. * @@ -351,6 +397,8 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition * @property {String} model Name of the model. + * @property {Boolean} [isObject] Indicates that the definition describes object element. + * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. */ /** @@ -358,23 +406,11 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. - * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. * @property {Boolean} isBlock Indicates that the definition describes block element. * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. * @extends module:content-compatibility/dataschema~DataSchemaDefinition */ -/** - * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for object elements. - * - * @typedef {Object} module:content-compatibility/dataschema~DataSchemaObjectElementDefinition - * @property {String} view Name of the view element. - * @property {Boolean} isObject Indicates that the definition describes object element. - * @property {Boolean} [isBlock] Indicates that the definition describes block element. - * Set by {@link module:content-compatibility/dataschema~DataSchema#registerObjectElement} method. - * @extends module:content-compatibility/dataschema~DataSchemaDefinition - */ - /** * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for inline elements. * diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 8d786f65867..766295c05e6 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -131,12 +131,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - '' + - '' + - '' + '' ); expect( editor.getData() ).to.equal( '

    ' ); @@ -152,10 +147,8 @@ describe( 'DataFilter', () => { expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '' + - ' Your browser does not support the video tag."' + - ' view="video">' + - '' + + '' + + ' Your browser does not support the video tag.">' + '' ); @@ -167,10 +160,13 @@ describe( 'DataFilter', () => { } ); it( 'should recognize block elements', () => { - dataSchema.registerObjectElement( { + dataSchema.registerBlockElement( { model: 'htmlXyz', view: 'xyz', - isBlock: true + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectBlock' + } } ); dataFilter.allowElement( { name: 'xyz' } ); @@ -178,10 +174,7 @@ describe( 'DataFilter', () => { editor.setData( 'foobar' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + - '' + '' ); expect( editor.getData() ).to.equal( 'foobar' ); @@ -194,7 +187,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '', + data: '', attributes: { 1: { attributes: { @@ -214,7 +207,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '', + data: '', attributes: { 1: { styles: { @@ -234,7 +227,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '', + data: '', attributes: { 1: { classes: [ 'foobar' ] @@ -254,8 +247,8 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '' + - '' + + '' + + '' + '', attributes: { 1: { @@ -278,8 +271,8 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '' + - '' + + '' + + '' + '', attributes: { 1: { @@ -302,8 +295,8 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '' + - '' + + '' + + '' + '', attributes: {} } ); diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index a31c01b039d..49523214515 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -26,20 +26,6 @@ describe( 'DataSchema', () => { return editor.destroy(); } ); - describe( 'registerObjectElement()', () => { - it( 'should register proper definition', () => { - dataSchema.registerObjectElement( { model: 'htmlDef', view: 'def' } ); - - const result = dataSchema.getDefinitionsForView( 'def' ); - - expect( Array.from( result ) ).to.deep.equal( [ { - model: 'htmlDef', - view: 'def', - isObject: true - } ] ); - } ); - } ); - describe( 'registerInlineElement()', () => { it( 'should register proper definition', () => { dataSchema.registerInlineElement( { model: 'htmlDef', view: 'def' } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js index 8effa30f8b3..04766df7129 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -35,7 +35,13 @@ class ExtendHTMLSupport extends Plugin { init() { const { dataFilter, dataSchema } = this.editor.plugins.get( GeneralHtmlSupport ); - dataSchema.registerObjectElement( { model: 'htmlXyz', view: 'xyz', isBlock: true } ); + dataSchema.registerBlockElement( { model: 'htmlXyz', + view: 'xyz', + isObject: true, + modelSchema: { + inheritAllFrom: '$htmlObjectBlock' + } + } ); const definitions = [ { name: 'object', attributes: [ 'classid', 'codebase' ] }, diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index 5a2a0b913f8..647e4f7cccc 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -10,14 +10,6 @@ padding-top: calc(var(--ck-font-size-tiny) + var(--ck-spacing-large)); min-width: calc(76px + var(--ck-spacing-standard)); - &.html-object-embed-block { - margin: 1em auto; - } - - &.html-object-embed-inline { - display: inline-block; - } - &:not(.ck-widget_selected):not(:hover) { outline: var(--ck-html-object-embed-unfocused-outline-width) dashed var(--ck-color-widget-blurred-border); } @@ -53,3 +45,12 @@ pointer-events: none; } } + +div.ck-widget.html-object-embed { + margin: 1em auto; +} + +span.ck-widget.html-object-embed { + display: inline-block; +} + From fd1c79def2d0d55a0d748e40f52e3b019c4a5819 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 12 May 2021 11:12:45 +0200 Subject: [PATCH 131/217] Transformed data filter and data schema into plugins. --- .../src/datafilter.js | 34 +++++++++----- .../src/dataschema.js | 25 ++++++++++- .../src/generalhtmlsupport.js | 31 +++---------- .../tests/datafilter.js | 45 ++++++++++--------- .../tests/manual/objects.js | 3 +- 5 files changed, 79 insertions(+), 59 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e0319b05d93..e7c0b624fef 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -7,10 +7,12 @@ * @module content-compatibility/datafilter */ +import { Plugin } from 'ckeditor5/src/core'; import { Matcher } from 'ckeditor5/src/engine'; import { priorities, CKEditorError } from 'ckeditor5/src/utils'; -import { toWidget } from 'ckeditor5/src/widget'; +import { toWidget, Widget } from 'ckeditor5/src/widget'; import { cloneDeep } from 'lodash-es'; +import DataSchema from './dataschema'; import '../theme/datafilter.css'; @@ -40,10 +42,12 @@ import '../theme/datafilter.css'; * color: /[\s\S]+/ * } * } ); + * + * @extends module:core/plugin~Plugin */ -export default class DataFilter { - constructor( editor, dataSchema ) { - this.editor = editor; +export default class DataFilter extends Plugin { + constructor( editor ) { + super( editor ); /** * An instance of the {@link module:content-compatibility/dataschema~DataSchema}. @@ -52,7 +56,7 @@ export default class DataFilter { * @private * @member {module:content-compatibility/dataschema~DataSchema} #_dataSchema */ - this._dataSchema = dataSchema; + this._dataSchema = editor.plugins.get( 'DataSchema' ); /** * {@link module:engine/view/matcher~Matcher Matcher} instance describing rules upon which @@ -94,6 +98,20 @@ export default class DataFilter { this._registerElementsAfterInit(); } + /** + * @inheritDoc + */ + static get pluginName() { + return 'DataFilter'; + } + + /** + * @inheritDoc + */ + static get requires() { + return [ DataSchema, Widget ]; + } + /** * Allow the given element in the editor context. * @@ -195,11 +213,7 @@ export default class DataFilter { * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ _registerObjectElement( definition ) { - const schema = this.editor.model.schema; - - if ( !schema.isRegistered( definition.model ) ) { - schema.register( definition.model, definition.modelSchema ); - } + this.editor.model.schema.register( definition.model, definition.modelSchema ); this._addObjectElementConversion( definition ); this._addDisallowedAttributeConversion( definition ); diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index eb179b35b4f..f1757deb0fe 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -7,6 +7,7 @@ * @module content-compatibility/dataschema */ +import { Plugin } from 'ckeditor5/src/core'; import { toArray } from 'ckeditor5/src/utils'; /** @@ -36,9 +37,13 @@ import { toArray } from 'ckeditor5/src/utils'; * copyOnEnter: true * } * } ); + * + * @extends module:core/plugin~Plugin */ -export default class DataSchema { - constructor() { +export default class DataSchema extends Plugin { + constructor( editor ) { + super( editor ); + /** * A map of registered data schema definitions via {@link #register} method. * @@ -48,6 +53,22 @@ export default class DataSchema { */ this._definitions = new Map(); + this._registerDefaultDefinitions(); + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'DataSchema'; + } + + /** + * Registers default data schema definitions. + * + * @private + */ + _registerDefaultDefinitions() { // Block elements. this.registerBlockElement( { model: '$htmlBlock', diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index 0954f2789aa..ce614f769e0 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -8,9 +8,8 @@ */ import { Plugin } from 'ckeditor5/src/core'; - -import DataSchema from './dataschema'; import DataFilter from './datafilter'; +import DataSchema from './dataschema'; /** * The General HTML Support feature. @@ -22,32 +21,16 @@ import DataFilter from './datafilter'; */ export default class GeneralHtmlSupport extends Plugin { /** - * @param {module:core/editor/editor~Editor} editor - */ - constructor( editor ) { - super( editor ); - - /** - * An instance of the {@link module:content-compatibility/dataschema~DataSchema}. - * - * @readonly - * @member {module:content-compatibility/dataschema~DataSchema} #dataSchema - */ - this.dataSchema = new DataSchema(); - - /** - * An instance of the {@link module:content-compatibility/datafilter~DataFilter}. - * - * @readonly - * @member {module:content-compatibility/datafilter~DataFilter} #dataFilter - */ - this.dataFilter = new DataFilter( editor, this.dataSchema ); + * @inheritDoc + */ + static get pluginName() { + return 'GeneralHtmlSupport'; } /** * @inheritDoc */ - static get pluginName() { - return 'GeneralHtmlSupport'; + static get requires() { + return [ DataFilter, DataSchema ]; } } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 766295c05e6..0564ae815a3 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -3,11 +3,12 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; +import DataFilter from '../src/datafilter'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -15,36 +16,42 @@ import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-util import GeneralHtmlSupport from '../src/generalhtmlsupport'; describe( 'DataFilter', () => { - let editor, model, dataFilter, dataSchema; + let editor, model, editorElement, dataFilter, dataSchema; testUtils.createSinonSandbox(); beforeEach( () => { - return VirtualTestEditor - .create( { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { plugins: [ Paragraph, FontColorEditing, LinkEditing, GeneralHtmlSupport ] } ) .then( newEditor => { editor = newEditor; model = editor.model; - const plugin = editor.plugins.get( GeneralHtmlSupport ); - - dataFilter = plugin.dataFilter; - dataSchema = plugin.dataSchema; + dataFilter = editor.plugins.get( 'DataFilter' ); + dataSchema = editor.plugins.get( 'DataSchema' ); } ); } ); afterEach( () => { + editorElement.remove(); + return editor.destroy(); } ); describe( 'initialization', () => { - let initEditor, initModel; + let initEditor, initEditorElement, initModel; beforeEach( () => { - return VirtualTestEditor - .create( { + initEditorElement = document.createElement( 'div' ); + document.body.appendChild( initEditorElement ); + + return ClassicTestEditor + .create( initEditorElement, { // Keep FakeRTCPlugin before FakeExtentedHtmlPlugin, so it's registered first. plugins: [ Paragraph, FakeRTCPlugin, FakeExtentedHtmlPlugin ] } ) @@ -55,6 +62,8 @@ describe( 'DataFilter', () => { } ); afterEach( () => { + initEditorElement.remove(); + return initEditor.destroy(); } ); @@ -79,7 +88,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow element registered after editor initialization', () => { - const { dataFilter } = initEditor.plugins.get( GeneralHtmlSupport ); + const dataFilter = initEditor.plugins.get( DataFilter ); dataFilter.allowElement( { name: 'span' } ); @@ -113,12 +122,12 @@ describe( 'DataFilter', () => { } init() { - const { dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); + const dataFilter = this.editor.plugins.get( DataFilter ); dataFilter.allowElement( { name: 'article' } ); } afterInit() { - const { dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); + const dataFilter = this.editor.plugins.get( DataFilter ); dataFilter.allowElement( { name: 'section' } ); } } @@ -303,14 +312,6 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' ); } ); - - it( 'should register embed widget only once', () => { - dataFilter.allowElement( { name: 'video' } ); - - expect( () => { - dataFilter.allowElement( { name: 'audio' } ); - } ).to.not.throw(); - } ); } ); describe( 'block', () => { diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js index 04766df7129..cdabcd5531d 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -33,7 +33,8 @@ class ExtendHTMLSupport extends Plugin { } init() { - const { dataFilter, dataSchema } = this.editor.plugins.get( GeneralHtmlSupport ); + const dataFilter = this.editor.plugins.get( 'DataFilter' ); + const dataSchema = this.editor.plugins.get( 'DataSchema' ); dataSchema.registerBlockElement( { model: 'htmlXyz', view: 'xyz', From c60409dd5ef9cc39b8fa38fe13e660fb924c0668 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 12 May 2021 11:18:08 +0200 Subject: [PATCH 132/217] Docs. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 5 +++-- packages/ckeditor5-content-compatibility/tests/datafilter.js | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e7c0b624fef..af230b58b90 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -557,9 +557,10 @@ function setViewElementAttributes( writer, viewAttributes, viewElement ) { // Creates object view element from the given model element. // // @private +// @param {String} viewName // @param {module:engine/model/element~Element} modelElement -// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi -// @returns {module:engine/view/element~Element} viewElement +// @param {module:engine/view/downcastwriter~DowncastWriter} writer +// @returns {module:engine/view/element~Element} function createObjectViewElement( viewName, modelElement, writer ) { const viewAttributes = modelElement.getAttribute( 'htmlAttributes' ); const viewContent = modelElement.getAttribute( 'value' ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 0564ae815a3..428810ea290 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -15,6 +15,8 @@ import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-util import GeneralHtmlSupport from '../src/generalhtmlsupport'; +/* global document */ + describe( 'DataFilter', () => { let editor, model, editorElement, dataFilter, dataSchema; From 78af6e86b680e9dc4a457c6cae1ac7fbd138a2d1 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 12 May 2021 11:44:02 +0200 Subject: [PATCH 133/217] Fixed manual test. --- .../tests/manual/generalhtmlsupport.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 7603b63a6a8..ed4ffec1934 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -27,7 +27,8 @@ class ExtendHTMLSupport extends Plugin { } init() { - const { dataSchema, dataFilter } = this.editor.plugins.get( GeneralHtmlSupport ); + const dataFilter = this.editor.plugins.get( 'DataFilter' ); + const dataSchema = this.editor.plugins.get( 'DataSchema' ); // Extend schema with custom `xyz` element. dataSchema.registerBlockElement( { From f2e23caba77daf0adf4e6bd18ca94f42b6570264 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 14 May 2021 10:33:34 +0200 Subject: [PATCH 134/217] Codeblock support, huge refactoring. --- .../package.json | 1 + .../src/conversionutils.js | 158 ++++++ .../src/converters.js | 274 ++++++++++ .../src/datafilter.js | 516 +++++------------- .../src/dataschema.js | 10 + .../tests/manual/codeblock.html | 11 + .../tests/manual/codeblock.js | 59 ++ .../tests/manual/codeblock.md | 0 8 files changed, 649 insertions(+), 380 deletions(-) create mode 100644 packages/ckeditor5-content-compatibility/src/conversionutils.js create mode 100644 packages/ckeditor5-content-compatibility/src/converters.js create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/codeblock.html create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/codeblock.js create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/codeblock.md diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index 396610351f9..a349d9eee8d 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -21,6 +21,7 @@ "devDependencies": { "@ckeditor/ckeditor5-basic-styles": "^27.1.0", "@ckeditor/ckeditor5-block-quote": "^27.1.0", + "@ckeditor/ckeditor5-code-block": "^27.1.0", "@ckeditor/ckeditor5-core": "^27.1.0", "@ckeditor/ckeditor5-editor-classic": "^27.1.0", "@ckeditor/ckeditor5-engine": "^27.1.0", diff --git a/packages/ckeditor5-content-compatibility/src/conversionutils.js b/packages/ckeditor5-content-compatibility/src/conversionutils.js new file mode 100644 index 00000000000..a55dd09b745 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/src/conversionutils.js @@ -0,0 +1,158 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module content-compatibility/conversionutils + */ + +import { cloneDeep } from 'lodash-es'; + +/** + * Matches and consumes the given view attributes. + * + * @param {module:engine/view/element~Element} viewElement + * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi + * @param {module:engine/view/matcher~Matcher Matcher} matcher + * @returns {Object} [result] + * @returns {Object} result.attributes Set with matched attribute names. + * @returns {Object} result.styles Set with matched style names. + * @returns {Array.} result.classes Set with matched class names. + */ +export function consumeViewAttributes( viewElement, conversionApi, matcher ) { + const matches = consumeAttributeMatches( viewElement, conversionApi, matcher ); + const { attributes, styles, classes } = mergeMatchResults( matches ); + const viewAttributes = {}; + + if ( attributes.size ) { + viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); + } + + if ( styles.size ) { + viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); + } + + if ( classes.size ) { + viewAttributes.classes = Array.from( classes ); + } + + if ( !Object.keys( viewAttributes ).length ) { + return null; + } + + return viewAttributes; +} + +/** +* Helper function for downcast converter. Sets attributes on the given view element. +* +* @param {module:engine/view/downcastwriter~DowncastWriter} writer +* @param {Object} viewAttributes +* @param {module:engine/view/element~Element} viewElement +*/ +export function setViewAttributes( writer, viewAttributes, viewElement ) { + if ( viewAttributes.attributes ) { + for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) { + writer.setAttribute( key, value, viewElement ); + } + } + + if ( viewAttributes.styles ) { + writer.setStyle( viewAttributes.styles, viewElement ); + } + + if ( viewAttributes.classes ) { + writer.addClass( viewAttributes.classes, viewElement ); + } +} + +/** +* Merges view element attribute objects. +* +* @param {Object} oldValue +* @param {Object} newValue +* @returns {Object} +*/ +export function mergeViewElementAttributes( oldValue, newValue ) { + const result = cloneDeep( oldValue ); + + for ( const key in newValue ) { + // Merge classes. + if ( Array.isArray( newValue[ key ] ) ) { + result[ key ] = Array.from( new Set( [ ...oldValue[ key ], ...newValue[ key ] ] ) ); + } + + // Merge attributes or styles. + else { + result[ key ] = { ...oldValue[ key ], ...newValue[ key ] }; + } + } + + return result; +} + +// Consumes matched attributes. +// +// @private +// @param {module:engine/view/element~Element} viewElement +// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi +// @param {module:engine/view/matcher~Matcher Matcher} matcher +// @returns {Array.} Array with match information about found attributes. +function consumeAttributeMatches( viewElement, { consumable }, matcher ) { + const matches = matcher.matchAll( viewElement ) || []; + const consumedMatches = []; + + for ( const match of matches ) { + // We only want to consume attributes, so element can be still processed by other converters. + delete match.match.name; + + if ( consumable.consume( viewElement, match.match ) ) { + consumedMatches.push( match ); + } + } + + return consumedMatches; +} + +// Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. +// +// @private +// @param {Array.} matches +// @returns {Object} result +// @returns {Set.} result.attributes Set with matched attribute names. +// @returns {Set.} result.styles Set with matched style names. +// @returns {Set.} result.classes Set with matched class names. +function mergeMatchResults( matches ) { + const matchResult = { + attributes: new Set(), + classes: new Set(), + styles: new Set() + }; + + for ( const match of matches ) { + for ( const key in matchResult ) { + const values = match.match[ key ] || []; + + values.forEach( value => matchResult[ key ].add( value ) ); + } + } + + return matchResult; +} + +// Converts the given iterable object into an object. +// +// @private +// @param {Iterable.} iterable +// @param {Function} getValue Should result with value for the given object key. +// @returns {Object} +function iterableToObject( iterable, getValue ) { + const attributesObject = {}; + + for ( const prop of iterable ) { + attributesObject[ prop ] = getValue( prop ); + } + + return attributesObject; +} diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js new file mode 100644 index 00000000000..5ac4d2888cd --- /dev/null +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -0,0 +1,274 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module content-compatibility/converters + */ + +import { toWidget } from 'ckeditor5/src/widget'; +import { consumeViewAttributes, setViewAttributes, mergeViewElementAttributes } from './conversionutils'; + +/** + * Conversion helper consuming all attributes from the definition view element + * matched by the given matcher. + * + * This converter listenes on `high` priority to ensure that all attributes are consumed + * before standard priority converters. + * + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:engine/view/matcher~Matcher} matcher + * @returns {Function} Returns a conversion callback. +*/ +export function consumeViewAttributesConverter( { view: viewName }, matcher ) { + return dispatcher => { + dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { + consumeViewAttributes( data.viewItem, conversionApi, matcher ); + }, { priority: 'high' } ); + }; +} + +/** + * View-to-model conversion helper preserving attributes on {@link module:code-block/codeblock~CodeBlock Code Block} + * feature model element matched by the given matcher. + * + * Attributes are preserved as a value of `htmlAttributes` model attribute. + * + * @param {module:engine/view/matcher~Matcher} matcher + * @returns {Function} Returns a conversion callback. +*/ +export function viewToModelCodeBlockAttributeConverter( matcher ) { + return dispatcher => { + dispatcher.on( 'element:code', ( evt, data, conversionApi ) => { + if ( !data.modelRange ) { + return; + } + + const viewPreElement = data.viewItem.parent; + if ( !isPreElement( viewPreElement ) ) { + return; + } + + const viewAttributes = consumeViewAttributes( viewPreElement, conversionApi, matcher ); + + if ( viewAttributes ) { + conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange ); + } + }, { conversionPriority: 'low' } ); + }; +} + +/** + * Model-to-view conversion helper applying attributes from {@link module:code-block/codeblock~CodeBlock Code Block} + * feature model element. + * + * @returns {Function} Returns a conversion callback. +*/ +export function modelToViewCodeBlockAttributeConverter() { + return dispatcher => { + dispatcher.on( 'attribute:htmlAttributes:codeBlock', ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewPreElement = conversionApi.mapper.toViewElement( data.item ).parent; + + if ( isPreElement( viewPreElement ) ) { + setViewAttributes( conversionApi.writer, data.attributeNewValue, viewPreElement ); + } + } ); + }; +} + +/** + * View-to-model conversion helper for object elements. + * + * Preserves object element content in `value` attribute. Also, all matching attributes + * by the given matcher will be preserved on `htmlAttributes` attribute. + * + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:engine/view/matcher~Matcher} matcher + * @returns {Function} Returns a conversion callback. +*/ +export function viewToModelObjectConverter( { model: modelName }, matcher ) { + return ( viewElement, conversionApi ) => { + const htmlAttributes = consumeViewAttributes( viewElement, conversionApi, matcher ); + + // Let's keep element HTML and its attributes, so we can rebuild element in downcast conversions. + return conversionApi.writer.createElement( modelName, { + value: viewElement.getCustomProperty( '$rawContent' ), + ...( htmlAttributes && { htmlAttributes } ) + } ); + }; +} + +/** + * Conversion helper converting object element to HTML object widget. + * + * @param {module:core/editor/editor~Editor} editor + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @returns {Function} Returns a conversion callback. +*/ +export function toObjectWidgetConverter( editor, { view: viewName, isInline } ) { + return ( modelElement, { writer } ) => { + const widgetLabel = editor.t( 'HTML object' ); + + // Widget cannot be a raw element because the widget system would not be able + // to add its UI to it. Thus, we need separate view container. + const viewContainer = writer.createContainerElement( isInline ? 'span' : 'div', { + class: 'html-object-embed', + 'data-html-object-embed-label': widgetLabel, + dir: editor.locale.uiLanguageDirection + }, { + isAllowedInsideAttributeElement: isInline + } ); + + const viewElement = createObjectView( viewName, modelElement, writer ); + writer.addClass( 'html-object-embed__content', viewElement ); + + writer.insert( writer.createPositionAt( viewContainer, 0 ), viewElement ); + + return toWidget( viewContainer, writer, { widgetLabel } ); + }; +} + +/** +* Creates object view element from the given model element. +* +* Applies attributes preserved in `htmlAttributes` model attribute. +* +* @param {String} viewName +* @param {module:engine/model/element~Element} modelElement +* @param {module:engine/view/downcastwriter~DowncastWriter} writer +* @returns {module:engine/view/element~Element} +*/ +export function createObjectView( viewName, modelElement, writer ) { + const viewAttributes = modelElement.getAttribute( 'htmlAttributes' ); + const viewContent = modelElement.getAttribute( 'value' ); + + const viewElement = writer.createRawElement( viewName, null, function( domElement ) { + domElement.innerHTML = viewContent; + } ); + + if ( viewAttributes ) { + setViewAttributes( writer, viewAttributes, viewElement ); + } + + return viewElement; +} + +/** + * View-to-attribute conversion helper preserving inline element attributes on `$text`. + * + * All element attributes matched by the given matcher will be preserved as a value of + * {@link module:content-compatibility/dataschema~DataSchemaInlineElementDefinition~model definition model} + * attribute. + * + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:engine/view/matcher~Matcher} matcher + * @returns {Function} Returns a conversion callback. +*/ +export function viewToAttributeInlineConverter( { view: viewName, model: attributeKey }, matcher ) { + return dispatcher => { + dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { + const viewAttributes = consumeViewAttributes( data.viewItem, conversionApi, matcher ); + + // Since we are converting to attribute we need a range on which we will set the attribute. + // If the range is not created yet, we will create it. + if ( !data.modelRange ) { + data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); + } + + // Set attribute on each item in range according to the schema. + for ( const node of data.modelRange.getItems() ) { + if ( conversionApi.schema.checkAttribute( node, attributeKey ) ) { + // Node's children are converted recursively, so node can already include model attribute. + // We want to extend it, not replace. + const nodeAttributes = node.getAttribute( attributeKey ); + const attributesToAdd = mergeViewElementAttributes( viewAttributes || {}, nodeAttributes || {} ); + + conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); + } + } + }, { priority: 'low' } ); + }; +} + +/** + * Attribute-to-view conversion helper applying attributes to view element preserved on `$text`. + * + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @returns {Function} Returns a conversion callback. +*/ +export function attributeToViewInlineConverter( { priority, view: viewName } ) { + return ( attributeValue, conversionApi ) => { + if ( !attributeValue ) { + return; + } + + const { writer } = conversionApi; + const viewElement = writer.createAttributeElement( viewName, null, { priority } ); + + setViewAttributes( writer, attributeValue, viewElement ); + + return viewElement; + }; +} + +/** + * View-to-model conversion helper preserving attributes on block element matched by the given matcher. + * + * All matched attributes will be preserved on `htmlAttributes` attribute. + * + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:engine/view/matcher~Matcher} matcher + * @returns {Function} Returns a conversion callback. +*/ +export function viewToModelBlockAttributeConverter( { view: viewName }, matcher ) { + return dispatcher => { + dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { + if ( !data.modelRange ) { + return; + } + + const viewAttributes = consumeViewAttributes( data.viewItem, conversionApi, matcher ); + + if ( viewAttributes ) { + conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange ); + } + }, { priority: 'low' } ); + }; +} + +/** + * Model-to-view conversion helper applying attributes preserved in `htmlAttributes` attribute + * for block elements. + * + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @returns {Function} Returns a conversion callback. +*/ +export function modelToViewBlockAttributeConverter( { model: modelName } ) { + return dispatcher => { + dispatcher.on( `attribute:htmlAttributes:${ modelName }`, ( evt, data, conversionApi ) => { + const viewAttributes = data.attributeNewValue; + + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const viewElement = conversionApi.mapper.toViewElement( data.item ); + + setViewAttributes( viewWriter, viewAttributes, viewElement ); + } ); + }; +} + +// Checks if the given view element is `pre`. +// +// @param {module:engine/view/element~Element} [element] +// @returns {Boolean} +function isPreElement( element ) { + return element && element.is( 'element', 'pre' ); +} diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index af230b58b90..faf88bbf87b 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -7,12 +7,28 @@ * @module content-compatibility/datafilter */ +import DataSchema from './dataschema'; + import { Plugin } from 'ckeditor5/src/core'; import { Matcher } from 'ckeditor5/src/engine'; import { priorities, CKEditorError } from 'ckeditor5/src/utils'; -import { toWidget, Widget } from 'ckeditor5/src/widget'; -import { cloneDeep } from 'lodash-es'; -import DataSchema from './dataschema'; +import { Widget } from 'ckeditor5/src/widget'; +import { + consumeViewAttributesConverter, + + viewToModelCodeBlockAttributeConverter, + modelToViewCodeBlockAttributeConverter, + + viewToModelObjectConverter, + toObjectWidgetConverter, + createObjectView, + + viewToAttributeInlineConverter, + attributeToViewInlineConverter, + + viewToModelBlockAttributeConverter, + modelToViewBlockAttributeConverter +} from './converters'; import '../theme/datafilter.css'; @@ -186,92 +202,87 @@ export default class DataFilter extends Plugin { * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ _registerElement( definition ) { - if ( definition.isObject ) { - this._registerObjectElement( definition ); - } else if ( definition.isInline ) { - this._registerInlineElement( definition ); - } else if ( definition.isBlock ) { - this._registerBlockElement( definition ); - } else { - /** - * Only a definition marked as inline, block or object can be allowed. - * - * @error data-filter-invalid-definition-type - */ - throw new CKEditorError( - 'data-filter-invalid-definition-type', - null, - definition - ); + // Note that the order of element handlers is important, + // as the handler may interrupt handlers execution in case of returning + // anything else than `false` value. + const elementHandlers = [ + this._handleCodeBlockElement, + this._handleObjectElement, + this._handleInlineElement, + this._handleBlockElement + ]; + + for ( const elementHandler of elementHandlers ) { + if ( elementHandler.call( this, definition ) !== false ) { + return; + } } - } - - /** - * Registers object element and attribute converters for the given data schema definition. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition - */ - _registerObjectElement( definition ) { - this.editor.model.schema.register( definition.model, definition.modelSchema ); - this._addObjectElementConversion( definition ); - this._addDisallowedAttributeConversion( definition ); + /** + * The definition cannot be handled by the data filter. + * + * Make sure that the registered definition is correct. + * + * @error data-filter-invalid-definition + */ + throw new CKEditorError( + 'data-filter-invalid-definition', + null, + definition + ); } /** - * Registers block element and attribute converters for the given data schema definition. - * - * If the element model schema is already registered, this method will do nothing. + * Registers attribute converters for {@link module:code-block/codeblock~CodeBlock Code Block} feature. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @returns {Boolean} */ - _registerBlockElement( definition ) { - const schema = this.editor.model.schema; + _handleCodeBlockElement( definition ) { + const editor = this.editor; - if ( !schema.isRegistered( definition.model ) ) { - this.editor.model.schema.register( definition.model, definition.modelSchema ); - this._addBlockElementToElementConversion( definition ); + // We should only handle codeBlock model if CodeBlock plugin is available. + // Otherwise, let #_handleBlockElement() do the job. + if ( !editor.plugins.has( 'CodeBlock' ) || definition.model !== 'codeBlock' ) { + return false; } - this._addDisallowedAttributeConversion( definition ); - this._addBlockElementAttributeConversion( definition ); - } - - /** - * Registers inline element and attribute converters for the given data schema definition. - * - * Extends `$text` model schema to allow the given definition model attribute and its properties. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition - */ - _registerInlineElement( definition ) { - const schema = this.editor.model.schema; - - schema.extend( '$text', { - allowAttributes: definition.model - } ); + const schema = editor.model.schema; + const conversion = editor.conversion; - if ( definition.attributeProperties ) { - schema.setAttributeProperties( definition.model, definition.attributeProperties ); - } + // CodeBlock plugin is filtering out all attributes on `code` element. Let's add + // exception for `htmlCode` required for data filtration mechanism. + schema.on( 'checkAttribute', ( evt, [ context, attributeName ] ) => { + if ( attributeName === 'htmlCode' && context.endsWith( 'codeBlock $text' ) ) { + evt.return = true; + evt.stop(); + } + }, { priority: priorities.get( 'high' ) + 1 } ); - this._addDisallowedAttributeConversion( definition ); - this._addInlineElementConversion( definition ); + conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); + conversion.for( 'upcast' ).add( viewToModelCodeBlockAttributeConverter( this._allowedAttributes ) ); + conversion.for( 'downcast' ).add( modelToViewCodeBlockAttributeConverter() ); } /** - * Adds converters for the given data schema definition marked as - * {@link module:content-compatibility/dataschema~DataSchemaDefinition#isObject isObject}. + * Registers object element and attribute converters for the given data schema definition. * * @private * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @returns {Boolean} */ - _addObjectElementConversion( definition ) { + _handleObjectElement( definition ) { + if ( !definition.isObject ) { + return false; + } + + const editor = this.editor; + const schema = editor.model.schema; + const conversion = editor.conversion; const { view: viewName, model: modelName } = definition; - const conversion = this.editor.conversion; + + schema.register( definition.model, definition.modelSchema ); if ( !viewName ) { return; @@ -279,365 +290,110 @@ export default class DataFilter extends Plugin { // Store element content in special `$rawContent` custom property to // avoid editor's data filtering mechanism. - this.editor.data.registerRawContentMatcher( { + editor.data.registerRawContentMatcher( { name: viewName } ); + conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); conversion.for( 'upcast' ).elementToElement( { view: viewName, - model: ( viewElement, conversionApi ) => { - const htmlAttributes = this._matchAndConsumeAllowedAttributes( viewElement, conversionApi ); - - // Let's keep element HTML and its attributes, so we can rebuild element in downcast conversions. - return conversionApi.writer.createElement( modelName, { - value: viewElement.getCustomProperty( '$rawContent' ), - ...( htmlAttributes && { htmlAttributes } ) - } ); - } + model: viewToModelObjectConverter( definition, this._allowedAttributes ) } ); conversion.for( 'dataDowncast' ).elementToElement( { model: modelName, view: ( modelElement, { writer } ) => { - return createObjectViewElement( viewName, modelElement, writer ); + return createObjectView( viewName, modelElement, writer ); } } ); - conversion.for( 'editingDowncast' ).elementToElement( { model: modelName, - view: ( modelElement, { writer } ) => { - const widgetLabel = this.editor.t( 'HTML object' ); - - // Widget cannot be a raw element because the widget system would not be able - // to add its UI to it. Thus, we need separate view container. - const viewContainer = writer.createContainerElement( definition.isInline ? 'span' : 'div', { - class: 'html-object-embed', - 'data-html-object-embed-label': widgetLabel, - dir: this.editor.locale.uiLanguageDirection - }, { - isAllowedInsideAttributeElement: definition.isInline - } ); - - const viewElement = createObjectViewElement( viewName, modelElement, writer ); - writer.addClass( 'html-object-embed__content', viewElement ); - - writer.insert( writer.createPositionAt( viewContainer, 0 ), viewElement ); - - return toWidget( viewContainer, writer, { widgetLabel } ); - } + view: toObjectWidgetConverter( editor, definition ) } ); } /** - * Adds element to element converters for the given block element definition. + * Registers block element and attribute converters for the given data schema definition. * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition - */ - _addBlockElementToElementConversion( { model: modelName, view: viewName } ) { - const conversion = this.editor.conversion; - - if ( !viewName ) { - return; - } - - conversion.for( 'upcast' ).elementToElement( { - model: modelName, - view: viewName, - // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure - // this listener is called before it. If not, some elements will be transformed into a paragraph. - converterPriority: priorities.get( 'low' ) + 1 - } ); - - conversion.for( 'downcast' ).elementToElement( { - model: modelName, - view: viewName - } ); - } - - /** - * Adds attribute converters for the given block element definition. + * If the element model schema is already registered, this method will do nothing. * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @returns {Boolean} */ - _addBlockElementAttributeConversion( { model: modelName, view: viewName } ) { - const conversion = this.editor.conversion; - - if ( !viewName ) { - return; + _handleBlockElement( definition ) { + if ( !definition.isBlock ) { + return false; } - conversion.for( 'upcast' ).add( dispatcher => { - dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - if ( !data.modelRange ) { - return; - } - - const viewAttributes = this._matchAndConsumeAllowedAttributes( data.viewItem, conversionApi ); - - if ( viewAttributes ) { - conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange ); - } - }, { priority: 'low' } ); - } ); - - conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( `attribute:htmlAttributes:${ modelName }`, ( evt, data, conversionApi ) => { - const viewAttributes = data.attributeNewValue; + const editor = this.editor; + const schema = editor.model.schema; + const conversion = editor.conversion; + const { view: viewName, model: modelName } = definition; - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { - return; - } + if ( !schema.isRegistered( definition.model ) ) { + schema.register( definition.model, definition.modelSchema ); - const viewWriter = conversionApi.writer; - const viewElement = conversionApi.mapper.toViewElement( data.item ); + if ( !viewName ) { + return; + } - setViewElementAttributes( viewWriter, viewAttributes, viewElement ); + conversion.for( 'upcast' ).elementToElement( { + model: modelName, + view: viewName, + // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure + // this listener is called before it. If not, some elements will be transformed into a paragraph. + converterPriority: priorities.get( 'low' ) + 1 } ); - } ); - } - - /** - * Adds converters for the given inline element definition. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition - */ - _addInlineElementConversion( definition ) { - const conversion = this.editor.conversion; - const viewName = definition.view; - const attributeKey = definition.model; - - conversion.for( 'upcast' ).add( dispatcher => { - dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - const viewAttributes = this._matchAndConsumeAllowedAttributes( data.viewItem, conversionApi ); - - // Since we are converting to attribute we need a range on which we will set the attribute. - // If the range is not created yet, we will create it. - if ( !data.modelRange ) { - data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) ); - } - - // Set attribute on each item in range according to the schema. - for ( const node of data.modelRange.getItems() ) { - if ( conversionApi.schema.checkAttribute( node, attributeKey ) ) { - // Node's children are converted recursively, so node can already include model attribute. - // We want to extend it, not replace. - const nodeAttributes = node.getAttribute( attributeKey ); - const attributesToAdd = mergeViewElementAttributes( viewAttributes || {}, nodeAttributes || {} ); - - conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node ); - } - } - }, { priority: 'low' } ); - } ); - - conversion.for( 'downcast' ).attributeToElement( { - model: attributeKey, - view: ( attributeValue, conversionApi ) => { - if ( !attributeValue ) { - return; - } - - const { writer } = conversionApi; - const viewElement = writer.createAttributeElement( viewName, null, { priority: definition.priority } ); - setViewElementAttributes( writer, attributeValue, viewElement ); - - return viewElement; - } - } ); - } - - /** - * Adds converters responsible for consuming disallowed view attributes. - * - * @private - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition - */ - _addDisallowedAttributeConversion( { view: viewName } ) { - const conversion = this.editor.conversion; + conversion.for( 'downcast' ).elementToElement( { + model: modelName, + view: viewName + } ); + } if ( !viewName ) { return; } - // Consumes disallowed element attributes to prevent them of being processed by other converters. - conversion.for( 'upcast' ).add( dispatcher => { - dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - consumeAttributeMatches( data.viewItem, conversionApi, this._disallowedAttributes ); - }, { priority: 'high' } ); - } ); + conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); + conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this._allowedAttributes ) ); + conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) ); } /** - * Matches and consumes allowed view attributes. + * Registers inline element and attribute converters for the given data schema definition. + * + * Extends `$text` model schema to allow the given definition model attribute and its properties. * * @private - * @param {module:engine/view/element~Element} viewElement - * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi - * @returns {Object} [result] - * @returns {Object} result.attributes Set with matched attribute names. - * @returns {Object} result.styles Set with matched style names. - * @returns {Array.} result.classes Set with matched class names. + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @returns {Boolean} */ - _matchAndConsumeAllowedAttributes( viewElement, conversionApi ) { - const matches = consumeAttributeMatches( viewElement, conversionApi, this._allowedAttributes ); - const { attributes, styles, classes } = mergeMatchResults( matches ); - const viewAttributes = {}; - - if ( attributes.size ) { - viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); - } - - if ( styles.size ) { - viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); + _handleInlineElement( definition ) { + if ( !definition.isInline ) { + return false; } - if ( classes.size ) { - viewAttributes.classes = Array.from( classes ); - } - - if ( !Object.keys( viewAttributes ).length ) { - return null; - } - - return viewAttributes; - } -} - -// Consumes matched attributes. -// -// Returns sucessfully consumed attribute matches. -// -// @private -// @param {module:engine/view/element~Element} viewElement -// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi -// @param {module:engine/view/matcher~Matcher Matcher} matcher -// @returns {Array.} Array with match information about found attributes. -function consumeAttributeMatches( viewElement, { consumable }, matcher ) { - const matches = matcher.matchAll( viewElement ) || []; - const consumedMatches = []; - - for ( const match of matches ) { - // We only want to consume attributes, so element can be still processed by other converters. - delete match.match.name; - - if ( consumable.consume( viewElement, match.match ) ) { - consumedMatches.push( match ); - } - } - - return consumedMatches; -} - -// Helper function for downcast converter. Sets attributes on the given view element. -// -// @private -// @param {module:engine/view/downcastwriter~DowncastWriter} writer -// @param {Object} viewAttributes -// @param {module:engine/view/element~Element} viewElement -function setViewElementAttributes( writer, viewAttributes, viewElement ) { - if ( viewAttributes.attributes ) { - for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) { - writer.setAttribute( key, value, viewElement ); - } - } - - if ( viewAttributes.styles ) { - writer.setStyle( viewAttributes.styles, viewElement ); - } - - if ( viewAttributes.classes ) { - writer.addClass( viewAttributes.classes, viewElement ); - } -} - -// Creates object view element from the given model element. -// -// @private -// @param {String} viewName -// @param {module:engine/model/element~Element} modelElement -// @param {module:engine/view/downcastwriter~DowncastWriter} writer -// @returns {module:engine/view/element~Element} -function createObjectViewElement( viewName, modelElement, writer ) { - const viewAttributes = modelElement.getAttribute( 'htmlAttributes' ); - const viewContent = modelElement.getAttribute( 'value' ); - - const viewElement = writer.createRawElement( viewName, null, function( domElement ) { - domElement.innerHTML = viewContent; - } ); - - if ( viewAttributes ) { - setViewElementAttributes( writer, viewAttributes, viewElement ); - } + const editor = this.editor; + const schema = editor.model.schema; + const conversion = editor.conversion; + const attributeKey = definition.model; - return viewElement; -} + schema.extend( '$text', { + allowAttributes: attributeKey + } ); -// Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. -// -// @private -// @param {Array.} matches -// @returns {Object} result -// @returns {Set.} result.attributes Set with matched attribute names. -// @returns {Set.} result.styles Set with matched style names. -// @returns {Set.} result.classes Set with matched class names. -function mergeMatchResults( matches ) { - const matchResult = { - attributes: new Set(), - classes: new Set(), - styles: new Set() - }; - - for ( const match of matches ) { - for ( const key in matchResult ) { - const values = match.match[ key ] || []; - - values.forEach( value => matchResult[ key ].add( value ) ); + if ( definition.attributeProperties ) { + schema.setAttributeProperties( attributeKey, definition.attributeProperties ); } - } - return matchResult; -} - -// Converts the given iterable object into an object. -// -// @private -// @param {Iterable.} iterable -// @param {Function} getValue Should result with value for the given object key. -// @returns {Object} -function iterableToObject( iterable, getValue ) { - const attributesObject = {}; - - for ( const prop of iterable ) { - attributesObject[ prop ] = getValue( prop ); - } + conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); + conversion.for( 'upcast' ).add( viewToAttributeInlineConverter( definition, this._allowedAttributes ) ); - return attributesObject; -} - -// Merges view element attribute objects. -// -// @private -// @param {Object} oldValue -// @param {Object} newValue -// @returns {Object} -function mergeViewElementAttributes( oldValue, newValue ) { - const result = cloneDeep( oldValue ); - - for ( const key in newValue ) { - // Merge classes. - if ( Array.isArray( newValue[ key ] ) ) { - result[ key ] = Array.from( new Set( [ ...oldValue[ key ], ...newValue[ key ] ] ) ); - } - - // Merge attributes or styles. - else { - result[ key ] = { ...oldValue[ key ], ...newValue[ key ] }; - } + conversion.for( 'downcast' ).attributeToElement( { + model: attributeKey, + view: attributeToViewInlineConverter( definition ) + } ); } - - return result; } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index f1757deb0fe..e81c605c64e 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -79,6 +79,11 @@ export default class DataSchema extends Plugin { } } ); + this.registerBlockElement( { + model: 'codeBlock', + view: 'pre' + } ); + this.registerBlockElement( { model: 'paragraph', view: 'p' @@ -183,6 +188,11 @@ export default class DataSchema extends Plugin { } ); // Inline elements. + this.registerInlineElement( { + view: 'code', + model: 'htmlCode' + } ); + this.registerInlineElement( { view: 'a', model: 'htmlA', diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html new file mode 100644 index 00000000000..8fec2981af6 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html @@ -0,0 +1,11 @@ + + + + +
    +

    Put some text in the :

    +
    Hello world!
    +

    Then set the font color:

    +
    body { color: red }
    +
    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js new file mode 100644 index 00000000000..2ce9ed27874 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js @@ -0,0 +1,59 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console:false, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +import GeneralHtmlSupport from '../../src/generalhtmlsupport'; + +/** + * Client custom plugin extending HTML support for compatibility. + */ +class ExtendHTMLSupport extends Plugin { + static get requires() { + return [ GeneralHtmlSupport ]; + } + + init() { + const dataFilter = this.editor.plugins.get( 'DataFilter' ); + + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, styles: { color: /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, styles: { background: /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); + + dataFilter.disallowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': 'bar' } } ); + dataFilter.disallowAttributes( { name: /^(pre|code)$/, styles: { background: 'yellow' } } ); + } +} + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + Bold, + CodeBlock, + Essentials, + ExtendHTMLSupport, + Italic, + Paragraph, + Strikethrough + ], + toolbar: [ 'codeBlock', '|', 'bold', 'italic', 'strikethrough' ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.md b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.md new file mode 100644 index 00000000000..e69de29bb2d From 3ebc4c60936a25942dbaad1ba5d309b90be10a5f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 14 May 2021 13:16:17 +0200 Subject: [PATCH 135/217] Added test coverage, removed dead code. --- .../src/converters.js | 20 +- .../tests/datafilter.js | 208 +++++++++++++++++- 2 files changed, 209 insertions(+), 19 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index 5ac4d2888cd..0056969ff01 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -41,12 +41,9 @@ export function consumeViewAttributesConverter( { view: viewName }, matcher ) { export function viewToModelCodeBlockAttributeConverter( matcher ) { return dispatcher => { dispatcher.on( 'element:code', ( evt, data, conversionApi ) => { - if ( !data.modelRange ) { - return; - } - const viewPreElement = data.viewItem.parent; - if ( !isPreElement( viewPreElement ) ) { + + if ( !viewPreElement || !viewPreElement.is( 'element', 'pre' ) ) { return; } @@ -73,10 +70,7 @@ export function modelToViewCodeBlockAttributeConverter() { } const viewPreElement = conversionApi.mapper.toViewElement( data.item ).parent; - - if ( isPreElement( viewPreElement ) ) { - setViewAttributes( conversionApi.writer, data.attributeNewValue, viewPreElement ); - } + setViewAttributes( conversionApi.writer, data.attributeNewValue, viewPreElement ); } ); }; } @@ -264,11 +258,3 @@ export function modelToViewBlockAttributeConverter( { model: modelName } ) { } ); }; } - -// Checks if the given view element is `pre`. -// -// @param {module:engine/view/element~Element} [element] -// @returns {Boolean} -function isPreElement( element ) { - return element && element.is( 'element', 'pre' ); -} diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 428810ea290..fe556323579 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -8,6 +8,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; +import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; import DataFilter from '../src/datafilter'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; @@ -28,7 +29,7 @@ describe( 'DataFilter', () => { return ClassicTestEditor .create( editorElement, { - plugins: [ Paragraph, FontColorEditing, LinkEditing, GeneralHtmlSupport ] + plugins: [ Paragraph, FontColorEditing, LinkEditing, CodeBlock, GeneralHtmlSupport ] } ) .then( newEditor => { editor = newEditor; @@ -135,6 +136,188 @@ describe( 'DataFilter', () => { } } ); + describe( 'codeBlock', () => { + it( 'should allow attributes', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(2)">foobar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + }, + 2: { + attributes: { + 'data-foo': 'foo' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '
    ' +
    +				'foobar' +
    +				'
    ' ); + } ); + + it( 'should allow attributes (classes)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(2)">foobar', + attributes: { + 1: { + classes: [ 'foo' ] + }, + 2: { + classes: [ 'foo' ] + } + } + } ); + + expect( editor.getData() ).to.equal( '
    ' +
    +				'foobar' +
    +				'
    ' ); + } ); + + it( 'should allow attributes (styles)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); + dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(2)">foobar', + attributes: { + 1: { + styles: { + background: 'blue' + } + }, + 2: { + styles: { + color: 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '
    ' +
    +				'foobar' +
    +				'
    ' ); + } ); + + it( 'should disallow attributes', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(1)">foobar', + attributes: { + 1: {} + } + } ); + + expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); + + it( 'should disallow attributes (classes)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); + dataFilter.disallowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(1)">foobar', + attributes: { + 1: {} + } + } ); + + expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); + + it( 'should allow attributes (styles)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + + dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); + dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); + + dataFilter.disallowAttributes( { name: 'pre', styles: { background: 'blue' } } ); + dataFilter.disallowAttributes( { name: 'code', styles: { color: 'red' } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(1)">foobar', + attributes: { + 1: {} + } + } ); + + expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); + + it( 'should allow attributes on code element existing alone', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: 'code', attributes: { 'data-foo': /[\s\S]+/ } } ); + + editor.setData( '

    foobar

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(1)">foobar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

    foobar

    ' ); + } ); + + it( 'should not consume attribute already consumed (downcast)', () => { + editor.conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( 'attribute:htmlAttributes:codeBlock', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, evt.name ); + }, { priority: 'high' } ); + } ); + + dataFilter.allowElement( { name: 'pre' } ); + dataFilter.allowAttributes( { name: 'pre', attributes: { 'data-foo': true } } ); + + editor.setData( '
    foobar' );
    +
    +			expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( {
    +				data: 'foobar',
    +				// At this point, attribute should still be in the model, as we are testing downcast conversion.
    +				attributes: {
    +					1: {
    +						attributes: {
    +							'data-foo': ''
    +						}
    +					}
    +				}
    +			} );
    +
    +			expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); + } ); + describe( 'object', () => { it( 'should allow element', () => { dataFilter.allowElement( { name: 'input' } ); @@ -694,6 +877,27 @@ describe( 'DataFilter', () => { expect( getModelData( model, { withoutSelection: true } ) ).to.equal( 'foo' ); } ); + + it( 'should not register view converters for existing features if a view has not been provided', () => { + // Skipping `view` property on purpose. + dataSchema.registerBlockElement( { + model: 'htmlFoo', + modelSchema: { inheritAllFrom: '$block' } + } ); + + dataSchema.registerBlockElement( { + model: 'htmlBar', + view: 'bar', + modelSchema: { inheritAllFrom: 'htmlFoo' } + } ); + + editor.model.schema.register( 'htmlFoo', { inheritAllFrom: '$block' } ); + + // At this point we will be trying to register converter without valid view name. + expect( () => { + dataFilter.allowElement( { name: 'bar' } ); + } ).to.not.throw(); + } ); } ); describe( 'inline', () => { @@ -1246,7 +1450,7 @@ describe( 'DataFilter', () => { expectToThrowCKEditorError( () => { dataFilter.allowElement( { name: 'xyz' } ); - }, /data-filter-invalid-definition-type/, null, definition ); + }, /data-filter-invalid-definition/, null, definition ); } ); function getModelDataWithAttributes( model, options ) { From aa0a93df80b2a2a734164771073e0e55798ff6c5 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 25 May 2021 09:33:12 +0200 Subject: [PATCH 136/217] Refactoring. --- .../src/dataschema.js | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index e81c605c64e..94641027b67 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -63,6 +63,47 @@ export default class DataSchema extends Plugin { return 'DataSchema'; } + /** + * Add new data schema definition describing block element. + * + * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + */ + registerBlockElement( definition ) { + this._definitions.set( definition.model, { ...definition, isBlock: true } ); + } + + /** + * Add new data schema definition describing inline element. + * + * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + */ + registerInlineElement( definition ) { + this._definitions.set( definition.model, { ...definition, isInline: true } ); + } + + /** + * Returns all definitions matching the given view name. + * + * @param {String|RegExp} viewName + * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. + * @returns {Set.} + */ + getDefinitionsForView( viewName, includeReferences ) { + const definitions = new Set(); + + for ( const definition of this._getMatchingViewDefinitions( viewName ) ) { + if ( includeReferences ) { + for ( const reference of this._getReferences( definition.model ) ) { + definitions.add( reference ); + } + } + + definitions.add( definition ); + } + + return definitions; + } + /** * Registers default data schema definitions. * @@ -323,47 +364,6 @@ export default class DataSchema extends Plugin { } ); } - /** - * Add new data schema definition describing block element. - * - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition - */ - registerBlockElement( definition ) { - this._definitions.set( definition.model, { ...definition, isBlock: true } ); - } - - /** - * Add new data schema definition describing inline element. - * - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition - */ - registerInlineElement( definition ) { - this._definitions.set( definition.model, { ...definition, isInline: true } ); - } - - /** - * Returns all definitions matching the given view name. - * - * @param {String|RegExp} viewName - * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. - * @returns {Set.} - */ - getDefinitionsForView( viewName, includeReferences ) { - const definitions = new Set(); - - for ( const definition of this._getMatchingViewDefinitions( viewName ) ) { - if ( includeReferences ) { - for ( const reference of this._getReferences( definition.model ) ) { - definitions.add( reference ); - } - } - - definitions.add( definition ); - } - - return definitions; - } - /** * Returns definitions matching the given view name. * From f841e6fd5424e70c2b00d3ac825777312fbdc4b5 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 25 May 2021 13:21:47 +0200 Subject: [PATCH 137/217] Handle registered object elements. --- .../src/datafilter.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index faf88bbf87b..d98d57781c4 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -273,16 +273,21 @@ export default class DataFilter extends Plugin { * @returns {Boolean} */ _handleObjectElement( definition ) { - if ( !definition.isObject ) { - return false; - } - const editor = this.editor; const schema = editor.model.schema; const conversion = editor.conversion; const { view: viewName, model: modelName } = definition; - schema.register( definition.model, definition.modelSchema ); + if ( !definition.isObject ) { + return false; + } + + // If feature is already registered, #_handleBlockElement should take care of it. + if ( schema.isRegistered( modelName ) ) { + return false; + } + + schema.register( modelName, definition.modelSchema ); if ( !viewName ) { return; From 2b6fc2c5de5667d814c44f2d3b5d4959f328e97c Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 25 May 2021 13:34:13 +0200 Subject: [PATCH 138/217] Added converter priority for object elements. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index d98d57781c4..674c6ae3cdc 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -302,7 +302,10 @@ export default class DataFilter extends Plugin { conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); conversion.for( 'upcast' ).elementToElement( { view: viewName, - model: viewToModelObjectConverter( definition, this._allowedAttributes ) + model: viewToModelObjectConverter( definition, this._allowedAttributes ), + // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure + // this listener is called before it. If not, some elements will be transformed into a paragraph. + converterPriority: priorities.get( 'low' ) + 1 } ); conversion.for( 'dataDowncast' ).elementToElement( { From a76e1038dc36d6eea82b3c25b9b3e36a6a0f9d7c Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 25 May 2021 13:52:58 +0200 Subject: [PATCH 139/217] Register missing schema keys. --- .../src/converters.js | 6 ++--- .../src/datafilter.js | 8 ++++++ .../tests/datafilter.js | 26 +++++++++---------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index 0056969ff01..2be1c9f02b0 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -78,7 +78,7 @@ export function modelToViewCodeBlockAttributeConverter() { /** * View-to-model conversion helper for object elements. * - * Preserves object element content in `value` attribute. Also, all matching attributes + * Preserves object element content in `htmlContent` attribute. Also, all matching attributes * by the given matcher will be preserved on `htmlAttributes` attribute. * * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition @@ -91,7 +91,7 @@ export function viewToModelObjectConverter( { model: modelName }, matcher ) { // Let's keep element HTML and its attributes, so we can rebuild element in downcast conversions. return conversionApi.writer.createElement( modelName, { - value: viewElement.getCustomProperty( '$rawContent' ), + htmlContent: viewElement.getCustomProperty( '$rawContent' ), ...( htmlAttributes && { htmlAttributes } ) } ); }; @@ -139,7 +139,7 @@ export function toObjectWidgetConverter( editor, { view: viewName, isInline } ) */ export function createObjectView( viewName, modelElement, writer ) { const viewAttributes = modelElement.getAttribute( 'htmlAttributes' ); - const viewContent = modelElement.getAttribute( 'value' ); + const viewContent = modelElement.getAttribute( 'htmlContent' ); const viewElement = writer.createRawElement( viewName, null, function( domElement ) { domElement.innerHTML = viewContent; diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 674c6ae3cdc..78594c506ba 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -293,6 +293,10 @@ export default class DataFilter extends Plugin { return; } + schema.extend( definition.model, { + allowAttributes: [ 'htmlAttributes', 'htmlContent' ] + } ); + // Store element content in special `$rawContent` custom property to // avoid editor's data filtering mechanism. editor.data.registerRawContentMatcher( { @@ -364,6 +368,10 @@ export default class DataFilter extends Plugin { return; } + schema.extend( definition.model, { + allowAttributes: 'htmlAttributes' + } ); + conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this._allowedAttributes ) ); conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index fe556323579..0619cf10eda 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -325,7 +325,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + '' ); expect( editor.getData() ).to.equal( '

    ' ); @@ -341,7 +341,7 @@ describe( 'DataFilter', () => { expect( getModelData( model, { withoutSelection: true } ) ).to.equal( '' + - '' + + '' + ' Your browser does not support the video tag.">' + '' ); @@ -368,7 +368,7 @@ describe( 'DataFilter', () => { editor.setData( 'foobar' ); expect( getModelData( model, { withoutSelection: true } ) ).to.equal( - '' + '' ); expect( editor.getData() ).to.equal( 'foobar' ); @@ -381,7 +381,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '', + data: '', attributes: { 1: { attributes: { @@ -401,7 +401,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '', + data: '', attributes: { 1: { styles: { @@ -421,7 +421,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '', + data: '', attributes: { 1: { classes: [ 'foobar' ] @@ -441,8 +441,8 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '' + - '' + + '' + + '' + '', attributes: { 1: { @@ -465,8 +465,8 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '' + - '' + + '' + + '' + '', attributes: { 1: { @@ -489,8 +489,8 @@ describe( 'DataFilter', () => { expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '' + - '' + + '' + + '' + '', attributes: {} } ); @@ -1465,7 +1465,7 @@ describe( 'DataFilter', () => { let attributes = []; for ( const item of range.getItems() ) { for ( const [ key, value ] of sortAttributes( item.getAttributes() ) ) { - if ( key.startsWith( 'html' ) ) { + if ( key.startsWith( 'html' ) && key !== 'htmlContent' ) { attributes.push( value ); } } From ef3341497f75938429d6db5c7ac3a53ce0ba35f8 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 25 May 2021 13:54:47 +0200 Subject: [PATCH 140/217] Separated t from editor. --- packages/ckeditor5-content-compatibility/src/converters.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index 2be1c9f02b0..781cb7731b1 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -105,8 +105,10 @@ export function viewToModelObjectConverter( { model: modelName }, matcher ) { * @returns {Function} Returns a conversion callback. */ export function toObjectWidgetConverter( editor, { view: viewName, isInline } ) { + const t = editor.t; + return ( modelElement, { writer } ) => { - const widgetLabel = editor.t( 'HTML object' ); + const widgetLabel = t( 'HTML object' ); // Widget cannot be a raw element because the widget system would not be able // to add its UI to it. Thus, we need separate view container. From 957b8f7de9e2f5b0264a6646220bf57907288231 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 25 May 2021 13:57:02 +0200 Subject: [PATCH 141/217] Removed rtl support. --- .../ckeditor5-content-compatibility/src/converters.js | 3 +-- .../ckeditor5-content-compatibility/theme/datafilter.css | 9 ++------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index 781cb7731b1..d89cd26f26b 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -114,8 +114,7 @@ export function toObjectWidgetConverter( editor, { view: viewName, isInline } ) // to add its UI to it. Thus, we need separate view container. const viewContainer = writer.createContainerElement( isInline ? 'span' : 'div', { class: 'html-object-embed', - 'data-html-object-embed-label': widgetLabel, - dir: editor.locale.uiLanguageDirection + 'data-html-object-embed-label': widgetLabel }, { isAllowedInsideAttributeElement: isInline } ); diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index 647e4f7cccc..079295f49cc 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -30,13 +30,8 @@ font-family: var(--ck-font-face); } - &[dir="rtl"]::before { - left: auto; - right: var(--ck-spacing-standard); - } - - /* Make space for label but it only collides in LTR languages */ - &[dir="ltr"] .ck-widget__type-around .ck-widget__type-around__button.ck-widget__type-around__button_before { + /* Make space for label. */ + & .ck-widget__type-around .ck-widget__type-around__button.ck-widget__type-around__button_before { margin-left: 50px; } From a74247e86acb9b155680fe1cc33653cf2dac057d Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 25 May 2021 14:05:17 +0200 Subject: [PATCH 142/217] Compose block helper conversions instead of separate attribute conversion for objects. --- .../src/converters.js | 21 ++++--------------- .../src/datafilter.js | 4 +++- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index d89cd26f26b..87d2ff4f997 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -82,17 +82,13 @@ export function modelToViewCodeBlockAttributeConverter() { * by the given matcher will be preserved on `htmlAttributes` attribute. * * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition - * @param {module:engine/view/matcher~Matcher} matcher * @returns {Function} Returns a conversion callback. */ -export function viewToModelObjectConverter( { model: modelName }, matcher ) { +export function viewToModelObjectConverter( { model: modelName } ) { return ( viewElement, conversionApi ) => { - const htmlAttributes = consumeViewAttributes( viewElement, conversionApi, matcher ); - // Let's keep element HTML and its attributes, so we can rebuild element in downcast conversions. return conversionApi.writer.createElement( modelName, { - htmlContent: viewElement.getCustomProperty( '$rawContent' ), - ...( htmlAttributes && { htmlAttributes } ) + htmlContent: viewElement.getCustomProperty( '$rawContent' ) } ); }; } @@ -139,18 +135,9 @@ export function toObjectWidgetConverter( editor, { view: viewName, isInline } ) * @returns {module:engine/view/element~Element} */ export function createObjectView( viewName, modelElement, writer ) { - const viewAttributes = modelElement.getAttribute( 'htmlAttributes' ); - const viewContent = modelElement.getAttribute( 'htmlContent' ); - - const viewElement = writer.createRawElement( viewName, null, function( domElement ) { - domElement.innerHTML = viewContent; + return writer.createRawElement( viewName, null, function( domElement ) { + domElement.innerHTML = modelElement.getAttribute( 'htmlContent' ); } ); - - if ( viewAttributes ) { - setViewAttributes( writer, viewAttributes, viewElement ); - } - - return viewElement; } /** diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 78594c506ba..9317d7990cd 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -306,11 +306,12 @@ export default class DataFilter extends Plugin { conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); conversion.for( 'upcast' ).elementToElement( { view: viewName, - model: viewToModelObjectConverter( definition, this._allowedAttributes ), + model: viewToModelObjectConverter( definition ), // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure // this listener is called before it. If not, some elements will be transformed into a paragraph. converterPriority: priorities.get( 'low' ) + 1 } ); + conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this._allowedAttributes ) ); conversion.for( 'dataDowncast' ).elementToElement( { model: modelName, @@ -322,6 +323,7 @@ export default class DataFilter extends Plugin { model: modelName, view: toObjectWidgetConverter( editor, definition ) } ); + conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) ); } /** From 186433f5a6a87ed017de45d3ceec04bb3ecde54c Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 26 May 2021 12:23:17 +0200 Subject: [PATCH 143/217] Separate code block from data filter, improve conversion --- .../src/conversionutils.js | 100 ------ .../src/converters.js | 77 +---- .../src/datafilter.js | 286 ++++++++++++------ .../src/dataschema.js | 8 + .../src/generalhtmlsupport.js | 7 +- .../src/integrations/codeblock.js | 103 +++++++ .../tests/_utils/utils.js | 79 +++++ .../tests/codeblock.js | 219 ++++++++++++++ .../tests/datafilter.js | 248 +-------------- .../tests/manual/codeblock.html | 8 +- 10 files changed, 644 insertions(+), 491 deletions(-) create mode 100644 packages/ckeditor5-content-compatibility/src/integrations/codeblock.js create mode 100644 packages/ckeditor5-content-compatibility/tests/_utils/utils.js create mode 100644 packages/ckeditor5-content-compatibility/tests/codeblock.js diff --git a/packages/ckeditor5-content-compatibility/src/conversionutils.js b/packages/ckeditor5-content-compatibility/src/conversionutils.js index a55dd09b745..960d572b40c 100644 --- a/packages/ckeditor5-content-compatibility/src/conversionutils.js +++ b/packages/ckeditor5-content-compatibility/src/conversionutils.js @@ -9,41 +9,6 @@ import { cloneDeep } from 'lodash-es'; -/** - * Matches and consumes the given view attributes. - * - * @param {module:engine/view/element~Element} viewElement - * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi - * @param {module:engine/view/matcher~Matcher Matcher} matcher - * @returns {Object} [result] - * @returns {Object} result.attributes Set with matched attribute names. - * @returns {Object} result.styles Set with matched style names. - * @returns {Array.} result.classes Set with matched class names. - */ -export function consumeViewAttributes( viewElement, conversionApi, matcher ) { - const matches = consumeAttributeMatches( viewElement, conversionApi, matcher ); - const { attributes, styles, classes } = mergeMatchResults( matches ); - const viewAttributes = {}; - - if ( attributes.size ) { - viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); - } - - if ( styles.size ) { - viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); - } - - if ( classes.size ) { - viewAttributes.classes = Array.from( classes ); - } - - if ( !Object.keys( viewAttributes ).length ) { - return null; - } - - return viewAttributes; -} - /** * Helper function for downcast converter. Sets attributes on the given view element. * @@ -91,68 +56,3 @@ export function mergeViewElementAttributes( oldValue, newValue ) { return result; } - -// Consumes matched attributes. -// -// @private -// @param {module:engine/view/element~Element} viewElement -// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi -// @param {module:engine/view/matcher~Matcher Matcher} matcher -// @returns {Array.} Array with match information about found attributes. -function consumeAttributeMatches( viewElement, { consumable }, matcher ) { - const matches = matcher.matchAll( viewElement ) || []; - const consumedMatches = []; - - for ( const match of matches ) { - // We only want to consume attributes, so element can be still processed by other converters. - delete match.match.name; - - if ( consumable.consume( viewElement, match.match ) ) { - consumedMatches.push( match ); - } - } - - return consumedMatches; -} - -// Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. -// -// @private -// @param {Array.} matches -// @returns {Object} result -// @returns {Set.} result.attributes Set with matched attribute names. -// @returns {Set.} result.styles Set with matched style names. -// @returns {Set.} result.classes Set with matched class names. -function mergeMatchResults( matches ) { - const matchResult = { - attributes: new Set(), - classes: new Set(), - styles: new Set() - }; - - for ( const match of matches ) { - for ( const key in matchResult ) { - const values = match.match[ key ] || []; - - values.forEach( value => matchResult[ key ].add( value ) ); - } - } - - return matchResult; -} - -// Converts the given iterable object into an object. -// -// @private -// @param {Iterable.} iterable -// @param {Function} getValue Should result with value for the given object key. -// @returns {Object} -function iterableToObject( iterable, getValue ) { - const attributesObject = {}; - - for ( const prop of iterable ) { - attributesObject[ prop ] = getValue( prop ); - } - - return attributesObject; -} diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index 87d2ff4f997..a3a847e0b7a 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -8,78 +8,31 @@ */ import { toWidget } from 'ckeditor5/src/widget'; -import { consumeViewAttributes, setViewAttributes, mergeViewElementAttributes } from './conversionutils'; +import { setViewAttributes, mergeViewElementAttributes } from './conversionutils'; /** - * Conversion helper consuming all attributes from the definition view element - * matched by the given matcher. + * Conversion helper consuming all disallowed attributes from the definition view element. * * This converter listenes on `high` priority to ensure that all attributes are consumed * before standard priority converters. * * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition - * @param {module:engine/view/matcher~Matcher} matcher + * @param {module:content-compatibility/datafilter~DataFilter} dataFilter * @returns {Function} Returns a conversion callback. */ -export function consumeViewAttributesConverter( { view: viewName }, matcher ) { +export function disallowedAttributesConverter( { view: viewName }, dataFilter ) { return dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - consumeViewAttributes( data.viewItem, conversionApi, matcher ); + dataFilter.consumeDisallowedAttributes( data.viewItem, conversionApi ); }, { priority: 'high' } ); }; } -/** - * View-to-model conversion helper preserving attributes on {@link module:code-block/codeblock~CodeBlock Code Block} - * feature model element matched by the given matcher. - * - * Attributes are preserved as a value of `htmlAttributes` model attribute. - * - * @param {module:engine/view/matcher~Matcher} matcher - * @returns {Function} Returns a conversion callback. -*/ -export function viewToModelCodeBlockAttributeConverter( matcher ) { - return dispatcher => { - dispatcher.on( 'element:code', ( evt, data, conversionApi ) => { - const viewPreElement = data.viewItem.parent; - - if ( !viewPreElement || !viewPreElement.is( 'element', 'pre' ) ) { - return; - } - - const viewAttributes = consumeViewAttributes( viewPreElement, conversionApi, matcher ); - - if ( viewAttributes ) { - conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange ); - } - }, { conversionPriority: 'low' } ); - }; -} - -/** - * Model-to-view conversion helper applying attributes from {@link module:code-block/codeblock~CodeBlock Code Block} - * feature model element. - * - * @returns {Function} Returns a conversion callback. -*/ -export function modelToViewCodeBlockAttributeConverter() { - return dispatcher => { - dispatcher.on( 'attribute:htmlAttributes:codeBlock', ( evt, data, conversionApi ) => { - if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { - return; - } - - const viewPreElement = conversionApi.mapper.toViewElement( data.item ).parent; - setViewAttributes( conversionApi.writer, data.attributeNewValue, viewPreElement ); - } ); - }; -} - /** * View-to-model conversion helper for object elements. * - * Preserves object element content in `htmlContent` attribute. Also, all matching attributes - * by the given matcher will be preserved on `htmlAttributes` attribute. + * Preserves object element content in `htmlContent` attribute. Also, all allowed attributes + * will be preserved on `htmlAttributes` attribute. * * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition * @returns {Function} Returns a conversion callback. @@ -143,18 +96,18 @@ export function createObjectView( viewName, modelElement, writer ) { /** * View-to-attribute conversion helper preserving inline element attributes on `$text`. * - * All element attributes matched by the given matcher will be preserved as a value of + * All allowed element attributes will be preserved as a value of * {@link module:content-compatibility/dataschema~DataSchemaInlineElementDefinition~model definition model} * attribute. * * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition - * @param {module:engine/view/matcher~Matcher} matcher + * @param {module:content-compatibility/datafilter~DataFilter} dataFilter * @returns {Function} Returns a conversion callback. */ -export function viewToAttributeInlineConverter( { view: viewName, model: attributeKey }, matcher ) { +export function viewToAttributeInlineConverter( { view: viewName, model: attributeKey }, dataFilter ) { return dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - const viewAttributes = consumeViewAttributes( data.viewItem, conversionApi, matcher ); + const viewAttributes = dataFilter.consumeAllowedAttributes( data.viewItem, conversionApi ); // Since we are converting to attribute we need a range on which we will set the attribute. // If the range is not created yet, we will create it. @@ -199,22 +152,22 @@ export function attributeToViewInlineConverter( { priority, view: viewName } ) { } /** - * View-to-model conversion helper preserving attributes on block element matched by the given matcher. + * View-to-model conversion helper preserving allowed attributes on block element. * * All matched attributes will be preserved on `htmlAttributes` attribute. * * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition - * @param {module:engine/view/matcher~Matcher} matcher + * @param {module:content-compatibility/datafilter~DataFilter} dataFilter * @returns {Function} Returns a conversion callback. */ -export function viewToModelBlockAttributeConverter( { view: viewName }, matcher ) { +export function viewToModelBlockAttributeConverter( { view: viewName }, dataFilter ) { return dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { if ( !data.modelRange ) { return; } - const viewAttributes = consumeViewAttributes( data.viewItem, conversionApi, matcher ); + const viewAttributes = dataFilter.consumeAllowedAttributes( data.viewItem, conversionApi ); if ( viewAttributes ) { conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange ); diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 9317d7990cd..248d2bab0d6 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -14,10 +14,7 @@ import { Matcher } from 'ckeditor5/src/engine'; import { priorities, CKEditorError } from 'ckeditor5/src/utils'; import { Widget } from 'ckeditor5/src/widget'; import { - consumeViewAttributesConverter, - - viewToModelCodeBlockAttributeConverter, - modelToViewCodeBlockAttributeConverter, + disallowedAttributesConverter, viewToModelObjectConverter, toObjectWidgetConverter, @@ -112,6 +109,7 @@ export default class DataFilter extends Plugin { this._dataInitialized = false; this._registerElementsAfterInit(); + this._registerElementHandlers(); } /** @@ -149,7 +147,7 @@ export default class DataFilter extends Plugin { // If the data has not been initialized yet, _registerElementsAfterInit() method will take care of // registering elements. if ( this._dataInitialized ) { - this._registerElement( definition ); + this._fireRegisterEvent( definition ); } } @@ -174,6 +172,34 @@ export default class DataFilter extends Plugin { this._disallowedAttributes.add( config ); } + /** + * Matches and consumes allowed view attributes. + * + * @param {module:engine/view/element~Element} viewElement + * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi + * @returns {Object} [result] + * @returns {Object} result.attributes Set with matched attribute names. + * @returns {Object} result.styles Set with matched style names. + * @returns {Array.} result.classes Set with matched class names. + */ + consumeAllowedAttributes( viewElement, conversionApi ) { + return consumeAttributes( viewElement, conversionApi, this._allowedAttributes ); + } + + /** + * Matches and consumes disallowed view attributes. + * + * @param {module:engine/view/element~Element} viewElement + * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi + * @returns {Object} [result] + * @returns {Object} result.attributes Set with matched attribute names. + * @returns {Object} result.styles Set with matched style names. + * @returns {Array.} result.classes Set with matched class names. + */ + consumeDisallowedAttributes( viewElement, conversionApi ) { + return consumeAttributes( viewElement, conversionApi, this._disallowedAttributes ); + } + /** * Registers elements allowed by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method * once {@link module:core/editor~Editor#data editor's data controller} is initialized. @@ -185,7 +211,7 @@ export default class DataFilter extends Plugin { this._dataInitialized = true; for ( const definition of this._allowedElements ) { - this._registerElement( definition ); + this._fireRegisterEvent( definition ); } }, { // With high priority listener we are able to register elements right before @@ -196,73 +222,48 @@ export default class DataFilter extends Plugin { } /** - * Registers element and attribute converters for the given data schema definition. + * Registers default element handlers. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition */ - _registerElement( definition ) { - // Note that the order of element handlers is important, - // as the handler may interrupt handlers execution in case of returning - // anything else than `false` value. - const elementHandlers = [ - this._handleCodeBlockElement, - this._handleObjectElement, - this._handleInlineElement, - this._handleBlockElement - ]; - - for ( const elementHandler of elementHandlers ) { - if ( elementHandler.call( this, definition ) !== false ) { - return; + _registerElementHandlers() { + this.on( 'register', ( evt, definition ) => { + const schema = this.editor.model.schema; + + // Object element should be only registered for new features. + if ( definition.isObject && !schema.isRegistered( definition.model ) ) { + this._registerObjectElement( definition ); + } else if ( definition.isBlock ) { + this._registerBlockElement( definition ); + } else if ( definition.isInline ) { + this._registerInlineElement( definition ); + } else { + /** + * The definition cannot be handled by the data filter. + * + * Make sure that the registered definition is correct. + * + * @error data-filter-invalid-definition + */ + throw new CKEditorError( + 'data-filter-invalid-definition', + null, + definition + ); } - } - /** - * The definition cannot be handled by the data filter. - * - * Make sure that the registered definition is correct. - * - * @error data-filter-invalid-definition - */ - throw new CKEditorError( - 'data-filter-invalid-definition', - null, - definition - ); + evt.stop(); + }, { priority: 'lowest' } ); } /** - * Registers attribute converters for {@link module:code-block/codeblock~CodeBlock Code Block} feature. + * Fires `register` event for the given element definition. * * @private * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition - * @returns {Boolean} */ - _handleCodeBlockElement( definition ) { - const editor = this.editor; - - // We should only handle codeBlock model if CodeBlock plugin is available. - // Otherwise, let #_handleBlockElement() do the job. - if ( !editor.plugins.has( 'CodeBlock' ) || definition.model !== 'codeBlock' ) { - return false; - } - - const schema = editor.model.schema; - const conversion = editor.conversion; - - // CodeBlock plugin is filtering out all attributes on `code` element. Let's add - // exception for `htmlCode` required for data filtration mechanism. - schema.on( 'checkAttribute', ( evt, [ context, attributeName ] ) => { - if ( attributeName === 'htmlCode' && context.endsWith( 'codeBlock $text' ) ) { - evt.return = true; - evt.stop(); - } - }, { priority: priorities.get( 'high' ) + 1 } ); - - conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); - conversion.for( 'upcast' ).add( viewToModelCodeBlockAttributeConverter( this._allowedAttributes ) ); - conversion.for( 'downcast' ).add( modelToViewCodeBlockAttributeConverter() ); + _fireRegisterEvent( definition ) { + this.fire( definition.view ? `register:${ definition.view }` : 'register', definition ); } /** @@ -272,21 +273,12 @@ export default class DataFilter extends Plugin { * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition * @returns {Boolean} */ - _handleObjectElement( definition ) { + _registerObjectElement( definition ) { const editor = this.editor; const schema = editor.model.schema; const conversion = editor.conversion; const { view: viewName, model: modelName } = definition; - if ( !definition.isObject ) { - return false; - } - - // If feature is already registered, #_handleBlockElement should take care of it. - if ( schema.isRegistered( modelName ) ) { - return false; - } - schema.register( modelName, definition.modelSchema ); if ( !viewName ) { @@ -303,7 +295,7 @@ export default class DataFilter extends Plugin { name: viewName } ); - conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); + conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, this ) ); conversion.for( 'upcast' ).elementToElement( { view: viewName, model: viewToModelObjectConverter( definition ), @@ -311,7 +303,7 @@ export default class DataFilter extends Plugin { // this listener is called before it. If not, some elements will be transformed into a paragraph. converterPriority: priorities.get( 'low' ) + 1 } ); - conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this._allowedAttributes ) ); + conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this ) ); conversion.for( 'dataDowncast' ).elementToElement( { model: modelName, @@ -329,17 +321,11 @@ export default class DataFilter extends Plugin { /** * Registers block element and attribute converters for the given data schema definition. * - * If the element model schema is already registered, this method will do nothing. - * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition * @returns {Boolean} */ - _handleBlockElement( definition ) { - if ( !definition.isBlock ) { - return false; - } - + _registerBlockElement( definition ) { const editor = this.editor; const schema = editor.model.schema; const conversion = editor.conversion; @@ -374,8 +360,8 @@ export default class DataFilter extends Plugin { allowAttributes: 'htmlAttributes' } ); - conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); - conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this._allowedAttributes ) ); + conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, this ) ); + conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this ) ); conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) ); } @@ -388,11 +374,7 @@ export default class DataFilter extends Plugin { * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition * @returns {Boolean} */ - _handleInlineElement( definition ) { - if ( !definition.isInline ) { - return false; - } - + _registerInlineElement( definition ) { const editor = this.editor; const schema = editor.model.schema; const conversion = editor.conversion; @@ -406,12 +388,138 @@ export default class DataFilter extends Plugin { schema.setAttributeProperties( attributeKey, definition.attributeProperties ); } - conversion.for( 'upcast' ).add( consumeViewAttributesConverter( definition, this._disallowedAttributes ) ); - conversion.for( 'upcast' ).add( viewToAttributeInlineConverter( definition, this._allowedAttributes ) ); + conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, this ) ); + conversion.for( 'upcast' ).add( viewToAttributeInlineConverter( definition, this ) ); conversion.for( 'downcast' ).attributeToElement( { model: attributeKey, view: attributeToViewInlineConverter( definition ) } ); } + + /** + * Fired when {@link module:content-compatibility/datafilter~DataFilter} is registering element and attribute + * converters for the {@link module:content-compatibility/dataschema~DataSchemaDefinition element definition}. + * + * The event also accepts {@link module:content-compatibility/dataschema~DataSchemaDefinition#view} value + * as an event namespace, e.g. `register:span`. + * + * dataFilter.on( 'register', ( evt, definition ) => { + * editor.schema.register( definition.model, definition.modelSchema ); + * editor.conversion.elementToElement( { model: definition.model, view: definition.view } ); + * + * evt.stop(); + * } ); + * + * dataFilter.on( 'register:span', ( evt, definition ) => { + * editor.schema.extend( '$text', { allowAttributes: 'htmlSpan' } ); + * + * editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'span', model: 'htmlSpan' } ); + * editor.conversion.for( 'downcast' ).attributeToElement( { view: 'span', model: 'htmlSpan' } ); + * + * evt.stop(); + * }, { priority: 'high' } ) + * + * @event register + * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + */ +} + +// Matches and consumes the given view attributes. +// +// @private +// @param {module:engine/view/element~Element} viewElement +// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi +// @param {module:engine/view/matcher~Matcher Matcher} matcher +// @returns {Object} [result] +// @returns {Object} result.attributes Set with matched attribute names. +// @returns {Object} result.styles Set with matched style names. +// @returns {Array.} result.classes Set with matched class names. +function consumeAttributes( viewElement, conversionApi, matcher ) { + const matches = consumeAttributeMatches( viewElement, conversionApi, matcher ); + const { attributes, styles, classes } = mergeMatchResults( matches ); + const viewAttributes = {}; + + if ( attributes.size ) { + viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); + } + + if ( styles.size ) { + viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); + } + + if ( classes.size ) { + viewAttributes.classes = Array.from( classes ); + } + + if ( !Object.keys( viewAttributes ).length ) { + return null; + } + + return viewAttributes; +} + +// Consumes matched attributes. +// +// @private +// @param {module:engine/view/element~Element} viewElement +// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi +// @param {module:engine/view/matcher~Matcher Matcher} matcher +// @returns {Array.} Array with match information about found attributes. +function consumeAttributeMatches( viewElement, { consumable }, matcher ) { + const matches = matcher.matchAll( viewElement ) || []; + const consumedMatches = []; + + for ( const match of matches ) { + // We only want to consume attributes, so element can be still processed by other converters. + delete match.match.name; + + if ( consumable.consume( viewElement, match.match ) ) { + consumedMatches.push( match ); + } + } + + return consumedMatches; +} + +// Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. +// +// @private +// @param {Array.} matches +// @returns {Object} result +// @returns {Set.} result.attributes Set with matched attribute names. +// @returns {Set.} result.styles Set with matched style names. +// @returns {Set.} result.classes Set with matched class names. +function mergeMatchResults( matches ) { + const matchResult = { + attributes: new Set(), + classes: new Set(), + styles: new Set() + }; + + for ( const match of matches ) { + for ( const key in matchResult ) { + const values = match.match[ key ] || []; + + values.forEach( value => matchResult[ key ].add( value ) ); + } + } + + return matchResult; +} + +// Converts the given iterable object into an object. +// +// @private +// @param {Iterable.} iterable +// @param {Function} getValue Should result with value for the given object key. +// @returns {Object} +function iterableToObject( iterable, getValue ) { + const attributesObject = {}; + + for ( const prop of iterable ) { + attributesObject[ prop ] = getValue( prop ); + } + + return attributesObject; } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-content-compatibility/src/dataschema.js index 94641027b67..2eb74f3cac4 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-content-compatibility/src/dataschema.js @@ -140,6 +140,14 @@ export default class DataSchema extends Plugin { view: 'li' } ); + this.registerBlockElement( { + model: 'htmlPre', + view: 'pre', + modelSchema: { + inheritAllFrom: '$block' + } + } ); + this.registerBlockElement( { view: 'article', model: 'htmlArticle', diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index ce614f769e0..9bf7e0c6ced 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -10,6 +10,7 @@ import { Plugin } from 'ckeditor5/src/core'; import DataFilter from './datafilter'; import DataSchema from './dataschema'; +import CodeBlockHtmlSupport from './integrations/codeblock'; /** * The General HTML Support feature. @@ -31,6 +32,10 @@ export default class GeneralHtmlSupport extends Plugin { * @inheritDoc */ static get requires() { - return [ DataFilter, DataSchema ]; + return [ + DataFilter, + DataSchema, + CodeBlockHtmlSupport + ]; } } diff --git a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js new file mode 100644 index 00000000000..f8866d25ee1 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js @@ -0,0 +1,103 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module content-compatibility/integrations/codeblock + */ + +import { Plugin } from 'ckeditor5/src/core'; +import { disallowedAttributesConverter } from '../converters'; +import { setViewAttributes } from '../conversionutils.js'; + +import DataFilter from '../datafilter'; + +export default class CodeBlockHtmlSupport extends Plugin { + static get requires() { + return [ DataFilter ]; + } + + init() { + if ( !this.editor.plugins.has( 'CodeBlock' ) ) { + return; + } + + const dataFilter = this.editor.plugins.get( DataFilter ); + + dataFilter.on( 'register:pre', ( evt, definition ) => { + if ( definition.model !== 'codeBlock' ) { + return; + } + + const editor = this.editor; + const conversion = editor.conversion; + + conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, dataFilter ) ); + conversion.for( 'upcast' ).add( viewToModelCodeBlockAttributeConverter( dataFilter ) ); + conversion.for( 'downcast' ).add( modelToViewCodeBlockAttributeConverter() ); + + evt.stop(); + } ); + } +} + +// View-to-model conversion helper preserving allowed attributes on {@link module:code-block/codeblock~CodeBlock Code Block} +// feature model element. +// +// Attributes are preserved as a value of `htmlAttributes` model attribute. +// +// @param {module:content-compatibility/datafilter~DataFilter} dataFilter +// @returns {Function} Returns a conversion callback. +function viewToModelCodeBlockAttributeConverter( dataFilter ) { + return dispatcher => { + dispatcher.on( 'element:code', ( evt, data, conversionApi ) => { + const viewCodeElement = data.viewItem; + const viewPreElement = viewCodeElement.parent; + + if ( !viewPreElement || !viewPreElement.is( 'element', 'pre' ) ) { + return; + } + + preserveElementAttributes( viewPreElement, 'htmlAttributes' ); + preserveElementAttributes( viewCodeElement, 'htmlContentAttributes' ); + + function preserveElementAttributes( viewElement, attributeName ) { + const viewAttributes = dataFilter.consumeAllowedAttributes( viewElement, conversionApi ); + + if ( viewAttributes ) { + conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange ); + } + } + }, { priority: 'low' } ); + }; +} + +// Model-to-view conversion helper applying attributes from {@link module:code-block/codeblock~CodeBlock Code Block} +// feature model element. +// +// @returns {Function} Returns a conversion callback. +function modelToViewCodeBlockAttributeConverter() { + return dispatcher => { + dispatcher.on( 'attribute:htmlAttributes:codeBlock', ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewCodeElement = conversionApi.mapper.toViewElement( data.item ); + const viewPreElement = viewCodeElement.parent; + + setViewAttributes( conversionApi.writer, data.attributeNewValue, viewPreElement ); + } ); + + dispatcher.on( 'attribute:htmlContentAttributes:codeBlock', ( evt, data, conversionApi ) => { + if ( !conversionApi.consumable.consume( data.item, evt.name ) ) { + return; + } + + const viewCodeElement = conversionApi.mapper.toViewElement( data.item ); + + setViewAttributes( conversionApi.writer, data.attributeNewValue, viewCodeElement ); + } ); + }; +} diff --git a/packages/ckeditor5-content-compatibility/tests/_utils/utils.js b/packages/ckeditor5-content-compatibility/tests/_utils/utils.js new file mode 100644 index 00000000000..3c7ae979f86 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/_utils/utils.js @@ -0,0 +1,79 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +/** + * Writes the content of a model {@link module:engine/model/document~Document document} to an HTML-like string with + * indexed HTML Support attributes. + * + * getModelDataWithAttributes( editor.model ); + * // -> { + * // data: '<$text htmlSpan="(2)">foobar!', + * // attributes: { + * // 1: { classes: [ 'foo', 'bar' ] }, + * // 2: { attributes: { 'data-foo': 'foo' } } + * // } + * // } + * + * This function will index every attribute starting with `html*` keyword and return it's value in `result.attributes` property. + * + * @param {module:engine/model/model~Model} model + * @param {Object} [options] + * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will + * not be included in the returned string. + * @param {String} [options.rootName='main'] The name of the root from which the data should be stringified. If not provided, + * the default `main` name will be used. + * @param {Boolean} [options.convertMarkers=false] Whether to include markers in the returned string. + * @param {Array} [options.excludeAttributes] Attributes to exclude from the result. + * @returns {Object} result + * @returns {String} result.data The stringified data. + * @returns {Object} result.attributes Indexed data attributes. + */ +export function getModelDataWithAttributes( model, options ) { + // Simplify GHS attributes as they are not very readable at this point due to object structure. + let counter = 1; + const data = getModelData( model, options ).replace( /(html.*?)="{.*?}"/g, ( fullMatch, attributeName ) => { + return `${ attributeName }="(${ counter++ })"`; + } ); + + const range = model.createRangeIn( model.document.getRoot() ); + const excludeAttributes = options.excludeAttributes || []; + + let attributes = []; + for ( const item of range.getItems() ) { + for ( const [ key, value ] of sortAttributes( item.getAttributes() ) ) { + if ( key.startsWith( 'html' ) && !excludeAttributes.includes( key ) ) { + attributes.push( value ); + } + } + } + + attributes = attributes.reduce( ( prev, cur, index ) => { + prev[ index + 1 ] = cur; + return prev; + }, {} ); + + return { data, attributes }; +} + +function sortAttributes( attributes ) { + attributes = Array.from( attributes ); + + return attributes.sort( ( attr1, attr2 ) => { + const key1 = attr1[ 0 ]; + const key2 = attr2[ 0 ]; + + if ( key1 > key2 ) { + return 1; + } + + if ( key1 < key2 ) { + return -1; + } + + return 0; + } ); +} diff --git a/packages/ckeditor5-content-compatibility/tests/codeblock.js b/packages/ckeditor5-content-compatibility/tests/codeblock.js new file mode 100644 index 00000000000..cbed5810529 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/codeblock.js @@ -0,0 +1,219 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; +import GeneralHtmlSupport from '../src/generalhtmlsupport'; +import { getModelDataWithAttributes } from './_utils/utils'; + +/* global document */ + +describe( 'CodeBlockHtmlSupport', () => { + let editor, model, editorElement, dataFilter; + + beforeEach( () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ CodeBlock, Paragraph, GeneralHtmlSupport ] + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + + dataFilter = editor.plugins.get( 'DataFilter' ); + } ); + } ); + + afterEach( () => { + editorElement.remove(); + + return editor.destroy(); + } ); + + it( 'should allow attributes', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + }, + 2: { + attributes: { + 'data-foo': 'foo' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '
    ' +
    +			'foobar' +
    +			'
    ' ); + } ); + + it( 'should allow attributes (classes)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + classes: [ 'foo' ] + }, + 2: { + classes: [ 'foo' ] + } + } + } ); + + expect( editor.getData() ).to.equal( '
    ' +
    +			'foobar' +
    +			'
    ' ); + } ); + + it( 'should allow attributes (styles)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); + dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + background: 'blue' + } + }, + 2: { + styles: { + color: 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '
    ' +
    +			'foobar' +
    +			'
    ' ); + } ); + + it( 'should disallow attributes', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + dataFilter.disallowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: {} + } ); + + expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); + + it( 'should disallow attributes (classes)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); + dataFilter.disallowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: {} + } ); + + expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); + + it( 'should disallow attributes (styles)', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + + dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); + dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); + + dataFilter.disallowAttributes( { name: 'pre', styles: { background: 'blue' } } ); + dataFilter.disallowAttributes( { name: 'code', styles: { color: 'red' } } ); + + editor.setData( '
    foobar
    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: {} + } ); + + expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); + + it( 'should allow attributes on code element existing alone', () => { + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: 'code', attributes: { 'data-foo': /[\s\S]+/ } } ); + + editor.setData( '

    foobar

    ' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlCode="(1)">foobar', + attributes: { + 1: { + attributes: { + 'data-foo': 'foo' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

    foobar

    ' ); + } ); + + it( 'should not consume attributes already consumed (downcast)', () => { + [ 'htmlAttributes', 'htmlContentAttributes' ].forEach( attributeName => { + editor.conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( `attribute:${ attributeName }:codeBlock`, ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, evt.name ); + }, { priority: 'high' } ); + } ); + } ); + + dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': true } } ); + + editor.setData( '
    foobar' );
    +
    +		expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( {
    +			data: 'foobar',
    +			// At this point, attribute should still be in the model, as we are testing downcast conversion.
    +			attributes: {
    +				1: {
    +					attributes: {
    +						'data-foo': ''
    +					}
    +				},
    +				2: {
    +					attributes: {
    +						'data-foo': ''
    +					}
    +				}
    +			}
    +		} );
    +
    +		expect( editor.getData() ).to.equal( '
    foobar
    ' ); + } ); +} ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 0619cf10eda..d5b1f38f521 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -8,11 +8,11 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import LinkEditing from '@ckeditor/ckeditor5-link/src/linkediting'; import FontColorEditing from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorediting'; -import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock'; import DataFilter from '../src/datafilter'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { getModelDataWithAttributes } from './_utils/utils'; import GeneralHtmlSupport from '../src/generalhtmlsupport'; @@ -29,7 +29,7 @@ describe( 'DataFilter', () => { return ClassicTestEditor .create( editorElement, { - plugins: [ Paragraph, FontColorEditing, LinkEditing, CodeBlock, GeneralHtmlSupport ] + plugins: [ Paragraph, FontColorEditing, LinkEditing, GeneralHtmlSupport ] } ) .then( newEditor => { editor = newEditor; @@ -136,188 +136,6 @@ describe( 'DataFilter', () => { } } ); - describe( 'codeBlock', () => { - it( 'should allow attributes', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); - dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); - - editor.setData( '
    foobar
    ' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlCode="(2)">foobar', - attributes: { - 1: { - attributes: { - 'data-foo': 'foo' - } - }, - 2: { - attributes: { - 'data-foo': 'foo' - } - } - } - } ); - - expect( editor.getData() ).to.equal( '
    ' +
    -				'foobar' +
    -				'
    ' ); - } ); - - it( 'should allow attributes (classes)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); - dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); - - editor.setData( '
    foobar
    ' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlCode="(2)">foobar', - attributes: { - 1: { - classes: [ 'foo' ] - }, - 2: { - classes: [ 'foo' ] - } - } - } ); - - expect( editor.getData() ).to.equal( '
    ' +
    -				'foobar' +
    -				'
    ' ); - } ); - - it( 'should allow attributes (styles)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); - dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); - dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); - - editor.setData( '
    foobar
    ' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlCode="(2)">foobar', - attributes: { - 1: { - styles: { - background: 'blue' - } - }, - 2: { - styles: { - color: 'red' - } - } - } - } ); - - expect( editor.getData() ).to.equal( '
    ' +
    -				'foobar' +
    -				'
    ' ); - } ); - - it( 'should disallow attributes', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); - dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); - dataFilter.disallowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); - - editor.setData( '
    foobar
    ' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlCode="(1)">foobar', - attributes: { - 1: {} - } - } ); - - expect( editor.getData() ).to.equal( '
    foobar
    ' ); - } ); - - it( 'should disallow attributes (classes)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); - dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); - dataFilter.disallowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); - - editor.setData( '
    foobar
    ' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlCode="(1)">foobar', - attributes: { - 1: {} - } - } ); - - expect( editor.getData() ).to.equal( '
    foobar
    ' ); - } ); - - it( 'should allow attributes (styles)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); - - dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); - dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); - - dataFilter.disallowAttributes( { name: 'pre', styles: { background: 'blue' } } ); - dataFilter.disallowAttributes( { name: 'code', styles: { color: 'red' } } ); - - editor.setData( '
    foobar
    ' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlCode="(1)">foobar', - attributes: { - 1: {} - } - } ); - - expect( editor.getData() ).to.equal( '
    foobar
    ' ); - } ); - - it( 'should allow attributes on code element existing alone', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); - dataFilter.allowAttributes( { name: 'code', attributes: { 'data-foo': /[\s\S]+/ } } ); - - editor.setData( '

    foobar

    ' ); - - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlCode="(1)">foobar', - attributes: { - 1: { - attributes: { - 'data-foo': 'foo' - } - } - } - } ); - - expect( editor.getData() ).to.equal( '

    foobar

    ' ); - } ); - - it( 'should not consume attribute already consumed (downcast)', () => { - editor.conversion.for( 'downcast' ).add( dispatcher => { - dispatcher.on( 'attribute:htmlAttributes:codeBlock', ( evt, data, conversionApi ) => { - conversionApi.consumable.consume( data.item, evt.name ); - }, { priority: 'high' } ); - } ); - - dataFilter.allowElement( { name: 'pre' } ); - dataFilter.allowAttributes( { name: 'pre', attributes: { 'data-foo': true } } ); - - editor.setData( '
    foobar' );
    -
    -			expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( {
    -				data: 'foobar',
    -				// At this point, attribute should still be in the model, as we are testing downcast conversion.
    -				attributes: {
    -					1: {
    -						attributes: {
    -							'data-foo': ''
    -						}
    -					}
    -				}
    -			} );
    -
    -			expect( editor.getData() ).to.equal( '
    foobar
    ' ); - } ); - } ); - describe( 'object', () => { it( 'should allow element', () => { dataFilter.allowElement( { name: 'input' } ); @@ -380,7 +198,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + expect( getObjectModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '', attributes: { 1: { @@ -400,7 +218,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + expect( getObjectModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '', attributes: { 1: { @@ -420,7 +238,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + expect( getObjectModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '', attributes: { 1: { @@ -439,7 +257,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + expect( getObjectModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + '' + '' + @@ -463,7 +281,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + expect( getObjectModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + '' + '' + @@ -487,7 +305,7 @@ describe( 'DataFilter', () => { editor.setData( '

    ' ); - expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + expect( getObjectModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + '' + '' + @@ -497,6 +315,11 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' ); } ); + + function getObjectModelDataWithAttributes( model, options ) { + options.excludeAttributes = [ 'htmlContent' ]; + return getModelDataWithAttributes( model, options ); + } } ); describe( 'block', () => { @@ -1452,49 +1275,4 @@ describe( 'DataFilter', () => { dataFilter.allowElement( { name: 'xyz' } ); }, /data-filter-invalid-definition/, null, definition ); } ); - - function getModelDataWithAttributes( model, options ) { - // Simplify GHS attributes as they are not very readable at this point due to object structure. - let counter = 1; - const data = getModelData( model, options ).replace( /(html.*?)="{.*?}"/g, ( fullMatch, attributeName ) => { - return `${ attributeName }="(${ counter++ })"`; - } ); - - const range = model.createRangeIn( model.document.getRoot() ); - - let attributes = []; - for ( const item of range.getItems() ) { - for ( const [ key, value ] of sortAttributes( item.getAttributes() ) ) { - if ( key.startsWith( 'html' ) && key !== 'htmlContent' ) { - attributes.push( value ); - } - } - } - - attributes = attributes.reduce( ( prev, cur, index ) => { - prev[ index + 1 ] = cur; - return prev; - }, {} ); - - return { data, attributes }; - } - - function sortAttributes( attributes ) { - attributes = Array.from( attributes ); - - return attributes.sort( ( attr1, attr2 ) => { - const key1 = attr1[ 0 ]; - const key2 = attr2[ 0 ]; - - if ( key1 > key2 ) { - return 1; - } - - if ( key1 < key2 ) { - return -1; - } - - return 0; - } ); - } } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html index 8fec2981af6..6c928e2cb9c 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html @@ -4,8 +4,8 @@
    -

    Put some text in the :

    -
    Hello world!
    -

    Then set the font color:

    -
    body { color: red }
    +
    Code Block with custom properties!
    +
    Code Block with disallowed background!
    +
    Apes are strong!
    Let's check if inline elements are strong also!
    + Blue code with disallowed background!
    From 909f12c2f15562f1397a971763bde6dabb36d9c7 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Wed, 26 May 2021 13:06:49 +0200 Subject: [PATCH 144/217] Docs. --- .../src/integrations/codeblock.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js index f8866d25ee1..76af492637e 100644 --- a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js +++ b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js @@ -47,6 +47,7 @@ export default class CodeBlockHtmlSupport extends Plugin { // // Attributes are preserved as a value of `htmlAttributes` model attribute. // +// @private // @param {module:content-compatibility/datafilter~DataFilter} dataFilter // @returns {Function} Returns a conversion callback. function viewToModelCodeBlockAttributeConverter( dataFilter ) { @@ -76,6 +77,7 @@ function viewToModelCodeBlockAttributeConverter( dataFilter ) { // Model-to-view conversion helper applying attributes from {@link module:code-block/codeblock~CodeBlock Code Block} // feature model element. // +// @private // @returns {Function} Returns a conversion callback. function modelToViewCodeBlockAttributeConverter() { return dispatcher => { From 854e9182732d3363add8a958f454f868a4799594 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 1 Jun 2021 10:36:31 +0200 Subject: [PATCH 145/217] Docs, small fixes. --- .../ckeditor5-content-compatibility/src/datafilter.js | 9 +++------ .../src/integrations/codeblock.js | 2 +- .../ckeditor5-content-compatibility/theme/datafilter.css | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 248d2bab0d6..2871b491400 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -271,7 +271,6 @@ export default class DataFilter extends Plugin { * * @private * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition - * @returns {Boolean} */ _registerObjectElement( definition ) { const editor = this.editor; @@ -323,7 +322,6 @@ export default class DataFilter extends Plugin { * * @private * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition - * @returns {Boolean} */ _registerBlockElement( definition ) { const editor = this.editor; @@ -372,7 +370,6 @@ export default class DataFilter extends Plugin { * * @private * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition - * @returns {Boolean} */ _registerInlineElement( definition ) { const editor = this.editor; @@ -432,9 +429,9 @@ export default class DataFilter extends Plugin { // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi // @param {module:engine/view/matcher~Matcher Matcher} matcher // @returns {Object} [result] -// @returns {Object} result.attributes Set with matched attribute names. -// @returns {Object} result.styles Set with matched style names. -// @returns {Array.} result.classes Set with matched class names. +// @returns {Object} result.attributes +// @returns {Object} result.styles +// @returns {Array.} result.classes function consumeAttributes( viewElement, conversionApi, matcher ) { const matches = consumeAttributeMatches( viewElement, conversionApi, matcher ); const { attributes, styles, classes } = mergeMatchResults( matches ); diff --git a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js index 76af492637e..f7e846101c5 100644 --- a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js +++ b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js @@ -19,7 +19,7 @@ export default class CodeBlockHtmlSupport extends Plugin { } init() { - if ( !this.editor.plugins.has( 'CodeBlock' ) ) { + if ( !this.editor.plugins.has( 'CodeBlockEditing' ) ) { return; } diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-content-compatibility/theme/datafilter.css index 079295f49cc..d84a730fd90 100644 --- a/packages/ckeditor5-content-compatibility/theme/datafilter.css +++ b/packages/ckeditor5-content-compatibility/theme/datafilter.css @@ -36,7 +36,7 @@ } & .html-object-embed__content { - /* Disable user interation with embed content */ + /* Disable user interaction with embed content */ pointer-events: none; } } From 81af835ca0f47691c089e8ea8b5677c54a558a53 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 1 Jun 2021 10:40:27 +0200 Subject: [PATCH 146/217] Added codeBlock attributes. --- .../src/integrations/codeblock.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js index f7e846101c5..1e1a40af21a 100644 --- a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js +++ b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js @@ -31,8 +31,14 @@ export default class CodeBlockHtmlSupport extends Plugin { } const editor = this.editor; + const schema = editor.model.schema; const conversion = editor.conversion; + // Extend codeBlock to allow attributes required by attribute filtration. + schema.extend( 'codeBlock', { + allowAttributes: [ 'htmlAttributes', 'htmlContentAttributes' ] + } ); + conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, dataFilter ) ); conversion.for( 'upcast' ).add( viewToModelCodeBlockAttributeConverter( dataFilter ) ); conversion.for( 'downcast' ).add( modelToViewCodeBlockAttributeConverter() ); From 818f9365b5048fc5c006b13606907429069f4bde Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 1 Jun 2021 10:44:29 +0200 Subject: [PATCH 147/217] Removed invalid docs. --- packages/ckeditor5-content-compatibility/src/converters.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index a3a847e0b7a..85dd0b93055 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -31,8 +31,7 @@ export function disallowedAttributesConverter( { view: viewName }, dataFilter ) /** * View-to-model conversion helper for object elements. * - * Preserves object element content in `htmlContent` attribute. Also, all allowed attributes - * will be preserved on `htmlAttributes` attribute. + * Preserves object element content in `htmlContent` attribute. * * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition * @returns {Function} Returns a conversion callback. @@ -80,8 +79,6 @@ export function toObjectWidgetConverter( editor, { view: viewName, isInline } ) /** * Creates object view element from the given model element. * -* Applies attributes preserved in `htmlAttributes` model attribute. -* * @param {String} viewName * @param {module:engine/model/element~Element} modelElement * @param {module:engine/view/downcastwriter~DowncastWriter} writer From 2affa412d255d25249ba3d5d112a184642aa5ca3 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 1 Jun 2021 10:48:12 +0200 Subject: [PATCH 148/217] Improved docs. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2871b491400..1a6517624a0 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -231,6 +231,8 @@ export default class DataFilter extends Plugin { const schema = this.editor.model.schema; // Object element should be only registered for new features. + // If the model schema is already registered, it should be handled by + // #_registerBlockElement() or #_registerObjectElement() attribute handlers. if ( definition.isObject && !schema.isRegistered( definition.model ) ) { this._registerObjectElement( definition ); } else if ( definition.isBlock ) { From 5891ac44b4523c57aa9f14bf3098456e5822292a Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 1 Jun 2021 11:02:23 +0200 Subject: [PATCH 149/217] Protected API instead of public. --- packages/ckeditor5-content-compatibility/src/converters.js | 6 +++--- packages/ckeditor5-content-compatibility/src/datafilter.js | 6 ++++-- .../src/integrations/codeblock.js | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-content-compatibility/src/converters.js index 85dd0b93055..aa403cf8b78 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-content-compatibility/src/converters.js @@ -23,7 +23,7 @@ import { setViewAttributes, mergeViewElementAttributes } from './conversionutils export function disallowedAttributesConverter( { view: viewName }, dataFilter ) { return dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - dataFilter.consumeDisallowedAttributes( data.viewItem, conversionApi ); + dataFilter._consumeDisallowedAttributes( data.viewItem, conversionApi ); }, { priority: 'high' } ); }; } @@ -104,7 +104,7 @@ export function createObjectView( viewName, modelElement, writer ) { export function viewToAttributeInlineConverter( { view: viewName, model: attributeKey }, dataFilter ) { return dispatcher => { dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => { - const viewAttributes = dataFilter.consumeAllowedAttributes( data.viewItem, conversionApi ); + const viewAttributes = dataFilter._consumeAllowedAttributes( data.viewItem, conversionApi ); // Since we are converting to attribute we need a range on which we will set the attribute. // If the range is not created yet, we will create it. @@ -164,7 +164,7 @@ export function viewToModelBlockAttributeConverter( { view: viewName }, dataFilt return; } - const viewAttributes = dataFilter.consumeAllowedAttributes( data.viewItem, conversionApi ); + const viewAttributes = dataFilter._consumeAllowedAttributes( data.viewItem, conversionApi ); if ( viewAttributes ) { conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange ); diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 1a6517624a0..cc7fc13a30a 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -175,6 +175,7 @@ export default class DataFilter extends Plugin { /** * Matches and consumes allowed view attributes. * + * @protected * @param {module:engine/view/element~Element} viewElement * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi * @returns {Object} [result] @@ -182,13 +183,14 @@ export default class DataFilter extends Plugin { * @returns {Object} result.styles Set with matched style names. * @returns {Array.} result.classes Set with matched class names. */ - consumeAllowedAttributes( viewElement, conversionApi ) { + _consumeAllowedAttributes( viewElement, conversionApi ) { return consumeAttributes( viewElement, conversionApi, this._allowedAttributes ); } /** * Matches and consumes disallowed view attributes. * + * @protected * @param {module:engine/view/element~Element} viewElement * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi * @returns {Object} [result] @@ -196,7 +198,7 @@ export default class DataFilter extends Plugin { * @returns {Object} result.styles Set with matched style names. * @returns {Array.} result.classes Set with matched class names. */ - consumeDisallowedAttributes( viewElement, conversionApi ) { + _consumeDisallowedAttributes( viewElement, conversionApi ) { return consumeAttributes( viewElement, conversionApi, this._disallowedAttributes ); } diff --git a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js index 1e1a40af21a..c0fbaf273f3 100644 --- a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js +++ b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js @@ -70,7 +70,7 @@ function viewToModelCodeBlockAttributeConverter( dataFilter ) { preserveElementAttributes( viewCodeElement, 'htmlContentAttributes' ); function preserveElementAttributes( viewElement, attributeName ) { - const viewAttributes = dataFilter.consumeAllowedAttributes( viewElement, conversionApi ); + const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi ); if ( viewAttributes ) { conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange ); From 6f5a1cfc5fb101fbd4c90b683f75d4bd529b8ccd Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 7 Jun 2021 11:47:07 +0200 Subject: [PATCH 150/217] Updated docs. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 4 ++-- .../src/integrations/codeblock.js | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index cc7fc13a30a..9172972c6dd 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -41,7 +41,7 @@ import '../theme/datafilter.css'; * You can also allow or disallow specific element attributes: * * // Allow `data-foo` attribute on `section` element. - * dataFilter.allowedAttributes( { + * dataFilter.allowAttributes( { * name: 'section', * attributes: { * 'data-foo': true @@ -49,7 +49,7 @@ import '../theme/datafilter.css'; * } ); * * // Disallow `color` style attribute on 'section' element. - * dataFilter.disallowedAttributes( { + * dataFilter.disallowAttributes( { * name: 'section', * styles: { * color: /[\s\S]+/ diff --git a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js index c0fbaf273f3..56e64d98c64 100644 --- a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js +++ b/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js @@ -13,6 +13,11 @@ import { setViewAttributes } from '../conversionutils.js'; import DataFilter from '../datafilter'; +/** + * Provides the General HTML Support integration with {@link module:code-block/codeblock~CodeBlock Code Block} feature. + * + * @extends module:core/plugin~Plugin + */ export default class CodeBlockHtmlSupport extends Plugin { static get requires() { return [ DataFilter ]; From b56ba535c90396c69af8e194168405c61138954f Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Mon, 7 Jun 2021 13:43:00 +0200 Subject: [PATCH 151/217] Deps update to the latest release. --- .../package.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-content-compatibility/package.json index a349d9eee8d..93778128863 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-content-compatibility/package.json @@ -1,6 +1,6 @@ { "name": "@ckeditor/ckeditor5-content-compatibility", - "version": "27.1.0", + "version": "28.0.0", "description": "Content compatibility feature", "private": true, "keywords": [ @@ -15,24 +15,24 @@ ], "main": "src/index.js", "dependencies": { - "ckeditor5": "^27.1.0", + "ckeditor5": "^28.0.0", "lodash-es": "^4.17.15" }, "devDependencies": { - "@ckeditor/ckeditor5-basic-styles": "^27.1.0", - "@ckeditor/ckeditor5-block-quote": "^27.1.0", - "@ckeditor/ckeditor5-code-block": "^27.1.0", - "@ckeditor/ckeditor5-core": "^27.1.0", - "@ckeditor/ckeditor5-editor-classic": "^27.1.0", - "@ckeditor/ckeditor5-engine": "^27.1.0", - "@ckeditor/ckeditor5-essentials": "^27.1.0", - "@ckeditor/ckeditor5-font": "^27.1.0", - "@ckeditor/ckeditor5-highlight": "^27.1.0", - "@ckeditor/ckeditor5-link": "^27.1.0", - "@ckeditor/ckeditor5-list": "^27.1.0", - "@ckeditor/ckeditor5-paragraph": "^27.1.0", - "@ckeditor/ckeditor5-table": "^27.1.0", - "@ckeditor/ckeditor5-utils": "^27.1.0", + "@ckeditor/ckeditor5-basic-styles": "^28.0.0", + "@ckeditor/ckeditor5-block-quote": "^28.0.0", + "@ckeditor/ckeditor5-code-block": "^28.0.0", + "@ckeditor/ckeditor5-core": "^28.0.0", + "@ckeditor/ckeditor5-editor-classic": "^28.0.0", + "@ckeditor/ckeditor5-engine": "^28.0.0", + "@ckeditor/ckeditor5-essentials": "^28.0.0", + "@ckeditor/ckeditor5-font": "^28.0.0", + "@ckeditor/ckeditor5-highlight": "^28.0.0", + "@ckeditor/ckeditor5-link": "^28.0.0", + "@ckeditor/ckeditor5-list": "^28.0.0", + "@ckeditor/ckeditor5-paragraph": "^28.0.0", + "@ckeditor/ckeditor5-table": "^28.0.0", + "@ckeditor/ckeditor5-utils": "^28.0.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11" }, From 6368cb744408efeb4d83c45c810f7caf7e95d537 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Mon, 7 Jun 2021 18:10:49 +0200 Subject: [PATCH 152/217] Fixed manual test. --- .../tests/manual/codeblock.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html index 6c928e2cb9c..a6cf25bb452 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html @@ -4,8 +4,8 @@
    -
    Code Block with custom properties!
    -
    Code Block with disallowed background!
    +
    Code Block with custom properties!
    +
    Code Block with disallowed background!
    Apes are strong!
    Let's check if inline elements are strong also!
    Blue code with disallowed background!
    From 423d9c03821e2b79b0b720dffe20293359dfd570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Thu, 13 May 2021 15:25:24 +0200 Subject: [PATCH 153/217] Add data filter configuration loading. --- .../src/datafilter.js | 45 +++++++++++- .../tests/datafilter.js | 69 +++++++++++++++++++ 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 9172972c6dd..2bcbbf8539b 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -11,7 +11,7 @@ import DataSchema from './dataschema'; import { Plugin } from 'ckeditor5/src/core'; import { Matcher } from 'ckeditor5/src/engine'; -import { priorities, CKEditorError } from 'ckeditor5/src/utils'; +import { priorities, toArray, CKEditorError } from 'ckeditor5/src/utils'; import { Widget } from 'ckeditor5/src/widget'; import { disallowedAttributesConverter, @@ -126,6 +126,26 @@ export default class DataFilter extends Plugin { return [ DataSchema, Widget ]; } + /** + * Load a configuration of one or many elements, where their attributes should be allowed. + * + * @param {Array.} config Configuration of elements + * that should have their attributes accepted in the editor. + */ + loadAllowedConfig( config ) { + this._loadConfig( config ); + } + + /** + * Load a configuration of one or many elements, where their attributes should be disallowed. + * + * @param {Array.} config Configuration of elements + * that should have their attributes rejected from the editor. + */ + loadDisallowedConfig( config ) { + this._loadConfig( config, true ); + } + /** * Allow the given element in the editor context. * @@ -160,7 +180,7 @@ export default class DataFilter extends Plugin { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be allowed. */ allowAttributes( config ) { - this._allowedAttributes.add( config ); + this._allowedAttributes.add( toArray( config || [] ) ); } /** @@ -169,7 +189,26 @@ export default class DataFilter extends Plugin { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be disallowed. */ disallowAttributes( config ) { - this._disallowedAttributes.add( config ); + this._disallowedAttributes.add( toArray( config || [] ) ); + } + + /** + * Batch load of the filtering configuration. + * + * @private + * @param {Array.} config Filtering configuration. + * @param {Boolean} shouldDisallow Provided rules will reject attributes from matched elements instead of accepting them. + */ + _loadConfig( config, shouldDisallow = false ) { + for ( const { element, ...rules } of config ) { + this.allowElement( element ); + + if ( shouldDisallow ) { + this.disallowAttributes( { element, ...rules } ); + } else { + this.allowAttributes( { element, ...rules } ); + } + } } /** diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index d5b1f38f521..167c1cfce9b 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -1275,4 +1275,73 @@ describe( 'DataFilter', () => { dataFilter.allowElement( { name: 'xyz' } ); }, /data-filter-invalid-definition/, null, definition ); } ); + + describe.skip( 'fromConfig', () => { + // TODO: Better name + it( 'should load config', () => { + const config = [ + { + element: 'xyz', + attributes: { + title: 'foo' + } + } + ]; + + dataSchema.registerBlockElement( { view: 'xyz', model: 'modelXyz' } ); + dataFilter.fromConfig( config ); + + editor.setData( 'foo' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foo', + attributes: {} + } ); + } ); + } ); + + function getModelDataWithAttributes( model, options ) { + // Simplify GHS attributes as they are not very readable at this point due to object structure. + let counter = 1; + const data = getModelData( model, options ).replace( /(html.*?)="{.*?}"/g, ( fullMatch, attributeName ) => { + return `${ attributeName }="(${ counter++ })"`; + } ); + + const range = model.createRangeIn( model.document.getRoot() ); + + let attributes = []; + for ( const item of range.getItems() ) { + for ( const [ key, value ] of sortAttributes( item.getAttributes() ) ) { + if ( key.startsWith( 'html' ) ) { + attributes.push( value ); + } + } + } + + attributes = attributes.reduce( ( prev, cur, index ) => { + prev[ index + 1 ] = cur; + return prev; + }, {} ); + + return { data, attributes }; + } + + function sortAttributes( attributes ) { + attributes = Array.from( attributes ); + + return attributes.sort( ( attr1, attr2 ) => { + const key1 = attr1[ 0 ]; + const key2 = attr2[ 0 ]; + + if ( key1 > key2 ) { + return 1; + } + + if ( key1 < key2 ) { + return -1; + } + + return 0; + } ); + } } ); From f512de362ecc4397170dc59d705d51ae4c34af4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Mon, 17 May 2021 13:56:08 +0200 Subject: [PATCH 154/217] Add loading config. --- .../src/datafilter.js | 15 ++- .../tests/manual/datafilter.html | 61 ++++++++++ .../tests/manual/datafilter.js | 105 ++++++++++++++++++ .../tests/manual/datafilter.md | 1 + 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/datafilter.html create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/datafilter.js create mode 100644 packages/ckeditor5-content-compatibility/tests/manual/datafilter.md diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2bcbbf8539b..2c1e2f5dcd8 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -108,6 +108,9 @@ export default class DataFilter extends Plugin { */ this._dataInitialized = false; + this.loadAllowedConfig( this.editor.config.get( 'contentCompatibility.allowed' ) || [] ); + this.loadDisallowedConfig( this.editor.config.get( 'contentCompatibility.disallowed' ) || [] ); + this._registerElementsAfterInit(); this._registerElementHandlers(); } @@ -200,13 +203,17 @@ export default class DataFilter extends Plugin { * @param {Boolean} shouldDisallow Provided rules will reject attributes from matched elements instead of accepting them. */ _loadConfig( config, shouldDisallow = false ) { - for ( const { element, ...rules } of config ) { - this.allowElement( element ); + for ( const { name, ...rules } of config ) { + this.allowElement( { name } ); + + if ( !rules ) { + continue; + } if ( shouldDisallow ) { - this.disallowAttributes( { element, ...rules } ); + this.disallowAttributes( { name, ...rules } ); } else { - this.allowAttributes( { element, ...rules } ); + this.allowAttributes( { name, ...rules } ); } } } diff --git a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html new file mode 100644 index 00000000000..491f00db3be --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html @@ -0,0 +1,61 @@ + + + + +
    +
    +

    Section #1

    +

    Section #2

    +

    Section #3

    +
    + +
    + Summary + Hello world +
    + +
    +

    dt1

    +

    dt2

    +

    dd1

    +

    dd2

    +
    + +

    XYZ

    + +

    + Nested cite: I'm blue! +

    +

    + Nested span: I'm blue! +

    + +

    Deeply nested cite with span!

    + +

    Span with no attributes!

    + +

    Span with nested styles!

    + +
    Red quote!
    + +
      +
    • Blue!
    • +
    • Red!
    • +
    • Violet!
    • +
        +
      • Yellow!
      • +
      +
    +

    + Link with different background color. +

    +

    + Strong with font weight 400 +

    +

    + Italic with custom attributes +

    +

    + Red strike +

    +
    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.js b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.js new file mode 100644 index 00000000000..70d63e1f4c9 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.js @@ -0,0 +1,105 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console:false, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import List from '@ckeditor/ckeditor5-list/src/list'; +import Link from '@ckeditor/ckeditor5-link/src/link'; + +import GeneralHtmlSupport from '../../src/generalhtmlsupport'; + +/** + * Client custom plugin extending HTML support for compatibility. + */ +class ExtendHTMLSupport extends Plugin { + static get requires() { + return [ GeneralHtmlSupport ]; + } + + init() { + const { dataSchema } = this.editor.plugins.get( GeneralHtmlSupport ); + + // Extend schema with custom `xyz` element. + dataSchema.registerBlockElement( { + view: 'xyz', + model: 'htmlXyz', + modelSchema: { + inheritAllFrom: '$htmlBlock' + } + } ); + } +} + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ + Link, + BlockQuote, + Bold, + Essentials, + ExtendHTMLSupport, + Italic, + List, + Paragraph, + Strikethrough + ], + toolbar: [ + 'bold', + 'italic', + 'strikethrough', + '|', + 'numberedList', + 'bulletedList', + '|', + 'blockquote' + ], + contentCompatibility: { + allowed: [ + { name: 'article' }, + + { name: 'section', attributes: { id: /[\s\S]+/ } }, + { name: 'section', classes: /[\s\S]+/ }, + { name: 'section', styles: { color: 'red' } }, + + { name: /^(details|summary)$/ }, + { name: /^(dl|dd|dt)$/ }, + { name: 'xyz' }, + + { name: /^(span|cite)$/, attributes: { 'data-foo': /[\s\S]+/ } }, + { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } }, + { name: /^(span|cite)$/, attributes: { 'data-order-id': /[\s\S]+/ } }, + { name: /^(span|cite)$/, attributes: { 'data-item-id': /[\s\S]+/ } }, + + { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } }, + { name: 'p', styles: { 'background-color': /[\s\S]+/ } }, + + { name: 'blockquote', styles: { 'color': /[\s\S]+/ } }, + { name: 'li', styles: { 'color': /[\s\S]+/ } }, + { name: 'a', styles: { 'background-color': /[\s\S]+/ } }, + { name: 'strong', styles: { 'font-weight': /[\s\S]+/ } }, + { name: 'i', styles: { 'color': /[\s\S]+/ } }, + { name: 'i', attributes: { 'data-foo': /[\s\S]+/ } }, + { name: 's', styles: { 'color': /[\s\S]+/ } } + ], + disallowed: [ + { name: 'section', attributes: { id: /^_.*/ } }, + { name: /^(span|cite)$/, styles: { color: 'red' } } + ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.md b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.md new file mode 100644 index 00000000000..62e56632586 --- /dev/null +++ b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.md @@ -0,0 +1 @@ +# Data Filter From 3461201d0b7b1ea4c40a7897f5a1f90937ca1f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Tue, 18 May 2021 09:10:49 +0200 Subject: [PATCH 155/217] Fix config loading. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 4 ++-- .../tests/manual/datafilter.html | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 2c1e2f5dcd8..e2c8ff006f5 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -183,7 +183,7 @@ export default class DataFilter extends Plugin { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be allowed. */ allowAttributes( config ) { - this._allowedAttributes.add( toArray( config || [] ) ); + this._allowedAttributes.add( config ); } /** @@ -192,7 +192,7 @@ export default class DataFilter extends Plugin { * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be disallowed. */ disallowAttributes( config ) { - this._disallowedAttributes.add( toArray( config || [] ) ); + this._disallowedAttributes.add( config ); } /** diff --git a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html index 491f00db3be..0e9925b237c 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html @@ -23,6 +23,8 @@

    XYZ

    +

    Span with styles! This should not be red.

    +

    Nested cite: I'm blue!

    From 1fcec51f952c118f733488284c381069a348cf0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Tue, 18 May 2021 10:52:35 +0200 Subject: [PATCH 156/217] Review fixes. --- .../src/datafilter.js | 21 +++++---------- .../src/generalhtmlsupport.js | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index e2c8ff006f5..319c754d80e 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -108,9 +108,6 @@ export default class DataFilter extends Plugin { */ this._dataInitialized = false; - this.loadAllowedConfig( this.editor.config.get( 'contentCompatibility.allowed' ) || [] ); - this.loadDisallowedConfig( this.editor.config.get( 'contentCompatibility.disallowed' ) || [] ); - this._registerElementsAfterInit(); this._registerElementHandlers(); } @@ -136,7 +133,7 @@ export default class DataFilter extends Plugin { * that should have their attributes accepted in the editor. */ loadAllowedConfig( config ) { - this._loadConfig( config ); + this._loadConfig( config, pattern => this.allowAttributes( pattern ) ); } /** @@ -146,7 +143,7 @@ export default class DataFilter extends Plugin { * that should have their attributes rejected from the editor. */ loadDisallowedConfig( config ) { - this._loadConfig( config, true ); + this._loadConfig( config, pattern => this.disallowAttributes( pattern ) ); } /** @@ -155,7 +152,7 @@ export default class DataFilter extends Plugin { * This method will only allow elements described by the {@link module:content-compatibility/dataschema~DataSchema} used * to create data filter. * - * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all view elements which should be allowed. + * @param {String|RegExp} viewName String or regular expression matching view name. */ allowElement( config ) { for ( const definition of this._dataSchema.getDefinitionsForView( config.name, true ) ) { @@ -173,8 +170,6 @@ export default class DataFilter extends Plugin { this._fireRegisterEvent( definition ); } } - - this.allowAttributes( config ); } /** @@ -200,9 +195,9 @@ export default class DataFilter extends Plugin { * * @private * @param {Array.} config Filtering configuration. - * @param {Boolean} shouldDisallow Provided rules will reject attributes from matched elements instead of accepting them. + * @param {Function} handleAttributes Callback handling the way the attributes should be processed. */ - _loadConfig( config, shouldDisallow = false ) { + _loadConfig( config, handleAttributes ) { for ( const { name, ...rules } of config ) { this.allowElement( { name } ); @@ -210,11 +205,7 @@ export default class DataFilter extends Plugin { continue; } - if ( shouldDisallow ) { - this.disallowAttributes( { name, ...rules } ); - } else { - this.allowAttributes( { name, ...rules } ); - } + handleAttributes( { name, ...rules } ); } } diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index 9bf7e0c6ced..b4ec6162949 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -28,6 +28,33 @@ export default class GeneralHtmlSupport extends Plugin { return 'GeneralHtmlSupport'; } + /** + * @param {module:core/editor/editor~Editor} editor + */ + constructor( editor ) { + super( editor ); + + /** + * An instance of the {@link module:content-compatibility/dataschema~DataSchema}. + * + * @readonly + * @member {module:content-compatibility/dataschema~DataSchema} #dataSchema + */ + this.dataSchema = new DataSchema(); + + /** + * An instance of the {@link module:content-compatibility/datafilter~DataFilter}. + * + * @readonly + * @member {module:content-compatibility/datafilter~DataFilter} #dataFilter + */ + this.dataFilter = new DataFilter( editor, this.dataSchema ); + + // Load the filtering configuration. + this.dataFilter.loadAllowedConfig( this.editor.config.get( 'contentCompatibility.allowed' ) || [] ); + this.dataFilter.loadDisallowedConfig( this.editor.config.get( 'contentCompatibility.disallowed' ) || [] ); + } + /** * @inheritDoc */ From a781a74d64bcc803e98ce61668d58a391f843eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Tue, 18 May 2021 10:55:47 +0200 Subject: [PATCH 157/217] Merge manual tests. --- .../tests/manual/datafilter.html | 63 ----------- .../tests/manual/datafilter.js | 105 ------------------ .../tests/manual/datafilter.md | 1 - .../tests/manual/generalhtmlsupport.html | 2 + .../tests/manual/generalhtmlsupport.js | 85 ++++++-------- 5 files changed, 36 insertions(+), 220 deletions(-) delete mode 100644 packages/ckeditor5-content-compatibility/tests/manual/datafilter.html delete mode 100644 packages/ckeditor5-content-compatibility/tests/manual/datafilter.js delete mode 100644 packages/ckeditor5-content-compatibility/tests/manual/datafilter.md diff --git a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html deleted file mode 100644 index 0e9925b237c..00000000000 --- a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - -
    -
    -

    Section #1

    -

    Section #2

    -

    Section #3

    -
    - -
    - Summary - Hello world -
    - -
    -

    dt1

    -

    dt2

    -

    dd1

    -

    dd2

    -
    - -

    XYZ

    - -

    Span with styles! This should not be red.

    - -

    - Nested cite: I'm blue! -

    -

    - Nested span: I'm blue! -

    - -

    Deeply nested cite with span!

    - -

    Span with no attributes!

    - -

    Span with nested styles!

    - -
    Red quote!
    - -
      -
    • Blue!
    • -
    • Red!
    • -
    • Violet!
    • -
        -
      • Yellow!
      • -
      -
    -

    - Link with different background color. -

    -

    - Strong with font weight 400 -

    -

    - Italic with custom attributes -

    -

    - Red strike -

    -
    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.js b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.js deleted file mode 100644 index 70d63e1f4c9..00000000000 --- a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/* globals console:false, window, document */ - -import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; -import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; -import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; -import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'; -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; -import List from '@ckeditor/ckeditor5-list/src/list'; -import Link from '@ckeditor/ckeditor5-link/src/link'; - -import GeneralHtmlSupport from '../../src/generalhtmlsupport'; - -/** - * Client custom plugin extending HTML support for compatibility. - */ -class ExtendHTMLSupport extends Plugin { - static get requires() { - return [ GeneralHtmlSupport ]; - } - - init() { - const { dataSchema } = this.editor.plugins.get( GeneralHtmlSupport ); - - // Extend schema with custom `xyz` element. - dataSchema.registerBlockElement( { - view: 'xyz', - model: 'htmlXyz', - modelSchema: { - inheritAllFrom: '$htmlBlock' - } - } ); - } -} - -ClassicEditor - .create( document.querySelector( '#editor' ), { - plugins: [ - Link, - BlockQuote, - Bold, - Essentials, - ExtendHTMLSupport, - Italic, - List, - Paragraph, - Strikethrough - ], - toolbar: [ - 'bold', - 'italic', - 'strikethrough', - '|', - 'numberedList', - 'bulletedList', - '|', - 'blockquote' - ], - contentCompatibility: { - allowed: [ - { name: 'article' }, - - { name: 'section', attributes: { id: /[\s\S]+/ } }, - { name: 'section', classes: /[\s\S]+/ }, - { name: 'section', styles: { color: 'red' } }, - - { name: /^(details|summary)$/ }, - { name: /^(dl|dd|dt)$/ }, - { name: 'xyz' }, - - { name: /^(span|cite)$/, attributes: { 'data-foo': /[\s\S]+/ } }, - { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } }, - { name: /^(span|cite)$/, attributes: { 'data-order-id': /[\s\S]+/ } }, - { name: /^(span|cite)$/, attributes: { 'data-item-id': /[\s\S]+/ } }, - - { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } }, - { name: 'p', styles: { 'background-color': /[\s\S]+/ } }, - - { name: 'blockquote', styles: { 'color': /[\s\S]+/ } }, - { name: 'li', styles: { 'color': /[\s\S]+/ } }, - { name: 'a', styles: { 'background-color': /[\s\S]+/ } }, - { name: 'strong', styles: { 'font-weight': /[\s\S]+/ } }, - { name: 'i', styles: { 'color': /[\s\S]+/ } }, - { name: 'i', attributes: { 'data-foo': /[\s\S]+/ } }, - { name: 's', styles: { 'color': /[\s\S]+/ } } - ], - disallowed: [ - { name: 'section', attributes: { id: /^_.*/ } }, - { name: /^(span|cite)$/, styles: { color: 'red' } } - ] - } - } ) - .then( editor => { - window.editor = editor; - } ) - .catch( err => { - console.error( err.stack ); - } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.md b/packages/ckeditor5-content-compatibility/tests/manual/datafilter.md deleted file mode 100644 index 62e56632586..00000000000 --- a/packages/ckeditor5-content-compatibility/tests/manual/datafilter.md +++ /dev/null @@ -1 +0,0 @@ -# Data Filter diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index 491f00db3be..0e9925b237c 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -23,6 +23,8 @@

    XYZ

    +

    Span with styles! This should not be red.

    +

    Nested cite: I'm blue!

    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index ed4ffec1934..26f128b2ec7 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -38,56 +38,6 @@ class ExtendHTMLSupport extends Plugin { inheritAllFrom: '$htmlBlock' } } ); - - // Allow some elements, at this point model schema will include information about view-model mapping - // e.g. article -> ghsArticle - dataFilter.allowElement( { name: 'article' } ); - dataFilter.allowElement( { name: 'section' } ); - dataFilter.allowElement( { name: /^(details|summary)$/ } ); - dataFilter.allowElement( { name: /^(dl|dd|dt)$/ } ); - dataFilter.allowElement( { name: 'xyz' } ); - - // Let's extend 'section' with some attributes. Data filter will take care of - // creating proper converters and attribute matchers: - dataFilter.allowAttributes( { name: 'section', attributes: { id: /[\s\S]+/ } } ); - dataFilter.allowAttributes( { name: 'section', classes: /[\s\S]+/ } ); - dataFilter.allowAttributes( { name: 'section', styles: { color: 'red' } } ); - - // but disallow setting id attribute if it start with `_` prefix: - dataFilter.disallowAttributes( { name: 'section', attributes: { id: /^_.*/ } } ); - - // Let's also add some inline elements support: - dataFilter.allowElement( { name: /^(span|cite)$/ } ); - dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); - dataFilter.allowAttributes( { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } } ); - dataFilter.disallowAttributes( { name: /^(span|cite)$/, styles: { color: 'red' } } ); - - dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-order-id': /[\s\S]+/ } } ); - dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-item-id': /[\s\S]+/ } } ); - - // Allow existing features. - dataFilter.allowElement( { name: 'p' } ); - dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } } ); - dataFilter.allowAttributes( { name: 'p', styles: { 'background-color': /[\s\S]+/ } } ); - - dataFilter.allowElement( { name: 'blockquote' } ); - dataFilter.allowAttributes( { name: 'blockquote', styles: { 'color': /[\s\S]+/ } } ); - - dataFilter.allowElement( { name: 'li' } ); - dataFilter.allowAttributes( { name: 'li', styles: { 'color': /[\s\S]+/ } } ); - - dataFilter.allowElement( { name: 'a' } ); - dataFilter.allowAttributes( { name: 'a', styles: { 'background-color': /[\s\S]+/ } } ); - - dataFilter.allowElement( { name: 'strong' } ); - dataFilter.allowAttributes( { name: 'strong', styles: { 'font-weight': /[\s\S]+/ } } ); - - dataFilter.allowElement( { name: 'i' } ); - dataFilter.allowAttributes( { name: 'i', styles: { 'color': /[\s\S]+/ } } ); - dataFilter.allowAttributes( { name: 'i', attributes: { 'data-foo': /[\s\S]+/ } } ); - - dataFilter.allowElement( { name: 's' } ); - dataFilter.allowAttributes( { name: 's', styles: { 'color': /[\s\S]+/ } } ); } } @@ -113,7 +63,40 @@ ClassicEditor 'bulletedList', '|', 'blockquote' - ] + ], + contentCompatibility: { + allowed: [ + { name: 'article' }, + + { name: 'section', attributes: { id: /[\s\S]+/ } }, + { name: 'section', classes: /[\s\S]+/ }, + { name: 'section', styles: { color: 'red' } }, + + { name: /^(details|summary)$/ }, + { name: /^(dl|dd|dt)$/ }, + { name: 'xyz' }, + + { name: /^(span|cite)$/, attributes: { 'data-foo': /[\s\S]+/ } }, + { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } }, + { name: /^(span|cite)$/, attributes: { 'data-order-id': /[\s\S]+/ } }, + { name: /^(span|cite)$/, attributes: { 'data-item-id': /[\s\S]+/ } }, + + { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } }, + { name: 'p', styles: { 'background-color': /[\s\S]+/ } }, + + { name: 'blockquote', styles: { 'color': /[\s\S]+/ } }, + { name: 'li', styles: { 'color': /[\s\S]+/ } }, + { name: 'a', styles: { 'background-color': /[\s\S]+/ } }, + { name: 'strong', styles: { 'font-weight': /[\s\S]+/ } }, + { name: 'i', styles: { 'color': /[\s\S]+/ } }, + { name: 'i', attributes: { 'data-foo': /[\s\S]+/ } }, + { name: 's', styles: { 'color': /[\s\S]+/ } } + ], + disallowed: [ + { name: 'section', attributes: { id: /^_.*/ } }, + { name: /^(span|cite)$/, styles: { color: 'red' } } + ] + } } ) .then( editor => { window.editor = editor; From ee76ffbed8fdc7f2620236b91b910d32d49cf624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Thu, 20 May 2021 11:51:21 +0200 Subject: [PATCH 158/217] Match when any pattern group is matched. --- .../src/datafilter.js | 34 ++++++++++--- .../src/generalhtmlsupport.js | 4 +- .../tests/manual/generalhtmlsupport.js | 49 +++++++++++-------- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 319c754d80e..09397683b10 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -198,15 +198,37 @@ export default class DataFilter extends Plugin { * @param {Function} handleAttributes Callback handling the way the attributes should be processed. */ _loadConfig( config, handleAttributes ) { - for ( const { name, ...rules } of config ) { - this.allowElement( { name } ); + for ( const pattern of config ) { + this.allowElement( { name: pattern.name } ); - if ( !rules ) { - continue; - } + this._splitRules( pattern ) + .forEach( rule => handleAttributes( rule ) ); + } + } + + /** + * Rules are matched in conjunction (AND operation), but we want to have a match if any of the rules is matched (OR operation). + * By splitting the rules we force the latter effect. + * + * @private + * @param {module:engine/view/matcher~MatcherPattern} rules + * @returns {Array.} + */ + _splitRules( rules ) { + const { name, attributes, classes, styles } = rules; + const splitRules = []; - handleAttributes( { name, ...rules } ); + if ( attributes ) { + splitRules.push( { name, attributes } ); } + if ( classes ) { + splitRules.push( { name, classes } ); + } + if ( styles ) { + splitRules.push( { name, styles } ); + } + + return splitRules; } /** diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index b4ec6162949..7cc5dfcb7eb 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -51,8 +51,8 @@ export default class GeneralHtmlSupport extends Plugin { this.dataFilter = new DataFilter( editor, this.dataSchema ); // Load the filtering configuration. - this.dataFilter.loadAllowedConfig( this.editor.config.get( 'contentCompatibility.allowed' ) || [] ); - this.dataFilter.loadDisallowedConfig( this.editor.config.get( 'contentCompatibility.disallowed' ) || [] ); + this.dataFilter.loadAllowedConfig( this.editor.config.get( 'generalHtmlSupport.allowed' ) || [] ); + this.dataFilter.loadDisallowedConfig( this.editor.config.get( 'generalHtmlSupport.disallowed' ) || [] ); } /** diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 26f128b2ec7..940b12f8a0d 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -64,33 +64,40 @@ ClassicEditor '|', 'blockquote' ], - contentCompatibility: { + generalHtmlSupport: { allowed: [ { name: 'article' }, - - { name: 'section', attributes: { id: /[\s\S]+/ } }, - { name: 'section', classes: /[\s\S]+/ }, - { name: 'section', styles: { color: 'red' } }, - + { name: 'xyz' }, { name: /^(details|summary)$/ }, { name: /^(dl|dd|dt)$/ }, - { name: 'xyz' }, - - { name: /^(span|cite)$/, attributes: { 'data-foo': /[\s\S]+/ } }, - { name: /^(span|cite)$/, styles: { color: /[\s\S]+/ } }, - { name: /^(span|cite)$/, attributes: { 'data-order-id': /[\s\S]+/ } }, - { name: /^(span|cite)$/, attributes: { 'data-item-id': /[\s\S]+/ } }, - { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } }, - { name: 'p', styles: { 'background-color': /[\s\S]+/ } }, + { name: 'a', styles: { 'background-color': true } }, + { name: 'blockquote', styles: { 'color': true } }, + { name: 'li', styles: { 'color': true } }, + { name: 's', styles: { 'color': true } }, + { name: 'strong', styles: { 'font-weight': true } }, - { name: 'blockquote', styles: { 'color': /[\s\S]+/ } }, - { name: 'li', styles: { 'color': /[\s\S]+/ } }, - { name: 'a', styles: { 'background-color': /[\s\S]+/ } }, - { name: 'strong', styles: { 'font-weight': /[\s\S]+/ } }, - { name: 'i', styles: { 'color': /[\s\S]+/ } }, - { name: 'i', attributes: { 'data-foo': /[\s\S]+/ } }, - { name: 's', styles: { 'color': /[\s\S]+/ } } + { + name: 'i', + styles: { 'color': true }, + attributes: { 'data-foo': true } + }, + { + name: 'section', + attributes: { id: true }, + classes: true, + styles: { color: 'red' } + }, + { + name: /^(span|cite)$/, + styles: { color: true }, + attributes: [ 'data-foo', 'data-order-id', 'data-item-id' ] + }, + { + name: 'p', + attributes: { 'data-foo': true }, + styles: { 'background-color': true } + } ], disallowed: [ { name: 'section', attributes: { id: /^_.*/ } }, From 8563013a00ae8718269ad38d0e0a777733b1d288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Thu, 20 May 2021 14:14:02 +0200 Subject: [PATCH 159/217] Match any pattern inside a rule. --- .../src/datafilter.js | 41 +++++++++++++-- .../tests/datafilter.js | 52 +++++++++++++++---- 2 files changed, 79 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 09397683b10..dc94d06d639 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -23,9 +23,13 @@ import { viewToAttributeInlineConverter, attributeToViewInlineConverter, +<<<<<<< HEAD viewToModelBlockAttributeConverter, modelToViewBlockAttributeConverter } from './converters'; +======= +import { cloneDeep, isPlainObject } from 'lodash-es'; +>>>>>>> b60cdaaadc (Match any pattern inside a rule.) import '../theme/datafilter.css'; @@ -219,13 +223,13 @@ export default class DataFilter extends Plugin { const splitRules = []; if ( attributes ) { - splitRules.push( { name, attributes } ); + splitRules.push( ...this._splitPattern( { name, attributes }, 'attributes' ) ); } if ( classes ) { - splitRules.push( { name, classes } ); + splitRules.push( ...this._splitPattern( { name, classes }, 'classes' ) ); } if ( styles ) { - splitRules.push( { name, styles } ); + splitRules.push( ...this._splitPattern( { name, styles }, 'styles' ) ); } return splitRules; @@ -261,6 +265,37 @@ export default class DataFilter extends Plugin { return consumeAttributes( viewElement, conversionApi, this._disallowedAttributes ); } + /** + * TODO: JSdoc, refactoring. + * Separate multiple patterns into separate rules to make them disjunctive. + * + * @param {*} pattern + * @param {*} attributeName + * @returns + */ + _splitPattern( pattern, attributeName ) { + const { name } = pattern; + + if ( isPlainObject( pattern[ attributeName ] ) ) { + return Object.entries( pattern[ attributeName ] ).map( + ( [ key, value ] ) => ( { + name, + [ attributeName ]: { + [ key ]: value + } + } ) ); + } else if ( Array.isArray( pattern[ attributeName ] ) ) { + return pattern[ attributeName ].map( + value => ( { + name, + [ attributeName ]: [ value ] + } ) + ); + } + + return pattern; + } + /** * Registers elements allowed by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method * once {@link module:core/editor~Editor#data editor's data controller} is initialized. diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 167c1cfce9b..f2c8584256a 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -1276,27 +1276,57 @@ describe( 'DataFilter', () => { }, /data-filter-invalid-definition/, null, definition ); } ); - describe.skip( 'fromConfig', () => { - // TODO: Better name - it( 'should load config', () => { + describe( 'loadAllowedConfig', () => { + it( 'should load config with simple allowed rule', () => { const config = [ { - element: 'xyz', - attributes: { - title: 'foo' + name: 'span', + styles: { color: true }, + classes: [ 'foo' ] + } + ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( '

    foobar

    ' ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '<$text htmlSpan="(1)">foobar', + attributes: { + 1: { + classes: [ 'foo' ] } } + } ); + + expect( editor.getData() ).to.equal( '

    foobar

    ' ); + } ); + + it( 'should load config and match whenever a single match has been found', () => { + const config = [ + { + name: 'span', + styles: { color: true }, + classes: [ 'foo', 'bar', 'test' ] + } ]; - dataSchema.registerBlockElement( { view: 'xyz', model: 'modelXyz' } ); - dataFilter.fromConfig( config ); + dataFilter.loadAllowedConfig( config ); - editor.setData( 'foo' ); + editor.setData( '

    foobar

    ' ); + // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: 'foo', - attributes: {} + data: '<$text fontColor="blue" htmlSpan="(1)">foobar', + attributes: { + 1: { + classes: [ 'foo', 'bar' ] + } + } } ); + + expect( editor.getData() ).to.equal( '

    foobar

    ' ); } ); } ); From d6a96306fd4807a3a32be61ebf64ee2dfde17825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Thu, 20 May 2021 14:19:56 +0200 Subject: [PATCH 160/217] Return pattern as array in _splitPattern method. --- packages/ckeditor5-content-compatibility/src/datafilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index dc94d06d639..9b6783196e0 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -293,7 +293,7 @@ export default class DataFilter extends Plugin { ); } - return pattern; + return [ pattern ]; } /** From 141bdc759c517ef58a424fa442bfe76737716240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Thu, 20 May 2021 14:32:34 +0200 Subject: [PATCH 161/217] Display the source content as before passed through the editor. --- .../tests/manual/generalhtmlsupport.html | 130 ++++++++++-------- 1 file changed, 72 insertions(+), 58 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index 0e9925b237c..f7c5bd5a7aa 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -1,63 +1,77 @@ + +
    +
    +
    +
    +

    Section #1

    +

    Section #2

    +

    Section #3

    +
    + +
    + Summary + Hello world +
    + +
    +

    dt1

    +

    dt2

    +

    dd1

    +

    dd2

    +
    + +

    XYZ

    + +

    Span with styles! This should not be red (due to disallowed content).

    + +

    + Nested cite: I'm blue! +

    +

    + Nested span: I'm blue! +

    + +

    Deeply nested cite with span!

    + +

    Span with no attributes!

    + +

    Span with nested styles!

    + +
    Red quote!
    + +
      +
    • Blue!
    • +
    • Red!
    • +
    • Violet!
    • +
        +
      • Yellow!
      • +
      +
    +

    + Link with different background color. +

    +

    + Strong with font weight 400 +

    +

    + Italic with custom attributes +

    +

    + Red strike +

    +
    +
    -
    -
    -

    Section #1

    -

    Section #2

    -

    Section #3

    -
    - -
    - Summary - Hello world -
    - -
    -

    dt1

    -

    dt2

    -

    dd1

    -

    dd2

    -
    - -

    XYZ

    - -

    Span with styles! This should not be red.

    - -

    - Nested cite: I'm blue! -

    -

    - Nested span: I'm blue! -

    - -

    Deeply nested cite with span!

    - -

    Span with no attributes!

    - -

    Span with nested styles!

    - -
    Red quote!
    - -
      -
    • Blue!
    • -
    • Red!
    • -
    • Violet!
    • -
        -
      • Yellow!
      • -
      -
    -

    - Link with different background color. -

    -

    - Strong with font weight 400 -

    -

    - Italic with custom attributes -

    -

    - Red strike -

    +
    +

    Source content:

    +
    +
    + From e13e84e73b7d44b78c36778faeb6309ec3feab43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Thu, 20 May 2021 14:54:41 +0200 Subject: [PATCH 162/217] Some more examples. --- .../tests/manual/generalhtmlsupport.html | 16 +++++++++++----- .../tests/manual/generalhtmlsupport.js | 12 ++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index f7c5bd5a7aa..a07a831fe6e 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -8,9 +8,15 @@
    -

    Section #1

    -

    Section #2

    -

    Section #3

    +
    +

    Section #1

    +
    +
    +

    Section #2

    +
    +
    +

    Section #3

    +
    @@ -55,10 +61,10 @@

    Link with different background color.

    -

    +

    Strong with font weight 400

    -

    +

    Italic with custom attributes

    diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 940b12f8a0d..f4ac8ac4b49 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -84,7 +84,10 @@ ClassicEditor }, { name: 'section', - attributes: { id: true }, + attributes: { + id: true, + 'data-section-id': /^\d+$/ + }, classes: true, styles: { color: 'red' } }, @@ -95,7 +98,12 @@ ClassicEditor }, { name: 'p', - attributes: { 'data-foo': true }, + attributes: [ + { + key: /^data-/, + value: true + } + ], styles: { 'background-color': true } } ], From 63797d648012d5f63b82d6b6b56abed82672d78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Mon, 31 May 2021 16:38:54 +0200 Subject: [PATCH 163/217] Add missing tests and docs. --- .../src/datafilter.js | 16 +- .../src/generalhtmlsupport.js | 2 + .../tests/datafilter.js | 410 +++++++++++++++++- .../tests/manual/generalhtmlsupport.js | 1 - 4 files changed, 407 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 9b6783196e0..d1c36f84dd9 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -23,13 +23,10 @@ import { viewToAttributeInlineConverter, attributeToViewInlineConverter, -<<<<<<< HEAD viewToModelBlockAttributeConverter, modelToViewBlockAttributeConverter } from './converters'; -======= -import { cloneDeep, isPlainObject } from 'lodash-es'; ->>>>>>> b60cdaaadc (Match any pattern inside a rule.) +import { isPlainObject } from 'lodash-es'; import '../theme/datafilter.css'; @@ -266,12 +263,13 @@ export default class DataFilter extends Plugin { } /** - * TODO: JSdoc, refactoring. - * Separate multiple patterns into separate rules to make them disjunctive. + * Matcher by default has to match **all** patterns to count it as an actual match. By splitting the pattern + * into separate patterns means that any matched pattern will be count as a match. * - * @param {*} pattern - * @param {*} attributeName - * @returns + * @private + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern to split. + * @param {String} attributeName Name of the attribute to split (e.g. 'attributes', 'classes', 'styles'). + * @returns {Array.} */ _splitPattern( pattern, attributeName ) { const { name } = pattern; diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index 7cc5dfcb7eb..e7eb657620b 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -49,7 +49,9 @@ export default class GeneralHtmlSupport extends Plugin { * @member {module:content-compatibility/datafilter~DataFilter} #dataFilter */ this.dataFilter = new DataFilter( editor, this.dataSchema ); + } + init() { // Load the filtering configuration. this.dataFilter.loadAllowedConfig( this.editor.config.get( 'generalHtmlSupport.allowed' ) || [] ); this.dataFilter.loadDisallowedConfig( this.editor.config.get( 'generalHtmlSupport.disallowed' ) || [] ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index f2c8584256a..200955117d6 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -1277,56 +1277,442 @@ describe( 'DataFilter', () => { } ); describe( 'loadAllowedConfig', () => { - it( 'should load config with simple allowed rule', () => { + it( 'should load config and match whenever a single match has been found', () => { const config = [ { name: 'span', styles: { color: true }, - classes: [ 'foo' ] + classes: [ 'foo', 'bar', 'test' ], + attributes: [ { key: /data-foo.*/, value: true } ] } ]; dataFilter.loadAllowedConfig( config ); - editor.setData( '

    foobar

    ' ); + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text htmlSpan="(1)">foobar', + data: '' + + '<$text fontColor="blue" htmlSpan="(1)">foobar' + + '<$text htmlSpan="(2)">foo data' + + '<$text htmlSpan="(3)">bar data' + + '', attributes: { 1: { - classes: [ 'foo' ] + classes: [ 'foo', 'bar' ] + }, + 2: { + attributes: { + 'data-foo': 'foo data' + } + }, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + '' + + 'foobar' + + '' + + 'foo data' + + 'bar data' + + '

    ' + ); + } ); + + it( 'should match all values', () => { + // Sanity check test for splitting patterns that are not objects nor arrays. + + const config = [ + { + name: 'span', + styles: true, + classes: true, + attributes: true + } + ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text fontColor="blue" htmlSpan="(1)">foobar' + + '<$text htmlSpan="(2)">foo data' + + '<$text htmlSpan="(3)">bar data' + + '', + attributes: { + 1: { + classes: [ 'foo', 'bar' ] + }, + 2: { + attributes: { + 'data-foo': 'foo data' + } + }, + 3: { + attributes: { + 'data-bar': 'bar data' + } } } } ); - expect( editor.getData() ).to.equal( '

    foobar

    ' ); + expect( editor.getData() ).to.equal( + '

    ' + + '' + + 'foobar' + + '' + + 'foo data' + + 'bar data' + + '

    ' + ); } ); - it( 'should load config and match whenever a single match has been found', () => { + it( 'should match attributes', () => { + const config = [ + { + name: 'span', + attributes: [ { key: /data-foo.*/, value: true } ] + } + ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text fontColor="blue" htmlSpan="(1)">foobar' + + '<$text htmlSpan="(2)">foo data' + + '<$text htmlSpan="(3)">bar data' + + '', + attributes: { + 1: {}, + 2: { + attributes: { + 'data-foo': 'foo data' + } + }, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + '' + + 'foobar' + + '' + + 'foo data' + + 'bar data' + + '

    ' + ); + } ); + + it( 'should match classes', () => { const config = [ { name: 'span', - styles: { color: true }, classes: [ 'foo', 'bar', 'test' ] } ]; dataFilter.loadAllowedConfig( config ); - editor.setData( '

    foobar

    ' ); + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { - data: '<$text fontColor="blue" htmlSpan="(1)">foobar', + data: '' + + '<$text fontColor="blue" htmlSpan="(1)">foobar' + + '<$text htmlSpan="(2)">foo databar data' + + '', attributes: { 1: { classes: [ 'foo', 'bar' ] - } + }, + 2: {}, + 3: {} } } ); - expect( editor.getData() ).to.equal( '

    foobar

    ' ); + expect( editor.getData() ).to.equal( + '

    ' + + '' + + 'foobar' + + '' + + 'foo databar data' + + '

    ' + ); + } ); + + it( 'should match styles', () => { + const config = [ + { + name: 'span', + styles: { color: true } + } + ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text fontColor="blue" htmlSpan="(1)">foobar' + + '<$text htmlSpan="(2)">foo databar data' + + '', + attributes: { + 1: {}, + 2: {}, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + '' + + 'foobar' + + '' + + 'foo databar data' + + '

    ' + ); + } ); + } ); + + describe( 'loadDisallowedConfig', () => { + it( 'should load config and match whenever a single match has been found', () => { + const config = [ + { + name: 'span', + styles: { color: true }, + classes: [ 'foo', 'bar', 'test' ], + attributes: [ { key: /data-foo.*/, value: true } ] + } + ]; + + dataFilter.loadDisallowedConfig( config ); + + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlSpan="(1)">foobarfoo databar data' + + '', + attributes: { + 1: {}, + 2: {}, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + 'foobarfoo databar data' + + '

    ' + ); + } ); + + it( 'should match all values', () => { + // Sanity check test for splitting patterns that are not objects nor arrays. + + const config = [ + { + name: 'span', + styles: true, + classes: true, + attributes: true + } + ]; + + dataFilter.loadDisallowedConfig( config ); + + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlSpan="(1)">foobarfoo databar data' + + '', + attributes: { + 1: {}, + 2: {}, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + 'foobarfoo databar data' + + '

    ' + ); + } ); + + it( 'should match attributes', () => { + const config = [ + { + name: 'span', + attributes: [ { key: /data-foo.*/, value: true } ] + } + ]; + + dataFilter.loadDisallowedConfig( config ); + + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text fontColor="blue" htmlSpan="(1)">foobar' + + '<$text htmlSpan="(2)">foo databar data' + + '', + attributes: { + 1: {}, + 2: {}, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + '' + + 'foobar' + + '' + + 'foo databar data' + + '

    ' + ); + } ); + + it( 'should match classes', () => { + const config = [ + { + name: 'span', + classes: [ 'foo', 'bar', 'test' ] + } + ]; + + dataFilter.loadDisallowedConfig( config ); + + editor.setData( + '

    ' + + 'foobar' + + 'foo data' + + 'bar data' + + '

    ' + ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text fontColor="blue" htmlSpan="(1)">foobar' + + '<$text htmlSpan="(2)">foo databar data' + + '', + attributes: { + 1: {}, + 2: {}, + 3: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + '' + + 'foobar' + + '' + + 'foo databar data' + + '

    ' + ); + } ); + + it( 'should match styles', () => { + const config = [ + { + name: 'span', + styles: { color: true } + } + ]; + + dataFilter.loadDisallowedConfig( config ); + + editor.setData( + '

    ' + + '' + + 'foobar' + + 'foo databar data' + + '

    ' + ); + + // Font feature should take over color CSS property. + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlSpan="(1)">foobarfoo databar data' + + '', + attributes: { + 1: {}, + 2: {} + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + 'foobarfoo databar data' + + '

    ' + ); } ); } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index f4ac8ac4b49..0a6bcd468d6 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -67,7 +67,6 @@ ClassicEditor generalHtmlSupport: { allowed: [ { name: 'article' }, - { name: 'xyz' }, { name: /^(details|summary)$/ }, { name: /^(dl|dd|dt)$/ }, From 87b0b4e858eff2416a8d74c6a43eecd92fcdceb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Wed, 2 Jun 2021 14:21:14 +0200 Subject: [PATCH 164/217] Review fixes. --- .../ckeditor5-content-compatibility/src/datafilter.js | 3 +-- .../ckeditor5-content-compatibility/tests/datafilter.js | 8 ++++---- .../tests/manual/generalhtmlsupport.js | 3 +++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index d1c36f84dd9..92be8aeec59 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -202,8 +202,7 @@ export default class DataFilter extends Plugin { for ( const pattern of config ) { this.allowElement( { name: pattern.name } ); - this._splitRules( pattern ) - .forEach( rule => handleAttributes( rule ) ); + this._splitRules( pattern ).forEach( handleAttributes ); } } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 200955117d6..77420a044f3 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -1283,7 +1283,7 @@ describe( 'DataFilter', () => { name: 'span', styles: { color: true }, classes: [ 'foo', 'bar', 'test' ], - attributes: [ { key: /data-foo.*/, value: true } ] + attributes: [ { key: /^data-foo.*$/, value: true } ] } ]; @@ -1389,7 +1389,7 @@ describe( 'DataFilter', () => { const config = [ { name: 'span', - attributes: [ { key: /data-foo.*/, value: true } ] + attributes: [ { key: /^data-foo.*$/, value: true } ] } ]; @@ -1524,7 +1524,7 @@ describe( 'DataFilter', () => { name: 'span', styles: { color: true }, classes: [ 'foo', 'bar', 'test' ], - attributes: [ { key: /data-foo.*/, value: true } ] + attributes: [ { key: /^data-foo.*$/, value: true } ] } ]; @@ -1601,7 +1601,7 @@ describe( 'DataFilter', () => { const config = [ { name: 'span', - attributes: [ { key: /data-foo.*/, value: true } ] + attributes: [ { key: /^data-foo.*$/, value: true } ] } ]; diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index 0a6bcd468d6..defe6d24752 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -38,6 +38,9 @@ class ExtendHTMLSupport extends Plugin { inheritAllFrom: '$htmlBlock' } } ); + + // Custom elements need to be filtered using direct API instead of config. + dataFilter.allowElement( { name: 'xyz' } ); } } From 5a8528ea345a98c9baa30f4af06d88e4cf373f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Fri, 4 Jun 2021 08:53:31 +0200 Subject: [PATCH 165/217] Update `allowElement()` method and tests. --- .../src/datafilter.js | 10 +- .../tests/datafilter.js | 93 ++++++++++--------- .../tests/manual/generalhtmlsupport.js | 2 +- 3 files changed, 54 insertions(+), 51 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 92be8aeec59..bbf63546c38 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -35,9 +35,7 @@ import '../theme/datafilter.css'; * * To enable registered element in the editor, use {@link module:content-compatibility/datafilter~DataFilter#allowElement} method: * - * dataFilter.allowElement( { - * name: 'section' - * } ); + * dataFilter.allowElement( 'section' ); * * You can also allow or disallow specific element attributes: * @@ -155,8 +153,8 @@ export default class DataFilter extends Plugin { * * @param {String|RegExp} viewName String or regular expression matching view name. */ - allowElement( config ) { - for ( const definition of this._dataSchema.getDefinitionsForView( config.name, true ) ) { + allowElement( viewName ) { + for ( const definition of this._dataSchema.getDefinitionsForView( viewName, true ) ) { if ( this._allowedElements.has( definition ) ) { continue; } @@ -200,7 +198,7 @@ export default class DataFilter extends Plugin { */ _loadConfig( config, handleAttributes ) { for ( const pattern of config ) { - this.allowElement( { name: pattern.name } ); + this.allowElement( pattern.name ); this._splitRules( pattern ).forEach( handleAttributes ); } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 77420a044f3..f17923f07de 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -93,7 +93,7 @@ describe( 'DataFilter', () => { it( 'should allow element registered after editor initialization', () => { const dataFilter = initEditor.plugins.get( DataFilter ); - dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowElement( 'span' ); initEditor.setData( '

    foobar

    ' ); @@ -126,12 +126,12 @@ describe( 'DataFilter', () => { init() { const dataFilter = this.editor.plugins.get( DataFilter ); - dataFilter.allowElement( { name: 'article' } ); + dataFilter.allowElement( 'article' ); } afterInit() { const dataFilter = this.editor.plugins.get( DataFilter ); - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); } } } ); @@ -324,7 +324,7 @@ describe( 'DataFilter', () => { describe( 'block', () => { it( 'should allow element', () => { - dataFilter.allowElement( { name: 'article' } ); + dataFilter.allowElement( 'article' ); editor.setData( '
    ' + '
    section1
    ' + @@ -339,7 +339,7 @@ describe( 'DataFilter', () => { '

    section1section2

    ' ); - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); editor.setData( '
    ' + '
    section1
    ' + @@ -360,7 +360,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow deeply nested structure', () => { - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); editor.setData( '

    1

    ' + @@ -385,7 +385,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes', () => { - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', attributes: { @@ -412,7 +412,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes (styles)', () => { - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', styles: { @@ -441,7 +441,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes (classes)', () => { - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); editor.setData( '

    foobar

    ' ); @@ -459,7 +459,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow nested attributes', () => { - dataFilter.allowElement( { name: /^(article|section)$/ } ); + dataFilter.allowElement( /^(article|section)$/ ); dataFilter.allowAttributes( { name: /[\s\S]+/, attributes: { 'data-foo': /foo|bar/ } } ); editor.setData( '
    ' + @@ -494,7 +494,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes for all allowed definitions', () => { - dataFilter.allowElement( { name: /^(section|article)$/ } ); + dataFilter.allowElement( /^(section|article)$/ ); dataFilter.allowAttributes( { name: /^(section|article)$/, attributes: { 'data-foo': 'foo' } } ); dataFilter.allowAttributes( { name: /^(section|article)$/, attributes: { 'data-bar': 'bar' } } ); @@ -528,7 +528,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes', () => { - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'section', attributes: { 'data-foo': 'bar' } } ); @@ -556,7 +556,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (styles)', () => { - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', styles: { color: /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'section', styles: { color: 'red' } } ); @@ -584,7 +584,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (classes)', () => { - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', classes: [ 'foo', 'bar' ] } ); dataFilter.disallowAttributes( { name: 'section', classes: [ 'bar' ] } ); @@ -616,7 +616,7 @@ describe( 'DataFilter', () => { } ); expect( () => { - dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowElement( 'xyz' ); } ).to.not.throw(); } ); @@ -627,7 +627,7 @@ describe( 'DataFilter', () => { } ); } ); - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); editor.setData( '

    foo

    ' ); @@ -647,7 +647,7 @@ describe( 'DataFilter', () => { }, { priority: 'high' } ); } ); - dataFilter.allowElement( { name: 'section' } ); + dataFilter.allowElement( 'section' ); dataFilter.allowAttributes( { name: 'section', attributes: { 'data-foo': true } } ); editor.setData( '

    foo

    ' ); @@ -670,7 +670,7 @@ describe( 'DataFilter', () => { it( 'should not convert attributes if the model schema item definition is not registered', () => { dataSchema.registerBlockElement( { view: 'xyz', model: 'modelXyz' } ); - dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowElement( 'xyz' ); dataFilter.allowAttributes( { name: 'xyz', attributes: { 'data-foo': 'foo' } } ); // We are not registering model schema anywhere, to check if upcast @@ -694,7 +694,7 @@ describe( 'DataFilter', () => { editor.model.schema.register( 'htmlXyz', { inheritAllFrom: '$block' } ); - dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowElement( 'xyz' ); editor.setData( 'foo' ); @@ -725,7 +725,7 @@ describe( 'DataFilter', () => { describe( 'inline', () => { it( 'should allow element', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); editor.setData( '

    foobar

    ' ); @@ -740,7 +740,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow deeply nested structure', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); editor.setData( '

    foobarbaz' ); @@ -757,7 +757,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', attributes: { @@ -782,7 +782,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes (styles)', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', styles: { @@ -811,7 +811,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes (classes)', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', classes: [ 'foo', 'bar' ] } ); editor.setData( '

    foobar

    ' ); @@ -827,7 +827,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow nested attributes', () => { - dataFilter.allowElement( { name: /^(span|cite)$/ } ); + dataFilter.allowElement( /^(span|cite)$/ ); dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-foo': 'foo' } } ); dataFilter.allowAttributes( { name: /^(span|cite)$/, attributes: { 'data-bar': 'bar' } } ); @@ -869,7 +869,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'cite', attributes: { 'data-foo': 'bar' } } ); @@ -891,7 +891,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (styles)', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', styles: { color: /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'cite', styles: { color: 'red' } } ); @@ -920,7 +920,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (classes)', () => { - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', classes: [ 'foo', 'bar' ] } ); dataFilter.disallowAttributes( { name: 'cite', classes: [ 'bar' ] } ); @@ -949,7 +949,7 @@ describe( 'DataFilter', () => { } ); } ); - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', attributes: { 'data-foo': true } } ); editor.setData( '

    foo

    ' ); @@ -971,7 +971,7 @@ describe( 'DataFilter', () => { }, { priority: 'high' } ); } ); - dataFilter.allowElement( { name: 'cite' } ); + dataFilter.allowElement( 'cite' ); dataFilter.allowAttributes( { name: 'cite', attributes: { 'data-foo': true } } ); editor.setData( '

    foo

    ' ); @@ -992,7 +992,7 @@ describe( 'DataFilter', () => { } ); it( 'should correctly merge class names', () => { - dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowElement( 'span' ); dataFilter.allowAttributes( { name: 'span', classes: /[\s\S]+/ } ); editor.setData( '

    foobarbaz

    ' ); @@ -1027,7 +1027,7 @@ describe( 'DataFilter', () => { } } ); - dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowElement( 'xyz' ); expect( editor.model.schema.getAttributeProperties( 'htmlXyz' ) ).to.deep.equal( { copyOnEnter: true } ); } ); @@ -1038,7 +1038,7 @@ describe( 'DataFilter', () => { model: 'htmlXyz' } ); - dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowElement( 'xyz' ); expect( editor.model.schema.getAttributeProperties( 'htmlXyz' ) ).to.deep.equal( {} ); } ); @@ -1059,7 +1059,7 @@ describe( 'DataFilter', () => { } } ); - dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowElement( 'xyz' ); editor.setData( '

    foobar

    ' ); @@ -1072,7 +1072,7 @@ describe( 'DataFilter', () => { // 'a' element is registered by data schema with priority 5. // We are checking if this element will be correctly nested due to different // AttributeElement priority than default. - dataFilter.allowElement( { name: 'a' } ); + dataFilter.allowElement( 'a' ); dataFilter.allowAttributes( { name: 'a', attributes: { 'data-foo': 'foo' } } ); editor.setData( '

    link

    ' ); @@ -1093,7 +1093,7 @@ describe( 'DataFilter', () => { } ); it( 'should correctly resolve attributes nesting order', () => { - dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowElement( 'span' ); dataFilter.allowAttributes( { name: 'span', styles: { 'font-weight': /[\s\S]+/ } } ); editor.setData( '

    foobar' ); @@ -1111,7 +1111,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow using attributes by other features', () => { - dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowElement( 'span' ); dataFilter.allowAttributes( { name: 'span', styles: { 'color': /[\s\S]+/ } } ); editor.setData( '

    foobar

    ' ); @@ -1129,7 +1129,7 @@ describe( 'DataFilter', () => { describe( 'existing features', () => { it( 'should allow additional attributes', () => { - dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowElement( 'p' ); dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': 'foo' } } ); editor.setData( '

    foo

    ' ); @@ -1147,7 +1147,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow additional attributes (classes)', () => { - dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowElement( 'p' ); dataFilter.allowAttributes( { name: 'p', classes: /[\s\S]+/ } ); editor.setData( '

    foo

    bar

    ' ); @@ -1168,7 +1168,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow additional attributes (styles)', () => { - dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowElement( 'p' ); dataFilter.allowAttributes( { name: 'p', styles: { 'color': /[\s\S]+/ } } ); editor.setData( '

    foo

    ' ); @@ -1186,7 +1186,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes', () => { - dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowElement( 'p' ); dataFilter.allowAttributes( { name: 'p', attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'p', attributes: { 'data-foo': 'bar' } } ); @@ -1207,7 +1207,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (styles)', () => { - dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowElement( 'p' ); dataFilter.allowAttributes( { name: 'p', styles: { color: /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: 'p', styles: { color: 'red' } } ); @@ -1228,7 +1228,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (classes)', () => { - dataFilter.allowElement( { name: 'p' } ); + dataFilter.allowElement( 'p' ); dataFilter.allowAttributes( { name: 'p', classes: [ 'foo', 'bar' ] } ); dataFilter.disallowAttributes( { name: 'p', classes: [ 'bar' ] } ); @@ -1244,7 +1244,7 @@ describe( 'DataFilter', () => { } ); it( 'should preserve attributes not used by other features', () => { - dataFilter.allowElement( { name: 'span' } ); + dataFilter.allowElement( 'span' ); dataFilter.allowAttributes( { name: 'span', styles: { 'color': /[\s\S]+/ } } ); dataFilter.allowAttributes( { name: 'span', classes: [ 'foo', 'bar' ] } ); @@ -1272,8 +1272,13 @@ describe( 'DataFilter', () => { sinon.stub( dataSchema, 'getDefinitionsForView' ).returns( new Set( [ definition ] ) ); expectToThrowCKEditorError( () => { +<<<<<<< HEAD dataFilter.allowElement( { name: 'xyz' } ); }, /data-filter-invalid-definition/, null, definition ); +======= + dataFilter.allowElement( 'xyz' ); + }, /data-filter-invalid-definition-type/, null, definition ); +>>>>>>> 8dd542497f (Update `allowElement()` method and tests.) } ); describe( 'loadAllowedConfig', () => { diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js index defe6d24752..5863fa3bb29 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js @@ -40,7 +40,7 @@ class ExtendHTMLSupport extends Plugin { } ); // Custom elements need to be filtered using direct API instead of config. - dataFilter.allowElement( { name: 'xyz' } ); + dataFilter.allowElement( 'xyz' ); } } From 3df9664cf56dd7e60f0f10f157d1700c7891bd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Fri, 4 Jun 2021 12:58:17 +0200 Subject: [PATCH 166/217] Rebase fixes. --- .../tests/codeblock.js | 16 ++-- .../tests/datafilter.js | 74 +++---------------- .../tests/manual/codeblock.js | 2 +- .../tests/manual/objects.js | 2 +- 4 files changed, 22 insertions(+), 72 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/tests/codeblock.js b/packages/ckeditor5-content-compatibility/tests/codeblock.js index cbed5810529..eab4b694294 100644 --- a/packages/ckeditor5-content-compatibility/tests/codeblock.js +++ b/packages/ckeditor5-content-compatibility/tests/codeblock.js @@ -37,7 +37,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); it( 'should allow attributes', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); editor.setData( '
    foobar
    ' ); @@ -64,7 +64,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); it( 'should allow attributes (classes)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); editor.setData( '
    foobar
    ' ); @@ -87,7 +87,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); it( 'should allow attributes (styles)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); @@ -115,7 +115,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); it( 'should disallow attributes', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); dataFilter.disallowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); @@ -130,7 +130,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); it( 'should disallow attributes (classes)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); dataFilter.disallowAttributes( { name: /^(pre|code)$/, classes: [ 'foo' ] } ); @@ -145,7 +145,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); it( 'should disallow attributes (styles)', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: 'pre', styles: { background: 'blue' } } ); dataFilter.allowAttributes( { name: 'code', styles: { color: 'red' } } ); @@ -164,7 +164,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); it( 'should allow attributes on code element existing alone', () => { - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: 'code', attributes: { 'data-foo': /[\s\S]+/ } } ); editor.setData( '

    foobar

    ' ); @@ -192,7 +192,7 @@ describe( 'CodeBlockHtmlSupport', () => { } ); } ); - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': true } } ); editor.setData( '
    foobar
    ' ); diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index f17923f07de..51d46db9971 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -138,7 +138,7 @@ describe( 'DataFilter', () => { describe( 'object', () => { it( 'should allow element', () => { - dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowElement( 'input' ); editor.setData( '

    ' ); @@ -150,7 +150,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow element content', () => { - dataFilter.allowElement( { name: 'video' } ); + dataFilter.allowElement( 'video' ); editor.setData( '

    ' ); @@ -213,7 +213,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes (styles)', () => { - dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowElement( 'input' ); dataFilter.allowAttributes( { name: 'input', styles: { color: 'red' } } ); editor.setData( '

    ' ); @@ -233,7 +233,7 @@ describe( 'DataFilter', () => { } ); it( 'should allow attributes (classes)', () => { - dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowElement( 'input' ); dataFilter.allowAttributes( { name: 'input', classes: [ 'foobar' ] } ); editor.setData( '

    ' ); @@ -251,7 +251,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes', () => { - dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowElement( 'input' ); dataFilter.allowAttributes( { name: 'input', attributes: { type: true } } ); dataFilter.disallowAttributes( { name: 'input', attributes: { type: 'hidden' } } ); @@ -275,7 +275,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (styles)', () => { - dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowElement( 'input' ); dataFilter.allowAttributes( { name: 'input', styles: { color: /^(red|blue)$/ } } ); dataFilter.disallowAttributes( { name: 'input', styles: { color: 'red' } } ); @@ -299,7 +299,7 @@ describe( 'DataFilter', () => { } ); it( 'should disallow attributes (classes)', () => { - dataFilter.allowElement( { name: 'input' } ); + dataFilter.allowElement( 'input' ); dataFilter.allowAttributes( { name: 'input', classes: [ 'foo', 'bar' ] } ); dataFilter.disallowAttributes( { name: 'input', classes: [ 'bar' ] } ); @@ -718,7 +718,7 @@ describe( 'DataFilter', () => { // At this point we will be trying to register converter without valid view name. expect( () => { - dataFilter.allowElement( { name: 'bar' } ); + dataFilter.allowElement( 'bar' ); } ).to.not.throw(); } ); } ); @@ -1272,13 +1272,8 @@ describe( 'DataFilter', () => { sinon.stub( dataSchema, 'getDefinitionsForView' ).returns( new Set( [ definition ] ) ); expectToThrowCKEditorError( () => { -<<<<<<< HEAD - dataFilter.allowElement( { name: 'xyz' } ); - }, /data-filter-invalid-definition/, null, definition ); -======= dataFilter.allowElement( 'xyz' ); - }, /data-filter-invalid-definition-type/, null, definition ); ->>>>>>> 8dd542497f (Update `allowElement()` method and tests.) + }, /data-filter-invalid-definition/, null, definition ); } ); describe( 'loadAllowedConfig', () => { @@ -1720,49 +1715,4 @@ describe( 'DataFilter', () => { ); } ); } ); - - function getModelDataWithAttributes( model, options ) { - // Simplify GHS attributes as they are not very readable at this point due to object structure. - let counter = 1; - const data = getModelData( model, options ).replace( /(html.*?)="{.*?}"/g, ( fullMatch, attributeName ) => { - return `${ attributeName }="(${ counter++ })"`; - } ); - - const range = model.createRangeIn( model.document.getRoot() ); - - let attributes = []; - for ( const item of range.getItems() ) { - for ( const [ key, value ] of sortAttributes( item.getAttributes() ) ) { - if ( key.startsWith( 'html' ) ) { - attributes.push( value ); - } - } - } - - attributes = attributes.reduce( ( prev, cur, index ) => { - prev[ index + 1 ] = cur; - return prev; - }, {} ); - - return { data, attributes }; - } - - function sortAttributes( attributes ) { - attributes = Array.from( attributes ); - - return attributes.sort( ( attr1, attr2 ) => { - const key1 = attr1[ 0 ]; - const key2 = attr2[ 0 ]; - - if ( key1 > key2 ) { - return 1; - } - - if ( key1 < key2 ) { - return -1; - } - - return 0; - } ); - } } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js index 2ce9ed27874..bda1ed39ed4 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js @@ -27,7 +27,7 @@ class ExtendHTMLSupport extends Plugin { init() { const dataFilter = this.editor.plugins.get( 'DataFilter' ); - dataFilter.allowElement( { name: /^(pre|code)$/ } ); + dataFilter.allowElement( /^(pre|code)$/ ); dataFilter.allowAttributes( { name: /^(pre|code)$/, styles: { color: /[\s\S]+/ } } ); dataFilter.allowAttributes( { name: /^(pre|code)$/, styles: { background: /[\s\S]+/ } } ); dataFilter.allowAttributes( { name: /^(pre|code)$/, attributes: { 'data-foo': /[\s\S]+/ } } ); diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-content-compatibility/tests/manual/objects.js index cdabcd5531d..76a6c9b97b4 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/objects.js +++ b/packages/ckeditor5-content-compatibility/tests/manual/objects.js @@ -63,7 +63,7 @@ class ExtendHTMLSupport extends Plugin { ]; for ( const definition of definitions ) { - dataFilter.allowElement( { name: definition.name } ); + dataFilter.allowElement( definition.name ); for ( const key of ( definition.attributes || [] ) ) { const attributes = {}; From f8a2f3988ef9f9c08272c9f55666f6c9fb11751a Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 8 Jun 2021 10:41:54 +0200 Subject: [PATCH 167/217] Fixed loading data filter by ghs plugin. --- .../src/generalhtmlsupport.js | 36 ++++--------------- .../tests/dataschema.js | 6 ++-- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js index e7eb657620b..81e615ba2e9 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js @@ -9,14 +9,13 @@ import { Plugin } from 'ckeditor5/src/core'; import DataFilter from './datafilter'; -import DataSchema from './dataschema'; import CodeBlockHtmlSupport from './integrations/codeblock'; /** * The General HTML Support feature. * - * This is a "glue" plugin which initializes the {@link module:content-compatibility/dataschema~DataSchema data schema} - * and {@link module:content-compatibility/datafilter~DataFilter data filter} features. + * This is a "glue" plugin which initializes the {@link module:content-compatibility/datafilter~DataFilter data filter} configuration + * and features integration with the General HTML Support. * * @extends module:core/plugin~Plugin */ @@ -28,33 +27,13 @@ export default class GeneralHtmlSupport extends Plugin { return 'GeneralHtmlSupport'; } - /** - * @param {module:core/editor/editor~Editor} editor - */ - constructor( editor ) { - super( editor ); - - /** - * An instance of the {@link module:content-compatibility/dataschema~DataSchema}. - * - * @readonly - * @member {module:content-compatibility/dataschema~DataSchema} #dataSchema - */ - this.dataSchema = new DataSchema(); - - /** - * An instance of the {@link module:content-compatibility/datafilter~DataFilter}. - * - * @readonly - * @member {module:content-compatibility/datafilter~DataFilter} #dataFilter - */ - this.dataFilter = new DataFilter( editor, this.dataSchema ); - } - init() { + const editor = this.editor; + const dataFilter = editor.plugins.get( DataFilter ); + // Load the filtering configuration. - this.dataFilter.loadAllowedConfig( this.editor.config.get( 'generalHtmlSupport.allowed' ) || [] ); - this.dataFilter.loadDisallowedConfig( this.editor.config.get( 'generalHtmlSupport.disallowed' ) || [] ); + dataFilter.loadAllowedConfig( editor.config.get( 'generalHtmlSupport.allowed' ) || [] ); + dataFilter.loadDisallowedConfig( editor.config.get( 'generalHtmlSupport.disallowed' ) || [] ); } /** @@ -63,7 +42,6 @@ export default class GeneralHtmlSupport extends Plugin { static get requires() { return [ DataFilter, - DataSchema, CodeBlockHtmlSupport ]; } diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-content-compatibility/tests/dataschema.js index 49523214515..46f4160cebb 100644 --- a/packages/ckeditor5-content-compatibility/tests/dataschema.js +++ b/packages/ckeditor5-content-compatibility/tests/dataschema.js @@ -14,11 +14,13 @@ describe( 'DataSchema', () => { beforeEach( () => { return VirtualTestEditor - .create() + .create( { + plugins: [ DataSchema ] + } ) .then( newEditor => { editor = newEditor; - dataSchema = new DataSchema(); + dataSchema = editor.plugins.get( DataSchema ); } ); } ); From 9d9adaaa2858ecb2f6ff34bd6a1afed3f61b3b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Tue, 8 Jun 2021 13:41:23 +0200 Subject: [PATCH 168/217] Review fixes - more tests. --- .../src/datafilter.js | 2 +- .../tests/datafilter.js | 176 +++++++++++++++++- 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index bbf63546c38..b2ce91b359d 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -11,7 +11,7 @@ import DataSchema from './dataschema'; import { Plugin } from 'ckeditor5/src/core'; import { Matcher } from 'ckeditor5/src/engine'; -import { priorities, toArray, CKEditorError } from 'ckeditor5/src/utils'; +import { priorities, CKEditorError } from 'ckeditor5/src/utils'; import { Widget } from 'ckeditor5/src/widget'; import { disallowedAttributesConverter, diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 51d46db9971..bfc0546e6f9 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -1328,7 +1328,7 @@ describe( 'DataFilter', () => { ); } ); - it( 'should match all values', () => { + it( 'should match all values - across styles, classes and attributes', () => { // Sanity check test for splitting patterns that are not objects nor arrays. const config = [ @@ -1385,6 +1385,180 @@ describe( 'DataFilter', () => { ); } ); + it( 'should match all values - array of values', () => { + const config = [ + { + name: 'span', + classes: [ 'foo', 'bar' ] + } + ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( + '

    ' + + 'foo bar' + + 'foo' + + 'bar' + + 'bar baz' + + '

    ' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlSpan="(1)">foo bar' + + '<$text htmlSpan="(2)">foo' + + '<$text htmlSpan="(3)">barbar baz' + + '', + attributes: { + 1: { + classes: [ 'foo', 'bar' ] + }, + 2: { + classes: [ 'foo' ] + }, + 3: { + classes: [ 'bar' ] + }, + 4: { + classes: [ 'bar' ] + } + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + 'foo bar' + + 'foo' + + 'barbar baz' + + '

    ' + ); + } ); + + it( 'should match all values - array of objects', () => { + const config = [ + { + name: 'span', + styles: [ + { key: 'position', value: true }, + { key: 'visibility', value: true } + ] + } + ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( + '

    ' + + 'foo bar' + + 'foo' + + 'bar' + + 'bar baz' + + '

    ' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlSpan="(1)">foo bar' + + '<$text htmlSpan="(2)">foo' + + '<$text htmlSpan="(3)">barbar baz' + + '', + attributes: { + 1: { + styles: { + position: 'absolute', + visibility: 'hidden' + } + }, + 2: { + styles: { + position: 'absolute' + } + }, + 3: { + styles: { + visibility: 'hidden' + } + }, + 4: { + styles: { + visibility: 'hidden' + } + } + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + 'foo bar' + + 'foo' + + 'barbar baz' + + '

    ' + ); + } ); + + it( 'should match all values - object', () => { + const config = [ + { + name: 'span', + styles: { + position: true, + visibility: true + } + } + ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( + '

    ' + + 'foo bar' + + 'foo' + + 'bar' + + 'bar baz' + + '

    ' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: '' + + '<$text htmlSpan="(1)">foo bar' + + '<$text htmlSpan="(2)">foo' + + '<$text htmlSpan="(3)">barbar baz' + + '', + attributes: { + 1: { + styles: { + position: 'absolute', + visibility: 'hidden' + } + }, + 2: { + styles: { + position: 'absolute' + } + }, + 3: { + styles: { + visibility: 'hidden' + } + }, + 4: { + styles: { + visibility: 'hidden' + } + } + } + } ); + + expect( editor.getData() ).to.equal( + '

    ' + + 'foo bar' + + 'foo' + + 'barbar baz' + + '

    ' + ); + } ); + it( 'should match attributes', () => { const config = [ { From 4a444d579e787fe827476b26cd1d71f88c0178e1 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 8 Jun 2021 14:52:26 +0200 Subject: [PATCH 169/217] Allow matching all elements by skipping name. --- .../src/datafilter.js | 6 +++++- .../tests/datafilter.js | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index b2ce91b359d..f8a76e74f64 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -198,7 +198,11 @@ export default class DataFilter extends Plugin { */ _loadConfig( config, handleAttributes ) { for ( const pattern of config ) { - this.allowElement( pattern.name ); + // MatcherPattern allows omitting `name` to not narrow searches to specific elements. + // Let's keep it consistent and match every element if a `name` has not been provided. + const elementName = pattern.name || /[\s\S]+/; + + this.allowElement( elementName ); this._splitRules( pattern ).forEach( handleAttributes ); } diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index bfc0546e6f9..6a5f6d756b6 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -1277,6 +1277,25 @@ describe( 'DataFilter', () => { } ); describe( 'loadAllowedConfig', () => { + it( 'should allow match all elements by ommiting pattern name', () => { + dataSchema.registerBlockElement( { + model: 'htmlXyz', + view: 'xyz', + modelSchema: { + inheritAllFrom: '$block' + } + } ); + + const config = [ {} ]; + + dataFilter.loadAllowedConfig( config ); + + editor.setData( 'foobar' ); + + expect( getModelData( model, { withoutSelection: true } ) ).to.equal( 'foobar' ); + expect( editor.getData() ).to.equal( 'foobar' ); + } ); + it( 'should load config and match whenever a single match has been found', () => { const config = [ { From 4171bfdba1debdd62eb9dc66071d2883a6f8e5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Wed, 9 Jun 2021 13:56:56 +0200 Subject: [PATCH 170/217] Move methods outside of a class. --- .../src/datafilter.js | 112 +++++++++--------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index f8a76e74f64..62563c5a9f5 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -204,35 +204,10 @@ export default class DataFilter extends Plugin { this.allowElement( elementName ); - this._splitRules( pattern ).forEach( handleAttributes ); + splitRules( pattern ).forEach( handleAttributes ); } } - /** - * Rules are matched in conjunction (AND operation), but we want to have a match if any of the rules is matched (OR operation). - * By splitting the rules we force the latter effect. - * - * @private - * @param {module:engine/view/matcher~MatcherPattern} rules - * @returns {Array.} - */ - _splitRules( rules ) { - const { name, attributes, classes, styles } = rules; - const splitRules = []; - - if ( attributes ) { - splitRules.push( ...this._splitPattern( { name, attributes }, 'attributes' ) ); - } - if ( classes ) { - splitRules.push( ...this._splitPattern( { name, classes }, 'classes' ) ); - } - if ( styles ) { - splitRules.push( ...this._splitPattern( { name, styles }, 'styles' ) ); - } - - return splitRules; - } - /** * Matches and consumes allowed view attributes. * @@ -263,38 +238,6 @@ export default class DataFilter extends Plugin { return consumeAttributes( viewElement, conversionApi, this._disallowedAttributes ); } - /** - * Matcher by default has to match **all** patterns to count it as an actual match. By splitting the pattern - * into separate patterns means that any matched pattern will be count as a match. - * - * @private - * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern to split. - * @param {String} attributeName Name of the attribute to split (e.g. 'attributes', 'classes', 'styles'). - * @returns {Array.} - */ - _splitPattern( pattern, attributeName ) { - const { name } = pattern; - - if ( isPlainObject( pattern[ attributeName ] ) ) { - return Object.entries( pattern[ attributeName ] ).map( - ( [ key, value ] ) => ( { - name, - [ attributeName ]: { - [ key ]: value - } - } ) ); - } else if ( Array.isArray( pattern[ attributeName ] ) ) { - return pattern[ attributeName ].map( - value => ( { - name, - [ attributeName ]: [ value ] - } ) - ); - } - - return [ pattern ]; - } - /** * Registers elements allowed by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method * once {@link module:core/editor~Editor#data editor's data controller} is initialized. @@ -617,3 +560,56 @@ function iterableToObject( iterable, getValue ) { return attributesObject; } + +// Matcher by default has to match **all** patterns to count it as an actual match. By splitting the pattern +// into separate patterns means that any matched pattern will be count as a match. +// +// @private +// @param {module:engine/view/matcher~MatcherPattern} pattern Pattern to split. +// @param {String} attributeName Name of the attribute to split (e.g. 'attributes', 'classes', 'styles'). +// @returns {Array.} +function splitPattern( pattern, attributeName ) { + const { name } = pattern; + + if ( isPlainObject( pattern[ attributeName ] ) ) { + return Object.entries( pattern[ attributeName ] ).map( + ( [ key, value ] ) => ( { + name, + [ attributeName ]: { + [ key ]: value + } + } ) ); + } else if ( Array.isArray( pattern[ attributeName ] ) ) { + return pattern[ attributeName ].map( + value => ( { + name, + [ attributeName ]: [ value ] + } ) + ); + } + + return [ pattern ]; +} + +// Rules are matched in conjunction (AND operation), but we want to have a match if *any* of the rules is matched (OR operation). +// By splitting the rules we force the latter effect. +// +// @private +// @param {module:engine/view/matcher~MatcherPattern} rules +// @returns {Array.} +function splitRules( rules ) { + const { name, attributes, classes, styles } = rules; + const splittedRules = []; + + if ( attributes ) { + splittedRules.push( ...splitPattern( { name, attributes }, 'attributes' ) ); + } + if ( classes ) { + splittedRules.push( ...splitPattern( { name, classes }, 'classes' ) ); + } + if ( styles ) { + splittedRules.push( ...splitPattern( { name, styles }, 'styles' ) ); + } + + return splittedRules; +} From a14f1e059e0724297ca1118943304d14ce30db7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maksymilian=20Barna=C5=9B?= Date: Thu, 10 Jun 2021 11:02:28 +0200 Subject: [PATCH 171/217] Review fixes. --- .../src/datafilter.js | 4 +- .../tests/datafilter.js | 306 ++++++++++-------- 2 files changed, 182 insertions(+), 128 deletions(-) diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-content-compatibility/src/datafilter.js index 62563c5a9f5..bfc80152c04 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-content-compatibility/src/datafilter.js @@ -579,7 +579,9 @@ function splitPattern( pattern, attributeName ) { [ key ]: value } } ) ); - } else if ( Array.isArray( pattern[ attributeName ] ) ) { + } + + if ( Array.isArray( pattern[ attributeName ] ) ) { return pattern[ attributeName ].map( value => ( { name, diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-content-compatibility/tests/datafilter.js index 6a5f6d756b6..085079bfeeb 100644 --- a/packages/ckeditor5-content-compatibility/tests/datafilter.js +++ b/packages/ckeditor5-content-compatibility/tests/datafilter.js @@ -1310,18 +1310,18 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text fontColor="blue" htmlSpan="(1)">foobar' + - '<$text htmlSpan="(2)">foo data' + - '<$text htmlSpan="(3)">bar data' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">ccc' + '', attributes: { 1: { @@ -1338,11 +1338,9 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - '' + - 'foobar' + - '' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); } ); @@ -1363,22 +1361,25 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text fontColor="blue" htmlSpan="(1)">foobar' + - '<$text htmlSpan="(2)">foo data' + - '<$text htmlSpan="(3)">bar data' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">ccc' + '', attributes: { 1: { - classes: [ 'foo', 'bar' ] + attributes: { + class: 'foo bar', + style: 'font-weight:400;line-height:1em;' + } }, 2: { attributes: { @@ -1395,11 +1396,9 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - '' + - 'foobar' + - '' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); } ); @@ -1416,18 +1415,18 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foo bar' + - 'foo' + - 'bar' + - 'bar baz' + + 'aaa' + + 'bbb' + + 'ccc' + + 'ddd' + '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text htmlSpan="(1)">foo bar' + - '<$text htmlSpan="(2)">foo' + - '<$text htmlSpan="(3)">barbar baz' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">cccddd' + '', attributes: { 1: { @@ -1447,9 +1446,9 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - 'foo bar' + - 'foo' + - 'barbar baz' + + 'aaa' + + 'bbb' + + 'cccddd' + '

    ' ); } ); @@ -1469,18 +1468,18 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foo bar' + - 'foo' + - 'bar' + - 'bar baz' + + 'aaa' + + 'bbb' + + 'ccc' + + 'ddd' + '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text htmlSpan="(1)">foo bar' + - '<$text htmlSpan="(2)">foo' + - '<$text htmlSpan="(3)">barbar baz' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">cccddd' + '', attributes: { 1: { @@ -1509,9 +1508,9 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - 'foo bar' + - 'foo' + - 'barbar baz' + + 'aaa' + + 'bbb' + + 'cccddd' + '

    ' ); } ); @@ -1531,18 +1530,18 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foo bar' + - 'foo' + - 'bar' + - 'bar baz' + + 'aaa' + + 'bbb' + + 'ccc' + + 'ddd' + '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text htmlSpan="(1)">foo bar' + - '<$text htmlSpan="(2)">foo' + - '<$text htmlSpan="(3)">barbar baz' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">cccddd' + '', attributes: { 1: { @@ -1571,9 +1570,9 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - 'foo bar' + - 'foo' + - 'barbar baz' + + 'aaa' + + 'bbb' + + 'cccddd' + '

    ' ); } ); @@ -1590,18 +1589,18 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text fontColor="blue" htmlSpan="(1)">foobar' + - '<$text htmlSpan="(2)">foo data' + - '<$text htmlSpan="(3)">bar data' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">ccc' + '', attributes: { 1: {}, @@ -1616,11 +1615,9 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - '' + - 'foobar' + - '' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); } ); @@ -1637,17 +1634,17 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text fontColor="blue" htmlSpan="(1)">foobar' + - '<$text htmlSpan="(2)">foo databar data' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbbccc' + '', attributes: { 1: { @@ -1660,10 +1657,8 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - '' + - 'foobar' + - '' + - 'foo databar data' + + 'aaa' + + 'bbbccc' + '

    ' ); } ); @@ -1672,7 +1667,7 @@ describe( 'DataFilter', () => { const config = [ { name: 'span', - styles: { color: true } + styles: { 'line-height': true } } ]; @@ -1680,31 +1675,38 @@ describe( 'DataFilter', () => { editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); - // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text fontColor="blue" htmlSpan="(1)">foobar' + - '<$text htmlSpan="(2)">foo databar data' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">ccc' + '', attributes: { - 1: {}, + 1: { + styles: { + 'line-height': '1em' + } + }, 2: {}, - 3: {} + 3: { + styles: { + 'line-height': '2em' + } + } } } ); expect( editor.getData() ).to.equal( '

    ' + - '' + - 'foobar' + - '' + - 'foo databar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); } ); @@ -1721,19 +1723,23 @@ describe( 'DataFilter', () => { } ]; + // First, allow all the elements matching config. + dataFilter.loadAllowedConfig( config ); + + // Then, disallow and verify it's actually working. dataFilter.loadDisallowedConfig( config ); editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text htmlSpan="(1)">foobarfoo databar data' + + '<$text htmlSpan="(1)">aaabbbccc' + '', attributes: { 1: {}, @@ -1744,7 +1750,7 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - 'foobarfoo databar data' + + 'aaabbbccc' + '

    ' ); } ); @@ -1761,20 +1767,24 @@ describe( 'DataFilter', () => { } ]; + // First, allow all the elements matching config. + dataFilter.loadAllowedConfig( config ); + + // Then, disallow and verify it's actually working. dataFilter.loadDisallowedConfig( config ); editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text htmlSpan="(1)">foobarfoo databar data' + + '<$text htmlSpan="(1)">aaabbbccc' + '', attributes: { 1: {}, @@ -1785,7 +1795,7 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - 'foobarfoo databar data' + + 'aaabbbccc' + '

    ' ); } ); @@ -1798,21 +1808,24 @@ describe( 'DataFilter', () => { } ]; + // First, allow all the elements matching config. + dataFilter.loadAllowedConfig( config ); + + // Then, disallow and verify it's actually working. dataFilter.loadDisallowedConfig( config ); editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text fontColor="blue" htmlSpan="(1)">foobar' + - '<$text htmlSpan="(2)">foo databar data' + + '<$text htmlSpan="(1)">aaabbbccc' + '', attributes: { 1: {}, @@ -1823,87 +1836,126 @@ describe( 'DataFilter', () => { expect( editor.getData() ).to.equal( '

    ' + - '' + - 'foobar' + - '' + - 'foo databar data' + + 'aaabbbccc' + '

    ' ); } ); it( 'should match classes', () => { - const config = [ + const allowedConfig = [ + { + name: 'span', + attributes: true, + // Allow it to really verify that the disallowing works. + classes: [ 'foo', 'bar', 'test' ] + } + ]; + const disallowedConfig = [ { name: 'span', classes: [ 'foo', 'bar', 'test' ] } ]; - dataFilter.loadDisallowedConfig( config ); + dataFilter.loadAllowedConfig( allowedConfig ); + dataFilter.loadDisallowedConfig( disallowedConfig ); editor.setData( '

    ' + - 'foobar' + - 'foo data' + - 'bar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text fontColor="blue" htmlSpan="(1)">foobar' + - '<$text htmlSpan="(2)">foo databar data' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">ccc' + '', attributes: { 1: {}, - 2: {}, - 3: {} + 2: { + attributes: { + 'data-foo': 'foo data' + } + }, + 3: { + attributes: { + 'data-bar': 'bar data' + } + } } } ); expect( editor.getData() ).to.equal( '

    ' + - '' + - 'foobar' + - '' + - 'foo databar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); } ); it( 'should match styles', () => { - const config = [ + const allowedConfig = [ + { + name: 'span', + attributes: true, + // Allow it to really verify that the disallowing works. + styles: { 'line-height': true } + } + ]; + const disallowedConfig = [ { name: 'span', - styles: { color: true } + styles: { 'line-height': true } } ]; - dataFilter.loadDisallowedConfig( config ); + // First, allow all the elements matching config. + dataFilter.loadAllowedConfig( allowedConfig ); + + // Then, disallow and verify it's actually working. + dataFilter.loadDisallowedConfig( disallowedConfig ); editor.setData( '

    ' + - '' + - 'foobar' + - 'foo databar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); // Font feature should take over color CSS property. expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { data: '' + - '<$text htmlSpan="(1)">foobarfoo databar data' + + '<$text htmlSpan="(1)">aaa' + + '<$text htmlSpan="(2)">bbb' + + '<$text htmlSpan="(3)">ccc' + '', attributes: { 1: {}, - 2: {} + 2: { + attributes: { + 'data-foo': 'foo data' + } + }, + 3: { + attributes: { + 'data-bar': 'bar data' + } + } } } ); expect( editor.getData() ).to.equal( '

    ' + - 'foobarfoo databar data' + + 'aaa' + + 'bbb' + + 'ccc' + '

    ' ); } ); From b93e929e033d5a16d1d3b2be1cc1f436834901a2 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Thu, 10 Jun 2021 12:47:50 +0200 Subject: [PATCH 172/217] Corrected sample. --- .../tests/manual/generalhtmlsupport.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html index a07a831fe6e..36c562b8946 100644 --- a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html +++ b/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html @@ -48,7 +48,7 @@

    Span with nested styles!

    -
    Red quote!
    +

    Red quote!

    • Blue!
    • From 5d963b966a56cd09c5c48c667df9138d0e9820b7 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 11 Jun 2021 08:50:50 +0200 Subject: [PATCH 173/217] Package renaming. --- .../package.json | 2 +- .../src/conversionutils.js | 2 +- .../src/converters.js | 24 ++++++------ .../src/datafilter.js | 34 ++++++++--------- .../src/dataschema.js | 38 +++++++++---------- .../src/generalhtmlsupport.js | 4 +- .../src/integrations/codeblock.js | 4 +- .../tests/_utils/utils.js | 0 .../tests/codeblock.js | 0 .../tests/datafilter.js | 0 .../tests/dataschema.js | 0 .../tests/manual/codeblock.html | 0 .../tests/manual/codeblock.js | 0 .../tests/manual/codeblock.md | 0 .../tests/manual/generalhtmlsupport.html | 0 .../tests/manual/generalhtmlsupport.js | 0 .../tests/manual/generalhtmlsupport.md | 0 .../tests/manual/objects.html | 0 .../tests/manual/objects.js | 0 .../tests/manual/objects.md | 0 .../theme/datafilter.css | 0 21 files changed, 54 insertions(+), 54 deletions(-) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/package.json (96%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/src/conversionutils.js (96%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/src/converters.js (86%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/src/datafilter.js (92%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/src/dataschema.js (84%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/src/generalhtmlsupport.js (85%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/src/integrations/codeblock.js (96%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/_utils/utils.js (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/codeblock.js (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/datafilter.js (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/dataschema.js (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/codeblock.html (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/codeblock.js (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/codeblock.md (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/generalhtmlsupport.html (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/generalhtmlsupport.js (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/generalhtmlsupport.md (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/objects.html (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/objects.js (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/tests/manual/objects.md (100%) rename packages/{ckeditor5-content-compatibility => ckeditor5-html-support}/theme/datafilter.css (100%) diff --git a/packages/ckeditor5-content-compatibility/package.json b/packages/ckeditor5-html-support/package.json similarity index 96% rename from packages/ckeditor5-content-compatibility/package.json rename to packages/ckeditor5-html-support/package.json index 93778128863..4b292806e3c 100644 --- a/packages/ckeditor5-content-compatibility/package.json +++ b/packages/ckeditor5-html-support/package.json @@ -1,5 +1,5 @@ { - "name": "@ckeditor/ckeditor5-content-compatibility", + "name": "@ckeditor/ckeditor5-html-support", "version": "28.0.0", "description": "Content compatibility feature", "private": true, diff --git a/packages/ckeditor5-content-compatibility/src/conversionutils.js b/packages/ckeditor5-html-support/src/conversionutils.js similarity index 96% rename from packages/ckeditor5-content-compatibility/src/conversionutils.js rename to packages/ckeditor5-html-support/src/conversionutils.js index 960d572b40c..b844a2e613e 100644 --- a/packages/ckeditor5-content-compatibility/src/conversionutils.js +++ b/packages/ckeditor5-html-support/src/conversionutils.js @@ -4,7 +4,7 @@ */ /** - * @module content-compatibility/conversionutils + * @module html-support/conversionutils */ import { cloneDeep } from 'lodash-es'; diff --git a/packages/ckeditor5-content-compatibility/src/converters.js b/packages/ckeditor5-html-support/src/converters.js similarity index 86% rename from packages/ckeditor5-content-compatibility/src/converters.js rename to packages/ckeditor5-html-support/src/converters.js index aa403cf8b78..6f2662031f9 100644 --- a/packages/ckeditor5-content-compatibility/src/converters.js +++ b/packages/ckeditor5-html-support/src/converters.js @@ -4,7 +4,7 @@ */ /** - * @module content-compatibility/converters + * @module html-support/converters */ import { toWidget } from 'ckeditor5/src/widget'; @@ -16,8 +16,8 @@ import { setViewAttributes, mergeViewElementAttributes } from './conversionutils * This converter listenes on `high` priority to ensure that all attributes are consumed * before standard priority converters. * - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition - * @param {module:content-compatibility/datafilter~DataFilter} dataFilter + * @param {module:html-support/dataschema~DataSchemaDefinition} definition + * @param {module:html-support/datafilter~DataFilter} dataFilter * @returns {Function} Returns a conversion callback. */ export function disallowedAttributesConverter( { view: viewName }, dataFilter ) { @@ -33,7 +33,7 @@ export function disallowedAttributesConverter( { view: viewName }, dataFilter ) * * Preserves object element content in `htmlContent` attribute. * - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:html-support/dataschema~DataSchemaDefinition} definition * @returns {Function} Returns a conversion callback. */ export function viewToModelObjectConverter( { model: modelName } ) { @@ -49,7 +49,7 @@ export function viewToModelObjectConverter( { model: modelName } ) { * Conversion helper converting object element to HTML object widget. * * @param {module:core/editor/editor~Editor} editor - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition * @returns {Function} Returns a conversion callback. */ export function toObjectWidgetConverter( editor, { view: viewName, isInline } ) { @@ -94,11 +94,11 @@ export function createObjectView( viewName, modelElement, writer ) { * View-to-attribute conversion helper preserving inline element attributes on `$text`. * * All allowed element attributes will be preserved as a value of - * {@link module:content-compatibility/dataschema~DataSchemaInlineElementDefinition~model definition model} + * {@link module:html-support/dataschema~DataSchemaInlineElementDefinition~model definition model} * attribute. * - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition - * @param {module:content-compatibility/datafilter~DataFilter} dataFilter + * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:html-support/datafilter~DataFilter} dataFilter * @returns {Function} Returns a conversion callback. */ export function viewToAttributeInlineConverter( { view: viewName, model: attributeKey }, dataFilter ) { @@ -130,7 +130,7 @@ export function viewToAttributeInlineConverter( { view: viewName, model: attribu /** * Attribute-to-view conversion helper applying attributes to view element preserved on `$text`. * - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition * @returns {Function} Returns a conversion callback. */ export function attributeToViewInlineConverter( { priority, view: viewName } ) { @@ -153,8 +153,8 @@ export function attributeToViewInlineConverter( { priority, view: viewName } ) { * * All matched attributes will be preserved on `htmlAttributes` attribute. * - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition - * @param {module:content-compatibility/datafilter~DataFilter} dataFilter + * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:html-support/datafilter~DataFilter} dataFilter * @returns {Function} Returns a conversion callback. */ export function viewToModelBlockAttributeConverter( { view: viewName }, dataFilter ) { @@ -177,7 +177,7 @@ export function viewToModelBlockAttributeConverter( { view: viewName }, dataFilt * Model-to-view conversion helper applying attributes preserved in `htmlAttributes` attribute * for block elements. * - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition * @returns {Function} Returns a conversion callback. */ export function modelToViewBlockAttributeConverter( { model: modelName } ) { diff --git a/packages/ckeditor5-content-compatibility/src/datafilter.js b/packages/ckeditor5-html-support/src/datafilter.js similarity index 92% rename from packages/ckeditor5-content-compatibility/src/datafilter.js rename to packages/ckeditor5-html-support/src/datafilter.js index bfc80152c04..f5856727cb8 100644 --- a/packages/ckeditor5-content-compatibility/src/datafilter.js +++ b/packages/ckeditor5-html-support/src/datafilter.js @@ -4,7 +4,7 @@ */ /** - * @module content-compatibility/datafilter + * @module html-support/datafilter */ import DataSchema from './dataschema'; @@ -31,9 +31,9 @@ import { isPlainObject } from 'lodash-es'; import '../theme/datafilter.css'; /** - * Allows to validate elements and element attributes registered by {@link module:content-compatibility/dataschema~DataSchema}. + * Allows to validate elements and element attributes registered by {@link module:html-support/dataschema~DataSchema}. * - * To enable registered element in the editor, use {@link module:content-compatibility/datafilter~DataFilter#allowElement} method: + * To enable registered element in the editor, use {@link module:html-support/datafilter~DataFilter#allowElement} method: * * dataFilter.allowElement( 'section' ); * @@ -62,11 +62,11 @@ export default class DataFilter extends Plugin { super( editor ); /** - * An instance of the {@link module:content-compatibility/dataschema~DataSchema}. + * An instance of the {@link module:html-support/dataschema~DataSchema}. * * @readonly * @private - * @member {module:content-compatibility/dataschema~DataSchema} #_dataSchema + * @member {module:html-support/dataschema~DataSchema} #_dataSchema */ this._dataSchema = editor.plugins.get( 'DataSchema' ); @@ -91,11 +91,11 @@ export default class DataFilter extends Plugin { this._disallowedAttributes = new Matcher(); /** - * Allowed element definitions by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method. + * Allowed element definitions by {@link module:html-support/datafilter~DataFilter#allowElement} method. * * @readonly * @private - * @member {Set.} #_allowedElements + * @member {Set.} #_allowedElements */ this._allowedElements = new Set(); @@ -148,7 +148,7 @@ export default class DataFilter extends Plugin { /** * Allow the given element in the editor context. * - * This method will only allow elements described by the {@link module:content-compatibility/dataschema~DataSchema} used + * This method will only allow elements described by the {@link module:html-support/dataschema~DataSchema} used * to create data filter. * * @param {String|RegExp} viewName String or regular expression matching view name. @@ -239,7 +239,7 @@ export default class DataFilter extends Plugin { } /** - * Registers elements allowed by {@link module:content-compatibility/datafilter~DataFilter#allowElement} method + * Registers elements allowed by {@link module:html-support/datafilter~DataFilter#allowElement} method * once {@link module:core/editor~Editor#data editor's data controller} is initialized. * * @private @@ -300,7 +300,7 @@ export default class DataFilter extends Plugin { * Fires `register` event for the given element definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:html-support/dataschema~DataSchemaDefinition} definition */ _fireRegisterEvent( definition ) { this.fire( definition.view ? `register:${ definition.view }` : 'register', definition ); @@ -310,7 +310,7 @@ export default class DataFilter extends Plugin { * Registers object element and attribute converters for the given data schema definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:html-support/dataschema~DataSchemaDefinition} definition */ _registerObjectElement( definition ) { const editor = this.editor; @@ -361,7 +361,7 @@ export default class DataFilter extends Plugin { * Registers block element and attribute converters for the given data schema definition. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition */ _registerBlockElement( definition ) { const editor = this.editor; @@ -409,7 +409,7 @@ export default class DataFilter extends Plugin { * Extends `$text` model schema to allow the given definition model attribute and its properties. * * @private - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition */ _registerInlineElement( definition ) { const editor = this.editor; @@ -435,10 +435,10 @@ export default class DataFilter extends Plugin { } /** - * Fired when {@link module:content-compatibility/datafilter~DataFilter} is registering element and attribute - * converters for the {@link module:content-compatibility/dataschema~DataSchemaDefinition element definition}. + * Fired when {@link module:html-support/datafilter~DataFilter} is registering element and attribute + * converters for the {@link module:html-support/dataschema~DataSchemaDefinition element definition}. * - * The event also accepts {@link module:content-compatibility/dataschema~DataSchemaDefinition#view} value + * The event also accepts {@link module:html-support/dataschema~DataSchemaDefinition#view} value * as an event namespace, e.g. `register:span`. * * dataFilter.on( 'register', ( evt, definition ) => { @@ -458,7 +458,7 @@ export default class DataFilter extends Plugin { * }, { priority: 'high' } ) * * @event register - * @param {module:content-compatibility/dataschema~DataSchemaDefinition} definition + * @param {module:html-support/dataschema~DataSchemaDefinition} definition */ } diff --git a/packages/ckeditor5-content-compatibility/src/dataschema.js b/packages/ckeditor5-html-support/src/dataschema.js similarity index 84% rename from packages/ckeditor5-content-compatibility/src/dataschema.js rename to packages/ckeditor5-html-support/src/dataschema.js index 2eb74f3cac4..d53ef461a0c 100644 --- a/packages/ckeditor5-content-compatibility/src/dataschema.js +++ b/packages/ckeditor5-html-support/src/dataschema.js @@ -4,7 +4,7 @@ */ /** - * @module content-compatibility/dataschema + * @module html-support/dataschema */ import { Plugin } from 'ckeditor5/src/core'; @@ -17,7 +17,7 @@ import { toArray } from 'ckeditor5/src/utils'; * Data schema is represented by data schema definitions. * * To add new definition for block element, - * use {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method: + * use {@link module:html-support/dataschema~DataSchema#registerBlockElement} method: * * dataSchema.registerBlockElement( { * view: 'section', @@ -28,7 +28,7 @@ import { toArray } from 'ckeditor5/src/utils'; * } ); * * To add new definition for inline element, - * use {@link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method: + * use {@link module:html-support/dataschema~DataSchema#registerInlineElement} method: * * dataSchema.registerInlineElement( { * view: 'span', @@ -49,7 +49,7 @@ export default class DataSchema extends Plugin { * * @readonly * @private - * @member {Map.} #_definitions + * @member {Map.} #_definitions */ this._definitions = new Map(); @@ -66,7 +66,7 @@ export default class DataSchema extends Plugin { /** * Add new data schema definition describing block element. * - * @param {module:content-compatibility/dataschema~DataSchemaBlockElementDefinition} definition + * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition */ registerBlockElement( definition ) { this._definitions.set( definition.model, { ...definition, isBlock: true } ); @@ -75,7 +75,7 @@ export default class DataSchema extends Plugin { /** * Add new data schema definition describing inline element. * - * @param {module:content-compatibility/dataschema~DataSchemaInlineElementDefinition} definition + * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition */ registerInlineElement( definition ) { this._definitions.set( definition.model, { ...definition, isInline: true } ); @@ -86,7 +86,7 @@ export default class DataSchema extends Plugin { * * @param {String|RegExp} viewName * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models. - * @returns {Set.} + * @returns {Set.} */ getDefinitionsForView( viewName, includeReferences ) { const definitions = new Set(); @@ -377,7 +377,7 @@ export default class DataSchema extends Plugin { * * @private * @param {String|RegExp} viewName - * @returns {Array.} + * @returns {Array.} */ _getMatchingViewDefinitions( viewName ) { return Array.from( this._definitions.values() ) @@ -389,7 +389,7 @@ export default class DataSchema extends Plugin { * * @private * @param {String} modelName Data schema model name. - * @returns {Iterable.} + * @returns {Iterable.} */ * _getReferences( modelName ) { const { modelSchema } = this._definitions.get( modelName ); @@ -432,33 +432,33 @@ function testViewName( pattern, viewName ) { } /** - * A base definition of {@link module:content-compatibility/dataschema~DataSchema data schema}. + * A base definition of {@link module:html-support/dataschema~DataSchema data schema}. * - * @typedef {Object} module:content-compatibility/dataschema~DataSchemaDefinition + * @typedef {Object} module:html-support/dataschema~DataSchemaDefinition * @property {String} model Name of the model. * @property {Boolean} [isObject] Indicates that the definition describes object element. * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. */ /** - * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for block elements. + * A definition of {@link module:html-support/dataschema~DataSchema data schema} for block elements. * - * @typedef {Object} module:content-compatibility/dataschema~DataSchemaBlockElementDefinition + * @typedef {Object} module:html-support/dataschema~DataSchemaBlockElementDefinition * @property {String} [view] Name of the view element. * @property {Boolean} isBlock Indicates that the definition describes block element. - * Set by {@link module:content-compatibility/dataschema~DataSchema#registerBlockElement} method. - * @extends module:content-compatibility/dataschema~DataSchemaDefinition + * Set by {@link module:html-support/dataschema~DataSchema#registerBlockElement} method. + * @extends module:html-support/dataschema~DataSchemaDefinition */ /** - * A definition of {@link module:content-compatibility/dataschema~DataSchema data schema} for inline elements. + * A definition of {@link module:html-support/dataschema~DataSchema data schema} for inline elements. * - * @typedef {Object} module:content-compatibility/dataschema~DataSchemaInlineElementDefinition + * @typedef {Object} module:html-support/dataschema~DataSchemaInlineElementDefinition * @property {String} view Name of the view element. * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. * @property {Boolean} isInline Indicates that the definition descibes inline element. * @property {Number} [priority] Element priority. Decides in what order elements are wrapped by * {@link module:engine/view/downcastwriter~DowncastWriter}. - * Set by {@link module:content-compatibility/dataschema~DataSchema#registerInlineElement} method. - * @extends module:content-compatibility/dataschema~DataSchemaDefinition + * Set by {@link module:html-support/dataschema~DataSchema#registerInlineElement} method. + * @extends module:html-support/dataschema~DataSchemaDefinition */ diff --git a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js b/packages/ckeditor5-html-support/src/generalhtmlsupport.js similarity index 85% rename from packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js rename to packages/ckeditor5-html-support/src/generalhtmlsupport.js index 81e615ba2e9..4048a8c2244 100644 --- a/packages/ckeditor5-content-compatibility/src/generalhtmlsupport.js +++ b/packages/ckeditor5-html-support/src/generalhtmlsupport.js @@ -4,7 +4,7 @@ */ /** - * @module content-compatibility/generalhtmlsupport + * @module html-support/generalhtmlsupport */ import { Plugin } from 'ckeditor5/src/core'; @@ -14,7 +14,7 @@ import CodeBlockHtmlSupport from './integrations/codeblock'; /** * The General HTML Support feature. * - * This is a "glue" plugin which initializes the {@link module:content-compatibility/datafilter~DataFilter data filter} configuration + * This is a "glue" plugin which initializes the {@link module:html-support/datafilter~DataFilter data filter} configuration * and features integration with the General HTML Support. * * @extends module:core/plugin~Plugin diff --git a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js b/packages/ckeditor5-html-support/src/integrations/codeblock.js similarity index 96% rename from packages/ckeditor5-content-compatibility/src/integrations/codeblock.js rename to packages/ckeditor5-html-support/src/integrations/codeblock.js index 56e64d98c64..e0cca88cbff 100644 --- a/packages/ckeditor5-content-compatibility/src/integrations/codeblock.js +++ b/packages/ckeditor5-html-support/src/integrations/codeblock.js @@ -4,7 +4,7 @@ */ /** - * @module content-compatibility/integrations/codeblock + * @module html-support/integrations/codeblock */ import { Plugin } from 'ckeditor5/src/core'; @@ -59,7 +59,7 @@ export default class CodeBlockHtmlSupport extends Plugin { // Attributes are preserved as a value of `htmlAttributes` model attribute. // // @private -// @param {module:content-compatibility/datafilter~DataFilter} dataFilter +// @param {module:html-support/datafilter~DataFilter} dataFilter // @returns {Function} Returns a conversion callback. function viewToModelCodeBlockAttributeConverter( dataFilter ) { return dispatcher => { diff --git a/packages/ckeditor5-content-compatibility/tests/_utils/utils.js b/packages/ckeditor5-html-support/tests/_utils/utils.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/_utils/utils.js rename to packages/ckeditor5-html-support/tests/_utils/utils.js diff --git a/packages/ckeditor5-content-compatibility/tests/codeblock.js b/packages/ckeditor5-html-support/tests/codeblock.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/codeblock.js rename to packages/ckeditor5-html-support/tests/codeblock.js diff --git a/packages/ckeditor5-content-compatibility/tests/datafilter.js b/packages/ckeditor5-html-support/tests/datafilter.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/datafilter.js rename to packages/ckeditor5-html-support/tests/datafilter.js diff --git a/packages/ckeditor5-content-compatibility/tests/dataschema.js b/packages/ckeditor5-html-support/tests/dataschema.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/dataschema.js rename to packages/ckeditor5-html-support/tests/dataschema.js diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.html b/packages/ckeditor5-html-support/tests/manual/codeblock.html similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/codeblock.html rename to packages/ckeditor5-html-support/tests/manual/codeblock.html diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.js b/packages/ckeditor5-html-support/tests/manual/codeblock.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/codeblock.js rename to packages/ckeditor5-html-support/tests/manual/codeblock.js diff --git a/packages/ckeditor5-content-compatibility/tests/manual/codeblock.md b/packages/ckeditor5-html-support/tests/manual/codeblock.md similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/codeblock.md rename to packages/ckeditor5-html-support/tests/manual/codeblock.md diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html b/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.html similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.html rename to packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.html diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.js rename to packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.js diff --git a/packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md b/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.md similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/generalhtmlsupport.md rename to packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.md diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.html b/packages/ckeditor5-html-support/tests/manual/objects.html similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/objects.html rename to packages/ckeditor5-html-support/tests/manual/objects.html diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.js b/packages/ckeditor5-html-support/tests/manual/objects.js similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/objects.js rename to packages/ckeditor5-html-support/tests/manual/objects.js diff --git a/packages/ckeditor5-content-compatibility/tests/manual/objects.md b/packages/ckeditor5-html-support/tests/manual/objects.md similarity index 100% rename from packages/ckeditor5-content-compatibility/tests/manual/objects.md rename to packages/ckeditor5-html-support/tests/manual/objects.md diff --git a/packages/ckeditor5-content-compatibility/theme/datafilter.css b/packages/ckeditor5-html-support/theme/datafilter.css similarity index 100% rename from packages/ckeditor5-content-compatibility/theme/datafilter.css rename to packages/ckeditor5-html-support/theme/datafilter.css From c0b07ba4304d2633925b96d49a652d8425b431dc Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 11 Jun 2021 08:55:11 +0200 Subject: [PATCH 174/217] Renamed config. --- packages/ckeditor5-html-support/src/generalhtmlsupport.js | 4 ++-- .../tests/manual/generalhtmlsupport.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-html-support/src/generalhtmlsupport.js b/packages/ckeditor5-html-support/src/generalhtmlsupport.js index 4048a8c2244..9c7aa3caf32 100644 --- a/packages/ckeditor5-html-support/src/generalhtmlsupport.js +++ b/packages/ckeditor5-html-support/src/generalhtmlsupport.js @@ -32,8 +32,8 @@ export default class GeneralHtmlSupport extends Plugin { const dataFilter = editor.plugins.get( DataFilter ); // Load the filtering configuration. - dataFilter.loadAllowedConfig( editor.config.get( 'generalHtmlSupport.allowed' ) || [] ); - dataFilter.loadDisallowedConfig( editor.config.get( 'generalHtmlSupport.disallowed' ) || [] ); + dataFilter.loadAllowedConfig( editor.config.get( 'htmlSupport.allow' ) || [] ); + dataFilter.loadDisallowedConfig( editor.config.get( 'htmlSupport.disallow' ) || [] ); } /** diff --git a/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.js b/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.js index 5863fa3bb29..81a1f326591 100644 --- a/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.js +++ b/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.js @@ -67,8 +67,8 @@ ClassicEditor '|', 'blockquote' ], - generalHtmlSupport: { - allowed: [ + htmlSupport: { + allow: [ { name: 'article' }, { name: /^(details|summary)$/ }, { name: /^(dl|dd|dt)$/ }, @@ -109,7 +109,7 @@ ClassicEditor styles: { 'background-color': true } } ], - disallowed: [ + disallow: [ { name: 'section', attributes: { id: /^_.*/ } }, { name: /^(span|cite)$/, styles: { color: 'red' } } ] From 8cf0364e65fa6115ffb7e08655f06a28c9d74985 Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Fri, 11 Jun 2021 09:06:42 +0200 Subject: [PATCH 175/217] Added missing renaming. --- packages/ckeditor5-html-support/package.json | 2 +- packages/ckeditor5-html-support/src/dataschema.js | 2 +- .../ckeditor5-html-support/tests/manual/generalhtmlsupport.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-html-support/package.json b/packages/ckeditor5-html-support/package.json index 4b292806e3c..d0b6e7d8655 100644 --- a/packages/ckeditor5-html-support/package.json +++ b/packages/ckeditor5-html-support/package.json @@ -1,7 +1,7 @@ { "name": "@ckeditor/ckeditor5-html-support", "version": "28.0.0", - "description": "Content compatibility feature", + "description": "HTML Support feature", "private": true, "keywords": [ "ckeditor", diff --git a/packages/ckeditor5-html-support/src/dataschema.js b/packages/ckeditor5-html-support/src/dataschema.js index d53ef461a0c..c245fc3a7d5 100644 --- a/packages/ckeditor5-html-support/src/dataschema.js +++ b/packages/ckeditor5-html-support/src/dataschema.js @@ -12,7 +12,7 @@ import { toArray } from 'ckeditor5/src/utils'; /** * Holds representation of the extended HTML document type definitions to be used by the - * editor in content compatibility support. + * editor in HTML support. * * Data schema is represented by data schema definitions. * diff --git a/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.md b/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.md index 4f09838d7e4..c357cf7f28a 100644 --- a/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.md +++ b/packages/ckeditor5-html-support/tests/manual/generalhtmlsupport.md @@ -1 +1 @@ -# Content compatibility +# General HTML Support From 4191bf4c8a6ba7d9542a987c1cfd4e9cefe8adac Mon Sep 17 00:00:00 2001 From: Jacek Bogdanski Date: Tue, 15 Jun 2021 13:53:30 +0200 Subject: [PATCH 176/217] Updated docs, package.json. --- packages/ckeditor5-html-support/package.json | 23 +++++-- .../ckeditor5-html-support/src/converters.js | 4 -- .../ckeditor5-html-support/src/datafilter.js | 5 +- .../ckeditor5-html-support/src/dataschema.js | 5 +- .../src/generalhtmlsupport.js | 65 +++++++++++++++++++ packages/ckeditor5-html-support/src/index.js | 12 ++++ 6 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 packages/ckeditor5-html-support/src/index.js diff --git a/packages/ckeditor5-html-support/package.json b/packages/ckeditor5-html-support/package.json index d0b6e7d8655..2835d584301 100644 --- a/packages/ckeditor5-html-support/package.json +++ b/packages/ckeditor5-html-support/package.json @@ -1,8 +1,7 @@ { "name": "@ckeditor/ckeditor5-html-support", "version": "28.0.0", - "description": "HTML Support feature", - "private": true, + "description": "HTML Support feature for CKEditor 5.", "keywords": [ "ckeditor", "ckeditor5", @@ -11,6 +10,7 @@ "ckeditor5-plugin", "compatibility", "content compatibility", + "html support", "generic html support" ], "main": "src/index.js", @@ -40,11 +40,22 @@ "node": ">=12.0.0", "npm": ">=5.7.1" }, - "license": "SEE LICENSE IN LICENSE.md", "author": "CKSource (http://cksource.com/)", - "homepage": "https://ckeditor.com", + "license": "GPL-2.0-or-later", + "homepage": "https://ckeditor.com/ckeditor-5", "bugs": "https://github.com/ckeditor/ckeditor5/issues", + "repository": { + "type": "git", + "url": "https://github.com/ckeditor/ckeditor5.git", + "directory": "packages/ckeditor5-language" + }, "files": [ - "src" - ] + "lang", + "src", + "theme", + "build" + ], + "scripts": { + "dll:build": "webpack" + } } diff --git a/packages/ckeditor5-html-support/src/converters.js b/packages/ckeditor5-html-support/src/converters.js index 6f2662031f9..ee1c87afd53 100644 --- a/packages/ckeditor5-html-support/src/converters.js +++ b/packages/ckeditor5-html-support/src/converters.js @@ -93,10 +93,6 @@ export function createObjectView( viewName, modelElement, writer ) { /** * View-to-attribute conversion helper preserving inline element attributes on `$text`. * - * All allowed element attributes will be preserved as a value of - * {@link module:html-support/dataschema~DataSchemaInlineElementDefinition~model definition model} - * attribute. - * * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition * @param {module:html-support/datafilter~DataFilter} dataFilter * @returns {Function} Returns a conversion callback. diff --git a/packages/ckeditor5-html-support/src/datafilter.js b/packages/ckeditor5-html-support/src/datafilter.js index f5856727cb8..d1e5bf2ccd2 100644 --- a/packages/ckeditor5-html-support/src/datafilter.js +++ b/packages/ckeditor5-html-support/src/datafilter.js @@ -100,7 +100,8 @@ export default class DataFilter extends Plugin { this._allowedElements = new Set(); /** - * Indicates if {@link module:core/editor~Editor#data editor's data controller} data has been already initialized. + * Indicates if {@link module:engine/controller/datacontroller~DataController editor's data controller} + * data has been already initialized. * * @private * @member {Boolean} [#_dataInitialized=false] @@ -240,7 +241,7 @@ export default class DataFilter extends Plugin { /** * Registers elements allowed by {@link module:html-support/datafilter~DataFilter#allowElement} method - * once {@link module:core/editor~Editor#data editor's data controller} is initialized. + * once {@link module:engine/controller/datacontroller~DataController editor's data controller} is initialized. * * @private */ diff --git a/packages/ckeditor5-html-support/src/dataschema.js b/packages/ckeditor5-html-support/src/dataschema.js index c245fc3a7d5..17db7ac2edb 100644 --- a/packages/ckeditor5-html-support/src/dataschema.js +++ b/packages/ckeditor5-html-support/src/dataschema.js @@ -45,7 +45,7 @@ export default class DataSchema extends Plugin { super( editor ); /** - * A map of registered data schema definitions via {@link #register} method. + * A map of registered data schema definitions. * * @readonly * @private @@ -436,6 +436,7 @@ function testViewName( pattern, viewName ) { * * @typedef {Object} module:html-support/dataschema~DataSchemaDefinition * @property {String} model Name of the model. + * @property {String} [view] Name of the view element. * @property {Boolean} [isObject] Indicates that the definition describes object element. * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model. */ @@ -444,7 +445,6 @@ function testViewName( pattern, viewName ) { * A definition of {@link module:html-support/dataschema~DataSchema data schema} for block elements. * * @typedef {Object} module:html-support/dataschema~DataSchemaBlockElementDefinition - * @property {String} [view] Name of the view element. * @property {Boolean} isBlock Indicates that the definition describes block element. * Set by {@link module:html-support/dataschema~DataSchema#registerBlockElement} method. * @extends module:html-support/dataschema~DataSchemaDefinition @@ -454,7 +454,6 @@ function testViewName( pattern, viewName ) { * A definition of {@link module:html-support/dataschema~DataSchema data schema} for inline elements. * * @typedef {Object} module:html-support/dataschema~DataSchemaInlineElementDefinition - * @property {String} view Name of the view element. * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute. * @property {Boolean} isInline Indicates that the definition descibes inline element. * @property {Number} [priority] Element priority. Decides in what order elements are wrapped by diff --git a/packages/ckeditor5-html-support/src/generalhtmlsupport.js b/packages/ckeditor5-html-support/src/generalhtmlsupport.js index 9c7aa3caf32..e8e46b7fb50 100644 --- a/packages/ckeditor5-html-support/src/generalhtmlsupport.js +++ b/packages/ckeditor5-html-support/src/generalhtmlsupport.js @@ -46,3 +46,68 @@ export default class GeneralHtmlSupport extends Plugin { ]; } } + +/** + * The configuration of the General HTML Support feature. + * Introduced by the {@link module:html-support/generalhtmlsupport~GeneralHtmlSupport} feature. + * + * Read more in {@link module:html-support/generalhtmlsupport~GeneralHtmlSupportConfig}. + * + * @member {module:htmlsupport/generalhtmlsupport~GeneralHtmlSupportConfig} module:core/editor/editorconfig~EditorConfig#htmlSupport + */ + +/** + * The configuration of the General HTML Support feature. + * The option is used by the {@link module:html-support/generalhtmlsupport~GeneralHtmlSupport} feature. + * + * ClassicEditor + * .create( { + * htmlSupport: ... // General HTML Support feature config. + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * + * @interface GeneralHtmlSupportConfig + */ + +/** + * The configuration of allowed content rules used by General HTML Support. + * + * Setting this configuration option will enable HTML features that are not explicitly supported by any other dedicated CKEditor 5 features. + * + * const htmlSupportConfig.allow = [ + * { + * name: 'div', // Enable 'div' element support, + * classes: [ 'special-container' ], // allow 'special-container' class, + * styles: 'background', // allow 'background' style, + * attributes: true // allow any attribute (can be empty). + * }, + * { + * name: 'p', // Extend existing Paragraph feature, + * classes: 'highlighted' // with 'highlighted' class, + * attributes: [ + * { key: 'data-i18n-context, value: true } // and i18n attribute. + * ] + * } + * ]; + * + * @member {Array.} module:html-support/generalhtmlsupport~GeneralHtmlSupportConfig#allow + */ + +/** + * The configuration of disallowed content rules used by General HTML Support. + * + * Setting this configuration option will disable listed HTML features. + * + * const htmlSupportConfig.disallow = [ + * { + * name: /[\s\S]+/ // For every HTML feature, + * attributes: { + * key: /^on.*$/ // disable 'on*' attributes, like 'onClick', 'onError' etc. + * } + * } + * ]; + * @member {Array.} module:html-support/generalhtmlsupport~GeneralHtmlSupportConfig#disallow + */ diff --git a/packages/ckeditor5-html-support/src/index.js b/packages/ckeditor5-html-support/src/index.js new file mode 100644 index 00000000000..0b58680969b --- /dev/null +++ b/packages/ckeditor5-html-support/src/index.js @@ -0,0 +1,12 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module html-support + */ + +export { default as GeneralHtmlSupport } from './generalhtmlsupport'; +export { default as DataFilter } from './datafilter'; +export { default as DataSchema } from './dataschema'; From b44e02d6dcf75aba5f8c88467c27112d164e02d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Smyrek?= Date: Wed, 16 Jun 2021 08:00:44 +0200 Subject: [PATCH 177/217] Added missing `@ckeditor/ckeditor5-html-support` dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 03381929b2b..f3430ebf199 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@ckeditor/ckeditor5-highlight": "^28.0.0", "@ckeditor/ckeditor5-horizontal-line": "^28.0.0", "@ckeditor/ckeditor5-html-embed": "^28.0.0", + "@ckeditor/ckeditor5-html-support": "^28.0.0", "@ckeditor/ckeditor5-image": "^28.0.0", "@ckeditor/ckeditor5-indent": "^28.0.0", "@ckeditor/ckeditor5-language": "^28.0.0", From ac66883e688378f00b9b4ec9804e5b7252ebf7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 14 Jun 2021 09:23:01 +0200 Subject: [PATCH 178/217] First draft of the docs. --- .../features/general-html-support.html | 22 ++ .../features/general-html-support.js | 115 ++++++++++ docs/_snippets/features/mathtype.js | 2 + docs/features/general-html-support.md | 215 ++++++++++++++++++ .../docs/features/source-editing.md | 3 +- 5 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 docs/_snippets/features/general-html-support.html create mode 100644 docs/_snippets/features/general-html-support.js create mode 100644 docs/features/general-html-support.md diff --git a/docs/_snippets/features/general-html-support.html b/docs/_snippets/features/general-html-support.html new file mode 100644 index 00000000000..3648628f0d3 --- /dev/null +++ b/docs/_snippets/features/general-html-support.html @@ -0,0 +1,22 @@ +
      +

      General HTML support

      + +
      +

      This is an experimental feature.

      + +
      + Read more + +

      The General HTML support feature is experimental and not yet production-ready.

      +

      Follow the "Stabilize and release a production-ready General HTML support feature" issue for more updates.

      +
      +
      + +

      The GHS feature is enabled in this editor and configured to support the following content (in addition to content already support by other editor features):

      + +
        +
      • <div>, <details>, and <summary> elements with all kind of attributes,
      • +
      • paragraphs and headings with data-* attributes and all classes,
      • +
      • <span> elements with all inline styles and <abbr> elements with `title` attributes.
      • +
      +
      diff --git a/docs/_snippets/features/general-html-support.js b/docs/_snippets/features/general-html-support.js new file mode 100644 index 00000000000..2456c18133c --- /dev/null +++ b/docs/_snippets/features/general-html-support.js @@ -0,0 +1,115 @@ +/** + * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config.js'; + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import Code from '@ckeditor/ckeditor5-basic-styles/src/code'; +import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage'; +import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload'; +import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices'; + +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; +import GeneralHtmlSupport from '@ckeditor/ckeditor5-content-compatibility/src/generalhtmlsupport'; + +ClassicEditor + .create( document.querySelector( '#snippet-general-html-support' ), { + plugins: [ + ArticlePluginSet, + Code, + EasyImage, + ImageUpload, + CloudServices, + SourceEditing, + GeneralHtmlSupport + ], + toolbar: { + items: [ + 'sourceEditing', + '|', + 'heading', + '|', + 'bold', + 'italic', + 'code', + 'bulletedList', + 'numberedList', + '|', + 'outdent', + 'indent', + '|', + 'blockQuote', + 'link', + 'mediaEmbed', + 'insertTable', + '|', + 'undo', + 'redo' + ], + viewportTopOffset: window.getViewportTopOffsetConfig() + }, + image: { + toolbar: [ + 'imageStyle:inline', + 'imageStyle:wrapText', + 'imageStyle:breakText', + '|', + 'toggleImageCaption', + 'imageTextAlternative' + ] + }, + table: { + contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] + }, + cloudServices: CS_CONFIG, + + generalHtmlSupport: { + allowed: [ + // Enables
      ,
      , and elements with all kind of attributes. + { + name: /^(div|details|summary)$/, + styles: true, + classes: true, + attributes: true + }, + + // Extends the existing Paragraph and Heading features + // with classes and data-* attributes. + { + name: /^(p|h[2-4])$/, + classes: true, + attributes: /^data-/ + }, + + // Enables s with any inline styles. + { + name: 'span', + styles: true + }, + + // Enables s with the title attribute. + { + name: 'abbr', + attributes: [ 'title' ] + } + ] + } + } ) + .then( editor => { + window.editor = editor; + + window.attachTourBalloon( { + target: window.findToolbarItem( editor.ui.view.toolbar, + item => item.label && item.label === 'Source' ), + text: 'Switch to the source mode to check out the source of the content and play with it.', + editor + } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/mathtype.js b/docs/_snippets/features/mathtype.js index 8a8c51bb512..8e8aaed31b3 100644 --- a/docs/_snippets/features/mathtype.js +++ b/docs/_snippets/features/mathtype.js @@ -64,6 +64,8 @@ ClassicEditor } ) .then( editor => { + window.editor = editor; + window.attachTourBalloon( { target: window.findToolbarItem( editor.ui.view.toolbar, item => item.label && item.label === 'Insert a math equation - MathType' ), diff --git a/docs/features/general-html-support.md b/docs/features/general-html-support.md new file mode 100644 index 00000000000..c665a04ea89 --- /dev/null +++ b/docs/features/general-html-support.md @@ -0,0 +1,215 @@ +--- +category: features +modified_at: 2021-06-13 +--- + +# General HTML support + +The General HTML support ("GHS") feature allows you to easily enable HTML features that are not supported by any other "specific" CKEditor 5 features. + +Some examples of HTML features that can be easily enabled thanks to GHS: + +* `
      `, `
      `, and `
      ` elements, +* `