bug 348901. Remove extra whitespace from source when exposing accessible text. r+sr=roc, r=surkov. a=dbaron
authoraaronleventhal@moonset.net
Fri, 03 Aug 2007 18:12:24 -0700
changeset 4261 325cd0ae2d87df9986c6141d91a34357d38498da
parent 4260 dcb1a926902327a9d185775e82333a2e705fb681
child 4262 63948f1270e7b8486a764a47ac6da58322b50d9d
push idunknown
push userunknown
push dateunknown
reviewerssurkov, dbaron
bugs348901
milestone1.9a8pre
bug 348901. Remove extra whitespace from source when exposing accessible text. r+sr=roc, r=surkov. a=dbaron
accessible/build/Makefile.in
accessible/public/nsPIAccessible.idl
accessible/src/base/nsAccessible.cpp
accessible/src/base/nsAccessible.h
accessible/src/base/nsDocAccessible.cpp
accessible/src/base/nsTextAccessible.cpp
accessible/src/base/nsTextAccessible.h
accessible/src/html/Makefile.in
accessible/src/html/nsHTMLTextAccessible.cpp
accessible/src/html/nsHTMLTextAccessible.h
accessible/src/html/nsHyperTextAccessible.cpp
accessible/src/html/nsHyperTextAccessible.h
layout/generic/nsIFrame.h
layout/generic/nsTextFrameThebes.cpp
--- a/accessible/build/Makefile.in
+++ b/accessible/build/Makefile.in
@@ -74,16 +74,17 @@ SHARED_LIBRARY_LIBS += ../src/xul/$(LIB_
 endif
 
 ifndef DISABLE_XFORMS_HOOKS
 SHARED_LIBRARY_LIBS += ../src/xforms/$(LIB_PREFIX)accessibility_xforms_s.$(LIB_SUFFIX)
 endif
 
 EXTRA_DSO_LIBS = \
 	gkgfx \
+	thebes \
 	$(NULL)
 
 EXTRA_DSO_LDOPTS =           \
 	$(LIBS_DIR)              \
 	$(EXTRA_DSO_LIBS)        \
 	$(MOZ_UNICHARUTIL_LIBS)  \
 	$(MOZ_COMPONENT_LIBS)    \
 	$(NULL)
--- a/accessible/public/nsPIAccessible.idl
+++ b/accessible/public/nsPIAccessible.idl
@@ -36,17 +36,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 
 interface nsIAccessible;
 interface nsIAccessibleEvent;
 
-[uuid(817ae493-b238-4fbc-a623-d20ed81eebcd)]
+[uuid(03932812-53d1-4dc7-965d-6b6ad8a872b1)]
 interface nsPIAccessible : nsISupports
 {
   /**
    * Set accessible parent.
    */
   void setParent(in nsIAccessible aAccParent);
 
   /**
@@ -95,11 +95,12 @@ interface nsPIAccessible : nsISupports
    * Assert if child not in parent's cache.
    */
   void testChildCache(in nsIAccessible aCachedChild);
 
   /**
    * Returns text of accessible if accessible has text role otherwise empty
    * string.
    */
-  AString getContentText();
+  void appendTextTo(out AString aString, in unsigned long aStartOffset,
+                    in unsigned long aLength);
 };
 
--- a/accessible/src/base/nsAccessible.cpp
+++ b/accessible/src/base/nsAccessible.cpp
@@ -1514,22 +1514,23 @@ nsresult nsAccessible::AppendFlatStringF
           isHTMLBlock = PR_TRUE;
           if (!aFlatString->IsEmpty()) {
             aFlatString->Append(PRUnichar(' '));
           }
         }
       }
     }
     if (aContent->TextLength() > 0) {
-      nsAutoString text;
-      aContent->AppendTextTo(text);
-      if (!text.IsEmpty())
-        aFlatString->Append(text);
-      if (isHTMLBlock && !aFlatString->IsEmpty())
+      nsIFrame *frame = shell->GetPrimaryFrameFor(aContent);
+      NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+      nsresult rv = frame->GetRenderedText(aFlatString);
+      NS_ENSURE_SUCCESS(rv, rv);
+      if (isHTMLBlock && !aFlatString->IsEmpty()) {
         aFlatString->Append(PRUnichar(' '));
+      }
     }
     return NS_OK;
   }
 
   nsAutoString textEquivalent;
   if (!aContent->IsNodeOfType(nsINode::eHTML)) {
     if (aContent->IsNodeOfType(nsINode::eXUL)) {
       nsCOMPtr<nsIPresShell> shell = GetPresShell();
@@ -3142,28 +3143,44 @@ nsresult nsAccessible::GetLinkOffset(PRI
   return NS_ERROR_FAILURE;
 }
 
 PRInt32 nsAccessible::TextLength(nsIAccessible *aAccessible)
 {
   if (!IsText(aAccessible))
     return 1;
 
+  nsCOMPtr<nsPIAccessNode> pAccNode(do_QueryInterface(aAccessible));
+  NS_ASSERTION(pAccNode, "QI to nsPIAccessNode failed");
+
+  nsIFrame *frame = pAccNode->GetFrame();
+  if (frame) { // Optimal way to get the text length -- no string copy
+    nsIContent *content = frame->GetContent();
+    if (content) {
+      PRUint32 length;
+      nsresult rv = nsHyperTextAccessible::ContentToRenderedOffset(frame, content->TextLength(), &length);
+      return NS_SUCCEEDED(rv) ? length : -1;
+    }
+  }
+
+  // For list bullets (or anything other accessible which would compute its own text
+  // They don't have their own frame.
+  // XXX In the future, list bullets may have frame and anon content, so 
+  // we should be able to remove this at that point
   nsCOMPtr<nsPIAccessible> pAcc(do_QueryInterface(aAccessible));
-  NS_ENSURE_TRUE(pAcc, NS_ERROR_FAILURE);
+  NS_ASSERTION(pAcc, "QI to nsPIAccessible failed");
 
   nsAutoString text;
-  pAcc->GetContentText(text);
+  pAcc->AppendTextTo(text, 0, PR_UINT32_MAX); // Get all the text
   return text.Length();
 }
 
 NS_IMETHODIMP
-nsAccessible::GetContentText(nsAString& aText)
+nsAccessible::AppendTextTo(nsAString& aText, PRUint32 aStartOffset, PRUint32 aLength)
 {
-  aText.Truncate();
   return NS_OK;
 }
 
 already_AddRefed<nsIAccessible>
 nsAccessible::GetFirstAvailableAccessible(nsIDOMNode *aStartNode, PRBool aRequireLeaf)
 {
   nsIAccessibilityService *accService = GetAccService();
   nsCOMPtr<nsIAccessible> accessible;
--- a/accessible/src/base/nsAccessible.h
+++ b/accessible/src/base/nsAccessible.h
@@ -144,17 +144,17 @@ public:
   static PRBool IsTextInterfaceSupportCorrect(nsIAccessible *aAccessible);
 #endif
 
   static PRBool IsCorrectFrameType(nsIFrame* aFrame, nsIAtom* aAtom);
   static PRUint32 State(nsIAccessible *aAcc) { PRUint32 state; aAcc->GetFinalState(&state, nsnull); return state; }
   static PRUint32 Role(nsIAccessible *aAcc) { PRUint32 role; aAcc->GetFinalRole(&role); return role; }
   static PRBool IsText(nsIAccessible *aAcc) { PRUint32 role = Role(aAcc); return role == nsIAccessibleRole::ROLE_TEXT_LEAF || role == nsIAccessibleRole::ROLE_STATICTEXT; }
   static PRBool IsEmbeddedObject(nsIAccessible *aAcc) { PRUint32 role = Role(aAcc); return role != nsIAccessibleRole::ROLE_TEXT_LEAF && role != nsIAccessibleRole::ROLE_WHITESPACE && role != nsIAccessibleRole::ROLE_STATICTEXT; }
-  static PRInt32 TextLength(nsIAccessible *aAccessible);
+  static PRInt32 TextLength(nsIAccessible *aAccessible); // Returns -1 on failure
   static PRBool IsLeaf(nsIAccessible *aAcc) { PRInt32 numChildren; aAcc->GetChildCount(&numChildren); return numChildren > 0; }
   static PRBool IsNodeRelevant(nsIDOMNode *aNode); // Is node something that could have an attached accessible
   
   already_AddRefed<nsIAccessible> GetParent() {
     nsIAccessible *parent = nsnull;
     GetParent(&parent);
     return parent;
   }
--- a/accessible/src/base/nsDocAccessible.cpp
+++ b/accessible/src/base/nsDocAccessible.cpp
@@ -1203,17 +1203,17 @@ nsDocAccessible::FireTextChangedEventOnD
     return;
 
   PRInt32 start = aInfo->mChangeStart;
   PRUint32 end = aInfo->mChangeEnd;
   PRInt32 length = end - start;
   PRUint32 replaceLen = aInfo->mReplaceLength;
 
   PRInt32 offset = 0;
-  rv = textAccessible->DOMPointToOffset(node, start, &offset);
+  rv = textAccessible->DOMPointToHypertextOffset(node, start, &offset);
   if (NS_FAILED(rv))
     return;
 
   // Text has been removed.
   if (length > 0) {
     nsCOMPtr<nsIAccessibleTextChangeEvent> event =
       new nsAccTextChangeEvent(accessible, offset, length, PR_FALSE);
     textAccessible->FireAccessibleEvent(event);
@@ -1264,17 +1264,17 @@ nsDocAccessible::FireTextChangedEventOnD
     }
   }
 
   nsCOMPtr<nsIDOMNode> parentNode(do_QueryInterface(aContainer));
   if (!parentNode)
     return;
 
   PRInt32 offset = 0;
-  rv = textAccessible->DOMPointToOffset(parentNode, aIndexInContainer, &offset);
+  rv = textAccessible->DOMPointToHypertextOffset(parentNode, aIndexInContainer, &offset);
   if (NS_FAILED(rv))
     return;
 
   nsCOMPtr<nsIAccessibleTextChangeEvent> event =
     new nsAccTextChangeEvent(accessible, offset, length, PR_TRUE);
   if (!event)
     return;
 
@@ -1318,17 +1318,17 @@ nsDocAccessible::FireTextChangedEventOnD
     }
   }
 
   nsCOMPtr<nsIDOMNode> parentNode(do_QueryInterface(aContainer));
   if (!parentNode)
     return;
 
   PRInt32 offset = 0;
-  rv = textAccessible->DOMPointToOffset(parentNode, aIndexInContainer, &offset);
+  rv = textAccessible->DOMPointToHypertextOffset(parentNode, aIndexInContainer, &offset);
   if (NS_FAILED(rv))
     return;
 
   nsCOMPtr<nsIAccessibleTextChangeEvent> event =
     new nsAccTextChangeEvent(accessible, offset, length, PR_FALSE);
   if (!event)
     return;
 
--- a/accessible/src/base/nsTextAccessible.cpp
+++ b/accessible/src/base/nsTextAccessible.cpp
@@ -84,21 +84,16 @@ NS_IMETHODIMP nsTextAccessible::GetLastC
   */
 NS_IMETHODIMP nsTextAccessible::GetChildCount(PRInt32 *_retval)
 {
   *_retval = 0;
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsTextAccessible::GetContentText(nsAString& aText)
+nsTextAccessible::AppendTextTo(nsAString& aText, PRUint32 aStartOffset, PRUint32 aLength)
 {
-  nsresult rv = nsLinkableAccessible::GetContentText(aText);
-  NS_ENSURE_SUCCESS(rv, rv);
-
   nsIFrame *frame = GetFrame();
-  if (!frame)
-    return NS_OK;
+  NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
 
-  frame->GetContent()->AppendTextTo(aText);
-  return NS_OK;
+  return frame->GetRenderedText(&aText, nsnull, nsnull, aStartOffset, aLength);
 }
 
--- a/accessible/src/base/nsTextAccessible.h
+++ b/accessible/src/base/nsTextAccessible.h
@@ -55,14 +55,14 @@ public:
 
   // nsIAccessible
   NS_IMETHOD GetRole(PRUint32 *_retval); 
   NS_IMETHOD GetFirstChild(nsIAccessible **_retval);
   NS_IMETHOD GetLastChild(nsIAccessible **_retval);
   NS_IMETHOD GetChildCount(PRInt32 *_retval);
 
   // nsPIAccessible
-  NS_IMETHOD GetContentText(nsAString& aText);
+  NS_IMETHOD AppendTextTo(nsAString& aText, PRUint32 aStartOffset, PRUint32 aLength);
 };
 
 
 #endif
 
--- a/accessible/src/html/Makefile.in
+++ b/accessible/src/html/Makefile.in
@@ -54,16 +54,17 @@ REQUIRES	= content \
 		  gfx \
 		  imglib2 \
 		  intl \
 		  js \
 		  layout \
 		  locale \
 		  necko \
 		  string \
+		  thebes \
 		  webshell \
 		  widget \
 		  xpcom \
 		  xpconnect \
 		  $(NULL)
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),gtk2)
 REQUIRES += editor
--- a/accessible/src/html/nsHTMLTextAccessible.cpp
+++ b/accessible/src/html/nsHTMLTextAccessible.cpp
@@ -51,34 +51,17 @@
 nsHTMLTextAccessible::nsHTMLTextAccessible(nsIDOMNode* aDomNode, nsIWeakReference* aShell):
 nsTextAccessibleWrap(aDomNode, aShell)
 { 
 }
 
 NS_IMETHODIMP nsHTMLTextAccessible::GetName(nsAString& aName)
 {
   aName.Truncate();
-  if (!mDOMNode) {
-    return NS_ERROR_FAILURE;
-  }
-
-  nsIFrame *frame = GetFrame();
-  NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
-
-  nsAutoString name;
-  nsresult rv = mDOMNode->GetNodeValue(name);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  if (!frame->GetStyleText()->WhiteSpaceIsSignificant()) {
-    // Replace \r\n\t in markup with space unless in this is preformatted text
-    // where those characters are significant
-    name.ReplaceChar("\r\n\t", ' ');
-  }
-  aName = name;
-  return rv;
+  return AppendTextTo(aName, 0, PR_UINT32_MAX);
 }
 
 NS_IMETHODIMP nsHTMLTextAccessible::GetRole(PRUint32 *aRole)
 {
   nsIFrame *frame = GetFrame();
   NS_ENSURE_TRUE(frame, NS_ERROR_NULL_POINTER);
 
   if (frame->IsGeneratedContentFrame()) {
@@ -362,19 +345,24 @@ nsHTMLListBulletAccessible::SetParent(ns
 NS_IMETHODIMP
 nsHTMLListBulletAccessible::GetParent(nsIAccessible **aParentAccessible)
 {
   NS_IF_ADDREF(*aParentAccessible = mWeakParent);
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsHTMLListBulletAccessible::GetContentText(nsAString& aText)
+nsHTMLListBulletAccessible::AppendTextTo(nsAString& aText, PRUint32 aStartOffset,
+                                         PRUint32 aLength)
 {
-  aText = mBulletText;
+  PRUint32 maxLength = mBulletText.Length() - aStartOffset;
+  if (aLength > maxLength) {
+    aLength = maxLength;
+  }
+  aText += nsDependentSubstring(mBulletText, aStartOffset, aLength);
   return NS_OK;
 }
 
 // nsHTMLListAccessible
 
 NS_IMETHODIMP
 nsHTMLListAccessible::GetState(PRUint32 *aState, PRUint32 *aExtraState)
 {
--- a/accessible/src/html/nsHTMLTextAccessible.h
+++ b/accessible/src/html/nsHTMLTextAccessible.h
@@ -106,17 +106,17 @@ public:
 
   // Don't cache via unique ID -- bullet accessible shares the same dom node as
   // this LI accessible. Also, don't cache via mParent/SetParent(), prevent
   // circular reference since li holds onto us.
   NS_IMETHOD SetParent(nsIAccessible *aParentAccessible);
   NS_IMETHOD GetParent(nsIAccessible **aParentAccessible);
 
   // nsPIAccessible
-  NS_IMETHOD GetContentText(nsAString& aText);
+  NS_IMETHOD AppendTextTo(nsAString& aText, PRUint32 aStartOffset, PRUint32 aLength);
 
 protected:
   // XXX: Ideally we'd get the bullet text directly from the bullet frame via
   // nsBulletFrame::GetListItemText(), but we'd need an interface for getting
   // text from contentless anonymous frames. Perhaps something like
   // nsIAnonymousFrame::GetText() ? However, in practice storing the bullet text
   // here should not be a problem if we invalidate the right parts of
   // the accessibility cache when mutation events occur.
--- a/accessible/src/html/nsHyperTextAccessible.cpp
+++ b/accessible/src/html/nsHyperTextAccessible.cpp
@@ -51,16 +51,17 @@
 #include "nsIDOMRange.h"
 #include "nsIDOMWindowInternal.h"
 #include "nsIDOMXULDocument.h"
 #include "nsIFontMetrics.h"
 #include "nsIFrame.h"
 #include "nsIPlaintextEditor.h"
 #include "nsIServiceManager.h"
 #include "nsTextFragment.h"
+#include "gfxSkipChars.h"
 
 static NS_DEFINE_IID(kRangeCID, NS_RANGE_CID);
 
 // ------------
 // nsHyperTextAccessible
 // ------------
 
 NS_IMPL_ADDREF_INHERITED(nsHyperTextAccessible, nsAccessible)
@@ -233,23 +234,34 @@ void nsHyperTextAccessible::CacheChildre
       walker.GetNextSibling();
       privatePrevAccessible->SetNextSibling(walker.mState.accessible);
     }
     mAccChildCount = childCount;
   }
 }
 
 // Substring must be entirely within the same text node
-nsIntRect nsHyperTextAccessible::GetBoundsForString(nsIFrame *aFrame, PRInt32 aStartOffset, PRInt32 aLength)
+nsIntRect nsHyperTextAccessible::GetBoundsForString(nsIFrame *aFrame, PRInt32 aStartContentOffset,
+                                                    PRInt32 aEndContentOffset)
 {
   nsIntRect screenRect;
+  NS_ENSURE_TRUE(aFrame, screenRect);
+
+  PRUint32 startRenderedOFfset, endRenderedOFfset;
+  nsresult rv = ContentToRenderedOffset(aFrame, aStartContentOffset, &startRenderedOFfset);
+  NS_ENSURE_SUCCESS(rv, screenRect);
+  rv = ContentToRenderedOffset(aFrame, aEndContentOffset, &endRenderedOFfset);
+  NS_ENSURE_SUCCESS(rv, screenRect);
+
   nsIFrame *frame;
-  PRInt32 startOffsetInFrame;
-  nsresult rv = aFrame->GetChildFrameContainingOffset(aStartOffset, PR_FALSE,
-                                                      &startOffsetInFrame, &frame);
+  PRInt32 startRenderedOFfsetInFrame;
+  // Get the right frame continuation -- not really a child, but a sibling of
+  // the primary frame passed in
+  rv = aFrame->GetChildFrameContainingOffset(startRenderedOFfset, PR_FALSE,
+                                             &startRenderedOFfsetInFrame, &frame);
   NS_ENSURE_SUCCESS(rv, screenRect);
 
   nsCOMPtr<nsIPresShell> shell = GetPresShell();
   NS_ENSURE_TRUE(shell, screenRect);
 
   nsCOMPtr<nsIRenderingContext> rc;
   shell->CreateRenderingContext(frame, getter_AddRefs(rc));
   NS_ENSURE_TRUE(rc, screenRect);
@@ -257,47 +269,47 @@ nsIntRect nsHyperTextAccessible::GetBoun
   const nsStyleFont *font = frame->GetStyleFont();
   const nsStyleVisibility *visibility = frame->GetStyleVisibility();
 
   rv = rc->SetFont(font->mFont, visibility->mLangGroup);
   NS_ENSURE_SUCCESS(rv, screenRect);
 
   nsPresContext *context = shell->GetPresContext();
 
-  while (frame && aLength > 0) {
+  while (frame && startRenderedOFfset < endRenderedOFfset) {
     // Start with this frame's screen rect, which we will 
     // shrink based on the substring we care about within it.
     // We will then add that frame to the total screenRect we
     // are returning.
     nsIntRect frameScreenRect = frame->GetScreenRectExternal();
 
     // Get the length of the substring in this frame that we want the bounds for
     PRInt32 startFrameTextOffset, endFrameTextOffset;
     frame->GetOffsets(startFrameTextOffset, endFrameTextOffset);
     PRInt32 frameTotalTextLength = endFrameTextOffset - startFrameTextOffset;
-    PRInt32 frameSubStringLength = PR_MIN(frameTotalTextLength - startOffsetInFrame, aLength);
+    PRInt32 seekLength = endRenderedOFfset - startRenderedOFfset;
+    PRInt32 frameSubStringLength = PR_MIN(frameTotalTextLength - startRenderedOFfsetInFrame, seekLength);
 
     // Add the point where the string starts to the frameScreenRect
     nsPoint frameTextStartPoint;
-    rv = frame->GetPointFromOffset(context, rc, aStartOffset, &frameTextStartPoint);
+    rv = frame->GetPointFromOffset(context, rc, startRenderedOFfset, &frameTextStartPoint);
     NS_ENSURE_SUCCESS(rv, nsRect());   
     frameScreenRect.x += context->AppUnitsToDevPixels(frameTextStartPoint.x);
 
     // Use the point for the end offset to calculate the width
     nsPoint frameTextEndPoint;
-    rv = frame->GetPointFromOffset(context, rc, aStartOffset + frameSubStringLength, &frameTextEndPoint);
+    rv = frame->GetPointFromOffset(context, rc, startRenderedOFfset + frameSubStringLength, &frameTextEndPoint);
     NS_ENSURE_SUCCESS(rv, nsRect());   
     frameScreenRect.width = context->AppUnitsToDevPixels(frameTextEndPoint.x - frameTextStartPoint.x);
 
     screenRect.UnionRect(frameScreenRect, screenRect);
 
     // Get ready to loop back for next frame continuation
-    aStartOffset += frameSubStringLength;
-    startOffsetInFrame = 0;
-    aLength -= frameSubStringLength;
+    startRenderedOFfset += frameSubStringLength;
+    startRenderedOFfsetInFrame = 0;
     frame = frame->GetNextContinuation();
   }
 
   return screenRect;
 }
 
 /*
  * Gets the specified text.
@@ -326,75 +338,80 @@ nsIFrame* nsHyperTextAccessible::GetPosA
   }
   if (aBoundsRect) {
     aBoundsRect->Empty();
   }
 
   nsIntRect unionRect;
   nsCOMPtr<nsIAccessible> accessible;
 
+  gfxSkipChars skipChars;
+  gfxSkipCharsIterator iter;
+
   // Loop through children and collect valid offsets, text and bounds
   // depending on what we need for out parameters
   while (NextChild(accessible)) {
     nsCOMPtr<nsPIAccessNode> accessNode(do_QueryInterface(accessible));
     nsIFrame *frame = accessNode->GetFrame();
     if (!frame) {
       continue;
     }
     if (IsText(accessible)) {
-      nsCOMPtr<nsPIAccessible> pAcc(do_QueryInterface(accessible));
-      nsAutoString newText;
-      pAcc->GetContentText(newText);
-
-      PRInt32 substringEndOffset = newText.Length();
+      // We only need info up to rendered offset -- that is what we're converting to content offset
+      PRInt32 substringEndOffset;
+      nsresult rv = frame->GetRenderedText(nsnull, &skipChars, &iter);
+      PRUint32 ourRenderedStart = iter.GetSkippedOffset();
+      PRInt32 ourContentStart = iter.GetOriginalOffset();
+      if (NS_SUCCEEDED(rv)) {
+        substringEndOffset = iter.ConvertOriginalToSkipped(skipChars.GetOriginalCharCount() + ourContentStart) -
+                    ourRenderedStart;
+      }
+      else {
+        // XXX for non-textframe text like list bullets, should go away after list bullet rewrite
+        substringEndOffset = TextLength(accessible);
+      }
       if (startOffset < substringEndOffset) {
         // Our start is within this substring
-        // XXX Can we somehow optimize further by getting the nsTextFragment
-        // and use CopyTo to a PRUnichar buffer to copy it directly to
-        // the string?
-
         if (startOffset > 0 || endOffset < substringEndOffset) {
-          // XXX the Substring operation is efficient, but does the 
-          // reassignment to the original nsAutoString cause a copy?
+          // We don't want the whole string for this accessible
+          // Get out the continuing text frame with this offset
           PRInt32 outStartLineUnused;
-          frame->GetChildFrameContainingOffset(startOffset, PR_TRUE, &outStartLineUnused, &frame);
-          if (endOffset < substringEndOffset) {
-            // Don't take entire substring: stop before the end
-            substringEndOffset = endOffset;
-          }
-          if (aText) {
-            newText = Substring(newText, startOffset,
-                                substringEndOffset - startOffset);
-          }
+          PRInt32 contentOffset = iter.ConvertSkippedToOriginal(startOffset) + ourRenderedStart - ourContentStart;
+          frame->GetChildFrameContainingOffset(contentOffset, PR_TRUE, &outStartLineUnused, &frame);
           if (aEndFrame) {
             *aEndFrame = frame; // We ended in the current frame
           }
+          if (substringEndOffset > endOffset) {
+            // Need to stop before the end of the available text
+            substringEndOffset = endOffset;
+          }
           aEndOffset = endOffset;
         }
         if (aText) {
-          if (!frame->GetStyleText()->WhiteSpaceIsSignificant()) {
-            // Replace \r\n\t in markup with space unless in this is
-            // preformatted text  where those characters are significant
-            newText.ReplaceChar("\r\n\t", ' ');
-          }
-          *aText += newText;
+          nsCOMPtr<nsPIAccessible> pAcc(do_QueryInterface(accessible));
+          pAcc->AppendTextTo(*aText, startOffset, substringEndOffset - startOffset);
         }
-        if (aBoundsRect) {
+        if (aBoundsRect) {    // Caller wants the bounds of the text
           aBoundsRect->UnionRect(*aBoundsRect, GetBoundsForString(frame, startOffset,
-                                                                  substringEndOffset - startOffset));
+                                 substringEndOffset));
         }
         if (!startFrame) {
           startFrame = frame;
           aStartOffset = startOffset;
         }
+        // We already started copying in this accessible's string,
+        // for the next accessible we'll start at offset 0
         startOffset = 0;
       }
       else {
+        // We have not found the start position yet, get the new startOffset
+        // that is relative to next accessible
         startOffset -= substringEndOffset;
       }
+      // The endOffset needs to be relative to the new startOffset
       endOffset -= substringEndOffset;
     }
     else {
       // Embedded object, append marker
       // XXX Append \n for <br>'s
       if (startOffset >= 1) {
         -- startOffset;
       }
@@ -443,74 +460,88 @@ NS_IMETHODIMP nsHyperTextAccessible::Get
   *aCharacterCount = 0;
   if (!mDOMNode) {
     return NS_ERROR_FAILURE;
   }
 
   nsCOMPtr<nsIAccessible> accessible;
 
   while (NextChild(accessible)) {
-    *aCharacterCount += TextLength(accessible);
+    PRInt32 textLength = TextLength(accessible);
+    NS_ENSURE_TRUE(textLength >= 0, nsnull);
+    *aCharacterCount += textLength;
   }
   return NS_OK;
 }
 
 /*
  * Gets the specified character.
  */
 NS_IMETHODIMP nsHyperTextAccessible::GetCharacterAtOffset(PRInt32 aOffset, PRUnichar *aCharacter)
 {
   if (!mDOMNode) {
     return NS_ERROR_FAILURE;
   }
   nsAutoString text;
   nsresult rv = GetText(aOffset, aOffset + 1, text);
   NS_ENSURE_SUCCESS(rv, rv);
+  if (text.IsEmpty()) {
+    return NS_ERROR_FAILURE;
+  }
   *aCharacter = text.First();
   return NS_OK;
 }
 
-nsresult nsHyperTextAccessible::DOMPointToOffset(nsIDOMNode* aNode, PRInt32 aNodeOffset, PRInt32* aResult,
-                                                 nsIAccessible **aFinalAccessible)
+nsresult nsHyperTextAccessible::DOMPointToHypertextOffset(nsIDOMNode* aNode, PRInt32 aNodeOffset,
+                                                          PRInt32* aHyperTextOffset,
+                                                          nsIAccessible **aFinalAccessible)
 {
   // Turn a DOM Node and offset into an offset into this hypertext.
   // On failure, return null. On success, return the DOM node which contains the offset.
-  NS_ENSURE_ARG_POINTER(aResult);
-  *aResult = 0;
+  NS_ENSURE_ARG_POINTER(aHyperTextOffset);
+  *aHyperTextOffset = 0;
   NS_ENSURE_ARG_POINTER(aNode);
   NS_ENSURE_TRUE(aNodeOffset >= 0, NS_ERROR_INVALID_ARG);
   if (aFinalAccessible) {
     *aFinalAccessible = nsnull;
   }
 
-  PRInt32 addTextOffset = 0;
+  PRUint32 addTextOffset = 0;
   nsCOMPtr<nsIDOMNode> findNode;
 
   unsigned short nodeType;
   aNode->GetNodeType(&nodeType);
   if (nodeType == nsIDOMNode::TEXT_NODE) {
     // For text nodes, aNodeOffset comes in as a character offset
     // Text offset will be added at the end, if we find the offset in this hypertext
-    addTextOffset = aNodeOffset;
+    // We want the "skipped" offset into the text (rendered text without the extra whitespace)
+    nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
+    NS_ASSERTION(content, "No nsIContent for dom node");
+    nsCOMPtr<nsIPresShell> presShell = GetPresShell();
+    NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
+    nsIFrame *frame = presShell->GetPrimaryFrameFor(content);
+    NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+    nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &addTextOffset);
+    NS_ENSURE_SUCCESS(rv, rv);
     // Get the child node and 
     findNode = aNode;
   }
   else {
     // For non-text nodes, aNodeOffset comes in as a child node index
     nsCOMPtr<nsIContent> parentContent(do_QueryInterface(aNode));
     // Should not happen, but better to protect against crash if doc node is somehow passed in
     NS_ENSURE_TRUE(parentContent, NS_ERROR_FAILURE);
     // findNode could be null if aNodeOffset == # of child nodes, which means one of two things:
     // 1) we're at the end of the children, keep findNode = null, so that we get the last possible offset
     // 2) there are no children, use parentContent for the node to find. In this case parentContent can't be
     //    the nsIAccessibleText, because an accesible text must have children
      
     findNode = do_QueryInterface(parentContent->GetChildAt(aNodeOffset));
     if (!findNode && !aNodeOffset) {
-      NS_ASSERTION(!SameCOMIdentity(parentContent, mDOMNode), "Cannot find child for DOMPointToOffset search");
+      NS_ASSERTION(!SameCOMIdentity(parentContent, mDOMNode), "Cannot find child for DOMPointToHypertextOffset search");
       findNode = do_QueryInterface(parentContent); // Case #2: there are no children
     }
   }
 
   // Get accessible for this findNode, or if that node isn't accessible, use the
   // accessible for the next DOM node which has one (based on forward depth first search)
   nsCOMPtr<nsIAccessible> descendantAccessible;
   if (findNode) {
@@ -538,22 +569,24 @@ nsresult nsHyperTextAccessible::DOMPoint
   }  
 
   // Loop through, adding offsets until we reach childAccessible
   // If childAccessible is null we will end up adding up the entire length of
   // the hypertext, which is good -- it just means our offset node
   // came after the last accessible child's node
   nsCOMPtr<nsIAccessible> accessible;
   while (NextChild(accessible) && accessible != childAccessible) {
-    *aResult += TextLength(accessible);
+    PRInt32 textLength = TextLength(accessible);
+    NS_ENSURE_TRUE(textLength >= 0, nsnull);
+    *aHyperTextOffset += textLength;
   }
   if (accessible) {
-    *aResult += addTextOffset;
+    *aHyperTextOffset += addTextOffset;
     NS_ASSERTION(accessible == childAccessible, "These should be equal whenever we exit loop and accessible != nsnull");
-    if (aFinalAccessible && (NextChild(accessible) || addTextOffset < TextLength(childAccessible))) {  
+    if (aFinalAccessible && (NextChild(accessible) || static_cast<PRInt32>(addTextOffset) < TextLength(childAccessible))) {  
       // If not at end of last text node, we will return the accessible we were in
       NS_ADDREF(*aFinalAccessible = childAccessible);
     }
   }
   return NS_OK;
 }
 
 PRInt32 nsHyperTextAccessible::GetRelativeOffset(nsIPresShell *aPresShell, nsIFrame *aFromFrame, PRInt32 aFromOffset,
@@ -566,43 +599,48 @@ PRInt32 nsHyperTextAccessible::GetRelati
 
   EWordMovementType wordMovementType = aNeedsStart ? eStartWord : eEndWord;
   if (aAmount == eSelectLine) {
     aAmount = (aDirection == eDirNext) ? eSelectEndLine : eSelectBeginLine;
   }
 
   // Ask layout for the new node and offset, after moving the appropriate amount
   nsPeekOffsetStruct pos;
-  pos.SetData(aAmount, aDirection, aFromOffset, 0, kIsJumpLinesOk,
-              kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi,
+
+  PRInt32 contentOffset;
+  nsresult rv = RenderedToContentOffset(aFromFrame, aFromOffset, &contentOffset);
+  NS_ENSURE_SUCCESS(rv, -1);
+
+  pos.SetData(aAmount, aDirection, contentOffset,
+              0, kIsJumpLinesOk, kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi,
               wordMovementType);
-  nsresult rv = aFromFrame->PeekOffset(&pos);
+  rv = aFromFrame->PeekOffset(&pos);
   if (NS_FAILED(rv)) {
     if (aDirection == eDirPrevious) {
       // Use passed-in frame as starting point in failure case for now,
       // this is a hack to deal with starting on a list bullet frame,
       // which fails in PeekOffset() because the line iterator doesn't see it.
       // XXX Need to look at our overall handling of list bullets, which are an odd case
       pos.mResultContent = aFromFrame->GetContent();
       PRInt32 endOffsetUnused;
       aFromFrame->GetOffsets(pos.mContentOffset, endOffsetUnused);
     }
     else {
-      return rv;
+      return -1;
     }
   }
 
   // Turn the resulting node and offset into a hyperTextOffset
   PRInt32 hyperTextOffset;
   nsCOMPtr<nsIDOMNode> resultNode = do_QueryInterface(pos.mResultContent);
   NS_ENSURE_TRUE(resultNode, -1);
 
   nsCOMPtr<nsIAccessible> finalAccessible;
-  rv = DOMPointToOffset(resultNode, pos.mContentOffset, &hyperTextOffset, getter_AddRefs(finalAccessible));
-  // If finalAccessible == nsnull, then DOMPointToOffset() searched through the hypertext
+  rv = DOMPointToHypertextOffset(resultNode, pos.mContentOffset, &hyperTextOffset, getter_AddRefs(finalAccessible));
+  // If finalAccessible == nsnull, then DOMPointToHypertextOffset() searched through the hypertext
   // children without finding the node/offset position
   NS_ENSURE_SUCCESS(rv, -1);
 
   if (!finalAccessible && aDirection == eDirPrevious) {
     // If we reached the end during search, this means we didn't find the DOM point
     // and we're actually at the start of the paragraph
     hyperTextOffset = 0;
   }  
@@ -708,17 +746,21 @@ nsresult nsHyperTextAccessible::GetTextH
     {
       // XXX We should merge identically formatted frames
       // XXX deal with static text case
       // XXX deal with boundary type
       nsIContent *textContent = startFrame->GetContent();
       // If not text, then it's represented by an embedded object char 
       // (length of 1)
       // XXX did this mean to check for eTEXT?
+      // XXX This is completely wrong, needs to be reimplemented
       PRInt32 textLength = textContent ? textContent->TextLength() : 1;
+      if (textLength < 0) {
+        return NS_ERROR_FAILURE;
+      }
       *aStartOffset = aOffset - startOffset;
       *aEndOffset = *aStartOffset + textLength;
       startOffset = *aStartOffset;
       endOffset = *aEndOffset;
       return GetText(startOffset, endOffset, aText);
     }
   default:  // Note, sentence support is deprecated and falls through to here
     return NS_ERROR_INVALID_ARG;
@@ -807,16 +849,17 @@ NS_IMETHODIMP nsHyperTextAccessible::Get
   if (!mDOMNode) {
     return NS_ERROR_FAILURE;
   }
 
   nsCOMPtr<nsIAccessible> accessible;
   
   while (NextChild(accessible)) {
     PRInt32 length = TextLength(accessible);
+    NS_ENSURE_TRUE(length >= 0, NS_ERROR_FAILURE);
     if (*aRangeStartOffset + length > aOffset) {
       *aRangeEndOffset = *aRangeStartOffset + length;
       NS_ADDREF(*aAccessibleWithAttrs = accessible);
       return NS_OK;
     }
     *aRangeStartOffset += length;
   }
 
@@ -988,24 +1031,29 @@ nsHyperTextAccessible::GetOffsetAtPoint(
       nsSize frameSize = frame->GetSize();
       if (pointInFrame.x < frameSize.width && pointInFrame.y < frameSize.height) {
         // Finished
         if (IsText(accessible)) {
           nsIFrame::ContentOffsets contentOffsets = frame->GetContentOffsetsFromPointExternal(pointInFrame, PR_TRUE);
           if (contentOffsets.IsNull() || contentOffsets.content != content) {
             return NS_OK; // Not found, will return -1
           }
-          offset += contentOffsets.offset;
+          PRUint32 addToOffset;
+          nsresult rv = ContentToRenderedOffset(frame, contentOffsets.offset, &addToOffset);
+          NS_ENSURE_SUCCESS(rv, rv);
+          offset += addToOffset;
         }
         *aOffset = offset;
         return NS_OK;
       }
       frame = frame->GetNextContinuation();
     }
-    offset += TextLength(accessible);
+    PRInt32 textLength = TextLength(accessible);
+    NS_ENSURE_TRUE(textLength >= 0, NS_ERROR_FAILURE);
+    offset += textLength;
   }
 
   return NS_OK; // Not found, will return -1
 }
 
 // ------- nsIAccessibleHyperText ---------------
 NS_IMETHODIMP nsHyperTextAccessible::GetLinks(PRInt32 *aLinks)
 {
@@ -1054,17 +1102,19 @@ NS_IMETHODIMP nsHyperTextAccessible::Get
   }
 
   nsCOMPtr<nsIAccessible> accessible;
 
   while (NextChild(accessible) && characterCount <= aCharIndex) {
     PRUint32 role = Role(accessible);
     if (role == nsIAccessibleRole::ROLE_TEXT_LEAF ||
         role == nsIAccessibleRole::ROLE_STATICTEXT) {
-      characterCount += TextLength(accessible);
+      PRInt32 textLength = TextLength(accessible);
+      NS_ENSURE_TRUE(textLength >= 0, NS_ERROR_FAILURE);
+      characterCount += textLength;
     }
     else {
       if (characterCount ++ == aCharIndex) {
         *aLinkIndex = linkIndex;
         break;
       }
       if (role != nsIAccessibleRole::ROLE_WHITESPACE) {
         ++ linkIndex;
@@ -1203,17 +1253,17 @@ NS_IMETHODIMP nsHyperTextAccessible::Get
 
   nsCOMPtr<nsIDOMNode> caretNode;
   rv = domSel->GetFocusNode(getter_AddRefs(caretNode));
   NS_ENSURE_SUCCESS(rv, rv);
 
   PRInt32 caretOffset;
   domSel->GetFocusOffset(&caretOffset);
 
-  return DOMPointToOffset(caretNode, caretOffset, aCaretOffset);
+  return DOMPointToHypertextOffset(caretNode, caretOffset, aCaretOffset);
 }
 
 nsresult nsHyperTextAccessible::GetSelections(nsISelectionController **aSelCon, nsISelection **aDomSel)
 {
   if (aSelCon) {
     *aSelCon = nsnull;
   }
   if (aDomSel) {
@@ -1296,29 +1346,29 @@ NS_IMETHODIMP nsHyperTextAccessible::Get
   nsCOMPtr<nsIDOMRange> range;
   rv = domSel->GetRangeAt(aSelectionNum, getter_AddRefs(range));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIDOMNode> startNode;
   range->GetStartContainer(getter_AddRefs(startNode));
   PRInt32 startOffset;
   range->GetStartOffset(&startOffset);
-  rv = DOMPointToOffset(startNode, startOffset, aStartOffset);
+  rv = DOMPointToHypertextOffset(startNode, startOffset, aStartOffset);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIDOMNode> endNode;
   range->GetEndContainer(getter_AddRefs(endNode));
   PRInt32 endOffset;
   range->GetEndOffset(&endOffset);
   if (startNode == endNode && startOffset == endOffset) {
     // Shortcut for collapsed selection case (caret)
     *aEndOffset = *aStartOffset;
     return NS_OK;
   }
-  return DOMPointToOffset(endNode, endOffset, aEndOffset);
+  return DOMPointToHypertextOffset(endNode, endOffset, aEndOffset);
 }
 
 /*
  * Changes the start and end offset of the specified selection.
  */
 NS_IMETHODIMP nsHyperTextAccessible::SetSelectionBounds(PRInt32 aSelectionNum, PRInt32 aStartOffset, PRInt32 aEndOffset)
 {
   nsCOMPtr<nsISelection> domSel;
@@ -1409,8 +1459,44 @@ NS_IMETHODIMP nsHyperTextAccessible::Rem
   if (aSelectionNum < 0 || aSelectionNum >= rangeCount)
     return NS_ERROR_INVALID_ARG;
 
   nsCOMPtr<nsIDOMRange> range;
   domSel->GetRangeAt(aSelectionNum, getter_AddRefs(range));
   return domSel->RemoveRange(range);
 }
 
+nsresult nsHyperTextAccessible::ContentToRenderedOffset(nsIFrame *aFrame, PRInt32 aContentOffset,
+                                                        PRUint32 *aRenderedOffset)
+{
+  gfxSkipChars skipChars;
+  gfxSkipCharsIterator iter;
+  // Only get info up to original ofset, we know that will be larger than skipped offset
+  nsresult rv = aFrame->GetRenderedText(nsnull, &skipChars, &iter, 0, aContentOffset);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  PRUint32 ourRenderedStart = iter.GetSkippedOffset();
+  PRInt32 ourContentStart = iter.GetOriginalOffset();
+
+  *aRenderedOffset = iter.ConvertOriginalToSkipped(aContentOffset + ourContentStart) -
+                    ourRenderedStart;
+
+  return NS_OK;
+}
+
+nsresult nsHyperTextAccessible::RenderedToContentOffset(nsIFrame *aFrame, PRUint32 aRenderedOffset,
+                                                        PRInt32 *aContentOffset)
+{
+  gfxSkipChars skipChars;
+  gfxSkipCharsIterator iter;
+  // We only need info up to skipped offset -- that is what we're converting to original offset
+  nsresult rv = aFrame->GetRenderedText(nsnull, &skipChars, &iter, 0, aRenderedOffset);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  PRUint32 ourRenderedStart = iter.GetSkippedOffset();
+  PRInt32 ourContentStart = iter.GetOriginalOffset();
+
+  *aContentOffset = iter.ConvertSkippedToOriginal(aRenderedOffset + ourRenderedStart) - ourContentStart;
+
+  return NS_OK;
+}
+
+
--- a/accessible/src/html/nsHyperTextAccessible.h
+++ b/accessible/src/html/nsHyperTextAccessible.h
@@ -81,34 +81,42 @@ public:
   NS_DECL_NSIACCESSIBLEEDITABLETEXT
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_HYPERTEXTACCESSIBLE_IMPL_CID)
 
   NS_IMETHOD GetRole(PRUint32 *aRole);
   NS_IMETHOD GetState(PRUint32 *aState, PRUint32 *aExtraState);
   virtual nsresult GetAttributesInternal(nsIPersistentProperties *aAttributes);
   void CacheChildren();
 
+  // Convert content offset to rendered text offset  
+  static nsresult ContentToRenderedOffset(nsIFrame *aFrame, PRInt32 aContentOffset,
+                                          PRUint32 *aRenderedOffset);
+  
+  // Convert rendered text offset to content offset
+  static nsresult RenderedToContentOffset(nsIFrame *aFrame, PRUint32 aRenderedOffset,
+                                          PRInt32 *aContentOffset);
+
   /**
     * Turn a DOM Node and offset into a character offset into this hypertext.
     * Will look for closest match when the DOM node does not have an accessible
     * object associated with it. Will return an offset for the end of
     * the string if the node is not found.
     *
     * @param aNode - the node to look for
     * @param aNodeOffset - the offset to look for
     * @param aResultOffset - the character offset into the current
     *                        nsHyperTextAccessible
     * @param aFinalAccessible [optional] - returns the accessible child which
     *                                      contained the offset, if it is within
     *                                      the current nsHyperTextAccessible,
     *                                      otherwise it is set to nsnull.
     */
-  nsresult DOMPointToOffset(nsIDOMNode* aNode, PRInt32 aNodeOffset,
-                            PRInt32 *aResultOffset,
-                            nsIAccessible **aFinalAccessible = nsnull);
+  nsresult DOMPointToHypertextOffset(nsIDOMNode* aNode, PRInt32 aNodeOffset,
+                                     PRInt32 *aHypertextOffset,
+                                     nsIAccessible **aFinalAccessible = nsnull);
 
 protected:
   PRBool IsHyperText();
 
   /*
    * This does the work for nsIAccessibleText::GetText[At|Before|After]Offset
    * @param aType, eGetBefore, eGetAt, eGetAfter
    * @param aBoundaryType, char/word-start/word-end/line-start/line-end/paragraph/attribute
@@ -133,28 +141,28 @@ protected:
     * @return, the resulting offset into this hypertext
     */
   PRInt32 GetRelativeOffset(nsIPresShell *aPresShell, nsIFrame *aFromFrame, PRInt32 aFromOffset,
                             nsSelectionAmount aAmount, nsDirection aDirection, PRBool aNeedsStart);
   /**
     * Given a start offset and end offset, get substring information. Different info is returned depending
     * on what optional paramters are provided.
     * @param aStartOffset, the start offset into the hyper text. This is also an out parameter used to return
-    *                      the offset into the start frame's text content (start frame is the @return)
-    * @param aEndOffset, the endoffset into the hyper text. This is also an out parameter used to return
-    *                    the offset into the end frame's text content
+    *                      the offset into the start frame's rendered text content (start frame is the @return)
+    * @param aEndHyperOffset, the endoffset into the hyper text. This is also an out parameter used to return
+    *                    the offset into the end frame's rendered text content
     * @param aText (optional), return the substring's text
     * @param aEndFrame (optional), return the end frame for this substring
     * @param aBoundsRect (optional), return the bounds rectangle for this substring
     * @return the start frame for this substring
     */
   nsIFrame* GetPosAndText(PRInt32& aStartOffset, PRInt32& aEndOffset, nsAString *aText = nsnull,
                           nsIFrame **aEndFrame = nsnull, nsIntRect *aBoundsRect = nsnull);
 
-  nsIntRect GetBoundsForString(nsIFrame *aFrame, PRInt32 aStartOffset, PRInt32 aLength);
+  nsIntRect GetBoundsForString(nsIFrame *aFrame, PRInt32 aStartContentOffset, PRInt32 aEndContentOffset);
 
   // Editor helpers, subclasses of nsHyperTextAccessible may have editor
   virtual void SetEditor(nsIEditor *aEditor) { return; }
   virtual already_AddRefed<nsIEditor> GetEditor() { return nsnull; }
 
   // Selection helpers
   nsresult GetSelections(nsISelectionController **aSelCon, nsISelection **aDomSel);
   nsresult SetSelectionRange(PRInt32 aStartPos, PRInt32 aEndPos);
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -85,16 +85,18 @@ class nsISelectionController;
 class nsBoxLayoutState;
 class nsIBoxLayout;
 #ifdef ACCESSIBILITY
 class nsIAccessible;
 #endif
 class nsDisplayListBuilder;
 class nsDisplayListSet;
 class nsDisplayList;
+class gfxSkipChars;
+class gfxSkipCharsIterator;
 
 struct nsPeekOffsetStruct;
 struct nsPoint;
 struct nsRect;
 struct nsSize;
 struct nsMargin;
 
 typedef class nsIFrame nsIBox;
@@ -1394,16 +1396,38 @@ public:
   // Justification helper method that is used to remove trailing
   // whitespace before justification.
   NS_IMETHOD TrimTrailingWhiteSpace(nsPresContext* aPresContext,
                                     nsIRenderingContext& aRC,
                                     nscoord& aDeltaWidth,
                                     PRBool& aLastCharIsJustifiable) = 0;
 
   /**
+   * Append the rendered text to the passed-in string.
+   * The appended text will often not contain all the whitespace from source,
+   * depending on whether the CSS rule "white-space: pre" is active for this frame.
+   * if aStartOffset + aLength goes past end, or if aLength is not specified
+   * then use the text up to the string's end.
+   * Call this on the primary frame for a text node.
+   * @param aAppendToString   String to append text to, or null if text should not be returned
+   * @param aSkipChars         if aSkipIter is non-null, this must also be non-null.
+   * This gets used as backing data for the iterator so it should outlive the iterator.
+   * @param aSkipIter         Where to fill in the gfxSkipCharsIterator info, or null if not needed by caller
+   * @param aStartOffset       Skipped (rendered text) start offset
+   * @param aSkippedMaxLength  Maximum number of characters to return
+   * The iterator can be used to map content offsets to offsets in the returned string, or vice versa.
+   */
+  virtual nsresult GetRenderedText(nsAString* aAppendToString = nsnull,
+                                   gfxSkipChars* aSkipChars = nsnull,
+                                   gfxSkipCharsIterator* aSkipIter = nsnull,
+                                   PRUint32 aSkippedStartOffset = 0,
+                                   PRUint32 aSkippedMaxLength = PR_UINT32_MAX)
+  { return NS_ERROR_NOT_IMPLEMENTED; }
+
+  /**
    * Accessor functions to get/set the associated view object
    *
    * GetView returns non-null if and only if |HasView| returns true.
    */
   PRBool HasView() const { return mState & NS_FRAME_HAS_VIEW; }
   nsIView* GetView() const;
   virtual nsIView* GetViewExternal() const;
   nsresult SetView(nsIView* aView);
--- a/layout/generic/nsTextFrameThebes.cpp
+++ b/layout/generic/nsTextFrameThebes.cpp
@@ -76,16 +76,17 @@
 #include "nsLayoutUtils.h"
 #include "nsDisplayList.h"
 #include "nsFrame.h"
 #include "nsTextFrameUtils.h"
 #include "nsTextRunTransformations.h"
 #include "nsFrameManager.h"
 #include "nsTextFrameTextRunCache.h"
 #include "nsExpirationTracker.h"
+#include "nsICaseConversion.h"
 
 #include "nsTextFragment.h"
 #include "nsGkAtoms.h"
 #include "nsFrameSelection.h"
 #include "nsISelection.h"
 #include "nsIDOMRange.h"
 #include "nsILookAndFeel.h"
 #include "nsCSSRendering.h"
@@ -444,16 +445,21 @@ public:
                     nsHTMLReflowMetrics& aMetrics,
                     const nsHTMLReflowState& aReflowState,
                     nsReflowStatus& aStatus);
   virtual PRBool CanContinueTextRun() const;
   NS_IMETHOD TrimTrailingWhiteSpace(nsPresContext* aPresContext,
                                     nsIRenderingContext& aRC,
                                     nscoord& aDeltaWidth,
                                     PRBool& aLastCharIsJustifiable);
+  virtual nsresult GetRenderedText(nsAString* aString = nsnull,
+                                   gfxSkipChars* aSkipChars = nsnull,
+                                   gfxSkipCharsIterator* aSkipIter = nsnull,
+                                   PRUint32 aSkippedStartOffset = 0,
+                                   PRUint32 aSkippedMaxLength = PR_UINT32_MAX);
 
   void AddInlineMinWidthForFlow(nsIRenderingContext *aRenderingContext,
                                 nsIFrame::InlineMinWidthData *aData);
   void AddInlinePrefWidthForFlow(nsIRenderingContext *aRenderingContext,
                                  InlinePrefWidthData *aData);
 
   gfxFloat GetSnappedBaselineY(gfxContext* aContext, gfxFloat aY);
 
@@ -534,16 +540,17 @@ public:
   void SetTextRun(gfxTextRun* aTextRun) { mTextRun = aTextRun; }
 
   // Get the DOM content range mapped by this frame after excluding
   // whitespace subject to start-of-line and end-of-line trimming.
   // The textrun must have been created before calling this.
   struct TrimmedOffsets {
     PRInt32 mStart;
     PRInt32 mLength;
+    PRInt32 GetEnd() { return mStart + mLength; }
   };
   TrimmedOffsets GetTrimmedOffsets(const nsTextFragment* aFrag,
                                    PRBool aTrimAfter);
 
 protected:
   virtual ~nsTextFrame();
   
   nsIFrame*   mNextContinuation;
@@ -2043,17 +2050,17 @@ nsTextFrame::GetTrimmedOffsets(const nsT
       GetTrimmableWhitespaceCount(aFrag, offsets.mStart, offsets.mLength, 1);
     offsets.mStart += whitespaceCount;
     offsets.mLength -= whitespaceCount;
   }
 
   if (aTrimAfter && (GetStateBits() & TEXT_END_OF_LINE) &&
       textStyle->WhiteSpaceCanWrap()) {
     PRInt32 whitespaceCount =
-      GetTrimmableWhitespaceCount(aFrag, offsets.mStart + offsets.mLength - 1,
+      GetTrimmableWhitespaceCount(aFrag, offsets.GetEnd() - 1,
                                   offsets.mLength, -1);
     offsets.mLength -= whitespaceCount;
   }
   return offsets;
 }
 
 /*
  * Currently only Unicode characters below 0x10000 have their spacing modified
@@ -3255,16 +3262,23 @@ public:
   virtual nsIFrame* GetFirstInFlow() const;
   virtual nsIFrame* GetFirstContinuation() const;
 
   virtual void AddInlineMinWidth(nsIRenderingContext *aRenderingContext,
                                  InlineMinWidthData *aData);
   virtual void AddInlinePrefWidth(nsIRenderingContext *aRenderingContext,
                                   InlinePrefWidthData *aData);
   
+  virtual nsresult GetRenderedText(nsAString* aString = nsnull,
+                                   gfxSkipChars* aSkipChars = nsnull,
+                                   gfxSkipCharsIterator* aSkipIter = nsnull,
+                                   PRUint32 aSkippedStartOffset = 0,
+                                   PRUint32 aSkippedMaxLength = PR_UINT32_MAX)
+  { return NS_ERROR_NOT_IMPLEMENTED; } // Call on a primary text frame only
+
 protected:
   nsContinuingTextFrame(nsStyleContext* aContext) : nsTextFrame(aContext) {}
   nsIFrame* mPrevContinuation;
 };
 
 NS_IMETHODIMP
 nsContinuingTextFrame::Init(nsIContent* aContent,
                             nsIFrame*   aParent,
@@ -4584,17 +4598,17 @@ nsTextFrame::PeekOffsetNoAmount(PRBool a
   NS_ASSERTION(aOffset && *aOffset <= mContentLength, "aOffset out of range");
 
   gfxSkipCharsIterator iter = EnsureTextRun();
   if (!mTextRun)
     return PR_FALSE;
 
   TrimmedOffsets trimmed = GetTrimmedOffsets(mContent->GetText(), PR_TRUE);
   // Check whether there are nonskipped characters in the trimmmed range
-  return iter.ConvertOriginalToSkipped(trimmed.mStart + trimmed.mLength) >
+  return iter.ConvertOriginalToSkipped(trimmed.GetEnd()) >
          iter.ConvertOriginalToSkipped(trimmed.mStart);
 }
 
 /**
  * This class iterates through the clusters before or after the given
  * aPosition (which is a content offset). You can test each cluster
  * to see if it's whitespace (as far as selection/caret movement is concerned),
  * or punctuation, or if there is a word break before the cluster. ("Before"
@@ -4639,34 +4653,34 @@ nsTextFrame::PeekOffsetCharacter(PRBool 
 
   TrimmedOffsets trimmed = GetTrimmedOffsets(mContent->GetText(), PR_TRUE);
 
   // A negative offset means "end of frame".
   PRInt32 startOffset = mContentOffset + (*aOffset < 0 ? mContentLength : *aOffset);
 
   if (!aForward) {
     PRInt32 i;
-    for (i = PR_MIN(trimmed.mStart + trimmed.mLength, startOffset) - 1;
+    for (i = PR_MIN(trimmed.GetEnd(), startOffset) - 1;
          i >= trimmed.mStart; --i) {
       iter.SetOriginalOffset(i);
       if (!iter.IsOriginalCharSkipped() &&
           mTextRun->IsClusterStart(iter.GetSkippedOffset())) {
         *aOffset = i - mContentOffset;
         return PR_TRUE;
       }
     }
     *aOffset = 0;
   } else {
     PRInt32 i;
-    for (i = startOffset + 1; i <= trimmed.mStart + trimmed.mLength; ++i) {
+    for (i = startOffset + 1; i <= trimmed.GetEnd(); ++i) {
       iter.SetOriginalOffset(i);
       // XXX we can't necessarily stop at the end of this frame,
       // but we really have no choice right now. We need to do a deeper
       // fix/restructuring of PeekOffsetCharacter
-      if (i == trimmed.mStart + trimmed.mLength ||
+      if (i == trimmed.GetEnd() ||
           (!iter.IsOriginalCharSkipped() &&
            mTextRun->IsClusterStart(iter.GetSkippedOffset()))) {
         *aOffset = i - mContentOffset;
         return PR_TRUE;
       }
     }
     *aOffset = mContentLength;
   }
@@ -4712,32 +4726,32 @@ PRBool
 ClusterIterator::NextCluster()
 {
   if (!mDirection)
     return PR_FALSE;
   gfxTextRun* textRun = mTextFrame->GetTextRun();
 
   while (PR_TRUE) {
     if (mDirection > 0) {
-      if (mIterator.GetOriginalOffset() >= mTrimmed.mStart + mTrimmed.mLength)
+      if (mIterator.GetOriginalOffset() >= mTrimmed.GetEnd())
         return PR_FALSE;
       if (mIterator.IsOriginalCharSkipped() ||
           mIterator.GetOriginalOffset() < mTrimmed.mStart ||
           !textRun->IsClusterStart(mIterator.GetSkippedOffset())) {
         mIterator.AdvanceOriginal(1);
         continue;
       }
       mCharIndex = mIterator.GetOriginalOffset();
       mIterator.AdvanceOriginal(1);
     } else {
       if (mIterator.GetOriginalOffset() <= mTrimmed.mStart)
         return PR_FALSE;
       mIterator.AdvanceOriginal(-1);
       if (mIterator.IsOriginalCharSkipped() ||
-          mIterator.GetOriginalOffset() >= mTrimmed.mStart + mTrimmed.mLength ||
+          mIterator.GetOriginalOffset() >= mTrimmed.GetEnd() ||
           !textRun->IsClusterStart(mIterator.GetSkippedOffset()))
         continue;
       mCharIndex = mIterator.GetOriginalOffset();
     }
 
     return PR_TRUE;
   }
 }
@@ -5576,23 +5590,23 @@ nsTextFrame::TrimTrailingWhiteSpace(nsPr
   gfxSkipCharsIterator start = EnsureTextRun(&aRC);
   if (!mTextRun)
     return NS_ERROR_FAILURE;
   PRUint32 trimmedStart = start.GetSkippedOffset();
 
   const nsTextFragment* frag = mContent->GetText();
   TrimmedOffsets trimmed = GetTrimmedOffsets(frag, PR_TRUE);
   gfxSkipCharsIterator iter = start;
-  PRUint32 trimmedEnd = iter.ConvertOriginalToSkipped(trimmed.mStart + trimmed.mLength);
+  PRUint32 trimmedEnd = iter.ConvertOriginalToSkipped(trimmed.GetEnd());
   const nsStyleText* textStyle = GetStyleText();
   gfxFloat delta = 0;
 
   if (GetStateBits() & TEXT_TRIMMED_TRAILING_WHITESPACE) {
     aLastCharIsJustifiable = PR_TRUE;
-  } else if (trimmed.mStart + trimmed.mLength < mContentOffset + mContentLength) {
+  } else if (trimmed.GetEnd() < GetContentEnd()) {
     gfxSkipCharsIterator end = iter;
     PRUint32 endOffset = end.ConvertOriginalToSkipped(mContentOffset + mContentLength);
     if (trimmedEnd < endOffset) {
       // We can't be dealing with tabs here ... they wouldn't be trimmed. So it's
       // OK to pass null for the line container.
       PropertyProvider provider(mTextRun, textStyle, frag, this, start, mContentLength,
                                 nsnull, 0);
       delta = mTextRun->GetAdvanceWidth(trimmedEnd, endOffset - trimmedEnd, &provider);
@@ -5608,17 +5622,17 @@ nsTextFrame::TrimTrailingWhiteSpace(nsPr
     // Check if any character in the last cluster is justifiable
     PropertyProvider provider(mTextRun, textStyle, frag, this, start, mContentLength,
                               nsnull, 0);
     PRBool isCJK = IsChineseJapaneseLangGroup(this);
     gfxSkipCharsIterator justificationEnd(iter);
     provider.FindEndOfJustificationRange(&justificationEnd);
 
     PRInt32 i;
-    for (i = justificationEnd.GetOriginalOffset(); i < trimmed.mStart + trimmed.mLength; ++i) {
+    for (i = justificationEnd.GetOriginalOffset(); i < trimmed.GetEnd(); ++i) {
       if (IsJustifiableCharacter(frag, i, isCJK)) {
         aLastCharIsJustifiable = PR_TRUE;
       }
     }
   }
 
   gfxContext* ctx = static_cast<gfxContext*>
                                (aRC.GetNativeGraphicData(nsIRenderingContext::NATIVE_THEBES_CONTEXT));
@@ -5648,16 +5662,108 @@ nsTextFrame::TrimTrailingWhiteSpace(nsPr
 
 #ifdef NOISY_TRIM
   ListTag(stdout);
   printf(": trim => %d\n", aDeltaWidth);
 #endif
   return NS_OK;
 }
 
+static PRUnichar TransformChar(const nsStyleText* aStyle, gfxTextRun* aTextRun,
+                               PRUint32 aSkippedOffset, PRUnichar aChar)
+{
+  if (aChar == '\n' || aChar == '\r') {
+    return aStyle->WhiteSpaceIsSignificant() ? aChar : ' ';
+  }
+  switch (aStyle->mTextTransform) {
+  case NS_STYLE_TEXT_TRANSFORM_LOWERCASE:
+    nsContentUtils::GetCaseConv()->ToLower(aChar, &aChar);
+    break;
+  case NS_STYLE_TEXT_TRANSFORM_UPPERCASE:
+    nsContentUtils::GetCaseConv()->ToUpper(aChar, &aChar);
+    break;
+  case NS_STYLE_TEXT_TRANSFORM_CAPITALIZE:
+    if (aTextRun->CanBreakLineBefore(aSkippedOffset)) {
+      nsContentUtils::GetCaseConv()->ToTitle(aChar, &aChar);
+    }
+    break;
+  }
+
+  return aChar;
+}
+
+nsresult nsTextFrame::GetRenderedText(nsAString* aAppendToString,
+                                      gfxSkipChars* aSkipChars,
+                                      gfxSkipCharsIterator* aSkipIter,
+                                      PRUint32 aSkippedStartOffset,
+                                      PRUint32 aSkippedMaxLength)
+{
+  // The handling of aSkippedStartOffset and aSkippedMaxLength could be more efficient...
+  gfxSkipCharsBuilder skipCharsBuilder;
+  nsTextFrame* textFrame;
+  const nsTextFragment* textFrag = mContent->GetText();
+  PRInt32 keptCharsLength = 0;
+  PRInt32 validCharsLength = 0;
+
+  // Build skipChars and copy text, for each text frame in this continuation block
+  for (textFrame = this; textFrame;
+       textFrame = static_cast<nsTextFrame*>(textFrame->GetNextContinuation())) {
+    // For each text frame continuation in this block ...
+
+    // Ensure the text run and grab the gfxSkipCharsIterator for it
+    gfxSkipCharsIterator iter = textFrame->EnsureTextRun();
+    if (!textFrame->mTextRun)
+      return NS_ERROR_FAILURE;
+
+    // Skip to the start of the text run, past ignored chars at start of line
+    // XXX In the future we may decide to trim extra spaces before a hard line
+    // break, in which case we need to accurately detect those sitations and 
+    // call GetTrimmedOffsets() with PR_TRUE to trim whitespace at the line's end
+    TrimmedOffsets trimmedContentOffsets = textFrame->GetTrimmedOffsets(textFrag, PR_FALSE);
+    PRInt32 startOfLineSkipChars = trimmedContentOffsets.mStart - textFrame->mContentOffset;
+    if (startOfLineSkipChars > 0) {
+      skipCharsBuilder.SkipChars(startOfLineSkipChars);
+      iter.SetOriginalOffset(trimmedContentOffsets.mStart);
+    }
+
+    // Keep and copy the appropriate chars withing the caller's requested range
+    const nsStyleText* textStyle = textFrame->GetStyleText();
+    while (iter.GetOriginalOffset() < trimmedContentOffsets.GetEnd() &&
+           keptCharsLength < aSkippedMaxLength) {
+      // For each original char from content text
+      if (iter.IsOriginalCharSkipped() || ++ validCharsLength <= aSkippedStartOffset) {
+        skipCharsBuilder.SkipChar();
+      } else {
+        ++ keptCharsLength;
+        skipCharsBuilder.KeepChar();
+        if (aAppendToString) {
+          aAppendToString->Append(
+              TransformChar(textStyle, textFrame->mTextRun, iter.GetSkippedOffset(),
+                            textFrag->CharAt(iter.GetOriginalOffset())));
+        }
+      }
+      iter.AdvanceOriginal(1);
+    }
+    if (keptCharsLength >= aSkippedMaxLength) {
+      break; // Already past the end, don't build string or gfxSkipCharsIter anymore
+    }
+  }
+  
+  if (aSkipChars) {
+    aSkipChars->TakeFrom(&skipCharsBuilder); // Copy skipChars into aSkipChars
+    if (aSkipIter) {
+      // Caller must provide both pointers in order to retrieve a gfxSkipCharsIterator,
+      // because the gfxSkipCharsIterator holds a weak pointer to the gfxSkipCars.
+      *aSkipIter = gfxSkipCharsIterator(*aSkipChars, GetContentLength());
+    }
+  }
+
+  return NS_OK;
+}
+
 #ifdef DEBUG
 // Translate the mapped content into a string that's printable
 void
 nsTextFrame::ToCString(nsString& aBuf, PRInt32* aTotalContentLength) const
 {
   // Get the frames text content
   const nsTextFragment* frag = mContent->GetText();
   if (!frag) {