diff --git a/Userland/Libraries/LibWeb/DOM/CharacterData.cpp b/Userland/Libraries/LibWeb/DOM/CharacterData.cpp index eb65ff1d79c..3bf4232ff7c 100644 --- a/Userland/Libraries/LibWeb/DOM/CharacterData.cpp +++ b/Userland/Libraries/LibWeb/DOM/CharacterData.cpp @@ -130,6 +130,8 @@ WebIDL::ExceptionOr CharacterData::replace_data(size_t offset, size_t coun if (m_grapheme_segmenter) m_grapheme_segmenter->set_segmented_text(m_data); + if (m_word_segmenter) + m_word_segmenter->set_segmented_text(m_data); return {}; } @@ -165,4 +167,14 @@ Unicode::Segmenter& CharacterData::grapheme_segmenter() return *m_grapheme_segmenter; } +Unicode::Segmenter& CharacterData::word_segmenter() +{ + if (!m_word_segmenter) { + m_word_segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word); + m_word_segmenter->set_segmented_text(m_data); + } + + return *m_word_segmenter; +} + } diff --git a/Userland/Libraries/LibWeb/DOM/CharacterData.h b/Userland/Libraries/LibWeb/DOM/CharacterData.h index 1b35f9d285f..1ce793d3456 100644 --- a/Userland/Libraries/LibWeb/DOM/CharacterData.h +++ b/Userland/Libraries/LibWeb/DOM/CharacterData.h @@ -41,6 +41,7 @@ public: WebIDL::ExceptionOr replace_data(size_t offset_in_utf16_code_units, size_t count_in_utf16_code_units, String const&); Unicode::Segmenter& grapheme_segmenter(); + Unicode::Segmenter& word_segmenter(); protected: CharacterData(Document&, NodeType, String const&); @@ -51,6 +52,7 @@ private: String m_data; OwnPtr m_grapheme_segmenter; + OwnPtr m_word_segmenter; }; } diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 976c5d99ae3..802c229484e 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -5423,6 +5423,24 @@ bool Document::decrement_cursor_position_offset() return true; } +bool Document::increment_cursor_position_to_next_word() +{ + if (!m_cursor_position->increment_offset_to_next_word()) + return false; + + reset_cursor_blink_cycle(); + return true; +} + +bool Document::decrement_cursor_position_to_previous_word() +{ + if (!m_cursor_position->decrement_offset_to_previous_word()) + return false; + + reset_cursor_blink_cycle(); + return true; +} + void Document::user_did_edit_document_text(Badge) { reset_cursor_blink_cycle(); diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 4f951bb5201..9d1f1c72cad 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -693,6 +693,8 @@ public: void set_cursor_position(JS::NonnullGCPtr); bool increment_cursor_position_offset(); bool decrement_cursor_position_offset(); + bool increment_cursor_position_to_next_word(); + bool decrement_cursor_position_to_previous_word(); bool cursor_blink_state() const { return m_cursor_blink_state; } diff --git a/Userland/Libraries/LibWeb/DOM/Position.cpp b/Userland/Libraries/LibWeb/DOM/Position.cpp index 4407c38dd49..41da742e5da 100644 --- a/Userland/Libraries/LibWeb/DOM/Position.cpp +++ b/Userland/Libraries/LibWeb/DOM/Position.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -66,6 +67,60 @@ bool Position::decrement_offset() return false; } +static bool should_continue_beyond_word(Utf8View const& word) +{ + for (auto code_point : word) { + if (!Unicode::code_point_has_punctuation_general_category(code_point) && !Unicode::code_point_has_separator_general_category(code_point)) + return false; + } + + return true; +} + +bool Position::increment_offset_to_next_word() +{ + if (!is(*m_node) || offset_is_at_end_of_node()) + return false; + + auto& node = static_cast(*m_node); + + while (true) { + if (auto offset = node.word_segmenter().next_boundary(m_offset); offset.has_value()) { + auto word = node.data().code_points().substring_view(m_offset, *offset - m_offset); + m_offset = *offset; + + if (should_continue_beyond_word(word)) + continue; + } + + break; + } + + return true; +} + +bool Position::decrement_offset_to_previous_word() +{ + if (!is(*m_node) || m_offset == 0) + return false; + + auto& node = static_cast(*m_node); + + while (true) { + if (auto offset = node.word_segmenter().previous_boundary(m_offset); offset.has_value()) { + auto word = node.data().code_points().substring_view(*offset, m_offset - *offset); + m_offset = *offset; + + if (should_continue_beyond_word(word)) + continue; + } + + break; + } + + return true; +} + bool Position::offset_is_at_end_of_node() const { if (!is(*m_node)) diff --git a/Userland/Libraries/LibWeb/DOM/Position.h b/Userland/Libraries/LibWeb/DOM/Position.h index e1466d0d768..bcfb89a97f1 100644 --- a/Userland/Libraries/LibWeb/DOM/Position.h +++ b/Userland/Libraries/LibWeb/DOM/Position.h @@ -36,6 +36,9 @@ public: bool increment_offset(); bool decrement_offset(); + bool increment_offset_to_next_word(); + bool decrement_offset_to_previous_word(); + bool equals(JS::NonnullGCPtr other) const { return m_node.ptr() == other->m_node.ptr() && m_offset == other->m_offset;