| /* |
| * Copyright (C) 2019 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 "TextManipulationController.h" |
| |
| #include "CharacterData.h" |
| #include "Editing.h" |
| #include "ElementAncestorIterator.h" |
| #include "ScriptDisallowedScope.h" |
| #include "TextIterator.h" |
| #include "VisibleUnits.h" |
| |
| namespace WebCore { |
| |
| inline bool TextManipulationController::ExclusionRule::match(const Element& element) const |
| { |
| return switchOn(rule, [&element] (ElementRule rule) { |
| return rule.localName == element.localName(); |
| }, [&element] (AttributeRule rule) { |
| return equalIgnoringASCIICase(element.getAttribute(rule.name), rule.value); |
| }, [&element] (ClassRule rule) { |
| return element.hasClass() && element.classNames().contains(rule.className); |
| }); |
| } |
| |
| class ExclusionRuleMatcher { |
| public: |
| using ExclusionRule = TextManipulationController::ExclusionRule; |
| using Type = TextManipulationController::ExclusionRule::Type; |
| |
| ExclusionRuleMatcher(const Vector<ExclusionRule>& rules) |
| : m_rules(rules) |
| { } |
| |
| bool isExcluded(Node* node) |
| { |
| if (!node) |
| return false; |
| |
| RefPtr<Element> startingElement = is<Element>(*node) ? downcast<Element>(node) : node->parentElement(); |
| if (!startingElement) |
| return false; |
| |
| Type type = Type::Include; |
| RefPtr<Element> matchingElement; |
| for (auto& element : elementLineage(startingElement.get())) { |
| if (auto typeOrNullopt = typeForElement(element)) { |
| type = *typeOrNullopt; |
| matchingElement = &element; |
| break; |
| } |
| } |
| |
| for (auto& element : elementLineage(startingElement.get())) { |
| m_cache.set(element, type); |
| if (&element == matchingElement) |
| break; |
| } |
| |
| return type == Type::Exclude; |
| } |
| |
| Optional<Type> typeForElement(Element& element) |
| { |
| auto it = m_cache.find(element); |
| if (it != m_cache.end()) |
| return it->value; |
| |
| for (auto& rule : m_rules) { |
| if (rule.match(element)) |
| return rule.type; |
| } |
| |
| return WTF::nullopt; |
| } |
| |
| private: |
| const Vector<ExclusionRule>& m_rules; |
| HashMap<Ref<Element>, ExclusionRule::Type> m_cache; |
| }; |
| |
| TextManipulationController::TextManipulationController(Document& document) |
| : m_document(makeWeakPtr(document)) |
| { |
| } |
| |
| void TextManipulationController::startObservingParagraphs(ManipulationItemCallback&& callback, Vector<ExclusionRule>&& exclusionRules) |
| { |
| auto document = makeRefPtr(m_document.get()); |
| if (!document) |
| return; |
| |
| m_callback = WTFMove(callback); |
| m_exclusionRules = WTFMove(exclusionRules); |
| |
| VisiblePosition start = firstPositionInNode(m_document.get()); |
| VisiblePosition end = lastPositionInNode(m_document.get()); |
| TextIterator iterator { start.deepEquivalent(), end.deepEquivalent() }; |
| if (!document) |
| return; // VisiblePosition or TextIterator's constructor may have updated the layout and executed arbitrary scripts. |
| |
| ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules); |
| Vector<ManipulationToken> tokensInCurrentParagraph; |
| Position startOfCurrentParagraph = start.deepEquivalent(); |
| while (!iterator.atEnd()) { |
| StringView currentText = iterator.text(); |
| |
| if (startOfCurrentParagraph.isNull()) |
| startOfCurrentParagraph = iterator.range()->startPosition(); |
| |
| size_t endOfLastNewLine = 0; |
| size_t offsetOfNextNewLine = 0; |
| while ((offsetOfNextNewLine = currentText.find('\n', endOfLastNewLine)) != notFound) { |
| if (endOfLastNewLine < offsetOfNextNewLine) { |
| auto stringUntilEndOfLine = currentText.substring(endOfLastNewLine, offsetOfNextNewLine - endOfLastNewLine).toString(); |
| tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), stringUntilEndOfLine, exclusionRuleMatcher.isExcluded(iterator.node()) }); |
| } |
| |
| auto lastRange = iterator.range(); |
| if (offsetOfNextNewLine < currentText.length()) { |
| lastRange->setStart(firstPositionInOrBeforeNode(iterator.node())); // Move the start to the beginning of the current node. |
| TextIterator::subrange(lastRange, 0, offsetOfNextNewLine); |
| } |
| Position endOfCurrentParagraph = lastRange->endPosition(); |
| |
| if (!tokensInCurrentParagraph.isEmpty()) |
| addItem(startOfCurrentParagraph, endOfCurrentParagraph, WTFMove(tokensInCurrentParagraph)); |
| startOfCurrentParagraph.clear(); |
| endOfLastNewLine = offsetOfNextNewLine + 1; |
| } |
| |
| auto remainingText = currentText.substring(endOfLastNewLine); |
| if (remainingText.length()) |
| tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), exclusionRuleMatcher.isExcluded(iterator.node()) }); |
| |
| iterator.advance(); |
| } |
| |
| if (!tokensInCurrentParagraph.isEmpty()) |
| addItem(startOfCurrentParagraph, end.deepEquivalent(), WTFMove(tokensInCurrentParagraph)); |
| } |
| |
| void TextManipulationController::addItem(const Position& startOfParagraph, const Position& endOfParagraph, Vector<ManipulationToken>&& tokens) |
| { |
| ASSERT(m_document); |
| auto result = m_items.add(m_itemIdentifier.generate(), ManipulationItem { startOfParagraph, endOfParagraph, WTFMove(tokens) }); |
| m_callback(*m_document, result.iterator->key, result.iterator->value.tokens); |
| } |
| |
| auto TextManipulationController::completeManipulation(ItemIdentifier itemIdentifier, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult |
| { |
| if (!itemIdentifier) |
| return ManipulationResult::InvalidItem; |
| |
| auto itemIterator = m_items.find(itemIdentifier); |
| if (itemIterator == m_items.end()) |
| return ManipulationResult::InvalidItem; |
| |
| auto didReplace = replace(itemIterator->value, replacementTokens); |
| |
| m_items.remove(itemIterator); |
| |
| return didReplace; |
| } |
| |
| struct DOMChange { |
| Ref<CharacterData> node; |
| String newData; |
| }; |
| |
| auto TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult |
| { |
| TextIterator iterator { item.start, item.end }; |
| size_t currentTokenIndex = 0; |
| HashMap<TokenIdentifier, std::pair<RefPtr<Node>, const ManipulationToken*>> tokenToNodeTokenPair; |
| |
| while (!iterator.atEnd()) { |
| auto string = iterator.text().toString(); |
| if (currentTokenIndex >= item.tokens.size()) |
| return ManipulationResult::ContentChanged; |
| auto& currentToken = item.tokens[currentTokenIndex]; |
| if (iterator.text() != currentToken.content) |
| return ManipulationResult::ContentChanged; |
| tokenToNodeTokenPair.set(currentToken.identifier, std::pair<RefPtr<Node>, const ManipulationToken*> { iterator.node(), ¤tToken }); |
| iterator.advance(); |
| ++currentTokenIndex; |
| } |
| |
| // FIXME: This doesn't preseve the order of the replacement at all. |
| Vector<DOMChange> changes; |
| for (auto& newToken : replacementTokens) { |
| auto it = tokenToNodeTokenPair.find(newToken.identifier); |
| if (it == tokenToNodeTokenPair.end()) |
| return ManipulationResult::InvalidToken; |
| auto& oldToken = *it->value.second; |
| if (oldToken.isExcluded) |
| return ManipulationResult::ExclusionViolation; |
| auto* node = it->value.first.get(); |
| if (!node || !is<CharacterData>(*node)) |
| continue; |
| changes.append({ downcast<CharacterData>(*node), newToken.content }); |
| } |
| |
| for (auto& change : changes) |
| change.node->setData(change.newData); |
| |
| return ManipulationResult::Success; |
| } |
| |
| } // namespace WebCore |