Coverage Report

Created: 2025-11-02 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/Page/EditEventHandler.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2020-2021, the SerenityOS developers.
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/StringBuilder.h>
8
#include <AK/Utf8View.h>
9
#include <LibLocale/Segmenter.h>
10
#include <LibWeb/DOM/Document.h>
11
#include <LibWeb/DOM/Position.h>
12
#include <LibWeb/DOM/Range.h>
13
#include <LibWeb/DOM/Text.h>
14
#include <LibWeb/HTML/BrowsingContext.h>
15
#include <LibWeb/Layout/Viewport.h>
16
#include <LibWeb/Page/EditEventHandler.h>
17
18
namespace Web {
19
20
void EditEventHandler::handle_delete_character_after(JS::NonnullGCPtr<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> cursor_position)
21
0
{
22
0
    auto& node = verify_cast<DOM::Text>(*cursor_position->node());
23
0
    auto& text = node.data();
24
25
0
    auto next_offset = node.grapheme_segmenter().next_boundary(cursor_position->offset());
26
0
    if (!next_offset.has_value()) {
27
        // FIXME: Move to the next node and delete the first character there.
28
0
        return;
29
0
    }
30
31
0
    StringBuilder builder;
32
0
    builder.append(text.bytes_as_string_view().substring_view(0, cursor_position->offset()));
33
0
    builder.append(text.bytes_as_string_view().substring_view(*next_offset));
34
0
    node.set_data(MUST(builder.to_string()));
35
36
0
    document->user_did_edit_document_text({});
37
0
}
38
39
// This method is quite convoluted but this is necessary to make editing feel intuitive.
40
void EditEventHandler::handle_delete(JS::NonnullGCPtr<DOM::Document> document, DOM::Range& range)
41
0
{
42
0
    auto* start = verify_cast<DOM::Text>(range.start_container());
43
0
    auto* end = verify_cast<DOM::Text>(range.end_container());
44
45
0
    if (start == end) {
46
0
        StringBuilder builder;
47
0
        builder.append(start->data().bytes_as_string_view().substring_view(0, range.start_offset()));
48
0
        builder.append(end->data().bytes_as_string_view().substring_view(range.end_offset()));
49
50
0
        start->set_data(MUST(builder.to_string()));
51
0
    } else {
52
        // Remove all the nodes that are fully enclosed in the range.
53
0
        HashTable<DOM::Node*> queued_for_deletion;
54
0
        for (auto* node = start->next_in_pre_order(); node; node = node->next_in_pre_order()) {
55
0
            if (node == end)
56
0
                break;
57
58
0
            queued_for_deletion.set(node);
59
0
        }
60
0
        for (auto* parent = start->parent(); parent; parent = parent->parent())
61
0
            queued_for_deletion.remove(parent);
62
0
        for (auto* parent = end->parent(); parent; parent = parent->parent())
63
0
            queued_for_deletion.remove(parent);
64
0
        for (auto* node : queued_for_deletion)
65
0
            node->remove();
66
67
        // Join the parent nodes of start and end.
68
0
        DOM::Node *insert_after = start, *remove_from = end, *parent_of_end = end->parent();
69
0
        while (remove_from) {
70
0
            auto* next_sibling = remove_from->next_sibling();
71
72
0
            remove_from->remove();
73
0
            insert_after->parent()->insert_before(*remove_from, *insert_after);
74
75
0
            insert_after = remove_from;
76
0
            remove_from = next_sibling;
77
0
        }
78
0
        if (!parent_of_end->has_children()) {
79
0
            if (parent_of_end->parent())
80
0
                parent_of_end->remove();
81
0
        }
82
83
        // Join the start and end nodes.
84
0
        StringBuilder builder;
85
0
        builder.append(start->data().bytes_as_string_view().substring_view(0, range.start_offset()));
86
0
        builder.append(end->data().bytes_as_string_view().substring_view(range.end_offset()));
87
88
0
        start->set_data(MUST(builder.to_string()));
89
0
        end->remove();
90
0
    }
91
92
0
    document->user_did_edit_document_text({});
93
0
}
94
95
void EditEventHandler::handle_insert(JS::NonnullGCPtr<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> position, u32 code_point)
96
0
{
97
0
    StringBuilder builder;
98
0
    builder.append_code_point(code_point);
99
0
    handle_insert(document, position, MUST(builder.to_string()));
100
0
}
101
102
void EditEventHandler::handle_insert(JS::NonnullGCPtr<DOM::Document> document, JS::NonnullGCPtr<DOM::Position> position, String data)
103
0
{
104
0
    if (is<DOM::Text>(*position->node())) {
105
0
        auto& node = verify_cast<DOM::Text>(*position->node());
106
107
0
        StringBuilder builder;
108
0
        builder.append(node.data().bytes_as_string_view().substring_view(0, position->offset()));
109
0
        builder.append(data);
110
0
        builder.append(node.data().bytes_as_string_view().substring_view(position->offset()));
111
112
        // Cut string by max length
113
        // FIXME: Cut by UTF-16 code units instead of raw bytes
114
0
        if (auto max_length = node.max_length(); max_length.has_value() && builder.string_view().length() > *max_length) {
115
0
            node.set_data(MUST(String::from_utf8(builder.string_view().substring_view(0, *max_length))));
116
0
        } else {
117
0
            node.set_data(MUST(builder.to_string()));
118
0
        }
119
0
        node.invalidate_style(DOM::StyleInvalidationReason::EditingInsertion);
120
0
    } else {
121
0
        auto& node = *position->node();
122
0
        auto& realm = node.realm();
123
0
        auto text = realm.heap().allocate<DOM::Text>(realm, node.document(), data);
124
0
        MUST(node.append_child(*text));
125
0
        position->set_node(text);
126
0
        position->set_offset(1);
127
0
    }
128
129
0
    document->user_did_edit_document_text({});
130
0
}
131
132
}