diff options
Diffstat (limited to 'core/htmlparser/fragment.js')
| -rw-r--r-- | core/htmlparser/fragment.js | 613 |
1 files changed, 0 insertions, 613 deletions
diff --git a/core/htmlparser/fragment.js b/core/htmlparser/fragment.js deleted file mode 100644 index 10b19fe..0000000 --- a/core/htmlparser/fragment.js +++ /dev/null @@ -1,613 +0,0 @@ -/** - * @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 DOM structure. - * - * @class - * @constructor Creates a fragment class instance. - */ -CKEDITOR.htmlParser.fragment = function() { - /** - * The nodes contained in the root of this fragment. - * - * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); - * alert( fragment.children.length ); // 2 - */ - this.children = []; - - /** - * Get the fragment parent. Should always be null. - * - * @property {Object} [=null] - */ - this.parent = null; - - /** @private */ - this._ = { - isBlockLike: true, - hasInlineStarted: false - }; -}; - -(function() { - // Block-level elements whose internal structure should be respected during - // parser fixing. - var nonBreakingBlocks = CKEDITOR.tools.extend( { table:1,ul:1,ol:1,dl:1 }, CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl ); - - var listBlocks = { ol:1,ul:1 }; - - // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>. - var rootDtd = CKEDITOR.tools.extend( {}, { html:1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style:1,script:1 } ); - - function isRemoveEmpty( node ) { - // Empty link is to be removed when empty but not anchor. (#7894) - return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ]; - } - - /** - * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. - * - * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); - * alert( fragment.children[ 0 ].name ); // 'b' - * alert( fragment.children[ 1 ].value ); // ' Text' - * - * @static - * @param {String} fragmentHtml The HTML to be parsed, filling the fragment. - * @param {CKEDITOR.htmlParser.element/String} [parent] Optional contextual - * element which makes the content been parsed as the content of this element and fix - * to match it. - * If not provided, then {@link CKEDITOR.htmlParser.fragment} will be used - * as the parent and it will be returned. - * @param {String/Boolean} [fixingBlock] When `parent` is a block limit element, - * and the param is a string value other than `false`, it is to - * avoid having block-less content as the direct children of parent by wrapping - * the content with a block element of the specified tag, e.g. - * when `fixingBlock` specified as `p`, the content `<body><i>foo</i></body>` - * will be fixed into `<body><p><i>foo</i></p></body>`. - * @returns {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} The created fragment or passed `parent`. - */ - CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, parent, fixingBlock ) { - var parser = new CKEDITOR.htmlParser(); - - var root = parent instanceof CKEDITOR.htmlParser.element ? parent : typeof parent == 'string' ? new CKEDITOR.htmlParser.element( parent ) : new CKEDITOR.htmlParser.fragment(); - - var pendingInline = [], - pendingBRs = [], - currentNode = root, - // Indicate we're inside a <textarea> element, spaces should be touched differently. - inTextarea = root.name == 'textarea', - // Indicate we're inside a <pre> element, spaces should be touched differently. - inPre = root.name == 'pre'; - - function checkPending( newTagName ) { - var pendingBRsSent; - - if ( pendingInline.length > 0 ) { - for ( var i = 0; i < pendingInline.length; i++ ) { - var pendingElement = pendingInline[ i ], - pendingName = pendingElement.name, - pendingDtd = CKEDITOR.dtd[ pendingName ], - currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ]; - - if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) { - if ( !pendingBRsSent ) { - sendPendingBRs(); - pendingBRsSent = 1; - } - - // Get a clone for the pending element. - pendingElement = pendingElement.clone(); - - // Add it to the current node and make it the current, - // so the new element will be added inside of it. - pendingElement.parent = currentNode; - currentNode = pendingElement; - - // Remove the pending element (back the index by one - // to properly process the next entry). - pendingInline.splice( i, 1 ); - i--; - } else { - // Some element of the same type cannot be nested, flat them, - // e.g. <a href="#">foo<a href="#">bar</a></a>. (#7894) - if ( pendingName == currentNode.name ) - addElement( currentNode, currentNode.parent, 1 ), i--; - } - } - } - } - - function sendPendingBRs() { - while ( pendingBRs.length ) - addElement( pendingBRs.shift(), currentNode ); - } - - // Rtrim empty spaces on block end boundary. (#3585) - function removeTailWhitespace( element ) { - if ( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) { - - var length = element.children.length, - lastChild = element.children[ length - 1 ], - text; - if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT ) { - if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) ) - element.children.length = length - 1; - else - lastChild.value = text; - } - } - } - - // Beside of simply append specified element to target, this function also takes - // care of other dirty lifts like forcing block in body, trimming spaces at - // the block boundaries etc. - // - // @param {Element} element The element to be added as the last child of {@link target}. - // @param {Element} target The parent element to relieve the new node. - // @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless - // there's a return point node specified on the element, otherwise move current onto {@link target} node. - // - function addElement( element, target, moveCurrent ) { - target = target || currentNode || root; - - // Current element might be mangled by fix body below, - // save it for restore later. - var savedCurrent = currentNode; - - // Ignore any element that has already been added. - if ( element.previous === undefined ) { - if ( checkAutoParagraphing( target, element ) ) { - // Create a <p> in the fragment. - currentNode = target; - parser.onTagOpen( fixingBlock, {} ); - - // The new target now is the <p>. - element.returnPoint = target = currentNode; - } - - removeTailWhitespace( element ); - - // Avoid adding empty inline. - if ( !( isRemoveEmpty( element ) && !element.children.length ) ) - target.add( element ); - - if ( element.name == 'pre' ) - inPre = false; - - if ( element.name == 'textarea' ) - inTextarea = false; - } - - if ( element.returnPoint ) { - currentNode = element.returnPoint; - delete element.returnPoint; - } else - currentNode = moveCurrent ? target : savedCurrent; - } - - // Auto paragraphing should happen when inline content enters the root element. - function checkAutoParagraphing( parent, node ) { - - // Check for parent that can contain block. - if ( ( parent == root || parent.name == 'body' ) && fixingBlock && - ( !parent.name || CKEDITOR.dtd[ parent.name ][ fixingBlock ] ) ) - { - var name, realName; - if ( node.attributes && ( realName = node.attributes[ 'data-cke-real-element-type' ] ) ) - name = realName; - else - name = node.name; - - // Text node, inline elements are subjected, except for <script>/<style>. - return name && name in CKEDITOR.dtd.$inline && - !( name in CKEDITOR.dtd.head ) && - !node.isOrphan || - node.type == CKEDITOR.NODE_TEXT; - } - } - - // Judge whether two element tag names are likely the siblings from the same - // structural element. - function possiblySibling( tag1, tag2 ) { - - if ( tag1 in CKEDITOR.dtd.$listItem || tag1 in CKEDITOR.dtd.$tableContent ) - return tag1 == tag2 || tag1 == 'dt' && tag2 == 'dd' || tag1 == 'dd' && tag2 == 'dt'; - - return false; - } - - parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose ) { - var element = new CKEDITOR.htmlParser.element( tagName, attributes ); - - // "isEmpty" will be always "false" for unknown elements, so we - // must force it if the parser has identified it as a selfClosing tag. - if ( element.isUnknown && selfClosing ) - element.isEmpty = true; - - // Check for optional closed elements, including browser quirks and manually opened blocks. - element.isOptionalClose = optionalClose; - - // This is a tag to be removed if empty, so do not add it immediately. - if ( isRemoveEmpty( element ) ) { - pendingInline.push( element ); - return; - } else if ( tagName == 'pre' ) - inPre = true; - else if ( tagName == 'br' && inPre ) { - currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) ); - return; - } else if ( tagName == 'textarea' ) - inTextarea = true; - - if ( tagName == 'br' ) { - pendingBRs.push( element ); - return; - } - - while ( 1 ) { - var currentName = currentNode.name; - - var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd; - - // If the element cannot be child of the current element. - if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) { - // Current node doesn't have a close tag, time for a close - // as this element isn't fit in. (#7497) - if ( currentNode.isOptionalClose ) - parser.onTagClose( currentName ); - // Fixing malformed nested lists by moving it into a previous list item. (#3828) - else if ( tagName in listBlocks && currentName in listBlocks ) { - var children = currentNode.children, - lastChild = children[ children.length - 1 ]; - - // Establish the list item if it's not existed. - if ( !( lastChild && lastChild.name == 'li' ) ) - addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode ); - - !element.returnPoint && ( element.returnPoint = currentNode ); - currentNode = lastChild; - } - // Establish new list root for orphan list items, but NOT to create - // new list for the following ones, fix them instead. (#6975) - // <dl><dt>foo<dd>bar</dl> - // <ul><li>foo<li>bar</ul> - else if ( tagName in CKEDITOR.dtd.$listItem && - !possiblySibling( tagName, currentName ) ) { - parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 ); - } - // We're inside a structural block like table and list, AND the incoming element - // is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it, - // and most importantly, return back to here once this element is added, - // e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table> - else if ( currentName in nonBreakingBlocks && - !possiblySibling( tagName, currentName ) ) { - !element.returnPoint && ( element.returnPoint = currentNode ); - currentNode = currentNode.parent; - } else { - // The current element is an inline element, which - // need to be continued even after the close, so put - // it in the pending list. - if ( currentName in CKEDITOR.dtd.$inline ) - pendingInline.unshift( currentNode ); - - // The most common case where we just need to close the - // current one and append the new one to the parent. - if ( currentNode.parent ) - addElement( currentNode, currentNode.parent, 1 ); - // We've tried our best to fix the embarrassment here, while - // this element still doesn't find it's parent, mark it as - // orphan and show our tolerance to it. - else { - element.isOrphan = 1; - break; - } - } - } else - break; - } - - checkPending( tagName ); - sendPendingBRs(); - - element.parent = currentNode; - - if ( element.isEmpty ) - addElement( element ); - else - currentNode = element; - }; - - parser.onTagClose = function( tagName ) { - // Check if there is any pending tag to be closed. - for ( var i = pendingInline.length - 1; i >= 0; i-- ) { - // If found, just remove it from the list. - if ( tagName == pendingInline[ i ].name ) { - pendingInline.splice( i, 1 ); - return; - } - } - - var pendingAdd = [], - newPendingInline = [], - candidate = currentNode; - - while ( candidate != root && candidate.name != tagName ) { - // If this is an inline element, add it to the pending list, if we're - // really closing one of the parents element later, they will continue - // after it. - if ( !candidate._.isBlockLike ) - newPendingInline.unshift( candidate ); - - // This node should be added to it's parent at this point. But, - // it should happen only if the closing tag is really closing - // one of the nodes. So, for now, we just cache it. - pendingAdd.push( candidate ); - - // Make sure return point is properly restored. - candidate = candidate.returnPoint || candidate.parent; - } - - if ( candidate != root ) { - // Add all elements that have been found in the above loop. - for ( i = 0; i < pendingAdd.length; i++ ) { - var node = pendingAdd[ i ]; - addElement( node, node.parent ); - } - - currentNode = candidate; - - if ( candidate._.isBlockLike ) - sendPendingBRs(); - - addElement( candidate, candidate.parent ); - - // The parent should start receiving new nodes now, except if - // addElement changed the currentNode. - if ( candidate == currentNode ) - currentNode = currentNode.parent; - - pendingInline = pendingInline.concat( newPendingInline ); - } - - if ( tagName == 'body' ) - fixingBlock = false; - }; - - parser.onText = function( text ) { - // Trim empty spaces at beginning of text contents except <pre> and <textarea>. - if ( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre && !inTextarea ) { - text = CKEDITOR.tools.ltrim( text ); - - if ( text.length === 0 ) - return; - } - - var currentName = currentNode.name, - currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd; - - // Fix orphan text in list/table. (#8540) (#8870) - if ( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) { - parser.onTagOpen( currentName in listBlocks ? 'li' : currentName == 'dl' ? 'dd' : currentName == 'table' ? 'tr' : currentName == 'tr' ? 'td' : '' ); - parser.onText( text ); - return; - } - - sendPendingBRs(); - checkPending(); - - // Shrinking consequential spaces into one single for all elements - // text contents. - if ( !inPre && !inTextarea ) - text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' ); - - text = new CKEDITOR.htmlParser.text( text ); - - - if ( checkAutoParagraphing( currentNode, text ) ) - this.onTagOpen( fixingBlock, {}, 0, 1 ); - - currentNode.add( text ); - }; - - parser.onCDATA = function( cdata ) { - currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) ); - }; - - parser.onComment = function( comment ) { - sendPendingBRs(); - checkPending(); - currentNode.add( new CKEDITOR.htmlParser.comment( comment ) ); - }; - - // Parse it. - parser.parse( fragmentHtml ); - - // Send all pending BRs except one, which we consider a unwanted bogus. (#5293) - sendPendingBRs( !CKEDITOR.env.ie && 1 ); - - // Close all pending nodes, make sure return point is properly restored. - while ( currentNode != root ) - addElement( currentNode, currentNode.parent, 1 ); - - removeTailWhitespace( root ); - - return root; - }; - - CKEDITOR.htmlParser.fragment.prototype = { - - /** - * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}. - * - * @readonly - * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT] - */ - type: CKEDITOR.NODE_DOCUMENT_FRAGMENT, - - /** - * Adds a node to this fragment. - * - * @param {CKEDITOR.htmlParser.node} node The node to be added. - * @param {Number} [index] From where the insertion happens. - */ - add: function( node, index ) { - isNaN( index ) && ( index = this.children.length ); - - var previous = index > 0 ? this.children[ index - 1 ] : null; - if ( previous ) { - // If the block to be appended is following text, trim spaces at - // the right of it. - if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT ) { - previous.value = CKEDITOR.tools.rtrim( previous.value ); - - // If we have completely cleared the previous node. - if ( previous.value.length === 0 ) { - // Remove it from the list and add the node again. - this.children.pop(); - this.add( node ); - return; - } - } - - previous.next = node; - } - - node.previous = previous; - node.parent = this; - - this.children.splice( index, 0, node ); - - if ( !this._.hasInlineStarted ) - this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike ); - }, - - /** - * Filter this fragment's content with given filter. - * - * @since 4.1 - * @param {CKEDITOR.htmlParser.filter} filter - */ - filter: function( filter ) { - // Apply the root filter. - filter.onRoot( this ); - - this.filterChildren( filter ); - }, - - /** - * Filter this fragment's children with given filter. - * - * Element's children may only be filtered once by one - * instance of filter. - * - * @since 4.1 - * @param {CKEDITOR.htmlParser.filter} filter - * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`. - */ - filterChildren: function( filter, filterRoot ) { - // If this element's children were already filtered - // by current filter, don't filter them 2nd time. - // This situation may occur when filtering bottom-up - // (filterChildren() called manually in element's filter), - // or in unpredictable edge cases when filter - // is manipulating DOM structure. - if ( this.childrenFilteredBy == filter.id ) - return; - - // Filtering root if enforced. - if ( filterRoot && !this.parent ) - filter.onRoot( this ); - - this.childrenFilteredBy = filter.id; - - // Don't cache anything, children array may be modified by filter rule. - for ( var i = 0; i < this.children.length; i++ ) { - // Stay in place if filter returned false, what means - // that node has been removed. - if ( this.children[ i ].filter( filter ) === false ) - i--; - } - }, - - /** - * Writes the fragment HTML to a {@link CKEDITOR.htmlParser.basicWriter}. - * - * var writer = new CKEDITOR.htmlWriter(); - * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' ); - * fragment.writeHtml( writer ); - * alert( writer.getHtml() ); // '<p><b>Example</b></p>' - * - * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. - * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML. - */ - writeHtml: function( writer, filter ) { - if ( filter ) - this.filter( filter ); - - this.writeChildrenHtml( writer ); - }, - - /** - * Write and filtering the child nodes of this fragment. - * - * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. - * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML. - * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`. - */ - writeChildrenHtml: function( writer, filter, filterRoot ) { - // Filtering root if enforced. - if ( filterRoot && !this.parent && filter ) - filter.onRoot( this ); - - if ( filter ) - this.filterChildren( filter ); - - for ( var i = 0, children = this.children, l = children.length; i < l; i++ ) - children[ i ].writeHtml( writer ); - }, - - /** - * Execute callback on each node (of given type) in this document fragment. - * - * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>foo<b>bar</b>bom</p>' ); - * fragment.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 fragment. - */ - forEach: function( callback, type, skipRoot ) { - if ( !skipRoot && ( !type || this.type == type ) ) - callback( this ); - - var children = this.children, - node, - i = 0, - l = children.length; - - for ( ; i < l; i++ ) { - node = children[ i ]; - if ( node.type == CKEDITOR.NODE_ELEMENT ) - node.forEach( callback, type ); - else if ( !type || node.type == type ) - callback( node ); - } - } - }; -})(); |
