/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
/**
* A lightweight representation of an HTML element.
*
* @class
* @extends CKEDITOR.htmlParser.node
* @constructor Creates an element class instance.
* @param {String} name The element name.
* @param {Object} attributes An object storing all attributes defined for
* this element.
*/
CKEDITOR.htmlParser.element = function( name, attributes ) {
/**
* The element name.
*
* @property {String}
*/
this.name = name;
/**
* Stores the attributes defined for this element.
*
* @property {Object}
*/
this.attributes = attributes || {};
/**
* The nodes that are direct children of this element.
*/
this.children = [];
// Reveal the real semantic of our internal custom tag name (#6639),
// when resolving whether it's block like.
var realName = name || '',
prefixed = realName.match( /^cke:(.*)/ );
prefixed && ( realName = prefixed[ 1 ] );
var isBlockLike = !!( CKEDITOR.dtd.$nonBodyContent[ realName ] || CKEDITOR.dtd.$block[ realName ] ||
CKEDITOR.dtd.$listItem[ realName ] || CKEDITOR.dtd.$tableContent[ realName ] ||
CKEDITOR.dtd.$nonEditable[ realName ] || realName == 'br' );
this.isEmpty = !!CKEDITOR.dtd.$empty[ name ];
this.isUnknown = !CKEDITOR.dtd[ name ];
/** @private */
this._ = {
isBlockLike: isBlockLike,
hasInlineStarted: this.isEmpty || !isBlockLike
};
};
/**
* Object presentation of the CSS style declaration text.
*
* @class
* @constructor Creates a `cssStyle` class instance.
* @param {CKEDITOR.htmlParser.element/String} elementOrStyleText
* An HTML parser element or the inline style text.
*/
CKEDITOR.htmlParser.cssStyle = function() {
var styleText,
arg = arguments[ 0 ],
rules = {};
styleText = arg instanceof CKEDITOR.htmlParser.element ? arg.attributes.style : arg;
// html-encoded quote might be introduced by 'font-family'
// from MS-Word which confused the following regexp. e.g.
//'font-family: "Lucida, Console"'
// TODO reuse CSS methods from tools.
( styleText || '' ).replace( /"/g, '"' ).replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) {
name == 'font-family' && ( value = value.replace( /["']/g, '' ) );
rules[ name.toLowerCase() ] = value;
} );
return {
rules: rules,
/**
* Applies the styles to the specified element or object.
*
* @param {CKEDITOR.htmlParser.element/CKEDITOR.dom.element/Object} obj
*/
populate: function( obj ) {
var style = this.toString();
if ( style )
obj instanceof CKEDITOR.dom.element ? obj.setAttribute( 'style', style ) : obj instanceof CKEDITOR.htmlParser.element ? obj.attributes.style = style : obj.style = style;
},
/**
* Serializes CSS style declaration to a string.
*
* @returns {String}
*/
toString: function() {
var output = [];
for ( var i in rules )
rules[ i ] && output.push( i, ':', rules[ i ], ';' );
return output.join( '' );
}
};
};
/** @class CKEDITOR.htmlParser.element */
( function() {
// Used to sort attribute entries in an array, where the first element of
// each object is the attribute name.
var sortAttribs = function( a, b ) {
a = a[ 0 ];
b = b[ 0 ];
return a < b ? -1 : a > b ? 1 : 0;
},
fragProto = CKEDITOR.htmlParser.fragment.prototype;
CKEDITOR.htmlParser.element.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
/**
* The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
*
* @readonly
* @property {Number} [=CKEDITOR.NODE_ELEMENT]
*/
type: CKEDITOR.NODE_ELEMENT,
/**
* Adds a node to the element children list.
*
* @method
* @param {CKEDITOR.htmlParser.node} node The node to be added.
* @param {Number} [index] From where the insertion happens.
*/
add: fragProto.add,
/**
* Clones this element.
*
* @returns {CKEDITOR.htmlParser.element} The element clone.
*/
clone: function() {
return new CKEDITOR.htmlParser.element( this.name, this.attributes );
},
/**
* Filters this element and its children with the given filter.
*
* @since 4.1
* @param {CKEDITOR.htmlParser.filter} filter
* @returns {Boolean} The method returns `false` when this element has
* been removed or replaced with another. This information means that
* {@link #filterChildren} has to repeat the filter on the current
* position in parent's children array.
*/
filter: function( filter, context ) {
var element = this,
originalName, name;
context = element.getFilterContext( context );
// Do not process elements with data-cke-processor attribute set to off.
if ( context.off )
return true;
// Filtering if it's the root node.
if ( !element.parent )
filter.onRoot( context, element );
while ( true ) {
originalName = element.name;
if ( !( name = filter.onElementName( context, originalName ) ) ) {
this.remove();
return false;
}
element.name = name;
if ( !( element = filter.onElement( context, element ) ) ) {
this.remove();
return false;
}
// New element has been returned - replace current one
// and process it (stop processing this and return false, what
// means that element has been removed).
if ( element !== this ) {
this.replaceWith( element );
return false;
}
// If name has been changed - continue loop, so in next iteration
// filters for new name will be applied to this element.
// If name hasn't been changed - stop.
if ( element.name == originalName )
break;
// If element has been replaced with something of a
// different type, then make the replacement filter itself.
if ( element.type != CKEDITOR.NODE_ELEMENT ) {
this.replaceWith( element );
return false;
}
// This indicate that the element has been dropped by
// filter but not the children.
if ( !element.name ) {
this.replaceWithChildren();
return false;
}
}
var attributes = element.attributes,
a, value, newAttrName;
for ( a in attributes ) {
newAttrName = a;
value = attributes[ a ];
// Loop until name isn't modified.
// A little bit senseless, but IE would do that anyway
// because it iterates with for-in loop even over properties
// created during its run.
while ( true ) {
if ( !( newAttrName = filter.onAttributeName( context, a ) ) ) {
delete attributes[ a ];
break;
} else if ( newAttrName != a ) {
delete attributes[ a ];
a = newAttrName;
continue;
} else {
break;
}
}
if ( newAttrName ) {
if ( ( value = filter.onAttribute( context, element, newAttrName, value ) ) === false )
delete attributes[ newAttrName ];
else
attributes[ newAttrName ] = value;
}
}
if ( !element.isEmpty )
this.filterChildren( filter, false, context );
return true;
},
/**
* Filters this element's children with the given filter.
*
* Element's children may only be filtered once by one
* instance of the filter.
*
* @method filterChildren
* @param {CKEDITOR.htmlParser.filter} filter
*/
filterChildren: fragProto.filterChildren,
/**
* Writes the element HTML to the CKEDITOR.htmlWriter.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written.
* @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
* **Note:** It is unsafe to filter an offline (not appended) node.
*/
writeHtml: function( writer, filter ) {
if ( filter )
this.filter( filter );
var name = this.name,
attribsArray = [],
attributes = this.attributes,
attrName,
attr, i, l;
// Open element tag.
writer.openTag( name, attributes );
// Copy all attributes to an array.
for ( attrName in attributes )
attribsArray.push( [ attrName, attributes[ attrName ] ] );
// Sort the attributes by name.
if ( writer.sortAttributes )
attribsArray.sort( sortAttribs );
// Send the attributes.
for ( i = 0, l = attribsArray.length; i < l; i++ ) {
attr = attribsArray[ i ];
writer.attribute( attr[ 0 ], attr[ 1 ] );
}
// Close the tag.
writer.openTagClose( name, this.isEmpty );
this.writeChildrenHtml( writer );
// Close the element.
if ( !this.isEmpty )
writer.closeTag( name );
},
/**
* Sends children of this element to the writer.
*
* @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written.
* @param {CKEDITOR.htmlParser.filter} [filter]
*/
writeChildrenHtml: fragProto.writeChildrenHtml,
/**
* Replaces this element with its children.
*
* @since 4.1
*/
replaceWithChildren: function() {
var children = this.children;
for ( var i = children.length; i; )
children[ --i ].insertAfter( this );
this.remove();
},
/**
* Executes a callback on each node (of the given type) in this element.
*
* // Create a <p> element with foo<b>bar</b>bom as its content.
* var elP = CKEDITOR.htmlParser.fragment.fromHtml( 'foo<b>bar</b>bom', 'p' );
* elP.forEach( function( node ) {
* console.log( node );
* } );
* // Will log:
* // 1. document fragment,
* // 2. <p> element,
* // 3. "foo" text node,
* // 4. <b> element,
* // 5. "bar" text node,
* // 6. "bom" text node.
*
* @since 4.1
* @param {Function} callback Function to be executed on every node.
* **Since 4.3**: If `callback` returned `false`, the descendants of the current node will be ignored.
* @param {CKEDITOR.htmlParser.node} callback.node Node passed as an argument.
* @param {Number} [type] Whether the specified `callback` will be executed only on nodes of this type.
* @param {Boolean} [skipRoot] Do not execute `callback` on this element.
*/
forEach: fragProto.forEach,
/**
* Gets this element's first child. If `condition` is given, this method returns
* the first child which satisfies that condition.
*
* @since 4.3
* @param {String/Object/Function} condition Name of a child, a hash of names, or a validator function.
* @returns {CKEDITOR.htmlParser.node}
*/
getFirst: function( condition ) {
if ( !condition )
return this.children.length ? this.children[ 0 ] : null;
if ( typeof condition != 'function' )
condition = nameCondition( condition );
for ( var i = 0, l = this.children.length; i < l; ++i ) {
if ( condition( this.children[ i ] ) )
return this.children[ i ];
}
return null;
},
/**
* Gets this element's inner HTML.
*
* @since 4.3
* @returns {String}
*/
getHtml: function() {
var writer = new CKEDITOR.htmlParser.basicWriter();
this.writeChildrenHtml( writer );
return writer.getHtml();
},
/**
* Sets this element's inner HTML.
*
* @since 4.3
* @param {String} html
*/
setHtml: function( html ) {
var children = this.children = CKEDITOR.htmlParser.fragment.fromHtml( html ).children;
for ( var i = 0, l = children.length; i < l; ++i )
children[ i ].parent = this;
},
/**
* Gets this element's outer HTML.
*
* @since 4.3
* @returns {String}
*/
getOuterHtml: function() {
var writer = new CKEDITOR.htmlParser.basicWriter();
this.writeHtml( writer );
return writer.getHtml();
},
/**
* Splits this element at the given index.
*
* @since 4.3
* @param {Number} index Index at which the element will be split — `0` means the beginning,
* `1` after the first child node, etc.
* @returns {CKEDITOR.htmlParser.element} The new element following this one.
*/
split: function( index ) {
var cloneChildren = this.children.splice( index, this.children.length - index ),
clone = this.clone();
for ( var i = 0; i < cloneChildren.length; ++i )
cloneChildren[ i ].parent = clone;
clone.children = cloneChildren;
if ( cloneChildren[ 0 ] )
cloneChildren[ 0 ].previous = null;
if ( index > 0 )
this.children[ index - 1 ].next = null;
this.parent.add( clone, this.getIndex() + 1 );
return clone;
},
/**
* Searches through the current node children to find nodes matching the `criteria`.
*
* @param {String/Function} criteria Tag name or evaluator function.
* @param {Boolean} [recursive=false]
* @returns {CKEDITOR.htmlParser.node[]}
*/
find: function( criteria, recursive ) {
if ( recursive === undefined ) {
recursive = false;
}
var ret = [],
i;
for ( i = 0; i < this.children.length; i++ ) {
var curChild = this.children[ i ];
if ( typeof criteria == 'function' && criteria( curChild ) ) {
ret.push( curChild );
} else if ( typeof criteria == 'string' && curChild.name === criteria ) {
ret.push( curChild );
}
if ( recursive && curChild.find ) {
ret = ret.concat( curChild.find( criteria, recursive ) );
}
}
return ret;
},
/**
* Adds a class name to the list of classes.
*
* @since 4.4
* @param {String} className The class name to be added.
*/
addClass: function( className ) {
if ( this.hasClass( className ) )
return;
var c = this.attributes[ 'class' ] || '';
this.attributes[ 'class' ] = c + ( c ? ' ' : '' ) + className;
},
/**
* Removes a class name from the list of classes.
*
* @since 4.3
* @param {String} className The class name to be removed.
*/
removeClass: function( className ) {
var classes = this.attributes[ 'class' ];
if ( !classes )
return;
// We can safely assume that className won't break regexp.
// http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names
classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) );
if ( classes )
this.attributes[ 'class' ] = classes;
else
delete this.attributes[ 'class' ];
},
/**
* Checkes whether this element has a class name.
*
* @since 4.3
* @param {String} className The class name to be checked.
* @returns {Boolean} Whether this element has a `className`.
*/
hasClass: function( className ) {
var classes = this.attributes[ 'class' ];
if ( !classes )
return false;
return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes );
},
getFilterContext: function( ctx ) {
var changes = [];
if ( !ctx ) {
ctx = {
off: false,
nonEditable: false,
nestedEditable: false
};
}
if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' )
changes.push( 'off', true );
if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' )
changes.push( 'nonEditable', true );
// A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372, #11698).
// Special case: #11504 - filter starts on <body contenteditable=true>,
// so ctx.nonEditable has not been yet set to true.
else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' )
changes.push( 'nestedEditable', true );
if ( changes.length ) {
ctx = CKEDITOR.tools.copy( ctx );
for ( var i = 0; i < changes.length; i += 2 )
ctx[ changes[ i ] ] = changes[ i + 1 ];
}
return ctx;
}
}, true );
function nameCondition( condition ) {
return function( el ) {
return el.type == CKEDITOR.NODE_ELEMENT &&
( typeof condition == 'string' ? el.name == condition : el.name in condition );
};
}
} )();
|