blob: dc39a0f0d18fffca7dfb465219cd612a9f738f8f [file] [log] [blame]
/*
* Copyright (C) 2020 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 INC. AND ITS CONTRIBUTORS ``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 INC. OR ITS 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 "AppHighlightStorage.h"
#include "AppHighlight.h"
#include "AppHighlightRangeData.h"
#include "Chrome.h"
#include "ChromeClient.h"
#include "Document.h"
#include "DocumentMarkerController.h"
#include "Editor.h"
#include "ElementInlines.h"
#include "HTMLBodyElement.h"
#include "HighlightRegister.h"
#include "Node.h"
#include "Position.h"
#include "RenderedDocumentMarker.h"
#include "SimpleRange.h"
#include "StaticRange.h"
#include "TextIndicator.h"
#include "TextIterator.h"
#include <wtf/UUID.h>
namespace WebCore {
#if ENABLE(APP_HIGHLIGHTS)
static constexpr unsigned textPreviewLength = 500;
static RefPtr<Node> findNodeByPathIndex(const Node& parent, unsigned pathIndex, const String& nodeName)
{
if (!is<ContainerNode>(parent))
return nullptr;
for (auto* child = downcast<ContainerNode>(parent).firstChild(); child; child = child->nextSibling()) {
if (nodeName != child->nodeName())
continue;
if (!pathIndex--)
return child;
}
return nullptr;
}
static std::pair<RefPtr<Node>, size_t> findNodeStartingAtPathComponentIndex(const AppHighlightRangeData::NodePath& path, Node& initialNode, size_t initialIndexToFollow)
{
if (initialIndexToFollow >= path.size())
return { nullptr, initialIndexToFollow };
RefPtr currentNode = &initialNode;
size_t currentPathIndex = initialIndexToFollow;
for (; currentPathIndex < path.size(); ++currentPathIndex) {
auto& component = path[currentPathIndex];
auto nextNode = findNodeByPathIndex(*currentNode, component.pathIndex, component.nodeName);
if (!nextNode)
return { nullptr, currentPathIndex };
if (is<CharacterData>(*nextNode) && downcast<CharacterData>(*nextNode).data() != component.textData)
return { nullptr, currentPathIndex };
currentNode = WTFMove(nextNode);
}
return { currentNode, currentPathIndex };
}
static RefPtr<Node> findNode(const AppHighlightRangeData::NodePath& path, Document& document)
{
if (path.isEmpty() || !document.body())
return nullptr;
auto [foundNode, nextIndex] = findNodeStartingAtPathComponentIndex(path, *document.body(), 0);
if (foundNode)
return foundNode;
while (nextIndex < path.size()) {
auto& component = path[nextIndex++];
if (component.identifier.isEmpty())
continue;
RefPtr elementWithIdentifier = document.getElementById(component.identifier);
if (!elementWithIdentifier || elementWithIdentifier->nodeName() != component.nodeName)
continue;
std::tie(foundNode, nextIndex) = findNodeStartingAtPathComponentIndex(path, *elementWithIdentifier, nextIndex);
if (foundNode)
return foundNode;
}
return nullptr;
}
static std::optional<SimpleRange> findRangeByIdentifyingStartAndEndPositions(const AppHighlightRangeData& range, Document& document)
{
auto startContainer = findNode(range.startContainer(), document);
if (!startContainer)
return std::nullopt;
auto endContainer = findNode(range.endContainer(), document);
if (!endContainer)
return std::nullopt;
auto start = makeContainerOffsetPosition(startContainer.get(), range.startOffset());
auto end = makeContainerOffsetPosition(endContainer.get(), range.endOffset());
if (start.isOrphan() || end.isOrphan())
return std::nullopt;
return makeSimpleRange(start, end);
}
static std::optional<SimpleRange> findRangeBySearchingText(const AppHighlightRangeData& range, Document& document)
{
HashSet<String> identifiersInStartPath;
for (auto& component : range.startContainer()) {
if (!component.identifier.isEmpty())
identifiersInStartPath.add(component.identifier);
}
RefPtr<Element> foundElement = document.body();
for (auto iterator = range.endContainer().rbegin(), end = range.endContainer().rend(); iterator != end; ++iterator) {
auto elementIdentifier = iterator->identifier;
if (elementIdentifier.isEmpty() || !identifiersInStartPath.contains(elementIdentifier))
continue;
foundElement = document.getElementById(elementIdentifier);
if (foundElement)
break;
}
if (!foundElement)
return std::nullopt;
auto foundElementRange = makeRangeSelectingNodeContents(*foundElement);
auto foundText = plainText(foundElementRange);
if (auto index = foundText.find(range.text()); index != notFound && index == foundText.reverseFind(range.text()))
return resolveCharacterRange(foundElementRange, { index, range.text().length() });
return std::nullopt;
}
static std::optional<SimpleRange> findRange(const AppHighlightRangeData& range, Document& document)
{
if (auto foundRange = findRangeByIdentifyingStartAndEndPositions(range, document))
return foundRange;
return findRangeBySearchingText(range, document);
}
static unsigned computePathIndex(const Node& node)
{
String nodeName = node.nodeName();
unsigned index = 0;
for (auto* previousSibling = node.previousSibling(); previousSibling; previousSibling = previousSibling->previousSibling()) {
if (previousSibling->nodeName() == nodeName)
index++;
}
return index;
}
static AppHighlightRangeData::NodePathComponent createNodePathComponent(const Node& node)
{
return {
is<Element>(node) ? downcast<Element>(node).getIdAttribute().string() : nullString(),
node.nodeName(),
is<CharacterData>(node) ? downcast<CharacterData>(node).data() : nullString(),
computePathIndex(node)
};
}
static AppHighlightRangeData::NodePath makeNodePath(RefPtr<Node>&& node)
{
AppHighlightRangeData::NodePath components;
auto body = node->document().body();
for (auto ancestor = node; ancestor && ancestor != body; ancestor = ancestor->parentNode())
components.append(createNodePathComponent(*ancestor));
components.reverse();
return { components };
}
static AppHighlightRangeData createAppHighlightRangeData(const StaticRange& range)
{
auto text = plainText(range);
text.truncate(textPreviewLength);
auto identifier = createCanonicalUUIDString();
return {
identifier,
text,
makeNodePath(&range.startContainer()),
range.startOffset(),
makeNodePath(&range.endContainer()),
range.endOffset()
};
}
AppHighlightStorage::AppHighlightStorage(Document& document)
: m_document(document)
{
}
AppHighlightStorage::~AppHighlightStorage() = default;
void AppHighlightStorage::storeAppHighlight(Ref<StaticRange>&& range)
{
auto data = createAppHighlightRangeData(range);
std::optional<String> text;
if (!data.text().isEmpty())
text = data.text();
AppHighlight highlight = {data.toSharedBuffer(), text, CreateNewGroupForHighlight::No, HighlightRequestOriginatedInApp::No};
m_document->page()->chrome().storeAppHighlight(WTFMove(highlight));
}
void AppHighlightStorage::restoreAndScrollToAppHighlight(Ref<SharedBuffer>&& buffer, ScrollToHighlight scroll)
{
auto appHighlightRangeData = AppHighlightRangeData::create(buffer);
if (!appHighlightRangeData)
return;
if (!attemptToRestoreHighlightAndScroll(appHighlightRangeData.value(), scroll)) {
if (scroll == ScrollToHighlight::Yes)
m_unrestoredScrollHighlight = appHighlightRangeData;
else
m_unrestoredHighlights.append(appHighlightRangeData.value());
}
m_timeAtLastRangeSearch = MonotonicTime::now();
}
bool AppHighlightStorage::attemptToRestoreHighlightAndScroll(AppHighlightRangeData& highlight, ScrollToHighlight scroll)
{
if (!m_document)
return false;
RefPtr strongDocument = m_document.get();
auto range = findRange(highlight, *strongDocument);
if (!range)
return false;
strongDocument->appHighlightRegister().addAppHighlight(StaticRange::create(*range));
if (scroll == ScrollToHighlight::Yes) {
auto textIndicator = TextIndicator::createWithRange(range.value(), { TextIndicatorOption::DoNotClipToVisibleRect }, WebCore::TextIndicatorPresentationTransition::Bounce);
if (textIndicator)
m_document->page()->chrome().client().setTextIndicator(textIndicator->data());
TemporarySelectionChange selectionChange(*strongDocument, { range.value() }, { TemporarySelectionOption::DelegateMainFrameScroll, TemporarySelectionOption::SmoothScroll, TemporarySelectionOption::RevealSelectionBounds });
}
return true;
}
void AppHighlightStorage::restoreUnrestoredAppHighlights()
{
Vector<AppHighlightRangeData> remainingRanges;
for (auto& highlight : m_unrestoredHighlights) {
if (!attemptToRestoreHighlightAndScroll(highlight, ScrollToHighlight::No))
remainingRanges.append(highlight);
}
if (m_unrestoredScrollHighlight) {
if (attemptToRestoreHighlightAndScroll(m_unrestoredScrollHighlight.value(), ScrollToHighlight::Yes))
m_unrestoredScrollHighlight.reset();
}
m_timeAtLastRangeSearch = MonotonicTime::now();
m_unrestoredHighlights = WTFMove(remainingRanges);
}
#endif
} // namespace WebCore