Index: /CKEditor/trunk/_source/core/dom/document.js
===================================================================
--- /CKEditor/trunk/_source/core/dom/document.js	(revision 3064)
+++ /CKEditor/trunk/_source/core/dom/document.js	(revision 3065)
@@ -88,4 +88,45 @@
 		},
 
+		getByAddress : function( address, normalized )
+		{
+			var $ = this.$.documentElement;
+
+			for ( var i = 0 ; $ && i < address.length ; i++ )
+			{
+				var target = address[ i ];
+
+				if ( !normalized )
+				{
+					$ = $.childNodes[ target ];
+					continue;
+				}
+
+				var currentIndex = -1;
+
+				for (var j = 0 ; j < $.childNodes.length ; j++ )
+				{
+					var candidate = $.childNodes[ j ];
+
+					if ( normalized === true &&
+							candidate.nodeType == 3 &&
+							candidate.previousSibling &&
+							candidate.previousSibling.nodeType == 3 )
+					{
+						continue;
+					}
+
+					currentIndex++;
+
+					if ( currentIndex == target )
+					{
+						$ = candidate;
+						break;
+					}
+				}
+			}
+
+			return $ ? new CKEDITOR.dom.node( $ ) : null;
+		},
+
 		/**
 		 * Gets the &lt;head&gt; element for this document.
Index: /CKEditor/trunk/_source/core/dom/node.js
===================================================================
--- /CKEditor/trunk/_source/core/dom/node.js	(revision 3064)
+++ /CKEditor/trunk/_source/core/dom/node.js	(revision 3065)
@@ -150,4 +150,55 @@
 
 		/**
+		 * Retrieves a uniquely identifiable tree address for this node.
+		 * The tree address returns is an array of integers, with each integer
+		 * indicating a child index of a DOM node, starting from
+		 * document.documentElement.
+		 *
+		 * For example, assuming <body> is the second child from <html> (<head>
+		 * being the first), and we'd like to address the third child under the
+		 * fourth child of body, the tree address returned would be:
+		 * [1, 3, 2]
+		 *
+		 * The tree address cannot be used for finding back the DOM tree node once
+		 * the DOM tree structure has been modified.
+		 */
+		getAddress : function( normalized )
+		{
+			var address = [];
+			var $documentElement = this.getDocument().$.documentElement;
+			var node = this.$;
+
+			while ( node && node != $documentElement )
+			{
+				var parentNode = node.parentNode;
+				var currentIndex = -1;
+
+				for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
+				{
+					var candidate = parentNode.childNodes[i];
+
+					if ( normalized &&
+							candidate.nodeType == 3 &&
+							candidate.previousSibling &&
+							candidate.previousSibling.nodeType == 3 )
+					{
+						continue;
+					}
+
+					currentIndex++;
+
+					if ( candidate == node )
+						break;
+				}
+
+				address.unshift( currentIndex );
+
+				node = node.parentNode;
+			}
+
+			return address;
+		},
+
+		/**
 		 * Gets a DOM tree descendant under the current node.
 		 * @param {Array|Number} indices The child index or array of child indices under the node.
Index: /CKEditor/trunk/_source/core/dom/range.js
===================================================================
--- /CKEditor/trunk/_source/core/dom/range.js	(revision 3064)
+++ /CKEditor/trunk/_source/core/dom/range.js	(revision 3065)
@@ -430,25 +430,139 @@
 		},
 
+		/**
+		 * Creates a "non intrusive" and "mutation sensible" bookmark. This
+		 * kind of bookmark should be used only when the DOM is supposed to
+		 * remain stable after its creation.
+		 * @param {Boolean} [normalized] Indicates that the bookmark must
+		 *		normalized. When normalized, the successive text nodes are
+		 *		considered a single node. To sucessful load a normalized
+		 *		bookmark, the DOM tree must be also normalized before calling
+		 *		moveToBookmark.
+		 * @returns {Object} An object representing the bookmark.
+		 */
+		createBookmark2 : function( normalized )
+		{
+			var startContainer	= this.startContainer,
+				endContainer	= this.endContainer;
+
+			var startOffset	= this.startOffset,
+				endOffset	= this.endOffset;
+
+			var child, previous;
+
+			// If there is no range then get out of here.
+			// It happens on initial load in Safari #962 and if the editor it's
+			// hidden also in Firefox
+			if ( !startContainer || !endContainer )
+				return { start : 0, end : 0 };
+
+			if ( normalized )
+			{
+				// Find out if the start is pointing to a text node that will
+				// be normalized.
+				if ( startContainer.type == CKEDITOR.NODE_ELEMENT )
+				{
+					var child = startContainer.getChild( startOffset );
+
+					// In this case, move the start information to that text
+					// node.
+					if ( child && child.type == CKEDITOR.NODE_TEXT && child.getPrevious().type == CKEDITOR.NODE_TEXT )
+					{
+						startContainer = child;
+						startOffset = 0;
+					}
+				}
+
+				// Normalize the start.
+				while ( startContainer.type == CKEDITOR.NODE_TEXT
+						&& ( previous = startContainer.getPrevious() )
+						&& previous.type == CKEDITOR.NODE_TEXT )
+				{
+					startContainer = previous;
+					startOffset += previous.getLength();
+				}
+
+				// Process the end only if not normalized.
+				if ( !this.isCollapsed )
+				{
+					// Find out if the start is pointing to a text node that
+					// will be normalized.
+					if ( endContainer.type == CKEDITOR.NODE_ELEMENT )
+					{
+						child = endContainer.getChild( endOffset );
+
+						// In this case, move the start information to that
+						// text node.
+						if ( child && child.type == CKEDITOR.NODE_TEXT && child.getPrevious().type == CKEDITOR.NODE_TEXT )
+						{
+							endContainer = child;
+							endOffset = 0;
+						}
+					}
+
+					// Normalize the end.
+					while ( endContainer.type == CKEDITOR.NODE_TEXT
+							&& ( previous = endContainer.getPrevious() )
+							&& previous.type == CKEDITOR.NODE_TEXT )
+					{
+						endContainer = previous;
+						endOffset += previous.getLength();
+					}
+				}
+			}
+
+			return {
+				start		: startContainer.getAddress( normalized ),
+				end			: this.isCollapsed ? null : endContainer.getAddress( normalized ),
+				startOffset	: startOffset,
+				endOffset	: endOffset,
+				normalized	: normalized,
+				is2			: true		// It's a createBookmark2 bookmark.
+			};
+		},
+
 		moveToBookmark : function( bookmark )
 		{
-			var serializable = bookmark.serializable,
-				startNode	= serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
-				endNode		= serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
-
-			// Set the range start at the bookmark start node position.
-			this.setStartBefore( startNode );
-
-			// Remove it, because it may interfere in the setEndBefore call.
-			startNode.remove();
-
-			// Set the range end at the bookmark end node position, or simply
-			// collapse it if it is not available.
-			if ( endNode )
-			{
-				this.setEndBefore( endNode );
-				endNode.remove();
-			}
-			else
-				this.collapse( true );
+			if ( bookmark.is2 )		// Created with createBookmark2().
+			{
+				// Get the start information.
+				var startContainer	= this.document.getByAddress( bookmark.start, bookmark.normalized ),
+					startOffset	= bookmark.startOffset;
+
+				// Get the end information.
+				var endContainer	= bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
+					endOffset	= bookmark.endOffset;
+
+				// Set the start boundary.
+				this.setStart( startContainer, startOffset );
+
+				// Set the end boundary. If not available, collapse it.
+				if ( endContainer )
+					this.setEnd( endContainer, endOffset );
+				else
+					this.collapse( true );
+			}
+			else					// Created with createBookmark().
+			{
+				var serializable = bookmark.serializable,
+					startNode	= serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
+					endNode		= serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
+
+				// Set the range start at the bookmark start node position.
+				this.setStartBefore( startNode );
+
+				// Remove it, because it may interfere in the setEndBefore call.
+				startNode.remove();
+
+				// Set the range end at the bookmark end node position, or simply
+				// collapse it if it is not available.
+				if ( endNode )
+				{
+					this.setEndBefore( endNode );
+					endNode.remove();
+				}
+				else
+					this.collapse( true );
+			}
 		},
 
Index: /CKEditor/trunk/_source/tests/core/dom/range.html
===================================================================
--- /CKEditor/trunk/_source/tests/core/dom/range.html	(revision 3064)
+++ /CKEditor/trunk/_source/tests/core/dom/range.html	(revision 3065)
@@ -12,4 +12,5 @@
 
 var html1, html2;
+var tests;
 
 CKEDITOR.test.addTestCase( (function()
@@ -21,5 +22,5 @@
 	var doc = new CKEDITOR.dom.document( document );
 
-	return {
+	return tests = {
 		test__constructor : function()
 		{
@@ -1511,4 +1512,98 @@
 		},
 
+		test_createBookmark2_1 : function()
+		{
+			doc.getById( 'playground' ).setHtml( '<p id="P">This is <b id="B">a test</b></p>' );
+
+			var range = new CKEDITOR.dom.range( doc );
+
+			range.setStart( doc.getById( 'P' ), 0 );
+			range.setEnd( doc.getById( 'B' ).getFirst(), 3 );
+
+			var bookmark = range.createBookmark2();
+
+			range = new CKEDITOR.dom.range( doc );
+			range.moveToBookmark( bookmark );
+
+			assert.areSame( document.getElementById('P'), range.startContainer.$, 'range.startContainer' );
+			assert.areSame( 0, range.startOffset, 'range.startOffset' );
+			assert.areSame( document.getElementById('B').firstChild, range.endContainer.$, 'range.endContainer' );
+			assert.areSame( 3, range.endOffset, 'range.endOffset' );
+			assert.isFalse( range.collapsed, 'range.collapsed' );
+		},
+
+		// This test is just like test_createBookmark2_3, but uses a "non
+		// normalized" bookmark.
+		test_createBookmark2_2 : function()
+		{
+			var html = '<p id="P">A B <b>C </b>D E</p>';
+
+			doc.getById( 'playground' ).setHtml( html );
+
+			var p = doc.getById( 'P' );
+
+			// Split the text nodes.
+			p.getFirst().split( 2 );	// Right before "B"
+			p.getChild( 3 ).split( 2 );	// Right before "E"
+
+			assert.areSame( 5, p.getChildCount(), 'The number of nodes after split doesn\'t match' );
+
+			var range = new CKEDITOR.dom.range( doc );
+
+			// Create a range that enbraces "E".
+			range.setStartBefore( p.getChild( 4 ) );
+			range.setEndAfter( p.getChild( 4 ) );
+
+			var bookmark = range.createBookmark2();
+
+			range = new CKEDITOR.dom.range( doc );
+			range.moveToBookmark( bookmark );
+
+			assert.areSame( document.getElementById('P'), range.startContainer.$, 'range.startContainer' );
+			assert.areSame( 4, range.startOffset, 'range.startOffset' );
+			assert.areSame( document.getElementById('P'), range.endContainer.$, 'range.endContainer' );
+			assert.areSame( 5, range.endOffset, 'range.endOffset' );
+			assert.isFalse( range.collapsed, 'range.collapsed' );
+		},
+
+		test_createBookmark2_3 : function()
+		{
+			var html = '<p id="P">A B <b>C </b>D E</p>';
+
+			doc.getById( 'playground' ).setHtml( html );
+
+			var p = doc.getById( 'P' );
+
+			// Split the text nodes.
+			p.getFirst().split( 2 );	// Right before "B"
+			p.getChild( 3 ).split( 2 );	// Right before "E"
+
+			assert.areSame( 5, p.getChildCount(), 'The number of nodes after split doesn\'t match' );
+
+			var range = new CKEDITOR.dom.range( doc );
+
+			// Create a range that enbraces "E".
+			range.setStartBefore( p.getChild( 4 ) );
+			range.setEndAfter( p.getChild( 4 ) );
+
+			var bookmark = range.createBookmark2( true );
+
+			// Normalize the contents.
+			doc.getById( 'playground' ).setHtml( html );
+
+			range = new CKEDITOR.dom.range( doc );
+			range.moveToBookmark( bookmark );
+
+			assert.areSame( document.getElementById('P').childNodes[2], range.startContainer.$, 'range.startContainer' );
+			assert.areSame( 2, range.startOffset, 'range.startOffset' );
+			assert.areSame( document.getElementById('P'), range.endContainer.$, 'range.endContainer' );
+
+			// Note that the endOffset doesn't get normalized as it's not
+			// needed. Any offset pointing over the container size is meant to
+			// be at the end of it.
+			assert.areSame( 5, range.endOffset, 'range.endOffset' );
+			assert.isFalse( range.collapsed, 'range.collapsed' );
+		},
+
 		/////////////
 
@@ -1525,17 +1620,5 @@
 //window.onload = function()
 //{
-//	// Local references.
-//	var assert			= CKEDITOR.test.assert;
-//	var getInnerHtml	= CKEDITOR.test.getInnerHtml;
-
-//	var doc = new CKEDITOR.dom.document( document );
-
-//			doc.getById( '_EnlargeP' ).setHtml( 'this <i>is some </i>sample text' );
-
-//			var range = new CKEDITOR.dom.range( doc );
-//			range.setStart( doc.getById( '_EnlargeP' ), 0 );
-//			range.setEnd( doc.getById( '_EnlargeP' ).getChild( 1 ), 0 );
-
-//			range.enlarge( CKEDITOR.ENLARGE_ELEMENT );
+//	tests.test_createBookmark2_3();
 //}
 
