From 30a6fcfc43a904ba21e85d2c9de96ecb56aa1acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 5 Feb 2013 12:51:50 +0100 Subject: [PATCH 01/10] Transform content based on feature.contentTransformations. --- core/filter.js | 107 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 10 deletions(-) diff --git a/core/filter.js b/core/filter.js index be89dd0ee1e..638764a91d8 100644 --- a/core/filter.js +++ b/core/filter.js @@ -49,8 +49,10 @@ this.editor = null; this._ = { - // Optimized rules. + // Optimized allowed content rules. rules: {}, + // Object: element name => array of functions. + transformations: {}, cachedTests: {} }; @@ -254,6 +256,8 @@ if ( feature.toFeature ) feature = feature.toFeature( this.editor ); + this.addTransformations( feature.contentTransformations ); + // If default configuration (will be checked inside #allow()), // then add allowed content rules. this.allow( feature.allowedContent ); @@ -264,6 +268,23 @@ return true; }, + addTransformations: function( transformations ) { + if ( !transformations ) + return; + + var optimized = this._.transformations, + name, fn; + + for ( name in transformations ) { + fn = transformations[ name ]; + + if ( !optimized[ name ] ) + optimized[ name ] = []; + + optimized[ name ].push( fn ); + } + }, + /** * Checks whether content defined in test argument is allowed * by this filter. @@ -293,7 +314,8 @@ toBeRemoved = []; // Filter clone of mocked element. - getFilterFunction( this, toBeRemoved )( clone ); + // Do not run transformations. + getFilterFunction( this, toBeRemoved )( clone, true ); // Element has been marked for removal. if ( toBeRemoved.length > 0 ) @@ -422,12 +444,25 @@ unprotectElementsNamesRegexp = /^cke:(object|embed|param|html|body|head|title)$/; // Return and cache created function. - return privObj.filterFunction = function( element ) { + return privObj.filterFunction = function( element, withoutTransformations ) { var name = element.name; // Unprotect elements names previously protected by htmlDataProcessor // (see protectElementNames and protectSelfClosingElements functions). name = name.replace( unprotectElementsNamesRegexp, '$1' ); + var transformations = privObj.transformations[ name ], + i, l; + + if ( !withoutTransformations && transformations ) { + populateProperties( element ); + for ( i = 0; i < transformations.length; ++i ) { + transformations[ i ]( element, transformationsTools ); + } + } + + // Name could be changed by transformations. + name = element.name; + var rules = optimizedRules.elements[ name ], genericRules = optimizedRules.generic, status = { @@ -444,8 +479,7 @@ allAttributes: false, allClasses: false, allStyles: false - }, - i, l; + }; // Early return - if there are no rules for this element (specific or generic), remove it. if ( !rules && !genericRules ) { @@ -453,11 +487,9 @@ return; } - // Parse classes and styles if that hasn't been done by filter#check yet. - if ( !element.styles ) - element.styles = CKEDITOR.tools.parseCssText( element.attributes.style || '', 1 ); - if ( !element.classes ) - element.classes = element.attributes[ 'class' ] ? element.attributes[ 'class' ].split( /\s+/ ) : []; + // Could not be done yet if there were no transformations and if this + // is real (not mocked) object. + populateProperties( element ); if ( rules ) { for ( i = 0, l = rules.length; i < l; ++i ) @@ -651,6 +683,14 @@ return group ? trim( group[ 1 ] ) : null; } + function populateProperties( element ) { + // Parse classes and styles if that hasn't been done before. + if ( !element.styles ) + element.styles = CKEDITOR.tools.parseCssText( element.attributes.style || '', 1 ); + if ( !element.classes ) + element.classes = element.attributes[ 'class' ] ? element.attributes[ 'class' ].split( /\s+/ ) : []; + } + // Update element object based on status of filtering. function updateElement( element, status ) { var validAttrs = status.validAttributes, @@ -836,4 +876,51 @@ element.remove(); } + // + // TRANSFORMATION TOOLS --------------------------------------------------- + // + + var transformationsTools = { + sizeToStyle: function( element ) { + this.lengthToStyle( element, 'width' ); + this.lengthToStyle( element, 'height' ); + }, + + sizeToAttribute: function( element ) { + this.lengthToAttribute( element, 'width' ); + this.lengthToAttribute( element, 'height' ); + }, + + lengthToStyle: function( element, attrName, styleName ) { + styleName = styleName || attrName; + + if ( !( styleName in element.styles ) ) { + var value = element.attributes[ attrName ]; + + if ( value ) { + if ( ( /^\d+$/ ).test( value ) ) + value += 'px'; + + element.styles[ styleName ] = value; + } + } + + delete element.attributes[ attrName ]; + }, + + lengthToAttribute: function( element, styleName, attrName ) { + attrName = attrName || styleName; + + if ( !( attrName in element.attributes ) ) { + var value = element.styles[ styleName ], + match = value && value.match( /^(\d+)(?:\.\d*)?px$/ ); + + if ( match ) + element.attributes[ attrName ] = match[ 1 ]; + } + + delete element.styles[ styleName ]; + } + }; + })(); \ No newline at end of file From 561fa35d5ab22ff7cb80ef666a5e00f46ce3b20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 5 Feb 2013 12:56:47 +0100 Subject: [PATCH 02/10] Add basic content transformations for image and table plugins. --- plugins/image/plugin.js | 10 +++++++++- plugins/table/plugin.js | 10 +++++++++- samples/filter.html | 4 ++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/plugins/image/plugin.js b/plugins/image/plugin.js index 3fcf72e2fa2..484b641ff1e 100644 --- a/plugins/image/plugin.js +++ b/plugins/image/plugin.js @@ -22,7 +22,15 @@ // Register the command. editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, { allowedContent: 'img[align,alt,dir,id,lang,longdesc,src,title]{*}(*)', - requiredContent: 'img[alt,src]' + requiredContent: 'img[alt,src]', + contentTransformations: { + img: function( el, t ) { + if ( editor.filter.check( 'img{width}' ) ) + t.sizeToStyle( el ); + else if ( editor.filter.check( 'img[width]' ) ) + t.sizeToAttribute( el ); + } + } } ) ); // Register the toolbar button. diff --git a/plugins/table/plugin.js b/plugins/table/plugin.js index 75bdae4dec8..081ece99c4c 100755 --- a/plugins/table/plugin.js +++ b/plugins/table/plugin.js @@ -20,7 +20,15 @@ CKEDITOR.plugins.add( 'table', { 'caption tbody thead tfoot;' + 'th td tr[scope];' + ( editor.plugins.dialogadvtab ? 'table' + editor.plugins.dialogadvtab.allowedContent() : '' ), - requiredContent: 'table' // TODO We should also check td and tr. + requiredContent: 'table', // TODO We should also check td and tr. + contentTransformations: { + table: function( el, t ) { + if ( editor.filter.check( 'table{width}' ) ) + t.sizeToStyle( el ); + else if ( editor.filter.check( 'table[width]' ) ) + t.sizeToAttribute( el ); + } + } } ) ); function createDef( def ) { diff --git a/samples/filter.html b/samples/filter.html index 62ec9115bf9..35cbf2e3d32 100644 --- a/samples/filter.html +++ b/samples/filter.html @@ -63,7 +63,7 @@

allowedContent: 'h1 h2 h3 p blockquote strong em;' + 'a[href];' + - 'img(left,right)[src,alt]{width,height};' + + 'img(left,right)[src,alt,width,height];' + 'table tr th td caption;' + 'span{font-family,color}' } ); @@ -78,7 +78,7 @@

allowedContent: 'h1 h2 h3 p blockquote strong em;' + 'a[href];' + - 'img(left,right)[src,alt]{width,height};' + + 'img(left,right)[src,alt,width,height];' + 'table tr th td caption;' + 'span{font-family,color}' } ); From 437f4f4e385412261b8064d081e41d945415fe6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 5 Feb 2013 13:48:58 +0100 Subject: [PATCH 03/10] Simplified contentTransformations. --- core/filter.js | 38 ++++++++++++++++++++++++++++---------- plugins/image/plugin.js | 8 ++------ plugins/table/plugin.js | 8 ++------ 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/core/filter.js b/core/filter.js index 638764a91d8..5e1b3236980 100644 --- a/core/filter.js +++ b/core/filter.js @@ -273,15 +273,24 @@ return; var optimized = this._.transformations, - name, fn; + name, fn, rule; - for ( name in transformations ) { - fn = transformations[ name ]; + for ( rule in transformations ) { + fn = transformations[ rule ]; + + // Extract element name. + name = rule.match( /^([a-z0-9]+)/g )[ 0 ]; if ( !optimized[ name ] ) optimized[ name ] = []; - optimized[ name ].push( fn ); + optimized[ name ].push( { + // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it. + rule: name != rule ? rule : null, + + // Handle shorthand format. E.g.: { 'table[width]': 'sizeToAttribute' }. + fn: typeof fn == 'string' ? getTransformationFn( fn ) : fn + } ); } }, @@ -451,12 +460,14 @@ name = name.replace( unprotectElementsNamesRegexp, '$1' ); var transformations = privObj.transformations[ name ], - i, l; + i, l, trans; if ( !withoutTransformations && transformations ) { populateProperties( element ); for ( i = 0; i < transformations.length; ++i ) { - transformations[ i ]( element, transformationsTools ); + trans = transformations[ i ]; + if ( !trans.rule || that.check( trans.rule ) ) + trans.fn( element, transformationsTools ); } } @@ -512,6 +523,12 @@ }; } + function getTransformationFn( toolName ) { + return function( el, tools ) { + tools[ toolName ]( el ); + }; + } + // Create pseudo element that will be passed through filter // to check if tested string is allowed. function mockElementFromString( str ) { @@ -638,17 +655,18 @@ optimizedRules.generic = genericRules.length ? genericRules : null; } + // < elements >< styles, attributes and classes >< separator > + var rulePattern = /^([a-z0-9*\s]+)((?:\s*{[\w\-,\s\*]+}\s*|\s*\[[\w\-,\s\*]+\]\s*|\s*\([\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i; + function parseRulesString( input ) { - // < elements >< styles, attributes and classes >< separator > - var groupPattern = /^([a-z0-9*\s]+)((?:\s*{[\w\-,\s\*]+}\s*|\s*\[[\w\-,\s\*]+\]\s*|\s*\([\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i, - match, + var match, props, styles, attrs, classes, rules = {}, groupNum = 1; input = trim( input ); - while ( ( match = input.match( groupPattern ) ) ) { + while ( ( match = input.match( rulePattern ) ) ) { if ( ( props = match[ 2 ] ) ) { styles = parseProperties( props, 'styles' ); attrs = parseProperties( props, 'attrs' ); diff --git a/plugins/image/plugin.js b/plugins/image/plugin.js index 484b641ff1e..49f6dd47ef1 100644 --- a/plugins/image/plugin.js +++ b/plugins/image/plugin.js @@ -24,12 +24,8 @@ allowedContent: 'img[align,alt,dir,id,lang,longdesc,src,title]{*}(*)', requiredContent: 'img[alt,src]', contentTransformations: { - img: function( el, t ) { - if ( editor.filter.check( 'img{width}' ) ) - t.sizeToStyle( el ); - else if ( editor.filter.check( 'img[width]' ) ) - t.sizeToAttribute( el ); - } + 'img{width}': 'sizeToStyle', + 'img[width]': 'sizeToAttribute' } } ) ); diff --git a/plugins/table/plugin.js b/plugins/table/plugin.js index 081ece99c4c..86e504c717b 100755 --- a/plugins/table/plugin.js +++ b/plugins/table/plugin.js @@ -22,12 +22,8 @@ CKEDITOR.plugins.add( 'table', { ( editor.plugins.dialogadvtab ? 'table' + editor.plugins.dialogadvtab.allowedContent() : '' ), requiredContent: 'table', // TODO We should also check td and tr. contentTransformations: { - table: function( el, t ) { - if ( editor.filter.check( 'table{width}' ) ) - t.sizeToStyle( el ); - else if ( editor.filter.check( 'table[width]' ) ) - t.sizeToAttribute( el ); - } + 'table{width}': 'sizeToStyle', + 'table[width]': 'sizeToAttribute' } } ) ); From 018e7748b935f70e264794896998e0c7f81a7d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 5 Feb 2013 14:19:34 +0100 Subject: [PATCH 04/10] Prevent conflicts where running filter fn twice at the same time. --- core/filter.js | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/core/filter.js b/core/filter.js index 5e1b3236980..8e456ff4b7d 100644 --- a/core/filter.js +++ b/core/filter.js @@ -174,10 +174,14 @@ */ applyTo: function( fragment ) { var toBeRemoved = [], - filterFn = getFilterFunction( this, toBeRemoved ); + rules = this._.rules, + transformations = this._.transformations, + filterFn = getFilterFunction( this ); // Filter all children, skip root (fragment or editable-like wrapper used by data processor). - fragment.forEach( filterFn, CKEDITOR.NODE_ELEMENT, true ); + fragment.forEach( function( el ) { + filterFn( el, rules, transformations, toBeRemoved ); + }, CKEDITOR.NODE_ELEMENT, true ); var element, toBeChecked = []; @@ -324,7 +328,7 @@ // Filter clone of mocked element. // Do not run transformations. - getFilterFunction( this, toBeRemoved )( clone, true ); + getFilterFunction( this )( clone, this._.rules, false, toBeRemoved ); // Element has been marked for removal. if ( toBeRemoved.length > 0 ) @@ -439,30 +443,23 @@ // {@link #allow} method. // // @param {CKEDITOR.filter} that - function getFilterFunction( that, toBeRemoved ) { - // If filter function is cached we'll return function from different scope - // than this, so we need to pass toBeRemoved array by reference. - var privObj = that._; - privObj.toBeRemoved = toBeRemoved; - + function getFilterFunction( that ) { // Return cached function. - if ( privObj.filterFunction ) - return privObj.filterFunction; + if ( that._.filterFunction ) + return that._.filterFunction; - var optimizedRules = privObj.rules, - unprotectElementsNamesRegexp = /^cke:(object|embed|param|html|body|head|title)$/; + var unprotectElementsNamesRegexp = /^cke:(object|embed|param|html|body|head|title)$/; // Return and cache created function. - return privObj.filterFunction = function( element, withoutTransformations ) { - var name = element.name; + return that._.filterFunction = function( element, optimizedRules, transformations, toBeRemoved ) { + var name = element.name, + i, l, trans; + // Unprotect elements names previously protected by htmlDataProcessor // (see protectElementNames and protectSelfClosingElements functions). name = name.replace( unprotectElementsNamesRegexp, '$1' ); - var transformations = privObj.transformations[ name ], - i, l, trans; - - if ( !withoutTransformations && transformations ) { + if ( ( transformations = transformations && transformations[ name ] ) ) { populateProperties( element ); for ( i = 0; i < transformations.length; ++i ) { trans = transformations[ i ]; @@ -494,7 +491,7 @@ // Early return - if there are no rules for this element (specific or generic), remove it. if ( !rules && !genericRules ) { - privObj.toBeRemoved.push( element ); + toBeRemoved.push( element ); return; } @@ -514,7 +511,7 @@ // Finally, if after running all filter rules it still hasn't been allowed - remove it. if ( !status.valid ) { - privObj.toBeRemoved.push( element ); + toBeRemoved.push( element ); return; } From b77a73d61217eb4d8d6dd672e1ded428d215850b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Tue, 5 Feb 2013 16:04:37 +0100 Subject: [PATCH 05/10] Made transformations more accurate - only first one in a group which is matching ACRs will be executed. --- core/filter.js | 100 ++++++++++++++++++++++++++-------------- plugins/image/plugin.js | 7 ++- plugins/table/plugin.js | 7 ++- 3 files changed, 71 insertions(+), 43 deletions(-) diff --git a/core/filter.js b/core/filter.js index 8e456ff4b7d..d66ac9d8c31 100644 --- a/core/filter.js +++ b/core/filter.js @@ -51,7 +51,7 @@ this._ = { // Optimized allowed content rules. rules: {}, - // Object: element name => array of functions. + // Object: element name => array of transformations groups. transformations: {}, cachedTests: {} }; @@ -277,24 +277,15 @@ return; var optimized = this._.transformations, - name, fn, rule; + group, i; - for ( rule in transformations ) { - fn = transformations[ rule ]; + for ( i = 0; i < transformations.length; ++i ) { + group = optimizeTransformations( transformations[ i ] ); - // Extract element name. - name = rule.match( /^([a-z0-9]+)/g )[ 0 ]; + if ( !optimized[ group.name ] ) + optimized[ group.name ] = []; - if ( !optimized[ name ] ) - optimized[ name ] = []; - - optimized[ name ].push( { - // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it. - rule: name != rule ? rule : null, - - // Handle shorthand format. E.g.: { 'table[width]': 'sizeToAttribute' }. - fn: typeof fn == 'string' ? getTransformationFn( fn ) : fn - } ); + optimized[ group.name ].push( group.rules ); } }, @@ -461,11 +452,9 @@ if ( ( transformations = transformations && transformations[ name ] ) ) { populateProperties( element ); - for ( i = 0; i < transformations.length; ++i ) { - trans = transformations[ i ]; - if ( !trans.rule || that.check( trans.rule ) ) - trans.fn( element, transformationsTools ); - } + + for ( i = 0; i < transformations.length; ++i ) + applyTransformationsGroup( that, element, transformations[ i ] ); } // Name could be changed by transformations. @@ -520,12 +509,6 @@ }; } - function getTransformationFn( toolName ) { - return function( el, tools ) { - tools[ toolName ]( el ); - }; - } - // Create pseudo element that will be passed through filter // to check if tested string is allowed. function mockElementFromString( str ) { @@ -653,7 +636,12 @@ } // < elements >< styles, attributes and classes >< separator > - var rulePattern = /^([a-z0-9*\s]+)((?:\s*{[\w\-,\s\*]+}\s*|\s*\[[\w\-,\s\*]+\]\s*|\s*\([\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i; + var rulePattern = /^([a-z0-9*\s]+)((?:\s*{[\w\-,\s\*]+}\s*|\s*\[[\w\-,\s\*]+\]\s*|\s*\([\w\-,\s\*]+\)\s*){0,3})(?:;\s*|$)/i, + groupsPatterns = { + styles: /{([^}]+)}/, + attrs: /\[([^\]]+)\]/, + classes: /\(([^\)]+)\)/ + }; function parseRulesString( input ) { var match, @@ -687,12 +675,6 @@ return rules; } - var groupsPatterns = { - styles: /{([^}]+)}/, - attrs: /\[([^\]]+)\]/, - classes: /\(([^\)]+)\)/ - }; - function parseProperties( properties, groupName ) { var group = properties.match( groupsPatterns[ groupName ] ); return group ? trim( group[ 1 ] ) : null; @@ -892,9 +874,57 @@ } // - // TRANSFORMATION TOOLS --------------------------------------------------- + // TRANSFORMATIONS -------------------------------------------------------- // + function applyTransformationsGroup( filter, element, group ) { + var i, rule; + + for ( i = 0; i < group.length; ++i ) { + rule = group[ i ]; + + if ( !rule.left || filter.check( rule.left ) ) { + rule.right( element, transformationsTools ); + return; // Only first matching rule in a group is executed. + } + } + } + + function getTransformationFn( toolName ) { + return function( el, tools ) { + tools[ toolName ]( el ); + }; + } + + function optimizeTransformations( rules ) { + var groupName, i, rule, + optimizedRules = []; + + for ( i = 0; i < rules.length; ++i ) { + rule = rules[ i ]; + + if ( typeof rule == 'string' ) + rule = rule.split( /\s*:\s*/ ); + + // Extract element name. + if ( !groupName ) + groupName = rule[ 0 ].match( /^([a-z0-9]+)/g )[ 0 ]; + + optimizedRules.push( { + // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it. + left: groupName == rule[ 0 ] ? null : rule[ 0 ], + + // Handle shorthand format. E.g.: 'table[width]:sizeToAttribute'. + right: typeof rule[ 1 ] == 'string' ? getTransformationFn( rule[ 1 ] ) : rule[ 1 ] + } ); + } + + return { + name: groupName, + rules: optimizedRules + }; + } + var transformationsTools = { sizeToStyle: function( element ) { this.lengthToStyle( element, 'width' ); diff --git a/plugins/image/plugin.js b/plugins/image/plugin.js index 49f6dd47ef1..082d7ccd586 100644 --- a/plugins/image/plugin.js +++ b/plugins/image/plugin.js @@ -23,10 +23,9 @@ editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, { allowedContent: 'img[align,alt,dir,id,lang,longdesc,src,title]{*}(*)', requiredContent: 'img[alt,src]', - contentTransformations: { - 'img{width}': 'sizeToStyle', - 'img[width]': 'sizeToAttribute' - } + contentTransformations: [ + [ 'img{width}: sizeToStyle', 'img[width]: sizeToAttribute' ] + ] } ) ); // Register the toolbar button. diff --git a/plugins/table/plugin.js b/plugins/table/plugin.js index 86e504c717b..68c9b06532d 100755 --- a/plugins/table/plugin.js +++ b/plugins/table/plugin.js @@ -21,10 +21,9 @@ CKEDITOR.plugins.add( 'table', { 'th td tr[scope];' + ( editor.plugins.dialogadvtab ? 'table' + editor.plugins.dialogadvtab.allowedContent() : '' ), requiredContent: 'table', // TODO We should also check td and tr. - contentTransformations: { - 'table{width}': 'sizeToStyle', - 'table[width]': 'sizeToAttribute' - } + contentTransformations: [ + [ 'table{width}: sizeToStyle', 'table[width]: sizeToAttribute' ] + ] } ) ); function createDef( def ) { From 84957b488cffd62ea7240da0a5b1b3a1d0a9b423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Wed, 6 Feb 2013 11:47:43 +0100 Subject: [PATCH 06/10] Implement content forms unification by filter. --- core/filter.js | 170 ++++++++++++++++++++++++++++++++-- core/style.js | 15 ++- plugins/basicstyles/plugin.js | 53 ++++++++++- plugins/dialog/plugin.js | 1 + samples/filter.html | 4 +- 5 files changed, 227 insertions(+), 16 deletions(-) diff --git a/core/filter.js b/core/filter.js index d66ac9d8c31..fd00399ec5c 100644 --- a/core/filter.js +++ b/core/filter.js @@ -237,6 +237,35 @@ this._.toDataFormatListener.removeListener(); }, + addContentForms: function( forms ) { + if ( this.disabled ) + return; + + if ( !forms ) + return; + + var i, form, + transfGroups = [], + preferredForm; + + // First, find preferred form - this is, first allowed. + for ( i = 0; i < forms.length && !preferredForm; ++i ) { + form = forms[ i ]; + + if ( ( typeof form == 'string' || form instanceof CKEDITOR.style ) && this.check( form ) ) + preferredForm = form; + } + + if ( !preferredForm ) + return; + + for ( i = 0; i < forms.length; ++i ) { + transfGroups.push( getContentFormTransformationGroup( forms[ i ], preferredForm ) ); + } + + this.addTransformations( transfGroups ); + }, + /** * Checks whether a feature can be enabled for the HTML restrictions in place * for the current CKEditor instance, based on the HTML the feature might @@ -260,11 +289,13 @@ if ( feature.toFeature ) feature = feature.toFeature( this.editor ); - this.addTransformations( feature.contentTransformations ); - // If default configuration (will be checked inside #allow()), // then add allowed content rules. this.allow( feature.allowedContent ); + + this.addTransformations( feature.contentTransformations ); + this.addContentForms( feature.contentForms ); + // If custom configuration, then check if required content is allowed. if ( this.customConfig && feature.requiredContent ) return this.check( feature.requiredContent ); @@ -273,6 +304,9 @@ }, addTransformations: function( transformations ) { + if ( this.disabled ) + return; + if ( !transformations ) return; @@ -280,7 +314,7 @@ group, i; for ( i = 0; i < transformations.length; ++i ) { - group = optimizeTransformations( transformations[ i ] ); + group = optimizeTransformationsGroup( transformations[ i ] ); if ( !optimized[ group.name ] ) optimized[ group.name ] = []; @@ -883,39 +917,122 @@ for ( i = 0; i < group.length; ++i ) { rule = group[ i ]; - if ( !rule.left || filter.check( rule.left ) ) { + if ( ( !rule.check || filter.check( rule.check ) ) && + ( !rule.left || rule.left( element ) ) ) { rule.right( element, transformationsTools ); return; // Only first matching rule in a group is executed. } } } + function elementMatchesStyle( element, style ) { + var def = style.getDefinition(), + defAttrs = def.attributes, + defStyles = def.styles, + attrName, styleName, + classes, classPattern, cl; + + if ( element.name != def.element ) + return false; + + for ( attrName in defAttrs ) { + if ( attrName == 'class' ) { + classes = defAttrs[ attrName ].split( /\s+/ ); + classPattern = element.classes.join( '|' ); + while ( ( cl = classes.pop() ) ) { + if ( classPattern.indexOf( cl ) == -1 ) + return false; + } + } else { + if ( element.attributes[ attrName ] != defAttrs[ attrName ] ) + return false; + } + } + + for ( styleName in defStyles ) { + if ( element.styles[ styleName ] != defStyles[ styleName ] ) + return false; + } + + return true; + } + + function getContentFormTransformationGroup( form, preferredForm ) { + var element, left; + + if ( typeof form == 'string' ) + element = form; + else if ( form instanceof CKEDITOR.style ) { + left = form; + } + else { + element = form[ 0 ]; + left = form[ 1 ]; + } + + return [ { + element: element, + left: left, + right: function( el, tools ) { + tools.transform( el, preferredForm ); + } + } ]; + } + + function getElementNameForTransformation( rule, check ) { + if ( rule.element ) + return rule.element; + if ( check ) + return check.match( /^([a-z0-9]+)/i )[ 0 ]; + return rule.left.getDefinition().element; + } + + function getMatchStyleFn( style ) { + return function( el ) { + return transformationsTools.matchStyle( el, style ); + }; + } + function getTransformationFn( toolName ) { return function( el, tools ) { tools[ toolName ]( el ); }; } - function optimizeTransformations( rules ) { + function optimizeTransformationsGroup( rules ) { var groupName, i, rule, + check, left, right, optimizedRules = []; for ( i = 0; i < rules.length; ++i ) { rule = rules[ i ]; - if ( typeof rule == 'string' ) + if ( typeof rule == 'string' ) { rule = rule.split( /\s*:\s*/ ); + check = rule[ 0 ]; + left = null; + right = rule[ 1 ]; + } else { + check = rule.check; + left = rule.left; + right = rule.right; + } // Extract element name. if ( !groupName ) - groupName = rule[ 0 ].match( /^([a-z0-9]+)/g )[ 0 ]; + groupName = getElementNameForTransformation( rule, check ); + + if ( left instanceof CKEDITOR.style ) + left = getMatchStyleFn( left ); optimizedRules.push( { // It doesn't make sense to test against name rule (e.g. 'table'), so don't save it. - left: groupName == rule[ 0 ] ? null : rule[ 0 ], + check: check == groupName ? null : check, + + left: left, // Handle shorthand format. E.g.: 'table[width]:sizeToAttribute'. - right: typeof rule[ 1 ] == 'string' ? getTransformationFn( rule[ 1 ] ) : rule[ 1 ] + right: typeof right == 'string' ? getTransformationFn( right ) : right } ); } @@ -965,6 +1082,41 @@ } delete element.styles[ styleName ]; + }, + + matchStyle: elementMatchesStyle, + + transform: function( el, form ) { + if ( typeof form == 'string' ) + el.name = form; + // Form is an instance of CKEDITOR.style. + else { + var def = form.getDefinition(), + defStyles = def.styles, + defAttrs = def.attributes, + attrName, styleName, + existingClassesPattern, defClasses, cl; + + el.name = def.element; + + for ( attrName in defAttrs ) { + if ( attrName == 'class' ) { + existingClassesPattern = el.classes.join( '|' ); + defClasses = defAttrs[ attrName ].split( /\s+/ ); + + while ( ( cl = defClasses.pop() ) ) { + if ( existingClassesPattern.indexOf( cl ) == -1 ) + el.classes.push( cl ); + } + } else { + el.attributes[ attrName ] = defAttrs[ attrName ]; + } + } + + for ( styleName in defStyles ) { + el.styles[ styleName ] = defStyles[ styleName ]; + } + } } }; diff --git a/core/style.js b/core/style.js index e47d7b906e3..ee769b65490 100644 --- a/core/style.js +++ b/core/style.js @@ -1406,14 +1406,25 @@ CKEDITOR.STYLE_OBJECT = 3; })(); /** + * Generic style command. It applies a specific style when executed. + * + * var boldStyle = new CKEDITOR.style( { element: 'strong' } ); + * // Register the "bold" command, which applies the bold style. + * editor.addCommand( 'bold', new CKEDITOR.dialogCommand( boldStyle ) ); + * * @class - * @todo + * @constructor Creates a styleCommand class instance. + * @extends CKEDITOR.commandDefinition + * @param {CKEDITOR.style} style The style to be applied when command is executed. + * @param {Object} [ext] Additional command definition's properties. */ -CKEDITOR.styleCommand = function( style ) { +CKEDITOR.styleCommand = function( style, ext ) { this.style = style; // TODO shouldn't we create content def only when explicitly requested (by argument)? this.allowedContent = style; this.requiredContent = style; + + CKEDITOR.tools.extend( this, ext, true ); }; /** diff --git a/plugins/basicstyles/plugin.js b/plugins/basicstyles/plugin.js index 1bd8aeb7fb4..170ee23d13a 100644 --- a/plugins/basicstyles/plugin.js +++ b/plugins/basicstyles/plugin.js @@ -15,7 +15,11 @@ CKEDITOR.plugins.add( 'basicstyles', { if ( !styleDefiniton ) return; - var style = new CKEDITOR.style( styleDefiniton ); + var style = new CKEDITOR.style( styleDefiniton ), + forms = contentForms[ commandName ]; + + // Put the style as the most important form. + forms.unshift( style ); // Listen to contextual style activation. editor.attachStyleStateChange( style, function( state ) { @@ -23,7 +27,9 @@ CKEDITOR.plugins.add( 'basicstyles', { }); // Create the command that can be used to apply the style. - editor.addCommand( commandName, new CKEDITOR.styleCommand( style ) ); + editor.addCommand( commandName, new CKEDITOR.styleCommand( style, { + contentForms: forms + } ) ); // Register the button, if the button plugin is loaded. if ( editor.ui.addButton ) { @@ -35,7 +41,48 @@ CKEDITOR.plugins.add( 'basicstyles', { } }; - var config = editor.config, + var contentForms = { + bold: [ + 'strong', + 'b', + [ 'span', function( el ) { + var fw = el.styles[ 'font-weight' ]; + return fw == 'bold' || +fw >= 700; + } ] + ], + + italic: [ + 'em', + 'i', + [ 'span', function( el ) { + return el.styles[ 'font-style' ] == 'italic'; + } ] + ], + + underline: [ + 'u', + [ 'span', function( el ) { + return el.styles[ 'text-decoration' ] == 'underline'; + } ] + ], + + strike: [ + 'strike', + 's', + [ 'span', function( el ) { + return el.styles[ 'text-decoration' ] == 'line-through'; + } ] + ], + + subscript: [ + 'sub' + ], + + superscript: [ + 'sup' + ] + }, + config = editor.config, lang = editor.lang.basicstyles; addButtonCommand( 'Bold', lang.bold, 'bold', config.coreStyles_bold ); diff --git a/plugins/dialog/plugin.js b/plugins/dialog/plugin.js index 4b11438d1b8..ad8af7cc339 100644 --- a/plugins/dialog/plugin.js +++ b/plugins/dialog/plugin.js @@ -2795,6 +2795,7 @@ CKEDITOR.DIALOG_RESIZE_BOTH = 3; * @extends CKEDITOR.commandDefinition * @param {String} dialogName The name of the dialog to open when executing * this command. + * @param {Object} [ext] Additional command definition's properties. */ CKEDITOR.dialogCommand = function( dialogName, ext ) { this.dialogName = dialogName; diff --git a/samples/filter.html b/samples/filter.html index 35cbf2e3d32..45dd7c24ad4 100644 --- a/samples/filter.html +++ b/samples/filter.html @@ -99,7 +99,7 @@

 CKEDITOR.replace( 'editor3', {
 	allowedContent: {
-		'strong em ul ol': true,
+		'b i ul ol': true,
 		'h1 h2 h3 p blockquote li': {
 			styles: 'text-align'
 		},
@@ -120,7 +120,7 @@ 

CKEDITOR.replace( 'editor3', { allowedContent: { - 'strong em ul ol': true, + 'b i ul ol': true, 'h1 h2 h3 p blockquote li': { styles: 'text-align' }, From db79ad58eac8d5b74e926cc9a4056b96ec6ea9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Wed, 6 Feb 2013 14:08:38 +0100 Subject: [PATCH 07/10] Unprotect and then protect back elements names. --- core/filter.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/core/filter.js b/core/filter.js index fd00399ec5c..71677c77a4a 100644 --- a/core/filter.js +++ b/core/filter.js @@ -84,7 +84,7 @@ // Add element filter before htmlDataProcessor.dataFilter // when purifying input data to correct html. this._.toHtmlListener = editor.on( 'toHtml', function( evt ) { - this.applyTo( evt.data.dataValue ); + this.applyTo( evt.data.dataValue, true ); }, this, null, 6 ); // Filter outcoming "data". @@ -171,8 +171,9 @@ * of filtering is DOM tree without disallowed content. * * @param {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} fragment Node to be filtered. + * @param {Boolean} toHtml Set to `true` if filter is used together with {@link CKEDITOR.htmlDataProcessor#toHtml}. */ - applyTo: function( fragment ) { + applyTo: function( fragment, toHtml ) { var toBeRemoved = [], rules = this._.rules, transformations = this._.transformations, @@ -180,7 +181,7 @@ // Filter all children, skip root (fragment or editable-like wrapper used by data processor). fragment.forEach( function( el ) { - filterFn( el, rules, transformations, toBeRemoved ); + filterFn( el, rules, transformations, toBeRemoved, toHtml ); }, CKEDITOR.NODE_ELEMENT, true ); var element, @@ -473,16 +474,19 @@ if ( that._.filterFunction ) return that._.filterFunction; - var unprotectElementsNamesRegexp = /^cke:(object|embed|param|html|body|head|title)$/; + var unprotectElementsNamesRegexp = /^cke:(object|embed|param)$/, + protectElementsNamesRegexp = /^(object|embed|param)$/; // Return and cache created function. - return that._.filterFunction = function( element, optimizedRules, transformations, toBeRemoved ) { + return that._.filterFunction = function( element, optimizedRules, transformations, toBeRemoved, toHtml ) { var name = element.name, i, l, trans; // Unprotect elements names previously protected by htmlDataProcessor // (see protectElementNames and protectSelfClosingElements functions). - name = name.replace( unprotectElementsNamesRegexp, '$1' ); + // Note: body, title, etc. are not protected by htmlDataP (or are protected and then unprotected). + if ( toHtml ) + element.name = name = name.replace( unprotectElementsNamesRegexp, '$1' ); if ( ( transformations = transformations && transformations[ name ] ) ) { populateProperties( element ); @@ -540,6 +544,10 @@ // Update element's attributes based on status of filtering. updateElement( element, status ); + + // Protect previously unprotected elements. + if ( toHtml ) + element.name = element.name.replace( protectElementsNamesRegexp, 'cke:$1' ); }; } From a03d5f7ed921a528e97ed91d1e9280443ac2d10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Wed, 6 Feb 2013 15:30:44 +0100 Subject: [PATCH 08/10] Docs for new features. --- core/filter.js | 153 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 5 deletions(-) diff --git a/core/filter.js b/core/filter.js index 71677c77a4a..b02fac5a93d 100644 --- a/core/filter.js +++ b/core/filter.js @@ -238,6 +238,26 @@ this._.toDataFormatListener.removeListener(); }, + /** + * Add array of Feature's content forms. All forms + * will be then transformed to the first form which is allowed. + * + * editor.filter.allow( 'i' ); + * editor.filter.addContentForms( [ + * 'em', + * 'i', + * [ 'span', function( el ) { + * return el.styles[ 'font-style' ] == 'italic'; + * } ] + * ] ); + * // Now and will be replaced with + * // because this is the first allowed form. + * + * This method is used by editor to add {@link CKEDITOR.feature#contentForms} + * when adding feature by {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. + * + * @param {Array} forms + */ addContentForms: function( forms ) { if ( this.disabled ) return; @@ -253,16 +273,17 @@ for ( i = 0; i < forms.length && !preferredForm; ++i ) { form = forms[ i ]; + // Check only strings and styles - array format isn't supported by #check(). if ( ( typeof form == 'string' || form instanceof CKEDITOR.style ) && this.check( form ) ) preferredForm = form; } + // This feature doesn't have preferredForm, so ignore it. if ( !preferredForm ) return; - for ( i = 0; i < forms.length; ++i ) { + for ( i = 0; i < forms.length; ++i ) transfGroups.push( getContentFormTransformationGroup( forms[ i ], preferredForm ) ); - } this.addTransformations( transfGroups ); }, @@ -304,6 +325,69 @@ return true; }, + /** + * Add array of content transformations groups. One group + * may contain many transformations rules, but only the first + * matching rule in a group is executed. + * + * Single transformation rule is an object with 4 properties: + * + * * `check` (optional) - if set and {@link CKEDITOR.filter} doesn't + * accept this allowed content rule, this transformation rule + * won't be executed (it doesn't *match*). This value is passed + * to {@link #check}. + * * `element` (optional) - this string property tells filter on which + * element this transformation can be ran. It's optional, because + * element's name can be obtained from `check` (if it's a String format) + * or `left` (if it's a {@link CKEDITOR.style} instance). + * * `left` (optional) - a function accepting element or {@link CKEDITOR.style} + * instance verifying whether transformation should be + * executed on this specific element. If it returns `false` or element + * doesn't match this style this transformation rule doesn't *match*. + * * `right` - a function accepting element and {@link CKEDITOR.filter.transformationsTools} + * or a string containing name of {@link CKEDITOR.filter.transformationsTools} method + * that should be called on element. + * + * There's also a shorthand format. Transformation rule can be defined by + * single string `'check:right'`. String before `':'` will be used as + * a `check` property and the second part as `right`. + * + * Transformation rules can be grouped. Filter will try to apply + * first rule in a group. If it *matches* it will ignore next rules and + * go to the next group. If it doesn't *match* it will check next one. + * + * Examples: + * + * editor.filter.addTransformations( [ + * // First group. + * [ + * // First rule. If table{width} is allowed + * // executes {@link CKEDITOR.filter.transformationsTools#sizeToStyle} on table element. + * 'table{width}: sizeToStyle', + * // Second rule shouldn't be executed if first was. + * 'table[width]: sizeToAttribute' + * ], + * // Second group. + * [ + * // This rule will add foo="1" attribute to all images that + * // don't have it. + * { + * element: 'img', + * left: function( el ) { + * return !el.attributes.foo; + * }, + * right: function( el, tools ) { + * el.attributes.foo = '1'; + * } + * } + * ] + * ] ); + * + * This method is used by editor to add {@link CKEDITOR.feature#contentTransformations} + * when adding feature by {@link #addFeature} or {@link CKEDITOR.editor#addFeature}. + * + * @param {Array} transformations + */ addTransformations: function( transformations ) { if ( this.disabled ) return; @@ -488,6 +572,7 @@ if ( toHtml ) element.name = name = name.replace( unprotectElementsNamesRegexp, '$1' ); + // If transformations are set apply all groups. if ( ( transformations = transformations && transformations[ name ] ) ) { populateProperties( element ); @@ -919,12 +1004,14 @@ // TRANSFORMATIONS -------------------------------------------------------- // + // Apply giver transformations group to the element. function applyTransformationsGroup( filter, element, group ) { var i, rule; for ( i = 0; i < group.length; ++i ) { rule = group[ i ]; + // Test with #check or #left only if it's set. if ( ( !rule.check || filter.check( rule.check ) ) && ( !rule.left || rule.left( element ) ) ) { rule.right( element, transformationsTools ); @@ -933,6 +1020,10 @@ } } + // Check whether element matches CKEDITOR.style. + // The element can be a "superset" of style, + // e.g. it may have more classes, but need to have + // at least those defined in style. function elementMatchesStyle( element, style ) { var def = style.getDefinition(), defAttrs = def.attributes, @@ -965,6 +1056,8 @@ return true; } + // Return transformation group for content form. + // One content form makes one transformation rule in one group. function getContentFormTransformationGroup( form, preferredForm ) { var element, left; @@ -987,6 +1080,8 @@ } ]; } + // Obtain element's name from transformation rule. + // It will be defined by #element, or #check or #left (styleDef.element). function getElementNameForTransformation( rule, check ) { if ( rule.element ) return rule.element; @@ -997,7 +1092,7 @@ function getMatchStyleFn( style ) { return function( el ) { - return transformationsTools.matchStyle( el, style ); + return elementMatchesStyle( el, style ); }; } @@ -1050,17 +1145,40 @@ }; } - var transformationsTools = { + /** + * Singleton containing tools useful for transformations rules. + * + * @class CKEDITOR.filter.transformationsTools + * @singleton + */ + var transformationsTools = CKEDITOR.filter.transformationsTools = { + /** + * Convert `width` and `height` attributes to styles. + * + * @param {CKEDITOR.htmlParser.element} element + */ sizeToStyle: function( element ) { this.lengthToStyle( element, 'width' ); this.lengthToStyle( element, 'height' ); }, + /** + * Convert `width` and `height` styles to attributes. + * + * @param {CKEDITOR.htmlParser.element} element + */ sizeToAttribute: function( element ) { this.lengthToAttribute( element, 'width' ); this.lengthToAttribute( element, 'height' ); }, + /** + * Convert length in `attrName` attribute to a valid CSS length (like `width` or `height`). + * + * @param {CKEDITOR.htmlParser.element} element + * @param {String} attrName Name of an attribute that will be converted. + * @param {String} [styleName=attrName] Name of a style into which attribute will be converted. + */ lengthToStyle: function( element, attrName, styleName ) { styleName = styleName || attrName; @@ -1078,6 +1196,13 @@ delete element.attributes[ attrName ]; }, + /** + * Convert length in `styleName` style to a valid length attribute (like `width` or `height`). + * + * @param {CKEDITOR.htmlParser.element} element + * @param {String} styleName Name of a style that will be converted. + * @param {String} [attrName=styleName] Name of an attribute into which style will be converted. + */ lengthToAttribute: function( element, styleName, attrName ) { attrName = attrName || styleName; @@ -1092,8 +1217,26 @@ delete element.styles[ styleName ]; }, - matchStyle: elementMatchesStyle, + /** + * Check whether element matches given {@link CKEDITOR.style}. + * The element can be a "superset" of style, e.g. it may have + * more classes, but need to have at least those defined in style. + * + * @param {CKEDITOR.htmlParser.element} element + * @param {CKEDITOR.style} style + */ + matchesStyle: elementMatchesStyle, + /* + * Transform element to given form. + * + * Form may be a: + * * {@link CKEDITOR.style}, + * * string - the new name of an element, + * + * @param {CKEDITOR.htmlParser.element} el + * @param {CKEDITOR.style/String} form + */ transform: function( el, form ) { if ( typeof form == 'string' ) el.name = form; From 4cda886fda354a8ae1bf017ff5073f90ef868f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Wed, 6 Feb 2013 15:54:36 +0100 Subject: [PATCH 09/10] Apply transformations when checking content. --- core/filter.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/filter.js b/core/filter.js index b02fac5a93d..9484b964d21 100644 --- a/core/filter.js +++ b/core/filter.js @@ -413,9 +413,10 @@ * by this filter. * * @param {String/CKEDITOR.style} test + * @param {Boolean} [applyTransformations=true] Whether to use registered transformations. * @returns {Boolean} Returns `true` if content is allowed. */ - check: function( test ) { + check: function( test, applyTransformations ) { if ( this.disabled ) return true; @@ -438,7 +439,7 @@ // Filter clone of mocked element. // Do not run transformations. - getFilterFunction( this )( clone, this._.rules, false, toBeRemoved ); + getFilterFunction( this )( clone, this._.rules, applyTransformations === false ? false : this._.transformations, toBeRemoved ); // Element has been marked for removal. if ( toBeRemoved.length > 0 ) @@ -1012,7 +1013,8 @@ rule = group[ i ]; // Test with #check or #left only if it's set. - if ( ( !rule.check || filter.check( rule.check ) ) && + // Do not apply transformations because that creates infinite loop. + if ( ( !rule.check || filter.check( rule.check, false ) ) && ( !rule.left || rule.left( element ) ) ) { rule.right( element, transformationsTools ); return; // Only first matching rule in a group is executed. From d0a50f000f2dd67224bb0505b69296832aba032d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Reinmar=20Koszuli=C5=84ski?= Date: Wed, 6 Feb 2013 16:06:29 +0100 Subject: [PATCH 10/10] Fixed typo. --- core/filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filter.js b/core/filter.js index 9484b964d21..c5783ad37ec 100644 --- a/core/filter.js +++ b/core/filter.js @@ -1005,7 +1005,7 @@ // TRANSFORMATIONS -------------------------------------------------------- // - // Apply giver transformations group to the element. + // Apply given transformations group to the element. function applyTransformationsGroup( filter, element, group ) { var i, rule;