/* * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "VisibleUnits.h" #include "Document.h" #include "Element.h" #include "HTMLNames.h" #include "InlineTextBox.h" #include "NodeTraversal.h" #include "Position.h" #include "RenderBlock.h" #include "RenderObject.h" #include "RenderedPosition.h" #include "Text.h" #include "TextBoundaries.h" #include "TextIterator.h" #include "VisiblePosition.h" #include "VisibleSelection.h" #include "htmlediting.h" namespace WebCore { using namespace HTMLNames; using namespace WTF::Unicode; static Node* previousLeafWithSameEditability(Node* node, EditableType editableType) { bool editable = node->rendererIsEditable(editableType); node = previousLeafNode(node); while (node) { if (editable == node->rendererIsEditable(editableType)) return node; node = previousLeafNode(node); } return 0; } static Node* nextLeafWithSameEditability(Node* node, EditableType editableType = ContentIsEditable) { if (!node) return 0; bool editable = node->rendererIsEditable(editableType); node = nextLeafNode(node); while (node) { if (editable == node->rendererIsEditable(editableType)) return node; node = nextLeafNode(node); } return 0; } // FIXME: consolidate with code in previousLinePosition. static Position previousRootInlineBoxCandidatePosition(Node* node, const VisiblePosition& visiblePosition, EditableType editableType) { Node* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent(), editableType); Node* previousNode = previousLeafWithSameEditability(node, editableType); while (previousNode && inSameLine(firstPositionInOrBeforeNode(previousNode), visiblePosition)) previousNode = previousLeafWithSameEditability(previousNode, editableType); while (previousNode && !previousNode->isShadowRoot()) { if (highestEditableRoot(firstPositionInOrBeforeNode(previousNode), editableType) != highestRoot) break; Position pos = previousNode->hasTagName(brTag) ? positionBeforeNode(previousNode) : createLegacyEditingPosition(previousNode, caretMaxOffset(previousNode)); if (pos.isCandidate()) return pos; previousNode = previousLeafWithSameEditability(previousNode, editableType); } return Position(); } static Position nextRootInlineBoxCandidatePosition(Node* node, const VisiblePosition& visiblePosition, EditableType editableType) { Node* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent(), editableType); Node* nextNode = nextLeafWithSameEditability(node, editableType); while (nextNode && inSameLine(firstPositionInOrBeforeNode(nextNode), visiblePosition)) nextNode = nextLeafWithSameEditability(nextNode, ContentIsEditable); while (nextNode && !nextNode->isShadowRoot()) { if (highestEditableRoot(firstPositionInOrBeforeNode(nextNode), editableType) != highestRoot) break; Position pos; pos = createLegacyEditingPosition(nextNode, caretMinOffset(nextNode)); if (pos.isCandidate()) return pos; nextNode = nextLeafWithSameEditability(nextNode, editableType); } return Position(); } class CachedLogicallyOrderedLeafBoxes { public: CachedLogicallyOrderedLeafBoxes(); const InlineTextBox* previousTextBox(const RootInlineBox*, const InlineTextBox*); const InlineTextBox* nextTextBox(const RootInlineBox*, const InlineTextBox*); size_t size() const { return m_leafBoxes.size(); } const InlineBox* firstBox() const { return m_leafBoxes[0]; } private: const Vector& collectBoxes(const RootInlineBox*); int boxIndexInLeaves(const InlineTextBox*) const; const RootInlineBox* m_rootInlineBox; Vector m_leafBoxes; }; CachedLogicallyOrderedLeafBoxes::CachedLogicallyOrderedLeafBoxes() : m_rootInlineBox(0) { }; const InlineTextBox* CachedLogicallyOrderedLeafBoxes::previousTextBox(const RootInlineBox* root, const InlineTextBox* box) { if (!root) return 0; collectBoxes(root); // If box is null, root is box's previous RootInlineBox, and previousBox is the last logical box in root. int boxIndex = m_leafBoxes.size() - 1; if (box) boxIndex = boxIndexInLeaves(box) - 1; for (int i = boxIndex; i >= 0; --i) { if (m_leafBoxes[i]->isInlineTextBox()) return toInlineTextBox(m_leafBoxes[i]); } return 0; } const InlineTextBox* CachedLogicallyOrderedLeafBoxes::nextTextBox(const RootInlineBox* root, const InlineTextBox* box) { if (!root) return 0; collectBoxes(root); // If box is null, root is box's next RootInlineBox, and nextBox is the first logical box in root. // Otherwise, root is box's RootInlineBox, and nextBox is the next logical box in the same line. size_t nextBoxIndex = 0; if (box) nextBoxIndex = boxIndexInLeaves(box) + 1; for (size_t i = nextBoxIndex; i < m_leafBoxes.size(); ++i) { if (m_leafBoxes[i]->isInlineTextBox()) return toInlineTextBox(m_leafBoxes[i]); } return 0; } const Vector& CachedLogicallyOrderedLeafBoxes::collectBoxes(const RootInlineBox* root) { if (m_rootInlineBox != root) { m_rootInlineBox = root; m_leafBoxes.clear(); root->collectLeafBoxesInLogicalOrder(m_leafBoxes); } return m_leafBoxes; } int CachedLogicallyOrderedLeafBoxes::boxIndexInLeaves(const InlineTextBox* box) const { for (size_t i = 0; i < m_leafBoxes.size(); ++i) { if (box == m_leafBoxes[i]) return i; } return 0; } static const InlineTextBox* logicallyPreviousBox(const VisiblePosition& visiblePosition, const InlineTextBox* textBox, bool& previousBoxInDifferentBlock, CachedLogicallyOrderedLeafBoxes& leafBoxes) { const InlineBox* startBox = textBox; const InlineTextBox* previousBox = leafBoxes.previousTextBox(startBox->root(), textBox); if (previousBox) return previousBox; previousBox = leafBoxes.previousTextBox(startBox->root()->prevRootBox(), 0); if (previousBox) return previousBox; while (1) { Node* startNode = startBox->renderer() ? startBox->renderer()->nonPseudoNode() : 0; if (!startNode) break; Position position = previousRootInlineBoxCandidatePosition(startNode, visiblePosition, ContentIsEditable); if (position.isNull()) break; RenderedPosition renderedPosition(position, DOWNSTREAM); RootInlineBox* previousRoot = renderedPosition.rootBox(); if (!previousRoot) break; previousBox = leafBoxes.previousTextBox(previousRoot, 0); if (previousBox) { previousBoxInDifferentBlock = true; return previousBox; } if (!leafBoxes.size()) break; startBox = leafBoxes.firstBox(); } return 0; } static const InlineTextBox* logicallyNextBox(const VisiblePosition& visiblePosition, const InlineTextBox* textBox, bool& nextBoxInDifferentBlock, CachedLogicallyOrderedLeafBoxes& leafBoxes) { const InlineBox* startBox = textBox; const InlineTextBox* nextBox = leafBoxes.nextTextBox(startBox->root(), textBox); if (nextBox) return nextBox; nextBox = leafBoxes.nextTextBox(startBox->root()->nextRootBox(), 0); if (nextBox) return nextBox; while (1) { Node* startNode = startBox->renderer() ? startBox->renderer()->nonPseudoNode() : 0; if (!startNode) break; Position position = nextRootInlineBoxCandidatePosition(startNode, visiblePosition, ContentIsEditable); if (position.isNull()) break; RenderedPosition renderedPosition(position, DOWNSTREAM); RootInlineBox* nextRoot = renderedPosition.rootBox(); if (!nextRoot) break; nextBox = leafBoxes.nextTextBox(nextRoot, 0); if (nextBox) { nextBoxInDifferentBlock = true; return nextBox; } if (!leafBoxes.size()) break; startBox = leafBoxes.firstBox(); } return 0; } static TextBreakIterator* wordBreakIteratorForMinOffsetBoundary(const VisiblePosition& visiblePosition, const InlineTextBox* textBox, int& previousBoxLength, bool& previousBoxInDifferentBlock, Vector& string, CachedLogicallyOrderedLeafBoxes& leafBoxes) { previousBoxInDifferentBlock = false; // FIXME: Handle the case when we don't have an inline text box. const InlineTextBox* previousBox = logicallyPreviousBox(visiblePosition, textBox, previousBoxInDifferentBlock, leafBoxes); int len = 0; string.clear(); if (previousBox) { previousBoxLength = previousBox->len(); string.append(previousBox->textRenderer()->text()->characters() + previousBox->start(), previousBoxLength); len += previousBoxLength; } string.append(textBox->textRenderer()->text()->characters() + textBox->start(), textBox->len()); len += textBox->len(); return wordBreakIterator(string.data(), len); } static TextBreakIterator* wordBreakIteratorForMaxOffsetBoundary(const VisiblePosition& visiblePosition, const InlineTextBox* textBox, bool& nextBoxInDifferentBlock, Vector& string, CachedLogicallyOrderedLeafBoxes& leafBoxes) { nextBoxInDifferentBlock = false; // FIXME: Handle the case when we don't have an inline text box. const InlineTextBox* nextBox = logicallyNextBox(visiblePosition, textBox, nextBoxInDifferentBlock, leafBoxes); int len = 0; string.clear(); string.append(textBox->textRenderer()->text()->characters() + textBox->start(), textBox->len()); len += textBox->len(); if (nextBox) { string.append(nextBox->textRenderer()->text()->characters() + nextBox->start(), nextBox->len()); len += nextBox->len(); } return wordBreakIterator(string.data(), len); } static bool isLogicalStartOfWord(TextBreakIterator* iter, int position, bool hardLineBreak) { bool boundary = hardLineBreak ? true : isTextBreak(iter, position); if (!boundary) return false; textBreakFollowing(iter, position); // isWordTextBreak returns true after moving across a word and false after moving across a punctuation/space. return isWordTextBreak(iter); } static bool islogicalEndOfWord(TextBreakIterator* iter, int position, bool hardLineBreak) { bool boundary = isTextBreak(iter, position); return (hardLineBreak || boundary) && isWordTextBreak(iter); } enum CursorMovementDirection { MoveLeft, MoveRight }; static VisiblePosition visualWordPosition(const VisiblePosition& visiblePosition, CursorMovementDirection direction, bool skipsSpaceWhenMovingRight) { if (visiblePosition.isNull()) return VisiblePosition(); TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent()); InlineBox* previouslyVisitedBox = 0; VisiblePosition current = visiblePosition; TextBreakIterator* iter = 0; CachedLogicallyOrderedLeafBoxes leafBoxes; Vector string; while (1) { VisiblePosition adjacentCharacterPosition = direction == MoveRight ? current.right(true) : current.left(true); if (adjacentCharacterPosition == current || adjacentCharacterPosition.isNull()) return VisiblePosition(); InlineBox* box; int offsetInBox; adjacentCharacterPosition.deepEquivalent().getInlineBoxAndOffset(UPSTREAM, box, offsetInBox); if (!box) break; if (!box->isInlineTextBox()) { current = adjacentCharacterPosition; continue; } InlineTextBox* textBox = toInlineTextBox(box); int previousBoxLength = 0; bool previousBoxInDifferentBlock = false; bool nextBoxInDifferentBlock = false; bool movingIntoNewBox = previouslyVisitedBox != box; if (offsetInBox == box->caretMinOffset()) iter = wordBreakIteratorForMinOffsetBoundary(visiblePosition, textBox, previousBoxLength, previousBoxInDifferentBlock, string, leafBoxes); else if (offsetInBox == box->caretMaxOffset()) iter = wordBreakIteratorForMaxOffsetBoundary(visiblePosition, textBox, nextBoxInDifferentBlock, string, leafBoxes); else if (movingIntoNewBox) { iter = wordBreakIterator(textBox->textRenderer()->text()->characters() + textBox->start(), textBox->len()); previouslyVisitedBox = box; } if (!iter) break; textBreakFirst(iter); int offsetInIterator = offsetInBox - textBox->start() + previousBoxLength; bool isWordBreak; bool boxHasSameDirectionalityAsBlock = box->direction() == blockDirection; bool movingBackward = (direction == MoveLeft && box->direction() == LTR) || (direction == MoveRight && box->direction() == RTL); if ((skipsSpaceWhenMovingRight && boxHasSameDirectionalityAsBlock) || (!skipsSpaceWhenMovingRight && movingBackward)) { bool logicalStartInRenderer = offsetInBox == static_cast(textBox->start()) && previousBoxInDifferentBlock; isWordBreak = isLogicalStartOfWord(iter, offsetInIterator, logicalStartInRenderer); } else { bool logicalEndInRenderer = offsetInBox == static_cast(textBox->start() + textBox->len()) && nextBoxInDifferentBlock; isWordBreak = islogicalEndOfWord(iter, offsetInIterator, logicalEndInRenderer); } if (isWordBreak) return adjacentCharacterPosition; current = adjacentCharacterPosition; } return VisiblePosition(); } VisiblePosition leftWordPosition(const VisiblePosition& visiblePosition, bool skipsSpaceWhenMovingRight) { VisiblePosition leftWordBreak = visualWordPosition(visiblePosition, MoveLeft, skipsSpaceWhenMovingRight); leftWordBreak = visiblePosition.honorEditingBoundaryAtOrBefore(leftWordBreak); // FIXME: How should we handle a non-editable position? if (leftWordBreak.isNull() && isEditablePosition(visiblePosition.deepEquivalent())) { TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent()); leftWordBreak = blockDirection == LTR ? startOfEditableContent(visiblePosition) : endOfEditableContent(visiblePosition); } return leftWordBreak; } VisiblePosition rightWordPosition(const VisiblePosition& visiblePosition, bool skipsSpaceWhenMovingRight) { VisiblePosition rightWordBreak = visualWordPosition(visiblePosition, MoveRight, skipsSpaceWhenMovingRight); rightWordBreak = visiblePosition.honorEditingBoundaryAtOrBefore(rightWordBreak); // FIXME: How should we handle a non-editable position? if (rightWordBreak.isNull() && isEditablePosition(visiblePosition.deepEquivalent())) { TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent()); rightWordBreak = blockDirection == LTR ? endOfEditableContent(visiblePosition) : startOfEditableContent(visiblePosition); } return rightWordBreak; } enum BoundarySearchContextAvailability { DontHaveMoreContext, MayHaveMoreContext }; typedef unsigned (*BoundarySearchFunction)(const UChar*, unsigned length, unsigned offset, BoundarySearchContextAvailability, bool& needMoreContext); static VisiblePosition previousBoundary(const VisiblePosition& c, BoundarySearchFunction searchFunction) { Position pos = c.deepEquivalent(); Node* boundary = pos.parentEditingBoundary(); if (!boundary) return VisiblePosition(); Document* d = boundary->document(); Position start = createLegacyEditingPosition(boundary, 0).parentAnchoredEquivalent(); Position end = pos.parentAnchoredEquivalent(); RefPtr searchRange = Range::create(d); Vector string; unsigned suffixLength = 0; ExceptionCode ec = 0; if (requiresContextForWordBoundary(c.characterBefore())) { RefPtr forwardsScanRange(d->createRange()); forwardsScanRange->setEndAfter(boundary, ec); forwardsScanRange->setStart(end.deprecatedNode(), end.deprecatedEditingOffset(), ec); TextIterator forwardsIterator(forwardsScanRange.get()); while (!forwardsIterator.atEnd()) { const UChar* characters = forwardsIterator.characters(); int length = forwardsIterator.length(); int i = endOfFirstWordBoundaryContext(characters, length); string.append(characters, i); suffixLength += i; if (i < length) break; forwardsIterator.advance(); } } searchRange->setStart(start.deprecatedNode(), start.deprecatedEditingOffset(), ec); searchRange->setEnd(end.deprecatedNode(), end.deprecatedEditingOffset(), ec); ASSERT(!ec); if (ec) return VisiblePosition(); SimplifiedBackwardsTextIterator it(searchRange.get()); unsigned next = 0; bool inTextSecurityMode = start.deprecatedNode() && start.deprecatedNode()->renderer() && start.deprecatedNode()->renderer()->style()->textSecurity() != TSNONE; bool needMoreContext = false; while (!it.atEnd()) { // iterate to get chunks until the searchFunction returns a non-zero value. if (!inTextSecurityMode) string.insert(0, it.characters(), it.length()); else { // Treat bullets used in the text security mode as regular characters when looking for boundaries String iteratorString(it.characters(), it.length()); iteratorString.fill('x'); string.insert(0, iteratorString.characters(), iteratorString.length()); } next = searchFunction(string.data(), string.size(), string.size() - suffixLength, MayHaveMoreContext, needMoreContext); if (next > 1) // FIXME: This is a work around for https://webkit.org/b/115070. We need to provide more contexts in general case. break; it.advance(); } if (needMoreContext) { // The last search returned the beginning of the buffer and asked for more context, // but there is no earlier text. Force a search with what's available. next = searchFunction(string.data(), string.size(), string.size() - suffixLength, DontHaveMoreContext, needMoreContext); ASSERT(!needMoreContext); } if (!next) return VisiblePosition(it.atEnd() ? it.range()->startPosition() : pos, DOWNSTREAM); Node* node = it.range()->startContainer(); if ((node->isTextNode() && static_cast(next) <= node->maxCharacterOffset()) || (node->renderer() && node->renderer()->isBR() && !next)) // The next variable contains a usable index into a text node return VisiblePosition(createLegacyEditingPosition(node, next), DOWNSTREAM); // Use the character iterator to translate the next value into a DOM position. BackwardsCharacterIterator charIt(searchRange.get()); charIt.advance(string.size() - suffixLength - next); // FIXME: charIt can get out of shadow host. return VisiblePosition(charIt.range()->endPosition(), DOWNSTREAM); } static VisiblePosition nextBoundary(const VisiblePosition& c, BoundarySearchFunction searchFunction) { Position pos = c.deepEquivalent(); Node* boundary = pos.parentEditingBoundary(); if (!boundary) return VisiblePosition(); Document* d = boundary->document(); RefPtr searchRange(d->createRange()); Position start(pos.parentAnchoredEquivalent()); Vector string; unsigned prefixLength = 0; if (requiresContextForWordBoundary(c.characterAfter())) { RefPtr backwardsScanRange(d->createRange()); backwardsScanRange->setEnd(start.deprecatedNode(), start.deprecatedEditingOffset(), IGNORE_EXCEPTION); SimplifiedBackwardsTextIterator backwardsIterator(backwardsScanRange.get()); while (!backwardsIterator.atEnd()) { const UChar* characters = backwardsIterator.characters(); int length = backwardsIterator.length(); int i = startOfLastWordBoundaryContext(characters, length); string.insert(0, characters + i, length - i); prefixLength += length - i; if (i > 0) break; backwardsIterator.advance(); } } searchRange->selectNodeContents(boundary, IGNORE_EXCEPTION); searchRange->setStart(start.deprecatedNode(), start.deprecatedEditingOffset(), IGNORE_EXCEPTION); TextIterator it(searchRange.get(), TextIteratorEmitsCharactersBetweenAllVisiblePositions); unsigned next = 0; bool inTextSecurityMode = start.deprecatedNode() && start.deprecatedNode()->renderer() && start.deprecatedNode()->renderer()->style()->textSecurity() != TSNONE; bool needMoreContext = false; while (!it.atEnd()) { // Keep asking the iterator for chunks until the search function // returns an end value not equal to the length of the string passed to it. if (!inTextSecurityMode) string.append(it.characters(), it.length()); else { // Treat bullets used in the text security mode as regular characters when looking for boundaries String iteratorString(it.characters(), it.length()); iteratorString.fill('x'); string.append(iteratorString.characters(), iteratorString.length()); } next = searchFunction(string.data(), string.size(), prefixLength, MayHaveMoreContext, needMoreContext); if (next != string.size()) break; it.advance(); } if (needMoreContext) { // The last search returned the end of the buffer and asked for more context, // but there is no further text. Force a search with what's available. next = searchFunction(string.data(), string.size(), prefixLength, DontHaveMoreContext, needMoreContext); ASSERT(!needMoreContext); } if (it.atEnd() && next == string.size()) { pos = it.range()->startPosition(); } else if (next != prefixLength) { // Use the character iterator to translate the next value into a DOM position. CharacterIterator charIt(searchRange.get(), TextIteratorEmitsCharactersBetweenAllVisiblePositions); charIt.advance(next - prefixLength - 1); RefPtr characterRange = charIt.range(); pos = characterRange->endPosition(); if (*charIt.characters() == '\n') { // FIXME: workaround for collapsed range (where only start position is correct) emitted for some emitted newlines (see rdar://5192593) VisiblePosition visPos = VisiblePosition(pos); if (visPos == VisiblePosition(characterRange->startPosition())) { charIt.advance(1); pos = charIt.range()->startPosition(); } } } // generate VisiblePosition, use UPSTREAM affinity if possible return VisiblePosition(pos, VP_UPSTREAM_IF_POSSIBLE); } // --------- static unsigned startWordBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { ASSERT(offset); if (mayHaveMoreContext && !startOfLastWordBoundaryContext(characters, offset)) { needMoreContext = true; return 0; } needMoreContext = false; int start, end; U16_BACK_1(characters, 0, offset); findWordBoundary(characters, length, offset, &start, &end); return start; } VisiblePosition startOfWord(const VisiblePosition &c, EWordSide side) { // FIXME: This returns a null VP for c at the start of the document // and side == LeftWordIfOnBoundary VisiblePosition p = c; if (side == RightWordIfOnBoundary) { // at paragraph end, the startofWord is the current position if (isEndOfParagraph(c)) return c; p = c.next(); if (p.isNull()) return c; } return previousBoundary(p, startWordBoundary); } static unsigned endWordBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { ASSERT(offset <= length); if (mayHaveMoreContext && endOfFirstWordBoundaryContext(characters + offset, length - offset) == static_cast(length - offset)) { needMoreContext = true; return length; } needMoreContext = false; int start, end; findWordBoundary(characters, length, offset, &start, &end); return end; } VisiblePosition endOfWord(const VisiblePosition &c, EWordSide side) { VisiblePosition p = c; if (side == LeftWordIfOnBoundary) { if (isStartOfParagraph(c)) return c; p = c.previous(); if (p.isNull()) return c; } else if (isEndOfParagraph(c)) return c; return nextBoundary(p, endWordBoundary); } static unsigned previousWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { if (mayHaveMoreContext && !startOfLastWordBoundaryContext(characters, offset)) { needMoreContext = true; return 0; } needMoreContext = false; return findNextWordFromIndex(characters, length, offset, false); } VisiblePosition previousWordPosition(const VisiblePosition &c) { VisiblePosition prev = previousBoundary(c, previousWordPositionBoundary); return c.honorEditingBoundaryAtOrBefore(prev); } static unsigned nextWordPositionBoundary(const UChar* characters, unsigned length, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext) { if (mayHaveMoreContext && endOfFirstWordBoundaryContext(characters + offset, length - offset) == static_cast(length - offset)) { needMoreContext = true; return length; } needMoreContext = false; return findNextWordFromIndex(characters, length, offset, true); } VisiblePosition nextWordPosition(const VisiblePosition &c) { VisiblePosition next = nextBoundary(c, nextWordPositionBoundary); return c.honorEditingBoundaryAtOrAfter(next); } bool isStartOfWord(const VisiblePosition& p) { return p.isNotNull() && p == startOfWord(p, RightWordIfOnBoundary); } // --------- enum LineEndpointComputationMode { UseLogicalOrdering, UseInlineBoxOrdering }; static VisiblePosition startPositionForLine(const VisiblePosition& c, LineEndpointComputationMode mode) { if (c.isNull()) return VisiblePosition(); RootInlineBox* rootBox = RenderedPosition(c).rootBox(); if (!rootBox) { // There are VisiblePositions at offset 0 in blocks without // RootInlineBoxes, like empty editable blocks and bordered blocks. Position p = c.deepEquivalent(); if (p.deprecatedNode()->renderer() && p.deprecatedNode()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset()) return c; return VisiblePosition(); } Node* startNode; InlineBox* startBox; if (mode == UseLogicalOrdering) { startNode = rootBox->getLogicalStartBoxWithNode(startBox); if (!startNode) return VisiblePosition(); } else { // Generated content (e.g. list markers and CSS :before and :after pseudoelements) have no corresponding DOM element, // and so cannot be represented by a VisiblePosition. Use whatever follows instead. startBox = rootBox->firstLeafChild(); while (true) { if (!startBox) return VisiblePosition(); RenderObject* startRenderer = startBox->renderer(); if (!startRenderer) return VisiblePosition(); startNode = startRenderer->nonPseudoNode(); if (startNode) break; startBox = startBox->nextLeafChild(); } } return startNode->isTextNode() ? Position(toText(startNode), toInlineTextBox(startBox)->start()) : positionBeforeNode(startNode); } static VisiblePosition startOfLine(const VisiblePosition& c, LineEndpointComputationMode mode) { // TODO: this is the current behavior that might need to be fixed. // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail. VisiblePosition visPos = startPositionForLine(c, mode); if (mode == UseLogicalOrdering) { if (Node* editableRoot = highestEditableRoot(c.deepEquivalent())) { if (!editableRoot->contains(visPos.deepEquivalent().containerNode())) return firstPositionInNode(editableRoot); } } return c.honorEditingBoundaryAtOrBefore(visPos); } // FIXME: Rename this function to reflect the fact it ignores bidi levels. VisiblePosition startOfLine(const VisiblePosition& currentPosition) { return startOfLine(currentPosition, UseInlineBoxOrdering); } VisiblePosition logicalStartOfLine(const VisiblePosition& currentPosition) { return startOfLine(currentPosition, UseLogicalOrdering); } static VisiblePosition endPositionForLine(const VisiblePosition& c, LineEndpointComputationMode mode) { if (c.isNull()) return VisiblePosition(); RootInlineBox* rootBox = RenderedPosition(c).rootBox(); if (!rootBox) { // There are VisiblePositions at offset 0 in blocks without // RootInlineBoxes, like empty editable blocks and bordered blocks. Position p = c.deepEquivalent(); if (p.deprecatedNode()->renderer() && p.deprecatedNode()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset()) return c; return VisiblePosition(); } Node* endNode; InlineBox* endBox; if (mode == UseLogicalOrdering) { endNode = rootBox->getLogicalEndBoxWithNode(endBox); if (!endNode) return VisiblePosition(); } else { // Generated content (e.g. list markers and CSS :before and :after pseudoelements) have no corresponding DOM element, // and so cannot be represented by a VisiblePosition. Use whatever precedes instead. endBox = rootBox->lastLeafChild(); while (true) { if (!endBox) return VisiblePosition(); RenderObject* endRenderer = endBox->renderer(); if (!endRenderer) return VisiblePosition(); endNode = endRenderer->nonPseudoNode(); if (endNode) break; endBox = endBox->prevLeafChild(); } } Position pos; if (endNode->hasTagName(brTag)) pos = positionBeforeNode(endNode); else if (endBox->isInlineTextBox() && endNode->isTextNode()) { InlineTextBox* endTextBox = toInlineTextBox(endBox); int endOffset = endTextBox->start(); if (!endTextBox->isLineBreak()) endOffset += endTextBox->len(); pos = Position(toText(endNode), endOffset); } else pos = positionAfterNode(endNode); return VisiblePosition(pos, VP_UPSTREAM_IF_POSSIBLE); } static bool inSameLogicalLine(const VisiblePosition& a, const VisiblePosition& b) { return a.isNotNull() && logicalStartOfLine(a) == logicalStartOfLine(b); } static VisiblePosition endOfLine(const VisiblePosition& c, LineEndpointComputationMode mode) { // TODO: this is the current behavior that might need to be fixed. // Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail. VisiblePosition visPos = endPositionForLine(c, mode); if (mode == UseLogicalOrdering) { // Make sure the end of line is at the same line as the given input position. For a wrapping line, the logical end // position for the not-last-2-lines might incorrectly hand back the logical beginning of the next line. // For example,
abcdefg abcdefg abcdefg // a abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg
// In this case, use the previous position of the computed logical end position. if (!inSameLogicalLine(c, visPos)) visPos = visPos.previous(); if (Node* editableRoot = highestEditableRoot(c.deepEquivalent())) { if (!editableRoot->contains(visPos.deepEquivalent().containerNode())) return lastPositionInNode(editableRoot); } return c.honorEditingBoundaryAtOrAfter(visPos); } // Make sure the end of line is at the same line as the given input position. Else use the previous position to // obtain end of line. This condition happens when the input position is before the space character at the end // of a soft-wrapped non-editable line. In this scenario, endPositionForLine would incorrectly hand back a position // in the next line instead. This fix is to account for the discrepancy between lines with webkit-line-break:after-white-space style // versus lines without that style, which would break before a space by default. if (!inSameLine(c, visPos)) { visPos = c.previous(); if (visPos.isNull()) return VisiblePosition(); visPos = endPositionForLine(visPos, UseInlineBoxOrdering); } return c.honorEditingBoundaryAtOrAfter(visPos); } // FIXME: Rename this function to reflect the fact it ignores bidi levels. VisiblePosition endOfLine(const VisiblePosition& currentPosition) { return endOfLine(currentPosition, UseInlineBoxOrdering); } VisiblePosition logicalEndOfLine(const VisiblePosition& currentPosition) { return endOfLine(currentPosition, UseLogicalOrdering); } bool inSameLine(const VisiblePosition &a, const VisiblePosition &b) { return a.isNotNull() && startOfLine(a) == startOfLine(b); } bool isStartOfLine(const VisiblePosition &p) { return p.isNotNull() && p == startOfLine(p); } bool isEndOfLine(const VisiblePosition &p) { return p.isNotNull() && p == endOfLine(p); } static inline IntPoint absoluteLineDirectionPointToLocalPointInBlock(RootInlineBox* root, int lineDirectionPoint) { ASSERT(root); RenderBlock* containingBlock = root->block(); FloatPoint absoluteBlockPoint = containingBlock->localToAbsolute(FloatPoint()); if (containingBlock->hasOverflowClip()) absoluteBlockPoint -= containingBlock->scrolledContentOffset(); if (root->block()->isHorizontalWritingMode()) return IntPoint(lineDirectionPoint - absoluteBlockPoint.x(), root->blockDirectionPointInLine()); return IntPoint(root->blockDirectionPointInLine(), lineDirectionPoint - absoluteBlockPoint.y()); } VisiblePosition previousLinePosition(const VisiblePosition &visiblePosition, int lineDirectionPoint, EditableType editableType) { Position p = visiblePosition.deepEquivalent(); Node* node = p.deprecatedNode(); if (!node) return VisiblePosition(); node->document()->updateLayoutIgnorePendingStylesheets(); RenderObject* renderer = node->renderer(); if (!renderer) return VisiblePosition(); RootInlineBox* root = 0; InlineBox* box; int ignoredCaretOffset; visiblePosition.getInlineBoxAndOffset(box, ignoredCaretOffset); if (box) { root = box->root()->prevRootBox(); // We want to skip zero height boxes. // This could happen in case it is a TrailingFloatsRootInlineBox. if (!root || !root->logicalHeight() || !root->firstLeafChild()) root = 0; } if (!root) { Position position = previousRootInlineBoxCandidatePosition(node, visiblePosition, editableType); if (position.isNotNull()) { RenderedPosition renderedPosition(position); root = renderedPosition.rootBox(); if (!root) return position; } } if (root) { // FIXME: Can be wrong for multi-column layout and with transforms. IntPoint pointInLine = absoluteLineDirectionPointToLocalPointInBlock(root, lineDirectionPoint); RenderObject* renderer = root->closestLeafChildForPoint(pointInLine, isEditablePosition(p))->renderer(); Node* node = renderer->node(); if (node && editingIgnoresContent(node)) return positionInParentBeforeNode(node); return renderer->positionForPoint(pointInLine); } // Could not find a previous line. This means we must already be on the first line. // Move to the start of the content in this block, which effectively moves us // to the start of the line we're on. Element* rootElement = node->rendererIsEditable(editableType) ? node->rootEditableElement(editableType) : node->document()->documentElement(); if (!rootElement) return VisiblePosition(); return VisiblePosition(firstPositionInNode(rootElement), DOWNSTREAM); } VisiblePosition nextLinePosition(const VisiblePosition &visiblePosition, int lineDirectionPoint, EditableType editableType) { Position p = visiblePosition.deepEquivalent(); Node* node = p.deprecatedNode(); if (!node) return VisiblePosition(); node->document()->updateLayoutIgnorePendingStylesheets(); RenderObject* renderer = node->renderer(); if (!renderer) return VisiblePosition(); RootInlineBox* root = 0; InlineBox* box; int ignoredCaretOffset; visiblePosition.getInlineBoxAndOffset(box, ignoredCaretOffset); if (box) { root = box->root()->nextRootBox(); // We want to skip zero height boxes. // This could happen in case it is a TrailingFloatsRootInlineBox. if (!root || !root->logicalHeight() || !root->firstLeafChild()) root = 0; } if (!root) { // FIXME: We need do the same in previousLinePosition. Node* child = node->childNode(p.deprecatedEditingOffset()); node = child ? child : node->lastDescendant(); Position position = nextRootInlineBoxCandidatePosition(node, visiblePosition, editableType); if (position.isNotNull()) { RenderedPosition renderedPosition(position); root = renderedPosition.rootBox(); if (!root) return position; } } if (root) { // FIXME: Can be wrong for multi-column layout and with transforms. IntPoint pointInLine = absoluteLineDirectionPointToLocalPointInBlock(root, lineDirectionPoint); RenderObject* renderer = root->closestLeafChildForPoint(pointInLine, isEditablePosition(p))->renderer(); Node* node = renderer->node(); if (node && editingIgnoresContent(node)) return positionInParentBeforeNode(node); return renderer->positionForPoint(pointInLine); } // Could not find a next line. This means we must already be on the last line. // Move to the end of the content in this block, which effectively moves us // to the end of the line we're on. Element* rootElement = node->rendererIsEditable(editableType) ? node->rootEditableElement(editableType) : node->document()->documentElement(); if (!rootElement) return VisiblePosition(); return VisiblePosition(lastPositionInNode(rootElement), DOWNSTREAM); } // --------- static unsigned startSentenceBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { TextBreakIterator* iterator = sentenceBreakIterator(characters, length); // FIXME: The following function can return -1; we don't handle that. return textBreakPreceding(iterator, length); } VisiblePosition startOfSentence(const VisiblePosition &c) { return previousBoundary(c, startSentenceBoundary); } static unsigned endSentenceBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { TextBreakIterator* iterator = sentenceBreakIterator(characters, length); return textBreakNext(iterator); } // FIXME: This includes the space after the punctuation that marks the end of the sentence. VisiblePosition endOfSentence(const VisiblePosition &c) { return nextBoundary(c, endSentenceBoundary); } static unsigned previousSentencePositionBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { // FIXME: This is identical to startSentenceBoundary. I'm pretty sure that's not right. TextBreakIterator* iterator = sentenceBreakIterator(characters, length); // FIXME: The following function can return -1; we don't handle that. return textBreakPreceding(iterator, length); } VisiblePosition previousSentencePosition(const VisiblePosition &c) { VisiblePosition prev = previousBoundary(c, previousSentencePositionBoundary); return c.honorEditingBoundaryAtOrBefore(prev); } static unsigned nextSentencePositionBoundary(const UChar* characters, unsigned length, unsigned, BoundarySearchContextAvailability, bool&) { // FIXME: This is identical to endSentenceBoundary. This isn't right, it needs to // move to the equivlant position in the following sentence. TextBreakIterator* iterator = sentenceBreakIterator(characters, length); return textBreakFollowing(iterator, 0); } VisiblePosition nextSentencePosition(const VisiblePosition &c) { VisiblePosition next = nextBoundary(c, nextSentencePositionBoundary); return c.honorEditingBoundaryAtOrAfter(next); } VisiblePosition startOfParagraph(const VisiblePosition& c, EditingBoundaryCrossingRule boundaryCrossingRule) { Position p = c.deepEquivalent(); Node* startNode = p.deprecatedNode(); if (!startNode) return VisiblePosition(); if (isRenderedAsNonInlineTableImageOrHR(startNode)) return positionBeforeNode(startNode); Node* startBlock = enclosingBlock(startNode); Node* node = startNode; Node* highestRoot = highestEditableRoot(p); int offset = p.deprecatedEditingOffset(); Position::AnchorType type = p.anchorType(); Node* n = startNode; while (n) { #if ENABLE(USERSELECT_ALL) if (boundaryCrossingRule == CannotCrossEditingBoundary && !Position::nodeIsUserSelectAll(n) && n->rendererIsEditable() != startNode->rendererIsEditable()) #else if (boundaryCrossingRule == CannotCrossEditingBoundary && n->rendererIsEditable() != startNode->rendererIsEditable()) #endif break; if (boundaryCrossingRule == CanSkipOverEditingBoundary) { while (n && n->rendererIsEditable() != startNode->rendererIsEditable()) n = NodeTraversal::previousPostOrder(n, startBlock); if (!n || !n->isDescendantOf(highestRoot)) break; } RenderObject* r = n->renderer(); if (!r) { n = NodeTraversal::previousPostOrder(n, startBlock); continue; } RenderStyle* style = r->style(); if (style->visibility() != VISIBLE) { n = NodeTraversal::previousPostOrder(n, startBlock); continue; } if (r->isBR() || isBlock(n)) break; if (r->isText() && toRenderText(r)->renderedTextLength()) { ASSERT_WITH_SECURITY_IMPLICATION(n->isTextNode()); type = Position::PositionIsOffsetInAnchor; if (style->preserveNewline()) { const UChar* chars = toRenderText(r)->characters(); int i = toRenderText(r)->textLength(); int o = offset; if (n == startNode && o < i) i = max(0, o); while (--i >= 0) { if (chars[i] == '\n') return VisiblePosition(Position(toText(n), i + 1), DOWNSTREAM); } } node = n; offset = 0; n = NodeTraversal::previousPostOrder(n, startBlock); } else if (editingIgnoresContent(n) || isTableElement(n)) { node = n; type = Position::PositionIsBeforeAnchor; n = n->previousSibling() ? n->previousSibling() : NodeTraversal::previousPostOrder(n, startBlock); } else n = NodeTraversal::previousPostOrder(n, startBlock); } if (type == Position::PositionIsOffsetInAnchor) { ASSERT(type == Position::PositionIsOffsetInAnchor || !offset); return VisiblePosition(Position(node, offset, type), DOWNSTREAM); } return VisiblePosition(Position(node, type), DOWNSTREAM); } VisiblePosition endOfParagraph(const VisiblePosition &c, EditingBoundaryCrossingRule boundaryCrossingRule) { if (c.isNull()) return VisiblePosition(); Position p = c.deepEquivalent(); Node* startNode = p.deprecatedNode(); if (isRenderedAsNonInlineTableImageOrHR(startNode)) return positionAfterNode(startNode); Node* startBlock = enclosingBlock(startNode); Node* stayInsideBlock = startBlock; Node* node = startNode; Node* highestRoot = highestEditableRoot(p); int offset = p.deprecatedEditingOffset(); Position::AnchorType type = p.anchorType(); Node* n = startNode; while (n) { #if ENABLE(USERSELECT_ALL) if (boundaryCrossingRule == CannotCrossEditingBoundary && !Position::nodeIsUserSelectAll(n) && n->rendererIsEditable() != startNode->rendererIsEditable()) #else if (boundaryCrossingRule == CannotCrossEditingBoundary && n->rendererIsEditable() != startNode->rendererIsEditable()) #endif break; if (boundaryCrossingRule == CanSkipOverEditingBoundary) { while (n && n->rendererIsEditable() != startNode->rendererIsEditable()) n = NodeTraversal::next(n, stayInsideBlock); if (!n || !n->isDescendantOf(highestRoot)) break; } RenderObject* r = n->renderer(); if (!r) { n = NodeTraversal::next(n, stayInsideBlock); continue; } RenderStyle* style = r->style(); if (style->visibility() != VISIBLE) { n = NodeTraversal::next(n, stayInsideBlock); continue; } if (r->isBR() || isBlock(n)) break; // FIXME: We avoid returning a position where the renderer can't accept the caret. if (r->isText() && toRenderText(r)->renderedTextLength()) { ASSERT_WITH_SECURITY_IMPLICATION(n->isTextNode()); int length = toRenderText(r)->textLength(); type = Position::PositionIsOffsetInAnchor; if (style->preserveNewline()) { const UChar* chars = toRenderText(r)->characters(); int o = n == startNode ? offset : 0; for (int i = o; i < length; ++i) { if (chars[i] == '\n') return VisiblePosition(Position(toText(n), i), DOWNSTREAM); } } node = n; offset = r->caretMaxOffset(); n = NodeTraversal::next(n, stayInsideBlock); } else if (editingIgnoresContent(n) || isTableElement(n)) { node = n; type = Position::PositionIsAfterAnchor; n = NodeTraversal::nextSkippingChildren(n, stayInsideBlock); } else n = NodeTraversal::next(n, stayInsideBlock); } if (type == Position::PositionIsOffsetInAnchor) return VisiblePosition(Position(node, offset, type), DOWNSTREAM); return VisiblePosition(Position(node, type), DOWNSTREAM); } // FIXME: isStartOfParagraph(startOfNextParagraph(pos)) is not always true VisiblePosition startOfNextParagraph(const VisiblePosition& visiblePosition) { VisiblePosition paragraphEnd(endOfParagraph(visiblePosition, CanSkipOverEditingBoundary)); VisiblePosition afterParagraphEnd(paragraphEnd.next(CannotCrossEditingBoundary)); // The position after the last position in the last cell of a table // is not the start of the next paragraph. if (isFirstPositionAfterTable(afterParagraphEnd)) return afterParagraphEnd.next(CannotCrossEditingBoundary); return afterParagraphEnd; } bool inSameParagraph(const VisiblePosition &a, const VisiblePosition &b, EditingBoundaryCrossingRule boundaryCrossingRule) { return a.isNotNull() && startOfParagraph(a, boundaryCrossingRule) == startOfParagraph(b, boundaryCrossingRule); } bool isStartOfParagraph(const VisiblePosition &pos, EditingBoundaryCrossingRule boundaryCrossingRule) { return pos.isNotNull() && pos == startOfParagraph(pos, boundaryCrossingRule); } bool isEndOfParagraph(const VisiblePosition &pos, EditingBoundaryCrossingRule boundaryCrossingRule) { return pos.isNotNull() && pos == endOfParagraph(pos, boundaryCrossingRule); } VisiblePosition previousParagraphPosition(const VisiblePosition& p, int x) { VisiblePosition pos = p; do { VisiblePosition n = previousLinePosition(pos, x); if (n.isNull() || n == pos) break; pos = n; } while (inSameParagraph(p, pos)); return pos; } VisiblePosition nextParagraphPosition(const VisiblePosition& p, int x) { VisiblePosition pos = p; do { VisiblePosition n = nextLinePosition(pos, x); if (n.isNull() || n == pos) break; pos = n; } while (inSameParagraph(p, pos)); return pos; } // --------- VisiblePosition startOfBlock(const VisiblePosition& visiblePosition, EditingBoundaryCrossingRule rule) { Position position = visiblePosition.deepEquivalent(); Node* startBlock; if (!position.containerNode() || !(startBlock = enclosingBlock(position.containerNode(), rule))) return VisiblePosition(); return firstPositionInNode(startBlock); } VisiblePosition endOfBlock(const VisiblePosition& visiblePosition, EditingBoundaryCrossingRule rule) { Position position = visiblePosition.deepEquivalent(); Node* endBlock; if (!position.containerNode() || !(endBlock = enclosingBlock(position.containerNode(), rule))) return VisiblePosition(); return lastPositionInNode(endBlock); } bool inSameBlock(const VisiblePosition &a, const VisiblePosition &b) { return !a.isNull() && enclosingBlock(a.deepEquivalent().containerNode()) == enclosingBlock(b.deepEquivalent().containerNode()); } bool isStartOfBlock(const VisiblePosition &pos) { return pos.isNotNull() && pos == startOfBlock(pos, CanCrossEditingBoundary); } bool isEndOfBlock(const VisiblePosition &pos) { return pos.isNotNull() && pos == endOfBlock(pos, CanCrossEditingBoundary); } // --------- VisiblePosition startOfDocument(const Node* node) { if (!node || !node->document() || !node->document()->documentElement()) return VisiblePosition(); return VisiblePosition(firstPositionInNode(node->document()->documentElement()), DOWNSTREAM); } VisiblePosition startOfDocument(const VisiblePosition &c) { return startOfDocument(c.deepEquivalent().deprecatedNode()); } VisiblePosition endOfDocument(const Node* node) { if (!node || !node->document() || !node->document()->documentElement()) return VisiblePosition(); Element* doc = node->document()->documentElement(); return VisiblePosition(lastPositionInNode(doc), DOWNSTREAM); } VisiblePosition endOfDocument(const VisiblePosition &c) { return endOfDocument(c.deepEquivalent().deprecatedNode()); } bool inSameDocument(const VisiblePosition &a, const VisiblePosition &b) { Position ap = a.deepEquivalent(); Node* an = ap.deprecatedNode(); if (!an) return false; Position bp = b.deepEquivalent(); Node* bn = bp.deprecatedNode(); if (an == bn) return true; return an->document() == bn->document(); } bool isStartOfDocument(const VisiblePosition &p) { return p.isNotNull() && p.previous(CanCrossEditingBoundary).isNull(); } bool isEndOfDocument(const VisiblePosition &p) { return p.isNotNull() && p.next(CanCrossEditingBoundary).isNull(); } // --------- VisiblePosition startOfEditableContent(const VisiblePosition& visiblePosition) { Node* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent()); if (!highestRoot) return VisiblePosition(); return firstPositionInNode(highestRoot); } VisiblePosition endOfEditableContent(const VisiblePosition& visiblePosition) { Node* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent()); if (!highestRoot) return VisiblePosition(); return lastPositionInNode(highestRoot); } bool isEndOfEditableOrNonEditableContent(const VisiblePosition &p) { return p.isNotNull() && p.next().isNull(); } VisiblePosition leftBoundaryOfLine(const VisiblePosition& c, TextDirection direction) { return direction == LTR ? logicalStartOfLine(c) : logicalEndOfLine(c); } VisiblePosition rightBoundaryOfLine(const VisiblePosition& c, TextDirection direction) { return direction == LTR ? logicalEndOfLine(c) : logicalStartOfLine(c); } }