diff options
Diffstat (limited to 'toolkit/components/find')
-rw-r--r-- | toolkit/components/find/moz.build | 29 | ||||
-rw-r--r-- | toolkit/components/find/nsFind.cpp | 990 | ||||
-rw-r--r-- | toolkit/components/find/nsFind.h | 62 | ||||
-rw-r--r-- | toolkit/components/find/nsFindService.cpp | 91 | ||||
-rw-r--r-- | toolkit/components/find/nsFindService.h | 44 | ||||
-rw-r--r-- | toolkit/components/find/nsIFind.idl | 34 | ||||
-rw-r--r-- | toolkit/components/find/nsIFindService.idl | 27 | ||||
-rw-r--r-- | toolkit/components/find/nsIWebBrowserFind.idl | 152 | ||||
-rw-r--r-- | toolkit/components/find/nsWebBrowserFind.cpp | 764 | ||||
-rw-r--r-- | toolkit/components/find/nsWebBrowserFind.h | 97 | ||||
-rw-r--r-- | toolkit/components/find/test/mochitest/mochitest.ini | 6 | ||||
-rw-r--r-- | toolkit/components/find/test/mochitest/test_bug499115.html | 66 | ||||
-rw-r--r-- | toolkit/components/find/test/mochitest/test_nsFind.html | 399 |
13 files changed, 2761 insertions, 0 deletions
diff --git a/toolkit/components/find/moz.build b/toolkit/components/find/moz.build new file mode 100644 index 0000000000..e4f3f396f9 --- /dev/null +++ b/toolkit/components/find/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Find Toolbar") + +XPIDL_SOURCES += [ + "nsIFind.idl", + "nsIFindService.idl", + "nsIWebBrowserFind.idl", +] + +XPIDL_MODULE = "mozfind" + +UNIFIED_SOURCES += [ + "nsFind.cpp", + "nsWebBrowserFind.cpp", +] + +SOURCES += [ + "nsFindService.cpp", +] + +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] + +FINAL_LIBRARY = "xul" diff --git a/toolkit/components/find/nsFind.cpp b/toolkit/components/find/nsFind.cpp new file mode 100644 index 0000000000..8a5c291fdf --- /dev/null +++ b/toolkit/components/find/nsFind.cpp @@ -0,0 +1,990 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// #define DEBUG_FIND 1 + +#include "nsFind.h" +#include "mozilla/Likely.h" +#include "nsContentCID.h" +#include "nsIContent.h" +#include "nsINode.h" +#include "nsIFrame.h" +#include "nsITextControlFrame.h" +#include "nsIFormControl.h" +#include "nsTextFragment.h" +#include "nsString.h" +#include "nsAtom.h" +#include "nsServiceManagerUtils.h" +#include "nsUnicharUtils.h" +#include "nsUnicodeProperties.h" +#include "nsCRT.h" +#include "nsRange.h" +#include "nsReadableUtils.h" +#include "nsContentUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/ChildIterator.h" +#include "mozilla/dom/TreeIterator.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/Text.h" +#include "mozilla/intl/Segmenter.h" +#include "mozilla/intl/UnicodeProperties.h" +#include "mozilla/StaticPrefs_browser.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::unicode; + +// Yikes! Casting a char to unichar can fill with ones! +#define CHAR_TO_UNICHAR(c) ((char16_t)(unsigned char)c) + +#define CH_SHY ((char16_t)0xAD) + +// nsFind::Find casts CH_SHY to char before calling StripChars +// This works correctly if and only if CH_SHY <= 255 +static_assert(CH_SHY <= 255, "CH_SHY is not an ascii character"); + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFind) + NS_INTERFACE_MAP_ENTRY(nsIFind) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFind) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFind) + +NS_IMPL_CYCLE_COLLECTION(nsFind) + +#ifdef DEBUG_FIND +# define DEBUG_FIND_PRINTF(...) printf(__VA_ARGS__) +#else +# define DEBUG_FIND_PRINTF(...) /* nothing */ +#endif + +static nsIContent& AnonymousSubtreeRootParentOrHost(const nsINode& aNode) { + MOZ_ASSERT(aNode.IsInNativeAnonymousSubtree()); + return *aNode.GetClosestNativeAnonymousSubtreeRootParentOrHost(); +} + +static void DumpNode(const nsINode* aNode) { +#ifdef DEBUG_FIND + if (!aNode) { + printf(">>>> Node: NULL\n"); + return; + } + nsString nodeName = aNode->NodeName(); + if (aNode->IsText()) { + nsAutoString newText; + aNode->AsText()->AppendTextTo(newText); + printf(">>>> Text node (node name %s): '%s'\n", + NS_LossyConvertUTF16toASCII(nodeName).get(), + NS_LossyConvertUTF16toASCII(newText).get()); + } else { + printf(">>>> Node: %s\n", NS_LossyConvertUTF16toASCII(nodeName).get()); + } +#endif +} + +static bool IsBlockNode(const nsIContent* aContent) { + if (aContent->IsElement() && aContent->AsElement()->IsDisplayContents()) { + return false; + } + + // FIXME(emilio): This is dubious... + if (aContent->IsAnyOfHTMLElements(nsGkAtoms::img, nsGkAtoms::hr, + nsGkAtoms::th, nsGkAtoms::td)) { + return true; + } + + nsIFrame* frame = aContent->GetPrimaryFrame(); + if (!frame) { + return false; + } + + const auto& disp = *frame->StyleDisplay(); + // We also treat internal table frames as "blocks" for the purpose of + // locating boundaries for searches (see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1645990). + return disp.IsBlockOutsideStyle() || disp.IsInternalTableStyleExceptCell(); +} + +static bool IsDisplayedNode(const nsINode* aNode) { + if (!aNode->IsContent()) { + return false; + } + + if (aNode->AsContent()->GetPrimaryFrame()) { + return true; + } + + // If there's no frame, it's not displayed, unless it's display: contents. + return aNode->IsElement() && aNode->AsElement()->IsDisplayContents(); +} + +static bool IsRubyAnnotationNode(const nsINode* aNode) { + if (!aNode->IsContent()) { + return false; + } + + nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame(); + if (!frame) { + return false; + } + + StyleDisplay display = frame->StyleDisplay()->mDisplay; + return StyleDisplay::RubyText == display || + StyleDisplay::RubyTextContainer == display; +} + +static bool IsVisibleNode(const nsINode* aNode) { + if (!IsDisplayedNode(aNode)) { + return false; + } + + nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame(); + if (!frame) { + // display: contents + return true; + } + + if (frame->HidesContent(nsIFrame::IncludeContentVisibility::Hidden) || + frame->IsHiddenByContentVisibilityOnAnyAncestor( + nsIFrame::IncludeContentVisibility::Hidden)) { + return false; + } + + return frame->StyleVisibility()->IsVisible(); +} + +static bool ShouldFindAnonymousContent(const nsIContent& aContent) { + MOZ_ASSERT(aContent.IsInNativeAnonymousSubtree()); + + nsIContent& host = AnonymousSubtreeRootParentOrHost(aContent); + if (nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(&host)) { + if (formControl->IsTextControl(/* aExcludePassword = */ true)) { + // Only editable NAC in textfields should be findable. That is, we want to + // find "bar" in `<input value="bar">`, but not in `<input + // placeholder="bar">`. + // + // TODO(emilio): Ideally we could lift this restriction, but we hide the + // placeholder text at paint-time instead of with CSS visibility, which + // means that we won't skip it even if invisible. We should probably fix + // that. + return aContent.IsEditable(); + } + + // We want to avoid finding in password inputs anyway, as it is confusing. + if (formControl->ControlType() == FormControlType::InputPassword) { + return false; + } + } + + return true; +} + +static bool SkipNode(const nsIContent* aContent) { + const nsIContent* content = aContent; + while (content) { + if (!IsDisplayedNode(content) || content->IsComment() || + content->IsAnyOfHTMLElements(nsGkAtoms::select)) { + DEBUG_FIND_PRINTF("Skipping node: "); + DumpNode(content); + return true; + } + + // Skip option nodes if their select is a combo box, or if they + // have no select (somehow). + if (const auto* option = HTMLOptionElement::FromNode(content)) { + auto* select = HTMLSelectElement::FromNodeOrNull(option->GetParent()); + if (!select || select->IsCombobox()) { + DEBUG_FIND_PRINTF("Skipping node: "); + DumpNode(content); + return true; + } + } + + if (StaticPrefs::browser_find_ignore_ruby_annotations() && + IsRubyAnnotationNode(content)) { + DEBUG_FIND_PRINTF("Skipping node: "); + DumpNode(content); + return true; + } + + if (content->IsInNativeAnonymousSubtree() && + !ShouldFindAnonymousContent(*content)) { + DEBUG_FIND_PRINTF("Skipping node: "); + DumpNode(content); + return true; + } + + // Only climb to the nearest block node + if (IsBlockNode(content)) { + return false; + } + + content = content->GetFlattenedTreeParent(); + } + + return false; +} + +static const nsIContent* GetBlockParent(const Text& aNode) { + for (const nsIContent* current = aNode.GetFlattenedTreeParent(); current; + current = current->GetFlattenedTreeParent()) { + if (IsBlockNode(current)) { + return current; + } + } + return nullptr; +} + +static bool NonTextNodeForcesBreak(const nsINode& aNode) { + nsIFrame* frame = + aNode.IsContent() ? aNode.AsContent()->GetPrimaryFrame() : nullptr; + // TODO(emilio): Maybe we should treat <br> more like a space instead of a + // forced break? Unclear... + return frame && frame->IsBrFrame(); +} + +static bool ForceBreakBetweenText(const Text& aPrevious, const Text& aNext) { + return GetBlockParent(aPrevious) != GetBlockParent(aNext); +} + +struct nsFind::State final { + State(bool aFindBackward, nsIContent& aRoot, const nsRange& aStartPoint) + : mFindBackward(aFindBackward), + mInitialized(false), + mFoundBreak(false), + mIterOffset(-1), + mIterator(aRoot), + mStartPoint(aStartPoint) {} + + void PositionAt(Text& aNode) { mIterator.Seek(aNode); } + + bool ForcedBreak() const { return mFoundBreak; } + + Text* GetCurrentNode() const { + if (MOZ_UNLIKELY(!mInitialized)) { + return nullptr; + } + nsINode* node = mIterator.GetCurrent(); + MOZ_ASSERT(!node || node->IsText()); + return node ? node->GetAsText() : nullptr; + } + + Text* GetNextNode(bool aAlreadyMatching) { + if (MOZ_UNLIKELY(!mInitialized)) { + MOZ_ASSERT(!aAlreadyMatching); + Initialize(); + } else { + Advance(Initializing::No, aAlreadyMatching); + mIterOffset = -1; // mIterOffset only really applies to the first node. + } + return GetCurrentNode(); + } + + private: + enum class Initializing { No, Yes }; + + // Advance to the next visible text-node. + void Advance(Initializing, bool aAlreadyMatching); + // Sets up the first node position and offset. + void Initialize(); + + // Returns whether the node should be used (true) or skipped over (false) + static bool AnalyzeNode(const nsINode& aNode, const Text* aPrev, + bool aAlreadyMatching, bool* aForcedBreak) { + if (!aNode.IsText()) { + *aForcedBreak = *aForcedBreak || NonTextNodeForcesBreak(aNode); + return false; + } + if (SkipNode(aNode.AsText())) { + return false; + } + *aForcedBreak = *aForcedBreak || + (aPrev && ForceBreakBetweenText(*aPrev, *aNode.AsText())); + if (*aForcedBreak) { + // If we've already found a break, we can stop searching and just use this + // node, regardless of the subtree we're on. There's no point to continue + // a match across different blocks, regardless of which subtree you're + // looking into. + return true; + } + + // TODO(emilio): We can't represent ranges that span native anonymous / + // shadow tree boundaries, but if we did the following check could / should + // be removed. + if (aAlreadyMatching && aPrev && + !nsContentUtils::IsInSameAnonymousTree(&aNode, aPrev)) { + // As an optimization, if we were finding inside an native-anonymous + // subtree (like a pseudo-element), we know those trees are "atomic" and + // can't have any other subtrees in between, so we can just break the + // match here. + if (aPrev->IsInNativeAnonymousSubtree()) { + *aForcedBreak = true; + return true; + } + // Otherwise we can skip the node and keep looking past this subtree. + return false; + } + + return true; + } + + const bool mFindBackward; + + // Whether we've called GetNextNode() at least once. + bool mInitialized; + + public: + // Whether we've found a forced break from the last node to the current one. + bool mFoundBreak; + // An offset into the text of the first node we're starting to search at. + int mIterOffset; + TreeIterator<StyleChildrenIterator> mIterator; + + // These are only needed for the first GetNextNode() call. + const nsRange& mStartPoint; +}; + +void nsFind::State::Advance(Initializing aInitializing, bool aAlreadyMatching) { + MOZ_ASSERT(mInitialized); + + // The Advance() call during Initialize() calls us in a partial state, where + // mIterator may not be pointing to a text node yet. aInitializing prevents + // tripping the invariants of GetCurrentNode(). + const Text* prev = + aInitializing == Initializing::Yes ? nullptr : GetCurrentNode(); + mFoundBreak = false; + + while (true) { + nsIContent* current = + mFindBackward ? mIterator.GetPrev() : mIterator.GetNext(); + if (!current) { + return; + } + if (AnalyzeNode(*current, prev, aAlreadyMatching, &mFoundBreak)) { + break; + } + } +} + +void nsFind::State::Initialize() { + MOZ_ASSERT(!mInitialized); + mInitialized = true; + mIterOffset = mFindBackward ? -1 : 0; + + nsINode* container = mFindBackward ? mStartPoint.GetStartContainer() + : mStartPoint.GetEndContainer(); + + // Set up ourselves at the first node we want to start searching at. + nsIContent* beginning = mFindBackward ? mStartPoint.GetChildAtStartOffset() + : mStartPoint.GetChildAtEndOffset(); + if (beginning) { + mIterator.Seek(*beginning); + // If the start point is pointing to a node, when looking backwards we'd + // start looking at the children of that node, and we don't really want + // that. When looking forwards, we look at the next sibling afterwards. + if (mFindBackward) { + mIterator.GetPrevSkippingChildren(); + } + } else if (container && container->IsContent()) { + // Text-only range, or pointing to past the end of the node, for example. + mIterator.Seek(*container->AsContent()); + } + + nsINode* current = mIterator.GetCurrent(); + if (!current) { + return; + } + + const bool kAlreadyMatching = false; + if (!AnalyzeNode(*current, nullptr, kAlreadyMatching, &mFoundBreak)) { + Advance(Initializing::Yes, kAlreadyMatching); + current = mIterator.GetCurrent(); + if (!current) { + return; + } + } + + if (current != container) { + return; + } + + mIterOffset = + mFindBackward ? mStartPoint.StartOffset() : mStartPoint.EndOffset(); +} + +class MOZ_STACK_CLASS nsFind::StateRestorer final { + public: + explicit StateRestorer(State& aState) + : mState(aState), + mIterOffset(aState.mIterOffset), + mFoundBreak(aState.mFoundBreak), + mCurrNode(aState.GetCurrentNode()) {} + + ~StateRestorer() { + mState.mFoundBreak = mFoundBreak; + mState.mIterOffset = mIterOffset; + if (mCurrNode) { + mState.PositionAt(*mCurrNode); + } + } + + private: + State& mState; + + int32_t mIterOffset; + bool mFoundBreak; + Text* mCurrNode; +}; + +NS_IMETHODIMP +nsFind::GetFindBackwards(bool* aFindBackward) { + if (!aFindBackward) { + return NS_ERROR_NULL_POINTER; + } + + *aFindBackward = mFindBackward; + return NS_OK; +} + +NS_IMETHODIMP +nsFind::SetFindBackwards(bool aFindBackward) { + mFindBackward = aFindBackward; + return NS_OK; +} + +NS_IMETHODIMP +nsFind::GetCaseSensitive(bool* aCaseSensitive) { + if (!aCaseSensitive) { + return NS_ERROR_NULL_POINTER; + } + + *aCaseSensitive = mCaseSensitive; + return NS_OK; +} + +NS_IMETHODIMP +nsFind::SetCaseSensitive(bool aCaseSensitive) { + mCaseSensitive = aCaseSensitive; + return NS_OK; +} + +/* attribute boolean entireWord; */ +NS_IMETHODIMP +nsFind::GetEntireWord(bool* aEntireWord) { + if (!aEntireWord) return NS_ERROR_NULL_POINTER; + + *aEntireWord = mEntireWord; + return NS_OK; +} + +NS_IMETHODIMP +nsFind::SetEntireWord(bool aEntireWord) { + mEntireWord = aEntireWord; + return NS_OK; +} + +NS_IMETHODIMP +nsFind::GetMatchDiacritics(bool* aMatchDiacritics) { + if (!aMatchDiacritics) { + return NS_ERROR_NULL_POINTER; + } + + *aMatchDiacritics = mMatchDiacritics; + return NS_OK; +} + +NS_IMETHODIMP +nsFind::SetMatchDiacritics(bool aMatchDiacritics) { + mMatchDiacritics = aMatchDiacritics; + return NS_OK; +} + +// Here begins the find code. A ten-thousand-foot view of how it works: Find +// needs to be able to compare across inline (but not block) nodes, e.g. find +// for "abc" should match a<b>b</b>c. So after we've searched a node, we're not +// done with it; in the case of a partial match we may need to reset the +// iterator to go back to a previously visited node, so we always save the +// "match anchor" node and offset. +// +// Text nodes store their text in an nsTextFragment, which is effectively a +// union of a one-byte string or a two-byte string. Single and double strings +// are intermixed in the dom. We don't have string classes which can deal with +// intermixed strings, so all the handling is done explicitly here. + +char32_t nsFind::DecodeChar(const char16_t* t2b, int32_t* index) const { + char32_t c = t2b[*index]; + if (mFindBackward) { + if (*index >= 1 && NS_IS_SURROGATE_PAIR(t2b[*index - 1], t2b[*index])) { + c = SURROGATE_TO_UCS4(t2b[*index - 1], t2b[*index]); + (*index)--; + } + } else { + if (NS_IS_SURROGATE_PAIR(t2b[*index], t2b[*index + 1])) { + c = SURROGATE_TO_UCS4(t2b[*index], t2b[*index + 1]); + (*index)++; + } + } + return c; +} + +bool nsFind::BreakInBetween(char32_t x, char32_t y) const { + nsAutoStringN<4> text; + AppendUCS4ToUTF16(x, text); + const uint32_t x16Len = text.Length(); + AppendUCS4ToUTF16(y, text); + + intl::WordBreakIteratorUtf16 iter(text); + return *iter.Seek(x16Len - 1) == x16Len; +} + +char32_t nsFind::PeekNextChar(State& aState, bool aAlreadyMatching) const { + // We need to restore the necessary state before this function returns. + StateRestorer restorer(aState); + + while (true) { + const Text* text = aState.GetNextNode(aAlreadyMatching); + if (!text || aState.ForcedBreak()) { + return L'\0'; + } + + const nsTextFragment& frag = text->TextFragment(); + uint32_t len = frag.GetLength(); + if (!len) { + continue; + } + + const char16_t* t2b = nullptr; + const char* t1b = nullptr; + + if (frag.Is2b()) { + t2b = frag.Get2b(); + } else { + t1b = frag.Get1b(); + } + + int32_t index = mFindBackward ? len - 1 : 0; + return t1b ? CHAR_TO_UNICHAR(t1b[index]) : DecodeChar(t2b, &index); + } +} + +#define NBSP_CHARCODE (CHAR_TO_UNICHAR(160)) +#define IsSpace(c) (nsCRT::IsAsciiSpace(c) || (c) == NBSP_CHARCODE) +#define OVERFLOW_PINDEX (mFindBackward ? pindex < 0 : pindex > patLen) +#define DONE_WITH_PINDEX (mFindBackward ? pindex <= 0 : pindex >= patLen) + +// Take nodes out of the tree with NextNode, until null (NextNode will return 0 +// at the end of our range). +NS_IMETHODIMP +nsFind::Find(const nsAString& aPatText, nsRange* aSearchRange, + nsRange* aStartPoint, nsRange* aEndPoint, nsRange** aRangeRet) { + DEBUG_FIND_PRINTF("============== nsFind::Find('%s'%s, %p, %p, %p)\n", + NS_LossyConvertUTF16toASCII(aPatText).get(), + mFindBackward ? " (backward)" : " (forward)", + (void*)aSearchRange, (void*)aStartPoint, (void*)aEndPoint); + + NS_ENSURE_ARG(aSearchRange); + NS_ENSURE_ARG(aStartPoint); + NS_ENSURE_ARG(aEndPoint); + NS_ENSURE_ARG_POINTER(aRangeRet); + + Document* document = + aStartPoint->GetRoot() ? aStartPoint->GetRoot()->OwnerDoc() : nullptr; + NS_ENSURE_ARG(document); + + Element* root = document->GetRootElement(); + NS_ENSURE_ARG(root); + + *aRangeRet = 0; + + nsAutoString patAutoStr(aPatText); + if (!mCaseSensitive) { + ToFoldedCase(patAutoStr); + } + if (!mMatchDiacritics) { + ToNaked(patAutoStr); + } + + // Ignore soft hyphens in the pattern + static const char16_t kShy[] = {CH_SHY, 0}; + patAutoStr.StripChars(kShy); + + const char16_t* patStr = patAutoStr.get(); + int32_t patLen = patAutoStr.Length() - 1; + + // If this function is called with an empty string, we should early exit. + if (patLen < 0) { + return NS_OK; + } + + const int32_t patternStart = mFindBackward ? patLen : 0; + + // current offset into the pattern -- reset to beginning/end: + int32_t pindex = patternStart; + + // Current offset into the fragment + int32_t findex = 0; + + // Direction to move pindex and ptr* + int incr = mFindBackward ? -1 : 1; + + const nsTextFragment* frag = nullptr; + int32_t fragLen = 0; + + // Pointers into the current fragment: + const char16_t* t2b = nullptr; + const char* t1b = nullptr; + + // Keep track of when we're in whitespace: + // (only matters when we're matching) + bool inWhitespace = false; + + // Place to save the range start point in case we find a match: + Text* matchAnchorNode = nullptr; + int32_t matchAnchorOffset = 0; + char32_t matchAnchorChar = 0; + + // Get the end point, so we know when to end searches: + nsINode* endNode = aEndPoint->GetEndContainer(); + uint32_t endOffset = aEndPoint->EndOffset(); + + char32_t c = 0; + char32_t patc = 0; + char32_t prevCharInMatch = 0; + + State state(mFindBackward, *root, *aStartPoint); + Text* current = nullptr; + + auto EndPartialMatch = [&]() -> bool { + // If we didn't match, go back to the beginning of patStr, and set findex + // back to the next char after we started the current match. + // + // There's no need to do this if we're still at the beginning of the pattern + // (this can happen e.g. with whitespace, and prevents exponential + // complexity when scanning a pattern that starts with whitespace). + const bool restart = !!matchAnchorNode && pindex != patternStart; + if (restart) { // we're ending a partial match + findex = matchAnchorOffset; + state.mIterOffset = matchAnchorOffset; + c = matchAnchorChar; + // +incr will be added to findex when we continue + + // Are we going back to a previous node? + if (matchAnchorNode != state.GetCurrentNode()) { + frag = nullptr; + state.PositionAt(*matchAnchorNode); + DEBUG_FIND_PRINTF("Repositioned anchor node\n"); + } + DEBUG_FIND_PRINTF( + "Ending a partial match; findex -> %d, mIterOffset -> %d\n", findex, + state.mIterOffset); + } + matchAnchorNode = nullptr; + matchAnchorOffset = 0; + matchAnchorChar = 0; + inWhitespace = false; + prevCharInMatch = 0; + pindex = patternStart; + DEBUG_FIND_PRINTF("Setting findex back to %d, pindex to %d\n", findex, + pindex); + return restart; + }; + + while (true) { + DEBUG_FIND_PRINTF("Loop (pindex = %d)...\n", pindex); + + // If this is our first time on a new node, reset the pointers: + if (!frag) { + current = state.GetNextNode(!!matchAnchorNode); + if (!current) { + DEBUG_FIND_PRINTF("Reached the end, matching: %d\n", !!matchAnchorNode); + if (EndPartialMatch()) { + continue; + } + return NS_OK; + } + + // We have a new text content. See if we need to force a break due to + // <br>, different blocks or what not. + if (state.ForcedBreak()) { + DEBUG_FIND_PRINTF("Forced break!\n"); + if (EndPartialMatch()) { + continue; + } + // This ensures word breaking thinks it has a new word, which is + // effectively what we want. + c = 0; + } + + frag = ¤t->TextFragment(); + fragLen = frag->GetLength(); + + // Set our starting point in this node. If we're going back to the anchor + // node, which means that we just ended a partial match, use the saved + // offset: + // + // FIXME(emilio): How could current ever be the anchor node, if we had not + // seen current so far? + if (current == matchAnchorNode) { + findex = matchAnchorOffset + (mFindBackward ? 1 : 0); + } else if (state.mIterOffset >= 0) { + findex = state.mIterOffset - (mFindBackward ? 1 : 0); + } else { + findex = mFindBackward ? (fragLen - 1) : 0; + } + + // Offset can only apply to the first node: + state.mIterOffset = -1; + + DEBUG_FIND_PRINTF("Starting from offset %d of %d\n", findex, fragLen); + + // If this is outside the bounds of the string, then skip this node: + if (findex < 0 || findex > fragLen - 1) { + DEBUG_FIND_PRINTF( + "At the end of a text node -- skipping to the next\n"); + frag = nullptr; + continue; + } + + if (frag->Is2b()) { + t2b = frag->Get2b(); + t1b = nullptr; +#ifdef DEBUG_FIND + nsAutoString str2(t2b, fragLen); + DEBUG_FIND_PRINTF("2 byte, '%s'\n", + NS_LossyConvertUTF16toASCII(str2).get()); +#endif + } else { + t1b = frag->Get1b(); + t2b = nullptr; +#ifdef DEBUG_FIND + nsAutoCString str1(t1b, fragLen); + DEBUG_FIND_PRINTF("1 byte, '%s'\n", str1.get()); +#endif + } + } else { + // Still on the old node. Advance the pointers, then see if we need to + // pull a new node. + findex += incr; + DEBUG_FIND_PRINTF("Same node -- (%d, %d)\n", pindex, findex); + if (mFindBackward ? (findex < 0) : (findex >= fragLen)) { + DEBUG_FIND_PRINTF( + "Will need to pull a new node: mAO = %d, frag len=%d\n", + matchAnchorOffset, fragLen); + // Done with this node. Pull a new one. + frag = nullptr; + continue; + } + } + + // Have we gone past the endpoint yet? If we have, and we're not in the + // middle of a match, return. + if (state.GetCurrentNode() == endNode && + ((mFindBackward && findex < static_cast<int32_t>(endOffset)) || + (!mFindBackward && findex > static_cast<int32_t>(endOffset)))) { + DEBUG_FIND_PRINTF("Reached the end and not in the middle of a match\n"); + return NS_OK; + } + + // Save the previous character for word boundary detection + char32_t prevChar = c; + // The two characters we'll be comparing are c and patc. If not matching + // diacritics, don't leave c set to a combining diacritical mark. (patc is + // already guaranteed to not be a combining diacritical mark.) + c = (t2b ? DecodeChar(t2b, &findex) : CHAR_TO_UNICHAR(t1b[findex])); + if (!mMatchDiacritics && IsCombiningDiacritic(c) && + !intl::UnicodeProperties::IsMathOrMusicSymbol(prevChar)) { + continue; + } + patc = DecodeChar(patStr, &pindex); + + DEBUG_FIND_PRINTF( + "Comparing '%c'=%#x to '%c'=%#x (%d of %d), findex=%d%s\n", (char)c, + (int)c, (char)patc, (int)patc, pindex, patLen, findex, + inWhitespace ? " (inWhitespace)" : ""); + + // Do we need to go back to non-whitespace mode? If inWhitespace, then this + // space in the pat str has already matched at least one space in the + // document. + if (inWhitespace && !IsSpace(c)) { + inWhitespace = false; + pindex += incr; +#ifdef DEBUG + // This shouldn't happen -- if we were still matching, and we were at the + // end of the pat string, then we should have caught it in the last + // iteration and returned success. + if (OVERFLOW_PINDEX) { + NS_ASSERTION(false, "Missed a whitespace match"); + } +#endif + patc = DecodeChar(patStr, &pindex); + } + if (!inWhitespace && IsSpace(patc)) { + inWhitespace = true; + } else if (!inWhitespace) { + if (!mCaseSensitive) { + c = ToFoldedCase(c); + } + if (!mMatchDiacritics) { + c = ToNaked(c); + } + } + + if (c == CH_SHY) { + // ignore soft hyphens in the document + continue; + } + + if (pindex != patternStart && c != patc && !inWhitespace) { + // A non-matching '\n' between CJ characters is ignored + if (c == '\n' && t2b && IS_CJ_CHAR(prevCharInMatch)) { + int32_t nindex = findex + incr; + if (mFindBackward ? (nindex >= 0) : (nindex < fragLen)) { + if (IS_CJ_CHAR(t2b[nindex])) { + continue; + } + } + } + + // We also ignore ZWSP and other default-ignorable characters. + if (IsDefaultIgnorable(c)) { + continue; + } + } + + // Figure whether the previous char is a word-breaking one, + // if we care about word boundaries. + bool wordBreakPrev = true; + if (mEntireWord && prevChar) { + if (prevChar == NBSP_CHARCODE) { + prevChar = CHAR_TO_UNICHAR(' '); + } + wordBreakPrev = BreakInBetween(prevChar, c); + } + + // Compare. Match if we're in whitespace and c is whitespace, or if the + // characters match and at least one of the following is true: + // a) we're not matching the entire word + // b) a match has already been stored + // c) the previous character is a different "class" than the current + // character. + if ((c == patc && (!mEntireWord || matchAnchorNode || wordBreakPrev)) || + (inWhitespace && IsSpace(c))) { + prevCharInMatch = c; + if (inWhitespace) { + DEBUG_FIND_PRINTF("YES (whitespace)(%d of %d)\n", pindex, patLen); + } else { + DEBUG_FIND_PRINTF("YES! '%c' == '%c' (%d of %d)\n", c, patc, pindex, + patLen); + } + + // Save the range anchors if we haven't already: + if (!matchAnchorNode) { + matchAnchorNode = state.GetCurrentNode(); + matchAnchorOffset = findex; + if (!IS_IN_BMP(c)) { + matchAnchorOffset -= incr; + } + matchAnchorChar = c; + } + + // Are we done? + if (DONE_WITH_PINDEX) { + // Matched the whole string! + DEBUG_FIND_PRINTF("Found a match!\n"); + + // Make the range: + // Check for word break (if necessary) + if (mEntireWord || inWhitespace) { + int32_t nextfindex = findex + incr; + + char32_t nextChar; + // If still in array boundaries, get nextChar. + if (mFindBackward ? (nextfindex >= 0) : (nextfindex < fragLen)) { + if (t2b) { + nextChar = DecodeChar(t2b, &nextfindex); + } else { + nextChar = CHAR_TO_UNICHAR(t1b[nextfindex]); + } + } else { + // Get next character from the next node. + nextChar = PeekNextChar(state, !!matchAnchorNode); + } + + if (nextChar == NBSP_CHARCODE) { + nextChar = CHAR_TO_UNICHAR(' '); + } + + // If a word break isn't there when it needs to be, reset search. + if (mEntireWord && nextChar && !BreakInBetween(c, nextChar)) { + matchAnchorNode = nullptr; + continue; + } + + if (inWhitespace && IsSpace(nextChar)) { + // If the next character is also an space, keep going, this space + // will collapse. + continue; + } + } + + int32_t matchStartOffset; + int32_t matchEndOffset; + // convert char index to range point: + int32_t mao = matchAnchorOffset + (mFindBackward ? 1 : 0); + Text* startParent; + Text* endParent; + if (mFindBackward) { + startParent = current; + endParent = matchAnchorNode; + matchStartOffset = findex; + matchEndOffset = mao; + } else { + startParent = matchAnchorNode; + endParent = current; + matchStartOffset = mao; + matchEndOffset = findex + 1; + } + + RefPtr<nsRange> range = nsRange::Create(current); + if (startParent && endParent && IsVisibleNode(startParent) && + IsVisibleNode(endParent)) { + IgnoredErrorResult rv; + range->SetStart(*startParent, matchStartOffset, rv); + if (!rv.Failed()) { + range->SetEnd(*endParent, matchEndOffset, rv); + } + if (!rv.Failed()) { + range.forget(aRangeRet); + return NS_OK; + } + } + + // This match is no good, continue on in document + matchAnchorNode = nullptr; + } + + if (matchAnchorNode) { + // Not done, but still matching. Advance and loop around for the next + // characters. But don't advance from a space to a non-space: + if (!inWhitespace || DONE_WITH_PINDEX || + IsSpace(patStr[pindex + incr])) { + pindex += incr; + inWhitespace = false; + DEBUG_FIND_PRINTF("Advancing pindex to %d\n", pindex); + } + + continue; + } + } + + DEBUG_FIND_PRINTF("NOT: %c == %c\n", c, patc); + EndPartialMatch(); + } +} diff --git a/toolkit/components/find/nsFind.h b/toolkit/components/find/nsFind.h new file mode 100644 index 0000000000..bf431a2d2f --- /dev/null +++ b/toolkit/components/find/nsFind.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsFind_h__ +#define nsFind_h__ + +#include "nsIFind.h" + +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsINode.h" + +#define NS_FIND_CONTRACTID "@mozilla.org/embedcomp/rangefind;1" + +#define NS_FIND_CID \ + { \ + 0x471f4944, 0x1dd2, 0x11b2, { \ + 0x87, 0xac, 0x90, 0xbe, 0x0a, 0x51, 0xd6, 0x09 \ + } \ + } + +class nsFind : public nsIFind { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIFIND + NS_DECL_CYCLE_COLLECTION_CLASS(nsFind) + + protected: + virtual ~nsFind() = default; + + // Parameters set from the interface: + bool mFindBackward = false; + bool mCaseSensitive = false; + bool mMatchDiacritics = false; + + // Use "find entire words" mode by setting mEntireWord to true; or false to + // disable "entire words" mode. + bool mEntireWord = false; + + struct State; + class StateRestorer; + + // Extract a character from a string, handling surrogate pairs and + // incrementing the index if a surrogate pair is encountered + char32_t DecodeChar(const char16_t* t2b, int32_t* index) const; + + // Determine if a line break can occur between two characters + // + // This could be improved because some languages require more context than two + // characters to determine where line breaks can occur + bool BreakInBetween(char32_t x, char32_t y) const; + + // Get the first character from the next node (last if mFindBackward). + // + // This will mutate the state, but then restore it afterwards. + char32_t PeekNextChar(State&, bool aAlreadyMatching) const; +}; + +#endif // nsFind_h__ diff --git a/toolkit/components/find/nsFindService.cpp b/toolkit/components/find/nsFindService.cpp new file mode 100644 index 0000000000..7499132c2c --- /dev/null +++ b/toolkit/components/find/nsFindService.cpp @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * The sole purpose of the Find service is to store globally the + * last used Find settings + * + */ + +#include "nsFindService.h" + +nsFindService::nsFindService() + : mFindBackwards(false), + mWrapFind(true), + mEntireWord(false), + mMatchCase(false) {} + +nsFindService::~nsFindService() = default; + +NS_IMPL_ISUPPORTS(nsFindService, nsIFindService) + +NS_IMETHODIMP nsFindService::GetSearchString(nsAString& aSearchString) { + aSearchString = mSearchString; + return NS_OK; +} + +NS_IMETHODIMP nsFindService::SetSearchString(const nsAString& aSearchString) { + mSearchString = aSearchString; + return NS_OK; +} + +NS_IMETHODIMP nsFindService::GetReplaceString(nsAString& aReplaceString) { + aReplaceString = mReplaceString; + return NS_OK; +} +NS_IMETHODIMP nsFindService::SetReplaceString(const nsAString& aReplaceString) { + mReplaceString = aReplaceString; + return NS_OK; +} + +NS_IMETHODIMP nsFindService::GetFindBackwards(bool* aFindBackwards) { + NS_ENSURE_ARG_POINTER(aFindBackwards); + *aFindBackwards = mFindBackwards; + return NS_OK; +} +NS_IMETHODIMP nsFindService::SetFindBackwards(bool aFindBackwards) { + mFindBackwards = aFindBackwards; + return NS_OK; +} + +NS_IMETHODIMP nsFindService::GetWrapFind(bool* aWrapFind) { + NS_ENSURE_ARG_POINTER(aWrapFind); + *aWrapFind = mWrapFind; + return NS_OK; +} +NS_IMETHODIMP nsFindService::SetWrapFind(bool aWrapFind) { + mWrapFind = aWrapFind; + return NS_OK; +} + +NS_IMETHODIMP nsFindService::GetEntireWord(bool* aEntireWord) { + NS_ENSURE_ARG_POINTER(aEntireWord); + *aEntireWord = mEntireWord; + return NS_OK; +} +NS_IMETHODIMP nsFindService::SetEntireWord(bool aEntireWord) { + mEntireWord = aEntireWord; + return NS_OK; +} + +NS_IMETHODIMP nsFindService::GetMatchCase(bool* aMatchCase) { + NS_ENSURE_ARG_POINTER(aMatchCase); + *aMatchCase = mMatchCase; + return NS_OK; +} +NS_IMETHODIMP nsFindService::SetMatchCase(bool aMatchCase) { + mMatchCase = aMatchCase; + return NS_OK; +} + +NS_IMETHODIMP nsFindService::GetMatchDiacritics(bool* aMatchDiacritics) { + NS_ENSURE_ARG_POINTER(aMatchDiacritics); + *aMatchDiacritics = mMatchDiacritics; + return NS_OK; +} +NS_IMETHODIMP nsFindService::SetMatchDiacritics(bool aMatchDiacritics) { + mMatchDiacritics = aMatchDiacritics; + return NS_OK; +} diff --git a/toolkit/components/find/nsFindService.h b/toolkit/components/find/nsFindService.h new file mode 100644 index 0000000000..5204076f5d --- /dev/null +++ b/toolkit/components/find/nsFindService.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * The sole purpose of the Find service is to store globally the + * last used Find settings + * + */ + +#include "nsString.h" + +#include "nsIFindService.h" + +// {5060b803-340e-11d5-be5b-b3e063ec6a3c} +#define NS_FIND_SERVICE_CID \ + { \ + 0x5060b803, 0x340e, 0x11d5, { \ + 0xbe, 0x5b, 0xb3, 0xe0, 0x63, 0xec, 0x6a, 0x3c \ + } \ + } + +#define NS_FIND_SERVICE_CONTRACTID "@mozilla.org/find/find_service;1" + +class nsFindService : public nsIFindService { + public: + nsFindService(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIFINDSERVICE + + protected: + virtual ~nsFindService(); + + nsString mSearchString; + nsString mReplaceString; + + bool mFindBackwards; + bool mWrapFind; + bool mEntireWord; + bool mMatchCase; + bool mMatchDiacritics; +}; diff --git a/toolkit/components/find/nsIFind.idl b/toolkit/components/find/nsIFind.idl new file mode 100644 index 0000000000..1e8016514b --- /dev/null +++ b/toolkit/components/find/nsIFind.idl @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl Range; + +[scriptable, uuid(40aba110-2a56-4678-be90-e2c17a9ae7d7)] +interface nsIFind : nsISupports +{ + attribute boolean findBackwards; + attribute boolean caseSensitive; + attribute boolean entireWord; + attribute boolean matchDiacritics; + + /** + * Find some text in the current context. The implementation is + * responsible for performing the find and highlighting the text. + * + * @param aPatText The text to search for. + * @param aSearchRange A Range specifying domain of search. + * @param aStartPoint A Range specifying search start point. + * If not collapsed, we'll start from + * end (forward) or start (backward). + * @param aEndPoint A Range specifying search end point. + * If not collapsed, we'll end at + * end (forward) or start (backward). + * @retval A range spanning the match that was found (or null). + */ + Range Find(in AString aPatText, in Range aSearchRange, + in Range aStartPoint, in Range aEndPoint); +}; diff --git a/toolkit/components/find/nsIFindService.idl b/toolkit/components/find/nsIFindService.idl new file mode 100644 index 0000000000..f5a5e18ce6 --- /dev/null +++ b/toolkit/components/find/nsIFindService.idl @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(5060b801-340e-11d5-be5b-b3e063ec6a3c)] +interface nsIFindService : nsISupports +{ + + /* + * The sole purpose of the Find service is to store globally the + * last used Find settings + * + */ + + attribute AString searchString; + attribute AString replaceString; + + attribute boolean findBackwards; + attribute boolean wrapFind; + attribute boolean entireWord; + attribute boolean matchCase; + attribute boolean matchDiacritics; + +}; diff --git a/toolkit/components/find/nsIWebBrowserFind.idl b/toolkit/components/find/nsIWebBrowserFind.idl new file mode 100644 index 0000000000..f06941329c --- /dev/null +++ b/toolkit/components/find/nsIWebBrowserFind.idl @@ -0,0 +1,152 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +#include "domstubs.idl" + +interface mozIDOMWindowProxy; + +/* THIS IS A PUBLIC EMBEDDING API */ + + +/** + * nsIWebBrowserFind + * + * Searches for text in a web browser. + * + * Get one by doing a GetInterface on an nsIWebBrowser. + * + * By default, the implementation will search the focussed frame, or + * if there is no focussed frame, the web browser content area. It + * does not by default search subframes or iframes. To change this + * behaviour, and to explicitly set the frame to search, + * QueryInterface to nsIWebBrowserFindInFrames. + */ + +[scriptable, uuid(e4920136-b3e0-49e0-b1cd-6c783d2591a8)] +interface nsIWebBrowserFind : nsISupports +{ + /** + * findNext + * + * Finds, highlights, and scrolls into view the next occurrence of the + * search string, using the current search settings. Fails if the + * search string is empty. + * + * @return Whether an occurrence was found + */ + boolean findNext(); + + /** + * searchString + * + * The string to search for. This must be non-empty to search. + */ + attribute AString searchString; + + /** + * findBackwards + * + * Whether to find backwards (towards the beginning of the document). + * Default is false (search forward). + */ + attribute boolean findBackwards; + + /** + * wrapFind + * + * Whether the search wraps around to the start (or end) of the document + * if no match was found between the current position and the end (or + * beginning). Works correctly when searching backwards. Default is + * false. + */ + attribute boolean wrapFind; + + /** + * entireWord + * + * Whether to match entire words only. Default is false. + */ + attribute boolean entireWord; + + /** + * matchCase + * + * Whether to match case (case sensitive) when searching. Default is false. + */ + attribute boolean matchCase; + + /** + * matchDiacritics + * + * Whether to match diacritics when searching. Default is false. + */ + attribute boolean matchDiacritics; + + /** + * searchFrames + * + * Whether to search through all frames in the content area. Default is true. + * + * Note that you can control whether the search propagates into child or + * parent frames explicitly using nsIWebBrowserFindInFrames, but if one, + * but not both, of searchSubframes and searchParentFrames are set, this + * returns false. + */ + attribute boolean searchFrames; +}; + + + +/** + * nsIWebBrowserFindInFrames + * + * Controls how find behaves when multiple frames or iframes are present. + * + * Get by doing a QueryInterface from nsIWebBrowserFind. + */ + +[scriptable, uuid(e0f5d182-34bc-11d5-be5b-b760676c6ebc)] +interface nsIWebBrowserFindInFrames : nsISupports +{ + /** + * currentSearchFrame + * + * Frame at which to start the search. Once the search is done, this will + * be set to be the last frame searched, whether or not a result was found. + * Has to be equal to or contained within the rootSearchFrame. + */ + attribute mozIDOMWindowProxy currentSearchFrame; + + /** + * rootSearchFrame + * + * Frame within which to confine the search (normally the content area frame). + * Set this to only search a subtree of the frame hierarchy. + */ + attribute mozIDOMWindowProxy rootSearchFrame; + + /** + * searchSubframes + * + * Whether to recurse down into subframes while searching. Default is true. + * + * Setting nsIWebBrowserfind.searchFrames to true sets this to true. + */ + attribute boolean searchSubframes; + + /** + * searchParentFrames + * + * Whether to allow the search to propagate out of the currentSearchFrame into its + * parent frame(s). Search is always confined within the rootSearchFrame. Default + * is true. + * + * Setting nsIWebBrowserfind.searchFrames to true sets this to true. + */ + attribute boolean searchParentFrames; + +}; diff --git a/toolkit/components/find/nsWebBrowserFind.cpp b/toolkit/components/find/nsWebBrowserFind.cpp new file mode 100644 index 0000000000..2e06f65fc8 --- /dev/null +++ b/toolkit/components/find/nsWebBrowserFind.cpp @@ -0,0 +1,764 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsWebBrowserFind.h" + +// Only need this for NS_FIND_CONTRACTID, +// else we could use nsRange.h and nsIFind.h. +#include "nsFind.h" + +#include "mozilla/dom/ScriptSettings.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsPIDOMWindow.h" +#include "nsIDocShell.h" +#include "nsPresContext.h" +#include "mozilla/dom/Document.h" +#include "nsISelectionController.h" +#include "nsIFrame.h" +#include "nsITextControlFrame.h" +#include "nsReadableUtils.h" +#include "nsIContent.h" +#include "nsContentCID.h" +#include "nsIObserverService.h" +#include "nsISupportsPrimitives.h" +#include "nsFind.h" +#include "nsError.h" +#include "nsFocusManager.h" +#include "nsRange.h" +#include "mozilla/PresShell.h" +#include "mozilla/Services.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Selection.h" +#include "nsISimpleEnumerator.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsGenericHTMLElement.h" + +#if DEBUG +# include "nsIWebNavigation.h" +# include "nsString.h" +#endif + +using namespace mozilla; +using mozilla::dom::Document; +using mozilla::dom::Element; +using mozilla::dom::Selection; + +nsWebBrowserFind::nsWebBrowserFind() + : mFindBackwards(false), + mWrapFind(false), + mEntireWord(false), + mMatchCase(false), + mMatchDiacritics(false), + mSearchSubFrames(true), + mSearchParentFrames(true) {} + +nsWebBrowserFind::~nsWebBrowserFind() = default; + +NS_IMPL_ISUPPORTS(nsWebBrowserFind, nsIWebBrowserFind, + nsIWebBrowserFindInFrames) + +NS_IMETHODIMP +nsWebBrowserFind::FindNext(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + + NS_ENSURE_TRUE(CanFindNext(), NS_ERROR_NOT_INITIALIZED); + + nsresult rv = NS_OK; + nsCOMPtr<nsPIDOMWindowOuter> searchFrame = + do_QueryReferent(mCurrentSearchFrame); + NS_ENSURE_TRUE(searchFrame, NS_ERROR_NOT_INITIALIZED); + + nsCOMPtr<nsPIDOMWindowOuter> rootFrame = do_QueryReferent(mRootSearchFrame); + NS_ENSURE_TRUE(rootFrame, NS_ERROR_NOT_INITIALIZED); + + // first, if there's a "cmd_findagain" observer around, check to see if it + // wants to perform the find again command . If it performs the find again + // it will return true, in which case we exit ::FindNext() early. + // Otherwise, nsWebBrowserFind needs to perform the find again command itself + // this is used by nsTypeAheadFind, which controls find again when it was + // the last executed find in the current window. + nsCOMPtr<nsIObserverService> observerSvc = + mozilla::services::GetObserverService(); + if (observerSvc) { + nsCOMPtr<nsISupportsInterfacePointer> windowSupportsData = + do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsISupports> searchWindowSupports = do_QueryInterface(rootFrame); + windowSupportsData->SetData(searchWindowSupports); + observerSvc->NotifyObservers(windowSupportsData, + "nsWebBrowserFind_FindAgain", + mFindBackwards ? u"up" : u"down"); + windowSupportsData->GetData(getter_AddRefs(searchWindowSupports)); + // findnext performed if search window data cleared out + *aResult = searchWindowSupports == nullptr; + if (*aResult) { + return NS_OK; + } + } + + // next, look in the current frame. If found, return. + + // Beware! This may flush notifications via synchronous + // ScrollSelectionIntoView. + rv = SearchInFrame(searchFrame, false, aResult); + if (NS_FAILED(rv)) { + return rv; + } + if (*aResult) { + return OnFind(searchFrame); // we are done + } + + // if we are not searching other frames, return + if (!mSearchSubFrames && !mSearchParentFrames) { + return NS_OK; + } + + nsIDocShell* rootDocShell = rootFrame->GetDocShell(); + if (!rootDocShell) { + return NS_ERROR_FAILURE; + } + + auto enumDirection = mFindBackwards ? nsIDocShell::ENUMERATE_BACKWARDS + : nsIDocShell::ENUMERATE_FORWARDS; + + nsTArray<RefPtr<nsIDocShell>> docShells; + rv = rootDocShell->GetAllDocShellsInSubtree(nsIDocShellTreeItem::typeAll, + enumDirection, docShells); + if (NS_FAILED(rv)) { + return rv; + } + + // remember where we started + nsCOMPtr<nsIDocShellTreeItem> startingItem = searchFrame->GetDocShell(); + + // XXX We should avoid searching in frameset documents here. + // We also need to honour mSearchSubFrames and mSearchParentFrames. + bool doFind = false; + for (const auto& curItem : docShells) { + if (doFind) { + searchFrame = curItem->GetWindow(); + if (!searchFrame) { + break; + } + + OnStartSearchFrame(searchFrame); + + // Beware! This may flush notifications via synchronous + // ScrollSelectionIntoView. + rv = SearchInFrame(searchFrame, false, aResult); + if (NS_FAILED(rv)) { + return rv; + } + if (*aResult) { + return OnFind(searchFrame); // we are done + } + + OnEndSearchFrame(searchFrame); + } + + if (curItem.get() == startingItem.get()) { + doFind = true; // start looking in frames after this one + } + } + + if (!mWrapFind) { + // remember where we left off + SetCurrentSearchFrame(searchFrame); + return NS_OK; + } + + // From here on, we're wrapping, first through the other frames, then finally + // from the beginning of the starting frame back to the starting point. + + // because nsISimpleEnumerator is bad and isn't resettable, I have to + // make a new one + rv = rootDocShell->GetAllDocShellsInSubtree(nsIDocShellTreeItem::typeAll, + enumDirection, docShells); + if (NS_FAILED(rv)) { + return rv; + } + + for (const auto& curItem : docShells) { + searchFrame = curItem->GetWindow(); + if (!searchFrame) { + rv = NS_ERROR_FAILURE; + break; + } + + if (curItem.get() == startingItem.get()) { + // Beware! This may flush notifications via synchronous + // ScrollSelectionIntoView. + rv = SearchInFrame(searchFrame, true, aResult); + if (NS_FAILED(rv)) { + return rv; + } + if (*aResult) { + return OnFind(searchFrame); // we are done + } + break; + } + + OnStartSearchFrame(searchFrame); + + // Beware! This may flush notifications via synchronous + // ScrollSelectionIntoView. + rv = SearchInFrame(searchFrame, false, aResult); + if (NS_FAILED(rv)) { + return rv; + } + if (*aResult) { + return OnFind(searchFrame); // we are done + } + + OnEndSearchFrame(searchFrame); + } + + // remember where we left off + SetCurrentSearchFrame(searchFrame); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Something failed"); + return rv; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetSearchString(nsAString& aSearchString) { + aSearchString = mSearchString; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetSearchString(const nsAString& aSearchString) { + mSearchString = aSearchString; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetFindBackwards(bool* aFindBackwards) { + NS_ENSURE_ARG_POINTER(aFindBackwards); + *aFindBackwards = mFindBackwards; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetFindBackwards(bool aFindBackwards) { + mFindBackwards = aFindBackwards; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetWrapFind(bool* aWrapFind) { + NS_ENSURE_ARG_POINTER(aWrapFind); + *aWrapFind = mWrapFind; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetWrapFind(bool aWrapFind) { + mWrapFind = aWrapFind; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetEntireWord(bool* aEntireWord) { + NS_ENSURE_ARG_POINTER(aEntireWord); + *aEntireWord = mEntireWord; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetEntireWord(bool aEntireWord) { + mEntireWord = aEntireWord; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetMatchCase(bool* aMatchCase) { + NS_ENSURE_ARG_POINTER(aMatchCase); + *aMatchCase = mMatchCase; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetMatchCase(bool aMatchCase) { + mMatchCase = aMatchCase; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetMatchDiacritics(bool* aMatchDiacritics) { + NS_ENSURE_ARG_POINTER(aMatchDiacritics); + *aMatchDiacritics = mMatchDiacritics; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetMatchDiacritics(bool aMatchDiacritics) { + mMatchDiacritics = aMatchDiacritics; + return NS_OK; +} + +void nsWebBrowserFind::SetSelectionAndScroll(nsPIDOMWindowOuter* aWindow, + nsRange* aRange) { + RefPtr<Document> doc = aWindow->GetDoc(); + if (!doc) { + return; + } + + PresShell* presShell = doc->GetPresShell(); + if (!presShell) { + return; + } + + nsCOMPtr<nsINode> node = aRange->GetStartContainer(); + nsCOMPtr<nsIContent> content(do_QueryInterface(node)); + nsIFrame* frame = content->GetPrimaryFrame(); + if (!frame) { + return; + } + nsCOMPtr<nsISelectionController> selCon; + frame->GetSelectionController(presShell->GetPresContext(), + getter_AddRefs(selCon)); + + // since the match could be an anonymous textnode inside a + // <textarea> or text <input>, we need to get the outer frame + nsITextControlFrame* tcFrame = nullptr; + for (; content; content = content->GetParent()) { + if (!content->IsInNativeAnonymousSubtree()) { + nsIFrame* f = content->GetPrimaryFrame(); + if (!f) { + return; + } + tcFrame = do_QueryFrame(f); + break; + } + } + + selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON); + RefPtr<Selection> selection = + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (selection) { + selection->RemoveAllRanges(IgnoreErrors()); + selection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, + IgnoreErrors()); + + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + if (tcFrame) { + RefPtr<Element> newFocusedElement = Element::FromNode(content); + fm->SetFocus(newFocusedElement, nsIFocusManager::FLAG_NOSCROLL); + } else { + RefPtr<Element> result; + fm->MoveFocus(aWindow, nullptr, nsIFocusManager::MOVEFOCUS_CARET, + nsIFocusManager::FLAG_NOSCROLL, getter_AddRefs(result)); + } + } + + // Scroll if necessary to make the selection visible: + // Must be the last thing to do - bug 242056 + + // After ScrollSelectionIntoView(), the pending notifications might be + // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. + selCon->ScrollSelectionIntoView( + nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_WHOLE_SELECTION, + nsISelectionController::SCROLL_CENTER_VERTICALLY | + nsISelectionController::SCROLL_SYNCHRONOUS); + } +} + +// Adapted from TextServicesDocument::GetDocumentContentRootNode +nsresult nsWebBrowserFind::GetRootNode(Document* aDoc, Element** aNode) { + NS_ENSURE_ARG_POINTER(aDoc); + NS_ENSURE_ARG_POINTER(aNode); + *aNode = 0; + + if (aDoc->IsHTMLOrXHTML()) { + Element* body = aDoc->GetBody(); + NS_ENSURE_ARG_POINTER(body); + NS_ADDREF(*aNode = body); + return NS_OK; + } + + // For non-HTML documents, the content root node will be the doc element. + Element* root = aDoc->GetDocumentElement(); + NS_ENSURE_ARG_POINTER(root); + NS_ADDREF(*aNode = root); + return NS_OK; +} + +nsresult nsWebBrowserFind::SetRangeAroundDocument(nsRange* aSearchRange, + nsRange* aStartPt, + nsRange* aEndPt, + Document* aDoc) { + RefPtr<Element> bodyContent; + nsresult rv = GetRootNode(aDoc, getter_AddRefs(bodyContent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_ARG_POINTER(bodyContent); + + uint32_t childCount = bodyContent->GetChildCount(); + + aSearchRange->SetStart(*bodyContent, 0, IgnoreErrors()); + aSearchRange->SetEnd(*bodyContent, childCount, IgnoreErrors()); + + if (mFindBackwards) { + aStartPt->SetStart(*bodyContent, childCount, IgnoreErrors()); + aStartPt->SetEnd(*bodyContent, childCount, IgnoreErrors()); + aEndPt->SetStart(*bodyContent, 0, IgnoreErrors()); + aEndPt->SetEnd(*bodyContent, 0, IgnoreErrors()); + } else { + aStartPt->SetStart(*bodyContent, 0, IgnoreErrors()); + aStartPt->SetEnd(*bodyContent, 0, IgnoreErrors()); + aEndPt->SetStart(*bodyContent, childCount, IgnoreErrors()); + aEndPt->SetEnd(*bodyContent, childCount, IgnoreErrors()); + } + + return NS_OK; +} + +// Set the range to go from the end of the current selection to the end of the +// document (forward), or beginning to beginning (reverse). or around the whole +// document if there's no selection. +nsresult nsWebBrowserFind::GetSearchLimits(nsRange* aSearchRange, + nsRange* aStartPt, nsRange* aEndPt, + Document* aDoc, Selection* aSel, + bool aWrap) { + NS_ENSURE_ARG_POINTER(aSel); + + // There is a selection. + const uint32_t rangeCount = aSel->RangeCount(); + if (rangeCount < 1) { + return SetRangeAroundDocument(aSearchRange, aStartPt, aEndPt, aDoc); + } + + // Need bodyContent, for the start/end of the document + RefPtr<Element> bodyContent; + nsresult rv = GetRootNode(aDoc, getter_AddRefs(bodyContent)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_ARG_POINTER(bodyContent); + + uint32_t childCount = bodyContent->GetChildCount(); + + // There are four possible range endpoints we might use: + // DocumentStart, SelectionStart, SelectionEnd, DocumentEnd. + + RefPtr<const nsRange> range; + nsCOMPtr<nsINode> node; + uint32_t offset; + + // Prevent the security checks in nsRange from getting into effect for the + // purposes of determining the search range. These ranges will never be + // exposed to content. + mozilla::dom::AutoNoJSAPI nojsapi; + + // Forward, not wrapping: SelEnd to DocEnd + if (!mFindBackwards && !aWrap) { + // This isn't quite right, since the selection's ranges aren't + // necessarily in order; but they usually will be. + range = aSel->GetRangeAt(rangeCount - 1); + if (!range) { + return NS_ERROR_UNEXPECTED; + } + node = range->GetEndContainer(); + if (!node) { + return NS_ERROR_UNEXPECTED; + } + offset = range->EndOffset(); + + aSearchRange->SetStart(*node, offset, IgnoreErrors()); + aSearchRange->SetEnd(*bodyContent, childCount, IgnoreErrors()); + aStartPt->SetStart(*node, offset, IgnoreErrors()); + aStartPt->SetEnd(*node, offset, IgnoreErrors()); + aEndPt->SetStart(*bodyContent, childCount, IgnoreErrors()); + aEndPt->SetEnd(*bodyContent, childCount, IgnoreErrors()); + } + // Backward, not wrapping: DocStart to SelStart + else if (mFindBackwards && !aWrap) { + range = aSel->GetRangeAt(0); + if (!range) { + return NS_ERROR_UNEXPECTED; + } + node = range->GetStartContainer(); + if (!node) { + return NS_ERROR_UNEXPECTED; + } + offset = range->StartOffset(); + + aSearchRange->SetStart(*bodyContent, 0, IgnoreErrors()); + aSearchRange->SetEnd(*bodyContent, childCount, IgnoreErrors()); + aStartPt->SetStart(*node, offset, IgnoreErrors()); + aStartPt->SetEnd(*node, offset, IgnoreErrors()); + aEndPt->SetStart(*bodyContent, 0, IgnoreErrors()); + aEndPt->SetEnd(*bodyContent, 0, IgnoreErrors()); + } + // Forward, wrapping: DocStart to SelEnd + else if (!mFindBackwards && aWrap) { + range = aSel->GetRangeAt(rangeCount - 1); + if (!range) { + return NS_ERROR_UNEXPECTED; + } + node = range->GetEndContainer(); + if (!node) { + return NS_ERROR_UNEXPECTED; + } + offset = range->EndOffset(); + + aSearchRange->SetStart(*bodyContent, 0, IgnoreErrors()); + aSearchRange->SetEnd(*bodyContent, childCount, IgnoreErrors()); + aStartPt->SetStart(*bodyContent, 0, IgnoreErrors()); + aStartPt->SetEnd(*bodyContent, 0, IgnoreErrors()); + aEndPt->SetStart(*node, offset, IgnoreErrors()); + aEndPt->SetEnd(*node, offset, IgnoreErrors()); + } + // Backward, wrapping: SelStart to DocEnd + else if (mFindBackwards && aWrap) { + range = aSel->GetRangeAt(0); + if (!range) { + return NS_ERROR_UNEXPECTED; + } + node = range->GetStartContainer(); + if (!node) { + return NS_ERROR_UNEXPECTED; + } + offset = range->StartOffset(); + + aSearchRange->SetStart(*bodyContent, 0, IgnoreErrors()); + aSearchRange->SetEnd(*bodyContent, childCount, IgnoreErrors()); + aStartPt->SetStart(*bodyContent, childCount, IgnoreErrors()); + aStartPt->SetEnd(*bodyContent, childCount, IgnoreErrors()); + aEndPt->SetStart(*node, offset, IgnoreErrors()); + aEndPt->SetEnd(*node, offset, IgnoreErrors()); + } + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetSearchFrames(bool* aSearchFrames) { + NS_ENSURE_ARG_POINTER(aSearchFrames); + // this only returns true if we are searching both sub and parent frames. + // There is ambiguity if the caller has previously set one, but not both of + // these. + *aSearchFrames = mSearchSubFrames && mSearchParentFrames; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetSearchFrames(bool aSearchFrames) { + mSearchSubFrames = aSearchFrames; + mSearchParentFrames = aSearchFrames; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetCurrentSearchFrame( + mozIDOMWindowProxy** aCurrentSearchFrame) { + NS_ENSURE_ARG_POINTER(aCurrentSearchFrame); + nsCOMPtr<mozIDOMWindowProxy> searchFrame = + do_QueryReferent(mCurrentSearchFrame); + searchFrame.forget(aCurrentSearchFrame); + return (*aCurrentSearchFrame) ? NS_OK : NS_ERROR_NOT_INITIALIZED; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetCurrentSearchFrame( + mozIDOMWindowProxy* aCurrentSearchFrame) { + // is it ever valid to set this to null? + NS_ENSURE_ARG(aCurrentSearchFrame); + mCurrentSearchFrame = do_GetWeakReference(aCurrentSearchFrame); + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetRootSearchFrame(mozIDOMWindowProxy** aRootSearchFrame) { + NS_ENSURE_ARG_POINTER(aRootSearchFrame); + nsCOMPtr<mozIDOMWindowProxy> searchFrame = do_QueryReferent(mRootSearchFrame); + searchFrame.forget(aRootSearchFrame); + return (*aRootSearchFrame) ? NS_OK : NS_ERROR_NOT_INITIALIZED; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetRootSearchFrame(mozIDOMWindowProxy* aRootSearchFrame) { + // is it ever valid to set this to null? + NS_ENSURE_ARG(aRootSearchFrame); + mRootSearchFrame = do_GetWeakReference(aRootSearchFrame); + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetSearchSubframes(bool* aSearchSubframes) { + NS_ENSURE_ARG_POINTER(aSearchSubframes); + *aSearchSubframes = mSearchSubFrames; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetSearchSubframes(bool aSearchSubframes) { + mSearchSubFrames = aSearchSubframes; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::GetSearchParentFrames(bool* aSearchParentFrames) { + NS_ENSURE_ARG_POINTER(aSearchParentFrames); + *aSearchParentFrames = mSearchParentFrames; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserFind::SetSearchParentFrames(bool aSearchParentFrames) { + mSearchParentFrames = aSearchParentFrames; + return NS_OK; +} + +/* + This method handles finding in a single window (aka frame). + +*/ +nsresult nsWebBrowserFind::SearchInFrame(nsPIDOMWindowOuter* aWindow, + bool aWrapping, bool* aDidFind) { + NS_ENSURE_ARG(aWindow); + NS_ENSURE_ARG_POINTER(aDidFind); + + *aDidFind = false; + + // Do security check, to ensure that the frame we're searching is + // accessible from the frame where the Find is being run. + + // get a uri for the window + RefPtr<Document> theDoc = aWindow->GetDoc(); + if (!theDoc) { + return NS_ERROR_FAILURE; + } + + if (!nsContentUtils::SubjectPrincipal()->Subsumes(theDoc->NodePrincipal())) { + return NS_ERROR_DOM_PROP_ACCESS_DENIED; + } + + nsresult rv; + nsCOMPtr<nsIFind> find = do_CreateInstance(NS_FIND_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + (void)find->SetCaseSensitive(mMatchCase); + (void)find->SetMatchDiacritics(mMatchDiacritics); + (void)find->SetFindBackwards(mFindBackwards); + + (void)find->SetEntireWord(mEntireWord); + + // Now make sure the content (for actual finding) and frame (for + // selection) models are up to date. + theDoc->FlushPendingNotifications(FlushType::Frames); + + RefPtr<Selection> sel = GetFrameSelection(aWindow); + NS_ENSURE_ARG_POINTER(sel); + + RefPtr<nsRange> searchRange = nsRange::Create(theDoc); + RefPtr<nsRange> startPt = nsRange::Create(theDoc); + RefPtr<nsRange> endPt = nsRange::Create(theDoc); + + RefPtr<nsRange> foundRange; + + rv = GetSearchLimits(searchRange, startPt, endPt, theDoc, sel, aWrapping); + NS_ENSURE_SUCCESS(rv, rv); + + rv = find->Find(mSearchString, searchRange, startPt, endPt, + getter_AddRefs(foundRange)); + + if (NS_SUCCEEDED(rv) && foundRange) { + *aDidFind = true; + sel->RemoveAllRanges(IgnoreErrors()); + // Beware! This may flush notifications via synchronous + // ScrollSelectionIntoView. + SetSelectionAndScroll(aWindow, foundRange); + } + + return rv; +} + +// called when we start searching a frame that is not the initial focussed +// frame. Prepare the frame to be searched. we clear the selection, so that the +// search starts from the top of the frame. +nsresult nsWebBrowserFind::OnStartSearchFrame(nsPIDOMWindowOuter* aWindow) { + return ClearFrameSelection(aWindow); +} + +// called when we are done searching a frame and didn't find anything, and about +// about to start searching the next frame. +nsresult nsWebBrowserFind::OnEndSearchFrame(nsPIDOMWindowOuter* aWindow) { + return NS_OK; +} + +already_AddRefed<Selection> nsWebBrowserFind::GetFrameSelection( + nsPIDOMWindowOuter* aWindow) { + RefPtr<Document> doc = aWindow->GetDoc(); + if (!doc) { + return nullptr; + } + + PresShell* presShell = doc->GetPresShell(); + if (!presShell) { + return nullptr; + } + + // text input controls have their independent selection controllers that we + // must use when they have focus. + nsPresContext* presContext = presShell->GetPresContext(); + + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + nsCOMPtr<nsIContent> focusedContent = nsFocusManager::GetFocusedDescendant( + aWindow, nsFocusManager::eOnlyCurrentWindow, + getter_AddRefs(focusedWindow)); + + nsIFrame* frame = + focusedContent ? focusedContent->GetPrimaryFrame() : nullptr; + + nsCOMPtr<nsISelectionController> selCon; + RefPtr<Selection> sel; + if (frame) { + frame->GetSelectionController(presContext, getter_AddRefs(selCon)); + sel = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (sel && sel->RangeCount() > 0) { + return sel.forget(); + } + } + + sel = presShell->GetSelection(nsISelectionController::SELECTION_NORMAL); + return sel.forget(); +} + +nsresult nsWebBrowserFind::ClearFrameSelection(nsPIDOMWindowOuter* aWindow) { + NS_ENSURE_ARG(aWindow); + RefPtr<Selection> selection = GetFrameSelection(aWindow); + if (selection) { + selection->RemoveAllRanges(IgnoreErrors()); + } + + return NS_OK; +} + +nsresult nsWebBrowserFind::OnFind(nsPIDOMWindowOuter* aFoundWindow) { + SetCurrentSearchFrame(aFoundWindow); + + // We don't want a selection to appear in two frames simultaneously + nsCOMPtr<nsPIDOMWindowOuter> lastFocusedWindow = + do_QueryReferent(mLastFocusedWindow); + if (lastFocusedWindow && lastFocusedWindow != aFoundWindow) { + ClearFrameSelection(lastFocusedWindow); + } + + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + // get the containing frame and focus it. For top-level windows, the right + // window should already be focused. + if (RefPtr<Element> frameElement = + aFoundWindow->GetFrameElementInternal()) { + fm->SetFocus(frameElement, 0); + } + + mLastFocusedWindow = do_GetWeakReference(aFoundWindow); + } + + return NS_OK; +} diff --git a/toolkit/components/find/nsWebBrowserFind.h b/toolkit/components/find/nsWebBrowserFind.h new file mode 100644 index 0000000000..0c846d2f17 --- /dev/null +++ b/toolkit/components/find/nsWebBrowserFind.h @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsWebBrowserFindImpl_h__ +#define nsWebBrowserFindImpl_h__ + +#include "nsIWebBrowserFind.h" + +#include "nsCOMPtr.h" +#include "nsIWeakReferenceUtils.h" +#include "nsPIDOMWindow.h" + +#include "nsString.h" + +class nsIDOMWindow; +class nsIDocShell; +class nsRange; + +namespace mozilla { +namespace dom { +class Document; +class Element; +class Selection; +} // namespace dom +} // namespace mozilla + +//***************************************************************************** +// class nsWebBrowserFind +//***************************************************************************** + +class nsWebBrowserFind : public nsIWebBrowserFind, + public nsIWebBrowserFindInFrames { + public: + nsWebBrowserFind(); + + // nsISupports + NS_DECL_ISUPPORTS + + // nsIWebBrowserFind + NS_DECL_NSIWEBBROWSERFIND + + // nsIWebBrowserFindInFrames + NS_DECL_NSIWEBBROWSERFINDINFRAMES + + protected: + virtual ~nsWebBrowserFind(); + + bool CanFindNext() { return mSearchString.Length() != 0; } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + SearchInFrame(nsPIDOMWindowOuter* aWindow, bool aWrapping, bool* aDidFind); + + nsresult OnStartSearchFrame(nsPIDOMWindowOuter* aWindow); + nsresult OnEndSearchFrame(nsPIDOMWindowOuter* aWindow); + + already_AddRefed<mozilla::dom::Selection> GetFrameSelection( + nsPIDOMWindowOuter* aWindow); + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + ClearFrameSelection(nsPIDOMWindowOuter* aWindow); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult OnFind(nsPIDOMWindowOuter* aFoundWindow); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void SetSelectionAndScroll( + nsPIDOMWindowOuter* aWindow, nsRange* aRange); + + nsresult GetRootNode(mozilla::dom::Document* aDomDoc, + mozilla::dom::Element** aNode); + nsresult GetSearchLimits(nsRange* aRange, nsRange* aStartPt, nsRange* aEndPt, + mozilla::dom::Document* aDoc, + mozilla::dom::Selection* aSel, bool aWrap); + nsresult SetRangeAroundDocument(nsRange* aSearchRange, nsRange* aStartPoint, + nsRange* aEndPoint, + mozilla::dom::Document* aDoc); + + protected: + nsString mSearchString; + + bool mFindBackwards; + bool mWrapFind; + bool mEntireWord; + bool mMatchCase; + bool mMatchDiacritics; + + bool mSearchSubFrames; + bool mSearchParentFrames; + + // These are all weak because who knows if windows can go away during our + // lifetime. + nsWeakPtr mCurrentSearchFrame; + nsWeakPtr mRootSearchFrame; + nsWeakPtr mLastFocusedWindow; +}; + +#endif diff --git a/toolkit/components/find/test/mochitest/mochitest.ini b/toolkit/components/find/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..4ccfd2335b --- /dev/null +++ b/toolkit/components/find/test/mochitest/mochitest.ini @@ -0,0 +1,6 @@ +[DEFAULT] +prefs = + layout.css.content-visibility.enabled=true + +[test_bug499115.html] +[test_nsFind.html] diff --git a/toolkit/components/find/test/mochitest/test_bug499115.html b/toolkit/components/find/test/mochitest/test_bug499115.html new file mode 100644 index 0000000000..18cea93690 --- /dev/null +++ b/toolkit/components/find/test/mochitest/test_bug499115.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=499115 +--> +<head> + <title>Test for Bug 499115</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="onLoad();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=499115">Mozilla Bug 499115</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 499115 */ + SimpleTest.waitForExplicitFinish(); + + const SEARCH_TEXT = "minefield"; + + function getMatches() { + var numMatches = 0; + + var searchRange = document.createRange(); + searchRange.selectNodeContents(document.body); + + var startPoint = searchRange.cloneRange(); + startPoint.collapse(true); + + var endPoint = searchRange.cloneRange(); + endPoint.collapse(false); + + var retRange = null; + var finder = SpecialPowers.Cc["@mozilla.org/embedcomp/rangefind;1"] + .createInstance(SpecialPowers.Ci.nsIFind); + + finder.caseSensitive = false; + + while ((retRange = finder.Find(SEARCH_TEXT, searchRange, + startPoint, endPoint))) { + numMatches++; + + // Start next search from end of current match + startPoint = retRange.cloneRange(); + startPoint.collapse(false); + } + + return numMatches; + } + + function onLoad() { + var matches = getMatches(); + is(matches, 2, "found second match in anonymous content"); + SimpleTest.finish(); + } + </script> + </pre> +<input type="text" value="minefield minefield"></body> +</html> diff --git a/toolkit/components/find/test/mochitest/test_nsFind.html b/toolkit/components/find/test/mochitest/test_nsFind.html new file mode 100644 index 0000000000..0d6bc3d562 --- /dev/null +++ b/toolkit/components/find/test/mochitest/test_nsFind.html @@ -0,0 +1,399 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=202251 +https://bugzilla.mozilla.org/show_bug.cgi?id=450048 +https://bugzilla.mozilla.org/show_bug.cgi?id=812837 +https://bugzilla.mozilla.org/show_bug.cgi?id=969980 +https://bugzilla.mozilla.org/show_bug.cgi?id=1589786 +https://bugzilla.mozilla.org/show_bug.cgi?id=1611568 +https://bugzilla.mozilla.org/show_bug.cgi?id=1649187 +https://bugzilla.mozilla.org/show_bug.cgi?id=1699599 +--> +<head> + <meta charset="UTF-8"> + <title>Test for nsFind::Find()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +async function runTests() { + // Check nsFind class and its nsIFind interface. + + // Inject some text that we'll search for later. + const NULL_CHARACTER = "\0"; + const INJECTED_NULL_TEXT = "injected null\0"; + const nullcharsinjected = document.getElementById("nullcharsinjected"); + const nulltextnode = document.createTextNode(INJECTED_NULL_TEXT); + nullcharsinjected.appendChild(nulltextnode); + + // Take steps to ensure that the frame is created for the newly added + // nulltextnode. Our find code is dependent upon finding visible frames. + // One way to ensure the frame exists is to ask for its bounds. + const injectionBounds = nullcharsinjected.getBoundingClientRect(); + ok(injectionBounds, "Got a bounding rect for the injected text container."); + + var rf = SpecialPowers.Cc["@mozilla.org/embedcomp/rangefind;1"] + .getService(SpecialPowers.Ci.nsIFind); + + var display = window.document.getElementById("display"); + var searchRange = window.document.createRange(); + searchRange.setStart(display, 0); + searchRange.setEnd(display, display.childNodes.length); + var startPt = searchRange; + var endPt = searchRange; + + var searchValue, retRange; + + rf.findBackwards = false; + + rf.caseSensitive = false; + rf.matchDiacritics = false; + rf.entireWord = false; + + searchValue = "TexT"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "λογος"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "유"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(!retRange, "\"" + searchValue + "\" found (not caseSensitive)"); + + searchValue = "కె"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(!retRange, "\"" + searchValue + "\" found (not caseSensitive)"); + + searchValue = "𑂥"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "istanbul"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "wroclaw"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "goteborg"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "degrees k"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "≠"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(!retRange, "\"" + searchValue + "\" found (not caseSensitive)"); + + searchValue = "guahe"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "g̃uah̰e"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "𐐸𐐯𐑊𐐬"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (not caseSensitive)"); + + searchValue = "東京駅" + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found"); + + rf.matchDiacritics = true; + + searchValue = "λογος"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(!retRange, "\"" + searchValue + "\" found (matchDiacritics on)"); + + searchValue = "g̃uahe"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (matchDiacritics on)"); + + searchValue = "guahe"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(!retRange, "\"" + searchValue + "\" found (matchDiacritics on)"); + + rf.caseSensitive = true; + + searchValue = "TexT"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(!retRange, "\"" + searchValue + "\" found (caseSensitive)"); + + searchValue = "text"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found"); + + // Matches |i<b>n­t</b>o|. + searchValue = "into"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found"); + + // Matches inside |search|. + searchValue = "ear"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found"); + + // Set new start point (to end of last search). + startPt = retRange.endContainer.ownerDocument.createRange(); + startPt.setStart(retRange.endContainer, retRange.endOffset); + startPt.setEnd(retRange.endContainer, retRange.endOffset); + + searchValue = "t"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (forward)"); + + searchValue = "the"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(!retRange, "\"" + searchValue + "\" found (forward)"); + + rf.findBackwards = true; + + // searchValue = "the"; + retRange = rf.Find(searchValue, searchRange, startPt, endPt); + ok(retRange, "\"" + searchValue + "\" not found (backward)"); + + // Curly quotes and straight quotes should match. + + rf.matchDiacritics = false; + rf.findBackwards = false; + + function find(node, value) { + var range = document.createRange(); + range.setStart(node, 0); + range.setEnd(node, node.childNodes.length); + return rf.Find(value, range, range, range); + } + + function assertFound(node, value) { + ok(find(node, value), "\"" + value + "\" should be found"); + } + + function assertNotFound(node, value) { + ok(!find(node, value), "\"" + value + "\" should not be found"); + } + + var quotes = document.getElementById("quotes"); + + assertFound(quotes, "\"straight\""); + assertFound(quotes, "\u201Cstraight\u201D"); + + assertNotFound(quotes, "'straight'"); + assertNotFound(quotes, "\u2018straight\u2019"); + assertNotFound(quotes, "\u2019straight\u2018"); + assertNotFound(quotes, ".straight."); + + assertFound(quotes, "\"curly\""); + assertFound(quotes, "\u201Ccurly\u201D"); + + assertNotFound(quotes, "'curly'"); + assertNotFound(quotes, "\u2018curly\u2019"); + assertNotFound(quotes, ".curly."); + + assertFound(quotes, "didn't"); + assertFound(quotes, "didn\u2018t"); + assertFound(quotes, "didn\u2019t"); + + assertNotFound(quotes, "didnt"); + assertNotFound(quotes, "didn t"); + assertNotFound(quotes, "didn.t"); + + assertFound(quotes, "'didn't'"); + assertFound(quotes, "'didn\u2018t'"); + assertFound(quotes, "'didn\u2019t'"); + assertFound(quotes, "\u2018didn't\u2019"); + assertFound(quotes, "\u2019didn't\u2018"); + assertFound(quotes, "\u2018didn't\u2018"); + assertFound(quotes, "\u2019didn't\u2019"); + assertFound(quotes, "\u2018didn\u2019t\u2019"); + assertFound(quotes, "\u2019didn\u2018t\u2019"); + assertFound(quotes, "\u2018didn\u2019t\u2018"); + + assertNotFound(quotes, "\"didn't\""); + assertNotFound(quotes, "\u201Cdidn't\u201D"); + + assertFound(quotes, "doesn't"); + assertFound(quotes, "doesn\u2018t"); + assertFound(quotes, "doesn\u2019t"); + + assertNotFound(quotes, "doesnt"); + assertNotFound(quotes, "doesn t"); + assertNotFound(quotes, "doesn.t"); + + assertFound(quotes, "'doesn't'"); + assertFound(quotes, "'doesn\u2018t'"); + assertFound(quotes, "'doesn\u2019t'"); + assertFound(quotes, "\u2018doesn't\u2019"); + assertFound(quotes, "\u2019doesn't\u2018"); + assertFound(quotes, "\u2018doesn't\u2018"); + assertFound(quotes, "\u2019doesn't\u2019"); + assertFound(quotes, "\u2018doesn\u2019t\u2019"); + assertFound(quotes, "\u2019doesn\u2018t\u2019"); + assertFound(quotes, "\u2018doesn\u2019t\u2018"); + + assertNotFound(quotes, "\"doesn't\""); + assertNotFound(quotes, "\u201Cdoesn't\u201D"); + + // Curly quotes and straight quotes should not match. + rf.matchDiacritics = true; + + assertFound(quotes, "\"straight\""); + assertNotFound(quotes, "\u201Cstraight\u201D"); + + assertNotFound(quotes, "\"curly\""); + assertFound(quotes, "\u201Ccurly\u201D"); + + assertFound(quotes, "\u2018didn't\u2019"); + assertNotFound(quotes, "'didn't'"); + + assertFound(quotes, "'doesn\u2019t'"); + assertNotFound(quotes, "'doesn\u2018t'"); + assertNotFound(quotes, "'doesn't'"); + + // Embedded strings containing null characters can't be found, because + // the HTML parser converts them to \ufffd, which is the replacement + // character. + const nullcharsnative = document.getElementById("nullcharsnative"); + assertFound(nullcharsnative, "native null\ufffd"); + + // Injected strings containing null characters can be found. + assertFound(nullcharsinjected, NULL_CHARACTER); + assertFound(nullcharsinjected, INJECTED_NULL_TEXT); + + // Content skipped via content-visibility can't be found. + assertNotFound(quotes, "Tardigrade"); + assertNotFound(quotes, "Amoeba"); + + // Simple checks for behavior of the entireWord option. + var entireWord = document.getElementById("entireWord"); + rf.entireWord = true; + assertFound(entireWord, "one"); + assertNotFound(entireWord, "on"); + assertFound(entireWord, "(one)"); + assertFound(entireWord, "two"); + assertFound(entireWord, "[two]"); + assertFound(entireWord, "[three]"); + assertFound(entireWord, "foo"); + assertFound(entireWord, "-foo"); + assertFound(entireWord, "bar"); + assertFound(entireWord, "-bar"); + assertNotFound(entireWord, "-fo"); + assertNotFound(entireWord, "-ba"); + + rf.entireWord = false; + assertFound(entireWord, "on"); + assertFound(entireWord, "-fo"); + assertFound(entireWord, "-ba"); + + // Searching in elements with display: table-*, bug 1645990 + var table = document.getElementById("tabular"); + assertFound(table, "One"); + assertFound(table, "TwoThree"); // across adjacent cells + assertNotFound(table, "wordsanother"); // not across rows + rf.entireWord = true; + assertNotFound(table, "One"); // because nothing separates it from next cell + assertFound(table, "several"); + assertFound(table, "whole"); + assertFound(table, "words"); + rf.entireWord = false; + + // Do these test at the end since they trigger failure screenshots in + // the test harness, and we want as much information as possible about + // any OTHER tests that may have already failed. + + // Check |null| detection on |aSearchRange| parameter. + try { + rf.Find("", null, startPt, endPt); + + ok(false, "Missing NS_ERROR_ILLEGAL_VALUE exception"); + } catch (e) { + let wrappedError = SpecialPowers.wrap(e); + if (wrappedError.result == SpecialPowers.Cr.NS_ERROR_ILLEGAL_VALUE) { + ok(true, null); + } else { + throw wrappedError; + } + } + + // Check |null| detection on |aStartPoint| parameter. + try { + rf.Find("", searchRange, null, endPt); + + ok(false, "Missing NS_ERROR_ILLEGAL_VALUE exception"); + } catch (e) { + let wrappedError = SpecialPowers.wrap(e); + if (wrappedError.result == SpecialPowers.Cr.NS_ERROR_ILLEGAL_VALUE) { + ok(true, null); + } else { + throw wrappedError; + } + } + + // Check |null| detection on |aEndPoint| parameter. + try { + rf.Find("", searchRange, startPt, null); + + ok(false, "Missing NS_ERROR_ILLEGAL_VALUE exception"); + } catch (e) { + let wrappedError = SpecialPowers.wrap(e); + if (wrappedError.result == SpecialPowers.Cr.NS_ERROR_ILLEGAL_VALUE) { + ok(true, null); + } else { + throw wrappedError; + } + } + + SimpleTest.finish(); +} +</script> +<style> +#tabular { display: table; } +#tabular div { display: table-row; } +#tabular div div { display: table-cell; } +</style> +</head> +<body onload="runTests()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450048">Mozilla Bug 450048</a> +<p id="display">This is the text to search i<b>n­t</b>o</p> +<p id="quotes">"straight" and “curly” and ‘didn't’ and 'doesn’t'</p> +<p id="nullcharsnative">native null�</p> +<p id="nullcharsinjected"></p> +<p id="greek">ΛΌΓΟΣ</p> +<p id="korean">위</p> +<p id="telugu">కై</p> +<p id="kaithi">𑂫</p> +<p id="turkish">İstanbul</p> +<p id="polish">Wrocław</p> +<p id="norwegian">Gøteborg</p> +<p id="kelvin">degrees K</p> +<p id="math">=</p> +<p id="guarani">G̃uahe</p> +<p id="deseret">𐐐𐐯𐑊𐐬 𐐶𐐯𐑉𐑊𐐼!</p> +<p id="ruby"><ruby>東<rt>とう</rt></ruby><ruby>京<rt>きょう</ruby>駅</p> +<div id="content-visibility-hidden" style="content-visibility: hidden"> + Tardigrade + <div>Amoeba</div> +</div> +<div id="entireWord"><p>(one)</p><p>[two] [three]</p><p>-foo -bar</p></div> +<div id="content" style="display: none"> + +</div> +<div id="tabular"> + <div><div>One</div><div>Two</div><div>Three</div></div> + <div><div></div><div></div><div>several whole words</div></div> + <div><div>one</div><div>more</div><div>row</div></div> +</div> +<pre id="test"> +</pre> +</body> +</html> |