Serenity Operating System
1/*
2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/CharacterTypes.h>
9#include <AK/StringBuilder.h>
10#include <LibUnicode/CharacterTypes.h>
11#include <LibWeb/DOM/Document.h>
12#include <LibWeb/Layout/BlockContainer.h>
13#include <LibWeb/Layout/InlineFormattingContext.h>
14#include <LibWeb/Layout/TextNode.h>
15#include <LibWeb/Painting/TextPaintable.h>
16
17namespace Web::Layout {
18
19TextNode::TextNode(DOM::Document& document, DOM::Text& text)
20 : Node(document, &text)
21{
22}
23
24TextNode::~TextNode() = default;
25
26static bool is_all_whitespace(StringView string)
27{
28 for (size_t i = 0; i < string.length(); ++i) {
29 if (!is_ascii_space(string[i]))
30 return false;
31 }
32 return true;
33}
34
35static ErrorOr<DeprecatedString> apply_text_transform(DeprecatedString const& string, CSS::TextTransform text_transform)
36{
37 if (text_transform == CSS::TextTransform::Uppercase)
38 return Unicode::to_unicode_uppercase_full(string);
39 if (text_transform == CSS::TextTransform::Lowercase)
40 return Unicode::to_unicode_lowercase_full(string);
41 return string;
42}
43
44// NOTE: This collapses whitespace into a single ASCII space if collapse is true.
45void TextNode::compute_text_for_rendering(bool collapse)
46{
47 auto data = apply_text_transform(dom_node().data(), computed_values().text_transform()).release_value_but_fixme_should_propagate_errors();
48
49 if (dom_node().is_password_input()) {
50 m_text_for_rendering = DeprecatedString::repeated('*', data.length());
51 return;
52 }
53
54 if (!collapse || data.is_empty()) {
55 m_text_for_rendering = data;
56 return;
57 }
58
59 // NOTE: A couple fast returns to avoid unnecessarily allocating a StringBuilder.
60 if (data.length() == 1) {
61 if (is_ascii_space(data[0])) {
62 static DeprecatedString s_single_space_string = " ";
63 m_text_for_rendering = s_single_space_string;
64 } else {
65 m_text_for_rendering = data;
66 }
67 return;
68 }
69
70 bool contains_space = false;
71 for (auto& c : data) {
72 if (is_ascii_space(c)) {
73 contains_space = true;
74 break;
75 }
76 }
77 if (!contains_space) {
78 m_text_for_rendering = data;
79 return;
80 }
81
82 StringBuilder builder(data.length());
83 size_t index = 0;
84
85 auto skip_over_whitespace = [&index, &data] {
86 while (index < data.length() && is_ascii_space(data[index]))
87 ++index;
88 };
89
90 while (index < data.length()) {
91 if (is_ascii_space(data[index])) {
92 builder.append(' ');
93 ++index;
94 skip_over_whitespace();
95 } else {
96 builder.append(data[index]);
97 ++index;
98 }
99 }
100
101 m_text_for_rendering = builder.to_deprecated_string();
102}
103
104TextNode::ChunkIterator::ChunkIterator(StringView text, bool wrap_lines, bool respect_linebreaks, bool is_generated_empty_string)
105 : m_wrap_lines(wrap_lines)
106 , m_respect_linebreaks(respect_linebreaks)
107 , m_should_emit_one_empty_chunk(is_generated_empty_string)
108 , m_utf8_view(text)
109 , m_iterator(m_utf8_view.begin())
110{
111}
112
113Optional<TextNode::Chunk> TextNode::ChunkIterator::next()
114{
115 if (m_should_emit_one_empty_chunk) {
116 m_should_emit_one_empty_chunk = false;
117 return Chunk {
118 .view = {},
119 .start = 0,
120 .length = 0,
121 .has_breaking_newline = false,
122 .is_all_whitespace = false,
123 };
124 }
125
126 if (m_iterator == m_utf8_view.end())
127 return {};
128
129 auto start_of_chunk = m_iterator;
130
131 while (m_iterator != m_utf8_view.end()) {
132 if (m_respect_linebreaks && *m_iterator == '\n') {
133 // Newline encountered, and we're supposed to preserve them.
134 // If we have accumulated some code points in the current chunk, commit them now and continue with the newline next time.
135 if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false); result.has_value())
136 return result.release_value();
137
138 // Otherwise, commit the newline!
139 ++m_iterator;
140 auto result = try_commit_chunk(start_of_chunk, m_iterator, true);
141 VERIFY(result.has_value());
142 return result.release_value();
143 }
144
145 if (m_wrap_lines) {
146 if (is_ascii_space(*m_iterator)) {
147 // Whitespace encountered, and we're allowed to break on whitespace.
148 // If we have accumulated some code points in the current chunk, commit them now and continue with the whitespace next time.
149 if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false); result.has_value())
150 return result.release_value();
151
152 // Otherwise, commit the whitespace!
153 ++m_iterator;
154 if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false); result.has_value())
155 return result.release_value();
156 continue;
157 }
158 }
159
160 ++m_iterator;
161 }
162
163 if (start_of_chunk != m_utf8_view.end()) {
164 // Try to output whatever's left at the end of the text node.
165 if (auto result = try_commit_chunk(start_of_chunk, m_utf8_view.end(), false); result.has_value())
166 return result.release_value();
167 }
168
169 return {};
170}
171
172Optional<TextNode::Chunk> TextNode::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline) const
173{
174 auto byte_offset = m_utf8_view.byte_offset_of(start);
175 auto byte_length = m_utf8_view.byte_offset_of(end) - byte_offset;
176
177 if (byte_length > 0) {
178 auto chunk_view = m_utf8_view.substring_view(byte_offset, byte_length);
179 return Chunk {
180 .view = chunk_view,
181 .start = byte_offset,
182 .length = byte_length,
183 .has_breaking_newline = has_breaking_newline,
184 .is_all_whitespace = is_all_whitespace(chunk_view.as_string()),
185 };
186 }
187
188 return {};
189}
190
191JS::GCPtr<Painting::Paintable> TextNode::create_paintable() const
192{
193 return Painting::TextPaintable::create(*this);
194}
195
196}