~mmach/netext73/webkit2gtk

« back to all changes in this revision

Viewing changes to Source/WebCore/editing/TextManipulationController.cpp

  • Committer: mmach
  • Date: 2023-06-16 17:21:37 UTC
  • Revision ID: netbit73@gmail.com-20230616172137-2rqx6yr96ga9g3kp
1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright (C) 2019 Apple Inc. All rights reserved.
 
3
 *
 
4
 * Redistribution and use in source and binary forms, with or without
 
5
 * modification, are permitted provided that the following conditions
 
6
 * are met:
 
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.
 
12
 *
 
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.
 
24
 */
 
25
 
 
26
#include "config.h"
 
27
#include "TextManipulationController.h"
 
28
 
 
29
#include "CharacterData.h"
 
30
#include "Editing.h"
 
31
#include "ElementAncestorIterator.h"
 
32
#include "EventLoop.h"
 
33
#include "NodeTraversal.h"
 
34
#include "PseudoElement.h"
 
35
#include "Range.h"
 
36
#include "ScriptDisallowedScope.h"
 
37
#include "Text.h"
 
38
#include "TextIterator.h"
 
39
#include "VisibleUnits.h"
 
40
 
 
41
namespace WebCore {
 
42
 
 
43
inline bool TextManipulationController::ExclusionRule::match(const Element& element) const
 
44
{
 
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);
 
51
    });
 
52
}
 
53
 
 
54
class ExclusionRuleMatcher {
 
55
public:
 
56
    using ExclusionRule = TextManipulationController::ExclusionRule;
 
57
    using Type = TextManipulationController::ExclusionRule::Type;
 
58
 
 
59
    ExclusionRuleMatcher(const Vector<ExclusionRule>& rules)
 
60
        : m_rules(rules)
 
61
    { }
 
62
 
 
63
    bool isExcluded(Node* node)
 
64
    {
 
65
        if (!node)
 
66
            return false;
 
67
 
 
68
        RefPtr<Element> startingElement = is<Element>(*node) ? downcast<Element>(node) : node->parentElement();
 
69
        if (!startingElement)
 
70
            return false;
 
71
 
 
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;
 
78
                break;
 
79
            }
 
80
        }
 
81
 
 
82
        for (auto& element : elementLineage(startingElement.get())) {
 
83
            m_cache.set(element, type);
 
84
            if (&element == matchingElement)
 
85
                break;
 
86
        }
 
87
 
 
88
        return type == Type::Exclude;
 
89
    }
 
90
 
 
91
    Optional<Type> typeForElement(Element& element)
 
92
    {
 
93
        auto it = m_cache.find(element);
 
94
        if (it != m_cache.end())
 
95
            return it->value;
 
96
 
 
97
        for (auto& rule : m_rules) {
 
98
            if (rule.match(element))
 
99
                return rule.type;
 
100
        }
 
101
 
 
102
        return WTF::nullopt;
 
103
    }
 
104
 
 
105
private:
 
106
    const Vector<ExclusionRule>& m_rules;
 
107
    HashMap<Ref<Element>, ExclusionRule::Type> m_cache;
 
108
};
 
109
 
 
110
TextManipulationController::TextManipulationController(Document& document)
 
111
    : m_document(makeWeakPtr(document))
 
112
{
 
113
}
 
114
 
 
115
void TextManipulationController::startObservingParagraphs(ManipulationItemCallback&& callback, Vector<ExclusionRule>&& exclusionRules)
 
116
{
 
117
    auto document = makeRefPtr(m_document.get());
 
118
    if (!document)
 
119
        return;
 
120
 
 
121
    m_callback = WTFMove(callback);
 
122
    m_exclusionRules = WTFMove(exclusionRules);
 
123
 
 
124
    VisiblePosition start = firstPositionInNode(m_document.get());
 
125
    VisiblePosition end = lastPositionInNode(m_document.get());
 
126
 
 
127
    observeParagraphs(start, end);
 
128
}
 
129
 
 
130
void TextManipulationController::observeParagraphs(VisiblePosition& start, VisiblePosition& end)
 
131
{
 
132
    auto document = makeRefPtr(start.deepEquivalent().document());
 
133
    ASSERT(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.
 
137
 
 
138
    ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
 
139
    Vector<ManipulationToken> tokensInCurrentParagraph;
 
140
    Position startOfCurrentParagraph = start.deepEquivalent();
 
141
    while (!iterator.atEnd()) {
 
142
        StringView currentText = iterator.text();
 
143
 
 
144
        if (startOfCurrentParagraph.isNull())
 
145
            startOfCurrentParagraph = iterator.range()->startPosition();
 
146
 
 
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()) });
 
153
            }
 
154
 
 
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);
 
159
            }
 
160
            Position endOfCurrentParagraph = lastRange->endPosition();
 
161
 
 
162
            if (!tokensInCurrentParagraph.isEmpty())
 
163
                addItem(startOfCurrentParagraph, endOfCurrentParagraph, WTFMove(tokensInCurrentParagraph));
 
164
            startOfCurrentParagraph.clear();
 
165
            endOfLastNewLine = offsetOfNextNewLine + 1;
 
166
        }
 
167
 
 
168
        auto remainingText = currentText.substring(endOfLastNewLine);
 
169
        if (remainingText.length())
 
170
            tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), exclusionRuleMatcher.isExcluded(iterator.node()) });
 
171
 
 
172
        iterator.advance();
 
173
    }
 
174
 
 
175
    if (!tokensInCurrentParagraph.isEmpty())
 
176
        addItem(startOfCurrentParagraph, end.deepEquivalent(), WTFMove(tokensInCurrentParagraph));
 
177
}
 
178
 
 
179
void TextManipulationController::didCreateRendererForElement(Element& element)
 
180
{
 
181
    if (m_recentlyInsertedElements.contains(element))
 
182
        return;
 
183
 
 
184
    if (m_mutatedElements.computesEmpty())
 
185
        scheduleObservartionUpdate();
 
186
 
 
187
    if (is<PseudoElement>(element)) {
 
188
        if (auto* host = downcast<PseudoElement>(element).hostElement())
 
189
            m_mutatedElements.add(*host);
 
190
    } else
 
191
        m_mutatedElements.add(element);
 
192
}
 
193
 
 
194
using PositionTuple = std::tuple<RefPtr<Node>, unsigned, unsigned>;
 
195
static const PositionTuple makePositionTuple(const Position& position)
 
196
{
 
197
    return { position.anchorNode(), static_cast<unsigned>(position.anchorType()), position.anchorType() == Position::PositionIsOffsetInAnchor ? position.offsetInContainerNode() : 0 }; 
 
198
}
 
199
 
 
200
static const std::pair<PositionTuple, PositionTuple> makeHashablePositionRange(const VisiblePosition& start, const VisiblePosition& end)
 
201
{
 
202
    return { makePositionTuple(start.deepEquivalent()), makePositionTuple(end.deepEquivalent()) };
 
203
}
 
204
 
 
205
void TextManipulationController::scheduleObservartionUpdate()
 
206
{
 
207
    if (!m_document)
 
208
        return;
 
209
 
 
210
    m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakThis = makeWeakPtr(*this)] {
 
211
        auto* controller = weakThis.get();
 
212
        if (!controller)
 
213
            return;
 
214
 
 
215
        HashSet<Ref<Element>> mutatedElements;
 
216
        for (auto& weakElement : controller->m_mutatedElements)
 
217
            mutatedElements.add(weakElement);
 
218
        controller->m_mutatedElements.clear();
 
219
 
 
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());
 
225
        }
 
226
        mutatedElements.clear();
 
227
 
 
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()));
 
232
 
 
233
            auto key = makeHashablePositionRange(start, end);
 
234
            if (!paragraphSets.add(key).isNewEntry)
 
235
                continue;
 
236
 
 
237
            auto* controller = weakThis.get();
 
238
            if (!controller)
 
239
                return; // Finding the start/end of paragraph may have updated layout & executed arbitrary scripts.
 
240
 
 
241
            controller->observeParagraphs(start, end);
 
242
        }
 
243
    });
 
244
}
 
245
 
 
246
void TextManipulationController::addItem(const Position& startOfParagraph, const Position& endOfParagraph, Vector<ManipulationToken>&& tokens)
 
247
{
 
248
    ASSERT(m_document);
 
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);
 
251
}
 
252
 
 
253
auto TextManipulationController::completeManipulation(ItemIdentifier itemIdentifier, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
 
254
{
 
255
    if (!itemIdentifier)
 
256
        return ManipulationResult::InvalidItem;
 
257
 
 
258
    auto itemIterator = m_items.find(itemIdentifier);
 
259
    if (itemIterator == m_items.end())
 
260
        return ManipulationResult::InvalidItem;
 
261
 
 
262
    ManipulationItem item;
 
263
    std::exchange(item, itemIterator->value);
 
264
    m_items.remove(itemIterator);
 
265
 
 
266
    return replace(item, replacementTokens);
 
267
}
 
268
 
 
269
struct TokenExchangeData {
 
270
    RefPtr<Node> node;
 
271
    String originalContent;
 
272
    bool isExcluded { false };
 
273
    bool isConsumed { false };
 
274
};
 
275
 
 
276
struct ReplacementData {
 
277
    Ref<Node> originalNode;
 
278
    String newData;
 
279
};
 
280
 
 
281
struct NodeInsertion {
 
282
    RefPtr<Node> parentIfDifferentFromCommonAncestor;
 
283
    Ref<Node> child;
 
284
};
 
285
 
 
286
auto TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
 
287
{
 
288
    if (item.start.isOrphan() || item.end.isOrphan())
 
289
        return ManipulationResult::ContentChanged;
 
290
 
 
291
    TextIterator iterator { item.start, item.end };
 
292
    size_t currentTokenIndex = 0;
 
293
    HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
 
294
 
 
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;
 
303
 
 
304
        auto currentNode = makeRefPtr(iterator.node());
 
305
        tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { currentNode.copyRef(), currentToken.content, currentToken.isExcluded });
 
306
 
 
307
        if (currentNode) {
 
308
            // FIXME: Take care of when currentNode is nullptr.
 
309
            if (!commonAncestor)
 
310
                commonAncestor = currentNode;
 
311
            else if (!currentNode->isDescendantOf(commonAncestor.get())) {
 
312
                commonAncestor = Range::commonAncestorContainer(commonAncestor.get(), currentNode.get());
 
313
                ASSERT(commonAncestor);
 
314
            }
 
315
        }
 
316
 
 
317
        iterator.advance();
 
318
        ++currentTokenIndex;
 
319
    }
 
320
    ASSERT(commonAncestor);
 
321
 
 
322
    RefPtr<Node> nodeAfterStart = item.start.computeNodeAfterPosition();
 
323
    if (!nodeAfterStart)
 
324
        nodeAfterStart = item.start.containerNode();
 
325
 
 
326
    RefPtr<Node> nodeAfterEnd = item.end.computeNodeAfterPosition();
 
327
    if (!nodeAfterEnd)
 
328
        nodeAfterEnd = NodeTraversal::nextSkippingChildren(*item.end.containerNode());
 
329
 
 
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);
 
335
    }
 
336
 
 
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;
 
344
 
 
345
        auto& exchangeData = it->value;
 
346
 
 
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);
 
355
        } else
 
356
            contentNode = Text::create(commonAncestor->document(), newToken.content);
 
357
 
 
358
        auto& originalNode = exchangeData.node ? *exchangeData.node : *commonAncestor;
 
359
        RefPtr<ContainerNode> currentNode = is<ContainerNode>(originalNode) ? &downcast<ContainerNode>(originalNode) : originalNode.parentNode();
 
360
 
 
361
        Vector<Ref<Node>> currentAncestors;
 
362
        for (; currentNode && currentNode != commonAncestor; currentNode = currentNode->parentNode())
 
363
            currentAncestors.append(*currentNode);
 
364
        currentAncestors.reverse();
 
365
 
 
366
        size_t i =0;
 
367
        while (i < currentElementStack.size() && i < currentAncestors.size() && currentElementStack[i].ptr() == currentAncestors[i].ptr())
 
368
            ++i;
 
369
 
 
370
        if (i == currentElementStack.size() && i == currentAncestors.size())
 
371
            insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
 
372
        else {
 
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);
 
382
                }
 
383
 
 
384
                insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, currentNode.copyRef() });
 
385
                currentElementStack.append(WTFMove(currentNode));
 
386
            }
 
387
            insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
 
388
        }
 
389
    }
 
390
 
 
391
    Position insertionPoint = item.start;
 
392
    while (insertionPoint.containerNode() != commonAncestor)
 
393
        insertionPoint = positionInParentBeforeNode(insertionPoint.containerNode());
 
394
    ASSERT(!insertionPoint.isNull());
 
395
 
 
396
    for (auto& node : nodesToRemove)
 
397
        node->remove();
 
398
 
 
399
    for (auto& insertion : insertions) {
 
400
        if (!insertion.parentIfDifferentFromCommonAncestor)
 
401
            insertionPoint.containerNode()->insertBefore(insertion.child, insertionPoint.computeNodeBeforePosition());
 
402
        else
 
403
            insertion.parentIfDifferentFromCommonAncestor->appendChild(insertion.child);
 
404
        if (is<Element>(insertion.child.get()))
 
405
            m_recentlyInsertedElements.add(downcast<Element>(insertion.child.get()));
 
406
    }
 
407
    m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakThis = makeWeakPtr(*this)] {
 
408
        if (auto strongThis = weakThis.get())
 
409
            strongThis->m_recentlyInsertedElements.clear();
 
410
    });
 
411
 
 
412
    return ManipulationResult::Success;
 
413
}
 
414
 
 
415
} // namespace WebCore