From 0bf48ef3be53ddaa52bbead65dfd75bf90e7a2b5 Mon Sep 17 00:00:00 2001 From: Ben Murdoch Date: Tue, 11 Aug 2009 17:01:47 +0100 Subject: Merge in WebKit r47029. --- WebCore/editing/TextIterator.cpp | 344 +++++++++++++++++++++++++++++---------- 1 file changed, 256 insertions(+), 88 deletions(-) (limited to 'WebCore/editing/TextIterator.cpp') diff --git a/WebCore/editing/TextIterator.cpp b/WebCore/editing/TextIterator.cpp index b311853..08fda16 100644 --- a/WebCore/editing/TextIterator.cpp +++ b/WebCore/editing/TextIterator.cpp @@ -42,6 +42,7 @@ #include "visible_units.h" #if USE(ICU_UNICODE) && !UCONFIG_NO_COLLATION +#include "TextBreakIteratorInternalICU.h" #include #endif @@ -56,7 +57,7 @@ using namespace HTMLNames; // Keeps enough of the previous text to be able to search in the future, but no more. // Non-breaking spaces are always equal to normal spaces. // Case folding is also done if is false. -class SearchBuffer : Noncopyable { +class SearchBuffer : public Noncopyable { public: SearchBuffer(const String& target, bool isCaseSensitive); ~SearchBuffer(); @@ -97,6 +98,148 @@ private: // -------- +static const unsigned bitsInWord = sizeof(unsigned) * 8; +static const unsigned bitInWordMask = bitsInWord - 1; + +BitStack::BitStack() + : m_size(0) +{ +} + +void BitStack::push(bool bit) +{ + unsigned index = m_size / bitsInWord; + unsigned shift = m_size & bitInWordMask; + if (!shift && index == m_words.size()) { + m_words.grow(index + 1); + m_words[index] = 0; + } + unsigned& word = m_words[index]; + unsigned mask = 1U << shift; + if (bit) + word |= mask; + else + word &= ~mask; + ++m_size; +} + +void BitStack::pop() +{ + if (m_size) + --m_size; +} + +bool BitStack::top() const +{ + if (!m_size) + return false; + unsigned shift = (m_size - 1) & bitInWordMask; + return m_words.last() & (1U << shift); +} + +unsigned BitStack::size() const +{ + return m_size; +} + +// -------- + +static inline Node* parentCrossingShadowBoundaries(Node* node) +{ + if (Node* parent = node->parentNode()) + return parent; + return node->shadowParentNode(); +} + +#ifndef NDEBUG + +static unsigned depthCrossingShadowBoundaries(Node* node) +{ + unsigned depth = 0; + for (Node* parent = parentCrossingShadowBoundaries(node); parent; parent = parentCrossingShadowBoundaries(parent)) + ++depth; + return depth; +} + +#endif + +// This function is like Range::pastLastNode, except for the fact that it can climb up out of shadow trees. +static Node* nextInPreOrderCrossingShadowBoundaries(Node* rangeEndContainer, int rangeEndOffset) +{ + if (!rangeEndContainer) + return 0; + if (rangeEndOffset >= 0 && !rangeEndContainer->offsetInCharacters()) { + if (Node* next = rangeEndContainer->childNode(rangeEndOffset)) + return next; + } + for (Node* node = rangeEndContainer; node; node = parentCrossingShadowBoundaries(node)) { + if (Node* next = node->nextSibling()) + return next; + } + return 0; +} + +static Node* previousInPostOrderCrossingShadowBoundaries(Node* rangeStartContainer, int rangeStartOffset) +{ + if (!rangeStartContainer) + return 0; + if (rangeStartOffset > 0 && !rangeStartContainer->offsetInCharacters()) { + if (Node* previous = rangeStartContainer->childNode(rangeStartOffset - 1)) + return previous; + } + for (Node* node = rangeStartContainer; node; node = parentCrossingShadowBoundaries(node)) { + if (Node* previous = node->previousSibling()) + return previous; + } + return 0; +} + +// -------- + +static inline bool fullyClipsContents(Node* node) +{ + RenderObject* renderer = node->renderer(); + if (!renderer || !renderer->isBox() || !renderer->hasOverflowClip()) + return false; + return toRenderBox(renderer)->size().isEmpty(); +} + +static inline bool ignoresContainerClip(Node* node) +{ + RenderObject* renderer = node->renderer(); + if (!renderer || renderer->isText()) + return false; + EPosition position = renderer->style()->position(); + return position == AbsolutePosition || position == FixedPosition; +} + +static void pushFullyClippedState(BitStack& stack, Node* node) +{ + ASSERT(stack.size() == depthCrossingShadowBoundaries(node)); + + // Push true if this node full clips its contents, or if a parent already has fully + // clipped and this is not a node that ignores its container's clip. + stack.push(fullyClipsContents(node) || stack.top() && !ignoresContainerClip(node)); +} + +static void setUpFullyClippedStack(BitStack& stack, Node* node) +{ + // Put the nodes in a vector so we can iterate in reverse order. + Vector ancestry; + for (Node* parent = parentCrossingShadowBoundaries(node); parent; parent = parentCrossingShadowBoundaries(parent)) + ancestry.append(parent); + + // Call pushFullyClippedState on each node starting with the earliest ancestor. + size_t size = ancestry.size(); + for (size_t i = 0; i < size; ++i) + pushFullyClippedState(stack, ancestry[size - i - 1]); + pushFullyClippedState(stack, node); + + ASSERT(stack.size() == 1 + depthCrossingShadowBoundaries(node)); +} + +// -------- + TextIterator::TextIterator() : m_startContainer(0) , m_startOffset(0) @@ -112,8 +255,7 @@ TextIterator::TextIterator() } TextIterator::TextIterator(const Range* r, bool emitCharactersBetweenAllVisiblePositions, bool enterTextControls) - : m_inShadowContent(false) - , m_startContainer(0) + : m_startContainer(0) , m_startOffset(0) , m_endContainer(0) , m_endOffset(0) @@ -144,23 +286,17 @@ TextIterator::TextIterator(const Range* r, bool emitCharactersBetweenAllVisibleP m_endContainer = endContainer; m_endOffset = endOffset; - for (Node* n = startContainer; n; n = n->parentNode()) { - if (n->isShadowNode()) { - m_inShadowContent = true; - break; - } - } - // set up the current node for processing m_node = r->firstNode(); - if (m_node == 0) + if (!m_node) return; + setUpFullyClippedStack(m_fullyClippedStack, m_node); m_offset = m_node == m_startContainer ? m_startOffset : 0; m_handledNode = false; m_handledChildren = false; // calculate first out of bounds node - m_pastEndNode = r->pastLastNode(); + m_pastEndNode = nextInPreOrderCrossingShadowBoundaries(endContainer, endOffset); // initialize node processing state m_needAnotherNewline = false; @@ -219,7 +355,7 @@ void TextIterator::advance() return; } - RenderObject *renderer = m_node->renderer(); + RenderObject* renderer = m_node->renderer(); if (!renderer) { m_handledNode = true; m_handledChildren = true; @@ -241,27 +377,20 @@ void TextIterator::advance() // find a new current node to handle in depth-first manner, // calling exitNode() as we come back thru a parent node - Node *next = m_handledChildren ? 0 : m_node->firstChild(); + Node* next = m_handledChildren ? 0 : m_node->firstChild(); m_offset = 0; if (!next) { next = m_node->nextSibling(); if (!next) { bool pastEnd = m_node->traverseNextNode() == m_pastEndNode; - Node* parentNode = m_node->parentNode(); - if (!parentNode && m_inShadowContent) { - m_inShadowContent = false; - parentNode = m_node->shadowParentNode(); - } + Node* parentNode = parentCrossingShadowBoundaries(m_node); while (!next && parentNode) { if ((pastEnd && parentNode == m_endContainer) || m_endContainer->isDescendantOf(parentNode)) return; bool haveRenderer = m_node->renderer(); m_node = parentNode; - parentNode = m_node->parentNode(); - if (!parentNode && m_inShadowContent) { - m_inShadowContent = false; - parentNode = m_node->shadowParentNode(); - } + m_fullyClippedStack.pop(); + parentNode = parentCrossingShadowBoundaries(m_node); if (haveRenderer) exitNode(); if (m_positionNode) { @@ -272,10 +401,13 @@ void TextIterator::advance() next = m_node->nextSibling(); } } + m_fullyClippedStack.pop(); } // set the new current node m_node = next; + if (m_node) + pushFullyClippedState(m_fullyClippedStack, m_node); m_handledNode = false; m_handledChildren = false; @@ -285,13 +417,16 @@ void TextIterator::advance() } } -static inline bool compareBoxStart(const InlineTextBox *first, const InlineTextBox *second) +static inline bool compareBoxStart(const InlineTextBox* first, const InlineTextBox* second) { return first->start() < second->start(); } bool TextIterator::handleTextNode() { + if (m_fullyClippedStack.top()) + return false; + RenderText* renderer = toRenderText(m_node->renderer()); if (renderer->style()->visibility() != VISIBLE) return false; @@ -325,7 +460,7 @@ bool TextIterator::handleTextNode() // Used when text boxes are out of order (Hebrew/Arabic w/ embeded LTR text) if (renderer->containsReversedText()) { m_sortedTextBoxes.clear(); - for (InlineTextBox * textBox = renderer->firstTextBox(); textBox; textBox = textBox->nextTextBox()) { + for (InlineTextBox* textBox = renderer->firstTextBox(); textBox; textBox = textBox->nextTextBox()) { m_sortedTextBoxes.append(textBox); } std::sort(m_sortedTextBoxes.begin(), m_sortedTextBoxes.end(), compareBoxStart); @@ -339,7 +474,7 @@ bool TextIterator::handleTextNode() void TextIterator::handleTextBox() { - RenderText *renderer = toRenderText(m_node->renderer()); + RenderText* renderer = toRenderText(m_node->renderer()); String str = renderer->text(); int start = m_offset; int end = (m_node == m_endContainer) ? m_endOffset : INT_MAX; @@ -348,7 +483,7 @@ void TextIterator::handleTextBox() int runStart = max(textBoxStart, start); // Check for collapsed space at the start of this run. - InlineTextBox *firstTextBox = renderer->containsReversedText() ? m_sortedTextBoxes[0] : renderer->firstTextBox(); + InlineTextBox* firstTextBox = renderer->containsReversedText() ? m_sortedTextBoxes[0] : renderer->firstTextBox(); bool needSpace = m_lastTextNodeEndedWithCollapsedSpace || (m_textBox == firstTextBox && textBoxStart == runStart && runStart > 0); if (needSpace && !isCollapsibleWhitespace(m_lastCharacter) && m_lastCharacter) { @@ -365,7 +500,7 @@ void TextIterator::handleTextBox() int runEnd = min(textBoxEnd, end); // Determine what the next text box will be, but don't advance yet - InlineTextBox *nextTextBox = 0; + InlineTextBox* nextTextBox = 0; if (renderer->containsReversedText()) { if (m_sortedTextBoxesPosition + 1 < m_sortedTextBoxes.size()) nextTextBox = m_sortedTextBoxes[m_sortedTextBoxesPosition + 1]; @@ -411,6 +546,9 @@ void TextIterator::handleTextBox() bool TextIterator::handleReplacedElement() { + if (m_fullyClippedStack.top()) + return false; + RenderObject* renderer = m_node->renderer(); if (renderer->style()->visibility() != VISIBLE) return false; @@ -421,10 +559,12 @@ bool TextIterator::handleReplacedElement() } if (m_enterTextControls && renderer->isTextControl()) { - m_node = toRenderTextControl(renderer)->innerTextElement(); - m_offset = 0; - m_inShadowContent = true; - return false; + if (HTMLElement* innerTextElement = toRenderTextControl(renderer)->innerTextElement()) { + m_node = innerTextElement->shadowTreeRootNode(); + pushFullyClippedState(m_fullyClippedStack, m_node); + m_offset = 0; + return false; + } } m_haveEmitted = true; @@ -459,7 +599,7 @@ static bool shouldEmitTabBeforeNode(Node* node) return false; // Want a tab before every cell other than the first one - RenderTableCell* rc = static_cast(r); + RenderTableCell* rc = toRenderTableCell(r); RenderTable* t = rc->table(); return t && (t->cellBefore(rc) || t->cellAbove(rc)); } @@ -509,7 +649,7 @@ static bool shouldEmitNewlinesBeforeAndAfterNode(Node* node) // Need to make an exception for table row elements, because they are neither // "inline" or "RenderBlock", but we want newlines for them. if (r->isTableRow()) { - RenderTable* t = static_cast(r)->table(); + RenderTable* t = toRenderTableRow(r)->table(); if (t && !t->isInline()) return true; } @@ -698,7 +838,7 @@ void TextIterator::exitNode() emitCharacter(' ', baseNode->parentNode(), baseNode, 1, 1); } -void TextIterator::emitCharacter(UChar c, Node *textNode, Node *offsetBaseNode, int textStartOffset, int textEndOffset) +void TextIterator::emitCharacter(UChar c, Node* textNode, Node* offsetBaseNode, int textStartOffset, int textEndOffset) { m_haveEmitted = true; @@ -774,14 +914,14 @@ Node* TextIterator::node() const // -------- -SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator() : m_positionNode(0) +SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator() + : m_positionNode(0) { } -SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range *r) +SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range* r) + : m_positionNode(0) { - m_positionNode = 0; - if (!r) return; @@ -806,6 +946,7 @@ SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range *r) } m_node = endNode; + setUpFullyClippedStack(m_fullyClippedStack, m_node); m_offset = endOffset; m_handledNode = false; m_handledChildren = endOffset == 0; @@ -822,15 +963,8 @@ SimplifiedBackwardsTextIterator::SimplifiedBackwardsTextIterator(const Range *r) m_lastTextNode = 0; m_lastCharacter = '\n'; - - if (startOffset == 0 || !startNode->firstChild()) { - m_pastStartNode = startNode->previousSibling(); - while (!m_pastStartNode && startNode->parentNode()) { - startNode = startNode->parentNode(); - m_pastStartNode = startNode->previousSibling(); - } - } else - m_pastStartNode = startNode->childNode(startOffset - 1); + + m_pastStartNode = previousInPostOrderCrossingShadowBoundaries(startNode, startOffset); advance(); } @@ -845,7 +979,7 @@ void SimplifiedBackwardsTextIterator::advance() while (m_node && m_node != m_pastStartNode) { // Don't handle node if we start iterating at [node, 0]. if (!m_handledNode && !(m_node == m_endNode && m_endOffset == 0)) { - RenderObject *renderer = m_node->renderer(); + RenderObject* renderer = m_node->renderer(); if (renderer && renderer->isText() && m_node->nodeType() == Node::TEXT_NODE) { // FIXME: What about CDATA_SECTION_NODE? if (renderer->style()->visibility() == VISIBLE && m_offset > 0) @@ -877,9 +1011,11 @@ void SimplifiedBackwardsTextIterator::advance() // Exit all other containers. next = m_node->previousSibling(); while (!next) { - if (!m_node->parentNode()) + Node* parentNode = parentCrossingShadowBoundaries(m_node); + if (!parentNode) break; - m_node = m_node->parentNode(); + m_node = parentNode; + m_fullyClippedStack.pop(); exitNode(); if (m_positionNode) { m_handledNode = true; @@ -888,9 +1024,12 @@ void SimplifiedBackwardsTextIterator::advance() } next = m_node->previousSibling(); } + m_fullyClippedStack.pop(); } m_node = next; + if (m_node) + pushFullyClippedState(m_fullyClippedStack, m_node); m_offset = m_node ? caretMaxOffset(m_node) : 0; m_handledNode = false; m_handledChildren = false; @@ -904,7 +1043,7 @@ bool SimplifiedBackwardsTextIterator::handleTextNode() { m_lastTextNode = m_node; - RenderText *renderer = toRenderText(m_node->renderer()); + RenderText* renderer = toRenderText(m_node->renderer()); String str = renderer->text(); if (!renderer->firstTextBox() && str.length() > 0) @@ -938,29 +1077,25 @@ bool SimplifiedBackwardsTextIterator::handleNonTextNode() { // We can use a linefeed in place of a tab because this simple iterator is only used to // find boundaries, not actual content. A linefeed breaks words, sentences, and paragraphs. - if (shouldEmitNewlineForNode(m_node) || - shouldEmitNewlineAfterNode(m_node) || - shouldEmitTabBeforeNode(m_node)) { + if (shouldEmitNewlineForNode(m_node) || shouldEmitNewlineAfterNode(m_node) || shouldEmitTabBeforeNode(m_node)) { unsigned index = m_node->nodeIndex(); - // The start of this emitted range is wrong, ensuring correctness would require - // VisiblePositions and so would be slow. previousBoundary expects this. + // The start of this emitted range is wrong. Ensuring correctness would require + // VisiblePositions and so would be slow. previousBoundary expects this. emitCharacter('\n', m_node->parentNode(), index + 1, index + 1); } - return true; } void SimplifiedBackwardsTextIterator::exitNode() { - if (shouldEmitNewlineForNode(m_node) || - shouldEmitNewlineBeforeNode(m_node) || - shouldEmitTabBeforeNode(m_node)) - // The start of this emitted range is wrong, ensuring correctness would require - // VisiblePositions and so would be slow. previousBoundary expects this. + if (shouldEmitNewlineForNode(m_node) || shouldEmitNewlineBeforeNode(m_node) || shouldEmitTabBeforeNode(m_node)) { + // The start of this emitted range is wrong. Ensuring correctness would require + // VisiblePositions and so would be slow. previousBoundary expects this. emitCharacter('\n', m_node, 0, 0); + } } -void SimplifiedBackwardsTextIterator::emitCharacter(UChar c, Node *node, int startOffset, int endOffset) +void SimplifiedBackwardsTextIterator::emitCharacter(UChar c, Node* node, int startOffset, int endOffset) { m_singleCharacterBuffer = c; m_positionNode = node; @@ -988,7 +1123,7 @@ CharacterIterator::CharacterIterator() { } -CharacterIterator::CharacterIterator(const Range *r, bool emitCharactersBetweenAllVisiblePositions, bool enterTextControls) +CharacterIterator::CharacterIterator(const Range* r, bool emitCharactersBetweenAllVisiblePositions, bool enterTextControls) : m_offset(0) , m_runOffset(0) , m_atBreak(true) @@ -1167,15 +1302,17 @@ void BackwardsCharacterIterator::advance(int count) // -------- WordAwareIterator::WordAwareIterator() -: m_previousText(0), m_didLookAhead(false) + : m_previousText(0) + , m_didLookAhead(false) { } -WordAwareIterator::WordAwareIterator(const Range *r) -: m_previousText(0), m_didLookAhead(false), m_textIterator(r) +WordAwareIterator::WordAwareIterator(const Range* r) + : m_previousText(0) + , m_didLookAhead(true) // so we consider the first chunk from the text iterator + , m_textIterator(r) { - m_didLookAhead = true; // so we consider the first chunk from the text iterator - advance(); // get in position over the first chunk of text + advance(); // get in position over the first chunk of text } // We're always in one of these modes: @@ -1185,7 +1322,7 @@ WordAwareIterator::WordAwareIterator(const Range *r) // (we looked ahead to the next chunk and found a word boundary) // - We built up our own chunk of text from many chunks from the text iterator -// FIXME: Perf could be bad for huge spans next to each other that don't fall on word boundaries +// FIXME: Performance could be bad for huge spans next to each other that don't fall on word boundaries. void WordAwareIterator::advance() { @@ -1256,8 +1393,40 @@ const UChar* WordAwareIterator::characters() const // -------- +static inline UChar foldQuoteMark(UChar c) +{ + switch (c) { + case hebrewPunctuationGershayim: + case leftDoubleQuotationMark: + case rightDoubleQuotationMark: + return '"'; + case hebrewPunctuationGeresh: + case leftSingleQuotationMark: + case rightSingleQuotationMark: + return '\''; + default: + return c; + } +} + +static inline void foldQuoteMarks(String& s) +{ + s.replace(hebrewPunctuationGeresh, '\''); + s.replace(hebrewPunctuationGershayim, '"'); + s.replace(leftDoubleQuotationMark, '"'); + s.replace(leftSingleQuotationMark, '\''); + s.replace(rightDoubleQuotationMark, '"'); + s.replace(rightSingleQuotationMark, '\''); +} + #if USE(ICU_UNICODE) && !UCONFIG_NO_COLLATION +static inline void foldQuoteMarks(UChar* data, size_t length) +{ + for (size_t i = 0; i < length; ++i) + data[i] = foldQuoteMark(data[i]); +} + static const size_t minimumSearchBufferSize = 8192; #ifndef NDEBUG @@ -1269,13 +1438,9 @@ static UStringSearch* createSearcher() // Provide a non-empty pattern and non-empty text so usearch_open will not fail, // but it doesn't matter exactly what it is, since we don't perform any searches // without setting both the pattern and the text. - - // Pass empty string for the locale for now to get the Unicode Collation Algorithm, - // rather than something locale-specific. - UErrorCode status = U_ZERO_ERROR; - UStringSearch* searcher = usearch_open(&newlineCharacter, 1, &newlineCharacter, 1, "", 0, &status); - ASSERT(status == U_ZERO_ERROR); + UStringSearch* searcher = usearch_open(&newlineCharacter, 1, &newlineCharacter, 1, currentSearchLocaleID(), 0, &status); + ASSERT(status == U_ZERO_ERROR || status == U_USING_FALLBACK_WARNING || status == U_USING_DEFAULT_WARNING); return searcher; } @@ -1307,7 +1472,12 @@ inline SearchBuffer::SearchBuffer(const String& target, bool isCaseSensitive) { ASSERT(!m_target.isEmpty()); - size_t targetLength = target.length(); + // FIXME: We'd like to tailor the searcher to fold quote marks for us instead + // of doing it in a separate replacement pass here, but ICU doesn't offer a way + // to add tailoring on top of the locale-specific tailoring as of this writing. + foldQuoteMarks(m_target); + + size_t targetLength = m_target.length(); m_buffer.reserveInitialCapacity(max(targetLength * 8, minimumSearchBufferSize)); m_overlap = m_buffer.capacity() / 4; @@ -1347,9 +1517,11 @@ inline size_t SearchBuffer::append(const UChar* characters, size_t length) m_buffer.shrink(m_overlap); } - size_t usableLength = min(m_buffer.capacity() - m_buffer.size(), length); + size_t oldLength = m_buffer.size(); + size_t usableLength = min(m_buffer.capacity() - oldLength, length); ASSERT(usableLength); m_buffer.append(characters, usableLength); + foldQuoteMarks(m_buffer.data() + oldLength, usableLength); return usableLength; } @@ -1416,6 +1588,7 @@ inline SearchBuffer::SearchBuffer(const String& target, bool isCaseSensitive) { ASSERT(!m_target.isEmpty()); m_target.replace(noBreakSpace, ' '); + foldQuoteMarks(m_target); } inline SearchBuffer::~SearchBuffer() @@ -1435,7 +1608,7 @@ inline bool SearchBuffer::atBreak() const inline void SearchBuffer::append(UChar c, bool isStart) { - m_buffer[m_cursor] = c == noBreakSpace ? ' ' : c; + m_buffer[m_cursor] = c == noBreakSpace ? ' ' : foldQuoteMark(c); m_isCharacterStartBuffer[m_cursor] = isStart; if (++m_cursor == m_target.length()) { m_cursor = 0; @@ -1507,7 +1680,7 @@ size_t SearchBuffer::length() const // -------- -int TextIterator::rangeLength(const Range *r, bool forSelectionPreservation) +int TextIterator::rangeLength(const Range* r, bool forSelectionPreservation) { int length = 0; for (TextIterator it(r, forSelectionPreservation); !it.atEnd(); it.advance()) @@ -1522,7 +1695,7 @@ PassRefPtr TextIterator::subrange(Range* entireRange, int characterOffset return characterSubrange(entireRangeIterator, characterOffset, characterCount); } -PassRefPtr TextIterator::rangeFromLocationAndLength(Element *scope, int rangeLocation, int rangeLength, bool forSelectionPreservation) +PassRefPtr TextIterator::rangeFromLocationAndLength(Element* scope, int rangeLocation, int rangeLength, bool forSelectionPreservation) { RefPtr resultRange = scope->document()->createRange(); @@ -1742,11 +1915,6 @@ tryAgain: PassRefPtr findPlainText(const Range* range, const String& target, bool forward, bool caseSensitive) { - // We can't search effectively for a string that's entirely made of collapsible - // whitespace, so we won't even try. This also takes care of the empty string case. - if (isAllCollapsibleWhitespace(target)) - return collapsedToBoundary(range, forward); - // First, find the text. size_t matchStart; size_t matchLength; -- cgit v1.1