2020-01-18 09:38:21 +01:00
/*
2021-04-27 13:05:50 +02:00
* Copyright ( c ) 2018 - 2021 , Andreas Kling < kling @ serenityos . org >
2022-01-20 20:30:00 +01:00
* Copyright ( c ) 2022 , Tobias Christiansen < tobyase @ serenityos . org >
2020-01-18 09:38:21 +01:00
*
2021-04-22 01:24:48 -07:00
* SPDX - License - Identifier : BSD - 2 - Clause
2020-01-18 09:38:21 +01:00
*/
2021-06-01 21:18:08 +02:00
# include <AK/CharacterTypes.h>
2019-09-25 12:36:44 +03:00
# include <AK/StringBuilder.h>
2020-03-07 10:32:51 +01:00
# include <LibWeb/DOM/Document.h>
2021-10-06 20:02:41 +02:00
# include <LibWeb/Layout/BlockContainer.h>
2020-12-05 20:10:39 +01:00
# include <LibWeb/Layout/InlineFormattingContext.h>
2020-11-22 15:53:01 +01:00
# include <LibWeb/Layout/TextNode.h>
2022-03-10 16:46:44 +01:00
# include <LibWeb/Painting/TextPaintable.h>
2019-06-15 22:49:44 +02:00
2020-11-22 15:53:01 +01:00
namespace Web : : Layout {
2020-03-07 10:27:02 +01:00
2020-11-22 15:53:01 +01:00
TextNode : : TextNode ( DOM : : Document & document , DOM : : Text & text )
: Node ( document , & text )
2019-06-15 22:49:44 +02:00
{
2019-10-06 11:09:38 +02:00
set_inline ( true ) ;
2019-06-15 22:49:44 +02:00
}
2022-03-14 13:21:51 -06:00
TextNode : : ~ TextNode ( ) = default ;
2019-06-15 23:17:08 +02:00
2021-11-11 00:55:02 +01:00
static bool is_all_whitespace ( StringView string )
2019-06-15 23:17:08 +02:00
{
2019-12-09 17:45:40 +01:00
for ( size_t i = 0 ; i < string . length ( ) ; + + i ) {
2021-06-01 21:18:08 +02:00
if ( ! is_ascii_space ( string [ i ] ) )
2019-06-15 23:17:08 +02:00
return false ;
}
return true ;
}
2021-06-03 22:33:53 +02:00
// NOTE: This collapes whitespace into a single ASCII space if collapse is true. If previous_is_empty_or_ends_in_whitespace, it also strips leading whitespace.
2021-04-29 08:51:31 +02:00
void TextNode : : compute_text_for_rendering ( bool collapse , bool previous_is_empty_or_ends_in_whitespace )
{
2021-06-03 22:33:53 +02:00
auto & data = dom_node ( ) . data ( ) ;
if ( ! collapse | | data . is_empty ( ) ) {
m_text_for_rendering = data ;
2021-04-29 08:51:31 +02:00
return ;
}
2021-06-03 22:33:53 +02:00
// NOTE: A couple fast returns to avoid unnecessarily allocating a StringBuilder.
if ( data . length ( ) = = 1 ) {
if ( is_ascii_space ( data [ 0 ] ) ) {
if ( previous_is_empty_or_ends_in_whitespace )
m_text_for_rendering = String : : empty ( ) ;
else {
static String s_single_space_string = " " ;
m_text_for_rendering = s_single_space_string ;
}
} else {
m_text_for_rendering = data ;
2021-04-29 08:51:31 +02:00
}
2021-06-03 22:33:53 +02:00
return ;
}
bool contains_space = false ;
for ( auto & c : data ) {
if ( is_ascii_space ( c ) ) {
contains_space = true ;
break ;
}
}
if ( ! contains_space ) {
m_text_for_rendering = data ;
return ;
}
StringBuilder builder ( data . length ( ) ) ;
size_t index = 0 ;
auto skip_over_whitespace = [ & index , & data ] {
while ( index < data . length ( ) & & is_ascii_space ( data [ index ] ) )
+ + index ;
2021-04-29 08:51:31 +02:00
} ;
2021-06-03 22:33:53 +02:00
2021-04-29 08:51:31 +02:00
if ( previous_is_empty_or_ends_in_whitespace )
skip_over_whitespace ( ) ;
2021-06-03 22:33:53 +02:00
while ( index < data . length ( ) ) {
if ( is_ascii_space ( data [ index ] ) ) {
2021-04-29 08:51:31 +02:00
builder . append ( ' ' ) ;
2021-06-03 22:33:53 +02:00
+ + index ;
2021-04-29 08:51:31 +02:00
skip_over_whitespace ( ) ;
2021-06-03 22:33:53 +02:00
} else {
builder . append ( data [ index ] ) ;
+ + index ;
2021-04-29 08:51:31 +02:00
}
}
2021-06-03 22:33:53 +02:00
2021-04-29 08:51:31 +02:00
m_text_for_rendering = builder . to_string ( ) ;
}
2021-11-11 00:55:02 +01:00
TextNode : : ChunkIterator : : ChunkIterator ( StringView text , LayoutMode layout_mode , bool wrap_lines , bool respect_linebreaks )
2021-04-27 13:05:50 +02:00
: m_layout_mode ( layout_mode )
, m_wrap_lines ( wrap_lines )
2021-08-28 00:53:59 +00:00
, m_respect_linebreaks ( respect_linebreaks )
2021-04-27 13:05:50 +02:00
, m_utf8_view ( text )
, m_iterator ( m_utf8_view . begin ( ) )
{
2021-06-01 21:18:08 +02:00
m_last_was_space = ! text . is_empty ( ) & & is_ascii_space ( * m_utf8_view . begin ( ) ) ;
2021-04-27 13:05:50 +02:00
}
Optional < TextNode : : Chunk > TextNode : : ChunkIterator : : next ( )
{
2021-08-28 00:45:28 +00:00
if ( m_iterator = = m_utf8_view . end ( ) )
return { } ;
auto start_of_chunk = m_iterator ;
2021-04-27 13:05:50 +02:00
while ( m_iterator ! = m_utf8_view . end ( ) ) {
2021-08-28 00:45:28 +00:00
+ + m_iterator ;
if ( m_last_was_newline ) {
// NOTE: This expression looks out for the case where we have
// multiple newlines in a row. Because every output next()
// that's a newline newline must be prepared for in advance by
// the previous next() call, we need to check whether the next
// character is a newline here as well. Otherwise, the newline
// becomes part of the next expression and causes rendering
// issues.
m_last_was_newline = m_iterator ! = m_utf8_view . end ( ) & & * m_iterator = = ' \n ' ;
if ( auto result = try_commit_chunk ( start_of_chunk , m_iterator , true ) ; result . has_value ( ) )
2021-04-27 13:05:50 +02:00
return result . release_value ( ) ;
}
2021-08-28 00:45:28 +00:00
// NOTE: The checks after this need to look at the current iterator
// position, which depends on not being at the end.
if ( m_iterator = = m_utf8_view . end ( ) )
break ;
// NOTE: When we're supposed to stop on linebreaks, we're actually
// supposed to output two chunks: "content" and "\n". Since we
// can't output two chunks at once, we store this information as a
// flag to output the newline immediately at the earliest
// opportunity.
2021-08-28 00:53:59 +00:00
if ( m_respect_linebreaks & & * m_iterator = = ' \n ' ) {
2021-04-27 13:05:50 +02:00
m_last_was_newline = true ;
2021-08-28 00:45:28 +00:00
if ( auto result = try_commit_chunk ( start_of_chunk , m_iterator , false ) ; result . has_value ( ) ) {
2021-04-27 13:05:50 +02:00
return result . release_value ( ) ;
2021-08-28 00:45:28 +00:00
}
2021-04-27 13:05:50 +02:00
}
2021-08-28 00:45:28 +00:00
2022-03-19 15:44:02 +01:00
if ( m_wrap_lines | | m_layout_mode = = LayoutMode : : MinContent ) {
2021-06-01 21:18:08 +02:00
bool is_space = is_ascii_space ( * m_iterator ) ;
2021-04-27 13:05:50 +02:00
if ( is_space ! = m_last_was_space ) {
m_last_was_space = is_space ;
2021-08-28 00:45:28 +00:00
if ( auto result = try_commit_chunk ( start_of_chunk , m_iterator , false ) ; result . has_value ( ) ) {
2021-04-27 13:05:50 +02:00
return result . release_value ( ) ;
2021-08-28 00:45:28 +00:00
}
2021-04-27 13:05:50 +02:00
}
}
}
2021-08-28 00:45:28 +00:00
if ( start_of_chunk ! = m_utf8_view . end ( ) ) {
// Try to output whatever's left at the end of the text node.
if ( auto result = try_commit_chunk ( start_of_chunk , m_utf8_view . end ( ) , false , true ) ; result . has_value ( ) )
2021-04-27 13:05:50 +02:00
return result . release_value ( ) ;
}
return { } ;
}
2022-03-06 22:23:47 +01:00
Optional < TextNode : : Chunk > TextNode : : ChunkIterator : : try_commit_chunk ( Utf8View : : Iterator const & start , Utf8View : : Iterator const & end , bool has_breaking_newline , bool must_commit ) const
2021-04-27 13:05:50 +02:00
{
2022-03-19 15:44:02 +01:00
if ( m_layout_mode = = LayoutMode : : MaxContent & & ! must_commit )
2021-04-27 13:05:50 +02:00
return { } ;
2021-08-28 00:45:28 +00:00
auto byte_offset = m_utf8_view . byte_offset_of ( start ) ;
auto byte_length = m_utf8_view . byte_offset_of ( end ) - byte_offset ;
2021-04-27 13:05:50 +02:00
2021-08-28 00:45:28 +00:00
if ( byte_length > 0 ) {
auto chunk_view = m_utf8_view . substring_view ( byte_offset , byte_length ) ;
2021-04-27 13:05:50 +02:00
return Chunk {
. view = chunk_view ,
2021-08-28 00:45:28 +00:00
. start = byte_offset ,
. length = byte_length ,
2021-04-27 13:05:50 +02:00
. has_breaking_newline = has_breaking_newline ,
. is_all_whitespace = is_all_whitespace ( chunk_view . as_string ( ) ) ,
} ;
}
return { } ;
}
2022-03-10 22:38:08 +01:00
RefPtr < Painting : : Paintable > TextNode : : create_paintable ( ) const
2022-03-10 16:46:44 +01:00
{
return Painting : : TextPaintable : : create ( * this ) ;
}
2020-03-07 10:27:02 +01:00
}