Coverage Report

Created: 2026-02-16 07:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/DOM/Position.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3
 * Copyright (c) 2021, Max Wipfli <mail@maxwipfli.ch>
4
 *
5
 * SPDX-License-Identifier: BSD-2-Clause
6
 */
7
8
#include <AK/Utf8View.h>
9
#include <LibLocale/Segmenter.h>
10
#include <LibUnicode/CharacterTypes.h>
11
#include <LibWeb/DOM/Node.h>
12
#include <LibWeb/DOM/Position.h>
13
#include <LibWeb/DOM/Text.h>
14
15
namespace Web::DOM {
16
17
JS_DEFINE_ALLOCATOR(Position);
18
19
Position::Position(JS::GCPtr<Node> node, unsigned offset)
20
0
    : m_node(node)
21
0
    , m_offset(offset)
22
0
{
23
0
}
24
25
void Position::visit_edges(Visitor& visitor)
26
0
{
27
0
    Base::visit_edges(visitor);
28
0
    visitor.visit(m_node);
29
0
}
30
31
ErrorOr<String> Position::to_string() const
32
0
{
33
0
    if (!node())
34
0
        return String::formatted("DOM::Position(nullptr, {})", offset());
35
0
    return String::formatted("DOM::Position({} ({})), {})", node()->node_name(), node().ptr(), offset());
36
0
}
37
38
bool Position::increment_offset()
39
0
{
40
0
    if (!is<DOM::Text>(*m_node))
41
0
        return false;
42
43
0
    auto& node = verify_cast<DOM::Text>(*m_node);
44
45
0
    if (auto offset = node.grapheme_segmenter().next_boundary(m_offset); offset.has_value()) {
46
0
        m_offset = *offset;
47
0
        return true;
48
0
    }
49
50
    // NOTE: Already at end of current node.
51
0
    return false;
52
0
}
53
54
bool Position::decrement_offset()
55
0
{
56
0
    if (!is<DOM::Text>(*m_node))
57
0
        return false;
58
59
0
    auto& node = verify_cast<DOM::Text>(*m_node);
60
61
0
    if (auto offset = node.grapheme_segmenter().previous_boundary(m_offset); offset.has_value()) {
62
0
        m_offset = *offset;
63
0
        return true;
64
0
    }
65
66
    // NOTE: Already at beginning of current node.
67
0
    return false;
68
0
}
69
70
static bool should_continue_beyond_word(Utf8View const& word)
71
0
{
72
0
    for (auto code_point : word) {
73
0
        if (!Unicode::code_point_has_punctuation_general_category(code_point) && !Unicode::code_point_has_separator_general_category(code_point))
74
0
            return false;
75
0
    }
76
77
0
    return true;
78
0
}
79
80
bool Position::increment_offset_to_next_word()
81
0
{
82
0
    if (!is<DOM::Text>(*m_node) || offset_is_at_end_of_node())
83
0
        return false;
84
85
0
    auto& node = static_cast<DOM::Text&>(*m_node);
86
87
0
    while (true) {
88
0
        if (auto offset = node.word_segmenter().next_boundary(m_offset); offset.has_value()) {
89
0
            auto word = node.data().code_points().substring_view(m_offset, *offset - m_offset);
90
0
            m_offset = *offset;
91
92
0
            if (should_continue_beyond_word(word))
93
0
                continue;
94
0
        }
95
96
0
        break;
97
0
    }
98
99
0
    return true;
100
0
}
101
102
bool Position::decrement_offset_to_previous_word()
103
0
{
104
0
    if (!is<DOM::Text>(*m_node) || m_offset == 0)
105
0
        return false;
106
107
0
    auto& node = static_cast<DOM::Text&>(*m_node);
108
109
0
    while (true) {
110
0
        if (auto offset = node.word_segmenter().previous_boundary(m_offset); offset.has_value()) {
111
0
            auto word = node.data().code_points().substring_view(*offset, m_offset - *offset);
112
0
            m_offset = *offset;
113
114
0
            if (should_continue_beyond_word(word))
115
0
                continue;
116
0
        }
117
118
0
        break;
119
0
    }
120
121
0
    return true;
122
0
}
123
124
bool Position::offset_is_at_end_of_node() const
125
0
{
126
0
    if (!is<DOM::Text>(*m_node))
127
0
        return false;
128
129
0
    auto& node = verify_cast<DOM::Text>(*m_node);
130
0
    auto text = node.data();
131
0
    return m_offset == text.bytes_as_string_view().length();
132
0
}
133
134
}