Index: /FCKeditor/trunk/editor/_source/classes/fckdomrange.js
===================================================================
--- /FCKeditor/trunk/editor/_source/classes/fckdomrange.js	(revision 407)
+++ /FCKeditor/trunk/editor/_source/classes/fckdomrange.js	(revision 408)
@@ -284,22 +284,5 @@
 		{
 			"Start" : [ this._Range.startOffset ],
-			"End" : [ this._Range.endOffset ],
-			"CompareStart" : function( bookmark )
-			{
-				for ( var i = 0 ; i < Math.min( this.Start.length, bookmark.Start.length ) ; i++ )
-				{
-					if ( this.Start[i] < bookmark.Start[i] )
-						return -1;
-					else if (this.Start[i] > bookmark.Start[i] )
-						return 1;
-				}
-
-				if ( this.Start.length < bookmark.Start.length )
-					return -1;
-				else if (this.Start.length > bookmark.Start.length )
-					return 1;
-
-				return 0;
-			}
+			"End" : [ this._Range.endOffset ]
 		} ;
 		// Then, we record down the precise position of the container nodes
@@ -336,5 +319,5 @@
 	},
 
-	MoveToBookmark2 : function(bookmark)
+	MoveToBookmark2 : function( bookmark )
 	{
 		// Reverse the childNode counting algorithm in CreateBookmark2()
@@ -649,2 +632,18 @@
 	}
 } ;
+
+FCKDomRange.CompareCursors = function( cursor1, cursor2 )
+{
+	for ( var i = 0 ; i < Math.min( cursor1.length, cursor2.length ) ; i++ )
+	{
+		if ( cursor1[i] < cursor2[i] )
+			return -1;
+		else if (cursor1[i] > cursor2[i] )
+			return 1;
+	}
+	if ( cursor1.length < cursor2.length )
+		return -1;
+	else if (cursor1.length > cursor2.length )
+		return 1;
+	return 0;
+}
Index: /FCKeditor/trunk/editor/dialog/fck_replace.html
===================================================================
--- /FCKeditor/trunk/editor/dialog/fck_replace.html	(revision 407)
+++ /FCKeditor/trunk/editor/dialog/fck_replace.html	(revision 408)
@@ -35,8 +35,6 @@
 	// First of all, translate the dialog box texts
 	oEditor.FCKLanguageManager.TranslatePage( document ) ;
-
+	
 	window.parent.SetAutoSize( true ) ;
-
-	oEditor.FCKUndo.SaveUndoStep() ;
 }
 
@@ -48,66 +46,295 @@
 }
 
-function ReplaceTextNodes( parentNode, regex, replaceValue, replaceAll, hasFound )
-{
-	for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
-	{
-		var oNode = parentNode.childNodes[i] ;
-		if ( oNode.nodeType == 3 )
-		{
-			var sReplaced = oNode.nodeValue.replace( regex, replaceValue ) ;
-			if ( oNode.nodeValue != sReplaced )
+function GetSelection()
+{
+	var range = new oEditor.FCKDomRange( oEditor.FCK.EditorWindow ) ;
+	range.MoveToSelection() ;
+	return range.CreateBookmark2() ;
+}
+
+function GetSearchString()
+{
+	return document.getElementById("txtFind").value ;
+}
+
+function GetReplaceString()
+{
+	return document.getElementById("txtReplace").value ;
+}
+
+function GetCheckCase()
+{
+	return !! ( document.getElementById("chkCase").checked ) ;
+}
+
+function GetMatchWord()
+{
+	return !! ( document.getElementById("chkWord").checked ) ;
+}
+
+// Get the data pointed to by a bookmark.
+function GetData( bookmark )
+{
+	var currentNode = oEditor.FCK.EditorDocument.documentElement;
+	for( var i = 0 ; i < bookmark.length ; i++ )
+	{
+		if ( currentNode.childNodes.length > bookmark[i] )
+			currentNode = currentNode.childNodes.item( bookmark[i] ) ;
+		else if ( currentNode.nodeType == 3 )	// text node
+		{
+			var c = currentNode.nodeValue.charAt( bookmark[i] ) ;
+			if ( i == bookmark.length - 1 )
+				return c != "" ? c : null ;
+			else
+				return null ;
+		}
+		else
+			return null;
+	}
+	return currentNode ;
+}
+
+// With this function, we can treat the bookmark as an iterator for DFS.
+function NextPosition( bookmark )
+{
+	// See if there's anything further down the tree.
+	var next = bookmark.concat( [0] ) ;
+
+	if ( GetData( next ) != null )
+		return next ;
+
+	// Nothing down there? See if there's anything next to me.
+	var next = bookmark.slice( 0, bookmark.length - 1 ).concat( [ bookmark[ bookmark.length - 1 ] + 1 ] ) ;
+	if ( GetData( next ) != null )
+		return next ;
+
+	// Nothing even next to me? See if there's anything next to my ancestors.
+	for ( var i = bookmark.length - 1 ; i > 0 ; i-- )
+	{
+		var next = bookmark.slice( 0, i - 1 ).concat( [ bookmark[ i - 1 ] + 1 ] ) ;
+		if ( GetData( next ) != null )
+			return next ;
+	}
+
+	// There's absolutely nothing left to walk, return null.
+	return null ;
+}
+
+// Is this character a unicode whitespace?
+// Reference: http://unicode.org/Public/UNIDATA/PropList.txt
+function CheckIsWhitespace( c )
+{
+	var code = c.charCodeAt( 0 );
+	if ( code >= 9 && code <= 0xd )
+		return true;
+	if ( code >= 0x2000 && code <= 0x200a )
+		return true;
+	switch ( code )
+	{
+		case 0x20:
+		case 0x85:
+		case 0xa0:
+		case 0x1680:
+		case 0x180e:
+		case 0x2028:
+		case 0x2029:
+		case 0x202f:
+		case 0x205f:
+		case 3000:
+			return true;
+		default:
+			return false;
+	}
+}
+
+// Knuth-Morris-Pratt Algorithm for stream input
+KMP_NOMATCH = 0 ;
+KMP_ADVANCED = 1 ;
+KMP_MATCHED = 2 ;
+function KmpMatch( pattern, ignoreCase )
+{
+	var overlap = [ -1 ] ;
+	for ( var i = 0 ; i < pattern.length ; i++ )
+	{
+		overlap.push( overlap[i] + 1 ) ;
+		while ( overlap[ i + 1 ] > 0 && pattern.charAt( i ) != pattern.charAt( overlap[ i + 1 ] - 1 ) )
+			overlap[ i + 1 ] = overlap[ overlap[ i + 1 ] - 1 ] + 1 ;
+	}
+	this._Overlap = overlap ;
+	this._State = 0 ;
+	this._IgnoreCase = ( ignoreCase === true ) ;
+	if ( ignoreCase )
+		this.Pattern = pattern.toLowerCase();
+	else
+		this.Pattern = pattern ;
+}
+KmpMatch.prototype = {
+	"FeedCharacter" : function( c )
+	{
+		if ( this._IgnoreCase )
+			c = c.toLowerCase();
+
+		while ( true )
+		{
+			if ( c == this.Pattern[ this._State ] )
 			{
-				oNode.nodeValue = sReplaced ;
-				if ( ! replaceAll )
-					return true ;
-				hasFound = true ;
+				this._State++ ;
+				if ( this._State == this.Pattern.length )
+				{
+					// found a match, start over, don't care about partial matches involving the current match
+					this._State = 0;
+					return KMP_MATCHED;
+				}
+				return KMP_ADVANCED;
 			}
-		}
-
-		hasFound = ReplaceTextNodes( oNode, regex, replaceValue, replaceAll, hasFound ) ;
-		if ( ! replaceAll && hasFound )
-			return true ;
-	}
-
-	return hasFound ;
-}
-
-function GetRegexExpr()
-{
-	var sExpr = EscapeRegexString( document.getElementById('txtFind').value ) ;
-
-	if ( document.getElementById('chkWord').checked )
-		sExpr = '\\b' + sExpr + '\\b' ;
-
-	return sExpr ;
-}
-
-function GetCase()
-{
-	return ( document.getElementById('chkCase').checked ? '' : 'i' ) ;
-}
-
-function GetReplacement()
-{
-	return document.getElementById('txtReplace').value.replace( /\$/g, '$$$$' ) ;
-}
-
-function EscapeRegexString( str )
-{
-	return str.replace( /[\\\^\$\*\+\?\{\}\.\(\)\!\|\[\]\-]/g, '\\$&' ) ;
+			else if ( this._State == 0 )
+				return KMP_NOMATCH;
+			else
+				this._State = this._Overlap[ this._State ];
+		}
+	},
+	"Reset" : function()
+	{
+		this._State = 0 ;
+	}
+};
+
+function Find()
+{
+	// Start from the end of the current selection.
+	var matcher = new KmpMatch( GetSearchString(), ! GetCheckCase() ) ;
+	var cursor = GetSelection().End ;
+	var matchState = KMP_NOMATCH ;
+	var matchBookmark = null ;
+
+	// Match finding.
+	while ( true )
+	{
+		// Perform KMP stream matching.
+		//	- Reset KMP matcher if we encountered a block element.
+		var data = GetData( cursor ) ;
+		if ( data == null)
+		{
+			// do nothing, skip other cases
+		}
+		else if ( data.tagName )
+		{
+			if ( oEditor.FCKListsLib.BlockElements[ data.tagName.toLowerCase() ] )
+				matcher.Reset();
+		}
+		else if ( data.charAt != undefined )
+		{
+			matchState = matcher.FeedCharacter(data) ;
+
+			if ( matchState == KMP_NOMATCH )
+				matchBookmark = null ;
+			else if ( matchState == KMP_ADVANCED && matchBookmark == null )
+				matchBookmark = { "Start" : cursor.concat( [] ) } ;
+			else if ( matchState == KMP_MATCHED )
+			{
+				matchBookmark.End = cursor.concat( [] ) ;
+				matchBookmark.End[ matchBookmark.End.length - 1 ]++;
+
+				// Wait, do we have to match a whole word?
+				if ( GetMatchWord() )
+				{
+					var startOk = false ;
+					var endOk = false ;
+					var start = matchBookmark.Start ;
+					var end = matchBookmark.End ;
+					if ( start[ start.length - 1 ] == 0 )
+						startOk = true ;
+					else
+					{
+						var cursorBeforeStart = start.slice( 0, start.length - 1 ) ;
+						cursorBeforeStart.push( start[ start.length - 1 ] - 1 ) ;
+						var dataBeforeStart = GetData( cursorBeforeStart ) ;
+						if ( dataBeforeStart == null || dataBeforeStart.charAt == undefined )
+							startOk = true ;
+						else if ( CheckIsWhitespace( dataBeforeStart ) )
+							startOk = true ;
+					}
+
+					// this is already one character beyond the last char, no need to move
+					var cursorAfterEnd = end ;
+					var dataAfterEnd = GetData( cursorAfterEnd );
+					if ( dataAfterEnd == null || dataAfterEnd.charAt == undefined )
+						endOk = true ;
+					else if ( CheckIsWhitespace( dataAfterEnd ) )
+						endOk = true ;
+
+					if ( startOk && endOk )
+						break ;
+					else
+						matcher.Reset() ;
+				}
+				else
+					break ;
+			}
+		}
+
+		// Perform DFS across the document, until we've reached the end.
+		cursor = NextPosition( cursor ) ;
+		if ( cursor == null )
+			break;
+	}
+
+	// If we've found a match, select the match.
+	if ( matchState == KMP_MATCHED )
+	{
+		var range = new oEditor.FCKDomRange( oEditor.FCK.EditorWindow ) ;
+		range.MoveToBookmark2( matchBookmark ) ;
+		range.Select() ;
+		return true;
+	}
+	else
+		return false;
 }
 
 function Replace()
 {
-	var oRegex = new RegExp( GetRegexExpr(), GetCase() ) ;
-	if ( !ReplaceTextNodes( oEditor.FCK.EditorDocument.body, oRegex, GetReplacement(), false, false ) )
+	var selection = new oEditor.FCKDomRange( oEditor.FCK.EditorWindow ) ;
+	selection.MoveToSelection() ;
+
+	if ( selection.CheckIsCollapsed() )
+	{
+		if (! Find() )
+			alert( oEditor.FCKLang.DlgFindNotFoundMsg ) ;
+	}
+	else
+	{
+		oEditor.FCKUndo.SaveUndoStep() ;
+		selection.DeleteContents() ;
+		selection.InsertNode( oEditor.FCK.EditorDocument.createTextNode( GetReplaceString() ) ) ;
+		selection.Collapse( false ) ;
+		selection.Select() ;
+		oEditor.FCK.EditorDocument.body.normalize() ;
+	}
+}
+
+function ReplaceAll()
+{
+	oEditor.FCKUndo.SaveUndoStep() ;
+
+	var range = new oEditor.FCKDomRange( oEditor.FCK.EditorWindow ) ;
+	range.SetStart( oEditor.FCK.EditorDocument.body, 1 ) ;
+	range.SetEnd( oEditor.FCK.EditorDocument.body, 1 ) ;
+	range.Collapse( true ) ;
+	range.Select() ;
+	var replaceCount = 0 ;
+
+	while ( Find() )
+	{
+		range.MoveToSelection() ;
+		range.DeleteContents() ;
+		range.InsertNode( oEditor.FCK.EditorDocument.createTextNode( GetReplaceString() ) ) ;
+		range.Collapse( false ) ;
+		range.Select() ;
+		replaceCount++ ;
+	}
+	if ( replaceCount == 0 )
 		alert( oEditor.FCKLang.DlgFindNotFoundMsg ) ;
-}
-
-function ReplaceAll()
-{
-	var oRegex = new RegExp( GetRegexExpr(), GetCase() + 'g' ) ;
-	if ( !ReplaceTextNodes( oEditor.FCK.EditorDocument.body, oRegex, GetReplacement(), true, false ) )
-		alert( oEditor.FCKLang.DlgFindNotFoundMsg ) ;
+	else
+		oEditor.FCK.EditorDocument.body.normalize() ;
 	window.parent.Cancel() ;
 }
