1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 20:07:36 +00:00

LibWeb: Teach line layout to collapse whitespace across fragments

This kind of HTML now produces a single piece of whitespace:

<span> </span> <span> </span> <span> </span>

We achieve this by checking if the last fragment on the last line ends
in whitespace. If so, we either don't add a fragment at all (for the
current chunk) or we simply skip over all whitespace at the head of
the current chunk (instead of collapsing it to a single ' '.)
This commit is contained in:
Andreas Kling 2020-06-13 14:59:17 +02:00
parent 4ab1b0b436
commit 07ccaa1934
5 changed files with 46 additions and 15 deletions

View file

@ -46,7 +46,7 @@ LayoutText::~LayoutText()
{ {
} }
static bool is_all_whitespace(const String& string) static bool is_all_whitespace(const StringView& string)
{ {
for (size_t i = 0; i < string.length(); ++i) { for (size_t i = 0; i < string.length(); ++i) {
if (!isspace(string[i])) if (!isspace(string[i]))
@ -111,7 +111,8 @@ void LayoutText::for_each_chunk(Callback callback, LayoutMode layout_mode, bool
int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_chunk); int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_chunk);
if (has_breaking_newline || length > 0) { if (has_breaking_newline || length > 0) {
callback(view.substring_view(start, length), start, length, has_breaking_newline); auto chunk_view = view.substring_view(start, length);
callback(chunk_view, start, length, has_breaking_newline, is_all_whitespace(chunk_view.as_string()));
} }
start_of_chunk = it; start_of_chunk = it;
@ -160,17 +161,23 @@ void LayoutText::split_into_lines_by_rules(LayoutBlock& container, LayoutMode la
if (do_collapse) { if (do_collapse) {
auto utf8_view = Utf8View(node().data()); auto utf8_view = Utf8View(node().data());
StringBuilder builder(node().data().length()); StringBuilder builder(node().data().length());
for (auto it = utf8_view.begin(); it != utf8_view.end(); ++it) { auto it = utf8_view.begin();
auto skip_over_whitespace = [&] {
auto prev = it;
while (it != utf8_view.end() && isspace(*it)) {
prev = it;
++it;
}
it = prev;
};
if (line_boxes.last().ends_in_whitespace())
skip_over_whitespace();
for (; it != utf8_view.end(); ++it) {
if (!isspace(*it)) { if (!isspace(*it)) {
builder.append(utf8_view.as_string().characters_without_null_termination() + utf8_view.byte_offset_of(it), it.codepoint_length_in_bytes()); builder.append(utf8_view.as_string().characters_without_null_termination() + utf8_view.byte_offset_of(it), it.codepoint_length_in_bytes());
} else { } else {
builder.append(' '); builder.append(' ');
auto prev = it; skip_over_whitespace();
while (it != utf8_view.end() && isspace(*it)) {
prev = it;
++it;
}
it = prev;
} }
} }
m_text_for_rendering = builder.to_string(); m_text_for_rendering = builder.to_string();
@ -182,25 +189,30 @@ void LayoutText::split_into_lines_by_rules(LayoutBlock& container, LayoutMode la
// !do_wrap_lines => chunks_are_lines // !do_wrap_lines => chunks_are_lines
struct Chunk { struct Chunk {
Utf8View view; Utf8View view;
int start; int start { 0 };
int length; int length { 0 };
bool is_break; bool is_break { false };
bool is_all_whitespace { false };
}; };
Vector<Chunk> chunks; Vector<Chunk> chunks;
for_each_chunk( for_each_chunk(
[&](const Utf8View& view, int start, int length, bool is_break) { [&](const Utf8View& view, int start, int length, bool is_break, bool is_all_whitespace) {
chunks.append({ Utf8View(view), start, length, is_break }); chunks.append({ Utf8View(view), start, length, is_break, is_all_whitespace });
}, },
layout_mode, do_wrap_lines, do_wrap_breaks); layout_mode, do_wrap_lines, do_wrap_breaks);
for (size_t i = 0; i < chunks.size(); ++i) { for (size_t i = 0; i < chunks.size(); ++i) {
auto& chunk = chunks[i]; auto& chunk = chunks[i];
// Collapse entire fragment into non-existence if previous fragment on line ended in whitespace.
if (do_collapse && line_boxes.last().ends_in_whitespace() && chunk.is_all_whitespace)
continue;
float chunk_width; float chunk_width;
bool need_collapse = false; bool need_collapse = false;
if (do_wrap_lines) { if (do_wrap_lines) {
bool need_collapse = do_collapse && isspace(*chunk.view.begin()); need_collapse = do_collapse && isspace(*chunk.view.begin()) && line_boxes.last().ends_in_whitespace();
if (need_collapse) if (need_collapse)
chunk_width = space_width; chunk_width = space_width;

View file

@ -73,4 +73,11 @@ void LineBox::trim_trailing_whitespace()
} }
} }
bool LineBox::ends_in_whitespace() const
{
if (m_fragments.is_empty())
return false;
return m_fragments.last().ends_in_whitespace();
}
} }

View file

@ -45,6 +45,8 @@ public:
void trim_trailing_whitespace(); void trim_trailing_whitespace();
bool ends_in_whitespace() const;
private: private:
friend class LayoutBlock; friend class LayoutBlock;
NonnullOwnPtrVector<LineBoxFragment> m_fragments; NonnullOwnPtrVector<LineBoxFragment> m_fragments;

View file

@ -30,6 +30,7 @@
#include <LibWeb/Layout/LayoutText.h> #include <LibWeb/Layout/LayoutText.h>
#include <LibWeb/Layout/LineBoxFragment.h> #include <LibWeb/Layout/LineBoxFragment.h>
#include <LibWeb/RenderingContext.h> #include <LibWeb/RenderingContext.h>
#include <ctype.h>
namespace Web { namespace Web {
@ -45,6 +46,14 @@ void LineBoxFragment::render(RenderingContext& context)
} }
} }
bool LineBoxFragment::ends_in_whitespace() const
{
auto text = this->text();
if (text.is_empty())
return false;
return isspace(text[text.length() - 1]);
}
bool LineBoxFragment::is_justifiable_whitespace() const bool LineBoxFragment::is_justifiable_whitespace() const
{ {
return text() == " "; return text() == " ";

View file

@ -62,6 +62,7 @@ public:
void render(RenderingContext&); void render(RenderingContext&);
bool ends_in_whitespace() const;
bool is_justifiable_whitespace() const; bool is_justifiable_whitespace() const;
StringView text() const; StringView text() const;