Coverage Report

Created: 2026-09-26 06:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/DOM/CharacterData.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <LibLocale/Segmenter.h>
8
#include <LibWeb/Bindings/CharacterDataPrototype.h>
9
#include <LibWeb/DOM/CharacterData.h>
10
#include <LibWeb/DOM/Document.h>
11
#include <LibWeb/DOM/MutationType.h>
12
#include <LibWeb/DOM/Range.h>
13
#include <LibWeb/DOM/StaticNodeList.h>
14
#include <LibWeb/Layout/TextNode.h>
15
16
namespace Web::DOM {
17
18
JS_DEFINE_ALLOCATOR(CharacterData);
19
20
CharacterData::CharacterData(Document& document, NodeType type, String const& data)
21
0
    : Node(document, type)
22
0
    , m_data(data)
23
0
{
24
0
}
25
26
0
CharacterData::~CharacterData() = default;
27
28
void CharacterData::initialize(JS::Realm& realm)
29
0
{
30
0
    Base::initialize(realm);
31
0
    WEB_SET_PROTOTYPE_FOR_INTERFACE(CharacterData);
32
0
}
33
34
// https://dom.spec.whatwg.org/#dom-characterdata-data
35
void CharacterData::set_data(String const& data)
36
0
{
37
    // [The data] setter must replace data with node this, offset 0, count this’s length, and data new value.
38
    // NOTE: Since the offset is 0, it can never be above data's length, so this can never throw.
39
    // NOTE: Setting the data to the same value as the current data still causes a mutation observer callback.
40
    // FIXME: Figure out a way to make this a no-op again if the passed in data is the same as the current data.
41
0
    MUST(replace_data(0, this->length_in_utf16_code_units(), data));
42
0
}
43
44
// https://dom.spec.whatwg.org/#concept-cd-substring
45
WebIDL::ExceptionOr<String> CharacterData::substring_data(size_t offset, size_t count) const
46
0
{
47
    // 1. Let length be node’s length.
48
    // FIXME: This is very inefficient!
49
0
    auto utf16_data = MUST(AK::utf8_to_utf16(m_data));
50
0
    Utf16View utf16_view { utf16_data };
51
0
    auto length = utf16_view.length_in_code_units();
52
53
    // 2. If offset is greater than length, then throw an "IndexSizeError" DOMException.
54
0
    if (offset > length)
55
0
        return WebIDL::IndexSizeError::create(realm(), "Substring offset out of range."_string);
56
57
    // 3. If offset plus count is greater than length, return a string whose value is the code units from the offsetth code unit
58
    //    to the end of node’s data, and then return.
59
0
    if (offset + count > length)
60
0
        return MUST(utf16_view.substring_view(offset).to_utf8(Utf16View::AllowInvalidCodeUnits::Yes));
61
62
    // 4. Return a string whose value is the code units from the offsetth code unit to the offset+countth code unit in node’s data.
63
0
    return MUST(utf16_view.substring_view(offset, count).to_utf8(Utf16View::AllowInvalidCodeUnits::Yes));
64
0
}
65
66
// https://dom.spec.whatwg.org/#concept-cd-replace
67
WebIDL::ExceptionOr<void> CharacterData::replace_data(size_t offset, size_t count, String const& data)
68
0
{
69
    // 1. Let length be node’s length.
70
    // FIXME: This is very inefficient!
71
0
    auto utf16_data = MUST(AK::utf8_to_utf16(m_data));
72
0
    Utf16View utf16_view { utf16_data };
73
0
    auto length = utf16_view.length_in_code_units();
74
75
    // 2. If offset is greater than length, then throw an "IndexSizeError" DOMException.
76
0
    if (offset > length)
77
0
        return WebIDL::IndexSizeError::create(realm(), "Replacement offset out of range."_string);
78
79
    // 3. If offset plus count is greater than length, then set count to length minus offset.
80
0
    if (offset + count > length)
81
0
        count = length - offset;
82
83
    // 4. Queue a mutation record of "characterData" for node with null, null, node’s data, « », « », null, and null.
84
0
    queue_mutation_record(MutationType::characterData, {}, {}, m_data, {}, {}, nullptr, nullptr);
85
86
    // 5. Insert data into node’s data after offset code units.
87
    // 6. Let delete offset be offset + data’s length.
88
    // 7. Starting from delete offset code units, remove count code units from node’s data.
89
0
    StringBuilder builder;
90
0
    builder.append(MUST(utf16_view.substring_view(0, offset).to_utf8(Utf16View::AllowInvalidCodeUnits::Yes)));
91
0
    builder.append(data);
92
0
    builder.append(MUST(utf16_view.substring_view(offset + count).to_utf8(Utf16View::AllowInvalidCodeUnits::Yes)));
93
0
    m_data = MUST(builder.to_string());
94
95
    // 8. For each live range whose start node is node and start offset is greater than offset but less than or equal to offset plus count, set its start offset to offset.
96
0
    for (auto& range : Range::live_ranges()) {
97
0
        if (range->start_container() == this && range->start_offset() > offset && range->start_offset() <= (offset + count))
98
0
            TRY(range->set_start(*range->start_container(), offset));
99
0
    }
100
101
    // 9. For each live range whose end node is node and end offset is greater than offset but less than or equal to offset plus count, set its end offset to offset.
102
0
    for (auto& range : Range::live_ranges()) {
103
0
        if (range->end_container() == this && range->end_offset() > offset && range->end_offset() <= (offset + count))
104
0
            TRY(range->set_end(*range->end_container(), offset));
105
0
    }
106
107
    // 10. For each live range whose start node is node and start offset is greater than offset plus count, increase its start offset by data’s length and decrease it by count.
108
0
    for (auto& range : Range::live_ranges()) {
109
0
        if (range->start_container() == this && range->start_offset() > (offset + count))
110
0
            TRY(range->set_start(*range->start_container(), range->start_offset() + data.bytes().size() - count));
111
0
    }
112
113
    // 11. For each live range whose end node is node and end offset is greater than offset plus count, increase its end offset by data’s length and decrease it by count.
114
0
    for (auto& range : Range::live_ranges()) {
115
0
        if (range->end_container() == this && range->end_offset() > (offset + count))
116
0
            TRY(range->set_end(*range->end_container(), range->end_offset() + data.bytes().size() - count));
117
0
    }
118
119
    // 12. If node’s parent is non-null, then run the children changed steps for node’s parent.
120
0
    if (parent())
121
0
        parent()->children_changed();
122
123
    // NOTE: Since the text node's data has changed, we need to invalidate the text for rendering.
124
    //       This ensures that the new text is reflected in layout, even if we don't end up
125
    //       doing a full layout tree rebuild.
126
0
    if (auto* layout_node = this->layout_node(); layout_node && layout_node->is_text_node())
127
0
        static_cast<Layout::TextNode&>(*layout_node).invalidate_text_for_rendering();
128
129
0
    document().set_needs_layout();
130
131
0
    if (m_grapheme_segmenter)
132
0
        m_grapheme_segmenter->set_segmented_text(m_data);
133
0
    if (m_word_segmenter)
134
0
        m_word_segmenter->set_segmented_text(m_data);
135
136
0
    return {};
137
0
}
138
139
// https://dom.spec.whatwg.org/#dom-characterdata-appenddata
140
WebIDL::ExceptionOr<void> CharacterData::append_data(String const& data)
141
0
{
142
    // The appendData(data) method steps are to replace data with node this, offset this’s length, count 0, and data data.
143
0
    return replace_data(this->length_in_utf16_code_units(), 0, data);
144
0
}
145
146
// https://dom.spec.whatwg.org/#dom-characterdata-insertdata
147
WebIDL::ExceptionOr<void> CharacterData::insert_data(size_t offset, String const& data)
148
0
{
149
    // The insertData(offset, data) method steps are to replace data with node this, offset offset, count 0, and data data.
150
0
    return replace_data(offset, 0, data);
151
0
}
152
153
// https://dom.spec.whatwg.org/#dom-characterdata-deletedata
154
WebIDL::ExceptionOr<void> CharacterData::delete_data(size_t offset, size_t count)
155
0
{
156
    // The deleteData(offset, count) method steps are to replace data with node this, offset offset, count count, and data the empty string.
157
0
    return replace_data(offset, count, String {});
158
0
}
159
160
Locale::Segmenter& CharacterData::grapheme_segmenter() const
161
0
{
162
0
    if (!m_grapheme_segmenter) {
163
0
        m_grapheme_segmenter = document().grapheme_segmenter().clone();
164
0
        m_grapheme_segmenter->set_segmented_text(m_data);
165
0
    }
166
167
0
    return *m_grapheme_segmenter;
168
0
}
169
170
Locale::Segmenter& CharacterData::word_segmenter() const
171
0
{
172
0
    if (!m_word_segmenter) {
173
0
        m_word_segmenter = document().word_segmenter().clone();
174
0
        m_word_segmenter->set_segmented_text(m_data);
175
0
    }
176
177
0
    return *m_word_segmenter;
178
0
}
179
180
}