diff options
Diffstat (limited to 'core/htmlparser/element.js')
| -rw-r--r-- | core/htmlparser/element.js | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/core/htmlparser/element.js b/core/htmlparser/element.js new file mode 100644 index 0000000..963940d --- /dev/null +++ b/core/htmlparser/element.js @@ -0,0 +1,348 @@ +/** + * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.html 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 And object holding all attributes defined for + * this element. + */ +CKEDITOR.htmlParser.element = function( name, attributes ) { + /** + * The element name. + * + * @property {String} + */ + this.name = name; + + /** + * Holds 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 CSS style declaration text. + * + * @class + * @constructor Creates a cssStyle class instance. + * @param {CKEDITOR.htmlParser.element/String} elementOrStyleText + * A 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, + + /** + * Apply the styles onto 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; + } + }, + + /** + * Serialize CSS style declaration to 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, + + /** + * Clone this element. + * + * @returns {CKEDITOR.htmlParser.element} The element clone. + */ + clone: function() { + return new CKEDITOR.htmlParser.element( this.name, this.attributes ); + }, + + /** + * Filter this element and its children with given filter. + * + * @since 4.1 + * @param {CKEDITOR.htmlParser.filter} filter + * @returns {Boolean} Method returns `false` when this element has + * been removed or replaced with other. This is an information for + * {@link #filterChildren} that it has to repeat filter on current + * position in parent's children array. + */ + filter: function( filter ) { + var element = this, + originalName, name; + + // Filtering if it's the root node. + if ( !element.parent ) + filter.onRoot( element ); + + while ( true ) { + originalName = element.name; + + if ( !( name = filter.onElementName( originalName ) ) ) { + this.remove(); + return false; + } + + element.name = name; + + if ( !( element = filter.onElement( 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( a ) ) ) { + delete attributes[ a ]; + break; + } else if ( newAttrName != a ) { + delete attributes[ a ]; + a = newAttrName; + continue; + } else + break; + } + + if ( newAttrName ) { + if ( ( value = filter.onAttribute( element, newAttrName, value ) ) === false ) + delete attributes[ newAttrName ]; + else + attributes[ newAttrName ] = value; + } + } + + if ( !element.isEmpty ) + this.filterChildren( filter ); + + return true; + }, + + /** + * Filter this element's children with given filter. + * + * Element's children may only be filtered once by one + * instance of filter. + * + * @method filterChildren + * @param {CKEDITOR.htmlParser.filter} filter + */ + filterChildren: fragProto.filterChildren, + + /** + * Writes the element HTML to a CKEDITOR.htmlWriter. + * + * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. + * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node. + * **Note:** it's unsafe to filter 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 ); + }, + + /** + * Send children of this element to the writer. + * + * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. + * @param {CKEDITOR.htmlParser.filter} [filter] + */ + writeChildrenHtml: fragProto.writeChildrenHtml, + + /** + * Replace 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(); + }, + + /** + * Execute callback on each node (of given type) in this element. + * + * // Create <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. + * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument. + * @param {Number} [type] If specified `callback` will be executed only on nodes of this type. + * @param {Boolean} [skipRoot] Don't execute `callback` on this element. + */ + forEach: fragProto.forEach + } ); +})(); |
