2
* Copyright (C) 2019 Apple Inc. All rights reserved.
4
* Redistribution and use in source and binary forms, with or without
5
* modification, are permitted provided that the following conditions
7
* 1. Redistributions of source code must retain the above copyright
8
* notice, this list of conditions and the following disclaimer.
9
* 2. Redistributions in binary form must reproduce the above copyright
10
* notice, this list of conditions and the following disclaimer in the
11
* documentation and/or other materials provided with the distribution.
13
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23
* THE POSSIBILITY OF SUCH DAMAGE.
27
#include "TextManipulationController.h"
29
#include "CharacterData.h"
31
#include "ElementAncestorIterator.h"
32
#include "EventLoop.h"
33
#include "NodeTraversal.h"
34
#include "PseudoElement.h"
36
#include "ScriptDisallowedScope.h"
38
#include "TextIterator.h"
39
#include "VisibleUnits.h"
43
inline bool TextManipulationController::ExclusionRule::match(const Element& element) const
45
return switchOn(rule, [&element] (ElementRule rule) {
46
return rule.localName == element.localName();
47
}, [&element] (AttributeRule rule) {
48
return equalIgnoringASCIICase(element.getAttribute(rule.name), rule.value);
49
}, [&element] (ClassRule rule) {
50
return element.hasClass() && element.classNames().contains(rule.className);
54
class ExclusionRuleMatcher {
56
using ExclusionRule = TextManipulationController::ExclusionRule;
57
using Type = TextManipulationController::ExclusionRule::Type;
59
ExclusionRuleMatcher(const Vector<ExclusionRule>& rules)
63
bool isExcluded(Node* node)
68
RefPtr<Element> startingElement = is<Element>(*node) ? downcast<Element>(node) : node->parentElement();
72
Type type = Type::Include;
73
RefPtr<Element> matchingElement;
74
for (auto& element : elementLineage(startingElement.get())) {
75
if (auto typeOrNullopt = typeForElement(element)) {
76
type = *typeOrNullopt;
77
matchingElement = &element;
82
for (auto& element : elementLineage(startingElement.get())) {
83
m_cache.set(element, type);
84
if (&element == matchingElement)
88
return type == Type::Exclude;
91
Optional<Type> typeForElement(Element& element)
93
auto it = m_cache.find(element);
94
if (it != m_cache.end())
97
for (auto& rule : m_rules) {
98
if (rule.match(element))
106
const Vector<ExclusionRule>& m_rules;
107
HashMap<Ref<Element>, ExclusionRule::Type> m_cache;
110
TextManipulationController::TextManipulationController(Document& document)
111
: m_document(makeWeakPtr(document))
115
void TextManipulationController::startObservingParagraphs(ManipulationItemCallback&& callback, Vector<ExclusionRule>&& exclusionRules)
117
auto document = makeRefPtr(m_document.get());
121
m_callback = WTFMove(callback);
122
m_exclusionRules = WTFMove(exclusionRules);
124
VisiblePosition start = firstPositionInNode(m_document.get());
125
VisiblePosition end = lastPositionInNode(m_document.get());
127
observeParagraphs(start, end);
130
void TextManipulationController::observeParagraphs(VisiblePosition& start, VisiblePosition& end)
132
auto document = makeRefPtr(start.deepEquivalent().document());
134
TextIterator iterator { start.deepEquivalent(), end.deepEquivalent() };
135
if (document != start.deepEquivalent().document() || document != end.deepEquivalent().document())
136
return; // TextIterator's constructor may have updated the layout and executed arbitrary scripts.
138
ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
139
Vector<ManipulationToken> tokensInCurrentParagraph;
140
Position startOfCurrentParagraph = start.deepEquivalent();
141
while (!iterator.atEnd()) {
142
StringView currentText = iterator.text();
144
if (startOfCurrentParagraph.isNull())
145
startOfCurrentParagraph = iterator.range()->startPosition();
147
size_t endOfLastNewLine = 0;
148
size_t offsetOfNextNewLine = 0;
149
while ((offsetOfNextNewLine = currentText.find('\n', endOfLastNewLine)) != notFound) {
150
if (endOfLastNewLine < offsetOfNextNewLine) {
151
auto stringUntilEndOfLine = currentText.substring(endOfLastNewLine, offsetOfNextNewLine - endOfLastNewLine).toString();
152
tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), stringUntilEndOfLine, exclusionRuleMatcher.isExcluded(iterator.node()) });
155
auto lastRange = iterator.range();
156
if (offsetOfNextNewLine < currentText.length()) {
157
lastRange->setStart(firstPositionInOrBeforeNode(iterator.node())); // Move the start to the beginning of the current node.
158
TextIterator::subrange(lastRange, 0, offsetOfNextNewLine);
160
Position endOfCurrentParagraph = lastRange->endPosition();
162
if (!tokensInCurrentParagraph.isEmpty())
163
addItem(startOfCurrentParagraph, endOfCurrentParagraph, WTFMove(tokensInCurrentParagraph));
164
startOfCurrentParagraph.clear();
165
endOfLastNewLine = offsetOfNextNewLine + 1;
168
auto remainingText = currentText.substring(endOfLastNewLine);
169
if (remainingText.length())
170
tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), exclusionRuleMatcher.isExcluded(iterator.node()) });
175
if (!tokensInCurrentParagraph.isEmpty())
176
addItem(startOfCurrentParagraph, end.deepEquivalent(), WTFMove(tokensInCurrentParagraph));
179
void TextManipulationController::didCreateRendererForElement(Element& element)
181
if (m_recentlyInsertedElements.contains(element))
184
if (m_mutatedElements.computesEmpty())
185
scheduleObservartionUpdate();
187
if (is<PseudoElement>(element)) {
188
if (auto* host = downcast<PseudoElement>(element).hostElement())
189
m_mutatedElements.add(*host);
191
m_mutatedElements.add(element);
194
using PositionTuple = std::tuple<RefPtr<Node>, unsigned, unsigned>;
195
static const PositionTuple makePositionTuple(const Position& position)
197
return { position.anchorNode(), static_cast<unsigned>(position.anchorType()), position.anchorType() == Position::PositionIsOffsetInAnchor ? position.offsetInContainerNode() : 0 };
200
static const std::pair<PositionTuple, PositionTuple> makeHashablePositionRange(const VisiblePosition& start, const VisiblePosition& end)
202
return { makePositionTuple(start.deepEquivalent()), makePositionTuple(end.deepEquivalent()) };
205
void TextManipulationController::scheduleObservartionUpdate()
210
m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakThis = makeWeakPtr(*this)] {
211
auto* controller = weakThis.get();
215
HashSet<Ref<Element>> mutatedElements;
216
for (auto& weakElement : controller->m_mutatedElements)
217
mutatedElements.add(weakElement);
218
controller->m_mutatedElements.clear();
220
HashSet<Ref<Element>> filteredElements;
221
for (auto& element : mutatedElements) {
222
auto* parentElement = element->parentElement();
223
if (!parentElement || !mutatedElements.contains(parentElement))
224
filteredElements.add(element.copyRef());
226
mutatedElements.clear();
228
HashSet<std::pair<PositionTuple, PositionTuple>> paragraphSets;
229
for (auto& element : filteredElements) {
230
auto start = startOfParagraph(firstPositionInOrBeforeNode(element.ptr()));
231
auto end = endOfParagraph(lastPositionInOrAfterNode(element.ptr()));
233
auto key = makeHashablePositionRange(start, end);
234
if (!paragraphSets.add(key).isNewEntry)
237
auto* controller = weakThis.get();
239
return; // Finding the start/end of paragraph may have updated layout & executed arbitrary scripts.
241
controller->observeParagraphs(start, end);
246
void TextManipulationController::addItem(const Position& startOfParagraph, const Position& endOfParagraph, Vector<ManipulationToken>&& tokens)
249
auto result = m_items.add(m_itemIdentifier.generate(), ManipulationItem { startOfParagraph, endOfParagraph, WTFMove(tokens) });
250
m_callback(*m_document, result.iterator->key, result.iterator->value.tokens);
253
auto TextManipulationController::completeManipulation(ItemIdentifier itemIdentifier, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
256
return ManipulationResult::InvalidItem;
258
auto itemIterator = m_items.find(itemIdentifier);
259
if (itemIterator == m_items.end())
260
return ManipulationResult::InvalidItem;
262
ManipulationItem item;
263
std::exchange(item, itemIterator->value);
264
m_items.remove(itemIterator);
266
return replace(item, replacementTokens);
269
struct TokenExchangeData {
271
String originalContent;
272
bool isExcluded { false };
273
bool isConsumed { false };
276
struct ReplacementData {
277
Ref<Node> originalNode;
281
struct NodeInsertion {
282
RefPtr<Node> parentIfDifferentFromCommonAncestor;
286
auto TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
288
if (item.start.isOrphan() || item.end.isOrphan())
289
return ManipulationResult::ContentChanged;
291
TextIterator iterator { item.start, item.end };
292
size_t currentTokenIndex = 0;
293
HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
295
RefPtr<Node> commonAncestor;
296
while (!iterator.atEnd()) {
297
auto string = iterator.text().toString();
298
if (currentTokenIndex >= item.tokens.size())
299
return ManipulationResult::ContentChanged;
300
auto& currentToken = item.tokens[currentTokenIndex];
301
if (iterator.text() != currentToken.content)
302
return ManipulationResult::ContentChanged;
304
auto currentNode = makeRefPtr(iterator.node());
305
tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { currentNode.copyRef(), currentToken.content, currentToken.isExcluded });
308
// FIXME: Take care of when currentNode is nullptr.
310
commonAncestor = currentNode;
311
else if (!currentNode->isDescendantOf(commonAncestor.get())) {
312
commonAncestor = Range::commonAncestorContainer(commonAncestor.get(), currentNode.get());
313
ASSERT(commonAncestor);
320
ASSERT(commonAncestor);
322
RefPtr<Node> nodeAfterStart = item.start.computeNodeAfterPosition();
324
nodeAfterStart = item.start.containerNode();
326
RefPtr<Node> nodeAfterEnd = item.end.computeNodeAfterPosition();
328
nodeAfterEnd = NodeTraversal::nextSkippingChildren(*item.end.containerNode());
330
HashSet<Ref<Node>> nodesToRemove;
331
for (RefPtr<Node> currentNode = nodeAfterStart; currentNode && currentNode != nodeAfterEnd; currentNode = NodeTraversal::next(*currentNode)) {
332
if (commonAncestor == currentNode)
333
commonAncestor = currentNode->parentNode();
334
nodesToRemove.add(*currentNode);
337
Vector<Ref<Node>> currentElementStack;
338
HashSet<Ref<Node>> reusedOriginalNodes;
339
Vector<NodeInsertion> insertions;
340
for (auto& newToken : replacementTokens) {
341
auto it = tokenExchangeMap.find(newToken.identifier);
342
if (it == tokenExchangeMap.end())
343
return ManipulationResult::InvalidToken;
345
auto& exchangeData = it->value;
347
RefPtr<Node> contentNode;
348
if (exchangeData.isExcluded) {
349
if (exchangeData.isConsumed)
350
return ManipulationResult::ExclusionViolation;
351
exchangeData.isConsumed = true;
352
if (!newToken.content.isNull() && newToken.content != exchangeData.originalContent)
353
return ManipulationResult::ExclusionViolation;
354
contentNode = Text::create(commonAncestor->document(), exchangeData.originalContent);
356
contentNode = Text::create(commonAncestor->document(), newToken.content);
358
auto& originalNode = exchangeData.node ? *exchangeData.node : *commonAncestor;
359
RefPtr<ContainerNode> currentNode = is<ContainerNode>(originalNode) ? &downcast<ContainerNode>(originalNode) : originalNode.parentNode();
361
Vector<Ref<Node>> currentAncestors;
362
for (; currentNode && currentNode != commonAncestor; currentNode = currentNode->parentNode())
363
currentAncestors.append(*currentNode);
364
currentAncestors.reverse();
367
while (i < currentElementStack.size() && i < currentAncestors.size() && currentElementStack[i].ptr() == currentAncestors[i].ptr())
370
if (i == currentElementStack.size() && i == currentAncestors.size())
371
insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
373
if (i < currentElementStack.size())
374
currentElementStack.shrink(i);
375
for (;i < currentAncestors.size(); ++i) {
376
Ref<Node> currentNode = currentAncestors[i].copyRef();
377
if (!reusedOriginalNodes.add(currentNode.copyRef()).isNewEntry) {
378
auto clonedNode = currentNode->cloneNodeInternal(currentNode->document(), Node::CloningOperation::OnlySelf);
379
if (auto* data = currentNode->eventTargetData())
380
data->eventListenerMap.copyEventListenersNotCreatedFromMarkupToTarget(clonedNode.ptr());
381
currentNode = WTFMove(clonedNode);
384
insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, currentNode.copyRef() });
385
currentElementStack.append(WTFMove(currentNode));
387
insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
391
Position insertionPoint = item.start;
392
while (insertionPoint.containerNode() != commonAncestor)
393
insertionPoint = positionInParentBeforeNode(insertionPoint.containerNode());
394
ASSERT(!insertionPoint.isNull());
396
for (auto& node : nodesToRemove)
399
for (auto& insertion : insertions) {
400
if (!insertion.parentIfDifferentFromCommonAncestor)
401
insertionPoint.containerNode()->insertBefore(insertion.child, insertionPoint.computeNodeBeforePosition());
403
insertion.parentIfDifferentFromCommonAncestor->appendChild(insertion.child);
404
if (is<Element>(insertion.child.get()))
405
m_recentlyInsertedElements.add(downcast<Element>(insertion.child.get()));
407
m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakThis = makeWeakPtr(*this)] {
408
if (auto strongThis = weakThis.get())
409
strongThis->m_recentlyInsertedElements.clear();
412
return ManipulationResult::Success;
415
} // namespace WebCore